Module: VivlioStarter::CLI::PostProcessCommands::FootnoteConverter
- Defined in:
- lib/vivlio_starter/cli/post_process/footnote_converter.rb
Overview
================================================================
Module: FootnoteConverter
【役割】
- 章末脚注(endnotes)をページ脚注(page footnotes)に変換
【処理の流れ】
- section.footnotes 内の
- を収集
- 戻りリンク/空段落を除去した内側HTMLを定義として保持
- footnotes セクションを削除
- 本文の参照アンカー直後に脚注を挿入
- 画面用:
- 印刷用:
- 未使用の定義は 末尾に追加
- 定義のない参照は前方リンクから推測して補完
================================================================
Class Method Summary collapse
-
.adjust_following_whitespace(node) ⇒ Object
インライン脚注後の空白をノーブレークスペースへ変換する.
-
.append_unused_footnotes_to_body!(doc, definitions) ⇒ Object
残った脚注定義を本文末尾の aside として追加する 未使用の定義は sideimage 内の脚注(footnote-anchor 経由)であり、 後段の process_sideimage_footnotes! が参照を生成した後、 move_body_asides_near_references! が参照の近くへ移動する.
-
.bare_definition_url(body) ⇒ String?
「URL」だけで構成された定義から URL を取り出す。 自動生成されたURL脚注の判定に使い、それ以外(手書きの脚注本文)は nil を返して修復対象から除外する。VFM の設定によっては定義が
で包まれることがあるため、単一の
ラッパーは許容する。.
-
.build_inline_footnote_node(doc, fid, body) ⇒ Nokogiri::XML::Element
インライン脚注用の span ノードを生成する.
-
.build_print_footnote_node(doc, fid, body, anchored: false) ⇒ Nokogiri::XML::Element
印刷用脚注の aside ノードを生成する.
-
.convert_endnotes_to_page_footnotes!(html) ⇒ String
章末脚注をページ脚注へ変換.
-
.extract_footnote_definitions(footnotes_section) ⇒ Hash<String, String>
section.footnotes 内の脚注定義を id => HTML として抽出する.
-
.fill_missing_footnote_references!(doc) ⇒ Object
定義が存在しない脚注参照を前方リンクから推測して補完する.
- .find_last_print_footnote_sibling(node) ⇒ Object
-
.find_sideimage_container(node) ⇒ Nokogiri::XML::Element?
sideimage のトップレベルコンテナ(div.sideimage-right 等)を探す sideimage-body は除外し、sideimage / sideimage-right / sideimage-left のみ対象.
-
.inferred_body_from_previous_link(anchor) ⇒ String?
脚注参照直前のリンク要素から本文 HTML を推定する。 内部リンク(#fn...
-
.insert_footnote_for_anchor!(doc, anchor, fid, body) ⇒ Object
アンカー位置に応じてインライン/印刷脚注を挿入する.
-
.insert_footnotes_for_references!(doc, definitions) ⇒ Object
本文中の脚注参照アンカーへ定義を差し込む.
-
.insert_inline_footnote!(doc, anchor, fid, body) ⇒ Object
インライン脚注 span を参照アンカー直後に挿入する.
-
.insert_print_footnote_after_anchor!(doc, anchor, fid, body) ⇒ Object
段落外参照(テーブルセル内など)の場合、参照直後に隠しインライン脚注と 印刷用脚注を配置する。 参照リンク(href="#fnN")の解決先が aside 自体になると Vivliostyle が 同じ脚注を複数回描画するため、必ず手前に span#fnN を置いて解決先にする。 sideimage コンテナ内の場合、aside はコンテナの直後に配置する。.
-
.insert_print_footnote_after_paragraph!(doc, anchor, fid, body) ⇒ Object
段落内参照の場合、段落直後に印刷用脚注 aside を差し込む sideimage コンテナ内の場合はコンテナの直後に配置する (aside を sideimage 内に置くと CSS Grid レイアウトが壊れるため。 参照リンクの解決先は insert_inline_footnote! が挿入する span#fnN に なるので、float:footnote でも重複描画は起きない).
-
.normalize_definition_ids!(doc, definitions) ⇒ Object
footnote-anchor span 内の参照を使って、definitions のキーを VFM が割り当てた実際のIDに正規化する 例: section.footnotes 内の fnurl3 は、footnote-anchor span 内の に対応するため、fnurl3 → fn4 に変換する.
- .print_footnote_related_node?(node) ⇒ Boolean
-
.repair_table_footnote_definitions!(doc, definitions) ⇒ Object
VFM 2.x(remark-footnotes)は、テーブルセル内から参照される脚注について 参照ID(fnref→fn の対応)は正しいまま、定義の「本文」だけを参照と 逆順に並べ替えてしまう(例: fn1 の本文に fn3 の URL が入る)。 自動生成されたURL脚注は「参照直前の外部リンク」が本来の本文なので、 テーブル内の各参照について定義本文を照合し、入れ替わっていれば修復する。 誤爆を防ぐため、(1) 定義本文が「URLそのものへのリンク」だけで構成され、 (2) 期待されるURLが他の定義に実在する(=入れ替わりの証拠がある) 場合のみ書き換える。.
Class Method Details
.adjust_following_whitespace(node) ⇒ Object
インライン脚注後の空白をノーブレークスペースへ変換する
366 367 368 369 370 371 372 |
# File 'lib/vivlio_starter/cli/post_process/footnote_converter.rb', line 366 def adjust_following_whitespace(node) following = node.next_sibling return unless following&.text? text = following.text following.content = " #{text.lstrip}" if text.start_with?(' ') end |
.append_unused_footnotes_to_body!(doc, definitions) ⇒ Object
残った脚注定義を本文末尾の aside として追加する 未使用の定義は sideimage 内の脚注(footnote-anchor 経由)であり、 後段の process_sideimage_footnotes! が参照を生成した後、 move_body_asides_near_references! が参照の近くへ移動する
199 200 201 202 203 204 205 206 207 208 |
# File 'lib/vivlio_starter/cli/post_process/footnote_converter.rb', line 199 def append_unused_footnotes_to_body!(doc, definitions) return if definitions.empty? body_el = doc.at_css('body') || doc definitions.each do |fid, body| aside = build_print_footnote_node(doc, fid, body) body_el.add_child("\n") body_el.add_child(aside) end end |
.bare_definition_url(body) ⇒ String?
「URL」だけで構成された定義から URL を取り出す。 自動生成されたURL脚注の判定に使い、それ以外(手書きの脚注本文)は nil を返して修復対象から除外する。VFM の設定によっては定義が
で包まれることがあるため、単一の
ラッパーは許容する。
278 279 280 281 282 283 284 285 286 287 |
# File 'lib/vivlio_starter/cli/post_process/footnote_converter.rb', line 278 def (body) html = body.to_s.strip if (wrapped = html.match(%r{\A<p>(.*)</p>\z}m)) html = wrapped[1].strip end m = html.match(%r{\A<a href="(https?://[^"]+)"[^>]*>([^<]+)</a>\z}) return nil unless m && m[1] == m[2] m[1] end |
.build_inline_footnote_node(doc, fid, body) ⇒ Nokogiri::XML::Element
インライン脚注用の span ノードを生成する
331 332 333 334 335 336 337 338 339 |
# File 'lib/vivlio_starter/cli/post_process/footnote_converter.rb', line 331 def build_inline_footnote_node(doc, fid, body) span = Nokogiri::XML::Node.new('span', doc) span['role'] = 'doc-footnote' span['class'] = 'page-footnote page-footnote-inline' span['id'] = fid span['data-footnote-number'] = fid.sub(/^fn/, '') span.inner_html = body span end |
.build_print_footnote_node(doc, fid, body, anchored: false) ⇒ Nokogiri::XML::Element
印刷用脚注の aside ノードを生成する
347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 |
# File 'lib/vivlio_starter/cli/post_process/footnote_converter.rb', line 347 def build_print_footnote_node(doc, fid, body, anchored: false) aside = Nokogiri::XML::Node.new('aside', doc) aside['role'] = 'doc-footnote' aside['class'] = 'page-footnote page-footnote-print' aside['id'] = fid # IDから脚注番号を抽出(例: fn5 -> 5, fnurl1 -> url1) footnote_number = fid.sub(/^fn/, '') aside['data-footnote-number'] = footnote_number # 参照位置に同内容の span#fnN があるなら、脚注フロートはそちらが担う。 # この aside は控えなので PDF では描画しない(CSS が属性で拾う。クラスでなく # data 属性にするのは img[data-vs-raster] と同じフック方式に揃えるため)。 # DOM に残すのは後処理(sideimage 変換・出現順の再番号付け)が aside を辿るため。 aside['data-footnote-anchored'] = '1' if anchored aside.inner_html = body aside end |
.convert_endnotes_to_page_footnotes!(html) ⇒ String
章末脚注をページ脚注へ変換
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
# File 'lib/vivlio_starter/cli/post_process/footnote_converter.rb', line 31 def convert_endnotes_to_page_footnotes!(html) doc = HtmlParser.parse_html_document(html) footnotes = doc.at_css('section.footnotes') return html unless footnotes definitions = extract_footnote_definitions(footnotes) footnotes.remove # footnote-anchor span 内の参照を使って、VFM が割り当てた実際のIDに # definitions のキーを正規化する(例: fn-url3 → fn4) normalize_definition_ids!(doc, definitions) # VFM 2.x はテーブルセル内の脚注について定義本文を逆順に入れ替えるため、 # 参照直前のリンクURLと照合して修復する repair_table_footnote_definitions!(doc, definitions) insert_footnotes_for_references!(doc, definitions) append_unused_footnotes_to_body!(doc, definitions) fill_missing_footnote_references!(doc) HtmlParser.render_html_document(doc) end |
.extract_footnote_definitions(footnotes_section) ⇒ Hash<String, String>
section.footnotes 内の脚注定義を id => HTML として抽出する
57 58 59 60 61 62 63 64 65 66 67 |
# File 'lib/vivlio_starter/cli/post_process/footnote_converter.rb', line 57 def extract_footnote_definitions(footnotes_section) footnotes_section.css('li[id]').each_with_object({}) do |li, memo| fid = li['id'] cleaned = li.dup # 戻りリンクを削除 cleaned.css('a.footnote-back, a.footnote-backref').each(&:remove) # 空段落を削除 cleaned.css('p').select { |p| p.text.strip.empty? }.each(&:remove) memo[fid] = cleaned.children.map(&:to_html).join.strip end end |
.fill_missing_footnote_references!(doc) ⇒ Object
定義が存在しない脚注参照を前方リンクから推測して補完する
291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 |
# File 'lib/vivlio_starter/cli/post_process/footnote_converter.rb', line 291 def fill_missing_footnote_references!(doc) doc.css('a.footnote-ref[href^="#fn"]').each do |anchor| # expose_container_footnotes! が生成した非表示参照はスキップする next if anchor.ancestors('span.footnote-anchor').any? fid = anchor['href']&.delete_prefix('#') next unless fid next if doc.at_css(%(##{fid})) body = inferred_body_from_previous_link(anchor) next unless body if anchor.ancestors('p').any? insert_inline_footnote!(doc, anchor, fid, body) insert_print_footnote_after_paragraph!(doc, anchor, fid, body) else insert_print_footnote_after_anchor!(doc, anchor, fid, body) end end end |
.find_last_print_footnote_sibling(node) ⇒ Object
142 143 144 145 146 147 148 149 150 |
# File 'lib/vivlio_starter/cli/post_process/footnote_converter.rb', line 142 def find_last_print_footnote_sibling(node) current = node while (sibling = current.next_sibling) break unless (sibling) current = sibling end current end |
.find_sideimage_container(node) ⇒ Nokogiri::XML::Element?
sideimage のトップレベルコンテナ(div.sideimage-right 等)を探す sideimage-body は除外し、sideimage / sideimage-right / sideimage-left のみ対象
186 187 188 189 190 191 |
# File 'lib/vivlio_starter/cli/post_process/footnote_converter.rb', line 186 def find_sideimage_container(node) node.ancestors('div').find do |d| classes = d['class'].to_s.split (classes & %w[sideimage sideimage-right sideimage-left]).any? end end |
.inferred_body_from_previous_link(anchor) ⇒ String?
脚注参照直前のリンク要素から本文 HTML を推定する。 内部リンク(#fn... など)は脚注本文として不適切なため除外する。
316 317 318 319 320 321 322 323 324 |
# File 'lib/vivlio_starter/cli/post_process/footnote_converter.rb', line 316 def inferred_body_from_previous_link(anchor) prev_link = anchor.previous_element prev_link = prev_link.previous_element while prev_link && prev_link.name != 'a' url = prev_link&.[]('href') # http(s):// で始まる外部URLのみを対象とし、内部リンク(#fn...)は除外する return unless url&.match?(/\Ahttps?:\/\//) %(<a href="#{url}">#{url}</a>) end |
.insert_footnote_for_anchor!(doc, anchor, fid, body) ⇒ Object
アンカー位置に応じてインライン/印刷脚注を挿入する
94 95 96 97 98 99 100 101 102 103 |
# File 'lib/vivlio_starter/cli/post_process/footnote_converter.rb', line 94 def insert_footnote_for_anchor!(doc, anchor, fid, body) if anchor.ancestors('p').any? # 段落内の参照の場合 insert_inline_footnote!(doc, anchor, fid, body) insert_print_footnote_after_paragraph!(doc, anchor, fid, body) else # 段落外の参照の場合 insert_print_footnote_after_anchor!(doc, anchor, fid, body) end end |
.insert_footnotes_for_references!(doc, definitions) ⇒ Object
本文中の脚注参照アンカーへ定義を差し込む
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
# File 'lib/vivlio_starter/cli/post_process/footnote_converter.rb', line 72 def insert_footnotes_for_references!(doc, definitions) # DOM順序で脚注参照を処理するため、本文中の全参照アンカーを取得 # expose_container_footnotes! が生成した非表示の footnote-anchor span 内の参照は # process_sideimage_footnotes! が別途処理するためスキップする doc.css('a.footnote-ref[href^="#fn"]').each do |anchor| next if anchor.ancestors('span.footnote-anchor').any? fid = anchor['href']&.delete_prefix('#') next unless fid next unless definitions.key?(fid) body = definitions[fid] insert_footnote_for_anchor!(doc, anchor, fid, body) definitions.delete(fid) end end |
.insert_inline_footnote!(doc, anchor, fid, body) ⇒ Object
インライン脚注 span を参照アンカー直後に挿入する
110 111 112 113 114 |
# File 'lib/vivlio_starter/cli/post_process/footnote_converter.rb', line 110 def insert_inline_footnote!(doc, anchor, fid, body) span = build_inline_footnote_node(doc, fid, body) anchor.add_next_sibling(span) adjust_following_whitespace(span) end |
.insert_print_footnote_after_anchor!(doc, anchor, fid, body) ⇒ Object
段落外参照(テーブルセル内など)の場合、参照直後に隠しインライン脚注と 印刷用脚注を配置する。 参照リンク(href="#fnN")の解決先が aside 自体になると Vivliostyle が 同じ脚注を複数回描画するため、必ず手前に span#fnN を置いて解決先にする。 sideimage コンテナ内の場合、aside はコンテナの直後に配置する。
168 169 170 171 172 173 174 175 176 177 178 179 180 |
# File 'lib/vivlio_starter/cli/post_process/footnote_converter.rb', line 168 def insert_print_footnote_after_anchor!(doc, anchor, fid, body) span = build_inline_footnote_node(doc, fid, body) anchor.add_next_sibling(span) aside = build_print_footnote_node(doc, fid, body, anchored: true) sideimage = find_sideimage_container(anchor) if sideimage sideimage.add_next_sibling(aside) else span.add_next_sibling(aside) end aside.add_next_sibling("\n") end |
.insert_print_footnote_after_paragraph!(doc, anchor, fid, body) ⇒ Object
段落内参照の場合、段落直後に印刷用脚注 aside を差し込む sideimage コンテナ内の場合はコンテナの直後に配置する (aside を sideimage 内に置くと CSS Grid レイアウトが壊れるため。 参照リンクの解決先は insert_inline_footnote! が挿入する span#fnN に なるので、float:footnote でも重複描画は起きない)
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
# File 'lib/vivlio_starter/cli/post_process/footnote_converter.rb', line 125 def insert_print_footnote_after_paragraph!(doc, anchor, fid, body) sideimage = find_sideimage_container(anchor) aside = build_print_footnote_node(doc, fid, body, anchored: true) if sideimage sideimage.add_next_sibling(aside) else para = anchor.ancestors('p').first if para insertion_point = find_last_print_footnote_sibling(para) insertion_point.add_next_sibling(aside) else anchor.add_next_sibling(aside) end end aside.add_next_sibling("\n") end |
.normalize_definition_ids!(doc, definitions) ⇒ Object
footnote-anchor span 内の参照を使って、definitions のキーを VFM が割り当てた実際のIDに正規化する 例: section.footnotes 内の fnurl3 は、footnote-anchor span 内の に対応するため、fnurl3 → fn4 に変換する
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 |
# File 'lib/vivlio_starter/cli/post_process/footnote_converter.rb', line 216 def normalize_definition_ids!(doc, definitions) # footnote-anchor span 内の参照を DOM 順で収集 anchor_refs = doc.css('span.footnote-anchor a.footnote-ref[href^="#fn"]') .map { |a| a['href']&.delete_prefix('#') } .compact return if anchor_refs.empty? # footnote-anchor span 内の参照に対応する定義IDのみを対象とする # (fn-urlN または fnurlN 形式のキーのみ) url_keys = definitions.keys.select { |k| k.match?(/\Afn-?url\d+\z/) } return if url_keys.empty? return if url_keys.size != anchor_refs.size url_keys.zip(anchor_refs).each do |old_id, new_id| next if old_id == new_id || new_id.nil? definitions[new_id] = definitions.delete(old_id) end end |
.print_footnote_related_node?(node) ⇒ Boolean
152 153 154 155 156 157 |
# File 'lib/vivlio_starter/cli/post_process/footnote_converter.rb', line 152 def (node) return false unless node (node.text? && node.text.strip.empty?) || (node.element? && node['class'].to_s.split.any? { |c| c == 'page-footnote' }) end |
.repair_table_footnote_definitions!(doc, definitions) ⇒ Object
VFM 2.x(remark-footnotes)は、テーブルセル内から参照される脚注について 参照ID(fnref→fn の対応)は正しいまま、定義の「本文」だけを参照と 逆順に並べ替えてしまう(例: fn1 の本文に fn3 の URL が入る)。 自動生成されたURL脚注は「参照直前の外部リンク」が本来の本文なので、 テーブル内の各参照について定義本文を照合し、入れ替わっていれば修復する。 誤爆を防ぐため、(1) 定義本文が「URLそのものへのリンク」だけで構成され、 (2) 期待されるURLが他の定義に実在する(=入れ替わりの証拠がある) 場合のみ書き換える。
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 |
# File 'lib/vivlio_starter/cli/post_process/footnote_converter.rb', line 246 def repair_table_footnote_definitions!(doc, definitions) known_urls = definitions.values.filter_map { (it) } repaired = {} doc.css('table a.footnote-ref[href^="#fn"]').each do |anchor| next if anchor.ancestors('span.footnote-anchor').any? fid = anchor['href']&.delete_prefix('#') next unless fid && definitions.key?(fid) next if repaired.key?(fid) expected = inferred_body_from_previous_link(anchor) next unless expected expected_url = (expected) current_url = (definitions[fid]) next unless expected_url && current_url next if current_url == expected_url next unless known_urls.include?(expected_url) repaired[fid] = expected end definitions.merge!(repaired) end |