coradoc-adoc 2.0.16 → 2.0.18

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e3b72474cf0652bc8f5702011009cab0caeb9dd5180a6f17510a2e081286951d
4
- data.tar.gz: 8218895ed5672fe9932747a025c54621ba02870daea6f964b1cc28d506968a1d
3
+ metadata.gz: d8b381debf5d0742992318f775804846b9a2560e59dcff0dad3518999fcb39ad
4
+ data.tar.gz: 5556652e925b9d3081562849790252900e9a2b2493ca7f6eb8ae023d888ed38a
5
5
  SHA512:
6
- metadata.gz: 46c11254ea13b32498f66d11eb0ead0d13df136f7a0cca65e701373cf48f1edf9d9e1beb447553ddc1a14e4459052d559038aa1b6edb580731a6f91d27969616
7
- data.tar.gz: 3d6b4701f38cfd0042bcd14d77d094c6c48626bc85ffab3557a274f2fd90041d1df95f37a156a18ef7a73e8d2d9d4900f8048c8c8d021425ddd8ec38a587db84
6
+ metadata.gz: 7bae7c856ed3b6c8c804c614e009a1efe36327c94cd8d1b5b48429b1fe849f75e99df68f1c85f719c63d57b05fd9584a707881bd01e8d232a5cf189bc9c7e69f
7
+ data.tar.gz: 121f12dea70e459c11f7f91233fb7189dedf92bdc0ad8665063c427df67456a8bf0d0bfa035dec43f46919b851fad078bf48598cdac323ff2c86a02102b560f4
@@ -21,10 +21,21 @@ module Coradoc
21
21
  id: section_ast[:id],
22
22
  level: extract_level(section_ast),
23
23
  children: build_section_contents(section_ast[:contents]) +
24
- build_subsections(section_ast[:sections])
24
+ build_subsections(section_ast[:sections]),
25
+ attributes: build_section_metadata(section_ast[:attribute_list])
25
26
  )
26
27
  end
27
28
 
29
+ # Coerces the AST attribute_list (hash / string / nil) through the
30
+ # normalizer, then delegates to Transform::AttributeListToMetadata
31
+ # for the typed Model::AttributeList -> CoreModel::Metadata step.
32
+ # Single source of truth lives in the transform layer (DRY/MECE).
33
+ def build_section_metadata(attribute_list)
34
+ list = Coradoc::AsciiDoc::Transformer::AttributeListNormalizer
35
+ .coerce(attribute_list)
36
+ Coradoc::AsciiDoc::Transform::AttributeListToMetadata.call(list)
37
+ end
38
+
28
39
  def build_section_contents(contents_ast)
29
40
  return [] unless contents_ast
30
41
 
@@ -58,8 +58,21 @@ module Coradoc
58
58
  Coradoc::AsciiDoc::Model::Section,
59
59
  collection: true,
60
60
  initialize_empty: true
61
+ # Block-header attribute list (e.g. `[appendix]`, `[bibliography]`).
62
+ # Single source of truth for the AsciiDoc `[style]` hint; downstream
63
+ # consumers (coradoc-mirror) read `style` from SectionElement#attributes
64
+ # to dispatch JS section types (annex, references, ...).
65
+ attribute :attribute_list, Coradoc::AsciiDoc::Model::AttributeList
61
66
  # attribute :anchor, Coradoc::AsciiDoc::Model::Inline::Anchor
62
67
 
68
+ # Style hint derived from the first positional in `attribute_list`.
69
+ # Returns nil when no list is attached. Kept here so callers don't
70
+ # need to know the AttributeList shape.
71
+ def style
72
+ first_positional = attribute_list&.positional&.first
73
+ first_positional&.value
74
+ end
75
+
63
76
  # Allow setting level directly during initialization
64
77
  def initialize(**attributes)
65
78
  level_value = attributes.delete(:level)
@@ -44,7 +44,7 @@ module Coradoc
44
44
  end
45
45
 
46
46
  def positional_value_unquoted
47
- match['a-zA-Z0-9_\-%.'].repeat(1)
47
+ match('[^\],]').repeat(1)
48
48
  end
49
49
 
50
50
  def positional_value_single_quote
@@ -139,29 +139,57 @@ module Coradoc
139
139
  closing_delimiter >> newline
140
140
  end
141
141
 
