Module: Arrolio::GenericAdapter::InlineRunCollection

Included in:
Arrolio::GenericAdapter
Defined in:
lib/arrolio/generic_adapter/inline_run_collection.rb

Overview

Extracted conversion seam (TODO 56): one module per concern, included into the adapter. The class keeps dispatch and glue.

Constant Summary collapse

UNICODE_SPACES =
Regexp.new("[\u00A0\u1680\u2000-\u200B\u202F\u205F\u3000\uFEFF]")

Instance Method Summary collapse

Instance Method Details

#collect_inline_runs(elem, default_style: :inline, exclude: nil, footnote_markers: false) ⇒ Object



9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/arrolio/generic_adapter/inline_run_collection.rb', line 9

def collect_inline_runs(elem, default_style: :inline, exclude: nil,
                        footnote_markers: false)
  runs = []
  stem_name = selector('stem')
  fn_name = footnote_markers ? selector('footnote_marker') : nil
  stem_formatted_name = selector('stem_formatted')
  math_name = selector('math')
  tab_name = selector('tab_inline')
  br_name = selector('break_inline')
  xref_name = selector('biblio_formattedref') ? 'fmt-xref' : nil
   = @rules['skip_metadata_elements'].to_a
  block_level = @rules['block_level_elements'].to_a
  inline_styles = @rules['inline_styles'] || {}

  walker = lambda do |node, style, baseline: Content::InlineRun::BASELINE_NORMAL, scale: 1.0, in_xref: false|
    case node
    when REXML::Text
      raw = normalize_text(node.value)
      next if raw.nil? || raw.empty?

      text = in_xref ? format_locality_text(raw) : raw
      runs << Content::InlineRun.new(text, style_id: style,
                                           baseline_shift: baseline,
                                           font_size_scale: scale)
    when REXML::Element
      next if exclude && node.name == exclude

      if fn_name && node.name == fn_name
        marker = node.attribute('reference')&.value ||
                 node.attribute('id')&.value
        unless marker.nil? || marker.empty?
          runs << Content::InlineRun.new(
            marker, style_id: style,
                    baseline_shift: Content::InlineRun::BASELINE_SUP,
                    font_size_scale: 0.7
          )
        end
        next
      end

      new_style = resolve_inline_style(node, style)
      sub_baseline, sub_scale = baseline_for_style(inline_styles, node,
                                                   baseline, scale)
      child_in_xref = in_xref || (xref_name && node.name == xref_name)
      case node.name
      when tab_name
        runs << Content::InlineRun.new("\t", style_id: style)
      when br_name
        runs << Content::InlineRun.new("\n", style_id: style)
      when stem_name
        unless sibling_exists?(node.parent, stem_formatted_name)
          walk_stem(node, style, runs, stem_formatted_name, math_name)
        end
      when stem_formatted_name
        walk_math(node, style, runs)
      when *
        next
      when *block_level
        next
      else
        node.children.each do |c|
 walker.call(c, new_style, baseline: sub_baseline, scale: sub_scale, in_xref: child_in_xref)
        end
      end
    end
  end
  elem.children.each { |c| walker.call(c, default_style) }
  runs
end

#format_locality_text(text) ⇒ Object



79
80
81
# File 'lib/arrolio/generic_adapter/inline_run_collection.rb', line 79

def format_locality_text(text)
  text.gsub(/([a-zA-Z]+)=/, '\\1 ')
end

#normalize_text(raw) ⇒ Object

Newline-bearing text nodes keep ONE space at each boundary: 'where\n' must render 'where Y' — stripping the trailing whitespace glued words to the following element and made long formula runs unbreakable (clipped at the page edge).



122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/arrolio/generic_adapter/inline_run_collection.rb', line 122

def normalize_text(raw)
  return raw unless raw.is_a?(String)
  cleaned = raw.gsub(UNICODE_SPACES, ' ')
  return cleaned unless cleaned.include?("\n")

  stripped = cleaned.strip
  return cleaned.match?(/\s/) ? ' ' : nil if stripped.empty?

  lead = cleaned.match?(/\A\s/) ? ' ' : ''
  trail = cleaned.match?(/\s\z/) ? ' ' : ''
  lead + stripped.gsub(/\s+/, ' ') + trail
end

#serialize_element_to_xml(elem) ⇒ Object



111
112
113
114
115
# File 'lib/arrolio/generic_adapter/inline_run_collection.rb', line 111

def serialize_element_to_xml(elem)
  io = StringIO.new(+'')
  REXML::Formatters::Default.new.write(elem, io)
  io.string
end

#walk_math(elem, style, runs) ⇒ Object

Walks a MathML tree emitting InlineRuns with appropriate baseline_shift + font_size_scale. Delegates the actual MathML parsing to the plurimath/mml gem via MathML::InlineRunExtractor, keeping this adapter free of its own MathML grammar.



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/arrolio/generic_adapter/inline_run_collection.rb', line 92

def walk_math(elem, style, runs)
  return unless elem

  math_name = selector('math')
  math_elem = if elem.name == math_name
    elem
  else
    found = nil
    elem.each_recursive { |e| found = e if e.name == math_name && found.nil? }
    found
  end
  return unless math_elem

  math_xml = serialize_element_to_xml(math_elem)
  extracted = ::Arrolio::MathML::InlineRunExtractor.extract_from_xml(math_xml,
                                                                     base_style: style)
  runs.concat(extracted)
end

#walk_stem(stem_elem, style, runs, formatted_name, math_name) ⇒ Object



83
84
85
86
# File 'lib/arrolio/generic_adapter/inline_run_collection.rb', line 83

def walk_stem(stem_elem, style, runs, formatted_name, math_name)
  target = find_first(stem_elem, formatted_name) || find_first(stem_elem, math_name)
  walk_math(target, style, runs) if target
end