coradoc-adoc 2.0.19 → 2.0.21

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: 817eba65c55a5dea765ab8e80ee5542a1353741659c8a32ef4eb80e39f052dcb
4
- data.tar.gz: 25a07bc63696b7ba27933c9724c0a5ada466dc7cd0f37350ae4c9a4a475afbec
3
+ metadata.gz: 1c4b5749f4fd1a691eb76fe9f7848b1d4bc2cd2e43cd2ee0a5f1afdcb697459c
4
+ data.tar.gz: 90e6bf66da9a7c4dba7c7eafbdabf378259e4b9792a93d4c88209d52bd991876
5
5
  SHA512:
6
- metadata.gz: c3d26bcc8f91f799f455f051109933875545219b14e9556cc4d5774dcda16e9abdcb1f0e6a090f1d9aea35c49d4d7f5ed8c6bae2b1e47a76cc75a86270e47cd7
7
- data.tar.gz: 50a575b3666d1d6df0a8813a9761345ced5d895b2c73bf80347d8fb24e36c396389d4901fa22a6f34e1adcea6cb4d5b697bdab121dc495603da88740753e821d
6
+ metadata.gz: e421d05a3a2c43387471224818a5261229478c1539ddade33a30957be8d7fb0775c9b0bb95b59afa32cb4e84f0adab07b62164113a521635fd5e70cdbe66ecec
7
+ data.tar.gz: dd6c2ea15fcb4c3e521a929006ba2e7ebbff5f6548902a2d7947cdac04a9005a792a9ae4fb07d68f53e30c844de9cbd00bd62a0fa034fa9f9c298209fe50ec7e
@@ -105,12 +105,24 @@ module Coradoc
105
105
  end
106
106
 
107
107
  def block_content(n_deep = 3)
108
- c = block_image
108
+ block_container_content(n_deep).repeat(1)
109
+ end
110
+
111
+ # Single source of truth for "what can appear inside a non-verbatim
112
+ # block container". Used by block_content, block_style (non-verbatim
113
+ # branch), and block_style_exact (non-verbatim branch).
114
+ #
115
+ # `table` is listed before `text_line` so a leading `|===` is parsed
116
+ # as a table rather than mis-bracketed as text. The recursive `block`
117
+ # alternative handles every other delimited block.
118
+ def block_container_content(n_deep)
119
+ c = table.as(:table)
120
+ c |= block_image
109
121
  c |= block(n_deep - 1) if n_deep.positive?
110
122
  c |= list
111
123
  c |= text_line(false, unguarded: true)
112
124
  c |= empty_line.as(:line_break)
113
- c.repeat(1)
125
+ c
114
126
  end
115
127
 
116
128
  # Block delimiter: 4+ identical characters, or 2 dashes for open
@@ -155,12 +167,7 @@ module Coradoc
155
167
  text_line(false, unguarded: true, verbatim: true) |
156
168
  empty_line.as(:line_break)
157
169
  else
158
- c = block_image
159
- c |= block(n_deep - 1) if n_deep.positive?
160
- c |= list
161
- c |= text_line(false, unguarded: true)
162
- c |= empty_line.as(:line_break)
163
- c
170
+ block_container_content(n_deep)
164
171
  end
165
172
 
166
173
  (closing_pattern.absent? >> content).repeat(1)
@@ -213,12 +220,7 @@ module Coradoc
213
220
  text_line(false, unguarded: true, verbatim: true) |
214
221
  empty_line.as(:line_break)
215
222
  else
216
- alt = block_image
217
- alt |= block(n_deep - 1) if n_deep.positive?
218
- alt |= list
219
- alt |= text_line(false, unguarded: true)
220
- alt |= empty_line.as(:line_break)
221
- alt
223
+ block_container_content(n_deep)
222
224
  end
223
225
 
224
226
  (closing_pattern.absent? >> content).repeat(1)
