Module: Vivlio::Starter::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' => { klass: 'centering' },
  'flushright' => { klass: 'text-right' },
  'column' => { klass: 'column', separator: "\n" }
}.freeze

Class Method Summary collapse

Class Method Details

.convert_block(text, tag, config) ⇒ Object

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



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/vivlio/starter/cli/import/markdown_converter.rb', line 124

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

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



165
166
167
168
169
170
171
172
# File 'lib/vivlio/starter/cli/import/markdown_converter.rb', line 165

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 箇条書きに変換



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/vivlio/starter/cli/import/markdown_converter.rb', line 184

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

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



110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/vivlio/starter/cli/import/markdown_converter.rb', line 110

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 テーブルに変換



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/vivlio/starter/cli/import/markdown_converter.rb', line 202

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

<img> タグを Markdown 画像記法に変換



92
93
94
95
96
97
# File 'lib/vivlio/starter/cli/import/markdown_converter.rb', line 92

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

.convert_quote_blocks(text) ⇒ Object

quote

ブロックの変換



157
158
159
160
161
162
# File 'lib/vivlio/starter/cli/import/markdown_converter.rb', line 157

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

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



227
228
229
230
231
232
233
# File 'lib/vivlio/starter/cli/import/markdown_converter.rb', line 227

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 を使用)

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



238
239
240
241
242
243
244
# File 'lib/vivlio/starter/cli/import/markdown_converter.rb', line 238

def detect_code_block_languages(text)
  text.gsub(/^```\s*\n(.*?)^```/m) do
    code = Regexp.last_match(1)
    lang = detect_lang(code)
    "```#{lang}\n#{code}```"
  end
end

.detect_lang(code) ⇒ String

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

Parameters:

  • code (String)

    コードブロックの内容

Returns:

  • (String)

    推定された言語タグ



250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# File 'lib/vivlio/starter/cli/import/markdown_converter.rb', line 250

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

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



149
150
151
152
153
154
# File 'lib/vivlio/starter/cli/import/markdown_converter.rb', line 149

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

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



144
145
146
# File 'lib/vivlio/starter/cli/import/markdown_converter.rb', line 144

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

.normalize_image_paths(text) ⇒ Object

Markdown 画像パスの正規化



175
176
177
178
179
180
181
# File 'lib/vivlio/starter/cli/import/markdown_converter.rb', line 175

def normalize_image_paths(text)
  text.gsub(%r{!\[((?:[^\[\]]|\[[^\]]*\])*)\]\(\./images/[^)]+/([^/]+)\.(?:png|jpg|jpeg|gif)\)}i) do
    alt = Regexp.last_match(1)
    filename = 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 ファイルが格納されたディレクトリ



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

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 テキスト



54
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
# File 'lib/vivlio/starter/cli/import/markdown_converter.rb', line 54

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