coradoc-adoc 2.0.22 → 2.0.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/lib/coradoc/asciidoc/model/attribute_list.rb +31 -4
  3. data/lib/coradoc/asciidoc/model/base.rb +7 -0
  4. data/lib/coradoc/asciidoc/model/image/attribute_extractor.rb +142 -0
  5. data/lib/coradoc/asciidoc/model/image/block_image.rb +8 -0
  6. data/lib/coradoc/asciidoc/model/image/core.rb +35 -26
  7. data/lib/coradoc/asciidoc/model/image/inline_image.rb +7 -0
  8. data/lib/coradoc/asciidoc/model/image.rb +1 -0
  9. data/lib/coradoc/asciidoc/parser/attribute_list.rb +4 -1
  10. data/lib/coradoc/asciidoc/serializer/serializers/image/core.rb +3 -1
  11. data/lib/coradoc/asciidoc/transform/element_transformers/admonition_styles.rb +6 -1
  12. data/lib/coradoc/asciidoc/transform/element_transformers/block_transformer.rb +39 -16
  13. data/lib/coradoc/asciidoc/transform/element_transformers/document_transformer.rb +2 -27
  14. data/lib/coradoc/asciidoc/transform/element_transformers/include_transformer.rb +2 -1
  15. data/lib/coradoc/asciidoc/transform/element_transformers/inline_transformer.rb +12 -6
  16. data/lib/coradoc/asciidoc/transform/element_transformers/list_transformer.rb +10 -4
  17. data/lib/coradoc/asciidoc/transform/element_transformers/other_transformer.rb +25 -22
  18. data/lib/coradoc/asciidoc/transform/element_transformers/table_transformer.rb +6 -3
  19. data/lib/coradoc/asciidoc/transform/from_core_model.rb +16 -40
  20. data/lib/coradoc/asciidoc/transform/inline_transform_visitor.rb +23 -5
  21. data/lib/coradoc/asciidoc/transform/to_core_model.rb +30 -6
  22. data/lib/coradoc/asciidoc/transformer/block_rules.rb +19 -5
  23. data/lib/coradoc/asciidoc/transformer/header_rules.rb +12 -3
  24. data/lib/coradoc/asciidoc/transformer/inline_rules.rb +68 -20
  25. data/lib/coradoc/asciidoc/transformer/list_rules.rb +20 -5
  26. data/lib/coradoc/asciidoc/transformer/misc_rules.rb +31 -12
  27. data/lib/coradoc/asciidoc/transformer/source_line_extractor.rb +67 -0
  28. data/lib/coradoc/asciidoc/transformer/structural_rules.rb +7 -3
  29. data/lib/coradoc/asciidoc/transformer/text_rules.rb +33 -9
  30. data/lib/coradoc/asciidoc/transformer.rb +6 -1
  31. data/lib/coradoc/asciidoc/version.rb +1 -1
  32. metadata +3 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 42db73896b69e9e5344105625afe528bd2b7b8f57a11822f61881e782be8b974
4
- data.tar.gz: bc3e5e3073992f0d2b71e12ab62fa07d11cdf6ded25299d673f863522cd6291d
3
+ metadata.gz: d135f788f7205a03a242f7d1e9242b84fdc76994f6eff372579db29c9c420d51
4
+ data.tar.gz: 6eafdb8eb6a101650e48e6a29d5e719f729d8c7888c506a5e67a33511640db2f
5
5
  SHA512:
6
- metadata.gz: 334c5ae5f1860ccde94bb518e8bc7f91be7dcce338d863d93ec137acbfedce8f742be44336fde3e4cec7ea6709b3f309c484c716ceb8c485da2830312515d59e
7
- data.tar.gz: 3f387aab6c8b6d8862319593b9c2b8c2304758c4c66f15aed0e5ac57209f2f35b4930d1d95a57b2cf4d940834599db35395546c81b8d7760384062d0a7a48d49
6
+ metadata.gz: cfe57d959c7d69cf47aadb30526cd826ac85720d1f3f4426c4f6fc4d769c2618ad2c31cd713e3b86b4480f79fdf41d473457623c145746eb7fc25155c47b2dfc
7
+ data.tar.gz: 07dcf6099269006d16c34a21f5ca9457227fe9e7fcd597607716511ddecf7a3a1b7d794d5f32a29c6542a4b4f93ced0f61b37ae7e06553d775a0068ad4f7f363
@@ -87,6 +87,20 @@ module Coradoc
87
87
  )
