Module: VivlioStarter::CLI::PostProcessCommands
- Defined in:
- lib/vivlio_starter/cli/post_process.rb,
lib/vivlio_starter/cli/post_process/html_parser.rb,
lib/vivlio_starter/cli/post_process/html_replacer.rb,
lib/vivlio_starter/cli/post_process/section_wrapper.rb,
lib/vivlio_starter/cli/post_process/heading_processor.rb,
lib/vivlio_starter/cli/post_process/replacement_rules.rb,
lib/vivlio_starter/cli/post_process/footnote_converter.rb,
lib/vivlio_starter/cli/post_process/body_class_injector.rb,
lib/vivlio_starter/cli/post_process/page_break_normalizer.rb,
lib/vivlio_starter/cli/post_process/terminal_block_converter.rb
Overview
================================================================
Module: PostProcessCommands
HTML後処理のThorコマンド群とヘルパーメソッドを提供
Defined Under Namespace
Modules: BodyClassInjector, FootnoteConverter, HeadingProcessor, HtmlParser, HtmlReplacer, PageBreakNormalizer, ReplacementRules, SectionWrapper, TerminalBlockConverter
Constant Summary collapse
- POST_PROCESS_DESC =
{ short: 'HTMLファイルのポスト置換処理を行います', long: <<~DESC 指定した HTML ファイルの後処理を行います。指定が無い場合はプロジェクトルートの全 .html を対象にします。 処理内容: - <body> タグにファイルタイプクラスを付与 - 組み込み置換ルールによる置換処理 - 章末脚注をページ脚注に変換 - ソースコードに行番号を追加(Prism.js対応) 例: vs post_process 11-install vs post_process 11-install.html 12-tutorial DESC }.freeze
Class Method Summary collapse
- .build_renumber_map(footnote_refs) ⇒ Object
- .collect_footnote_refs(doc) ⇒ Object
-
.execute_post_process(context_or_options, entries = []) ⇒ Object
Samovar/直接呼び出し用エントリポイント.
-
.extract_image_group_width_fraction(figure, img) ⇒ Object
image-group 内の先頭画像から width 指定(%)を取り出し、 0.0〜1.0 の範囲の比率として返す.
-
.extract_sideimage_width_fraction(figure) ⇒ Object
sideimage コンテナ内の figure/img から width 指定(%)を取り出し、 0.0〜1.0 の範囲の比率として返す.
- .included(base) ⇒ Object
-
.move_body_asides_near_references!(doc) ⇒ Object
body 直下の aside.page-footnote-print を対応する参照の近くに移動する append_unused_footnotes_to_body! が body 末尾に追加した aside を 参照と同じページに float:footnote で表示されるよう配置し直す.
- .needs_renumbering?(renumber_map) ⇒ Object
-
.normalize_image_group_width!(figure, img) ⇒ Object
列幅でレイアウトを制御するため、figure/img 側の width 指定は取り除く.
-
.normalize_options(context_or_options) ⇒ Object
オプションを正規化.
-
.normalize_sideimage_figure_width!(figure) ⇒ Object
列幅でレイアウトを制御するため、figure/img 側の width 指定は取り除く.
-
.process_image_groups!(html_file) ⇒ Object
image-group コンテナの列比率設定 ---------------------------------------------------------------- VFM が出力する
のような構造で、先頭画像の width 指定を検出し、 CSS 変数 --image-group-col1, --image-group-col2 として コンテナの style 属性に埋め込む。 ================================================================....
...
-
.process_sideimage_footnotes!(html_file) ⇒ Object
sideimage 内の脚注参照を処理 ---------------------------------------------------------------- Step 4 で page-footnote-print が生成された後に呼び出され、 sideimage-body 内の [^urlN] を N に変換する。 印刷読者のために、リンクのURLを脚注として表示する。 ================================================================.
- .remove_footnote_anchors(doc) ⇒ Object
-
.renumber_footnotes_by_document_order!(html_file) ⇒ Object
脚注をドキュメント出現順に再番号付け ---------------------------------------------------------------- sideimage 内の脚注と本文の脚注が混在する場合、処理順序により 番号が出現順にならないことがある。この関数で修正する。 ================================================================.
- .reorder_asides(doc, asides, sorted_asides) ⇒ Object
-
.resolve_entry_map(entries) ⇒ Hash{String => TokenResolver::Entry}
Entry 配列を解決し、HTML パス => Entry の Hash を返す 中間 HTML はワークスペースの html/ に置かれる(P4 §3.4-1).
-
.sanitize_image_group_fraction(raw) ⇒ Object
パーセンテージから得た比率を安全な範囲に丸める.
-
.sanitize_sideimage_fraction(raw) ⇒ Object
パーセンテージから得た比率を安全な範囲に丸める.
- .sort_footnotes_in_sections(doc) ⇒ Object
- .sort_section_footnotes(doc, section) ⇒ Object
- .update_aside_footnote(doc, old_fn_id, new_number) ⇒ Object
- .update_fnref_link(doc, old_fn_id, new_number) ⇒ Object
- .update_footnote_definitions(doc, renumber_map) ⇒ Object
- .update_footnote_ref_text(ref, new_number) ⇒ Object
- .update_footnote_refs(footnote_refs) ⇒ Object
-
.update_inline_footnote(doc, old_fn_id, new_number) ⇒ Object
脚注番号は data-footnote-number から ::before で描かれる(components.css)。 PDF で脚注本体を担うのは参照直後の span なので、id だけ振り直して番号を 据え置くと、本体には再番号付け前の番号が出たままになる。 aside 側(update_aside_footnote)と同じく両方を更新する。.
-
.wrap_cross_ref_code_blocks!(html_file) ⇒ Object
クロスリファレンス用コードブロックのラップ ---------------------------------------------------------------- pre_process で挿入した "" コメントを基準に、 直前の
(キャプション)と直後の
(Prism済みコード)を
で包みます。 旧スタイルのにも対応します。 行番号付与 (prism_lines) 後に実行します。 ================================================================.
- .wrap_img_text_blocks!(html_file) ⇒ Object
img-text / text-img 系コンテナの正規化 ---------------------------------------------------------------- VFM が出力する
テキスト………のような構造では、テキストノードや が独立したグリッドセルに なり、レイアウトが崩れる。figure 以外の子ノードを<img …> でまとめて包み、常にという 2 要素構造に正規化する。 ================================================================.テキスト…… - .wrap_sideimage_blocks!(html_file) ⇒ Object
sideimage コンテナ内テキストのラップ ---------------------------------------------------------------- Vivliostyle/VFM が出力する
のような構造では、figure 以外がテキストノードとなり CSS Grid の .sideimage-right > :not(figure) セレクタでは拾えない。 そこで、figure 以外の子ノードを… 本文テキスト…で まとめて包み、常にという 2 要素構造に正規化する。 ================================================================.… 本文…Class Method Details
.build_renumber_map(footnote_refs) ⇒ Object
794 795 796 797 798 799 800 801
# File 'lib/vivlio_starter/cli/post_process.rb', line 794 def build_renumber_map(footnote_refs) renumber_map = {} footnote_refs.each_with_index do |ref, idx| old_fn_id = ref['href'].sub('#', '') renumber_map[old_fn_id] = idx + 1 end renumber_map end
.collect_footnote_refs(doc) ⇒ Object
779 780 781 782 783 784 785 786 787 788 789 790 791
# File 'lib/vivlio_starter/cli/post_process.rb', line 779 def collect_footnote_refs(doc) refs = [] doc.traverse do |node| next unless node.element? next unless node.name == 'a' && node['class']&.include?('footnote-ref') && node['href']&.start_with?('#fn') parent = node.parent next if parent&.name == 'span' && parent['class']&.include?('footnote-anchor') refs << node end refs end
.execute_post_process(context_or_options, entries = []) ⇒ Object
Samovar/直接呼び出し用エントリポイント
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195
# File 'lib/vivlio_starter/cli/post_process.rb', line 79 def execute_post_process(, entries = []) opts = () ENV['VERBOSE'] = '1' if opts[:verbose] entry_map = resolve_entry_map(entries) entry_map.each do |html_file, entry| BodyClassInjector.inject_body_class(html_file, entry) # HtmlReplacer より前に <pre> 化しておく。以降 <pre> は退避対象となり、 # 端末の逐語出力に含まれる `---` 等が置換ルール(<hr class="pagebreak">)に食われない。 TerminalBlockConverter.convert_terminal_blocks!(html_file) end total_replacements = 0 entry_map.each do |html_file, entry| Common.log_action("処理中: #{html_file}") result = ReplacementRules.apply_builtin!(html_file) if result[:changed] total_replacements += result[:replacements] Common.log_success("#{html_file}: #{result[:replacements]}個の置換を反映") else Common.log_info("#{html_file}: 変更なし") end begin content_before = File.read(html_file, encoding: 'utf-8') content_after = SectionWrapper.wrap_h2_with_article_if_image_style!(content_before) if content_after != content_before File.write(html_file, content_after, encoding: 'utf-8') Common.log_success("#{html_file}: h2を<article.section-topic>でラップ(theme.style=image)") result2 = ReplacementRules.apply_builtin!(html_file) if result2[:changed] Common.log_success("#{html_file}: ラップ後の不要な空段落をクリーンアップ (#{result2[:replacements]}件)") end end rescue StandardError => e Common.log_error("#{html_file}: section-topicラップ中にエラー: #{e.}") end begin wrap_sideimage_blocks!(html_file) rescue StandardError => e Common.log_error("#{html_file}: sideimage ラップ中にエラー: #{e.}") end begin process_image_groups!(html_file) rescue StandardError => e Common.log_error("#{html_file}: image-group 処理中にエラー: #{e.}") end begin wrap_img_text_blocks!(html_file) rescue StandardError => e Common.log_error("#{html_file}: img-text ラップ中にエラー: #{e.}") end content = File.read(html_file, encoding: 'utf-8') converted = FootnoteConverter.convert_endnotes_to_page_footnotes!(content) if converted != content File.write(html_file, converted, encoding: 'utf-8') Common.log_success("#{html_file}: 章末脚注をページ脚注に変換") end begin process_sideimage_footnotes!(html_file) rescue StandardError => e Common.log_error("#{html_file}: sideimage 脚注処理中にエラー: #{e.}") end begin renumber_footnotes_by_document_order!(html_file) rescue StandardError => e Common.log_error("#{html_file}: 脚注再番号付け中にエラー: #{e.}") end # Prism.js 行番号付与(直接呼び出し) PrismLinesCommands.execute_prism_lines(html_file) wrap_cross_ref_code_blocks!(html_file) begin HeadingProcessor.inject_heading_markers!([html_file], max_level: 3) Common.log_info("#{html_file}: 見出しメタを付与 (class=data)") rescue StandardError => e Common.log_warn("#{html_file}: 見出しメタ付与に失敗: #{e}") end begin HeadingProcessor.inject_heading_number_spans!(html_file, entry) Common.log_info("#{html_file}: 見出し番号スパンを構築") rescue StandardError => e Common.log_warn("#{html_file}: 見出し番号スパン構築に失敗: #{e}") end # 最終クリーンアップ: Nokogiri 系のステップが <p><div>...</div></p> の # ねじれを正すときに残してしまう空の <p></p> などを除去する。 final_result = ReplacementRules.apply_builtin!(html_file) if final_result[:changed] total_replacements += final_result[:replacements] Common.log_info("#{html_file}: 最終クリーンアップで #{final_result[:replacements]} 件を整理") end # 二重改ページの正規化は最後に行う。改ページマーカー(ReplacementRules)と # article.section-topic ラップ(SectionWrapper)が揃った DOM でなければ # 「マーカーの直後が h2 か」を正しく判定できない(page-break-control-spec §2.1-5)。 begin PageBreakNormalizer.normalize!(html_file) rescue StandardError => e Common.log_error("#{html_file}: 改ページ正規化中にエラー: #{e.}") end end Common.log_success("ポスト置換処理完了 (合計: #{total_replacements}個の置換)") end
.extract_image_group_width_fraction(figure, img) ⇒ Object
image-group 内の先頭画像から width 指定(%)を取り出し、 0.0〜1.0 の範囲の比率として返す
602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622
# File 'lib/vivlio_starter/cli/post_process.rb', line 602 def extract_image_group_width_fraction(figure, img) width_attr = '' width_attr = figure['width'].to_s if figure width_attr = img['width'].to_s if width_attr.empty? && img if (m = width_attr.match(/([0-9]+(?:\.[0-9]+)?)\s*%/)) return sanitize_image_group_fraction(m[1].to_f / 100.0) end style_sources = [] style_sources << figure['style'].to_s if figure style_sources << img['style'].to_s if img style_sources.compact.each do |style| if (m = style.match(/width\s*:\s*([0-9]+(?:\.[0-9]+)?)\s*%/)) return sanitize_image_group_fraction(m[1].to_f / 100.0) end end nil end
.extract_sideimage_width_fraction(figure) ⇒ Object
sideimage コンテナ内の figure/img から width 指定(%)を取り出し、 0.0〜1.0 の範囲の比率として返す
956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978
# File 'lib/vivlio_starter/cli/post_process.rb', line 956 def extract_sideimage_width_fraction(figure) # figure 要素・子 img 要素の両方を対象とする img = figure.at_css('img') width_attr = figure['width'].to_s width_attr = img['width'].to_s if width_attr.empty? && img if (m = width_attr.match(/([0-9]+(?:\.[0-9]+)?)\s*%/)) return sanitize_sideimage_fraction(m[1].to_f / 100.0) end style_sources = [] style_sources << figure['style'].to_s style_sources << img['style'].to_s if img style_sources.compact.each do |style| if (m = style.match(/width\s*:\s*([0-9]+(?:\.[0-9]+)?)\s*%/)) return sanitize_sideimage_fraction(m[1].to_f / 100.0) end end nil end
.included(base) ⇒ Object
74
# File 'lib/vivlio_starter/cli/post_process.rb', line 74 def included(base); end
.move_body_asides_near_references!(doc) ⇒ Object
body 直下の aside.page-footnote-print を対応する参照の近くに移動する append_unused_footnotes_to_body! が body 末尾に追加した aside を 参照と同じページに float:footnote で表示されるよう配置し直す
919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951
# File 'lib/vivlio_starter/cli/post_process.rb', line 919 def move_body_asides_near_references!(doc) body = doc.at_css('body') return unless body body_asides = body.css('> aside.page-footnote-print').to_a return if body_asides.empty? body_asides.each do |aside| fn_id = aside['id'] next unless fn_id # この aside を参照している <a class="footnote-ref"> を探す ref = doc.at_css("a.footnote-ref[href='##{fn_id}']") # sideimage 内の参照は、コンテナの直後に配置する # (sideimage 内に aside を置くと CSS Grid レイアウトが壊れるため) sideimage = ref && FootnoteConverter.find_sideimage_container(ref) if sideimage aside.remove sideimage.add_next_sibling(aside) next end # 参照が見つからない場合は最後の section 末尾に移動 target_section = ref&.ancestors('section')&.first target_section ||= body.css('section').to_a.last next unless target_section aside.remove target_section.add_child("\n") target_section.add_child(aside) end end
.needs_renumbering?(renumber_map) ⇒ Object
804 805 806
# File 'lib/vivlio_starter/cli/post_process.rb', line 804 def needs_renumbering?(renumber_map) renumber_map.any? { |old_id, new_num| old_id.sub('fn', '').to_i != new_num } end
.normalize_image_group_width!(figure, img) ⇒ Object
列幅でレイアウトを制御するため、figure/img 側の width 指定は取り除く
635 636 637 638 639 640 641 642 643 644 645 646 647 648 649
# File 'lib/vivlio_starter/cli/post_process.rb', line 635 def normalize_image_group_width!(figure, img) [figure, img].compact.each do |node| node.remove_attribute('width') style = node['style'].to_s next if style.empty? cleaned = style.gsub(/width\s*:\s*[^;]+;?/, '').strip if cleaned.empty? node.remove_attribute('style') else node['style'] = cleaned end end end
.normalize_options(context_or_options) ⇒ Object
オプションを正規化
199 200 201 202 203 204 205 206 207
# File 'lib/vivlio_starter/cli/post_process.rb', line 199 def () if .is_a?(Hash) [:options] || elsif .respond_to?(:options) . || {} else {} end end
.normalize_sideimage_figure_width!(figure) ⇒ Object
列幅でレイアウトを制御するため、figure/img 側の width 指定は取り除く
989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005
# File 'lib/vivlio_starter/cli/post_process.rb', line 989 def normalize_sideimage_figure_width!(figure) img = figure.at_css('img') [figure, img].compact.each do |node| node.remove_attribute('width') style = node['style'].to_s next if style.empty? cleaned = style.gsub(/width\s*:\s*[^;]+;?/, '').strip if cleaned.empty? node.remove_attribute('style') else node['style'] = cleaned end end end
.process_image_groups!(html_file) ⇒ Object
================================================================
image-group コンテナの列比率設定
VFM が出力する
のような構造で、先頭画像の width 指定を検出し、 CSS 変数 --image-group-col1, --image-group-col2 として コンテナの style 属性に埋め込む。 ================================================================...
...
494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538
# File 'lib/vivlio_starter/cli/post_process.rb', line 494 def process_image_groups!(html_file) content = File.read(html_file, encoding: 'utf-8') doc = HtmlParser.parse_html_document(content) changed = false doc.css('div.image-group').each do |container| # 先頭の figure または img を取得 first_figure = container.at_css('> figure') first_img = first_figure&.at_css('img') || container.at_css('> img') next unless first_img || first_figure # 先頭画像の width 指定を取得 fraction = extract_image_group_width_fraction(first_figure, first_img) next unless fraction col1_fr = fraction.round(3) col2_fr = (1.0 - fraction).round(3) existing_style = container['style'].to_s # 既存の image-group 用変数定義を一度取り除く cleaned_style = existing_style .gsub(/--image-group-col1:[^;]+;?/, '') .gsub(/--image-group-col2:[^;]+;?/, '') .strip var_style = "--image-group-col1: #{col1_fr}fr; --image-group-col2: #{col2_fr}fr" container['style'] = if cleaned_style.empty? var_style else "#{cleaned_style.chomp(';')}; #{var_style}" end # 画像側の width 指定を削除(グリッド列幅で制御) normalize_image_group_width!(first_figure, first_img) changed = true end return unless changed HtmlParser.save_html_document(html_file, doc) Common.log_success("#{html_file}: image-group コンテナの列比率を設定しました") end
.process_sideimage_footnotes!(html_file) ⇒ Object
================================================================
sideimage 内の脚注参照を処理
Step 4 で page-footnote-print が生成された後に呼び出され、 sideimage-body 内の [^urlN] を N に変換する。 印刷読者のために、リンクのURLを脚注として表示する。
659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744
# File 'lib/vivlio_starter/cli/post_process.rb', line 659 def process_sideimage_footnotes!(html_file) content = File.read(html_file, encoding: 'utf-8') doc = HtmlParser.parse_html_document(content) changed = false # URLと脚注 aside のマッピングを構築 url_to_footnote = {} doc.css('aside.page-footnote-print').each do |aside| next unless aside['data-footnote-number'] link = aside.at_css('a') url_to_footnote[link['href']] = aside if link && link['href'] end return if url_to_footnote.empty? # sideimage-body 内の <a> タグの直後にある [^urlN] を処理 doc.css('div.sideimage-body').each do |body| body.css('a').each do |link_elem| next_node = link_elem.next_sibling next unless next_node&.text? text = next_node.text # [^urlN] または [^N] パターンを検出 next unless text.match?(/^\s*\[\^(?:url)?\d+\]/) # リンクのURLから対応する脚注 aside を取得 target_url = link_elem['href'] footnote_aside = url_to_footnote[target_url] if footnote_aside fn_num = footnote_aside['data-footnote-number'] # 脚注参照をスーパースクリプトリンクに置換 modified = text.sub(/^\s*\[\^(?:url)?\d+\]/, '') # スーパースクリプトリンクを生成 sup = Nokogiri::XML::Node.new('sup', doc) anchor = Nokogiri::XML::Node.new('a', doc) anchor['href'] = "#fn#{fn_num}" anchor['class'] = 'footnote-ref' anchor['role'] = 'doc-noteref' anchor.content = fn_num sup.add_child(anchor) # <a>タグの直後にスーパースクリプトを挿入 link_elem.add_next_sibling(sup) # 参照リンクの解決先となる隠しインライン脚注を直後に挿入する # (解決先が aside 自体だと Vivliostyle が脚注を重複描画するため) inline_span = FootnoteConverter.build_inline_footnote_node( doc, "fn#{fn_num}", footnote_aside.inner_html ) sup.add_next_sibling(inline_span) # 脚注フロートは挿入した span が担う。控えの aside にも印を付けないと # CSS(aside.page-footnote[data-footnote-anchored] の非表示)が効かず、 # 同じ脚注がページ下部へ二重に降りる。FootnoteConverter が自前で # span を挿すときに anchored: true を渡しているのと同じ意味付け。 footnote_aside['data-footnote-anchored'] = '1' # 残りのテキストを更新 if modified.strip.empty? next_node.remove else next_node.content = modified end else # 対応する脚注が見つからない場合は参照を削除 cleaned = text.gsub(/\s*\[\^(?:url)?\d+\]/, '') if cleaned.empty? next_node.remove else next_node.content = cleaned end end changed = true end end return unless changed HtmlParser.save_html_document(html_file, doc) Common.log_success("#{html_file}: sideimage 脚注参照を変換しました") end
.remove_footnote_anchors(doc) ⇒ Object
877 878 879 880 881 882 883 884 885 886
# File 'lib/vivlio_starter/cli/post_process.rb', line 877 def remove_footnote_anchors(doc) # 旧形式: <span class="footnote-anchor"> を削除 doc.css('span.footnote-anchor').each do |anchor| parent = anchor.parent anchor.remove parent.remove if parent&.name == 'p' && parent.content.strip.empty? end # 新形式: <p class="footnote-anchor"> を削除(VFM が {.footnote-anchor} から生成) doc.css('p.footnote-anchor').each(&:remove) end
.renumber_footnotes_by_document_order!(html_file) ⇒ Object
================================================================
脚注をドキュメント出現順に再番号付け
sideimage 内の脚注と本文の脚注が混在する場合、処理順序により 番号が出現順にならないことがある。この関数で修正する。
753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776
# File 'lib/vivlio_starter/cli/post_process.rb', line 753 def renumber_footnotes_by_document_order!(html_file) content = File.read(html_file, encoding: 'utf-8') doc = HtmlParser.parse_html_document(content) footnote_refs = collect_footnote_refs(doc) return if footnote_refs.empty? renumber_map = build_renumber_map(footnote_refs) if needs_renumbering?(renumber_map) update_footnote_refs(footnote_refs) update_footnote_definitions(doc, renumber_map) sort_footnotes_in_sections(doc) end # footnote-anchor span は再番号付けの有無に関わらず常に削除する remove_footnote_anchors(doc) # body 直下の aside.page-footnote-print を参照の近くに移動する move_body_asides_near_references!(doc) HtmlParser.save_html_document(html_file, doc) Common.log_success("#{html_file}: 脚注を出現順に再番号付けしました") end
.reorder_asides(doc, asides, sorted_asides) ⇒ Object
907 908 909 910 911 912 913
# File 'lib/vivlio_starter/cli/post_process.rb', line 907 def reorder_asides(doc, asides, sorted_asides) marker = Nokogiri::XML::Comment.new(doc, 'footnote-sort-marker') asides.first.add_previous_sibling(marker) asides.each(&:remove) sorted_asides.reverse_each { |aside| marker.add_next_sibling(aside) } marker.remove end
.resolve_entry_map(entries) ⇒ Hash{String => TokenResolver::Entry}
Entry 配列を解決し、HTML パス => Entry の Hash を返す 中間 HTML はワークスペースの html/ に置かれる(P4 §3.4-1)
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238
# File 'lib/vivlio_starter/cli/post_process.rb', line 214 def resolve_entry_map(entries) raw = Array(entries).compact resolver = TokenResolver::Resolver.new if raw.empty? # 全 HTML ファイルを TokenResolver で解決 Dir.glob(File.join(Common::BUILD_HTML_DIR, '*.html')).each_with_object({}) do |html_file, map| entry = resolver.resolve_file(html_file) map[html_file] = entry end elsif raw.first.respond_to?(:kind) # Entry 配列: HTML パスと紐付け raw.each_with_object({}) do |entry, map| html_file = File.join(Common::BUILD_HTML_DIR, "#{entry.basename}.html") map[html_file] = entry end else # basename/パスの場合は TokenResolver で解決 raw.each_with_object({}) do |bn, map| entry = resolver.resolve_file(bn) html_file = File.join(Common::BUILD_HTML_DIR, "#{entry.basename}.html") map[html_file] = entry end end end
.sanitize_image_group_fraction(raw) ⇒ Object
パーセンテージから得た比率を安全な範囲に丸める
626 627 628 629 630 631
# File 'lib/vivlio_starter/cli/post_process.rb', line 626 def sanitize_image_group_fraction(raw) return nil unless raw.positive? # 極端な比率を避けるため、5%〜95% にクランプ raw.clamp(0.05, 0.95) end
.sanitize_sideimage_fraction(raw) ⇒ Object
パーセンテージから得た比率を安全な範囲に丸める
981 982 983 984 985 986
# File 'lib/vivlio_starter/cli/post_process.rb', line 981 def sanitize_sideimage_fraction(raw) return nil unless raw.positive? # 極端な比率を避けるため、5%〜95% にクランプ raw.clamp(0.05, 0.95) end
.sort_footnotes_in_sections(doc) ⇒ Object
889 890 891 892 893
# File 'lib/vivlio_starter/cli/post_process.rb', line 889 def sort_footnotes_in_sections(doc) doc.css('section').each do |section| sort_section_footnotes(doc, section) end end
.sort_section_footnotes(doc, section) ⇒ Object
896 897 898 899 900 901 902 903 904
# File 'lib/vivlio_starter/cli/post_process.rb', line 896 def sort_section_footnotes(doc, section) asides = section.css('> aside.page-footnote-print').to_a return if asides.size < 2 sorted_asides = asides.sort_by { |a| a['data-footnote-number'].to_i } return if asides.map { |a| a['data-footnote-number'] } == sorted_asides.map { |a| a['data-footnote-number'] } reorder_asides(doc, asides, sorted_asides) end
.update_aside_footnote(doc, old_fn_id, new_number) ⇒ Object
848 849 850 851 852 853 854
# File 'lib/vivlio_starter/cli/post_process.rb', line 848 def update_aside_footnote(doc, old_fn_id, new_number) aside = doc.at_css("aside##{old_fn_id}") return unless aside aside['id'] = "fn#{new_number}" aside['data-footnote-number'] = new_number.to_s end
.update_fnref_link(doc, old_fn_id, new_number) ⇒ Object
870 871 872 873 874
# File 'lib/vivlio_starter/cli/post_process.rb', line 870 def update_fnref_link(doc, old_fn_id, new_number) old_fnref_id = old_fn_id.sub('fn', 'fnref') fnref = doc.at_css("a##{old_fnref_id}") fnref['id'] = "fnref#{new_number}" if fnref end
.update_footnote_definitions(doc, renumber_map) ⇒ Object
830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845
# File 'lib/vivlio_starter/cli/post_process.rb', line 830 def update_footnote_definitions(doc, renumber_map) # IDの衝突を避けるため、2段階で更新する # Phase 1: 全定義を一時ID(tmp_fnN)に変更 # ※ update_footnote_refs が先に fnref ID を更新済みのため fnref は対象外 renumber_map.each_key do |old_fn_id| tmp_id = "tmp_#{old_fn_id}" doc.at_css("aside##{old_fn_id}")&.tap { |n| n['id'] = tmp_id } doc.at_css("span##{old_fn_id}")&.tap { |n| n['id'] = tmp_id } end # Phase 2: 一時IDから最終IDへ変更 renumber_map.each do |old_fn_id, new_number| tmp_id = "tmp_#{old_fn_id}" update_aside_footnote(doc, tmp_id, new_number) update_inline_footnote(doc, tmp_id, new_number) end end
.update_footnote_ref_text(ref, new_number) ⇒ Object
819 820 821 822 823 824 825 826 827
# File 'lib/vivlio_starter/cli/post_process.rb', line 819 def update_footnote_ref_text(ref, new_number) if ref.parent&.name == 'sup' ref.content = new_number.to_s elsif ref.at_css('sup') ref.at_css('sup').content = new_number.to_s else ref.content = new_number.to_s end end
.update_footnote_refs(footnote_refs) ⇒ Object
809 810 811 812 813 814 815 816
# File 'lib/vivlio_starter/cli/post_process.rb', line 809 def update_footnote_refs(footnote_refs) footnote_refs.each_with_index do |ref, idx| new_number = idx + 1 ref['href'] = "#fn#{new_number}" ref['id'] = "fnref#{new_number}" if ref['id'] update_footnote_ref_text(ref, new_number) end end
.update_inline_footnote(doc, old_fn_id, new_number) ⇒ Object
脚注番号は data-footnote-number から ::before で描かれる(components.css)。 PDF で脚注本体を担うのは参照直後の span なので、id だけ振り直して番号を 据え置くと、本体には再番号付け前の番号が出たままになる。 aside 側(update_aside_footnote)と同じく両方を更新する。
861 862 863 864 865 866 867
# File 'lib/vivlio_starter/cli/post_process.rb', line 861 def update_inline_footnote(doc, old_fn_id, new_number) inline = doc.at_css("span##{old_fn_id}") return unless inline inline['id'] = "fn#{new_number}" inline['data-footnote-number'] = new_number.to_s end
.wrap_cross_ref_code_blocks!(html_file) ⇒ Object
================================================================
クロスリファレンス用コードブロックのラップ
pre_process で挿入した "" コメントを基準に、 直前の
(キャプション)と直後の
(Prism済みコード)を
で包みます。 旧スタイルのにも対応します。 行番号付与 (prism_lines) 後に実行します。
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 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 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 324 325 326 327 328 329 330 331 332
# File 'lib/vivlio_starter/cli/post_process.rb', line 250 def wrap_cross_ref_code_blocks!(html_file) content = File.read(html_file, encoding: 'utf-8') doc = HtmlParser.parse_html_document(content) changed = false # パターン1: <!--xref:ID--> コメントを基準にラップ doc.xpath('//comment()').each do |comment| text = comment.text.to_s.strip next unless text.start_with?('xref:') id = text.sub(/\Axref:/, '') next if id.empty? caption = comment.previous_element block = comment.next_element next unless caption&.name == 'p' next unless block # 直後が <pre> 単体、または <figure> 内に <pre> を含むパターンの両方に対応 if block.name == 'pre' code_container = block elsif block.name == 'figure' && block.at_css('pre') code_container = block else next end # キャプションに code-caption クラスを付与(既存クラスは保持) existing_classes = caption['class'].to_s.split(/\s+/).reject(&:empty?) unless existing_classes.include?('code-caption') caption['class'] = (existing_classes + ['code-caption']).uniq.join(' ') end wrapper = Nokogiri::XML::Node.new('div', doc) wrapper['id'] = id wrapper['class'] = 'cross-ref-list' # wrapper を caption の直前に挿入し、caption とコードブロック要素を移動 caption.add_previous_sibling(wrapper) wrapper.add_child(caption) wrapper.add_child(code_container) # マーカーコメントは削除 comment.remove changed = true end # パターン2(後方互換): <p class="code-caption" data-xref-id> + <pre> doc.css('p.code-caption[data-xref-id]').each do |p| id = p['data-xref-id'].to_s next if id.empty? node = p.next_sibling pre = nil while node if node.element? pre = node if node.name == 'pre' break end node = node.next_sibling end next unless pre wrapper = Nokogiri::XML::Node.new('div', doc) wrapper['id'] = id wrapper['class'] = 'cross-ref-list' p.remove_attribute('data-xref-id') p.add_previous_sibling(wrapper) wrapper.add_child(p) wrapper.add_child(pre) changed = true end return unless changed HtmlParser.save_html_document(html_file, doc) Common.log_success("#{html_file}: cross-ref list code blocks wrapped") end
.wrap_img_text_blocks!(html_file) ⇒ Object
================================================================
img-text / text-img 系コンテナの正規化
VFM が出力する
テキスト………のような構造では、テキストノードや が独立したグリッドセルに なり、レイアウトが崩れる。figure 以外の子ノードをでまとめて包み、常にという 2 要素構造に正規化する。 ================================================================テキスト…… 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597
# File 'lib/vivlio_starter/cli/post_process.rb', line 558 def wrap_img_text_blocks!(html_file) content = File.read(html_file, encoding: 'utf-8') doc = HtmlParser.parse_html_document(content) changed = false selectors = %w[img-text img-text2 img-text3 text-img text2-img text3-img] css_selector = selectors.map { "div.#{it}" }.join(', ') doc.css(css_selector).each do |container| figure = container.at_css('> figure') next unless figure children = container.children body_nodes = children.reject do |node| node == figure || (node.text? && node.text.strip.empty?) end # 既に img-text-body でラップ済みならスキップ if body_nodes.size == 1 && body_nodes.first.element? && body_nodes.first['class'].to_s.include?('img-text-body') next end next if body_nodes.empty? body_wrapper = Nokogiri::XML::Node.new('div', doc) body_wrapper['class'] = 'img-text-body' first_body = body_nodes.first first_body.add_previous_sibling(body_wrapper) body_nodes.each { body_wrapper.add_child(it) } changed = true end return unless changed HtmlParser.save_html_document(html_file, doc) Common.log_success("#{html_file}: img-text コンテナを正規化しました") end
.wrap_sideimage_blocks!(html_file) ⇒ Object
================================================================
sideimage コンテナ内テキストのラップ
Vivliostyle/VFM が出力する
のような構造では、figure 以外がテキストノードとなり CSS Grid の .sideimage-right > :not(figure) セレクタでは拾えない。 そこで、figure 以外の子ノードを… 本文テキスト…で まとめて包み、常にという 2 要素構造に正規化する。 ================================================================… 本文…352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479
# File 'lib/vivlio_starter/cli/post_process.rb', line 352 def wrap_sideimage_blocks!(html_file) content = File.read(html_file, encoding: 'utf-8') doc = HtmlParser.parse_html_document(content) changed = false doc.css('div.sideimage-right, div.sideimage-left, div.sideimage').each do |container| figure = container.at_css('> figure') next unless figure # 画像幅指定(width=50% など)から列比率を推定し、CSS変数として埋め込む if (fraction = extract_sideimage_width_fraction(figure)) text_fr = (1.0 - fraction).round(3) img_fr = fraction.round(3) existing_style = container['style'].to_s # 既存の sideimage 用変数定義を一度取り除く cleaned_style = existing_style .gsub(/--sideimage-text-fr:[^;]+;?/, '') .gsub(/--sideimage-img-fr:[^;]+;?/, '') .strip var_style = "--sideimage-text-fr: #{text_fr}fr; --sideimage-img-fr: #{img_fr}fr" container['style'] = if cleaned_style.empty? var_style else "#{cleaned_style.chomp(';')}; #{var_style}" end normalize_sideimage_figure_width!(figure) changed = true end children = container.children body_nodes = children.reject do |node| node == figure || (node.text? && node.text.strip.empty?) end # すでに sideimage-body がひとつだけある場合は何もしない if body_nodes.size == 1 && body_nodes.first.element? && body_nodes.first['class'].to_s.split.include?('sideimage-body') next end next if body_nodes.empty? body_wrapper = Nokogiri::XML::Node.new('div', doc) body_wrapper['class'] = 'sideimage-body' first_body = body_nodes.first first_body.add_previous_sibling(body_wrapper) body_nodes.each do |node| body_wrapper.add_child(node) end changed = true end # sideimage-body 内に残っているバッククォート付きコードを <code> 要素に変換 doc.css('div.sideimage-body').each do |body| body.traverse do |node| next unless node.text? text = node.text next unless text.include?('`') segments = text.split(/(`[^`]*`)/) next if segments.length == 1 # 元のテキストノードの直前に、新しいノードを順番に挿入していく segments.each do |seg| if seg.start_with?('`') && seg.end_with?('`') && seg.length >= 2 inner = seg[1..-2] code = Nokogiri::XML::Node.new('code', doc) code.content = inner node.add_previous_sibling(code) else node.add_previous_sibling(Nokogiri::XML::Text.new(seg, doc)) end end # 役目を終えた元のテキストノードは削除する node.remove changed = true end # sideimage-body 内のマークダウンリンクを <a> タグに変換 # [テキスト](URL) → <a href="URL">テキスト</a> # 脚注参照 [^urlN] は一旦そのまま残す(後で process_sideimage_footnotes! で処理) body.traverse do |node| next unless node.text? text = node.text next unless text.match?(/\[.+?\]\(.+?\)/) # マークダウンリンクと脚注参照を分割(脚注参照は後で処理するので残す) pattern = /(\[[^\]]+\]\([^)]+\))/ segments = text.split(pattern) next if segments.length == 1 segments.each do |seg| if (m = seg.match(/^\[([^\]]+)\]\(([^)]+)\)$/)) # マークダウンリンク: [text](url) → <a>text</a> link_text = m[1] link_url = m[2] anchor = Nokogiri::XML::Node.new('a', doc) anchor['href'] = link_url anchor.content = link_text node.add_previous_sibling(anchor) else # 通常テキスト(脚注参照 [^urlN] も含む) node.add_previous_sibling(Nokogiri::XML::Text.new(seg, doc)) end end node.remove changed = true end end return unless changed HtmlParser.save_html_document(html_file, doc) Common.log_success("#{html_file}: sideimage コンテナを正規化しました") end
- .wrap_img_text_blocks!(html_file) ⇒ Object