Module: Idml::Render::Footnote

Defined in:
lib/idml/render/footnote.rb

Overview

Footnote semantics: story-scoped numbering, body-text marker synthesis, footnote-paragraph extraction, and the geometry of the bottom-of-frame footnote area (height reservation + separator rule). Emission of footnote text lines stays in TextFrameRenderer — it owns all canvas text drawing — while this module owns everything footnote-specific (MECE).

Numbering is sequential per story, honoring FootnoteOption's StartAt / Prefix / Suffix when the package declares them. Document-wide continuous numbering and per-section restart are not modeled.

Defined Under Namespace

Classes: Counter, Entry, PositionedRun

Constant Summary collapse

DEFAULT_RULE_WEIGHT =
0.5
DEFAULT_RULE_GAP =
3.0
MARKER_TEXT_SEPARATOR =
" "

Class Method Summary collapse

Class Method Details

.counter_for(option) ⇒ Object



51
52
53
# File 'lib/idml/render/footnote.rb', line 51

def self.counter_for(option)
  Counter.new(option&.start_at)
end

.emit_separator(canvas, frame, y, option) ⇒ Object

Emits the separator rule at the top of the footnote area. Honors FootnoteOption RuleOn / RuleLineWeight / RuleWidth / RuleLeftIndent; defaults approximate InDesign (0.5pt rule spanning the column).



190
191
192
193
194
195
196
197
198
# File 'lib/idml/render/footnote.rb', line 190

def self.emit_separator(canvas, frame, y, option)
  return if option&.rule_on == false

  thickness = separator_weight(option)
  return unless thickness.positive?

  stroke_rule(canvas, separator_left(frame, option), y,
              separator_width(frame, option), thickness)
end

.extract(element, number, condition_filter: nil, style_lookup: nil, option: nil) ⇒ Object

Extracts the footnote's paragraphs via StyleResolver and prefixes the first paragraph's first run with the marker text ("1 ", "n2." …) so the footnote reads as numbered.



80
81
82
83
84
85
86
87
88
89
# File 'lib/idml/render/footnote.rb', line 80

def self.extract(element, number, condition_filter: nil,
                 style_lookup: nil, option: nil)
  paragraphs = StyleResolver.extract_container_paragraphs(
    element, condition_filter: condition_filter,
             style_lookup: style_lookup
  )
  marker = marker_text(number, option)
  prepend_marker(paragraphs, marker) unless marker.empty?
  paragraphs
end

.layout_entries(entries, frame, font, top_y, option = nil) ⇒ Object

Lays out all entries' paragraphs top-down starting at top_y. Returns [positioned_runs, bottom_y] where bottom_y is the cursor after the last line — the same walk drives both height measurement and rendering, so the reserved area always matches what gets drawn.



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/idml/render/footnote.rb', line 105

def self.layout_entries(entries, frame, font, top_y, option = nil)
  positioned = []
  cursor = top_y
  wrap_width = TextEngine::VerticalLayout.wrap_width(frame)

  entries.each_with_index do |entry, index|
    cursor -= (option&.space_between || 0).to_f if index.positive?
    entry.paragraphs.each do |paragraph|
      cursor = layout_paragraph(
        paragraph, frame, font, wrap_width, cursor, positioned
      )
    end
  end
  [positioned, cursor]
end

.marker_run(number, base, paragraphs, option) ⇒ Object

Builds the superscript marker run emitted into the body text where the footnote anchors. Inherits the base run's character styling so the marker matches its context; when the owning CSR has no text run, falls back to defaults.



65
66
67
68
69
70
71
72
73
74
75
# File 'lib/idml/render/footnote.rb', line 65

def self.marker_run(number, base, paragraphs, option)
  marker = base || StyleResolver::StyledRun.new(
    point_size: StyleResolver::DEFAULT_POINT_SIZE,
  )
  marker = marker.dup
  marker.text = marker_text(number, option)
  marker.position = "Superscript"
  marker.footnote_number = number
  marker.footnote_paragraphs = paragraphs
  marker
end

.marker_text(number, option) ⇒ Object

Marker text shown in the body text (and prefixed to the footnote's first paragraph): Prefix + number + Suffix.



57
58
59
# File 'lib/idml/render/footnote.rb', line 57

def self.marker_text(number, option)
  "#{option&.prefix}#{number}#{option&.suffix}"
end

.option(package) ⇒ Object

Returns the package's FootnoteOption (Preferences.xml), or nil when the package doesn't declare one.



46
47
48
49
# File 'lib/idml/render/footnote.rb', line 46

def self.option(package)
  preferences = package&.preferences
  preferences&.footnote_option&.first
end

.reserved_height(entries, font, frame, option = nil) ⇒ Object

Height to reserve at the frame bottom for the entries: separator gap + the measured paragraph stack.



171
172
173
174
175
176
177
178
# File 'lib/idml/render/footnote.rb', line 171

def self.reserved_height(entries, font, frame, option = nil)
  return 0.0 if entries.empty?

  content_bottom = TextEngine::VerticalLayout.bottom_limit(frame)
  _, bottom_y = layout_entries(entries, frame, font, content_bottom,
                               option)
  content_bottom - bottom_y + rule_gap(option)
end

.rule_gap(option) ⇒ Object

Vertical distance between the footnote area's top edge and the first footnote line — room for the separator rule.



182
183
184
# File 'lib/idml/render/footnote.rb', line 182

def self.rule_gap(option)
  option&.spacer || DEFAULT_RULE_GAP
end