88
88
  end
89
89
 
90
+ # Remove the first named attribute matching `name` and return its
91
+ # scalar value (or nil if absent). Used by promoters that lift a
92
+ # named attr out of the residual bag into a typed field.
93
+ # @param name [String, Symbol]
94
+ # @return [String, nil]
95
+ def delete_named(name)
96
+ name_str = name.to_s
97
+ idx = named.index { |n| n.name.to_s == name_str }
98
+ return nil unless idx
99
+
100
+ removed = @named.delete_at(idx)
101
+ removed.value.first&.to_s
102
+ end
103
+
90
104
  # Validate named attributes against validators
91
105
  #
92
106
  # @param validators [Hash] Hash of name => matcher pairs
@@ -209,12 +223,24 @@ module Coradoc
209
223
  positional.empty? && named.empty?
210
224
  end
211
225
 
212
- # Get a named attribute value by name
226
+ # Get the scalar value of the first named attribute matching `name`.
227
+ # Returns the first element of the underlying multi-value array, or
228
+ # nil if the name is absent. Use {#fetch_all} to retrieve the full
229
+ # multi-value array.
213
230
  # @param name [String, Symbol] The attribute name
214
- # @return [Object, nil] The attribute value or nil if not found
231
+ # @return [String, nil]
215
232
  def [](name)
216
233
  name_str = name.to_s
217
- named.find { |n| n.name.to_s == name_str }&.value
234
+ named.find { |n| n.name.to_s == name_str }&.value&.first&.to_s
235
+ end
236
+
237
+ # Get the full multi-value array for a named attribute.
238
+ # @param name [String, Symbol]
239
+ # @return [Array<String>] (empty when absent)
240
+ def fetch_all(name)
241
+ name_str = name.to_s
242
+ found = named.find { |n| n.name.to_s == name_str }
243
+ found ? found.value : []
218
244
  end
219
245
 
220
246
  # Get a named attribute value with default
@@ -222,7 +248,8 @@ module Coradoc
222
248
  # @param default [Object] The default value if not found
223
249
  # @return [Object] The attribute value or default
224
250
  def fetch(name, default = nil)
225
- self[name] || default
251
+ value = self[name]
252
+ value.nil? ? default : value
226
253
  end
227
254
  end
228
255
  end
@@ -35,6 +35,13 @@ module Coradoc
35
35
 
36
36
  attribute :id, :string
37
37
 
38
+ # 1-indexed source line where this element begins, when known.
39
+ # Populated by the Parslet transformer from the matched Slice's
40
+ # line_and_column. nil for programmatically constructed models.
41
+ # Single source of truth for source-position propagation through
42
+ # AsciiDoc::Model → CoreModel (Issue 1, STATUS-2026-06-28).
43
+ attribute :source_line, :integer
44
+
38
45
  # Element classification for spacing and serialization decisions.
39
46
  # Subclasses override these to declare their level.
