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.
- checksums.yaml +4 -4
- data/lib/coradoc/asciidoc/model/attribute_list.rb +31 -4
- data/lib/coradoc/asciidoc/model/base.rb +7 -0
- data/lib/coradoc/asciidoc/model/image/attribute_extractor.rb +142 -0
- data/lib/coradoc/asciidoc/model/image/block_image.rb +8 -0
- data/lib/coradoc/asciidoc/model/image/core.rb +35 -26
- data/lib/coradoc/asciidoc/model/image/inline_image.rb +7 -0
- data/lib/coradoc/asciidoc/model/image.rb +1 -0
- data/lib/coradoc/asciidoc/parser/attribute_list.rb +4 -1
- data/lib/coradoc/asciidoc/serializer/serializers/image/core.rb +3 -1
- data/lib/coradoc/asciidoc/transform/element_transformers/admonition_styles.rb +6 -1
- data/lib/coradoc/asciidoc/transform/element_transformers/block_transformer.rb +39 -16
- data/lib/coradoc/asciidoc/transform/element_transformers/document_transformer.rb +2 -27
- data/lib/coradoc/asciidoc/transform/element_transformers/include_transformer.rb +2 -1
- data/lib/coradoc/asciidoc/transform/element_transformers/inline_transformer.rb +12 -6
- data/lib/coradoc/asciidoc/transform/element_transformers/list_transformer.rb +10 -4
- data/lib/coradoc/asciidoc/transform/element_transformers/other_transformer.rb +25 -22
- data/lib/coradoc/asciidoc/transform/element_transformers/table_transformer.rb +6 -3
- data/lib/coradoc/asciidoc/transform/from_core_model.rb +16 -40
- data/lib/coradoc/asciidoc/transform/inline_transform_visitor.rb +23 -5
- data/lib/coradoc/asciidoc/transform/to_core_model.rb +30 -6
- data/lib/coradoc/asciidoc/transformer/block_rules.rb +19 -5
- data/lib/coradoc/asciidoc/transformer/header_rules.rb +12 -3
- data/lib/coradoc/asciidoc/transformer/inline_rules.rb +68 -20
- data/lib/coradoc/asciidoc/transformer/list_rules.rb +20 -5
- data/lib/coradoc/asciidoc/transformer/misc_rules.rb +31 -12
- data/lib/coradoc/asciidoc/transformer/source_line_extractor.rb +67 -0
- data/lib/coradoc/asciidoc/transformer/structural_rules.rb +7 -3
- data/lib/coradoc/asciidoc/transformer/text_rules.rb +33 -9
- data/lib/coradoc/asciidoc/transformer.rb +6 -1
- data/lib/coradoc/asciidoc/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d135f788f7205a03a242f7d1e9242b84fdc76994f6eff372579db29c9c420d51
|
|
4
|
+
data.tar.gz: 6eafdb8eb6a101650e48e6a29d5e719f729d8c7888c506a5e67a33511640db2f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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 [
|
|
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]
|
|
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
|
-
#
|
|
13
|
-
#
|
|
12
|
+
# Typed promotion of attribute-list slots
|
|
13
|
+
# ---------------------------------------
|
|
14
14
|
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
37
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
192
|
-
|
|
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
|
|
|
@@ -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
|