142
- # Block style parser with EXACT delimiter length (for open blocks)
143
- # Open blocks use exactly 2 dashes and cannot nest within themselves
142
+ # Block style parser with EXACT delimiter length (for open blocks).
143
+ # Open blocks use exactly 2 dashes. A `[source]`/`[listing]`/`[literal]`
144
+ # positional attribute casts the body to verbatim — block macros like
145
+ # `image::` must survive byte-for-byte, same as delimited source blocks.
144
146
  def block_style_exact(n_deep = 3, delimiter = '-', exact_chars = 2)
145
147
  capture_key = :"delimit_#{delimiter}_exact_#{exact_chars}_#{n_deep}"
148
+ attr_capture_key = :"#{capture_key}_attrs"
146
149
  current_delimiter = str(delimiter).repeat(exact_chars, exact_chars).capture(capture_key)
147
150
  closing_delimiter = dynamic do |_s, c|
148
151
  str(c.captures[capture_key].to_s.strip)
149
152
  end
150
153
 
154
+ # Closure so the call bypasses Parslet's method_missing inside the
155
+ # dynamic block. capture() stashes the parsed AST (a nested Hash for
156
+ # a `[source,ruby]` header). Inspect the structure directly so we
157
+ # don't depend on Ruby's Hash#to_s format (it changed in 3.4).
158
+ verbatim_cast = lambda do |raw|
159
+ values = []
160
+ queue = [raw]
161
+ while (node = queue.shift)
162
+ case node
163
+ when Hash then queue.concat(node.values)
164
+ when Array then queue.concat(node)
165
+ else values << node
166
+ end
167
+ end
168
+ castable = %w[source listing literal]
169
+ values.any? { |v| castable.include?(v.to_s) }
170
+ end
171
+
151
172
  block_content_with_closing = dynamic do |_s, c|
152
173
  delim_str = c.captures[capture_key].to_s.strip
174
+ raw_header = c.captures[attr_capture_key]
153
175
  closing_pattern = str(delim_str) >> newline
154
176
 
155
- content = block_image
156
- content |= block(n_deep - 1) if n_deep.positive?
157
- content |= list
158
- content |= text_line(false, unguarded: true)
159
- content |= empty_line.as(:line_break)
177
+ content = if verbatim_cast.call(raw_header)
178
+ text_line(false, unguarded: true, verbatim: true) |
179
+ empty_line.as(:line_break)
180
+ else
181
+ alt = block_image
182
+ alt |= block(n_deep - 1) if n_deep.positive?
183
+ alt |= list
184
+ alt |= text_line(false, unguarded: true)
185
+ alt |= empty_line.as(:line_break)
186
+ alt
187
+ end
160
188
 
161
189
  (closing_pattern.absent? >> content).repeat(1)
162
190
  end
163
191
 
164
- block_header >>
192
+ block_header.capture(attr_capture_key) >>
165
193
  line_start? >>
166
194
  current_delimiter.as(:delimiter) >> newline >>
167
195
  block_content_with_closing.as(:lines) >>
@@ -14,6 +14,14 @@ module Coradoc
14
14
  str('>>')
15
15
  ).as(:cross_reference)
16
16
  end
17
+
18
+ # AsciiDoc escape: `\<<` produces the literal text `<<` without
19
+ # firing the cross-reference rule. Without this, documentation that
20
+ # shows AsciiDoc xref syntax as a literal example gets rewritten
21
+ # into a broken link to a non-existent anchor.
22
+ def escaped_xref
23
+ str('\\') >> str('<<').as(:text)
24
+ end
17
25
  end
18
26
  end
19
27
  end
@@ -118,6 +118,7 @@ module Coradoc
118
118
 
119
119
  def inline_image
120
120
  (str('image:').present? >> str('image:') >>
121
+ str(':').absent? >>
121
122
  match('[A-Za-z0-9_.\\-:/&?=+,%#~;]+').repeat(1).as(:path) >>
122
123
  (str('[') >> match('[^\\]]').repeat(1).as(:text) >> str(']')).maybe
123
124
  ).as(:inline_image)
@@ -158,7 +159,8 @@ module Coradoc
158
159
  str('+++').present? |
159
160
  term_type.present? |
160
161
  str('footnote').present? |
161
- stem_type.present?
162
+ stem_type.present? |
163
+ str('\\<<').present?
162
164
  end
163
165
 
164
166
  def inline
@@ -175,6 +177,7 @@ module Coradoc
175
177
  superscript |