40
47
  def block_level?
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module AsciiDoc
5
+ module Model
6
+ module Image
7
+ # Pure-function promoter: lifts semantically meaningful slots out of
8
+ # a generic {Model::AttributeList} into the typed fields declared on
9
+ # an image class, and the inverse — composes a serialisable
10
+ # AttributeList from typed fields + residual.
11
+ #
12
+ # The promotion map is owned by the target class itself, via
13
+ # +promoted_positional+ and +promoted_named+. This module is the
14
+ # single place that consumes those declarations, so adding a new
15
+ # promoted field is a one-line class-level change (OCP).
16
+ #
17
+ # Positional slots are consumed by index; named slots are removed
18
+ # from the residual list so they don't appear twice. The residual
19
+ # list preserves order and identity for everything that wasn't
20
+ # promoted (e.g. +scaledwidth+, +pdfwidth+, +opts+).
21
+ #
22
+ # @example Extract for an inline image
23
+ # list = Model::AttributeList.new
24
+ # list.add_positional('Alt', 'Thumb')
25
+ # list.add_named('width', '640')
26
+ # extracted, residual = AttributeExtractor.call(list, InlineImage)
27
+ # extracted # => { alt: 'Alt', role: 'Thumb', width: '640' }
28
+ # residual.positional # => []
29
+ # residual.named # => []
30
+ #
31
+ # @example Compose for serialisation
32
+ # AttributeExtractor.compose(image)
33
+ # # => Model::AttributeList with positional [alt, role] + named
34
+ # # [width, height, link] + residual attrs, in declaration order
35
+ #
36
+ module AttributeExtractor
37
+ module_function
38
+
39
+ # @param attribute_list [Model::AttributeList, nil]
40
+ # @param target_class [Class] an Image::Core subclass
41
+ # @return [Array<(Hash{Symbol=>String}, Model::AttributeList)>]
42
+ # a tuple of promoted typed values and the residual list
43
+ def call(attribute_list, target_class)
44
+ source = attribute_list || Model::AttributeList.new
45
+ residual = Model::AttributeList.new
46
+ extracted = {}
47
+
48
+ promote_positional(extracted, residual, source, target_class)
49
+ promote_named(extracted, residual, source, target_class)
50
+
51
+ [extracted, residual]
52
+ end
53
+
54
+ # Inverse of {call}: rebuild a serialisable AttributeList from a
55
+ # model's typed fields plus its residual list. Used by the AsciiDoc
56
+ # image serializer so round-trips reproduce the original syntax.
57
+ #
58
+ # A field that's declared in both +promoted_positional+ and
59
+ # +promoted_named+ (e.g. inline image +role+) is emitted only
60
+ # once — via its positional slot when filled, otherwise via named.
61
+ # @param model [Coradoc::AsciiDoc::Model::Image::Core]
62
+ # @return [Model::AttributeList]
63
+ def compose(model)
64
+ composed = Model::AttributeList.new
65
+ filled = compose_positional(composed, model)
66
+ compose_named(composed, model, filled)
67
+ append_residual(composed, model.attributes)
68
+ composed
69
+ end
70
+
71
+ def compose_positional(composed, model)
72
+ filled = []
73
+ model.class.promoted_positional.each do |attr_name|
74
+ value = model.public_send(attr_name)
75
+ next if value.nil? || value.to_s.empty?
76
+
77
+ composed.add_positional(value.to_s)
78
+ filled << attr_name
79
+ end
80
+ filled
81
+ end
82
+
83
+ def compose_named(composed, model, filled)
84
+ model.class.promoted_named.each do |attr_name|
85
+ next if filled.include?(attr_name)
86
+
87
+ value = model.public_send(attr_name)
88
+ next if value.nil? || value.to_s.empty?
89
+
90
+ composed.add_named(attr_name.to_s, value.to_s)
91
+ end
92
+ end
93
+
94
+ def promote_positional(extracted, residual, source, target_class)
95
+ promoted = target_class.promoted_positional
96
+ source.positional.each_with_index do |positional_attr, index|
97
+ attr_name = promoted[index]
98
+ value = positional_attr.value
99
+ if attr_name && !value.to_s.empty?
100
+ extracted[attr_name] = value.to_s
101
+ else
102
+ residual.add_positional(value)
103
+ end
104
+ end
105
+ end
106
+
107
+ def promote_named(extracted, residual, source, target_class)
108
+ promoted_names = target_class.promoted_named.to_set(&:to_s)
109
+ source.named.each do |named_attr|
110
+ promote_one_named(extracted, residual, named_attr, promoted_names)
111
+ end
112
+ end
113
+
114
+ def promote_one_named(extracted, residual, named_attr, promoted_names)
115
+ name_str = named_attr.name.to_s
116
+ unless promoted_names.include?(name_str)
117
+ residual.add_named(named_attr.name, named_attr.value)
118
+ return
119
+ end
120
+ # Positional promotion wins: a slot already filled positionally
121
+ # is not overwritten by a same-named entry (rare, but possible
122
+ # when both `[alt, role, role=X]` are supplied).
123
+ key = name_str.to_sym
124
+ return if extracted.key?(key)
125
+
126
+ value = named_attr.value.first&.to_s
127
+ return if value.nil? || value.empty?
128
+
129
+ extracted[key] = value
130
+ end
131
+
132
+ def append_residual(composed, residual)
133
+ return unless residual.is_a?(Model::AttributeList)
134
+
135
+ residual.positional.each { |p| composed.add_positional(p.value) }
136
+ residual.named.each { |n| composed.add_named(n.name, n.value) }
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -18,6 +18,14 @@ module Coradoc
18
18
  default: lambda {
19
19
  ::Coradoc::AsciiDoc::Model::AttributeList.new
20
20
  }
