coradoc-adoc 2.0.23 → 2.0.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/lib/coradoc/asciidoc/builder/detection.rb +4 -4
  3. data/lib/coradoc/asciidoc/model/attribute_list/matchers.rb +4 -4
  4. data/lib/coradoc/asciidoc/model/base.rb +7 -0
  5. data/lib/coradoc/asciidoc/model/block/core.rb +1 -1
  6. data/lib/coradoc/asciidoc/model/content_list.rb +8 -8
  7. data/lib/coradoc/asciidoc/model/inline/bold.rb +1 -1
  8. data/lib/coradoc/asciidoc/model/inline/highlight.rb +1 -1
  9. data/lib/coradoc/asciidoc/model/inline/italic.rb +1 -1
  10. data/lib/coradoc/asciidoc/model/inline/monospace.rb +1 -1
  11. data/lib/coradoc/asciidoc/model/inline/strikethrough.rb +2 -2
  12. data/lib/coradoc/asciidoc/model/inline/subscript.rb +1 -1
  13. data/lib/coradoc/asciidoc/model/inline/superscript.rb +1 -1
  14. data/lib/coradoc/asciidoc/model/serialization/asciidoc_mapping.rb +3 -4
  15. data/lib/coradoc/asciidoc/model/text_element.rb +22 -0
  16. data/lib/coradoc/asciidoc/parser/block.rb +3 -3
  17. data/lib/coradoc/asciidoc/transform/element_transformers/admonition_styles.rb +6 -1
  18. data/lib/coradoc/asciidoc/transform/element_transformers/block_transformer.rb +39 -16
  19. data/lib/coradoc/asciidoc/transform/element_transformers/document_transformer.rb +2 -27
  20. data/lib/coradoc/asciidoc/transform/element_transformers/include_transformer.rb +2 -1
  21. data/lib/coradoc/asciidoc/transform/element_transformers/inline_transformer.rb +12 -6
  22. data/lib/coradoc/asciidoc/transform/element_transformers/list_transformer.rb +10 -4
  23. data/lib/coradoc/asciidoc/transform/element_transformers/other_transformer.rb +10 -5
  24. data/lib/coradoc/asciidoc/transform/element_transformers/table_transformer.rb +6 -3
  25. data/lib/coradoc/asciidoc/transform/from_core_model.rb +6 -30
  26. data/lib/coradoc/asciidoc/transform/inline_transform_visitor.rb +28 -5
  27. data/lib/coradoc/asciidoc/transform/to_core_model.rb +30 -6
  28. data/lib/coradoc/asciidoc/transformer/block_rules.rb +8 -2
  29. data/lib/coradoc/asciidoc/transformer/block_type_classifier.rb +10 -10
  30. data/lib/coradoc/asciidoc/transformer/header_rules.rb +9 -3
  31. data/lib/coradoc/asciidoc/transformer/inline_rules.rb +39 -15
  32. data/lib/coradoc/asciidoc/transformer/list_rules.rb +14 -4
  33. data/lib/coradoc/asciidoc/transformer/misc_rules.rb +14 -5
  34. data/lib/coradoc/asciidoc/transformer/source_line_extractor.rb +66 -0
  35. data/lib/coradoc/asciidoc/transformer/table_layout.rb +1 -3
  36. data/lib/coradoc/asciidoc/transformer/text_rules.rb +21 -6
  37. data/lib/coradoc/asciidoc/transformer.rb +30 -1
  38. data/lib/coradoc/asciidoc/version.rb +1 -1
  39. metadata +2 -1
@@ -14,7 +14,8 @@ module Coradoc
14
14
  Coradoc::CoreModel::Table.new(
15
15
  id: table.id,
16
16
  title: table.title&.to_s,
17
- rows: rows
17
+ rows: rows,
18
+ source_line: table.source_line
18
19
  )
19
20
  end
20
21
 
@@ -24,7 +25,8 @@ module Coradoc
24
25
  end
25
26
  Coradoc::CoreModel::TableRow.new(
26
27
  cells: cells,
27
- header: row.header
28
+ header: row.header,
29
+ source_line: row.source_line
28
30
  )
29
31
  end
30
32
 
@@ -38,7 +40,8 @@ module Coradoc
38
40
  colspan: cell.colspan,
