coradoc-adoc 2.0.20 → 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: fad1b11f8028b7157066b7a2d4474b2d20e3cab1e526e0347ca36015cdf3ad90
4
- data.tar.gz: d143bb70ec7ab2e3a1656a5e9ed72d52bb0b2f1341b3566efa02ef44343a2044
3
+ metadata.gz: 42db73896b69e9e5344105625afe528bd2b7b8f57a11822f61881e782be8b974
4
+ data.tar.gz: bc3e5e3073992f0d2b71e12ab62fa07d11cdf6ded25299d673f863522cd6291d
5
5
  SHA512:
6
- metadata.gz: 7848b8f80f104e960265ed5dbd295d4dfc7ad4cecb401b81765ae21ba6d16a31e02024ebfd7c65c1c86e0fd433512788a92ab4600b348293d895980bf2b5b446
7
- data.tar.gz: c6a55d7b4abecd086fcbde8015fd54b217eb3a442f86699bd1ab110affc1f1dc20f9ff54ef24668dcc77fdcb1cf609b09651308bc4122ea47a05535e18585879
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)
@@ -4,9 +4,12 @@ module Coradoc
4
4
  module AsciiDoc
5
5
  module Serializer
6
6
  # Registry for mapping Coradoc model classes to their AsciiDoc serializers.
7
- # This is the authoritative source for model→serializer mappings.
8
7
  #
9
- # Pattern mirrors Input::Html::Converters registry for symmetry.
8
+ # Thin layer over `Coradoc::Dispatch.strict`: each public method
9
+ # delegates to a single Dispatch instance so the dispatch mechanism
10
+ # (storage, miss-handling, override semantics) lives in one place.
11
+ # Adoc-specific concerns (the ArgumentError wording on miss, the
12
+ # `registered_models` name) stay here.
10
13
  #
11
14
  # @example Registering a custom serializer
12
15
  # ElementRegistry.override(Model::Paragraph, CustomParagraphSerializer)
@@ -15,78 +18,45 @@ module Coradoc
15
18
  # original = ElementRegistry.get(Model::Paragraph)
16
19
  # ElementRegistry.override(Model::Paragraph, WrapperSerializer.new(original))
17
20
  class ElementRegistry
21
+ DISPATCH = Coradoc::Dispatch.strict
22
+
18
23
  class << self
19
- # Register a serializer for a model class
20
- # @param model_class [Class] The Coradoc model class
21
- # @param serializer_class [Class] The serializer class
22
24
  def register(model_class, serializer_class)
23
- registry[model_class] = serializer_class
25
+ DISPATCH.register(model_class, serializer_class)
24
26
  end
25
27
 
26
- # Override a serializer for a model class
27
- # This is an alias for register that makes the intent explicit
28
- # @param model_class [Class] The Coradoc model class
29
- # @param serializer_class [Class] The new serializer class
30
- # @return [Class, nil] The previous serializer class, or nil if none
31
28
  def override(model_class, serializer_class)
32
- previous = registry[model_class]
33
- registry[model_class] = serializer_class
34
- previous
29
+ DISPATCH.override(model_class, serializer_class)
35
30
  end
36
31
 
37
- # Unregister a serializer for a model class
38
- # @param model_class [Class] The model class to unregister
39
- # @return [Class, nil] The removed serializer class, or nil if none
40
32
  def unregister(model_class)
41
- registry.delete(model_class)
33
+ DISPATCH.unregister(model_class)
42
34
  end
43
35
 
44
- # Get the serializer for a model class without raising
45
- # @param model_class [Class] The model class
46
- # @return [Class, nil] The serializer class, or nil if not registered
47
36
  def get(model_class)
48
- registry[model_class]
37
+ DISPATCH.lookup(model_class)
49
38
  end
50
39
 
51
- # Lookup serializer for a model class
52
- # @param model_class [Class] The model class
53
- # @return [Class] The serializer class
54
- # @raise [ArgumentError] If no serializer is registered
55
40
  def lookup(model_class)
56
- serializer_class = registry[model_class]
57
-
58
- unless serializer_class
59
- raise ArgumentError,
60
- "No serializer registered for #{model_class.name}. " \
61
- 'Please register a serializer in ElementRegistry, or the serializer ' \
62
- 'may not have been loaded yet (check Registrations.load_all!)'
63
- end
41
+ serializer_class = DISPATCH.lookup(model_class)
42
+ return serializer_class if serializer_class
64
43
 