21
+
22
+ # Block images support the legacy positional form
23
+ # `image::target[alt, caption, role, ...]`, so the 2nd positional
24
+ # is promoted to `caption` (Asciidoctor image block macro shorthand).
25
+ # @return [Array<Symbol>]
26
+ def self.promoted_positional
27
+ %i[alt caption]
28
+ end
21
29
  end
22
30
  end
23
31
  end
@@ -9,30 +9,21 @@ module Coradoc
9
9
  # Images can be block-level (standalone paragraphs) or inline (within text).
10
10
  # This base class provides common functionality for both types.
11
11
  #
12
- # @!attribute [r] id
13
- # @return [String, nil] Optional identifier for the image
12
+ # Typed promotion of attribute-list slots
13
+ # ---------------------------------------
14
14
  #
15
- # @!attribute [r] title
16
- # @return [String, nil] Optional image title/alt text
17
- #
18
- # @!attribute [r] src
19
- # @return [String] The image source URL or path
20
- #
21
- # @!attribute [r] attributes
22
- # @return [Coradoc::AsciiDoc::Model::Image::Core::AttributeList] Image-specific attributes
23
- #
24
- # @!attribute [r] annotate_missing
25
- # @return [String, nil] Annotation text for missing images
26
- #
27
- # @!attribute [r] line_break
28
- # @return [String] Line break character (default: "")
29
- #
30
- # @!attribute [r] colons
31
- # @return [String, nil] Colon positioning for attributes
32
- #
33
- # @see Coradoc::AsciiDoc::Model::Image::BlockImage Block-level images
34
- # @see Coradoc::AsciiDoc::Model::Image::InlineImage Inline images
15
+ # Semantically meaningful image attributes (`alt`, `role`, `width`,
16
+ # `height`, `link`) are declared as typed lutaml-model fields on
17
+ # `Core` itself — not as validators on a generic bag. The class-level
18
+ # {promoted_positional} and {promoted_named} methods are the single
19
+ # source of truth for which slots get lifted into typed fields and in
20
+ # what order; subclasses override them to reflect syntax differences
21
+ # (e.g. inline images treat the 2nd positional as `role`, block images
22
+ # do not).
35
23
  #
24
+ # The lift itself is performed by {AttributeExtractor}, a pure function
25
+ # over (AttributeList, target_class) → (extracted_hash, residual_list).
26
+ # Anything not promoted stays in `attributes` for round-trip fidelity.
36
27
  class Core < Coradoc::AsciiDoc::Model::Base
37
28
  # Autoload nested AttributeList class
38
29
  autoload :AttributeList, 'coradoc/asciidoc/model/image/core/attribute_list'
@@ -42,6 +33,12 @@ module Coradoc
42
33
  attribute :id, :string
43
34
  attribute :title, :string
44
35
  attribute :src, :string
36
+ attribute :alt, :string
37
+ attribute :caption, :string
38
+ attribute :role, :string
39
+ attribute :width, :string
40
+ attribute :height, :string
41
+ attribute :link, :string
45
42
  attribute :attributes,
46
43
  Coradoc::AsciiDoc::Model::Image::Core::AttributeList,