@@ -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'
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module AsciiDoc
5
+ module Transform
6
+ module ElementTransformers
7
+ # Admonition style registry. Single source of truth for "which
8
+ # positional attribute names map to an admonition block."
9
+ #
10
+ # Built-in AsciiDoc admonition styles are always recognized. Callers
11
+ # can register additional styles (e.g. +DANGER+, +SAFETY+) without
12
+ # modifying dispatch code — the registry is consulted by every
13
+ # block-form transformer that needs to decide between an admonition
14
+ # and the block's native type.
15
+ module AdmonitionStyles
16
+ BUILTIN = %w[note tip warning caution important editor todo].freeze
17
+
18
+ @custom = []
19
+
20
+ class << self
21
+ # True if +style+ (case-insensitive) is a known admonition style.
22
+ def admonition?(style)
23
+ return false if style.nil?
24
+
25
+ name = style.to_s.downcase
26
+ BUILTIN.include?(name) || custom.include?(name)
27
+ end
28
+
29
+ # Register an additional admonition style. Open for extension.
30
+ def register(style)
31
+ name = style.to_s.downcase
32
+ @custom << name unless @custom.include?(name) || BUILTIN.include?(name)
33
+ self
34
+ end
35
+
36
+ # Reset custom registrations. Intended for specs.
37
+ def reset!
38
+ @custom = []
39
+ self
40
+ end
41
+
42
+ def custom
43
+ @custom.dup
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -21,7 +21,7 @@ module Coradoc
21
21
  # line; we join with "\n" so whitespace, indentation, and blank
22
22
  # lines are preserved. Treating these as paragraphs would collapse
23
23
  # whitespace and join consecutive lines into a single flowing text.
24
- def transform_verbatim_block(block, klass)
24
+ def transform_verbatim_block(block, klass, language_override: nil)
25
25
  non_break_lines = Array(block.lines).reject do |line|
26
26
  line.is_a?(Coradoc::AsciiDoc::Model::LineBreak) ||
27
27
  line.is_a?(Coradoc::AsciiDoc::Model::Break::PageBreak)
@@ -34,7 +34,7 @@ module Coradoc
34
34
  id: block.id,
35
35
  title: ToCoreModel.extract_title_text(block.title),
36
36
  content: content_lines,
37
- language: ToCoreModel.extract_block_language(block)
37
+ language: language_override || ToCoreModel.extract_block_language(block)
38
38
  )
39
39
  end
40
40
 
@@ -47,9 +47,54 @@ module Coradoc
47
47
  end
48
48
 
49
49
  def transform_pass_block(block)
50
+ style = first_positional_attr(block)
51
+ return transform_stem_block(block, style) if STEM_STYLES.key?(style.to_s.downcase)
52
+
50
53
  transform_verbatim_block(block, Coradoc::CoreModel::PassBlock)
51
54
  end
52
55
 
56
+ # AsciiDoc recognizes three STEM block styles. The default
57
+ # `[stem]` resolves to LaTeX; the other two name their
58
+ # interpreter explicitly. Single source of truth for the
59
+ # style→language mapping.
60
+ STEM_STYLES = {
61
+ 'stem' => 'latex',
62
+ 'latexmath' => 'latex',
63
+ 'asciimath' => 'asciimath'
64
+ }.freeze
65
+
66
+ def transform_stem_block(block, style)
67
+ language = STEM_STYLES.fetch(style.to_s.downcase, 'latex')
68
+ transform_verbatim_block(block, Coradoc::CoreModel::StemBlock,
69
+ language_override: language)
70
+ end
71
+
72
+ # Dispatch entry point for any block whose AsciiDoc model carries
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.
79
+ 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)
84
+ end
85
+
86
+ def transform_example_block(block)
87
+ transform_with_admonition_check(block, Coradoc::CoreModel::ExampleBlock)
88
+ end
89
+
90
+ def transform_sidebar_block(block)
91
+ transform_with_admonition_check(block, Coradoc::CoreModel::SidebarBlock)
92
+ end
93
+
94
+ def transform_quote_block(block)
95
+ transform_with_admonition_check(block, Coradoc::CoreModel::QuoteBlock)
96
+ end
97
+
53
98
  # Open blocks (`--`) are generic containers. AsciiDoc allows
54
99
  # casting them to a different block type via positional
55
100
  # attributes: verbatim types (`[source]`, `[listing]`,
@@ -60,41 +105,53 @@ module Coradoc
60
105
  def transform_open_block(block)
61
106
  semantic = open_block_semantic(block)
62
107
  case semantic
63
- when :source_code
108
+ when :source
64
109
  transform_source_block(block)
65
110
  when :listing
66
111
  transform_listing_from_open(block)
67
112
  when :literal
68
113
  transform_literal_from_open(block)
69
114
  when :admonition
70
- transform_admonition_from_open(block)
115
+ transform_admonition_block(block, first_positional_attr(block))
71
116
  else
72
117
  transform_typed_block(block, Coradoc::CoreModel::OpenBlock)
