Module: VivlioStarter::CLI::PreProcessCommands::MermaidTransformer

Defined in:
lib/vivlio_starter/cli/pre_process/mermaid_transformer.rb

Overview

</code>

Constant Summary collapse

REL_BASE =

生成物の出力先(images/ 配下)と永続キャッシュの種別名。

'mermaid'
OPENER_HINT =
(大半の章はこれで即 return し、行走査すら行わない)。
/^[ \t]*(?:`{3,}|~{3,})[ \t]*mermaid[ \t]*$/i

Class Method Summary collapse

Class Method Details

.alt_text(source) ⇒ Object

alt テキスト(図ソースの最初の意味ある行=種別/宣言行)。



181
182
183
184
# File 'lib/vivlio_starter/cli/pre_process/mermaid_transformer.rb', line 181

def alt_text(source)
  line = source.each_line.map(&:strip).find { |l| !l.empty? && !l.start_with?('%%') }
  (line || 'mermaid diagram').gsub(/\s+/, ' ')[0, 120]
end

.available?(renderer: default_renderer) ⇒ Boolean

mmdc の描画が利用可能か(既定レンダラ経由)。

Returns:

  • (Boolean)


55
56
57
# File 'lib/vivlio_starter/cli/pre_process/mermaid_transformer.rb', line 55

def available?(renderer: default_renderer)
  renderer.available?
end

.block_source(block) ⇒ Object

フェンスブロックから図ソース(開始行・終了行を除く中身)を取り出す。



138
# File 'lib/vivlio_starter/cli/pre_process/mermaid_transformer.rb', line 138

def block_source(block) = block.lines[1..-2].to_a.join

.cache_key(source, font_family, renderer) ⇒ Object

図ソース+フォント+テーマ+mermaid バージョンのハッシュをキーにする(§4.1-2)。 図ソースを書き換えれば(=内容が変われば)別キーになり再生成される。



144
145
146
147
148
149
150
151
# File 'lib/vivlio_starter/cli/pre_process/mermaid_transformer.rb', line 144

def cache_key(source, font_family, renderer)
  version = renderer.respond_to?(:version) ? renderer.version.to_s : ''
  # v2: mmdc 設定を htmlLabels:false へ変更(foreignObject→native text)した際に
  # 旧キャッシュを無効化するためスキーマ版を上げた。
  payload = ['v2', Digest::SHA256.hexdigest(source), font_family.to_s,
             MermaidRenderer::DEFAULT_THEME, version].join('|')
  Digest::SHA256.hexdigest(payload)[0, 16]
end

.configured_font_familyObject

図中テキストの font-family(§5.1・案 1)。本書の見出しフォントを先頭に、 リーダー標準の和文 sans フォールバックを併記する(epub_heading_font_family と同型)。 設定未ロード(プロジェクト外・単体テスト)では nil(mmdc 既定フォント)。



156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/vivlio_starter/cli/pre_process/mermaid_transformer.rb', line 156

def configured_font_family
  return nil unless Common.configured?

  book = Common::CONFIG.typography.heading.font.to_s.strip
  stack = []
  stack << "'#{book}'" unless book.empty?
  stack.concat(["'Hiragino Sans'", "'Hiragino Kaku Gothic ProN'", "'Noto Sans JP'",
                "'Noto Sans CJK JP'", 'sans-serif'])
  stack.join(', ')
rescue StandardError
  nil
end

.default_rendererObject

既定のレンダラ(mmdc)。プロセス内で 1 つを共有する。



207
# File 'lib/vivlio_starter/cli/pre_process/mermaid_transformer.rb', line 207

def default_renderer = (@default_renderer ||= MermaidRenderer.new)

.embed_label_font(svg) ⇒ Object

図中に出る字だけを絞った @font-face を SVG 自身へ持たせる。

この SVG は <img> から参照される独立文書なので、configured_font_family が 名指しした書体は解決されず OS の既定和文フォントへ落ち、Chromium がそれを Type 3 で埋め込む(入稿で不可)。SVG 側は既に書体名を書いているため、 同名の @font-face を注ぐだけで解決する(font-family の書き換えは不要)。 詳細は type3-font-embedding-notes.md



120
121
122
123
124
# File 'lib/vivlio_starter/cli/pre_process/mermaid_transformer.rb', line 120

def embed_label_font(svg)
  style = SvgFontEmbedder.font_face_style(SvgFontEmbedder.characters_in(svg),
                                          family: SvgFontEmbedder.configured_heading_font)
  SvgFontEmbedder.inject(svg, style)
end

.escape_attr(str) ⇒ Object



186
187
188
# File 'lib/vivlio_starter/cli/pre_process/mermaid_transformer.rb', line 186

def escape_attr(str)
  str.to_s.gsub('&', '&amp;').gsub('<', '&lt;').gsub('>', '&gt;').gsub('"', '&quot;')
end

.figure(svg_rel, raster_rel, alt) ⇒ Object

置換後の HTML。前後に空行を補い独立段落として組ませる。ラスター参照は data-vs-raster に明示し、EpubBuilder が EPUB/Kindle で src を PNG へ差し替える。



173
174
175
176
177
178
# File 'lib/vivlio_starter/cli/pre_process/mermaid_transformer.rb', line 173

def figure(svg_rel, raster_rel, alt)
  "\n\n<figure class=\"vs-mermaid\">\n" \
    "<img class=\"vs-mermaid\" src=\"#{svg_rel}\" data-vs-raster=\"#{raster_rel}\" " \
    "alt=\"#{escape_attr(alt)}\">\n" \
    "</figure>\n\n"
end

.generate_pair!(source, font_family, renderer, cache_dir, key) ⇒ Object

SVG(PDF 用)とラスター PNG(EPUB/Kindle 用)を対でキャッシュへ描き出す。



102
103
104
105
106
107
108
109
110
111
# File 'lib/vivlio_starter/cli/pre_process/mermaid_transformer.rb', line 102

def generate_pair!(source, font_family, renderer, cache_dir, key)
  svg = renderer.render(source, format: :svg, font_family:)
  png = renderer.render(source, format: :png, font_family:)
  return false unless svg && png

  svg = embed_label_font(svg)
  File.write(File.join(cache_dir, "#{key}.svg"), svg, encoding: 'utf-8')
  File.binwrite(File.join(cache_dir, "#{key}.png"), png)
  true
end

.mermaid_block?(block) ⇒ Boolean

開始フェンス行の情報文字列が mermaid か(```mermaid / ~~~ mermaid、大小無視)。