47
44
  default: lambda {
@@ -51,17 +48,29 @@ module Coradoc
51
48
  attribute :line_break, :string, default: -> { '' }
52
49
  attribute :colons, :string
53
50
 
54
- # Aliases for common attribute accessors
55
51
  alias path src
56
- alias alt title
52
+
53
+ # Positional attribute-list slots that this image class promotes to
54
+ # typed fields, in order. Subclasses override to reflect their
55
+ # syntax. Index 0 → alt for all image kinds; index 1 → role for
56
+ # inline images only.
57
+ # @return [Array<Symbol>]
58
+ def self.promoted_positional
59
+ %i[alt]
60
+ end
61
+
62
+ # Named attribute-list keys that this image class promotes to typed
63
+ # fields. The same set applies to both inline and block images.
64
+ # @return [Array<Symbol>]
65
+ def self.promoted_named
66
+ %i[width height link role]
67
+ end
57
68
 
58
69
  # Custom to_adoc implementation that uses ElementRegistry directly
59
70
  # to avoid recursion issues with image serialization.
60
71
  #
61
72
  # @return [String] AsciiDoc representation of this image
62
73
  def to_adoc
63
- # Use the registered serializer rather than Coradoc::AsciiDoc::Serializer.serialize
64
- # to avoid recursion
65
74
  serializer_class = Coradoc::AsciiDoc::Serializer::ElementRegistry.lookup(self.class)
66
75
  serializer_class.new.to_adoc(self)
67
76
  end
@@ -10,6 +10,13 @@ module Coradoc
10
10
  end
11
11
 
12
12
  attribute :colons, :string, default: -> { ':' }
13
+
14
+ # Inline images use the 2nd positional slot for the role, per
15
+ # Asciidoctor: `image:target[alt, role, width=N, ...]`.
16
+ # @return [Array<Symbol>]
17
+ def self.promoted_positional
18
+ %i[alt role]
19
+ end
13
20
  end
14
21
  end
15
22
  end
@@ -8,6 +8,7 @@ module Coradoc
8
8
  autoload :Core, 'coradoc/asciidoc/model/image/core'
9
9
  autoload :InlineImage, 'coradoc/asciidoc/model/image/inline_image'
10
10
  autoload :BlockImage, 'coradoc/asciidoc/model/image/block_image'
11
+ autoload :AttributeExtractor, 'coradoc/asciidoc/model/image/attribute_extractor'
11
12
  end
12
13
  end
13
14
  end
@@ -44,7 +44,10 @@ module Coradoc
44
44
  end
45
45
 
46
46
  def positional_value_unquoted
47
- match('[^\],]').repeat(1)
47
+ # Exclude `=` so that `key=value` is matched by `named_attribute`
48
+ # rather than swallowed as a single positional token. A positional
49
+ # value that legitimately contains `=` must be quoted.
50
+ match('[^\],=]').repeat(1)
48
51
  end
49
52
 
50
53
  def positional_value_single_quote
@@ -14,7 +14,8 @@ module Coradoc
14
14
  end
15
15
  _anchor = model.anchor.nil? ? '' : "#{serialize_child(model.anchor)}\n"
16
16
  _title = model.title.to_s.empty? ? '' : ".#{model.title}\n"
17
- attrs = serialize_child(model.attributes, options)
17
+ composed = Model::Image::AttributeExtractor.compose(model)
18
+ attrs = serialize_child(composed, options)
18
19
  [missing, _anchor, _title, 'image', model.colons, model.src, attrs,
19
20
  model.line_break].join
20
21
  end
@@ -24,6 +25,7 @@ module Coradoc
24
25
  # Self-register this serializer
25
26
  ElementRegistry.register(Coradoc::AsciiDoc::Model::Image::Core, Image::Core)
26
27
  ElementRegistry.register(Coradoc::AsciiDoc::Model::Image::BlockImage, Image::Core)
28
+ ElementRegistry.register(Coradoc::AsciiDoc::Model::Image::InlineImage, Image::Core)
27
29
  end
28
30
  end
29
31
  end
@@ -13,7 +13,12 @@ module Coradoc
13
13
  # block-form transformer that needs to decide between an admonition
14
14
  # and the block's native type.
15
15
  module AdmonitionStyles
16
- BUILTIN = %w[note tip warning caution important editor todo].freeze
16
+ # Generic `[admonition]` (capitalized as ADMONITION by
17
+ # +canonicalize+) is the spec-defined generic admonition style.
18
+ # It is rarely used directly but exists in the AsciiDoc spec;
19
+ # treating it as a real admonition prevents it from collapsing
20
+ # to an ExampleBlock when applied to a delimited block.
21
+ BUILTIN = %w[note tip warning caution important editor todo admonition].freeze
17
22
 
18
23
  @custom = []
19
24
 
@@ -8,10 +8,13 @@ module Coradoc
8
8
  class << self
9
9
  def transform_paragraph(para)
10
10
  children = ToCoreModel.transform_inline_content(para.content)
11
+ source_lines = ToCoreModel.extract_source_lines(para.content)
11
12
 
12
13
  Coradoc::CoreModel::ParagraphBlock.new(
13
14
  id: para.id,
14
15
  content: ToCoreModel.extract_text_content(para.content),
16
+ lines: source_lines,
17
+ source_line: para.source_line,
15
18
  children: children
16
19
  )
17
20
  end
@@ -22,19 +25,15 @@ module Coradoc
22
25
  # lines are preserved. Treating these as paragraphs would collapse
23
26
  # whitespace and join consecutive lines into a single flowing text.
24
27
  def transform_verbatim_block(block, klass, language_override: nil)
25
- non_break_lines = Array(block.lines).reject do |line|
26
- line.is_a?(Coradoc::AsciiDoc::Model::LineBreak) ||
27
- line.is_a?(Coradoc::AsciiDoc::Model::Break::PageBreak)
28
- end
29
- content_lines = non_break_lines.map do |line|
30
- ToCoreModel.extract_text_content(line)
31
- end.join("\n")
28
+ source_lines = ToCoreModel.extract_source_lines(block.lines)
32
29
 
33
30
  klass.new(
34
31
  id: block.id,
35
32
  title: ToCoreModel.extract_title_text(block.title),
36
- content: content_lines,
37
- language: language_override || ToCoreModel.extract_block_language(block)
33
+ content: source_lines.join("\n"),
34
+ lines: source_lines,
35
+ language: language_override || ToCoreModel.extract_block_language(block),
36
+ source_line: block.source_line
38
37
  )
39
38
  end
40
39
 
@@ -74,7 +73,7 @@ module Coradoc
74
73
  # AsciiDoc spec, every delimited block accepts both admonition
75
74
  # labels and verbatim/stem casts in its attribute line. The cast
76
75
  # is resolved by +block_cast_semantic+ in MECE order:
77
- # admonition > stem > verbatim > fallback.
76
+ # admonition > stem > verbatim > typed-cast > fallback.
78
77
  def transform_with_admonition_check(block, native_class)
79
78
  semantic = block_cast_semantic(block)
80
79
  case semantic
@@ -88,6 +87,9 @@ module Coradoc
88
87
  transform_listing_from_open(block)
89
88
  when :literal
90
89
  transform_literal_from_open(block)
90
+ when :typed_cast
91
+ style = first_positional_attr(block).to_s.downcase
92
+ transform_typed_block(block, TYPED_BLOCK_CAST_STYLES.fetch(style))
91
93
  else
92
94
  transform_typed_block(block, native_class)
93
95
  end
@@ -119,11 +121,25 @@ module Coradoc
119
121
 
120
122
  VERBATILE_CAST_STYLES = %w[source listing literal].freeze
121
123
 
124
+ # Positional style → typed CoreModel class for blocks whose
125
+ # attribute line casts an open block into a richer semantic
126
+ # type (e.g. `[sidebar]\n--\n...\n--`). Single source of
127
+ # truth: adding a new style cast is one line here, no edits
128
+ # to dispatch (OCP).
129
+ TYPED_BLOCK_CAST_STYLES = {
130
+ 'sidebar' => Coradoc::CoreModel::SidebarBlock,
131
+ 'example' => Coradoc::CoreModel::ExampleBlock,
132
+ 'quote' => Coradoc::CoreModel::QuoteBlock,
133
+ 'abstract' => Coradoc::CoreModel::AbstractBlock,
134
+ 'partintro' => Coradoc::CoreModel::PartintroBlock
135
+ }.freeze
136
+
122
137
  # Resolve a delimited block's cast from its first positional
123
138
  # attribute. Single source of truth for the cast ladder used
124
139
  # by every block-form transformer (example/sidebar/quote/open).
125
140
  # Returns one of: :admonition, :stem, :source, :listing,
126
- # :literal, or nil (no cast — use the block's native class).
141
+ # :literal, :typed_cast, or nil (no cast — use the block's
142
+ # native class).
127
143
  def block_cast_semantic(block)
128
144
  style = first_positional_attr(block)
129
145
  return nil unless style
@@ -131,6 +147,7 @@ module Coradoc
131
147
  style_str = style.to_s.downcase
132
148
  return :admonition if AdmonitionStyles.admonition?(style)
133
149
  return :stem if STEM_STYLES.key?(style_str)
150
+ return :typed_cast if TYPED_BLOCK_CAST_STYLES.key?(style_str)
134
151
  return style_str.to_sym if VERBATILE_CAST_STYLES.include?(style_str)
135
152
 
136
153
  nil
@@ -163,7 +180,8 @@ module Coradoc
163
180
  Coradoc::CoreModel::AnnotationBlock.new(
164
181
  annotation_type: canonical,
165
182
  content: content_lines,
166
- title: ToCoreModel.extract_title_text(block.title)
183
+ title: ToCoreModel.extract_title_text(block.title),
184
+ source_line: block.source_line
167
185
  )
168
186
  end
169
187
 
@@ -176,7 +194,7 @@ module Coradoc
176
194
  end
177
195
 
178
196
  def transform_block(block, semantic_type_or_delimiter)
179
- content_lines = ToCoreModel.extract_block_lines(block)
197
+ source_lines = ToCoreModel.extract_source_lines(block.lines)
180
198
  semantic_type = if semantic_type_or_delimiter.is_a?(Symbol)
181
199
  semantic_type_or_delimiter
182
200
  else
@@ -188,8 +206,10 @@ module Coradoc
188
206
  delimiter_type: semantic_type_or_delimiter.is_a?(String) ? semantic_type_or_delimiter : nil,
189
207
  id: block.id,
190
208
  title: ToCoreModel.extract_title_text(block.title),
191
- content: content_lines,
192
- language: ToCoreModel.extract_block_language(block)
209
+ content: source_lines.join("\n"),
210
+ lines: source_lines,
211
+ language: ToCoreModel.extract_block_language(block),
212
+ source_line: block.source_line
193
213
  )
