coradoc-adoc 2.0.21 → 2.0.22

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: 1c4b5749f4fd1a691eb76fe9f7848b1d4bc2cd2e43cd2ee0a5f1afdcb697459c
4
- data.tar.gz: 90e6bf66da9a7c4dba7c7eafbdabf378259e4b9792a93d4c88209d52bd991876
3
+ metadata.gz: 42db73896b69e9e5344105625afe528bd2b7b8f57a11822f61881e782be8b974
4
+ data.tar.gz: bc3e5e3073992f0d2b71e12ab62fa07d11cdf6ded25299d673f863522cd6291d
5
5
  SHA512:
6
- metadata.gz: e421d05a3a2c43387471224818a5261229478c1539ddade33a30957be8d7fb0775c9b0bb95b59afa32cb4e84f0adab07b62164113a521635fd5e70cdbe66ecec
7
- data.tar.gz: dd6c2ea15fcb4c3e521a929006ba2e7ebbff5f6548902a2d7947cdac04a9005a792a9ae4fb07d68f53e30c844de9cbd00bd62a0fa034fa9f9c298209fe50ec7e
6
+ metadata.gz: 334c5ae5f1860ccde94bb518e8bc7f91be7dcce338d863d93ec137acbfedce8f742be44336fde3e4cec7ea6709b3f309c484c716ceb8c485da2830312515d59e
7
+ data.tar.gz: 3f387aab6c8b6d8862319593b9c2b8c2304758c4c66f15aed0e5ac57209f2f35b4930d1d95a57b2cf4d940834599db35395546c81b8d7760384062d0a7a48d49
@@ -4,13 +4,22 @@ module Coradoc
4
4
  module AsciiDoc
5
5
  module Parser
6
6
  module Admonition
7
+ # Match a single style name case-insensitively. Each character
8
+ # class `[Xx]` lets the same alternation accept +note+, +Note+,
9
+ # and +NOTE+ without three separate str() branches per style.
10
+ def case_insensitive_str(s)
11
+ s.chars.reduce(nil) do |acc, ch|
12
+ node = match("[#{ch.upcase}#{ch.downcase}]")
13
+ acc.nil? ? node : (acc >> node)
14
+ end
15
+ end
16
+
7
17
  def admonition_type
8
- str('NOTE') | str('TIP') | str('EDITOR') |
9
- str('IMPORTANT') | str('WARNING') | str('CAUTION') |
10
- str('TODO')
11
- # requires atypical syntax for access?
12
- # | str('DANGER')
13
- # | str('SAFETY PRECAUTION')
18
+ styles = Coradoc::AsciiDoc::Transform::ElementTransformers::AdmonitionStyles.all_styles
19
+ styles.reduce(nil) do |acc, style|
20
+ matcher = case_insensitive_str(style)
21
+ acc.nil? ? matcher : (acc | matcher)
22
+ end
14
23
  end
15
24
 
16
25
  def admonition_line
@@ -120,20 +120,38 @@ module Coradoc
120
120
  (str('image:').present? >> str('image:') >>
121
121
  str(':').absent? >>
122
122
  match('[A-Za-z0-9_.\\-:/&?=+,%#~;]+').repeat(1).as(:path) >>
123
- (str('[') >> match('[^\\]]').repeat(1).as(:text) >> str(']')).maybe
123
+ attribute_list(:attribute_list).maybe
124
124
  ).as(:inline_image)
125
125
  end
126
126
 
127
127
  # Triple-plus inline passthrough: `+++raw content+++`. The content
128
128
  # passes through all substitutions verbatim. Common use is to embed
129
129
  # raw HTML in AsciiDoc documents.
130
- def inline_passthrough
130
+ def inline_passthrough_triple_plus
131
131
  (str('+++') >>
132
132
  (str('+++').absent? >> match('[^\n]')).repeat(1).as(:raw) >>
133
133
  str('+++')
134
134
  ).as(:inline_passthrough)
135
135
  end
136
136
 
137
+ # `pass:[raw]` macro form. Equivalent semantic to triple-plus:
138
+ # the bracket payload survives all substitutions verbatim. Common
139
+ # use is inside monospace spans to keep characters like `<` from
140
+ # being re-interpreted as xref markers. The optional `subs` segment
141
+ # (`pass:quotes[...]`) is consumed but currently ignored — the
142
+ # payload is always passed through raw.
143
+ def inline_passthrough_macro
144
+ (str('pass:').present? >> str('pass:') >>
145
+ match('[a-zA-Z,+]').repeat(0) >>
146
+ str('[') >> (str(']]').absent? >> match('[^\]\n]')).repeat(1).as(:raw) >>
147
+ str(']')
148
+ ).as(:inline_passthrough)
149
+ end
150
+
151
+ def inline_passthrough
152
+ inline_passthrough_triple_plus | inline_passthrough_macro
153
+ end
154
+
137
155
  def underline