Returns:

  • (Boolean)


129
130
131
132
133
134
135
# File 'lib/vivlio_starter/cli/pre_process/mermaid_transformer.rb', line 129

def mermaid_block?(block)
  first = block.lines.first.to_s.lstrip
  marker = first[/\A(?:`{3,}|~{3,})/]
  return false unless marker

  first.delete_prefix(marker).strip.casecmp?('mermaid')
end

.render_block(source, lineno, chapter_slug:, source_filename:, renderer:) ⇒ Object

ブロック 1 つを figure へ変換する。生成できないときは nil(原文フェンス温存)。



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/vivlio_starter/cli/pre_process/mermaid_transformer.rb', line 84

def render_block(source, lineno, chapter_slug:, source_filename:, renderer:)
  font_family = configured_font_family
  key = cache_key(source, font_family, renderer)
  out_dir = File.join(Common::BUILD_HTML_DIR, 'images', REL_BASE, chapter_slug)

  ok = GeneratedAssetCache.fetch(REL_BASE, ["#{key}.svg", "#{key}.png"], out_dir:) do |cache_dir|
    generate_pair!(source, font_family, renderer, cache_dir, key)
  end
  unless ok
    warn_render_failed(source_filename, lineno)
    return nil
  end

  rel_dir = "images/#{REL_BASE}/#{chapter_slug}"
  figure("#{rel_dir}/#{key}.svg", "#{rel_dir}/#{key}.png", alt_text(source))
end

.transform(content, chapter_slug:, source_filename:, renderer: default_renderer) ⇒ String

本文中のトップレベル ```mermaid ブロックをすべて

へ置換する。

Parameters:

  • content (String)

    処理対象の Markdown 本文

  • chapter_slug (String)

    生成物の出力先章ディレクトリ名(例: "10-intro")

  • source_filename (String)

    警告に出す原稿ファイル名

  • renderer (#available?, #render, #version) (defaults to: default_renderer)

    mmdc レンダラ(テスト差し替え用)

Returns:

  • (String)

    置換後の本文



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/vivlio_starter/cli/pre_process/mermaid_transformer.rb', line 66

def transform(content, chapter_slug:, source_filename:, renderer: default_renderer)
  return content unless content.match?(OPENER_HINT)

  warned = false
  Masking.replace_top_level_fences(content) do |block, lineno|
    next nil unless mermaid_block?(block)

    unless renderer.available?
      warn_renderer_missing(source_filename) unless warned
      warned = true
      next nil
    end

    render_block(block_source(block), lineno, chapter_slug:, source_filename:, renderer:)
  end
end

.warn_render_failed(source_filename, lineno) ⇒ Object



199
200
201
202
203
204
# File 'lib/vivlio_starter/cli/pre_process/mermaid_transformer.rb', line 199

def warn_render_failed(source_filename, lineno)
  Common.log_warn(
    "[mermaid] #{source_filename}:#{lineno} - mermaid の図を生成できませんでした(当該ブロックはコードのまま残します)",
    detail: '→ 図ソースの構文を確認してください(例: `graph LR` の行から始める)'
  )
end

.warn_renderer_missing(source_filename) ⇒ Object

--- 警告(出現位置つき・修正案つき) ---



192
193
194
195
196
197
# File 'lib/vivlio_starter/cli/pre_process/mermaid_transformer.rb', line 192

def warn_renderer_missing(source_filename)
  Common.log_warn(
    "[mermaid] #{source_filename}: mmdc が無いため ```mermaid を図にできません(コードブロックのまま出力します)",
    detail: '→ `vs doctor --fix` で導入できます(npm install -g @mermaid-js/mermaid-cli)'
  )
end