39
41
  rowspan: cell.rowspan,
40
42
  style: cell.style_name,
41
- children: children
43
+ children: children,
44
+ source_line: cell.source_line
42
45
  )
43
46
  end
44
47
  end
@@ -38,18 +38,12 @@ module Coradoc
38
38
  Coradoc::AsciiDoc::Model::Header.new(title: '')
39
39
  end
40
40
 
41
- # Pull FrontmatterBlock out first so strip_title_heading sees
42
- # the body children in their natural order (frontmatter →
43
- # title heading → body). The reverse-direction strip then
44
- # matches on a level-0 HeaderElement wherever it sits among
45
- # the remaining children.
46
41
  without_frontmatter, frontmatter = extract_frontmatter(Array(element.children))
47
- without_title = strip_title_heading(without_frontmatter, element.title)
48
42
 
49
43
  Coradoc::AsciiDoc::Model::Document.new(
50
44
  id: element.id,
51
45
  header: header,
52
- sections: flatten_children(without_title),
46
+ sections: flatten_children(without_frontmatter),
53
47
  frontmatter: frontmatter
54
48
  )
55
49
  when CoreModel::SectionElement
@@ -68,27 +62,6 @@ module Coradoc
68
62
  end
69
63
  end
70
64
 
71
- # The forward direction (DocumentTransformer#insert_title_heading_after_frontmatter)
72
- # emits a level-0 HeaderElement after any FrontmatterBlock so
73
- # consumers that walk the children see the title. On the reverse
74
- # path, that HeaderElement would round-trip back as a separate
75
- # body element and the title would be emitted twice (once via
76
- # the document header, once via the heading child). Drop it
77
- # before serialization when it carries the same text as the
78
- # document title.
79
- def strip_title_heading(children, document_title)
80
- return children unless document_title && !document_title.strip.empty?
81
-
82
- index = children.find_index do |child|
83
- child.is_a?(CoreModel::HeaderElement) &&
84
- child.level.to_i.zero? &&
85
- child.title.to_s == document_title.to_s
86
- end
87
- return children unless index
88
-
89
- children.reject.with_index { |_, i| i == index }
90
- end
91
-
92
65
  # Transforms each CoreModel child and flattens one level so a
93
66
  # transform that returns multiple siblings (e.g. a source block
94
67
  # followed by its re-expanded callout paragraphs) stays in
@@ -327,8 +300,11 @@ module Coradoc
327
300
  end
328
301
 
329
302
  def transform_image(image)
330
- target_class = image.inline ? Coradoc::AsciiDoc::Model::Image::InlineImage
331
- : Coradoc::AsciiDoc::Model::Image::BlockImage
303
+ target_class = if image.inline
304
+ Coradoc::AsciiDoc::Model::Image::InlineImage
305
+ else
306
+ Coradoc::AsciiDoc::Model::Image::BlockImage
307
+ end
332
308
  target_class.new(**image_attributes(image))
333
309
  end
334
310
 
@@ -35,21 +35,44 @@ module Coradoc
35
35
  end
36
36
  end
37
37
 
38
+ # Folds soft source line breaks into a single text run: when two
39
+ # +Model::TextElement+s sit adjacent in the content array (the parser
40
+ # emits one per source line of a paragraph), a space is inserted
41
+ # between them so wrapped text renders as flowing prose rather than
42
+ # concatenated words.
43
+ #
44
+ # The previous item must ALSO be a TextElement — if a non-text
45
+ # inline (HardLineBreak, Passthrough, Image, etc.) sits between two
46
+ # TextElements, the source did not have a soft break there and no
47
+ # space should be synthesised. Without this guard, `foo +\nbar`
48
+ # would emit `["foo", hard_break, " ", "bar"]` (stray space) and
49
+ # `Before +pass:[RAW]+ after` would emit a double space around the
50
+ # passthrough.
38
51
  def visit_array(items)
39
52
  result = []
40
- items.each_with_index do |item, idx|
53
+ previous = nil
54
+ items.each do |item|
41
55
  transformed = visit_content(item)
42
56
  next if transformed.empty?
43
57
 
44
- needs_space = idx.positive? &&
45
- item.is_a?(Model::TextElement) &&
46
- item.line_break != '+'
47
- result << CoreModel::TextContent.new(text: ' ') if needs_space
58
+ result << CoreModel::TextContent.new(text: ' ') if soft_break_before?(previous, item)
48
59
  result.concat(transformed)
