Module: VivlioStarter::CLI::PostProcessCommands::PageBreakNormalizer

Defined in:
lib/vivlio_starter/cli/post_process/page_break_normalizer.rb

Overview

二重改ページを正規化するモジュール。

Constant Summary collapse

BREAK_MARKERS =

改ページを引き起こす著者由来のマーカー要素。

'hr.pagebreak, div.vs-break-page, div.vs-break-recto, div.vs-break-verso'
MERGED_CLASS =

recto/verso 指定へ合流した h2 に付ける印(CSS 側で改ページを打ち消す)。

'vs-break-merged'
WRAPPER_ELEMENTS =

走査中に透過するラッパー要素(この中へ降りて最初の内容要素を探す)。

%w[section article div].freeze

Class Method Summary collapse

Class Method Details

.add_class(element, name) ⇒ Object



162
163
164
165
166
167
# File 'lib/vivlio_starter/cli/post_process/page_break_normalizer.rb', line 162

def add_class(element, name)
  classes = element['class'].to_s.split
  return if classes.include?(name)

  element['class'] = (classes + [name]).join(' ')
end

.breaking_h2_scope(doc) ⇒ Object

CSS で改ページする h2 の範囲を返す。正規化は「h2 が改ページする」ことが 前提なので、この範囲の外にあるマーカーは著者の意図どおり残す。

:all                 … 既定(section_pagebreak: true)。全 h2 が改ページする
:first_section_only  … false かつ image スタイル。章扉を全面の扉絵で成立させる
                     ため章の最初の節だけ改ページが残る
                     (BookSettingsCss#chapter_frontispiece_guard_rule と対)
:none                … false かつ simple スタイル。どの h2 も改ページしない

image スタイルの判定は config ではなく body クラスで行う——付録は theme.style: image でも常に vs-header-simple なので、CSS が実際に見るものと 同じ印を見るのが唯一ずれない方法(BodyClassInjector#header_mode_class)。



78
79
80
81
82
83
# File 'lib/vivlio_starter/cli/post_process/page_break_normalizer.rb', line 78

def breaking_h2_scope(doc)
  return :all if section_pagebreak_enabled?

  body_classes = doc.at_css('body')&.[]('class').to_s.split
  body_classes.include?('vs-header-image') ? :first_section_only : :none
end

.descend_to_content(element) ⇒ Object

ラッパー(section / article / div)なら、その中の最初の内容要素まで降りる。 空のラッパーはそれ自体を内容要素として返す(h2 ではないので正規化されない)。



140
141
142
143
144
145
146
147
148
149
# File 'lib/vivlio_starter/cli/post_process/page_break_normalizer.rb', line 140

def descend_to_content(element)
  node = element
  while WRAPPER_ELEMENTS.include?(node.name)
    child = first_content_child(node)
    return node unless child

    node = child
  end
  node
end

.first_content_child(node) ⇒ Object

空白テキスト・コメントを飛ばして最初の子要素を返す。



152
153
154
155
# File 'lib/vivlio_starter/cli/post_process/page_break_normalizer.rb', line 152

def first_content_child(node)
  child = node.children.find { it.element? || (it.text? && !it.text.strip.empty?) }
  child&.element? ? child : nil
end

.first_section_of_chapter?(h2) ⇒ Boolean

章(section.level1)の最初の節かどうか。章扉保護の CSS セレクタ section.level2:first-of-type と同じ判定を DOM 側で再現する。

Returns:

  • (Boolean)


87
88
89
90
91
92
# File 'lib/vivlio_starter/cli/post_process/page_break_normalizer.rb', line 87

def first_section_of_chapter?(h2)
  level2 = h2.ancestors('section.level2').first
  return false unless level2

  level2.parent.element_children.find { it.name == 'section' }.equal?(level2)
end

.next_content_element(node) ⇒ Object

文書順で次に現れる「内容要素」を返す。 次の兄弟が無ければ親を遡り、見つけた要素がラッパーならその中へ降りる。



114
115
116
117
118
119
120
121
122
123
124
# File 'lib/vivlio_starter/cli/post_process/page_break_normalizer.rb', line 114

def next_content_element(node)
  current = node
  while current
    sibling = next_element_sibling(current)
    return descend_to_content(sibling) if sibling

    current = current.parent
    return nil unless current&.element?
  end
  nil
end

.next_element_sibling(node) ⇒ Object

空白テキスト・コメントを飛ばして次の兄弟要素を返す。



127
128
129
130
131
132
133
134
135
136
# File 'lib/vivlio_starter/cli/post_process/page_break_normalizer.rb', line 127

def next_element_sibling(node)
  sibling = node.next_sibling
  while sibling
    return sibling if sibling.element?
    return nil if sibling.text? && !sibling.text.strip.empty?

    sibling = sibling.next_sibling
  end
  nil
end

.normalize!(html_file) ⇒ Integer

二重改ページを正規化してファイルへ書き戻す。

Parameters:

  • html_file (String)

    対象 HTML のパス

Returns:

  • (Integer)

    正規化した件数



53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/vivlio_starter/cli/post_process/page_break_normalizer.rb', line 53

def normalize!(html_file)
  doc = HtmlParser.parse_html_document(File.read(html_file, encoding: 'utf-8'))
  scope = breaking_h2_scope(doc)
  return 0 if scope == :none

  count = doc.css(BREAK_MARKERS).count { normalize_marker(it, scope) }
  return 0 if count.zero?

  HtmlParser.save_html_document(html_file, doc)
  Common.log_info("#{html_file}: 冗長な改ページを #{count} 件正規化しました")
  count
end

.normalize_marker(marker, scope = :all) ⇒ Boolean

マーカー 1 つを見て、直後が h2 なら正規化する。

Parameters:

  • scope (Symbol) (defaults to: :all)

    breaking_h2_scope の戻り値

Returns:

  • (Boolean)

    正規化したか



97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/vivlio_starter/cli/post_process/page_break_normalizer.rb', line 97

def normalize_marker(marker, scope = :all)
  following = next_content_element(marker)
  return false unless following&.name == 'h2'
  return false if scope == :first_section_only && !first_section_of_chapter?(following)

  if recto_or_verso?(marker)
    # recto/verso 指定が勝つ。マーカーは残し、h2 側の改ページだけを無効化する。
    add_class(following, MERGED_CLASS)
  else
    # 単純改ページは h2 自身の改ページと重複するので、マーカーごと落とす。
    marker.remove
  end
  true
end

.recto_or_verso?(marker) ⇒ Boolean

Returns:

  • (Boolean)


157
158
159
160
# File 'lib/vivlio_starter/cli/post_process/page_break_normalizer.rb', line 157

def recto_or_verso?(marker)
  classes = marker['class'].to_s.split
  classes.include?('vs-break-recto') || classes.include?('vs-break-verso')
end

.section_pagebreak_enabled?Boolean

節(h2)で改ページする設定かどうか。BookSettingsCss と同じ判定 (明示的に false と書かれたときだけ無効)を用いる。

Returns:

  • (Boolean)


171
172
173
174
175
176
177
178
# File 'lib/vivlio_starter/cli/post_process/page_break_normalizer.rb', line 171

def section_pagebreak_enabled?
  return true unless Common.configured?

  case Common::CONFIG.page.section_pagebreak.to_s.strip.downcase
  in 'false' | 'no' | 'off' | '0' then false
  else true
  end
end