coradoc-adoc 2.0.21 → 2.0.23
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/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/admonition.rb +15 -6
- data/lib/coradoc/asciidoc/parser/attribute_list.rb +4 -1
- data/lib/coradoc/asciidoc/parser/inline.rb +39 -4
- data/lib/coradoc/asciidoc/parser/list.rb +7 -2
- data/lib/coradoc/asciidoc/serializer/serializers/image/core.rb +3 -1
- data/lib/coradoc/asciidoc/transform/element_transformers/admonition_styles.rb +15 -0
- data/lib/coradoc/asciidoc/transform/element_transformers/block_transformer.rb +39 -29
- data/lib/coradoc/asciidoc/transform/element_transformers/document_transformer.rb +11 -0
- data/lib/coradoc/asciidoc/transform/element_transformers/list_transformer.rb +19 -11
- data/lib/coradoc/asciidoc/transform/element_transformers/other_transformer.rb +18 -19
- data/lib/coradoc/asciidoc/transform/from_core_model.rb +17 -12
- data/lib/coradoc/asciidoc/transform/to_core_model_registrations.rb +13 -1
- data/lib/coradoc/asciidoc/transformer/block_rules.rb +11 -4
- data/lib/coradoc/asciidoc/transformer/inline_rules.rb +19 -3
- data/lib/coradoc/asciidoc/transformer/table_cell_builder.rb +13 -1
- data/lib/coradoc/asciidoc/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 69bc04902f8020decc17928a5397de2876f060ed8ee36de95f0920ba6312e8b4
|
|
4
|
+
data.tar.gz: ca56574d1b78863a574551732397ff65944f2b012781def3bb4ea4bcf8b0aaf1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 708cf53d7a9c6de58e15aac99a1e77343e004b7d014697335cd91f96143584b0701235e84030948a5ab6a09eba8612a361b215b1be9707ca07943d3f7179ad09
|
|
7
|
+
data.tar.gz: ff60d697d4fff0ec229ca5b597f1fe2f37d34129bff79f10276b72824ecd418f770d9229800dee6f143f95345af0c1432aceb100385b6c578fd08abb01a3767a
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
@@ -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
|
|
@@ -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
|
-
(
|
|
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
|
|
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
|
-
|
|
153
|
-
|
|
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)
|
|
@@ -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
|
|
@@ -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
|
|
75
|
-
# in its attribute line.
|
|
76
|
-
#
|
|
77
|
-
#
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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]`)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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:
|
|
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
|
-
|
|
71
|
-
|
|
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:
|
|
21
|
+
annotation_type: canonical,
|
|
21
22
|
content: ToCoreModel.extract_text_content(admonition.content)
|
|
22
23
|
)
|
|
23
24
|
block.children = children
|
|
@@ -25,24 +26,22 @@ module Coradoc
|
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
def transform_image(image)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
height: image.attributes&.[]('height')
|
|
45
|
-
)
|
|
29
|
+
Coradoc::CoreModel::Image.new(**image_attributes(image))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def image_attributes(image)
|
|
33
|
+
{
|
|
34
|
+
src: normalize_image_src(image.src),
|
|
35
|
+
alt: image.alt, title: image.title, caption: image.caption,
|
|
36
|
+
width: image.width, height: image.height,
|
|
37
|
+
link: image.link, role: image.role,
|
|
38
|
+
inline: image.is_a?(Coradoc::AsciiDoc::Model::Image::InlineImage)
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def normalize_image_src(src)
|
|
43
|
+
s = src.to_s
|
|
44
|
+
s.start_with?(':') ? s[1..] : s
|
|
46
45
|
end
|
|
47
46
|
|
|
48
47
|
def transform_bibliography(bib)
|
|
@@ -319,17 +319,25 @@ module Coradoc
|
|
|
319
319
|
)
|
|
320
320
|
when 'raw_inline'
|
|
321
321
|
Coradoc::AsciiDoc::Model::Inline::Passthrough.new(content: inline.content)
|
|
322
|
+
when 'hard_line_break'
|
|
323
|
+
Coradoc::AsciiDoc::Model::Inline::HardLineBreak.new
|
|
322
324
|
else
|
|
323
325
|
Coradoc::AsciiDoc::Model::TextElement.new(content: inline.content)
|
|
324
326
|
end
|
|
325
327
|
end
|
|
326
328
|
|
|
327
329
|
def transform_image(image)
|
|
328
|
-
Coradoc::AsciiDoc::Model::Image::
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
330
|
+
target_class = image.inline ? Coradoc::AsciiDoc::Model::Image::InlineImage
|
|
331
|
+
: Coradoc::AsciiDoc::Model::Image::BlockImage
|
|
332
|
+
target_class.new(**image_attributes(image))
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def image_attributes(image)
|
|
336
|
+
{
|
|
337
|
+
src: image.src, alt: image.alt, title: image.title,
|
|
338
|
+
caption: image.caption, width: image.width, height: image.height,
|
|
339
|
+
link: image.link, role: image.role
|
|
340
|
+
}
|
|
333
341
|
end
|
|
334
342
|
|
|
335
343
|
def transform_bibliography(bib)
|
|
@@ -528,6 +536,10 @@ module Coradoc
|
|
|
528
536
|
content.map { |item| create_text_elements(item) }
|
|
529
537
|
when Coradoc::CoreModel::InlineElement
|
|
530
538
|
transform_inline(content)
|
|
539
|
+
when Coradoc::CoreModel::TextContent
|
|
540
|
+
Coradoc::AsciiDoc::Model::TextElement.new(content: content.text.to_s)
|
|
541
|
+
when Coradoc::CoreModel::Base
|
|
542
|
+
transform(content)
|
|
531
543
|
when Coradoc::AsciiDoc::Model::Base
|
|
532
544
|
content
|
|
533
545
|
when Lutaml::Model::Serializable
|
|
@@ -553,13 +565,6 @@ module Coradoc
|
|
|
553
565
|
attrs
|
|
554
566
|
end
|
|
555
567
|
|
|
556
|
-
def build_image_attributes(image)
|
|
557
|
-
attrs = {}
|
|
558
|
-
attrs['width'] = image.width if image.width
|
|
559
|
-
attrs['height'] = image.height if image.height
|
|
560
|
-
attrs
|
|
561
|
-
end
|
|
562
|
-
|
|
563
568
|
def default_marker(marker_type)
|
|
564
569
|
case marker_type
|
|
565
570
|
when 'ordered' then '.'
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -51,12 +54,16 @@ module Coradoc
|
|
|
51
54
|
title = block_image[:title]
|
|
52
55
|
path = block_image[:path]
|
|
53
56
|
attrs = AttributeListNormalizer.coerce(block_image[:attribute_list_macro])
|
|
57
|
+
promoted, residual = Model::Image::AttributeExtractor.call(
|
|
58
|
+
attrs, Model::Image::BlockImage
|
|
59
|
+
)
|
|
54
60
|
Model::Image::BlockImage.new(
|
|
55
61
|
title: title,
|
|
56
62
|
id: id,
|
|
57
63
|
src: path,
|
|
58
|
-
attributes:
|
|
59
|
-
line_break: "\n"
|
|
64
|
+
attributes: residual,
|
|
65
|
+
line_break: "\n",
|
|
66
|
+
**promoted
|
|
60
67
|
)
|
|
61
68
|
end
|
|
62
69
|
end
|
|
@@ -36,14 +36,21 @@ 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])
|
|
40
|
+
promoted, residual = Model::Image::AttributeExtractor.call(
|
|
41
|
+
attrs, Model::Image::InlineImage
|
|
42
|
+
)
|
|
39
43
|
Model::Image::InlineImage.new(
|
|
40
|
-
title: inline_image[:text],
|
|
41
44
|
src: inline_image[:path],
|
|
42
|
-
attributes:
|
|
45
|
+
attributes: residual,
|
|
46
|
+
**promoted
|
|
43
47
|
)
|
|
44
48
|
end
|
|
45
49
|
|
|
46
|
-
# Inline passthrough (`+++raw content+++`)
|
|
50
|
+
# Inline passthrough (`+++raw content+++` or `pass:[raw]`).
|
|
51
|
+
# Both forms carry an opaque payload that survives all
|
|
52
|
+
# substitutions verbatim; `form` records which syntax was
|
|
53
|
+
# used so the AsciiDoc serializer can round-trip faithfully.
|
|
47
54
|
rule(inline_passthrough: subtree(:passthrough)) do
|
|
48
55
|
Model::Inline::Passthrough.new(
|
|
49
56
|
content: passthrough[:raw].to_s,
|
|
@@ -56,6 +63,15 @@ module Coradoc
|
|
|
56
63
|
Model::Inline::AttributeReference.new(name:)
|
|
57
64
|
end
|
|
58
65
|
|
|
66
|
+
# Hard line break (` +\n` or `\\n`). Emitted as a dedicated
|
|
67
|
+
# AsciiDoc model (Inline::HardLineBreak) distinct from
|
|
68
|
+
# Model::LineBreak, which only represents paragraph-separator
|
|
69
|
+
# blank lines. Hard breaks carry semantic meaning: HTML/Markdown
|
|
70
|
+
# renderers map them to <br>.
|
|
71
|
+
rule(hard_line_break: simple(:_)) do
|
|
72
|
+
Model::Inline::HardLineBreak.new
|
|
73
|
+
end
|
|
74
|
+
|
|
59
75
|
# Term
|
|
60
76
|
rule(
|
|
61
77
|
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.
|
|
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.
|
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.
|
|
4
|
+
version: 2.0.23
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -146,6 +146,7 @@ files:
|
|
|
146
146
|
- lib/coradoc/asciidoc/model/header.rb
|
|
147
147
|
- lib/coradoc/asciidoc/model/highlight.rb
|
|
148
148
|
- lib/coradoc/asciidoc/model/image.rb
|
|
149
|
+
- lib/coradoc/asciidoc/model/image/attribute_extractor.rb
|
|
149
150
|
- lib/coradoc/asciidoc/model/image/block_image.rb
|
|
150
151
|
- lib/coradoc/asciidoc/model/image/block_image/attribute_list.rb
|
|
151
152
|
- lib/coradoc/asciidoc/model/image/core.rb
|