coradoc-adoc 2.0.8 → 2.0.10
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/bibliography_entry.rb +18 -0
- data/lib/coradoc/asciidoc/model/document.rb +9 -0
- data/lib/coradoc/asciidoc/model/glossaries.rb +1 -1
- data/lib/coradoc/asciidoc/model/list/base.rb +41 -0
- data/lib/coradoc/asciidoc/model/list/core.rb +4 -24
- data/lib/coradoc/asciidoc/model/list/definition.rb +7 -0
- data/lib/coradoc/asciidoc/model/list/definition_item.rb +7 -1
- data/lib/coradoc/asciidoc/model/list/item.rb +1 -1
- data/lib/coradoc/asciidoc/model/list/nestable.rb +7 -3
- data/lib/coradoc/asciidoc/model/list.rb +4 -2
- data/lib/coradoc/asciidoc/parser/base.rb +10 -70
- data/lib/coradoc/asciidoc/parser/block.rb +3 -22
- data/lib/coradoc/asciidoc/parser/block_assembler.rb +37 -100
- data/lib/coradoc/asciidoc/parser/block_header.rb +55 -0
- data/lib/coradoc/asciidoc/parser/frontmatter_parser.rb +24 -0
- data/lib/coradoc/asciidoc/parser/list.rb +18 -13
- data/lib/coradoc/asciidoc/parser/paragraph.rb +1 -3
- data/lib/coradoc/asciidoc/parser/rule_dispatcher.rb +158 -0
- data/lib/coradoc/asciidoc/parser/section.rb +1 -3
- data/lib/coradoc/asciidoc/parser/table.rb +1 -4
- data/lib/coradoc/asciidoc/parser/text.rb +1 -3
- data/lib/coradoc/asciidoc/parser.rb +1 -0
- data/lib/coradoc/asciidoc/serializer/serializers/base.rb +1 -1
- data/lib/coradoc/asciidoc/serializer/serializers/document.rb +7 -0
- data/lib/coradoc/asciidoc/serializer/serializers/list/definition.rb +3 -1
- data/lib/coradoc/asciidoc/serializer/serializers/list/definition_item.rb +21 -1
- data/lib/coradoc/asciidoc/transform/element_transformers/block_transformer.rb +10 -1
- data/lib/coradoc/asciidoc/transform/element_transformers/document_transformer.rb +15 -1
- data/lib/coradoc/asciidoc/transform/element_transformers/list_transformer.rb +6 -0
- data/lib/coradoc/asciidoc/transform/element_transformers/other_transformer.rb +3 -1
- data/lib/coradoc/asciidoc/transform/from_core_model.rb +46 -9
- data/lib/coradoc/asciidoc/transform/from_core_model_registrations.rb +5 -1
- data/lib/coradoc/asciidoc/transform/frontmatter_attribute_map.rb +112 -0
- data/lib/coradoc/asciidoc/transform/text_extract_visitor.rb +33 -1
- data/lib/coradoc/asciidoc/transform/to_core_model.rb +10 -2
- data/lib/coradoc/asciidoc/transform/to_core_model_registrations.rb +15 -10
- data/lib/coradoc/asciidoc/transform.rb +1 -0
- data/lib/coradoc/asciidoc/transformer/attribute_list_normalizer.rb +69 -0
- data/lib/coradoc/asciidoc/transformer/block_rules.rb +4 -42
- data/lib/coradoc/asciidoc/transformer/block_type_classifier.rb +56 -0
- data/lib/coradoc/asciidoc/transformer/header_rules.rb +15 -53
- data/lib/coradoc/asciidoc/transformer/inline_rules.rb +39 -57
- data/lib/coradoc/asciidoc/transformer/list_rules.rb +54 -11
- data/lib/coradoc/asciidoc/transformer/misc_rules.rb +1 -24
- data/lib/coradoc/asciidoc/transformer/structural_rules.rb +18 -81
- data/lib/coradoc/asciidoc/transformer/table_cell_builder.rb +161 -0
- data/lib/coradoc/asciidoc/transformer/table_layout.rb +135 -0
- data/lib/coradoc/asciidoc/transformer/text_rules.rb +1 -25
- data/lib/coradoc/asciidoc/transformer.rb +38 -294
- data/lib/coradoc/asciidoc/version.rb +1 -1
- data/lib/coradoc/asciidoc.rb +6 -3
- metadata +10 -1
|
@@ -74,8 +74,33 @@ module Coradoc
|
|
|
74
74
|
model.alt || model.src || ''
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
+
def visit_inline_span(model)
|
|
78
|
+
model.text.to_s
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def visit_list(model)
|
|
82
|
+
visit_array(model.items)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def visit_block(model)
|
|
86
|
+
visit_array(model.lines)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def visit_core_model_list(model)
|
|
90
|
+
model.items.map { |i| visit(i.content) }.join(' ')
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def visit_definition_item(model)
|
|
94
|
+
terms_text = visit(model.terms)
|
|
95
|
+
contents_text = visit(model.contents)
|
|
96
|
+
[terms_text, contents_text].reject(&:empty?).join(': ')
|
|
97
|
+
end
|
|
98
|
+
|
|
77
99
|
def visit_base_model(model)
|
|
78
|
-
|
|
100
|
+
content = model.content
|
|
101
|
+
content ? visit(content) : ''
|
|
102
|
+
rescue NoMethodError
|
|
103
|
+
''
|
|
79
104
|
end
|
|
80
105
|
|
|
81
106
|
def visit_core_model_inline(model)
|
|
@@ -115,6 +140,13 @@ module Coradoc
|
|
|
115
140
|
when Model::Inline::Footnote then visit_footnote(model)
|
|
116
141
|
when Model::Inline::AttributeReference then visit_attribute_reference(model)
|
|
117
142
|
when Model::Image::Core then visit_adoc_image(model)
|
|
143
|
+
when CoreModel::ListBlock then visit_core_model_list(model)
|
|
144
|
+
when Model::Inline::Span then visit_inline_span(model)
|
|
145
|
+
when Model::List::Core then visit_list(model)
|
|
146
|
+
when Model::List::Definition then visit_list(model)
|
|
147
|
+
when Model::List::DefinitionItem then visit_definition_item(model)
|
|
148
|
+
when Model::Block::Core then visit_block(model)
|
|
149
|
+
when Model::LineBreak, Model::CommentLine, Model::CommentBlock then ''
|
|
118
150
|
when Model::Base then visit_base_model(model)
|
|
119
151
|
else
|
|
120
152
|
model.class.name.start_with?('Parslet::') ? model.to_s : ''
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative 'to_core_model_registrations'
|
|
4
|
-
|
|
5
3
|
module Coradoc
|
|
6
4
|
module AsciiDoc
|
|
7
5
|
module Transform
|
|
8
6
|
class ToCoreModel
|
|
9
7
|
include Coradoc::Transform::Base
|
|
10
8
|
|
|
9
|
+
@registered = false
|
|
10
|
+
|
|
11
11
|
class << self
|
|
12
|
+
def register!
|
|
13
|
+
return if @registered
|
|
14
|
+
|
|
15
|
+
Transform::ToCoreModelRegistrations.register_all!
|
|
16
|
+
@registered = true
|
|
17
|
+
end
|
|
18
|
+
|
|
12
19
|
def transform(model)
|
|
20
|
+
register!
|
|
13
21
|
return model.filter_map { |item| transform(item) } if model.is_a?(Array)
|
|
14
22
|
return model unless model.is_a?(Coradoc::AsciiDoc::Model::Base)
|
|
15
23
|
|
|
@@ -75,6 +75,15 @@ module Coradoc
|
|
|
75
75
|
)
|
|
76
76
|
}
|
|
77
77
|
)
|
|
78
|
+
|
|
79
|
+
Registry.register(
|
|
80
|
+
Coradoc::AsciiDoc::Model::CommentLine,
|
|
81
|
+
lambda { |model|
|
|
82
|
+
Coradoc::CoreModel::CommentLine.new(
|
|
83
|
+
text: model.text.to_s
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
)
|
|
78
87
|
end
|
|
79
88
|
|
|
80
89
|
def register_list_transformers!
|
|
@@ -179,6 +188,11 @@ module Coradoc
|
|
|
179
188
|
->(model) { Oth.transform_image(model) }
|
|
180
189
|
)
|
|
181
190
|
|
|
191
|
+
Registry.register(
|
|
192
|
+
Coradoc::AsciiDoc::Model::Image::InlineImage,
|
|
193
|
+
->(model) { Oth.transform_image(model) }
|
|
194
|
+
)
|
|
195
|
+
|
|
182
196
|
Registry.register(
|
|
183
197
|
Coradoc::AsciiDoc::Model::Bibliography,
|
|
184
198
|
->(model) { Oth.transform_bibliography(model) }
|
|
@@ -190,17 +204,11 @@ module Coradoc
|
|
|
190
204
|
)
|
|
191
205
|
|
|
192
206
|
[
|
|
193
|
-
Coradoc::AsciiDoc::Model::TextElement,
|
|
194
207
|
Coradoc::AsciiDoc::Model::Include,
|
|
195
208
|
Coradoc::AsciiDoc::Model::Audio,
|
|
196
209
|
Coradoc::AsciiDoc::Model::Video,
|
|
197
210
|
Coradoc::AsciiDoc::Model::ContentList,
|
|
198
|
-
Coradoc::AsciiDoc::Model::Tag
|
|
199
|
-
].each do |klass|
|
|
200
|
-
Registry.register(klass, ->(model) { model })
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
[
|
|
211
|
+
Coradoc::AsciiDoc::Model::Tag,
|
|
204
212
|
Coradoc::AsciiDoc::Model::LineBreak,
|
|
205
213
|
Coradoc::AsciiDoc::Model::Break::PageBreak
|
|
206
214
|
].each do |klass|
|
|
@@ -212,6 +220,3 @@ module Coradoc
|
|
|
212
220
|
end
|
|
213
221
|
end
|
|
214
222
|
end
|
|
215
|
-
|
|
216
|
-
# Auto-register when this file is loaded
|
|
217
|
-
Coradoc::AsciiDoc::Transform::ToCoreModelRegistrations.register_all!
|
|
@@ -12,6 +12,7 @@ module Coradoc
|
|
|
12
12
|
autoload :TextExtractVisitor, "#{__dir__}/transform/text_extract_visitor"
|
|
13
13
|
autoload :InlineTransformVisitor, "#{__dir__}/transform/inline_transform_visitor"
|
|
14
14
|
autoload :ElementTransformers, "#{__dir__}/transform/element_transformers"
|
|
15
|
+
autoload :FrontmatterAttributeMap, "#{__dir__}/transform/frontmatter_attribute_map"
|
|
15
16
|
end
|
|
16
17
|
end
|
|
17
18
|
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module AsciiDoc
|
|
5
|
+
class Transformer < Parslet::Transform
|
|
6
|
+
# Pure-function module for normalizing raw parser `:attribute_list`
|
|
7
|
+
# values into a single canonical Model::AttributeList.
|
|
8
|
+
#
|
|
9
|
+
# The parser's `block_header` rule captures every consecutive `[...]`
|
|
10
|
+
# block before a structural element as a Parslet sequence under
|
|
11
|
+
# `:attribute_list`. Real-world AsciiDoc often stacks multiple lists
|
|
12
|
+
# before a single delimiter:
|
|
13
|
+
#
|
|
14
|
+
# [role=quote]
|
|
15
|
+
# [source, ruby]
|
|
16
|
+
# ----
|
|
17
|
+
# code
|
|
18
|
+
# ----
|
|
19
|
+
#
|
|
20
|
+
# This module is the single source of truth for converting any of those
|
|
21
|
+
# shapes (nil, single list, array of lists, array of hashes) into one
|
|
22
|
+
# canonical AttributeList that downstream model constructors can use.
|
|
23
|
+
module AttributeListNormalizer
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# @param value [Object, nil] Raw parser value bound to :attribute_list
|
|
27
|
+
# @return [Model::AttributeList, nil]
|
|
28
|
+
def coerce(value)
|
|
29
|
+
case value
|
|
30
|
+
when nil then nil
|
|
31
|
+
when Model::AttributeList then value
|
|
32
|
+
when Array
|
|
33
|
+
lists = value.map { |entry| unwrap(entry) }.compact
|
|
34
|
+
return nil if lists.empty?
|
|
35
|
+
return lists.first if lists.size == 1
|
|
36
|
+
|
|
37
|
+
merge(lists)
|
|
38
|
+
else
|
|
39
|
+
value
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Merge several AttributeLists into one, preserving positional order
|
|
44
|
+
# and concatenating named keys in input order.
|
|
45
|
+
# @param lists [Array<Model::AttributeList>]
|
|
46
|
+
# @return [Model::AttributeList]
|
|
47
|
+
def merge(lists)
|
|
48
|
+
merged = Model::AttributeList.new
|
|
49
|
+
lists.each do |list|
|
|
50
|
+
next unless list.is_a?(Model::AttributeList)
|
|
51
|
+
|
|
52
|
+
list.positional.each { |p| merged.add_positional(p.value) }
|
|
53
|
+
list.named.each { |n| merged.add_named(n.name, n.value) }
|
|
54
|
+
end
|
|
55
|
+
merged
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Unwrap a single entry of the parser's :attribute_list sequence.
|
|
59
|
+
# @param entry [Object]
|
|
60
|
+
# @return [Model::AttributeList, nil]
|
|
61
|
+
def unwrap(entry)
|
|
62
|
+
return entry if entry.is_a?(Model::AttributeList)
|
|
63
|
+
|
|
64
|
+
entry[:attribute_list] if entry.is_a?(Hash) && entry.key?(:attribute_list)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -11,12 +11,11 @@ module Coradoc
|
|
|
11
11
|
rule(block: subtree(:block)) do
|
|
12
12
|
id = block[:id]
|
|
13
13
|
title = block[:title]
|
|
14
|
-
attribute_list = block[:attribute_list]
|
|
14
|
+
attribute_list = AttributeListNormalizer.coerce(block[:attribute_list])
|
|
15
15
|
delimiter = block[:delimiter].to_s
|
|
16
|
-
delimiter_c = delimiter[0]
|
|
17
16
|
lines = block[:lines]
|
|
18
17
|
ordering = block.keys.select do |k|
|
|
19
|
-
%i[id title attribute_list
|
|
18
|
+
%i[id title attribute_list].include?(k)
|
|
20
19
|
end
|
|
21
20
|
|
|
22
21
|
opts = {
|
|
@@ -26,44 +25,7 @@ module Coradoc
|
|
|
26
25
|
lines: lines,
|
|
27
26
|
ordering: ordering
|
|
28
27
|
}
|
|
29
|
-
opts
|
|
30
|
-
delimiter_len = opts[:delimiter_len]
|
|
31
|
-
|
|
32
|
-
if delimiter_c == '*'
|
|
33
|
-
if attribute_list
|
|
34
|
-
if attribute_list.positional == [] &&
|
|
35
|
-
attribute_list.named.first&.name == 'reviewer'
|
|
36
|
-
Model::Block::ReviewerComment.new(
|
|
37
|
-
id:,
|
|
38
|
-
title:,
|
|
39
|
-
lines:,
|
|
40
|
-
delimiter_len:,
|
|
41
|
-
attributes: attribute_list
|
|
42
|
-
)
|
|
43
|
-
else
|
|
44
|
-
Model::Block::Side.new(id:, title:, lines:, delimiter_len:,
|
|
45
|
-
attributes: attribute_list)
|
|
46
|
-
end
|
|
47
|
-
else
|
|
48
|
-
Model::Block::Side.new(id:, title:, lines:, delimiter_len:,
|
|
49
|
-
attributes: attribute_list)
|
|
50
|
-
end
|
|
51
|
-
elsif delimiter_c == '='
|
|
52
|
-
Model::Block::Example.new(id:, title:, lines:, delimiter_len:,
|
|
53
|
-
attributes: attribute_list)
|
|
54
|
-
elsif delimiter_c == '+'
|
|
55
|
-
Model::Block::Pass.new(id:, title:, lines:, delimiter_len:,
|
|
56
|
-
attributes: attribute_list)
|
|
57
|
-
elsif delimiter_c == '-' && delimiter.size == 2
|
|
58
|
-
Model::Block::Open.new(id:, title:, lines:, delimiter_len:,
|
|
59
|
-
attributes: attribute_list)
|
|
60
|
-
elsif delimiter_c == '-' && delimiter.size >= 4
|
|
61
|
-
Model::Block::SourceCode.new(id:, title:, lines:, delimiter_len:,
|
|
62
|
-
attributes: attribute_list)
|
|
63
|
-
elsif delimiter_c == '_'
|
|
64
|
-
Model::Block::Quote.new(id:, title:, lines:, delimiter_len:,
|
|
65
|
-
attributes: attribute_list)
|
|
66
|
-
end
|
|
28
|
+
BlockTypeClassifier.classify(delimiter, opts, attribute_list)
|
|
67
29
|
end
|
|
68
30
|
|
|
69
31
|
# Example
|
|
@@ -84,7 +46,7 @@ module Coradoc
|
|
|
84
46
|
id = block_image[:id]
|
|
85
47
|
title = block_image[:title]
|
|
86
48
|
path = block_image[:path]
|
|
87
|
-
attrs = block_image[:attribute_list]
|
|
49
|
+
attrs = AttributeListNormalizer.coerce(block_image[:attribute_list])
|
|
88
50
|
Model::Image::BlockImage.new(
|
|
89
51
|
title: title,
|
|
90
52
|
id: id,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module AsciiDoc
|
|
5
|
+
class Transformer < Parslet::Transform
|
|
6
|
+
# Single source of truth for "which delimiter maps to which block model".
|
|
7
|
+
#
|
|
8
|
+
# The block rule in BlockRules delegates here to convert a parser
|
|
9
|
+
# delimiter string (e.g., `----`, `****`, `--`) into the appropriate
|
|
10
|
+
# Model::Block::* subclass instance. Adding a new block type means
|
|
11
|
+
# appending one entry to DELIMITER_CLASSIFICATIONS — no edits to the
|
|
12
|
+
# block rule itself. (Open/Closed Principle.)
|
|
13
|
+
module BlockTypeClassifier
|
|
14
|
+
# Each entry is [char, min_length, max_length, factory].
|
|
15
|
+
# The factory is a callable taking (opts, attribute_list) and
|
|
16
|
+
# returning a Model::Block::* instance. `max_length` nil means
|
|
17
|
+
# unbounded.
|
|
18
|
+
DELIMITER_CLASSIFICATIONS = [
|
|
19
|
+
['*', 4, nil, ->(opts, attrs) {
|
|
20
|
+
if attrs && attrs.positional == [] && attrs.named.first&.name == 'reviewer'
|
|
21
|
+
Model::Block::ReviewerComment.new(**opts.merge(attributes: attrs))
|
|
22
|
+
else
|
|
23
|
+
Model::Block::Side.new(**opts.merge(attributes: attrs))
|
|
24
|
+
end
|
|
25
|
+
}],
|
|
26
|
+
['=', 4, nil, ->(opts, attrs) { Model::Block::Example.new(**opts.merge(attributes: attrs)) }],
|
|
27
|
+
['+', 4, nil, ->(opts, attrs) { Model::Block::Pass.new(**opts.merge(attributes: attrs)) }],
|
|
28
|
+
['_', 4, nil, ->(opts, attrs) { Model::Block::Quote.new(**opts.merge(attributes: attrs)) }],
|
|
29
|
+
['-', 4, nil, ->(opts, attrs) { Model::Block::SourceCode.new(**opts.merge(attributes: attrs)) }],
|
|
30
|
+
['-', 2, 2, ->(opts, attrs) { Model::Block::Open.new(**opts.merge(attributes: attrs)) }]
|
|
31
|
+
].freeze
|
|
32
|
+
|
|
33
|
+
module_function
|
|
34
|
+
|
|
35
|
+
# @param delimiter [String] e.g., "----", "**", "--"
|
|
36
|
+
# @param opts [Hash] Constructor options (id, title, lines, delimiter_len, ordering)
|
|
37
|
+
# @param attrs [Model::AttributeList, nil]
|
|
38
|
+
# @return [Model::Block::Base, nil]
|
|
39
|
+
def classify(delimiter, opts, attrs)
|
|
40
|
+
char = delimiter[0]
|
|
41
|
+
len = delimiter.size
|
|
42
|
+
entry = DELIMITER_CLASSIFICATIONS.find do |c, min_len, max_len, _|
|
|
43
|
+
next false unless c == char
|
|
44
|
+
next false unless len >= min_len
|
|
45
|
+
next false if max_len && len > max_len
|
|
46
|
+
|
|
47
|
+
true
|
|
48
|
+
end
|
|
49
|
+
return nil unless entry
|
|
50
|
+
|
|
51
|
+
entry.last.call(opts, attrs)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -7,40 +7,25 @@ module Coradoc
|
|
|
7
7
|
module HeaderRules
|
|
8
8
|
def self.apply(transformer_class)
|
|
9
9
|
transformer_class.class_eval do
|
|
10
|
-
# Header
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
end
|
|
10
|
+
# Header — single canonical rule covering all combinations of
|
|
11
|
+
# optional :author and :revision slots. The previous design had
|
|
12
|
+
# four explicit rules (one per combination); this version reads
|
|
13
|
+
# the same data with one `subtree` match.
|
|
14
|
+
rule(header: subtree(:header)) do
|
|
15
|
+
title = header[:title]
|
|
16
|
+
author = header[:author]
|
|
17
|
+
revision = header[:revision]
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
) do
|
|
25
|
-
id = title.is_a?(Model::Title) ? title.id : nil
|
|
26
|
-
Model::Header.new(id:, title:, author:, revision: nil)
|
|
27
|
-
end
|
|
19
|
+
id = header[:id]
|
|
20
|
+
id = title.id if title.is_a?(Model::Title) && title.id && !id
|
|
21
|
+
id = id.to_s unless id.nil?
|
|
22
|
+
id = nil if id && id.empty?
|
|
28
23
|
|
|
29
|
-
|
|
30
|
-
rule(
|
|
31
|
-
title: simple(:title),
|
|
32
|
-
revision: simple(:revision)
|
|
33
|
-
) do
|
|
34
|
-
id = title.is_a?(Model::Title) ? title.id : nil
|
|
35
|
-
Model::Header.new(id:, title:, author: nil, revision:)
|
|
24
|
+
Model::Header.new(id:, title:, author:, revision:)
|
|
36
25
|
end
|
|
37
26
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
title: simple(:title)
|
|
41
|
-
) do
|
|
42
|
-
id = title.is_a?(Model::Title) ? title.id : nil
|
|
43
|
-
Model::Header.new(id:, title:, author: nil, revision: nil)
|
|
27
|
+
rule(header: simple(:header)) do
|
|
28
|
+
header
|
|
44
29
|
end
|
|
45
30
|
|
|
46
31
|
# Author
|
|
@@ -60,29 +45,6 @@ module Coradoc
|
|
|
60
45
|
) do
|
|
61
46
|
Model::Revision.new(number:, date:, remark:)
|
|
62
47
|
end
|
|
63
|
-
|
|
64
|
-
# Unwrap header hash - handles cases where header wasn't transformed yet
|
|
65
|
-
rule(header: subtree(:header)) do
|
|
66
|
-
if header.is_a?(Hash) && header.key?(:title)
|
|
67
|
-
id = header[:id]
|
|
68
|
-
id = id.to_s unless id.nil?
|
|
69
|
-
id = nil if id && id.empty?
|
|
70
|
-
|
|
71
|
-
title = header[:title]
|
|
72
|
-
author = header[:author]
|
|
73
|
-
revision = header[:revision]
|
|
74
|
-
|
|
75
|
-
id = title.id if title.is_a?(Model::Title) && title.id && !id
|
|
76
|
-
|
|
77
|
-
Model::Header.new(id:, title:, author:, revision:)
|
|
78
|
-
else
|
|
79
|
-
header
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
rule(header: simple(:header)) do
|
|
84
|
-
header
|
|
85
|
-
end
|
|
86
48
|
end
|
|
87
49
|
end
|
|
88
50
|
end
|
|
@@ -5,6 +5,17 @@ module Coradoc
|
|
|
5
5
|
class Transformer < Parslet::Transform
|
|
6
6
|
# Module containing inline element transformation rules
|
|
7
7
|
module InlineRules
|
|
8
|
+
# Inline formatting variants that share the same rule shape:
|
|
9
|
+
# constrained and unconstrained forms of the same model class.
|
|
10
|
+
# `span` is excluded because it carries `text:` + `attributes:`
|
|
11
|
+
# rather than `content:`, so it gets its own pair of rules.
|
|
12
|
+
FORMATTING_VARIANTS = [
|
|
13
|
+
%i[bold Bold],
|
|
14
|
+
%i[italic Italic],
|
|
15
|
+
%i[highlight Highlight],
|
|
16
|
+
%i[monospace Monospace]
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
8
19
|
def self.apply(transformer_class)
|
|
9
20
|
transformer_class.class_eval do
|
|
10
21
|
# Link
|
|
@@ -82,64 +93,23 @@ module Coradoc
|
|
|
82
93
|
href_arg.to_s
|
|
83
94
|
end
|
|
84
95
|
|
|
85
|
-
#
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
content = Transformer.extract_inline_content(bold)
|
|
94
|
-
Model::Inline::Bold.new(content: content, unconstrained: true)
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# Italic (constrained)
|
|
98
|
-
rule(italic_constrained: subtree(:italic)) do
|
|
99
|
-
content = Transformer.extract_inline_content(italic)
|
|
100
|
-
Model::Inline::Italic.new(content: content, unconstrained: false)
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
# Italic (unconstrained)
|
|
104
|
-
rule(italic_unconstrained: subtree(:italic)) do
|
|
105
|
-
content = Transformer.extract_inline_content(italic)
|
|
106
|
-
Model::Inline::Italic.new(content: content, unconstrained: true)
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
# Highlight (constrained)
|
|
110
|
-
rule(highlight_constrained: subtree(:highlight)) do
|
|
111
|
-
content = Transformer.extract_inline_content(highlight)
|
|
112
|
-
Model::Inline::Highlight.new(content: content, unconstrained: false)
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
# Highlight (unconstrained)
|
|
116
|
-
rule(highlight_unconstrained: subtree(:highlight)) do
|
|
117
|
-
content = Transformer.extract_inline_content(highlight)
|
|
118
|
-
Model::Inline::Highlight.new(content: content, unconstrained: true)
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
# Monospace (constrained)
|
|
122
|
-
rule(monospace_constrained: subtree(:monospace)) do
|
|
123
|
-
content = Transformer.extract_inline_content(monospace)
|
|
124
|
-
Model::Inline::Monospace.new(content: content, unconstrained: false)
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
# Monospace (unconstrained)
|
|
128
|
-
rule(monospace_unconstrained: subtree(:monospace)) do
|
|
129
|
-
content = Transformer.extract_inline_content(monospace)
|
|
130
|
-
Model::Inline::Monospace.new(content: content, unconstrained: true)
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
# Superscript
|
|
134
|
-
rule(superscript: subtree(:superscript)) do
|
|
135
|
-
content = Transformer.extract_simple_inline_content(superscript)
|
|
136
|
-
Model::Inline::Superscript.new(content:)
|
|
137
|
-
end
|
|
96
|
+
# Inline formatting rules generated from a single registry.
|
|
97
|
+
# See InlineRules::FORMATTING_VARIANTS. `span` is special
|
|
98
|
+
# because it carries `text:` + `attributes:` rather than
|
|
99
|
+
# `content:`, so it stays inline below.
|
|
100
|
+
InlineRules::FORMATTING_VARIANTS.each do |prefix, class_name|
|
|
101
|
+
klass = Model::Inline.const_get(class_name)
|
|
102
|
+
constrained_key = :"#{prefix}_constrained"
|
|
103
|
+
unconstrained_key = :"#{prefix}_unconstrained"
|
|
138
104
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
105
|
+
rule(constrained_key => subtree(:subtree)) do
|
|
106
|
+
content = Transformer.extract_inline_content(subtree)
|
|
107
|
+
klass.new(content: content, unconstrained: false)
|
|
108
|
+
end
|
|
109
|
+
rule(unconstrained_key => subtree(:subtree)) do
|
|
110
|
+
content = Transformer.extract_inline_content(subtree)
|
|
111
|
+
klass.new(content: content, unconstrained: true)
|
|
112
|
+
end
|
|
143
113
|
end
|
|
144
114
|
|
|
145
115
|
# Span (constrained)
|
|
@@ -160,6 +130,18 @@ module Coradoc
|
|
|
160
130
|
)
|
|
161
131
|
end
|
|
162
132
|
|
|
133
|
+
# Superscript
|
|
134
|
+
rule(superscript: subtree(:superscript)) do
|
|
135
|
+
content = Transformer.extract_simple_inline_content(superscript)
|
|
136
|
+
Model::Inline::Superscript.new(content:)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Subscript
|
|
140
|
+
rule(subscript: subtree(:subscript)) do
|
|
141
|
+
content = Transformer.extract_simple_inline_content(subscript)
|
|
142
|
+
Model::Inline::Subscript.new(content:)
|
|
143
|
+
end
|
|
144
|
+
|
|
163
145
|
# Highlight (simple)
|
|
164
146
|
rule(highlight: simple(:text)) do
|
|
165
147
|
Model::Highlight.new(content: text)
|
|
@@ -5,6 +5,42 @@ module Coradoc
|
|
|
5
5
|
class Transformer < Parslet::Transform
|
|
6
6
|
# Module containing list transformation rules
|
|
7
7
|
module ListRules
|
|
8
|
+
class << self
|
|
9
|
+
def build_dlist_tree(items)
|
|
10
|
+
root = Model::List::Definition.new(items: [])
|
|
11
|
+
stack = [[root, 0]]
|
|
12
|
+
|
|
13
|
+
items.each do |item|
|
|
14
|
+
depth = dlist_depth(item.delimiter)
|
|
15
|
+
stack.pop while stack.last[1] >= depth
|
|
16
|
+
|
|
17
|
+
stack.last[0].items << item
|
|
18
|
+
nested_list = Model::List::Definition.new(items: [])
|
|
19
|
+
item.nested << nested_list
|
|
20
|
+
stack.push([nested_list, depth])
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
prune_empty_nested(root)
|
|
24
|
+
root
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def dlist_depth(delimiter)
|
|
28
|
+
delim = delimiter.to_s
|
|
29
|
+
return 1 if delim == ';;' || delim.empty?
|
|
30
|
+
|
|
31
|
+
[delim.count(':') - 1, 1].max
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def prune_empty_nested(list)
|
|
35
|
+
list.items.each do |item|
|
|
36
|
+
item.nested.select! do |n|
|
|
37
|
+
n.is_a?(Model::List::Definition) && n.items.any?
|
|
38
|
+
end
|
|
39
|
+
item.nested.each { |n| prune_empty_nested(n) }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
8
44
|
def self.apply(transformer_class)
|
|
9
45
|
transformer_class.class_eval do
|
|
10
46
|
# List item
|
|
@@ -68,7 +104,7 @@ module Coradoc
|
|
|
68
104
|
end
|
|
69
105
|
|
|
70
106
|
# Definition list term (with optional anchor)
|
|
71
|
-
rule(dlist_term: subtree(:term_data), delimiter: simple(:
|
|
107
|
+
rule(dlist_term: subtree(:term_data), delimiter: simple(:delim)) do
|
|
72
108
|
case term_data
|
|
73
109
|
when Hash
|
|
74
110
|
text = term_data[:text]
|
|
@@ -76,11 +112,11 @@ module Coradoc
|
|
|
76
112
|
text = text.content.to_s if text.is_a?(Model::TextElement)
|
|
77
113
|
id = term_data[:id]
|
|
78
114
|
id = id.to_s if id.is_a?(Parslet::Slice)
|
|
79
|
-
{ text: text.to_s, id: id }
|
|
115
|
+
{ text: text.to_s, id: id, delimiter: delim.to_s }
|
|
80
116
|
when Model::TextElement
|
|
81
|
-
{ text: term_data.content.to_s, id: term_data.id }
|
|
117
|
+
{ text: term_data.content.to_s, id: term_data.id, delimiter: delim.to_s }
|
|
82
118
|
else
|
|
83
|
-
{ text: term_data.to_s, id: nil }
|
|
119
|
+
{ text: term_data.to_s, id: nil, delimiter: delim.to_s }
|
|
84
120
|
end
|
|
85
121
|
end
|
|
86
122
|
|
|
@@ -95,13 +131,15 @@ module Coradoc
|
|
|
95
131
|
t.is_a?(Hash) ? t[:text].to_s : t.to_s
|
|
96
132
|
end
|
|
97
133
|
item_id = nil
|
|
134
|
+
item_delim = '::'
|
|
98
135
|
terms.each do |t|
|
|
99
|
-
next unless t.is_a?(Hash)
|
|
136
|
+
next unless t.is_a?(Hash)
|
|
100
137
|
|
|
101
|
-
item_id = t[:id].to_s
|
|
102
|
-
|
|
138
|
+
item_id = t[:id].to_s if t[:id]
|
|
139
|
+
item_delim = t[:delimiter].to_s if t[:delimiter]
|
|
103
140
|
end
|
|
104
|
-
Model::List::DefinitionItem.new(terms: term_strings, contents: contents,
|
|
141
|
+
Model::List::DefinitionItem.new(terms: term_strings, contents: contents,
|
|
142
|
+
id: item_id, delimiter: item_delim)
|
|
105
143
|
end
|
|
106
144
|
|
|
107
145
|
# Definition list item with hash terms (single term case)
|
|
@@ -111,6 +149,7 @@ module Coradoc
|
|
|
111
149
|
data = item_data.is_a?(Hash) ? item_data : { terms: Array(item_data), definition: '' }
|
|
112
150
|
|
|
113
151
|
item_id = nil
|
|
152
|
+
item_delim = '::'
|
|
114
153
|
terms_data = data[:terms]
|
|
115
154
|
definition = data[:definition].to_s
|
|
116
155
|
|
|
@@ -118,17 +157,19 @@ module Coradoc
|
|
|
118
157
|
case t
|
|
119
158
|
when Hash
|
|
120
159
|
item_id ||= t[:id].to_s if t[:id]
|
|
160
|
+
item_delim = t[:delimiter].to_s if t[:delimiter]
|
|
121
161
|
t[:text].to_s
|
|
122
162
|
else
|
|
123
163
|
t.to_s
|
|
124
164
|
end
|
|
125
165
|
end
|
|
126
166
|
|
|
127
|
-
Model::List::DefinitionItem.new(terms: terms, contents: definition,
|
|
167
|
+
Model::List::DefinitionItem.new(terms: terms, contents: definition,
|
|
168
|
+
id: item_id, delimiter: item_delim)
|
|
128
169
|
end
|
|
129
170
|
|
|
130
171
|
rule(definition_list: sequence(:list_items)) do
|
|
131
|
-
|
|
172
|
+
ListRules.build_dlist_tree(list_items)
|
|
132
173
|
end
|
|
133
174
|
|
|
134
175
|
# Definition list with attribute_list (e.g., [%key])
|
|
@@ -136,7 +177,9 @@ module Coradoc
|
|
|
136
177
|
attribute_list: simple(:attribute_list),
|
|
137
178
|
definition_list: sequence(:list_items)
|
|
138
179
|
) do
|
|
139
|
-
|
|
180
|
+
tree = ListRules.build_dlist_tree(list_items)
|
|
181
|
+
tree.attrs = attribute_list if attribute_list
|
|
182
|
+
tree
|
|
140
183
|
end
|
|
141
184
|
end
|
|
142
185
|
end
|