176
178
  subscript |
177
179
  attribute_reference |
180
+ escaped_xref |
178
181
  cross_reference |
179
182
  term_inline |
180
183
  term_inline2 |
@@ -188,8 +191,12 @@ module Coradoc
188
191
  end
189
192
 
190
193
  def text_unformatted
191
- # line_not_text? >>
192
- (inline.absent? >>
194
+ # `str('\\<<').absent?` stops text from consuming the backslash of
195
+ # an escaped xref (`\<<`). Without this guard, the `\` is taken as
196
+ # plain text, the `<<` re-enters cross_reference, and the literal
197
+ # escape is destroyed.
198
+ (str('\\<<').absent? >>
199
+ inline.absent? >>
193
200
  match("[^\n]")
194
201
  ).repeat(1)
195
202
  end
@@ -52,21 +52,19 @@ module Coradoc
52
52
 
53
53
  def olist_item(nesting_level = 1)
54
54
  item = olist_marker(nesting_level).as(:marker) >>
55
- match("\n").absent? >> space >> text_line(true, unguarded: true)
56
- # >>
57
- # (list_continuation.present? >> list_continuation >>
58
- # paragraph #| example_block(n_deep: 1)
59
- # ).repeat(0).as(:attached)
55
+ match("\n").absent? >> space >>
56
+ (text_line(true, unguarded: true) >>
57
+ list_item_continuation_lines).as(:lines)
60
58
 
61
59
  att = (list_continuation.present? >>
62
60
  list_continuation >>
63
- (admonition_line | paragraph | block) # (n_deep: 1))
61
+ (admonition_line | paragraph | block)
64
62
  ).repeat(0).as(:attached)
65
63
  item >>= att.maybe
66
64
 
67
65
  if nesting_level <= 4
68
66
  item >>= (list_marker(nesting_level + 1).present? >>
69
- list(nesting_level + 1)).repeat(0).as(:nested) # ).maybe
67
+ list(nesting_level + 1)).repeat(0).as(:nested)
70
68
  end
71
69
  olist_marker(nesting_level).present? >> item.as(:list_item)
72
70
  end
@@ -74,29 +72,65 @@ module Coradoc
74
72
  def ulist_marker(nesting_level = 1)
75
73
  line_start? >>
76
74
  (nesting_level > 1 ? literal_space.maybe : str('')) >>
77
- str('*' * nesting_level) >>
75
+ (
76
+ asterisk_marker(nesting_level) |
77
+ dash_marker(nesting_level)
78
+ )
79
+ end
80
+
81
+ # AsciiDoc standard bullet: `*`, `**`, `***`, ... matching the
82
+ # nesting level. Excludes table delimiters (`|===`) and deeper
83
+ # asterisk runs that belong to a sibling level.
84
+ def asterisk_marker(nesting_level)
85
+ str('*' * nesting_level) >>
78
86
  str('*').absent? >>
79
87
  str('===').absent?
80
88
  end
81
89
 
90
+ # Markdown-style dash bullet: `-`. Accepted only at the top level
91
+ # because Markdown nests via indentation rather than multi-char
92
+ # markers — deeper levels stay on the AsciiDoc `*` form. Guards
93
+ # exclude em-dashes (`--`), delimited-block fences (`----`),
94
+ # and negative-number runs (`-1`, `-42`) which are not list
95
+ # markers in any common dialect.
96
+ def dash_marker(nesting_level)
97
+ return match('').absent? unless nesting_level == 1
98
+
99
+ str('-') >>
100
+ str('-').absent? >>
101
+ match('[0-9]').absent?
102
+ end
103
+
82
104
  def ulist_item(nesting_level = 1)
83
105
  item = ulist_marker(nesting_level).as(:marker) >>
84
106
  str(' [[[').absent? >>
85
- match("\n").absent? >> space >> text_line(true, unguarded: true)
107
+ match("\n").absent? >> space >>
108
+ (text_line(true, unguarded: true) >>
109
+ list_item_continuation_lines).as(:lines)
86
110
 
87
111
  att = (list_continuation.present? >>
88
112
  list_continuation >>
89
- (admonition_line | paragraph | block) # (n_deep: 1))
113
+ (admonition_line | paragraph | block)
90
114
  ).repeat(0).as(:attached)
91
115
  item >>= att.maybe
92
116
 
93
117
  if nesting_level <= 4