60
+ previous = item
49
61
  end
50
62
  result
51
63
  end
52
64
 
65
+ # Two adjacent TextElements in the content array indicate the
66
+ # parser split a paragraph across source lines. Join them with a
67
+ # space (soft break) unless the second one carries a hard-break
68
+ # marker — the model owns that predicate, so this method does
69
+ # not reach into parser-specific line_break string values.
70
+ def soft_break_before?(previous, current)
71
+ previous.is_a?(Model::TextElement) &&
72
+ current.is_a?(Model::TextElement) &&
73
+ !current.hard_break?
74
+ end
75
+
53
76
  def visit_model(model)
54
77
  @to_core_model.transform(model)
55
78
  end
@@ -23,14 +23,38 @@ module Coradoc
23
23
  transformer ? transformer.call(model) : model
24
24
  end
25
25
 
26
+ # Returns the source-line view of an AsciiDoc content array as
27
+ # an Array<String>. Single source of truth for source-line
28
+ # extraction — every transformer that needs to populate
29
+ # CoreModel::Block#lines goes through here so the filtering
30
+ # rules (skip Model::LineBreak and Model::Break::PageBreak,
31
+ # which carry no renderable text) apply consistently.
32
+ def extract_source_lines(content)
33
+ Array(content).filter_map do |item|
34
+ next if item.is_a?(Coradoc::AsciiDoc::Model::LineBreak) ||
35
+ item.is_a?(Coradoc::AsciiDoc::Model::Break::PageBreak)
36
+
37
+ extract_text_content(item).to_s
38
+ end
39
+ end
40
+
26
41
  def extract_block_lines(block)
27
- non_break_lines = Array(block.lines).reject do |line|
28
- line.is_a?(Coradoc::AsciiDoc::Model::LineBreak) ||
29
- line.is_a?(Coradoc::AsciiDoc::Model::Break::PageBreak)
42
+ extract_source_lines(block.lines).join("\n")
43
+ end
44
+
45
+ # Extracts the source_line of the first AsciiDoc::Model::Base in
46
+ # +content+. Single source of truth for source-line propagation
47
+ # from AsciiDoc::Model trees into CoreModel objects built from
48
+ # grouped content (e.g., paragraphs inside typed blocks).
49
+ # Returns nil when no model carries a source_line.
50
+ def extract_source_line(content)
51
+ Array(content).each do |item|
52
+ next unless item.is_a?(Coradoc::AsciiDoc::Model::Base)
53
+
54
+ line = item.source_line
55
+ return line if line
30
56
  end
31
- non_break_lines.map do |line|
32
- extract_text_content(line)
33
- end.join("\n")
57
+ nil
34
58
  end
35
59
 
36
60
  def extract_title_text(title)
@@ -34,7 +34,10 @@ module Coradoc
34
34
 
35
35
  # Example
36
36
  rule(example: sequence(:example)) do
37
- Model::Block::Example.new(title: '', lines: example)
37
+ Model::Block::Example.new(
38
+ title: '',
39
+ lines: example
40
+ )
38
41
  end
39
42
 
40
43
  # Admonition. Canonicalise the type to uppercase so round-trips
@@ -45,7 +48,10 @@ module Coradoc
45
48
  content: sequence(:content)
46
49
  ) do
47
50
  canonical = Coradoc::AsciiDoc::Transform::ElementTransformers::AdmonitionStyles.canonicalize(admonition_type.to_s)
48
- Model::Admonition.new(content: content, type: canonical)
51
+ Model::Admonition.new(
52
+ content: content,
53
+ type: canonical
54
+ )
49
55
  end
50
56
 
51
57
  # Block image
@@ -16,23 +16,23 @@ module Coradoc
16
16
  # returning a Model::Block::* instance. `max_length` nil means
17
17
  # unbounded.
