Module: VivlioStarter::CLI::Import::MarkdownConverter

Defined in:
lib/vivlio_starter/cli/import/markdown_converter.rb

Overview

Markdown 追従変換モジュール

Constant Summary collapse

FENCE_BLOCK_DEFINITIONS =
{
  'abstract' => { klass: 'chapter-lead' },
  'tip' => { klass: 'tip' },
  'note' => { klass: 'note' },
  'notice' => { klass: 'notice' },
  # [centering] / [flushright] はブロックごと寄せる記法なので align-*(ブロック用)へ。
  # text-* は段落内の文字揃えだけを変える別の記法で、ここでは使わない。
  'centering' => { klass: 'align-center' },
  'flushright' => { klass: 'align-right' },
  'column' => { klass: 'column', separator: "\n" }
}.freeze
FENCE_LINE =

フェンスの開き(``` 以上のバッククォート + 情報文字列)

/\A(`{3,})(.*)\z/

Class Method Summary collapse

Class Method Details

.append_code_block(result, lines, open_index, match) ⇒ Object

1 ブロックぶんを result へ書き出し、次に読む行の番号を返す



273
274
275
276
277
278
279
280
281
282
283
# File 'lib/vivlio_starter/cli/import/markdown_converter.rb', line 273

def append_code_block(result, lines, open_index, match)
  fence = match[1]
  close_index = closing_fence_index(lines, open_index + 1, fence)
  body = lines[(open_index + 1)...close_index]

  result << (match[2].strip.empty? ? "#{fence}#{detect_lang(body.join)}\n" : lines[open_index])
  result.concat(body)
  result << lines[close_index] if close_index < lines.size

  close_index + 1
end

.closing_fence_index(lines, from, fence) ⇒ Object

閉じフェンス(開きと同じ長さ以上のバッククォートだけの行)を探す



286
287
288
289
# File 'lib/vivlio_starter/cli/import/markdown_converter.rb', line 286

def closing_fence_index(lines, from, fence)
  pattern = /\A#{fence}`*\s*\z/
  (from...lines.size).find { pattern.match?(lines[it].chomp) } || lines.size
end

.convert_block(text, tag, config) ⇒ Object

与えられたタグ設定に従って単一種類のフェンスブロックを Markdown 化する



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/vivlio_starter/cli/import/markdown_converter.rb', line 128