94
118
  item >>= (list_marker(nesting_level + 1).present? >>
95
- list(nesting_level + 1)).repeat(0).as(:nested) # ).maybe
119
+ list(nesting_level + 1)).repeat(0).as(:nested)
96
120
  end
97
121
  ulist_marker(nesting_level).present? >> item.as(:list_item)
98
122
  end
99
123
 
124
+ # Continuation lines of a list item's first paragraph. AsciiDoc
125
+ # joins consecutive non-blank lines into one paragraph within the
126
+ # item, but the lines must not start a sibling construct (another
127
+ # list marker, block delimiter, attribute list, section, element
128
+ # id, table boundary, list continuation, or list prefix).
129
+ # `line_not_text?` (from Paragraph) is that exact lookahead.
130
+ def list_item_continuation_lines
131
+ (line_not_text? >> text_line(true, unguarded: true)).repeat(0)
132
+ end
133
+
100
134
  def dlist_delimiter
101
135
  (
102
136
  (str(':::::') >> match(':').absent?) |
@@ -120,12 +154,18 @@ module Coradoc
120
154
  end
121
155
 
122
156
  def dlist_item(_delimiter = nil)
123
- (((dlist_term.as(:terms).repeat(1) >> line_ending >>
124
- empty_line.repeat(0)).repeat(1) >>
125
- dlist_definition) |
126
- (dlist_term.repeat(1, 1).as(:terms) >> space >>
127
- dlist_definition)
128
- ).as(:definition_list_item)
157
+ # Both forms below produce the same AST shape:
158
+ # {terms: [<dlist_term>, ...], definition: <text>}
159
+ # so the transformer's definition_list_item rule matches uniformly.
160
+ #
161
+ # Multi-line form: one or more term-lines (`term::` + newline +
162
+ # optional blank lines), then the definition on its own line(s).
163
+ # Single-line form: one term + inline space + definition.
164
+ term_line = dlist_term >> line_ending >> empty_line.repeat(0)
165
+
166
+ ((term_line.repeat(1).as(:terms) >> dlist_definition) |
167
+ (dlist_term.repeat(1, 1).as(:terms) >> space >>
168
+ dlist_definition)).as(:definition_list_item)
129
169
  end
130
170
  end
131
171
  end
@@ -13,7 +13,9 @@ module Coradoc
13
13
  list_prefix.absent? >>
14
14
  list_continuation.absent? >>
15
15
  element_id.absent? >>
16
- section_prefix.absent?
16
+ section_prefix.absent? >>
17
+ comment_line.absent? >>
18
+ tag.absent?
17
19
  end
18
20
 
19
21
  # NOTE: many_breaks parameter has three states for different parsing contexts:
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module AsciiDoc
5
+ module Transform
6
+ # Single source of truth for AsciiDoc Model::AttributeList ->
7
+ # CoreModel::Metadata conversion (DRY/MECE).
8
+ #
9
+ # Promotes the first positional attribute to `style` and the
10
+ # `role=` named attribute to `role`, matching the AsciiDoc
11
+ # block-header semantics that downstream consumers (coradoc-mirror)
12
+ # dispatch on to pick a JS section type (annex, abstract, ...).
13
+ #
14
+ # Returns nil for anything that isn't a typed AttributeList so
15
+ # callers can pass through optional/missing inputs without an
16
+ # extra guard.
17
+ module AttributeListToMetadata
18
+ module_function
19
+
20
+ def call(list)
21
+ return nil unless list.is_a?(Coradoc::AsciiDoc::Model::AttributeList)
22
+
23
+ metadata = Coradoc::CoreModel::Metadata.new
24
+ first_positional = list.positional.first
25
+ metadata['style'] = first_positional.value if first_positional
26
+ named_role = list.named.find { |n| n.name == 'role' }
27
+ metadata['role'] = named_role.value.first if named_role&.value&.any?
28
+ metadata
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -16,7 +16,12 @@ module Coradoc
16
16
  )
17
17
  end
18
18
 
19
- def transform_source_block(block)
19
+ # Verbatim blocks (source, listing, literal, pass) must round-trip
20
+ # their body byte-for-byte. Each source line becomes one content
21
+ # line; we join with "\n" so whitespace, indentation, and blank
22
+ # lines are preserved. Treating these as paragraphs would collapse
23
+ # whitespace and join consecutive lines into a single flowing text.
24
+ def transform_verbatim_block(block, klass)
20
25
  non_break_lines = Array(block.lines).reject do |line|