65
- serializer_class
44
+ raise ArgumentError,
45
+ "No serializer registered for #{model_class.name}. " \
46
+ 'Please register a serializer in ElementRegistry, or the serializer ' \
47
+ 'may not have been loaded yet (check Registrations.load_all!)'
66
48
  end
67
49
 
68
- # Get all registered model classes
69
- # @return [Array<Class>] Array of registered model classes
70
50
  def registered_models
71
- registry.keys
51
+ DISPATCH.registered_keys
72
52
  end
73
53
 
74
- # Check if a model class has a registered serializer
75
- # @param model_class [Class] The model class
76
- # @return [Boolean] True if registered
77
54
  def registered?(model_class)
78
- registry.key?(model_class)
55
+ DISPATCH.registered?(model_class)
79
56
  end
80
57
 
81
- # Clear all registrations (mainly for testing)
82
58
  def clear!
83
- registry.clear
84
- end
85
-
86
- # Get the registry hash
87
- # @return [Hash] Model class => Serializer class mapping
88
- def registry
89
- @@registry ||= {}
59
+ DISPATCH.clear!
90
60
  end
91
61
  end
92
62
  end
@@ -3,104 +3,21 @@
3
3
  module Coradoc
4
4
  module AsciiDoc
5
5
  module Serializer
6
- # Trigger loading of all serializer registrations
6
+ # Trigger loading of all serializer registrations.
7
7
  #
8
- # Each serializer file self-registers when loaded via autoload.
9
- # This module triggers the autoload of all serializers by accessing
10
- # their constants, which causes the registration code in each file to execute.
8
+ # Each serializer file self-registers when loaded via
9
+ # `ElementRegistry.register(...)` at file bottom. Walking the
10
+ # serializers directory is the single source of truth for "which
11
+ # serializers exist" — adding a file is the only step required for
12
+ # it to register.
11
13
  module Registrations
12
14
  class << self
13
- # Load all serializers to trigger their registration
14
- # rubocop:disable Lint/Void - Constants are referenced to trigger autoload
15
15
  def load_all!
16
- # Top-level serializers
17
- Serializers::Base
18
- Serializers::Admonition
19
- Serializers::Attribute
20
- Serializers::AttributeList
21
- Serializers::AttributeListAttribute
22
- Serializers::Audio
23
- Serializers::Author
24
- Serializers::Bibliography
25
- Serializers::BibliographyEntry
26
- Serializers::Break
27
- Serializers::CommentBlock
28
- Serializers::CommentLine
29
- Serializers::Document
30
- Serializers::DocumentAttributes
31
- Serializers::Header
32
- Serializers::Highlight
33
- Serializers::Include
34
- Serializers::LineBreak
35
- Serializers::List
36
- Serializers::NamedAttribute
37
- Serializers::Paragraph
38
- Serializers::ReviewerNote
39
- Serializers::Revision
40
- Serializers::Section
41
- Serializers::Tag
42
- Serializers::TableCell
43
- Serializers::TableRow
44
- Serializers::Table
45
- Serializers::Term
46
- Serializers::TextElement
47
- Serializers::Title
48
- Serializers::Video
49
-
50
- # Block serializers
51
- Serializers::Block
52
- Serializers::Block::Core
53
- Serializers::Block::Example
54
- Serializers::Block::Listing
55
- Serializers::Block::Literal
56
- Serializers::Block::Open
57
- Serializers::Block::Pass
58
- Serializers::Block::Quote
59
- Serializers::Block::ReviewerComment
60
- Serializers::Block::Side
61
- Serializers::Block::SourceCode
62
-
63
- # Image serializers
64
- Serializers::Image
65
- Serializers::Image::Core
66
-
67
- # Inline serializers
68
- Serializers::Inline
69
- Serializers::Inline::Anchor
70
- Serializers::Inline::AttributeReference
71
- Serializers::Inline::Bold
72
- Serializers::Inline::CrossReference
73
- Serializers::Inline::CrossReferenceArg
74
- Serializers::Inline::Footnote
75
- Serializers::Inline::HardLineBreak
76
- Serializers::Inline::Highlight
77
- Serializers::Inline::Italic
78
- Serializers::Inline::Link
79
- Serializers::Inline::Monospace
80
- Serializers::Inline::Quotation
81
- Serializers::Inline::Small
82
- Serializers::Inline::Span
83
- Serializers::Inline::Stem
84
- Serializers::Inline::Strikethrough
85
- Serializers::Inline::Subscript
86
- Serializers::Inline::Superscript
87
- Serializers::Inline::Underline
88
-
89
- # List serializers
90
- Serializers::List
91
- Serializers::List::Core
92
- Serializers::List::Definition
93
- Serializers::List::DefinitionItem
94
- Serializers::List::Item
95
- Serializers::List::Ordered
96
- Serializers::List::Unordered
97
-
16
+ Dir["#{__dir__}/serializers/**/*.rb"].sort.each { |path| require path }
98
17
  true
