coradoc-adoc 2.0.9 → 2.0.11
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 +1 -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 +19 -27
- 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/content.rb +10 -1
- data/lib/coradoc/asciidoc/parser/frontmatter_parser.rb +24 -0
- 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/transform/callout_merger.rb +94 -0
- data/lib/coradoc/asciidoc/transform/element_transformers/block_transformer.rb +10 -1
- data/lib/coradoc/asciidoc/transform/element_transformers/document_transformer.rb +17 -1
- data/lib/coradoc/asciidoc/transform/element_transformers/other_transformer.rb +3 -1
- data/lib/coradoc/asciidoc/transform/from_core_model.rb +70 -5
- 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 +2 -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/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 +11 -1
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module AsciiDoc
|
|
5
|
+
module Transform
|
|
6
|
+
# OCP opt-in bridge mapping AsciiDoc document attributes
|
|
7
|
+
# (`:author:`, `:revdate:`, etc.) to/from FrontmatterBlock data.
|
|
8
|
+
#
|
|
9
|
+
# NOT auto-registered. Users opt in by invoking the bridge methods
|
|
10
|
+
# from their pipeline (e.g., from a custom transformer extension
|
|
11
|
+
# or rake task). This honors OCP: core conversion never silently
|
|
12
|
+
# rewrites data; opt-in extensions add behavior explicitly.
|
|
13
|
+
#
|
|
14
|
+
# MECE: lives in its own file, dispatches on `attribute_name` /
|
|
15
|
+
# `frontmatter_key` pairs. Does not touch FrontmatterBlock::Codec
|
|
16
|
+
# (YAML I/O) or SchemaResolver (validation).
|
|
17
|
+
#
|
|
18
|
+
# Mappings (bidirectional):
|
|
19
|
+
#
|
|
20
|
+
# | Frontmatter key | AsciiDoc attribute | Notes |
|
|
21
|
+
# |-----------------|--------------------|--------------------|
|
|
22
|
+
# | author | author | |
|
|
23
|
+
# | date | revdate | |
|
|
24
|
+
# | tags | tags | Array <-> space str|
|
|
25
|
+
# | categories | categories | Array <-> space str|
|
|
26
|
+
module FrontmatterAttributeMap
|
|
27
|
+
# Single source of truth for the attribute <-> frontmatter
|
|
28
|
+
# mapping. Each tuple: [attribute_name(String), front_key(String),
|
|
29
|
+
# kind(:scalar | :array)]
|
|
30
|
+
MAPPINGS = [
|
|
31
|
+
['author', 'author', :scalar],
|
|
32
|
+
['revdate', 'date', :scalar],
|
|
33
|
+
['tags', 'tags', :array],
|
|
34
|
+
['categories', 'categories', :array]
|
|
35
|
+
].freeze
|
|
36
|
+
|
|
37
|
+
class << self
|
|
38
|
+
# Build a frontmatter data hash from an AsciiDoc document
|
|
39
|
+
# attributes Hash. Skips unknown keys and empty values.
|
|
40
|
+
#
|
|
41
|
+
# @param attributes [Hash{String=>Object}] AsciiDoc document
|
|
42
|
+
# attributes (e.g., from DocumentAttributes#to_hash)
|
|
43
|
+
# @return [Hash{String=>Object}] frontmatter data hash
|
|
44
|
+
def entries_from_attributes(attributes)
|
|
45
|
+
attributes = normalize_hash(attributes)
|
|
46
|
+
MAPPINGS.each_with_object({}) do |(attr_name, front_key, kind), h|
|
|
47
|
+
raw = attributes[attr_name]
|
|
48
|
+
next if raw.nil? || raw.to_s.strip.empty?
|
|
49
|
+
|
|
50
|
+
h[front_key] = build_value(raw, kind)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Reverse: walk a FrontmatterBlock's data and produce a Hash
|
|
55
|
+
# of AsciiDoc document attributes. Unknown entry keys are
|
|
56
|
+
# dropped (only mapped keys are translated).
|
|
57
|
+
#
|
|
58
|
+
# @param block [Coradoc::CoreModel::FrontmatterBlock]
|
|
59
|
+
# @return [Hash{String=>String}]
|
|
60
|
+
def attributes_from_block(block)
|
|
61
|
+
result = {}
|
|
62
|
+
return result unless block&.data
|
|
63
|
+
|
|
64
|
+
MAPPINGS.each do |(_, front_key, _)|
|
|
65
|
+
value = block.data[front_key]
|
|
66
|
+
next if value.nil?
|
|
67
|
+
|
|
68
|
+
attr_name = front_key_to_attribute(front_key)
|
|
69
|
+
next unless attr_name
|
|
70
|
+
|
|
71
|
+
result[attr_name] = serialize_value(value)
|
|
72
|
+
end
|
|
73
|
+
result
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def normalize_hash(attributes)
|
|
79
|
+
return {} unless attributes.is_a?(Hash)
|
|
80
|
+
|
|
81
|
+
attributes.each_with_object({}) do |(k, v), h|
|
|
82
|
+
h[k.to_s] = v
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def build_value(raw, kind)
|
|
87
|
+
case kind
|
|
88
|
+
when :array
|
|
89
|
+
raw.to_s.split(/\s+/).reject(&:empty?)
|
|
90
|
+
else
|
|
91
|
+
raw.to_s
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def serialize_value(value)
|
|
96
|
+
case value
|
|
97
|
+
when Array
|
|
98
|
+
value.map(&:to_s).join(' ')
|
|
99
|
+
else
|
|
100
|
+
value.to_s
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def front_key_to_attribute(front_key)
|
|
105
|
+
mapping = MAPPINGS.find { |_, fk, _| fk == front_key.to_s }
|
|
106
|
+
mapping&.first
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -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,8 @@ 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"
|
|
16
|
+
autoload :CalloutMerger, "#{__dir__}/transform/callout_merger"
|
|
15
17
|
end
|
|
16
18
|
end
|
|
17
19
|
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)
|