21
26
  line.is_a?(Coradoc::AsciiDoc::Model::LineBreak) ||
22
27
  line.is_a?(Coradoc::AsciiDoc::Model::Break::PageBreak)
@@ -25,16 +30,26 @@ module Coradoc
25
30
  ToCoreModel.extract_text_content(line)
26
31
  end.join("\n")
27
32
 
28
- language = ToCoreModel.extract_block_language(block)
29
-
30
- Coradoc::CoreModel::SourceBlock.new(
33
+ klass.new(
31
34
  id: block.id,
32
35
  title: ToCoreModel.extract_title_text(block.title),
33
36
  content: content_lines,
34
- language: language
37
+ language: ToCoreModel.extract_block_language(block)
35
38
  )
36
39
  end
37
40
 
41
+ def transform_source_block(block)
42
+ transform_verbatim_block(block, Coradoc::CoreModel::SourceBlock)
43
+ end
44
+
45
+ def transform_literal_block(block)
46
+ transform_verbatim_block(block, Coradoc::CoreModel::LiteralBlock)
47
+ end
48
+
49
+ def transform_pass_block(block)
50
+ transform_verbatim_block(block, Coradoc::CoreModel::PassBlock)
51
+ end
52
+
38
53
  # Open blocks (`--`) are generic containers. AsciiDoc allows
39
54
  # casting them to a different block type via positional
40
55
  # attributes: verbatim types (`[source]`, `[listing]`,
@@ -86,22 +101,11 @@ module Coradoc
86
101
  end
87
102
 
88
103
  def transform_listing_from_open(block)
89
- content_lines = Array(block.lines).map { |line| ToCoreModel.extract_text_content(line) }.join("\n")
90
- Coradoc::CoreModel::ListingBlock.new(
91
- id: block.id,
92
- title: ToCoreModel.extract_title_text(block.title),
93
- content: content_lines,
94
- language: ToCoreModel.extract_block_language(block)
95
- )
104
+ transform_verbatim_block(block, Coradoc::CoreModel::ListingBlock)
96
105
  end
97
106
 
98
107
  def transform_literal_from_open(block)
99
- content_lines = Array(block.lines).map { |line| ToCoreModel.extract_text_content(line) }.join("\n")
100
- Coradoc::CoreModel::LiteralBlock.new(
101
- id: block.id,
102
- title: ToCoreModel.extract_title_text(block.title),
103
- content: content_lines
104
- )
108
+ transform_verbatim_block(block, Coradoc::CoreModel::LiteralBlock)
105
109
  end
106
110
 
107
111
  def transform_block(block, semantic_type_or_delimiter)
@@ -123,15 +127,14 @@ module Coradoc
123
127
  end
124
128
 
125
129
  def transform_typed_block(block, klass, extra_attrs = {})
126
- lines = Array(block.lines).reject do |line|
127
- line.is_a?(Coradoc::AsciiDoc::Model::LineBreak) ||
128
- line.is_a?(Coradoc::AsciiDoc::Model::Break::PageBreak)
129
- end
130
-
131
- has_nested_blocks = lines.any?(Coradoc::AsciiDoc::Model::Block::Core)
130
+ raw_lines = Array(block.lines)
131
+ has_nested_blocks = raw_lines.any? { |line| block_level_child?(line) }
132
132
 
133
133
  if has_nested_blocks
134
- children = lines.filter_map do |line|
134
+ children = raw_lines.reject do |line|
135
+ line.is_a?(Coradoc::AsciiDoc::Model::LineBreak) ||
136
+ line.is_a?(Coradoc::AsciiDoc::Model::Break::PageBreak)
137
+ end.filter_map do |line|
135
138
  result = ToCoreModel.transform(line)
136
139
  next nil if result.nil?
137
140
  next result if result.is_a?(Coradoc::CoreModel::Base)
@@ -149,15 +152,21 @@ module Coradoc
149
152
  **extra_attrs
150
153
  )
151
154
  else