194
214
  end
195
215
 
@@ -216,6 +236,7 @@ module Coradoc
216
236
  title: ToCoreModel.extract_title_text(block.title),
217
237
  children: children,
218
238
  language: ToCoreModel.extract_block_language(block),
239
+ source_line: block.source_line,
219
240
  **extra_attrs
220
241
  )
221
242
  else
@@ -230,7 +251,8 @@ module Coradoc
230
251
  inline = [Coradoc::CoreModel::TextContent.new(text: '')] if inline.empty?
231
252
  Coradoc::CoreModel::ParagraphBlock.new(
232
253
  content: ToCoreModel.extract_text_content(group),
233
- children: Array(inline)
254
+ children: Array(inline),
255
+ source_line: ToCoreModel.extract_source_line(group)
234
256
  )
235
257
  end
236
258
 
@@ -240,6 +262,7 @@ module Coradoc
240
262
  content: content_lines,
241
263
  children: children,
242
264
  language: ToCoreModel.extract_block_language(block),
265
+ source_line: block.source_line,
243
266
  **extra_attrs
244
267
  )
245
268
  end
@@ -12,7 +12,6 @@ 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)
16
15
  children = resolve_attribute_references(children, attributes)
17
16
 
18
17
  Coradoc::CoreModel::DocumentElement.new(
@@ -34,31 +33,6 @@ module Coradoc
34
33
  [block, *children]
35
34
  end
36
35
 
37
- # Per the AsciiDoc spec, the document title (`= Title`) is the
38
- # document's level-0 heading. Asciidoctor renders it as an
39
- # <h1> at the top of the body by default. Emit a HeaderElement
40
- # at level 0 so consumers that walk the body's children see the
41
- # title (the standard ProseMirror/HTML rendering pattern)
42
- # instead of having to read Document.title separately.
43
- #
44
- # The title attribute on DocumentElement is preserved for
45
- # consumers that read it directly. The title heading is placed
46
- # after any FrontmatterBlock (frontmatter is metadata that
47
- # precedes the body).
48
- def insert_title_heading_after_frontmatter(children, title_text)
49
- return children if title_text.nil? || title_text.strip.empty?
50
-
51
- title_heading = Coradoc::CoreModel::HeaderElement.new(
52
- level: 0,
53
- title: title_text,
54
- content: title_text
55
- )
56
- frontmatter_count = children.count do |child|
57
- child.is_a?(Coradoc::CoreModel::FrontmatterBlock)
58
- end
59
- children.insert(frontmatter_count, title_heading)
60
- end
61
-
62
36
  # Resolve `{name}` attribute references in the body against the
63
37
  # document's own declared attributes. Single source of truth:
64
38
  # the resolver lives in core so other format gems can reuse it
@@ -86,7 +60,8 @@ module Coradoc
86
60
  level: section.level,
87
61
  title: title_text,
88
62
  children: content_children + nested_sections,
89
- attributes: section_metadata_from(section)
63
+ attributes: section_metadata_from(section),
64
+ source_line: section.source_line
90
65
  )