99
18
  end
100
- # rubocop:enable Lint/Void
101
19
  end
102
20
 
103
- # Auto-load all on module inclusion
104
21
  load_all!
105
22
  end
106
23
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module AsciiDoc
5
+ module Serializer
6
+ module Serializers
7
+ module Inline
8
+ # Serializer for the inline passthrough (`+++raw content+++`).
9
+ # Wraps the raw payload in triple-plus delimiters so downstream
10
+ # consumers know to skip inline substitution.
11
+ class Passthrough < Base
12
+ def to_adoc(model, _options = {})
13
+ content = serialize_content(model.content)
14
+ return '' if content.empty?
15
+
16
+ "+++#{content}+++"
17
+ end
18
+ end
19
+ end
20
+
21
+ ElementRegistry.register(Coradoc::AsciiDoc::Model::Inline::Passthrough, Inline::Passthrough)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -17,6 +17,7 @@ module Coradoc
17
17
  autoload :Italic, 'coradoc/asciidoc/serializer/serializers/inline/italic'
18
18
  autoload :Link, 'coradoc/asciidoc/serializer/serializers/inline/link'
19
19
  autoload :Monospace, 'coradoc/asciidoc/serializer/serializers/inline/monospace'
20
+ autoload :Passthrough, 'coradoc/asciidoc/serializer/serializers/inline/passthrough'
20
21
  autoload :Quotation, 'coradoc/asciidoc/serializer/serializers/inline/quotation'
21
22
  autoload :Small, 'coradoc/asciidoc/serializer/serializers/inline/small'
22
23
  autoload :Span, 'coradoc/asciidoc/serializer/serializers/inline/span'
@@ -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
@@ -5,8 +5,6 @@ module Coradoc
5
5
  module Transform
6
6
  # Transforms CoreModel to AsciiDoc models
7
7
  class FromCoreModel
8
- include Coradoc::Transform::Base
9
-
10
8
  @registered = false
11
9
 
12
10
  class << self
@@ -319,6 +317,10 @@ module Coradoc
319
317
  type: inline.stem_type || 'latexmath',
320
318
  content: inline.content
321
319
  )
320
+ when 'raw_inline'
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
@@ -567,6 +569,8 @@ module Coradoc
567
569
  end
568
570
  end
569
571
  end
572
+
573
+ def transform(model) = self.class.transform(model)
570
574
  end
571
575
  end
572
576
  end
@@ -4,8 +4,6 @@ module Coradoc
4
4
  module AsciiDoc
5
5
  module Transform
6
6
  class ToCoreModel
7
- include Coradoc::Transform::Base
8
-
9
7
  @registered = false
10
8
 
11
9
  class << self
@@ -133,6 +131,8 @@ module Coradoc
133
131
  text
134
132
  end
135
133
  end
134
+
135
+ def transform(model) = self.class.transform(model)
136
136
  end
137
137
  end
138
138
  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.20'
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.20
4
+ version: 2.0.22
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -283,6 +283,7 @@ files:
283
283
  - lib/coradoc/asciidoc/serializer/serializers/inline/italic.rb
284
284
  - lib/coradoc/asciidoc/serializer/serializers/inline/link.rb
285
285
  - lib/coradoc/asciidoc/serializer/serializers/inline/monospace.rb
286
+ - lib/coradoc/asciidoc/serializer/serializers/inline/passthrough.rb
286
287
  - lib/coradoc/asciidoc/serializer/serializers/inline/quotation.rb
287
288
  - lib/coradoc/asciidoc/serializer/serializers/inline/small.rb
288
289
  - lib/coradoc/asciidoc/serializer/serializers/inline/span.rb