73
118
  end
74
119
  end
75
120
 
76
- ADMONITION_TYPES = %w[note tip warning caution important].freeze
121
+ VERBATILE_CAST_STYLES = %w[source listing literal].freeze
77
122
 
78
123
  def open_block_semantic(block)
124
+ style = first_positional_attr(block)
125
+ return nil unless style
126
+
127
+ case style.to_s.downcase
128
+ when *VERBATILE_CAST_STYLES then :"#{style.downcase}"
129
+ else :admonition if AdmonitionStyles.admonition?(style)
130
+ end
131
+ end
132
+
133
+ # Returns the first positional attribute on the block's
134
+ # attribute list as a String, or nil if absent. Centralizes the
135
+ # shape-walking that was previously inlined in
136
+ # `transform_admonition_from_open` and `open_block_semantic`.
137
+ def first_positional_attr(block)
79
138
  attrs = block.attributes
80
139
  return nil unless attrs.is_a?(Coradoc::AsciiDoc::Model::AttributeList)
81
140
 
82
141
  first = attrs.positional&.first
83
142
  return nil unless first.is_a?(Coradoc::AsciiDoc::Model::AttributeListAttribute)
84
143
 
85
- case first.value.to_s.downcase
86
- when 'source' then :source_code
87
- when 'listing' then :listing
88
- when 'literal' then :literal
89
- when *ADMONITION_TYPES then :admonition
90
- end
144
+ first.value.to_s
91
145
  end
92
146
 
93
- def transform_admonition_from_open(block)
94
- type = block.attributes.positional.first.value.to_s.downcase
147
+ # Single admonition dispatch used by every block-form path
148
+ # (example / sidebar / quote / pass / open). Builds an
149
+ # AnnotationBlock with annotation_type set from the style and
150
+ # the block's body lines joined into a single content string.
151
+ def transform_admonition_block(block, type)
95
152
  content_lines = Array(block.lines).map { |line| ToCoreModel.extract_text_content(line) }.join("\n")
96
153
  Coradoc::CoreModel::AnnotationBlock.new(
97
- annotation_type: type,
154
+ annotation_type: type.to_s.downcase,
98
155
  content: content_lines,
99
156
  title: ToCoreModel.extract_title_text(block.title)
100
157
  )
@@ -12,6 +12,7 @@ module Coradoc
12
12
  children = ToCoreModel.transform(doc.sections || doc.contents || [])
13
13
  children = CalloutMerger.call(children)
14
14
  children = prepend_frontmatter(children, doc.frontmatter)
15
+ children = insert_title_heading_after_frontmatter(children, title_text)
15
16
 