152
- content_lines = lines.map { |line| ToCoreModel.extract_text_content(line) }.join("\n")
153
- children = lines.map do |line|
154
- inline = ToCoreModel.transform_inline_content(line)
155
- inline = [Coradoc::CoreModel::TextContent.new(text: "")] if inline.empty?
155
+ paragraph_groups = group_block_lines_into_paragraphs(raw_lines)
156
+
157
+ content_lines = paragraph_groups.map do |group|
158
+ ToCoreModel.extract_text_content(group)
159
+ end.join("\n\n")
160
+
161
+ children = paragraph_groups.map do |group|
162
+ inline = ToCoreModel.transform_inline_content(group)
163
+ inline = [Coradoc::CoreModel::TextContent.new(text: '')] if inline.empty?
156
164
  Coradoc::CoreModel::ParagraphBlock.new(
157
- content: ToCoreModel.extract_text_content(line),
165
+ content: ToCoreModel.extract_text_content(group),
158
166
  children: Array(inline)
159
167
  )
160
168
  end
169
+
161
170
  klass.new(
162
171
  id: block.id,
163
172
  title: ToCoreModel.extract_title_text(block.title),
@@ -168,6 +177,35 @@ module Coradoc
168
177
  )
169
178
  end
170
179
  end
180
+
181
+ # AsciiDoc joins consecutive non-blank lines into one paragraph;
182
+ # blank lines (parsed as Model::LineBreak) separate paragraphs.
183
+ def group_block_lines_into_paragraphs(lines)
184
+ groups = []
185
+ current = []
186
+ lines.each do |line|
187
+ if line.is_a?(Coradoc::AsciiDoc::Model::LineBreak) ||
188
+ line.is_a?(Coradoc::AsciiDoc::Model::Break::PageBreak)
189
+ groups << current if current.any?
190
+ current = []
191
+ else
192
+ current << line
193
+ end
194
+ end
195
+ groups << current if current.any?
196
+ groups
197
+ end
198
+
199
+ # A line that should be emitted as a direct child of the
200
+ # enclosing block rather than joined into a paragraph sibling.
201
+ # The AsciiDoc model layer declares level via `block_level?`
202
+ # (true for Block::Core delimited blocks, BlockImage, Table,
203
+ # List::Core, Section, CommentBlock, Attached). Inline content
204
+ # (String, TextElement, LineBreak, PageBreak) returns false and
205
+ # stays inside paragraphs.
206
+ def block_level_child?(line)
207
+ line.is_a?(Coradoc::AsciiDoc::Model::Base) && line.block_level?
208
+ end
171
209
  end
172
210
  end
173
211
  end
@@ -48,9 +48,19 @@ module Coradoc
48
48
  id: section_id,
49
49
  level: section.level,
50
50
  title: title_text,
51
- children: content_children + nested_sections
51
+ children: content_children + nested_sections,
52
+ attributes: section_metadata_from(section)
52
53
  )
53
54
  end
55
+
56
+ # Delegates to Transform::AttributeListToMetadata for the typed
57
+ # Model::AttributeList -> CoreModel::Metadata conversion. The
58
+ # helper handles the nil/non-AttributeList guard and is the
59
+ # single source of truth shared with Builder::ElementBuilder.
60
+ def section_metadata_from(section)
61
+ Coradoc::AsciiDoc::Transform::AttributeListToMetadata
62
+ .call(section.attribute_list)
63
+ end
54
64
  end
55
65
  end
56
66
  end
@@ -27,9 +27,19 @@ module Coradoc
27
27
  def transform_image(image)
28
28
  src = image.src.to_s
29
29
  src = src[1..] if src.start_with?(':')
30
+ positional = image.attributes&.positional || []
31
+ positional_alt = positional.first&.value&.to_s || ''
32
+ caption = positional[1]&.value&.to_s
33
+ title = image.title&.to_s
34
+ # AsciiDoc block-title (`.Caption`) is semantically a caption,
35
+ # but for compatibility with the existing pipeline that treated
36
+ # it as alt when no positional alt was supplied, fall back to it.
37
+ alt = positional_alt.empty? ? title : positional_alt
30
38
  Coradoc::CoreModel::Image.new(
31
39
  src: src,
32
- alt: image.title&.to_s,
40
+ alt: alt,
41
+ caption: caption,
42
+ title: title,
33
43
  width: image.attributes&.[]('width'),
34
44
  height: image.attributes&.[]('height')
35
45
  )