91
66
  end
92
67
 
@@ -21,7 +21,8 @@ module Coradoc
21
21
  target: model.path.to_s,
22
22
  options: options,
23
23
  raw_options: raw,
24
- line_break: model.line_break.to_s
24
+ line_break: model.line_break.to_s,
25
+ source_line: model.source_line
25
26
  )
26
27
  end
27
28
 
@@ -9,14 +9,16 @@ module Coradoc
9
9
  def transform_inline(inline, format_type)
10
10
  klass = Coradoc::CoreModel::InlineElement.format_type_class(format_type)
11
11
  klass.new(
12
- content: ToCoreModel.extract_text_content(inline.content)
12
+ content: ToCoreModel.extract_text_content(inline.content),
13
+ source_line: inline.source_line
13
14
  )
14
15
  end
15
16
 
16
17
  def transform_inline_text(inline, format_type)
17
18
  klass = Coradoc::CoreModel::InlineElement.format_type_class(format_type)
18
19
  klass.new(
19
- content: inline.text.to_s
20
+ content: inline.text.to_s,
21
+ source_line: inline.source_line
20
22
  )
21
23
  end
22
24
 
@@ -24,28 +26,32 @@ module Coradoc
24
26
  parsed_content = ToCoreModel.parse_and_transform_inline(footnote.text.to_s)
25
27
  Coradoc::CoreModel::FootnoteElement.new(
26
28
  target: footnote.id,
27
- content: parsed_content
29
+ content: parsed_content,
30
+ source_line: footnote.source_line
28
31
  )
29
32
  end
30
33
 
31
34
  def transform_link(link)
32
35
  Coradoc::CoreModel::LinkElement.new(
33
36
  target: link.path,
34
- content: link.name || link.path
37
+ content: link.name || link.path,
38
+ source_line: link.source_line
35
39
  )
36
40
  end
37
41
 
38
42
  def transform_cross_reference(xref)
39
43
  Coradoc::CoreModel::CrossReferenceElement.new(
40
44
  target: xref.href,
41
- content: xref.args&.first || xref.href
45
+ content: xref.args&.first || xref.href,
46
+ source_line: xref.source_line
42
47
  )
43
48
  end
44
49
 
45
50
  def transform_stem(stem)
46
51
  Coradoc::CoreModel::StemElement.new(
47
52
  content: stem.content,
48
- stem_type: stem.type || 'stem'
53
+ stem_type: stem.type || 'stem',
54
+ source_line: stem.source_line
49
55
  )
50
56
  end
51
57
  end