def convert_block(text, tag, config)
  klass = config.fetch(:klass)
  separator = config.fetch(:separator, "\n\n")

  pattern = %r{
    ^[ \t]*\[#{tag}\](?:[ \t]+(?<title>[^\r\n]+?))?[ \t]*\r?\n
    (?<body>.*?)
    \r?\n[ \t]*\[/#{tag}\][ \t]*$
  }mix

  text.gsub(pattern) do
    match = Regexp.last_match
    title = emphasize_title(extract_inline_title(match[:title]))
    body  = match[:body].strip
    content = [title, body].reject(&:empty?).join(separator)
    ":::{.#{klass}}\n#{content}\n:::\n"
  end
end

.convert_code_captions(text) ⇒ Object

コードブロックキャプションの変換



169
170
171
172
173
174
175
176
# File 'lib/vivlio_starter/cli/import/markdown_converter.rb', line 169

def convert_code_captions(text)
  text.gsub(%r{<span class="caption">▼([^<]+)</span>\s*\n```}i) do
    caption = Regexp.last_match(1).strip
    ext = File.extname(caption).delete('.').downcase
    ext = 'text' if ext.empty?
    "```#{ext}:#{caption}"
  end
end

.convert_definition_lists(text) ⇒ Object

dl/dt/dd タグを Markdown 箇条書きに変換



189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/vivlio_starter/cli/import/markdown_converter.rb', line 189

def convert_definition_lists(text)
  text.gsub(%r{<dl>\s*(.*?)\s*</dl>}m) do
    dl_content = Regexp.last_match(1)
    items = []
    dl_content.scan(%r{<dt>([^<]*)</dt>\s*<dd>\s*(.*?)\s*</dd>}m) do |dt, dd|
      term = dt.strip
      desc = dd.strip.gsub(/\n\s*/, "\n    ")
      if desc.include?("\n")
        lines = desc.split("\n")
        desc = lines.map.with_index { |l, i| i == lines.size - 1 ? l : "#{l}  " }.join("\n")
      end
      items << "- **#{term}**\n    #{desc}"
    end
    "#{items.join("\n\n")}\n"
  end
end

.convert_fence_blocks(text) ⇒ Object

フェンス記法([abstract], [tip], [note], [column] 等)を変換



114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/vivlio_starter/cli/import/markdown_converter.rb', line 114

def convert_fence_blocks(text)
  result = text.dup
  FENCE_BLOCK_DEFINITIONS.each do |tag, config|
    loop do
      updated = convert_block(result, tag, config)
      break if updated == result

      result = updated
    end
  end
  result
end

.convert_html_tables(text) ⇒ Object

HTML テーブルを Markdown テーブルに変換



207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/vivlio_starter/cli/import/markdown_converter.rb', line 207

def convert_html_tables(text)
  text.gsub(%r{<div class="table[^"]*">\s*(?:<p class="caption">([^<]*)</p>)?\s*<table>(.*?)</table>\s*</div>}m) do
    caption = Regexp.last_match(1)
    table_html = Regexp.last_match(2)

    rows = []
    table_html.scan(%r{<tr[^>]*>(.*?)</tr>}m) do |row_content|
      row = row_content[0]
      cells = row.scan(%r{<t[hd][^>]*>(.*?)</t[hd]>}m).map { |c| c[0].strip }
      rows << cells
    end

    next '' if rows.empty?

    md_table = []
    md_table << "**#{caption}**\n" if caption && !caption.strip.empty?
    md_table << "| #{rows[0].join(' | ')} |"
    md_table << "| #{rows[0].map { '---' }.join(' | ')} |"
    rows[1..].each { |row| md_table << "| #{row.join(' | ')} |" }

    "#{md_table.join("\n")}\n"
  end
end

.convert_img_tags(text) ⇒ Object

タグを Markdown 画像記法に変換 ファイル名は取り込み実体(image_processor)と同一基準でサニタイズし、参照と一致させる。



94
95
96
97
98
99
# File 'lib/vivlio_starter/cli/import/markdown_converter.rb', line 94

def convert_img_tags(text)
  text.gsub(%r{<img src=".*/([^/]+)\.(?:png|jpg|jpeg|gif)">}i) do
    file_name_no_ext = ImageFilenameSanitizer.sanitize(Regexp.last_match(1))
    "![](#{file_name_no_ext}.webp)"
  end
end

.convert_quote_blocks(text) ⇒ Object

[quote] ブロックの変換



161
162
163
164
165
166
# File 'lib/vivlio_starter/cli/import/markdown_converter.rb', line 161

def convert_quote_blocks(text)
  text.gsub(%r{^\[quote\][^\n]*\n(.*?)^\[/quote\]\s*$}m) do
    inner = Regexp.last_match(1).gsub(/\A\n+|\n+\z/, '')
    inner.lines.map { |l| "> #{l.rstrip}".strip }.join("\n") + "\n\n"
  end
end

.convert_ruby_notation(text) ⇒ Object

ルビ記法の変換: 漢字(よみ)→ 漢字|よみ



232
233
234
235
236
237
238
# File 'lib/vivlio_starter/cli/import/markdown_converter.rb', line 232

def convert_ruby_notation(text)
  text.gsub(/([一-龯々]+)(([ぁ-んァ-ヶー]+))/) do
    kanji = Regexp.last_match(1)
    reading = Regexp.last_match(2)
    "{#{kanji}|#{reading}}"
  end
end

.detect_code_block_languages(text) ⇒ Object

コードブロック言語の自動推定(Rouge を使用)

言語指定のないコードブロックに対して、内容から言語を推定して付与する。

行単位で開き/閉じを追うのは、/^```\s*\n(.*?)^```/m で拾うと 閉じフェンスを開きと誤認するため。言語付きで開いたブロック (c:foo.c)の閉じは裸の なので、そこから次の開きまでを 「言語未指定のブロック」と読み、閉じフェンスが ```text に化けていた。 以降の地の文とコードが総入れ替わりになる(実測: book_c の取り込みで 68 箇所・1 章では 105 個の開きに対し裸の閉じが 1 個だけになっていた)。



253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/vivlio_starter/cli/import/markdown_converter.rb', line 253

def detect_code_block_languages(text)
  lines = text.lines
  result = []
  index = 0

  while index < lines.size
    match = FENCE_LINE.match(lines[index].chomp)
    unless match
      result << lines[index]
      index += 1
      next
    end

    index = append_code_block(result, lines, index, match)
  end

  result.join
end

.detect_lang(code) ⇒ String

コード内容から言語を推定する

Parameters:

  • code (String)

    コードブロックの内容

Returns:

  • (String)

    推定された言語タグ



295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'lib/vivlio_starter/cli/import/markdown_converter.rb', line 295

def detect_lang(code)
  # シェルコマンドの判定($ や % で始まる行があれば zsh)
  return 'zsh' if code.match?(/^[ \t]*[$%][ \t]+/)

  begin
    require 'rouge'
    lexer = Rouge::Lexer.guess(source: code)
    tag = lexer.tag

    # Markdown で一般的に使われる短いタグ名に変換
    mapping = {
      'javascript' => 'js',
      'typescript' => 'ts',
      'markdown' => 'md',
      'plaintext' => 'text',
      'bash' => 'zsh',
      'shell' => 'zsh'
    }

    mapping.fetch(tag, tag)
  rescue LoadError
    # Rouge がない場合は text をデフォルトにする
    Common.log_warn('  Rouge gem が見つかりません。コードブロック言語推定をスキップします。')
    'text'
  rescue StandardError
    # Lexer.guess が失敗した場合も text
    'text'
  end
end

.emphasize_title(title) ⇒ Object

既に強調済みのタイトルはそのまま残し、未強調なら ... を付与する



153
154
155
156
157
158
# File 'lib/vivlio_starter/cli/import/markdown_converter.rb', line 153

def emphasize_title(title)
  normalized = title.to_s.strip
  return '' if normalized.empty?

  normalized.match?(/\A\*\*.*\*\*\z/) ? normalized : "**#{normalized}**"
end

.extract_inline_title(raw) ⇒ Object

インラインに付与されたタグを除去してタイトル文字列だけを返す



148
149
150
# File 'lib/vivlio_starter/cli/import/markdown_converter.rb', line 148

def extract_inline_title(raw)
  raw.to_s.strip.gsub(%r{</?[^>]+>}, '').strip
end

.normalize_image_paths(text) ⇒ Object

Markdown 画像パスの正規化 ファイル名は取り込み実体(image_processor)と同一基準でサニタイズし、参照と一致させる。



180
181
182
183
184
185
186
# File 'lib/vivlio_starter/cli/import/markdown_converter.rb', line 180

def normalize_image_paths(text)
  text.gsub(%r{!\[((?:[^\[\]]|\[[^\]]*\])*)\]\(\./images/[^)]+/([^/]+)\.(?:png|jpg|jpeg|gif)\)}i) do
    alt = Regexp.last_match(1)
    filename = ImageFilenameSanitizer.sanitize(Regexp.last_match(2))
    "![#{alt}](#{filename}.webp)"
  end
end

.process!(temp_dir) ⇒ void

This method returns an undefined value.

Markdown ファイルを vivlio-starter 用に変換する

Parameters:

  • temp_dir (String)

    変換対象の Markdown ファイルが格納されたディレクトリ



41
42
43
44
45
46
47
48
49
# File 'lib/vivlio_starter/cli/import/markdown_converter.rb', line 41

def process!(temp_dir)
  Common.log_info('  追従変換を実行中...')

  Dir.glob(File.join(temp_dir, '*.md')).each do |md_path|
    markdown = File.read(md_path)
    fixed = transform(markdown)
    File.write(md_path, fixed) if fixed != markdown
  end
end

.transform(markdown) ⇒ String

Markdown テキストを変換する(テスト用に公開)

Parameters:

  • markdown (String)

    変換対象の Markdown テキスト

Returns:

  • (String)

    変換後の Markdown テキスト



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/vivlio_starter/cli/import/markdown_converter.rb', line 55

def transform(markdown)
  fixed = markdown.dup

  # --- phase: HTML img タグの変換 ---
  fixed = convert_img_tags(fixed)

  # --- phase: フェンス記法の変換 ---
  fixed = convert_fence_blocks(fixed)

  # --- phase: quote ブロックの変換 ---
  fixed = convert_quote_blocks(fixed)

  # --- phase: br タグ → .aki ---
  fixed.gsub!(/^\s*<br>\s*$/, '{.aki}')

  # --- phase: コードブロックキャプションの変換 ---
  fixed = convert_code_captions(fixed)

  # --- phase: Markdown 画像パスの正規化 ---
  fixed = normalize_image_paths(fixed)

  # --- phase: dl/dt/dd タグの変換 ---
  fixed = convert_definition_lists(fixed)

  # --- phase: HTML テーブルの変換 ---
  fixed = convert_html_tables(fixed)

  # --- phase: ルビ記法の変換 ---
  fixed = convert_ruby_notation(fixed)

  # --- phase: コードブロック言語の自動推定 ---
  fixed = detect_code_block_languages(fixed)

  # --- phase: HTML 文字実体参照のデコード ---
  CGI.unescapeHTML(fixed)
end