Module: VivlioStarter::CLI::PostProcessCommands::HeadingProcessor
- Defined in:
- lib/vivlio_starter/cli/post_process/heading_processor.rb
Overview
================================================================
Module: HeadingProcessor
【役割】
- 見出し(h1..h6)にマーカーとメタデータを付与
- 章番号・節番号のスパンを構築
【処理内容】
-
見出しマーカー付与
- class: vs-h-marker
- data-heading: 見出しテキスト
- data-hN: レベル別見出しテキスト
- data-chapter: 章トークン
-
見出し番号スパン構築
- h1: 第N章タイトル
- h2: N-Mタイトル
- h3: ♣タイトル
================================================================
Constant Summary collapse
- MAIN_CHAPTER_RANGE =
(1..89)
- ORPHAN_GUARD_KINDS =
泣き別れ防止の対象。折り返す前提で大きく組まれる章題・節題だけに適用する (h3 は block-size 固定の小見出しで、折返し位置を動かすと段が崩れる)。
%i[chapter section].freeze
- WORD_JOINER =
見出しの末尾 1 文字だけが次の行へ落ちる「泣き別れ」を防ぐ。
日本語の見出しは 1 語で分割点を持たないことが多く(「コマンドラインオプション」)、 word-break: auto-phrase でも文節境界が無いため文字間で折れて末尾 1 文字が独り残る (pdf_h2_nakiwakare.png 実測)。末尾 2 文字を white-space: nowrap の span で括ると その 2 文字が分割できなくなり、折返しが 1 文字ぶん手前へ移って一緒に落ちる。
WORD JOINER(U+2060)を挟む方法は Vivliostyle が無視した(実測: HTML には 入るが組版結果は変わらない)。Vivliostyle は自前の行分割を持つため、制御文字より CSS で表現する方が確実。
見出し末尾は素のテキストとは限らない。索引語(
<span class="index-term">)や 用語集リンクが末尾に来るため、最も深い末尾のテキストノードまで降りて括る (「コマンドラインオプション」のように、 末尾が要素で終わる見出しは実際に多い)。 ""- NOBR_CLASS =
'vs-nobr'
Class Method Summary collapse
-
.add_chapter_token(heading, chapter_token) ⇒ Object
章トークンを追加.
-
.add_heading_data_attributes(heading, level) ⇒ Object
見出しテキストの data 属性を追加.
-
.add_marker_class(heading) ⇒ Object
vs-h-marker クラスを追加.
-
.add_number_span(node, number_class, number_text, doc) ⇒ Object
番号スパンを追加.
-
.add_title_span(node, title_class, title_text, original_nodes, doc, kind = nil) ⇒ Object
タイトルスパンを追加.
-
.apply_marker_to_heading(heading, level, chapter_token) ⇒ Object
見出し要素にマーカーを適用.
-
.build_h1_number_text(context) ⇒ Object
h1 の番号テキストを構築.
-
.build_h2_number_text(context, section_index) ⇒ Object
h2 の番号テキストを構築.
-
.build_heading_context(html_path, entry) ⇒ Object
見出し処理のコンテキストを構築.
- .chapter_tokens_override ⇒ Object
-
.chapter_tokens_override=(tokens) ⇒ Object
一時的な章トークンの並びを外部から指定するためのオーバーライド 例: vs build 54-56 のような単章/範囲ビルド時に、 そのビルド対象だけを 1,2,3...
-
.deepest_last_text_node(node) ⇒ Object
文書順で最後のテキストノードまで降りる(末尾が要素なら中へ入る).
-
.discovered_main_chapter_tokens ⇒ Array<String>
発見されたHTMLファイルから章トークンを取得 中間 HTML はワークスペースの html/ に置かれる(P4 §3.4-1).
-
.ensure_heading_id(heading, chapter_token) ⇒ Object
見出しに非空の id を保証する。
### $E = mc^2$のように見出しが数式(→ SVG 画像)のみだと VFM が生成する slug が空になり、id="" が EPUB で epubcheck RSC-005(id 値が不正)を招くため、 内容のハッシュから一意な id を補う(テキスト見出しの既存 id はそのまま残す)。. -
.extract_chapter_token(path) ⇒ Object
ファイルパスから章トークンを抽出.
-
.extract_heading_core_text(node) ⇒ String
見出しのコアテキストを抽出.
-
.extract_original_title_nodes(node, number_class, title_class) ⇒ Object
元のタイトルノードを抽出.
- .guard_last_character_orphan!(span, doc) ⇒ Object
-
.heading_plain_text(node) ⇒ Object
見出しの素のテキスト。data 属性(EPUB の合成見出し画像・目次の素材)に入れるため、 表示上の装飾を落とす: - 用語集の † リンク … 見出しを画像化するとクリックできず記号だけが残る - WORD JOINER … 泣き別れ防止で挟んだ制御文字(見た目に影響しないが 比較・再構築の冪等性を壊すので比較前に落とす).
-
.heading_span_classes(kind) ⇒ Object
見出し種別に応じたクラス名を取得.
-
.inject_heading_markers!(html_paths, max_level: 3) ⇒ Object
見出し(h1..hN)に本文参照用のマーカー(class と data 属性)を付与.
-
.inject_heading_number_spans!(html_path, entry) ⇒ Object
見出し番号スパンを構築.
-
.main_chapter_order ⇒ Array<String>
メイン章の順序を取得.
-
.main_chapter_token?(token) ⇒ Boolean
メイン章トークンかどうか判定.
-
.needs_heading_update?(node, number_text, title_text, number_class, title_class) ⇒ Boolean
見出しの更新が必要か判定.
-
.normalize_and_filter_tokens(list) ⇒ Array<String>
トークンリストを正規化してフィルタ.
-
.normalize_chapter_token(entry) ⇒ String?
章トークンを正規化.
-
.parse_html_document(html) ⇒ Object
HTMLドキュメントをパース(HtmlParser に委譲).
-
.process_h1_spans(doc, context) ⇒ Object
h1 のスパン処理.
-
.process_h2_spans(doc, context) ⇒ Object
h2 のスパン処理.
-
.process_h3_spans(doc) ⇒ Object
h3 のスパン処理.
-
.process_heading_markers_for_file(path, max_level) ⇒ Object
単一ファイルの見出しマーカー処理.
-
.rebuild_heading_with_spans(node, number_text, title_text, kind, doc) ⇒ Object
見出しを番号スパンとタイトルスパンで再構築.
-
.replace_with_nobr_words!(node, words, doc) ⇒ Object
語ごとに nowrap の span を置く。語末の空白は span の外へ出す—— 中に入れると nowrap がその空白での折返しまで禁止し、「(create / delete / rename」のような半角混じりの見出しが 1 つの塊になって版面をはみ出す (pdf_h1_chapter6.png 実測)。外に出せば空白が折返し候補として残る。.
-
.resolve_appendix_letter(chapter_token, chapter_number_i) ⇒ Object
付録のレター(A/B/C...)をビルド対象の付録の順番に基づいて解決する.
-
.resolve_main_chapter_display_number(chapter_token, chapter_number_i = nil) ⇒ Integer?
メイン章の表示番号を解決.
-
.save_html_document(path, doc) ⇒ Object
HTMLドキュメントを保存(HtmlParser に委譲).
-
.set_attr_if_changed(node, attr, value) ⇒ Object
属性値が変わった場合のみ設定.
-
.set_h1_id(heading, text) ⇒ Object
h1 に id を設定.
-
.update_h1_data_attributes(h1, number_text, title_text) ⇒ Object
h1 の data 属性を更新.
-
.wrap_words_in_nobr!(span, doc) ⇒ Boolean
見出し中の各テキストノードを語へ分割し、語ごとに nowrap の span で括る。 折返しは語と語の境目だけで起きるので「コマンドラインオプ/ション」のような 語中の分割が構造的に起きなくなる。.
Class Method Details
.add_chapter_token(heading, chapter_token) ⇒ Object
章トークンを追加
143 144 145 146 147 148 149 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 143 def add_chapter_token(heading, chapter_token) return false unless chapter_token return false if heading['data-chapter'] == chapter_token heading['data-chapter'] = chapter_token true end |
.add_heading_data_attributes(heading, level) ⇒ Object
見出しテキストの data 属性を追加
115 116 117 118 119 120 121 122 123 124 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 115 def add_heading_data_attributes(heading, level) text = extract_heading_core_text(heading) return false if text.nil? || text.empty? modified = false modified |= set_attr_if_changed(heading, 'data-heading', text) modified |= set_attr_if_changed(heading, "data-h#{level}", text) modified |= set_h1_id(heading, text) if level == 1 modified end |
.add_marker_class(heading) ⇒ Object
vs-h-marker クラスを追加
105 106 107 108 109 110 111 112 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 105 def add_marker_class(heading) classes = (heading['class'] || '').split return false if classes.include?('vs-h-marker') classes << 'vs-h-marker' heading['class'] = classes.join(' ').strip true end |
.add_number_span(node, number_class, number_text, doc) ⇒ Object
番号スパンを追加
317 318 319 320 321 322 323 324 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 317 def add_number_span(node, number_class, number_text, doc) return unless number_class && !number_text.empty? span = Nokogiri::XML::Node.new('span', doc) span['class'] = number_class span.content = number_text node.add_child(span) end |
.add_title_span(node, title_class, title_text, original_nodes, doc, kind = nil) ⇒ Object
タイトルスパンを追加
327 328 329 330 331 332 333 334 335 336 337 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 327 def add_title_span(node, title_class, title_text, original_nodes, doc, kind = nil) if title_class span = Nokogiri::XML::Node.new('span', doc) span['class'] = title_class original_nodes.empty? ? span.content = title_text : original_nodes.each { |c| span.add_child(c) } guard_last_character_orphan!(span, doc) if ORPHAN_GUARD_KINDS.include?(kind) node.add_child(span) elsif !title_text.empty? node.add_child(Nokogiri::XML::Text.new(title_text, doc)) end end |
.apply_marker_to_heading(heading, level, chapter_token) ⇒ Object
見出し要素にマーカーを適用
84 85 86 87 88 89 90 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 84 def apply_marker_to_heading(heading, level, chapter_token) modified = add_marker_class(heading) modified |= add_heading_data_attributes(heading, level) modified |= add_chapter_token(heading, chapter_token) modified |= ensure_heading_id(heading, chapter_token) modified end |
.build_h1_number_text(context) ⇒ Object
h1 の番号テキストを構築
208 209 210 211 212 213 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 208 def build_h1_number_text(context) return "付録 #{context[:appendix_letter]}" if context[:file_type] == 'appendix' && context[:appendix_letter] return "第#{context[:chapter_display_number]}章" if context[:chapter_display_number] nil end |
.build_h2_number_text(context, section_index) ⇒ Object
h2 の番号テキストを構築
236 237 238 239 240 241 242 243 244 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 236 def build_h2_number_text(context, section_index) if context[:file_type] == 'appendix' context[:appendix_letter] ? "#{context[:appendix_letter]}-#{section_index}" : section_index.to_s elsif context[:chapter_display_number] "#{context[:chapter_display_number]}-#{section_index}" else section_index.to_s end end |
.build_heading_context(html_path, entry) ⇒ Object
見出し処理のコンテキストを構築
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 178 def build_heading_context(html_path, entry) chapter_token = File.basename(html_path, File.extname(html_path)) chapter_number_i = entry.number&.to_i { file_type: entry.kind.to_s, chapter_display_number: if chapter_number_i resolve_main_chapter_display_number(chapter_token, chapter_number_i) end, appendix_letter: if chapter_number_i&.between?(90, 98) resolve_appendix_letter(chapter_token, chapter_number_i) end, process_headings: %i[chapter appendix].include?(entry.kind) } end |
.chapter_tokens_override ⇒ Object
48 49 50 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 48 def chapter_tokens_override @chapter_tokens_override || [] end |
.chapter_tokens_override=(tokens) ⇒ Object
一時的な章トークンの並びを外部から指定するためのオーバーライド 例: vs build 54-56 のような単章/範囲ビルド時に、 そのビルド対象だけを 1,2,3... の順番で扱いたい場合に使用する。
- nil または空配列の場合はオーバーライドなし(ワークスペースの HTML から自動検出)
- 設定された場合は、その並びを優先的に main_chapter_order の候補として利用する
42 43 44 45 46 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 42 def chapter_tokens_override=(tokens) @chapter_tokens_override = Array(tokens).compact.map(&:to_s) # 章並びを再計算させるためキャッシュを無効化 @main_chapter_order = nil end |
.deepest_last_text_node(node) ⇒ Object
文書順で最後のテキストノードまで降りる(末尾が要素なら中へ入る)
423 424 425 426 427 428 429 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 423 def deepest_last_text_node(node) child = node.children.last return nil if child.nil? return deepest_last_text_node(child) if child.element? child.text? ? child : nil end |
.discovered_main_chapter_tokens ⇒ Array<String>
発見されたHTMLファイルから章トークンを取得 中間 HTML はワークスペースの html/ に置かれる(P4 §3.4-1)
515 516 517 518 519 520 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 515 def discovered_main_chapter_tokens resolver = TokenResolver::Resolver.new html_tokens = Dir.glob(File.join(Common::BUILD_HTML_DIR, '*.html')) .map { |path| File.basename(path, '.html') } normalize_and_filter_tokens(html_tokens).sort_by { |token| resolver.resolve_file(token).number.to_i } end |
.ensure_heading_id(heading, chapter_token) ⇒ Object
見出しに非空の id を保証する。
### $E = mc^2$ のように見出しが数式(→ SVG 画像)のみだと VFM が生成する
slug が空になり、id="" が EPUB で epubcheck RSC-005(id 値が不正)を招くため、
内容のハッシュから一意な id を補う(テキスト見出しの既存 id はそのまま残す)。
96 97 98 99 100 101 102 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 96 def ensure_heading_id(heading, chapter_token) return false unless heading['id'].to_s.strip.empty? digest = Digest::SHA1.hexdigest(heading.inner_html.to_s)[0, 10] heading['id'] = "vsmath-#{chapter_token || 'h'}-#{digest}" true end |
.extract_chapter_token(path) ⇒ Object
ファイルパスから章トークンを抽出
152 153 154 155 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 152 def extract_chapter_token(path) token = File.basename(path, File.extname(path)).to_s.strip token.empty? ? nil : token end |
.extract_heading_core_text(node) ⇒ String
見出しのコアテキストを抽出
434 435 436 437 438 439 440 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 434 def extract_heading_core_text(node) %w[chapter-title section-title subsection-title].each do |cls| span = node.at_css("span.#{cls}") return heading_plain_text(span) if span end heading_plain_text(node) end |
.extract_original_title_nodes(node, number_class, title_class) ⇒ Object
元のタイトルノードを抽出
305 306 307 308 309 310 311 312 313 314 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 305 def extract_original_title_nodes(node, number_class, title_class) title_span = title_class ? node.at_css("span.#{title_class}") : nil if title_span title_span.children.map(&:dup) else node.children.reject do |child| number_class && child.element? && child['class'].to_s.split.include?(number_class) end.map(&:dup) end end |
.guard_last_character_orphan!(span, doc) ⇒ Object
361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 361 def guard_last_character_orphan!(span, doc) # 語の境界が取れるなら、各語を括って「語の途中では折らない」を保証する。 # 取れない環境(MeCab 不在)では末尾 2 文字だけを守る従来の縮退へ。 return if wrap_words_in_nobr!(span, doc) last = deepest_last_text_node(span) return if last.nil? return if last.parent['class'].to_s.split.include?(NOBR_CLASS) # 既に処理済み text = last.text return if text.strip.length < 2 nobr = Nokogiri::XML::Node.new('span', doc) nobr['class'] = NOBR_CLASS nobr.content = text[-2..] last.content = text[0..-3] last.add_next_sibling(nobr) end |
.heading_plain_text(node) ⇒ Object
見出しの素のテキスト。data 属性(EPUB の合成見出し画像・目次の素材)に入れるため、 表示上の装飾を落とす:
- 用語集の † リンク … 見出しを画像化するとクリックできず記号だけが残る
- WORD JOINER … 泣き別れ防止で挟んだ制御文字(見た目に影響しないが
比較・再構築の冪等性を壊すので比較前に落とす)
447 448 449 450 451 452 453 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 447 def heading_plain_text(node) return '' unless node copy = node.dup copy.css('a.glossary-link').each(&:remove) copy.text.to_s.delete(WORD_JOINER).strip end |
.heading_span_classes(kind) ⇒ Object
見出し種別に応じたクラス名を取得
284 285 286 287 288 289 290 291 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 284 def heading_span_classes(kind) case kind when :chapter then %w[chapter-number chapter-title] when :section then %w[section-number section-title] when :subsection then %w[subsection-marker subsection-title] else [nil, nil] end end |
.inject_heading_markers!(html_paths, max_level: 3) ⇒ Object
見出し(h1..hN)に本文参照用のマーカー(class と data 属性)を付与
55 56 57 58 59 60 61 62 63 64 65 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 55 def inject_heading_markers!(html_paths, max_level: 3) paths = Array(html_paths).select { |p| File.exist?(p) } return if paths.empty? max_l = max_level.to_i.clamp(1, 6) paths.each do |path| process_heading_markers_for_file(path, max_l) rescue StandardError => e Common.log_warn("見出しメタ付与に失敗: #{path} (#{e})") end end |
.inject_heading_number_spans!(html_path, entry) ⇒ Object
見出し番号スパンを構築
160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 160 def inject_heading_number_spans!(html_path, entry) return unless File.exist?(html_path) html = File.read(html_path, encoding: 'utf-8') doc = parse_html_document(html) context = build_heading_context(html_path, entry) return unless context[:process_headings] modified = process_h1_spans(doc, context) modified |= process_h2_spans(doc, context) modified |= process_h3_spans(doc) save_html_document(html_path, doc) if modified end |
.main_chapter_order ⇒ Array<String>
メイン章の順序を取得
章構成の正典は config/catalog.yml であり、ここでは絞り込みを解釈しない。 単章/選択ビルドは chapter_tokens_override で与えられ、それ以外は ワークスペースに並んだ HTML から順序を起こす。
496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 496 def main_chapter_order return @main_chapter_order if @main_chapter_order # ビルドコマンド等から一時的な章リストが与えられている場合はそれを優先 override = chapter_tokens_override if override && !override.empty? tokens = normalize_and_filter_tokens(override) if tokens && !tokens.empty? @main_chapter_order = tokens return @main_chapter_order end end @main_chapter_order = discovered_main_chapter_tokens end |
.main_chapter_token?(token) ⇒ Boolean
メイン章トークンかどうか判定
_ 始まりは章ではない——システムページ(_titlepage _part1 など)か、
著者向けの説明ファイル(contents/_README.md)である。
システムページは番号を持たないので従来も落ちていたが、_README は
TokenResolver が番号 01 を割り当てるため通ってしまい、本文章の並びの
先頭に居座って図表番号の章プレフィックスを 1 つずらしていた
(11-workflow が第 1 章ではなく第 2 章として採番される)。
綴りではなく接頭辞で弾く——_ の意味は「章として数えない」だからである。
566 567 568 569 570 571 572 573 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 566 def main_chapter_token?(token) return false if token.to_s.start_with?('_') entry = TokenResolver::Resolver.new.resolve_file(token) return false unless entry.number MAIN_CHAPTER_RANGE.include?(entry.number.to_i) end |
.needs_heading_update?(node, number_text, title_text, number_class, title_class) ⇒ Boolean
見出しの更新が必要か判定
294 295 296 297 298 299 300 301 302 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 294 def needs_heading_update?(node, number_text, title_text, number_class, title_class) current_number = number_class ? node.at_css("span.#{number_class}")&.text&.strip : nil current_title_span = title_class ? node.at_css("span.#{title_class}") : nil current_title = current_title_span&.text&.strip || extract_heading_core_text(current_title_span || node) number_changed = number_text.empty? ? !current_number.to_s.empty? : current_number != number_text title_changed = current_title != title_text number_changed || title_changed end |
.normalize_and_filter_tokens(list) ⇒ Array<String>
トークンリストを正規化してフィルタ
525 526 527 528 529 530 531 532 533 534 535 536 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 525 def normalize_and_filter_tokens(list) seen = {} Array(list).each_with_object([]) do |entry, acc| token = normalize_chapter_token(entry) next unless token next unless main_chapter_token?(token) next if seen[token] seen[token] = true acc << token end end |
.normalize_chapter_token(entry) ⇒ String?
章トークンを正規化
541 542 543 544 545 546 547 548 549 550 551 552 553 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 541 def normalize_chapter_token(entry) s = entry.to_s.strip return nil if s.empty? s = s.sub(%r{\A\./}, '') s = s.sub(%r{\A#{Regexp.escape(Common::CONTENTS_DIR)}/}i, '') s = s.sub(/\.(html|md)\z/i, '') s = s.sub(/\.(html|md)\z/i, '') s = s.strip return nil if s.empty? s end |
.parse_html_document(html) ⇒ Object
HTMLドキュメントをパース(HtmlParser に委譲)
259 260 261 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 259 def parse_html_document(html) HtmlParser.parse_html_document(html) end |
.process_h1_spans(doc, context) ⇒ Object
h1 のスパン処理
196 197 198 199 200 201 202 203 204 205 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 196 def process_h1_spans(doc, context) h1 = doc.at_css('h1') return false unless h1 title_text = extract_heading_core_text(h1) number_text = build_h1_number_text(context) modified = rebuild_heading_with_spans(h1, number_text, title_text, :chapter, doc) update_h1_data_attributes(h1, number_text, title_text) modified end |
.process_h2_spans(doc, context) ⇒ Object
h2 のスパン処理
222 223 224 225 226 227 228 229 230 231 232 233 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 222 def process_h2_spans(doc, context) modified = false doc.css('h2').each_with_index do |h2, idx| section_index = idx + 1 title_text = extract_heading_core_text(h2) number_text = build_h2_number_text(context, section_index) modified |= rebuild_heading_with_spans(h2, number_text, title_text, :section, doc) h2['data-section-number-display'] = number_text if number_text h2['data-section-title'] = title_text if title_text end modified end |
.process_h3_spans(doc) ⇒ Object
h3 のスパン処理
247 248 249 250 251 252 253 254 255 256 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 247 def process_h3_spans(doc) marker = Common::CONFIG.theme.markers.h3 || '♣' modified = false doc.css('h3').each do |h3| title_text = extract_heading_core_text(h3) modified |= rebuild_heading_with_spans(h3, marker, title_text, :subsection, doc) h3['data-subsection-title'] = title_text if title_text end modified end |
.process_heading_markers_for_file(path, max_level) ⇒ Object
単一ファイルの見出しマーカー処理
68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 68 def process_heading_markers_for_file(path, max_level) html = File.read(path, encoding: 'utf-8') doc = parse_html_document(html) chapter_token = extract_chapter_token(path) modified = false (1..max_level).each do |lvl| doc.css("h#{lvl}").each do |h| modified |= apply_marker_to_heading(h, lvl, chapter_token) end end save_html_document(path, doc) if modified end |
.rebuild_heading_with_spans(node, number_text, title_text, kind, doc) ⇒ Object
見出しを番号スパンとタイトルスパンで再構築
269 270 271 272 273 274 275 276 277 278 279 280 281 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 269 def rebuild_heading_with_spans(node, number_text, title_text, kind, doc) number_text = number_text.to_s.strip title_text = title_text.to_s.strip number_class, title_class = heading_span_classes(kind) return false unless needs_heading_update?(node, number_text, title_text, number_class, title_class) original_title_nodes = extract_original_title_nodes(node, number_class, title_class) node.children.remove add_number_span(node, number_class, number_text, doc) add_title_span(node, title_class, title_text, original_title_nodes, doc, kind) true end |
.replace_with_nobr_words!(node, words, doc) ⇒ Object
語ごとに nowrap の span を置く。語末の空白は span の外へ出す—— 中に入れると nowrap がその空白での折返しまで禁止し、「(create / delete / rename」のような半角混じりの見出しが 1 つの塊になって版面をはみ出す (pdf_h1_chapter6.png 実測)。外に出せば空白が折返し候補として残る。
406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 406 def replace_with_nobr_words!(node, words, doc) fragment = Nokogiri::XML::Node.new('span', doc) words.each do |word| core = word.sub(/\s+\z/, '') unless core.empty? nobr = Nokogiri::XML::Node.new('span', doc) nobr['class'] = NOBR_CLASS nobr.content = core fragment.add_child(nobr) end trailing = word[core.length..] fragment.add_child(Nokogiri::XML::Text.new(trailing, doc)) unless trailing.empty? end node.replace(fragment.children) end |
.resolve_appendix_letter(chapter_token, chapter_number_i) ⇒ Object
付録のレター(A/B/C...)をビルド対象の付録の順番に基づいて解決する
474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 474 def resolve_appendix_letter(chapter_token, chapter_number_i) override = chapter_tokens_override if override && !override.empty? # 単章/選択ビルド: ビルド対象の付録トークンから Entry を構築 resolver = TokenResolver::Resolver.new entries = override.filter_map do |token| entry = resolver.resolve_file(token) entry if entry&.kind == :appendix end Common.appendix_number_to_letter(chapter_number_i, entries: entries)&.upcase else # フルビルド: catalog.yml の全付録から順番を取得 Common.appendix_number_to_letter(chapter_number_i)&.upcase end end |
.resolve_main_chapter_display_number(chapter_token, chapter_number_i = nil) ⇒ Integer?
メイン章の表示番号を解決
459 460 461 462 463 464 465 466 467 468 469 470 471 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 459 def resolve_main_chapter_display_number(chapter_token, chapter_number_i = nil) return nil if chapter_token.nil? || chapter_token.empty? chapter_number_i ||= TokenResolver::Resolver.new.resolve_file(chapter_token).number&.to_i return nil unless chapter_number_i && MAIN_CHAPTER_RANGE.include?(chapter_number_i) order = main_chapter_order if (idx = order.index(chapter_token)) return idx + 1 end chapter_number_i end |
.save_html_document(path, doc) ⇒ Object
HTMLドキュメントを保存(HtmlParser に委譲)
264 265 266 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 264 def save_html_document(path, doc) HtmlParser.save_html_document(path, doc) end |
.set_attr_if_changed(node, attr, value) ⇒ Object
属性値が変わった場合のみ設定
127 128 129 130 131 132 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 127 def set_attr_if_changed(node, attr, value) return false if node[attr] == value node[attr] = value true end |
.set_h1_id(heading, text) ⇒ Object
h1 に id を設定
135 136 137 138 139 140 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 135 def set_h1_id(heading, text) return false unless heading['id'].to_s.strip.empty? heading['id'] = text true end |
.update_h1_data_attributes(h1, number_text, title_text) ⇒ Object
h1 の data 属性を更新
216 217 218 219 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 216 def update_h1_data_attributes(h1, number_text, title_text) number_text ? h1['data-chapter-number-display'] = number_text : h1.delete('data-chapter-number-display') title_text ? h1['data-chapter-title'] = title_text : h1.delete('data-chapter-title') end |
.wrap_words_in_nobr!(span, doc) ⇒ Boolean
見出し中の各テキストノードを語へ分割し、語ごとに nowrap の span で括る。 折返しは語と語の境目だけで起きるので「コマンドラインオプ/ション」のような 語中の分割が構造的に起きなくなる。
テキストノード単位で処理するのは、索引語や用語集リンクの要素構造を壊さないため (要素の境目はもともと折返し候補なので、分割の質は落ちない)。
387 388 389 390 391 392 393 394 395 396 397 398 399 400 |
# File 'lib/vivlio_starter/cli/post_process/heading_processor.rb', line 387 def wrap_words_in_nobr!(span, doc) wrapped = false span.xpath('.//text()').to_a.each do |node| next if node.parent['class'].to_s.split.include?(NOBR_CLASS) # 既に処理済み next if node.text.strip.empty? words = HeadingSegmenter.segment(node.text) next if words.size < 2 && node.text.strip.length < 2 replace_with_nobr_words!(node, words, doc) wrapped = true end wrapped end |