18
18
  DELIMITER_CLASSIFICATIONS = [
19
- ['*', 4, nil, ->(opts, attrs) {
19
+ ['*', 4, nil, lambda { |opts, attrs|
20
20
  if attrs && attrs.positional == [] && attrs.named.first&.name == 'reviewer'
21
- Model::Block::ReviewerComment.new(**opts.merge(attributes: attrs))
21
+ Model::Block::ReviewerComment.new(**opts, attributes: attrs)
22
22
  else
23
- Model::Block::Side.new(**opts.merge(attributes: attrs))
23
+ Model::Block::Side.new(**opts, attributes: attrs)
24
24
  end
25
25
  }],
26
- ['=', 4, nil, ->(opts, attrs) { Model::Block::Example.new(**opts.merge(attributes: attrs)) }],
27
- ['+', 4, nil, ->(opts, attrs) { Model::Block::Pass.new(**opts.merge(attributes: attrs)) }],
28
- ['.', 4, nil, ->(opts, attrs) { Model::Block::Literal.new(**opts.merge(attributes: attrs)) }],
29
- ['_', 4, nil, ->(opts, attrs) { Model::Block::Quote.new(**opts.merge(attributes: attrs)) }],
30
- ['-', 4, nil, ->(opts, attrs) { Model::Block::SourceCode.new(**opts.merge(attributes: attrs)) }],
31
- ['-', 2, 2, ->(opts, attrs) { Model::Block::Open.new(**opts.merge(attributes: attrs)) }],
26
+ ['=', 4, nil, ->(opts, attrs) { Model::Block::Example.new(**opts, attributes: attrs) }],
27
+ ['+', 4, nil, ->(opts, attrs) { Model::Block::Pass.new(**opts, attributes: attrs) }],
28
+ ['.', 4, nil, ->(opts, attrs) { Model::Block::Literal.new(**opts, attributes: attrs) }],
29
+ ['_', 4, nil, ->(opts, attrs) { Model::Block::Quote.new(**opts, attributes: attrs) }],
30
+ ['-', 4, nil, ->(opts, attrs) { Model::Block::SourceCode.new(**opts, attributes: attrs) }],
31
+ ['-', 2, 2, ->(opts, attrs) { Model::Block::Open.new(**opts, attributes: attrs) }],
32
32
  # Markdown-style triple-backtick fence: behaves as a SourceCode
33
33
  # block. The language tag parsed from the opening fence is passed
34
34
  # through opts[:lang]; extract_block_language prefers block.lang.
35
- ['`', 3, nil, ->(opts, attrs) {
35
+ ['`', 3, nil, lambda { |opts, attrs|
36
36
  model_opts = opts.merge(attributes: attrs, delimiter_char: '`')
37
37
  model_opts[:lang] = opts[:lang] if opts.key?(:lang)
38
38
  Model::Block::SourceCode.new(**model_opts)
@@ -21,7 +21,9 @@ module Coradoc
21
21
  id = id.to_s unless id.nil?
22
22
  id = nil if id && id.empty?
23
23
 
24
- Model::Header.new(id:, title:, author:, revision:)
24
+ Model::Header.new(
25
+ id:, title:, author:, revision:
26
+ )
25
27
  end
26
28
 
27
29
  rule(header: simple(:header)) do
@@ -34,7 +36,9 @@ module Coradoc
34
36
  last_name: simple(:last_name),
35
37
  email: simple(:email)
36
38
  ) do
37
- Model::Author.new(first_name:, last_name:, email:, middle_name: nil)
39
+ Model::Author.new(
40
+ first_name:, last_name:, email:, middle_name: nil
41
+ )
38
42
  end
39
43
 
40
44
  # Revision
@@ -43,7 +47,9 @@ module Coradoc
43
47
  date: simple(:date),
44
48
  remark: simple(:remark)
45
49
  ) do
46
- Model::Revision.new(number:, date:, remark:)
50
+ Model::Revision.new(
51
+ number:, date:, remark:
52
+ )
47
53
  end
48
54
  end
49
55
  end
@@ -10,8 +10,8 @@ module Coradoc
10
10
  # `span` is excluded because it carries `text:` + `attributes:`
11
11
  # rather than `content:`, so it gets its own pair of rules.
12
12
  FORMATTING_VARIANTS = [
13
- %i[bold Bold],
14
- %i[italic Italic],
13
+ %i[bold Bold],
14
+ %i[italic Italic],
15
15
  %i[highlight Highlight],
16
16
  %i[monospace Monospace]
17
17
  ].freeze
@@ -31,7 +31,9 @@ module Coradoc
31
31
  href = xref[:href].to_s
32
32
  text = xref[:text]
33
33
  args = text ? [text.to_s] : []
34
- Model::Inline::CrossReference.new(href:, args:)
34
+ Model::Inline::CrossReference.new(
35
+ href:, args:
36
+ )
35
37
  end
36
38
 
37
39
  # Inline image
@@ -60,7 +62,9 @@ module Coradoc
60
62
 
61
63
  # Attribute reference
62
64
  rule(attribute_reference: simple(:name)) do
63
- Model::Inline::AttributeReference.new(name:)
65
+ Model::Inline::AttributeReference.new(
66
+ name:
67
+ )
64
68
  end
65
69
 
66
70
  # Hard line break (` +\n` or `\\n`). Emitted as a dedicated
@@ -68,7 +72,7 @@ module Coradoc
68
72
  # Model::LineBreak, which only represents paragraph-separator
69
73
  # blank lines. Hard breaks carry semantic meaning: HTML/Markdown
70
74
  # renderers map them to <br>.
71
- rule(hard_line_break: simple(:_)) do
75
+ rule(hard_line_break: simple(:hard_line_break)) do
72
76
  Model::Inline::HardLineBreak.new
73
77
  end
74
78
 
@@ -77,30 +81,40 @@ module Coradoc
77
81
  term_type: simple(:term_type),
78
82
  term: simple(:term)
79
83
  ) do
80
- Coradoc::AsciiDoc::Model::Term.new(term:, type: term_type, lang: :en)
84
+ Coradoc::AsciiDoc::Model::Term.new(
85
+ term:, type: term_type, lang: :en
86
+ )
81
87
  end
82
88
 
83
89
  # Footnote
84
90
  rule(footnote: simple(:footnote)) do
85
91
  text_str = footnote.to_s
86
- Coradoc::AsciiDoc::Model::Inline::Footnote.new(text: text_str)
92
+ Coradoc::AsciiDoc::Model::Inline::Footnote.new(
93
+ text: text_str
94
+ )
87
95
  end
88
96
 
89
97
  rule(footnote: simple(:footnote), id: simple(:id)) do
90
98
  text_str = footnote.to_s
91
- Coradoc::AsciiDoc::Model::Inline::Footnote.new(text: text_str, id: id.to_s)
99
+ Coradoc::AsciiDoc::Model::Inline::Footnote.new(
100
+ text: text_str, id: id.to_s
101
+ )
92
102
  end
93
103
 
94
104
  # Footnote with empty content (reference to named footnote)
95
105
  rule(footnote: sequence(:footnote), id: simple(:id)) do
96
106
  text_str = footnote.map(&:to_s).join
97
- Coradoc::AsciiDoc::Model::Inline::Footnote.new(text: text_str, id: id.to_s)
107
+ Coradoc::AsciiDoc::Model::Inline::Footnote.new(
108
+ text: text_str, id: id.to_s
109
+ )
98
110
  end
99
111
 
100
112
  # Footnote with empty content and no id
101
113
  rule(footnote: sequence(:footnote)) do
102
114
  text_str = footnote.map(&:to_s).join
103
- Coradoc::AsciiDoc::Model::Inline::Footnote.new(text: text_str)
115
+ Coradoc::AsciiDoc::Model::Inline::Footnote.new(
116
+ text: text_str
117
+ )
104
118
  end
105
119
 
106
120
  # Inline formatting rules generated from a single registry.
@@ -114,11 +128,15 @@ module Coradoc
114
128
 
115
129
  rule(constrained_key => subtree(:subtree)) do
116
130
  content = Transformer.extract_inline_content(subtree)
117
- klass.new(content: content, unconstrained: false)
131
+ klass.new(
132
+ content: content, unconstrained: false
133
+ )
118
134
  end
119
135
  rule(unconstrained_key => subtree(:subtree)) do
120
136
  content = Transformer.extract_inline_content(subtree)
121
- klass.new(content: content, unconstrained: true)
137
+ klass.new(
138
+ content: content, unconstrained: true
139
+ )
122
140
  end
123
141
  end
124
142
 
@@ -143,18 +161,24 @@ module Coradoc
143
161
  # Superscript
144
162
  rule(superscript: subtree(:superscript)) do
145
163
  content = Transformer.extract_simple_inline_content(superscript)
146
- Model::Inline::Superscript.new(content:)
164
+ Model::Inline::Superscript.new(
165
+ content:
166
+ )
147
167
  end
148
168
 
149
169
  # Subscript
150
170
  rule(subscript: subtree(:subscript)) do
151
171
  content = Transformer.extract_simple_inline_content(subscript)
152
- Model::Inline::Subscript.new(content:)
172
+ Model::Inline::Subscript.new(
173
+ content:
174
+ )
153
175
  end
154
176
 
155
177
  # Highlight (simple)
156
178
  rule(highlight: simple(:text)) do
157
- Model::Highlight.new(content: text)
179
+ Model::Highlight.new(
180
+ content: text
181
+ )
158
182
  end
159
183
  # Stem
160
184
  rule(stem: subtree(:stem)) do
@@ -85,26 +85,36 @@ module Coradoc
85
85
 
86
86
  # Unordered list
87
87
  rule(unordered: sequence(:list_items)) do
88
- Model::List::Unordered.new(items: list_items)
88
+ Model::List::Unordered.new(
89
+ items: list_items
90
+ )
89
91
  end
90
92
 
91
93
  rule(
92
94
  attribute_list: simple(:attribute_list),
93
95
  unordered: sequence(:list_items)
94
96
  ) do
95
- Model::List::Unordered.new(items: list_items, attrs: attribute_list)
97
+ Model::List::Unordered.new(
98
+ items: list_items,
99
+ attrs: attribute_list
100
+ )
96
101
  end
97
102
 
98
103
  # Ordered list
99
104
  rule(ordered: sequence(:list_items)) do
100
- Model::List::Ordered.new(items: list_items)
105
+ Model::List::Ordered.new(
106
+ items: list_items
107
+ )
101
108
  end
102
109
 
103
110
  rule(
104
111
  attribute_list: simple(:attribute_list),
105
112
  ordered: sequence(:list_items)
106
113
  ) do
107
- Model::List::Ordered.new(items: list_items, attrs: attribute_list)
114
+ Model::List::Ordered.new(
115
+ items: list_items,
116
+ attrs: attribute_list
117
+ )
108
118
  end
109
119
 
110
120
  # Definition list term (with optional anchor)
@@ -12,11 +12,16 @@ module Coradoc
12
12
  comment_text: simple(:comment_text),
13
13
  line_break: simple(:line_break)
14
14
  }) do
15
- Model::CommentLine.new(text: comment_text, line_break: line_break)
15
+ Model::CommentLine.new(
16
+ text: comment_text,
17
+ line_break: line_break
18
+ )
16
19
  end
17
20
 
18
21
  rule(comment_block: { comment_text: simple(:comment_text) }) do
19
- Model::CommentBlock.new(text: comment_text)
22
+ Model::CommentBlock.new(
23
+ text: comment_text
24
+ )
20
25
  end
21
26
 
22
27
  # Page break
@@ -45,7 +50,7 @@ module Coradoc
45
50
  end
46
51
 
47
52
  rule(positional: simple(:positional)) do
48
- positional.to_s
53
+ positional
49
54
  end
50
55
 
51
56
  rule(attribute_array: nil) do
@@ -54,10 +59,14 @@ module Coradoc
54
59
 
55
60
  rule(attribute_array: sequence(:attributes)) do
56
61
  attr_list = Model::AttributeList.new
62
+
57
63
  attributes.each do |a|
58
- if a.is_a?(String)
64
+ case a
65
+ when Parslet::Slice
66
+ attr_list.add_positional(a.to_s)
67
+ when String
59
68
  attr_list.add_positional(a)
60
- elsif a.is_a?(Model::Attribute)
69
+ when Model::Attribute
61
70
  attr_list.add_named(a.key, a.value)
62
71
  end
63
72
  end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'parslet'
4
+
5
+ module Coradoc
6
+ module AsciiDoc
7
+ class Transformer < Parslet::Transform
8
+ # Walks a Parslet AST subtree (Hash/Array/Slice/String/Model) and
9
+ # returns the 1-indexed source line of the first +Parslet::Slice+
10
+ # or Model::Base#source_line it finds.
11
+ #
12
+ # Parslet preserves byte offsets on every matched slice
13
+ # (+slice.line_and_column+ returns +[line, column]+); the
14
+ # transformer receives these slices in the AST but most rules
15
+ # discard the position when building Model objects. This helper
16
+ # recovers the start line of any subtree so transformer rules
17
+ # can populate +Model::Base#source_line+ without changing the
18
+ # parser.
19
+ #
20
+ # Handles two distinct shapes:
21
+ # * Pre-transform AST (Hash/Array of Parslet::Slice) — used by
22
+ # rules that bind raw slices via +simple(:x)+.
23
+ # * Post-transform Model tree (Model::Base instances whose
24
+ # +source_line+ was populated by an earlier rule) — used by
25
+ # rules that bind via +subtree(:x)+ and receive already-
26
+ # transformed content.
27
+ #
28
+ # Returns nil when no position is found (programmatic input,
29
+ # already-stripped ASTs, etc.) — callers should treat nil as
30
+ # "source position unavailable".
31
+ module SourceLineExtractor
32
+ module_function
33
+
34
+ def extract(node)
35
+ case node
36
+ when Parslet::Slice then line_of(node)
37
+ when Coradoc::AsciiDoc::Model::Base then node.source_line
38
+ when Hash then extract_from_hash(node)
39
+ when Array then extract_from_array(node)
40
+ end
41
+ end
42
+
43
+ def line_of(slice)
44
+ line, _column = slice.line_and_column
45
+ line
46
+ end
47
+
48
+ def extract_from_hash(hash)
49
+ hash.each_value do |value|
50
+ line = extract(value)
51
+ return line if line
52
+ end
53
+ nil
54
+ end
55
+
56
+ def extract_from_array(array)
57
+ array.each do |value|
58
+ line = extract(value)
59
+ return line if line
60
+ end
61
+ nil
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -46,9 +46,7 @@ module Coradoc
46
46
  normalized_cells = cells.map { |cell| TableCellBuilder.normalize_cell(cell) }
47
47
 
48
48
  col_count = explicit_col_count
49
- if col_count.nil? || col_count.zero?
50
- col_count = infer_column_count(normalized_cells)
51
- end
49
+ col_count = infer_column_count(normalized_cells) if col_count.nil? || col_count.zero?
52
50
  col_count = normalized_cells.size if col_count.nil? || col_count.zero?
53
51
 
54
52
  rows = []
@@ -9,7 +9,9 @@ module Coradoc
9
9
  transformer_class.class_eval do
10
10
  # Text Model
11
11
  rule(text: simple(:text)) do
12
- Model::TextElement.new(content: text.to_s)
12
+ Model::TextElement.new(
13
+ content: text.to_s
14
+ )
13
15
  end
14
16
 
15
17
  rule(text_string: subtree(:text_string)) do
@@ -17,19 +19,30 @@ module Coradoc
17
19
  end
18
20
 
19
21
  rule(text: simple(:text), line_break: simple(:line_break)) do
20
- Model::TextElement.new(content: text.to_s, line_break: line_break)
22
+ Model::TextElement.new(
23
+ content: text.to_s,
24
+ line_break: line_break
25
+ )
21
26
  end
22
27
 
23
28
  rule(text: sequence(:text), line_break: simple(:line_break)) do
24
- Model::TextElement.new(content: text, line_break: line_break)
29
+ Model::TextElement.new(
30
+ content: text,
31
+ line_break: line_break
32
+ )
25
33
  end
26
34
 
27
35
  rule(id: simple(:id), text: simple(:text)) do
28
- Model::TextElement.new(content: text.to_s, id: id.to_s)
36
+ Model::TextElement.new(
37
+ content: text.to_s,
38
+ id: id.to_s
39
+ )
29
40
  end
30
41
 
31
42
  rule(text: sequence(:text)) do
32
- Model::TextElement.new(content: text)
43
+ Model::TextElement.new(
44
+ content: text
45
+ )
33
46
  end
34
47
 
35
48
  rule(
@@ -58,7 +71,9 @@ module Coradoc
58
71
 
59
72
  # Line break
60
73
  rule(line_break: simple(:line_break)) do
61
- Model::LineBreak.new(line_break:)
74
+ Model::LineBreak.new(
75
+ line_break:
76
+ )
62
77
  end
63
78
 
64
79
  # Unparsed text