@@ -50,9 +50,7 @@ module Coradoc
50
50
  {
51
51
  Coradoc::AsciiDoc::Model::Block::Quote => Coradoc::CoreModel::QuoteBlock,
52
52
  Coradoc::AsciiDoc::Model::Block::Example => Coradoc::CoreModel::ExampleBlock,
53
- Coradoc::AsciiDoc::Model::Block::Side => Coradoc::CoreModel::SidebarBlock,
54
- Coradoc::AsciiDoc::Model::Block::Literal => Coradoc::CoreModel::LiteralBlock,
55
- Coradoc::AsciiDoc::Model::Block::Pass => Coradoc::CoreModel::PassBlock
53
+ Coradoc::AsciiDoc::Model::Block::Side => Coradoc::CoreModel::SidebarBlock
56
54
  }.each do |block_class, core_model_class|
57
55
  Registry.register_with_priority(
58
56
  block_class,
@@ -61,6 +59,22 @@ module Coradoc
61
59
  )
62
60
  end
63
61
 
62
+ # Verbatim typed blocks (literal, pass) preserve their body
63
+ # byte-for-byte — same line-joining strategy as source/listing.
64
+ # Routing them through `transform_typed_block` would collapse
65
+ # intra-block whitespace and join consecutive lines as paragraphs.
66
+ Registry.register_with_priority(
67
+ Coradoc::AsciiDoc::Model::Block::Literal,
68
+ ->(model) { Blk.transform_literal_block(model) },
69
+ priority: 10
70
+ )
71
+
72
+ Registry.register_with_priority(
73
+ Coradoc::AsciiDoc::Model::Block::Pass,
74
+ ->(model) { Blk.transform_pass_block(model) },
75
+ priority: 10
76
+ )
77
+
64
78
  Registry.register_with_priority(
65
79
  Coradoc::AsciiDoc::Model::Block::Open,
66
80
  ->(model) { Blk.transform_open_block(model) },
@@ -14,6 +14,7 @@ module Coradoc
14
14
  autoload :ElementTransformers, "#{__dir__}/transform/element_transformers"
15
15
  autoload :FrontmatterAttributeMap, "#{__dir__}/transform/frontmatter_attribute_map"
16
16
  autoload :CalloutMerger, "#{__dir__}/transform/callout_merger"
17
+ autoload :AttributeListToMetadata, "#{__dir__}/transform/attribute_list_to_metadata"
17
18
  end
18
19
  end
19
20
  end
@@ -46,7 +46,7 @@ module Coradoc
46
46
  id = block_image[:id]
47
47
  title = block_image[:title]
48
48
  path = block_image[:path]
49
- attrs = AttributeListNormalizer.coerce(block_image[:attribute_list])
49
+ attrs = AttributeListNormalizer.coerce(block_image[:attribute_list_macro])
50
50
  Model::Image::BlockImage.new(
51
51
  title: title,
52
52
  id: id,
@@ -47,8 +47,12 @@ module Coradoc
47
47
  rule(list_item: subtree(:list_item)) do
48
48
  marker = list_item[:marker]
49
49
  id = list_item[:id]
50
- text = list_item[:text]
51
- text = list_item[:text].to_s if list_item[:text].instance_of?(Parslet::Slice)
50
+ lines = list_item[:lines]
51
+ content = if lines
52
+ Transformer.lines_to_text_elements(lines)
53
+ else
54
+ list_item[:text].to_s
55
+ end
52
56
  attached = list_item[:attached]
53
57
  nested = list_item[:nested]
54
58
  line_break = list_item[:line_break]
@@ -70,7 +74,7 @@ module Coradoc
70
74
  end
71
75
 
72
76
  Model::List::Item.new(
73
- content: text, id:, marker:, attached:, nested:, line_break:
77
+ content: content, id:, marker:, attached:, nested:, line_break:
74
78
  )
75
79
  end
76
80
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Coradoc
4
4
  module AsciiDoc
5
- VERSION = '2.0.16'
5
+ VERSION = '2.0.18'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: coradoc-adoc
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.16
4
+ version: 2.0.18
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -314,6 +314,7 @@ files:
314
314
  - lib/coradoc/asciidoc/serializer/serializers/video.rb
315
315
  - lib/coradoc/asciidoc/serializer/spacing_strategy.rb
316
316
  - lib/coradoc/asciidoc/transform.rb
317
+ - lib/coradoc/asciidoc/transform/attribute_list_to_metadata.rb
317
318
  - lib/coradoc/asciidoc/transform/callout_merger.rb
318
319
  - lib/coradoc/asciidoc/transform/element_transformers.rb
319
320
  - lib/coradoc/asciidoc/transform/element_transformers/block_transformer.rb