138
156
  (attribute_list >> match('\\[.underline\\]').as(:role) >>
139
157
  str('#') >>
@@ -157,10 +175,26 @@ module Coradoc
157
175
  str('link:').present? |
158
176
  str('image:').present? |
159
177
  str('+++').present? |
178
+ str('pass:').present? |
160
179
  term_type.present? |
161
180
  str('footnote').present? |
162
181
  stem_type.present? |
163
- str('\\<<').present?
182
+ str('\\<<').present? |
183
+ hard_line_break_marker?
184
+ end
185
+
186
+ # AsciiDoc hard line break: a space followed by `+` at end of line,
187
+ # or a backslash at end of line. Both forms render as `<br>` inside
188
+ # the enclosing paragraph/verse. Recognised ahead of `text_unformatted`
189
+ # so the marker isn't swallowed as plain text.
190
+ def hard_line_break_marker?
191
+ (str(' +') >> str("\n")).present? |
192
+ (str('\\') >> str("\n")).present?
193
+ end
194
+
195
+ def hard_line_break
196
+ ((str(' +') >> str("\n")) |
197
+ (str('\\') >> str("\n"))).as(:hard_line_break)
164
198
  end
165
199
 
166
200
  def inline
@@ -187,7 +221,8 @@ module Coradoc
187
221
  inline_image |
188
222
  inline_passthrough |
189
223
  underline |
190
- small
224
+ small |
225
+ hard_line_break
191
226
  end
192
227
 
193
228
  def text_unformatted
@@ -149,8 +149,13 @@ module Coradoc
149
149
  end
150
150
 
151
151
  def dlist_definition
152
- text
153
- .as(:definition) >> line_ending >> empty_line.repeat(0)
152
+ # AsciiDoc convention: the definition body is indented relative
153
+ # to the term. That leading whitespace is structural (marks
154
+ # the line as a continuation of the dlist item), not content.
155
+ # Consume it without capturing so downstream CoreModel text
156
+ # doesn't carry the source indentation into HTML/Markdown.
157
+ (match('[ \t]').repeat(0) >> text.as(:definition)) >>
158
+ line_ending >> empty_line.repeat(0)
154
159
  end
155
160
 
156
161
  def dlist_item(_delimiter = nil)
@@ -26,6 +26,21 @@ module Coradoc
26
26
  BUILTIN.include?(name) || custom.include?(name)
27
27
  end
28
28
 
29
+ # Canonical uppercase form for +style+, or nil if unknown.
30
+ # Single source of truth for the canonical casing used in
31
+ # CoreModel::AnnotationBlock#annotation_type and AsciiDoc
32
+ # round-trip output.
33
+ def canonicalize(style)
34
+ return nil unless admonition?(style)
35
+
36
+ style.to_s.upcase
37
+ end
38
+
39
+ # All registered style names (BUILTIN + custom), lowercased.
40
+ def all_styles
41
+ (BUILTIN + custom).freeze
42
+ end
43
+
29
44
  # Register an additional admonition style. Open for extension.
30
45
  def register(style)
31
46
  name = style.to_s.downcase
@@ -71,16 +71,26 @@ module Coradoc
71
71
 
72
72
  # Dispatch entry point for any block whose AsciiDoc model carries
73
73
  # an attribute list (delimited blocks + open blocks). Per the
74
- # AsciiDoc spec, every delimited block accepts admonition styles
75
- # in its attribute line. Admonition style wins for example,
76
- # sidebar, quote, and open blocks; verbatim blocks (source,
77
- # listing) intentionally ignore it because their verbatim
78
- # semantics are stronger than the annotation label.
74
+ # AsciiDoc spec, every delimited block accepts both admonition
75
+ # labels and verbatim/stem casts in its attribute line. The cast
76
+ # is resolved by +block_cast_semantic+ in MECE order:
77
+ # admonition > stem > verbatim > fallback.
79
78
  def transform_with_admonition_check(block, native_class)
80
- style = first_positional_attr(block)
81
- return transform_admonition_block(block, style) if AdmonitionStyles.admonition?(style)
82
-
83
- transform_typed_block(block, native_class)
79
+ semantic = block_cast_semantic(block)
80
+ case semantic
81
+ when :admonition
82
+ transform_admonition_block(block, first_positional_attr(block))
83
+ when :stem
84
+ transform_stem_block(block, first_positional_attr(block))
85
+ when :source
86
+ transform_source_block(block)
87
+ when :listing
88
+ transform_listing_from_open(block)
89
+ when :literal
90
+ transform_literal_from_open(block)
91
+ else
92
+ transform_typed_block(block, native_class)
93
+ end
84
94
  end
85
95
 
86
96
  def transform_example_block(block)
@@ -98,36 +108,32 @@ module Coradoc
98
108
  # Open blocks (`--`) are generic containers. AsciiDoc allows
99
109
  # casting them to a different block type via positional
100
110
  # attributes: verbatim types (`[source]`, `[listing]`,
101
- # `[literal]`) and admonition labels (`[NOTE]`, `[TIP]`,
111
+ # `[literal]`), STEM types (`[stem]`, `[latexmath]`,
112
+ # `[asciimath]`), and admonition labels (`[NOTE]`, `[TIP]`,
102
113
  # `[WARNING]`, `[CAUTION]`, `[IMPORTANT]`). When such a cast
103
114
  # is present, the block behaves like the corresponding
104
115
  # delimited block. Anything else stays an OpenBlock.
105
116
  def transform_open_block(block)
106
- semantic = open_block_semantic(block)
107
- case semantic
108
- when :source
109
- transform_source_block(block)
110
- when :listing
111
- transform_listing_from_open(block)
112
- when :literal
113
- transform_literal_from_open(block)
114
- when :admonition
115
- transform_admonition_block(block, first_positional_attr(block))
116
- else
117
- transform_typed_block(block, Coradoc::CoreModel::OpenBlock)
118
- end
117
+ transform_with_admonition_check(block, Coradoc::CoreModel::OpenBlock)
119
118
  end
120
119
 
121
120
  VERBATILE_CAST_STYLES = %w[source listing literal].freeze
122
121
 
123
- def open_block_semantic(block)
122
+ # Resolve a delimited block's cast from its first positional
123
+ # attribute. Single source of truth for the cast ladder used
124
+ # by every block-form transformer (example/sidebar/quote/open).
125
+ # Returns one of: :admonition, :stem, :source, :listing,
126
+ # :literal, or nil (no cast — use the block's native class).
127
+ def block_cast_semantic(block)
124
128
  style = first_positional_attr(block)
125
129
  return nil unless style
126
130
 
127
- case style.to_s.downcase
128
- when *VERBATILE_CAST_STYLES then :"#{style.downcase}"
129
- else :admonition if AdmonitionStyles.admonition?(style)
130
- end
131
+ style_str = style.to_s.downcase
132
+ return :admonition if AdmonitionStyles.admonition?(style)
133
+ return :stem if STEM_STYLES.key?(style_str)
134
+ return style_str.to_sym if VERBATILE_CAST_STYLES.include?(style_str)
135
+
136
+ nil
131
137
  end
132
138
 
133
139
  # Returns the first positional attribute on the block's
@@ -148,10 +154,14 @@ module Coradoc
148
154
  # (example / sidebar / quote / pass / open). Builds an
149
155
  # AnnotationBlock with annotation_type set from the style and
150
156
  # the block's body lines joined into a single content string.
157
+ # Type is canonicalised via AdmonitionStyles so every code path
158
+ # (line admonition, block admonition, attribute-cast admonition)
159
+ # produces the same uppercase key.
151
160
  def transform_admonition_block(block, type)
161
+ canonical = AdmonitionStyles.canonicalize(type) || type.to_s
152
162
  content_lines = Array(block.lines).map { |line| ToCoreModel.extract_text_content(line) }.join("\n")
153
163
  Coradoc::CoreModel::AnnotationBlock.new(
154
- annotation_type: type.to_s.downcase,
164
+ annotation_type: canonical,
155
165
  content: content_lines,
156
166
  title: ToCoreModel.extract_title_text(block.title)
157
167
  )
@@ -13,6 +13,7 @@ module Coradoc
13
13
  children = CalloutMerger.call(children)
14
14
  children = prepend_frontmatter(children, doc.frontmatter)
15
15
  children = insert_title_heading_after_frontmatter(children, title_text)
16
+ children = resolve_attribute_references(children, attributes)
16
17
 
17
18
  Coradoc::CoreModel::DocumentElement.new(
18
19
  id: doc.id,
@@ -58,6 +59,16 @@ module Coradoc
58
59
  children.insert(frontmatter_count, title_heading)
59
60
  end
60
61
 
62
+ # Resolve `{name}` attribute references in the body against the
63
+ # document's own declared attributes. Single source of truth:
64
+ # the resolver lives in core so other format gems can reuse it
65
+ # when they have equivalent reference macros.
66
+ def resolve_attribute_references(children, attributes)
67
+ return children if attributes.nil? || attributes.keys.empty?
68
+
69
+ Coradoc::CoreModel::AttributeReferenceResolver.call(children, attributes)
70
+ end
71
+
61
72
  def transform_section(section, parent_id: nil)
62
73
  title_text = ToCoreModel.extract_title_text(section.title)
63
74
  section_id = section.id || Coradoc::CoreModel::IdGenerator.generate_from_title(
@@ -67,20 +67,28 @@ module Coradoc
67
67
  )
68
68
  li.children = children
69
69
 
70
- if item.nested.is_a?(Coradoc::AsciiDoc::Model::List::Core)
71
- nested_core = transform_list(item.nested, list_marker_type(item.nested))
72
- li.children << nested_core
73
- elsif item.nested.is_a?(Array)
74
- item.nested.each do |n|
75
- next unless n.is_a?(Coradoc::AsciiDoc::Model::List::Core)
76
-
77
- li.children << transform_list(n, list_marker_type(n))
78
- end
79
- end
80
-
70
+ nested_lists = extract_nested_lists(item)
71
+ li.nested_list = nested_lists.first if nested_lists.size == 1
81
72
  li
82
73
  end
83
74
 
75
+ # Pull every nested List::Core off the AsciiDoc model item and
76
+ # transform each into a CoreModel::ListBlock. Returns [] when
77
+ # the item has no nested lists. Single source of truth for the
78
+ # nested-list shape so transform_list_item and any future caller
79
+ # share the same extraction logic.
80
+ def extract_nested_lists(item)
81
+ nested = item.nested
82
+ return [] if nested.nil?
83
+
84
+ candidates = nested.is_a?(Array) ? nested : [nested]
85
+ candidates.filter_map do |n|
86
+ next unless n.is_a?(Coradoc::AsciiDoc::Model::List::Core)
87
+
88
+ transform_list(n, list_marker_type(n))
89
+ end
90
+ end
91
+
84
92
  def list_marker_type(list)
85
93
  case list
86
94
  when Coradoc::AsciiDoc::Model::List::Ordered then 'ordered'
@@ -15,9 +15,10 @@ module Coradoc
15
15
  end
16
16
 
17
17
  def transform_admonition(admonition)
18
+ canonical = Coradoc::AsciiDoc::Transform::ElementTransformers::AdmonitionStyles.canonicalize(admonition.type) || admonition.type.to_s
18
19
  children = ToCoreModel.transform_inline_content(admonition.content)
19
20
  block = Coradoc::CoreModel::AnnotationBlock.new(
20
- annotation_type: admonition.type,
21
+ annotation_type: canonical,
21
22
  content: ToCoreModel.extract_text_content(admonition.content)
22
23
  )
23
24
  block.children = children
@@ -319,6 +319,8 @@ module Coradoc
319
319
  )
320
320
  when 'raw_inline'
321
321
  Coradoc::AsciiDoc::Model::Inline::Passthrough.new(content: inline.content)
322
+ when 'hard_line_break'
323
+ Coradoc::AsciiDoc::Model::Inline::HardLineBreak.new
322
324
  else
323
325
  Coradoc::AsciiDoc::Model::TextElement.new(content: inline.content)
324
326
  end
@@ -172,9 +172,11 @@ module Coradoc
172
172
  Registry.register(
173
173
  Coradoc::AsciiDoc::Model::Inline::AttributeReference,
174
174
  lambda { |model|
175
+ name = model.name.to_s
175
176
  Coradoc::CoreModel::InlineElement.new(
176
177
  format_type: 'attribute_reference',
177
- content: "{#{model.name}}"
178
+ target: name,
179
+ content: "{#{name}}"
178
180
  )
179
181
  }
180
182
  )
@@ -246,6 +248,16 @@ module Coradoc
246
248
  ->(model) { Inc.transform_include(model) }
247
249
  )