16
17
  Coradoc::CoreModel::DocumentElement.new(
17
18
  id: doc.id,
@@ -32,6 +33,31 @@ module Coradoc
32
33
  [block, *children]
33
34
  end
34
35
 
36
+ # Per the AsciiDoc spec, the document title (`= Title`) is the
37
+ # document's level-0 heading. Asciidoctor renders it as an
38
+ # <h1> at the top of the body by default. Emit a HeaderElement
39
+ # at level 0 so consumers that walk the body's children see the
40
+ # title (the standard ProseMirror/HTML rendering pattern)
41
+ # instead of having to read Document.title separately.
42
+ #
43
+ # The title attribute on DocumentElement is preserved for
44
+ # consumers that read it directly. The title heading is placed
45
+ # after any FrontmatterBlock (frontmatter is metadata that
46
+ # precedes the body).
47
+ def insert_title_heading_after_frontmatter(children, title_text)
48
+ return children if title_text.nil? || title_text.strip.empty?
49
+
50
+ title_heading = Coradoc::CoreModel::HeaderElement.new(
51
+ level: 0,
52
+ title: title_text,
53
+ content: title_text
54
+ )
55
+ frontmatter_count = children.count do |child|
56
+ child.is_a?(Coradoc::CoreModel::FrontmatterBlock)
57
+ end
58
+ children.insert(frontmatter_count, title_heading)
59
+ end
60
+
35
61
  def transform_section(section, parent_id: nil)
36
62
  title_text = ToCoreModel.extract_title_text(section.title)
37
63
  section_id = section.id || Coradoc::CoreModel::IdGenerator.generate_from_title(
@@ -11,6 +11,7 @@ module Coradoc
11
11
  autoload :TableTransformer, "#{__dir__}/element_transformers/table_transformer"
12
12
  autoload :OtherTransformer, "#{__dir__}/element_transformers/other_transformer"
13
13
  autoload :IncludeTransformer, "#{__dir__}/element_transformers/include_transformer"
14
+ autoload :AdmonitionStyles, "#{__dir__}/element_transformers/admonition_styles"
14
15
  end
15
16
  end
16
17
  end
@@ -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
@@ -40,12 +38,18 @@ module Coradoc
40
38
  Coradoc::AsciiDoc::Model::Header.new(title: '')
41
39
  end
42
40
 
43
- sections, frontmatter = extract_frontmatter(Array(element.children))
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
+ without_frontmatter, frontmatter = extract_frontmatter(Array(element.children))
47
+ without_title = strip_title_heading(without_frontmatter, element.title)
44
48
 
45
49
  Coradoc::AsciiDoc::Model::Document.new(
46
50
  id: element.id,
47
51
  header: header,
48
- sections: flatten_children(sections),
52
+ sections: flatten_children(without_title),
49
53
  frontmatter: frontmatter
50
54
  )
51
55
  when CoreModel::SectionElement
@@ -64,6 +68,27 @@ module Coradoc
64
68
  end
65
69
  end
66
70
 
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
+
67
92
  # Transforms each CoreModel child and flattens one level so a
68
93
  # transform that returns multiple siblings (e.g. a source block
69
94
  # followed by its re-expanded callout paragraphs) stays in
@@ -292,6 +317,8 @@ module Coradoc
292
317
  type: inline.stem_type || 'latexmath',
293
318
  content: inline.content
294
319
  )
320
+ when 'raw_inline'
321
+ Coradoc::AsciiDoc::Model::Inline::Passthrough.new(content: inline.content)
295
322
  else
296
323
  Coradoc::AsciiDoc::Model::TextElement.new(content: inline.content)
297
324
  end
@@ -540,6 +567,8 @@ module Coradoc
540
567
  end
541
568
  end
542
569
  end
570
+
571
+ def transform(model) = self.class.transform(model)
543
572
  end
544
573
  end
545
574
  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
@@ -48,14 +48,19 @@ module Coradoc
48
48
  priority: 10
49
49
  )
50
50
 
51
+ # Quote / Example / Sidebar blocks all share the same dispatch:
52
+ # check the attribute style for an admonition label first
53
+ # (`[NOTE]\n====` → AnnotationBlock), fall back to the block's
54
+ # native CoreModel type. Single source of truth lives in
55
+ # BlockTransformer#transform_with_admonition_check.
51
56
  {
52
- Coradoc::AsciiDoc::Model::Block::Quote => Coradoc::CoreModel::QuoteBlock,
53
- Coradoc::AsciiDoc::Model::Block::Example => Coradoc::CoreModel::ExampleBlock,
54
- Coradoc::AsciiDoc::Model::Block::Side => Coradoc::CoreModel::SidebarBlock
55
- }.each do |block_class, core_model_class|
57
+ Coradoc::AsciiDoc::Model::Block::Quote => :transform_quote_block,
58
+ Coradoc::AsciiDoc::Model::Block::Example => :transform_example_block,
59
+ Coradoc::AsciiDoc::Model::Block::Side => :transform_sidebar_block
60
+ }.each do |block_class, method|
56
61
  Registry.register_with_priority(
57
62
  block_class,
58
- ->(model) { Blk.transform_typed_block(model, core_model_class) },
63
+ ->(model) { Blk.public_send(method, model) },
59
64
  priority: 10
60
65
  )
61
66
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Coradoc
4
4
  module AsciiDoc
5
- VERSION = '2.0.19'
5
+ VERSION = '2.0.21'
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.19
4
+ version: 2.0.21
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
@@ -317,6 +318,7 @@ files:
317
318
  - lib/coradoc/asciidoc/transform/attribute_list_to_metadata.rb
318
319
  - lib/coradoc/asciidoc/transform/callout_merger.rb
319
320
  - lib/coradoc/asciidoc/transform/element_transformers.rb
321
+ - lib/coradoc/asciidoc/transform/element_transformers/admonition_styles.rb
320
322
  - lib/coradoc/asciidoc/transform/element_transformers/block_transformer.rb
321
323
  - lib/coradoc/asciidoc/transform/element_transformers/document_transformer.rb
322
324
  - lib/coradoc/asciidoc/transform/element_transformers/include_transformer.rb