248
250
 
251
+ # Inline hard line break — distinct from the structural
252
+ # Model::LineBreak (which represents paragraph-separator blank
253
+ # lines and is dropped at the CoreModel boundary). Hard breaks
254
+ # are semantic and round-trip as CoreModel::HardLineBreakElement
255
+ # so HTML/Markdown renderers can map them to <br>.
256
+ Registry.register(
257
+ Coradoc::AsciiDoc::Model::Inline::HardLineBreak,
258
+ ->(_model) { Coradoc::CoreModel::HardLineBreakElement.new(content: '') }
259
+ )
260
+
249
261
  [
250
262
  Coradoc::AsciiDoc::Model::Audio,
251
263
  Coradoc::AsciiDoc::Model::Video,
@@ -37,12 +37,15 @@ module Coradoc
37
37
  Model::Block::Example.new(title: '', lines: example)
38
38
  end
39
39
 
40
- # Admonition
40
+ # Admonition. Canonicalise the type to uppercase so round-trips
41
+ # through HTML/Markdown/DocBook all see the same key used by
42
+ # icon and CSS-class lookups.
41
43
  rule(
42
44
  admonition_type: simple(:admonition_type),
43
45
  content: sequence(:content)
44
46
  ) do
45
- Model::Admonition.new(content: content, type: admonition_type.to_s)
47
+ canonical = Coradoc::AsciiDoc::Transform::ElementTransformers::AdmonitionStyles.canonicalize(admonition_type.to_s)
48
+ Model::Admonition.new(content: content, type: canonical)
46
49
  end
47
50
 
48
51
  # Block image
@@ -36,14 +36,17 @@ module Coradoc
36
36
 
37
37
  # Inline image
38
38
  rule(inline_image: subtree(:inline_image)) do
39
+ attrs = AttributeListNormalizer.coerce(inline_image[:attribute_list])
39
40
  Model::Image::InlineImage.new(
40
- title: inline_image[:text],
41
41
  src: inline_image[:path],
42
- attributes: inline_image[:attribute_list]
42
+ attributes: attrs
43
43
  )
44
44
  end
45
45
 
46
- # Inline passthrough (`+++raw content+++`)
46
+ # Inline passthrough (`+++raw content+++` or `pass:[raw]`).
47
+ # Both forms carry an opaque payload that survives all
48
+ # substitutions verbatim; `form` records which syntax was
49
+ # used so the AsciiDoc serializer can round-trip faithfully.
47
50
  rule(inline_passthrough: subtree(:passthrough)) do
48
51
  Model::Inline::Passthrough.new(
49
52
  content: passthrough[:raw].to_s,
@@ -56,6 +59,15 @@ module Coradoc
56
59
  Model::Inline::AttributeReference.new(name:)
57
60
  end
58
61
 
62
+ # Hard line break (` +\n` or `\\n`). Emitted as a dedicated
63
+ # AsciiDoc model (Inline::HardLineBreak) distinct from
64
+ # Model::LineBreak, which only represents paragraph-separator
65
+ # blank lines. Hard breaks carry semantic meaning: HTML/Markdown
66
+ # renderers map them to <br>.
67
+ rule(hard_line_break: simple(:_)) do
68
+ Model::Inline::HardLineBreak.new
69
+ end
70
+
59
71
  # Term
60
72
  rule(
61
73
  term_type: simple(:term_type),
@@ -20,12 +20,24 @@ module Coradoc
20
20
  cell_opts = {}
21
21
  style = parse_format(format, cell_opts)
22
22
 
23
- unescaped_content = content.to_s.gsub(/\\([|!,:;])/, '\1')
23
+ unescaped_content = cell_content_to_string(content).gsub(/\\([|!,:;])/, '\1')
24
24
  cell_opts[:content] = parse_inline_content(unescaped_content, style)
25
25
 
26
26
  Model::TableCell.new(**cell_opts)
27
27
  end
28
28
 
29
+ # Coerce the parser-supplied cell content into a plain String.
30
+ # The cell parser emits `text:` as either a single Parslet::Slice
31
+ # or an Array of slices (from `.repeat(0)`). An empty Array must
32
+ # map to "" — Ruby's `[].to_s` returns "[]", which previously
33
+ # leaked into TableCell content as a phantom "[]" cell.
34
+ def cell_content_to_string(content)
35
+ return '' if content.nil?
36
+ return content.map(&:to_s).join if content.is_a?(Array)
37
+
38
+ content.to_s
39
+ end
40
+
29
41
  # Coerce a raw parser cell value into a TableCell.
30
42
  # Used by TableLayout.group_cells_into_rows when it encounters
31
43
  # cells that the parser emitted as Hashes or plain strings.
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Coradoc
4
4
  module AsciiDoc
5
- VERSION = '2.0.21'
5
+ VERSION = '2.0.22'
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.21
4
+ version: 2.0.22
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.