coradoc-adoc 2.0.0 → 2.0.6
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/Rakefile +8 -0
- data/lib/coradoc/asciidoc/builder/block_builder.rb +49 -0
- data/lib/coradoc/asciidoc/builder/detection.rb +184 -0
- data/lib/coradoc/asciidoc/builder/element_builder.rb +124 -0
- data/lib/coradoc/asciidoc/builder/list_builder.rb +126 -0
- data/lib/coradoc/asciidoc/builder/text_builder.rb +23 -0
- data/lib/coradoc/asciidoc/builder.rb +227 -0
- data/lib/coradoc/asciidoc/model/document.rb +1 -2
- data/lib/coradoc/asciidoc/model/serialization/asciidoc_mapping.rb +1 -0
- data/lib/coradoc/asciidoc/parser/base.rb +3 -3
- data/lib/coradoc/asciidoc/parser/block.rb +21 -24
- data/lib/coradoc/asciidoc/parser/list.rb +6 -8
- data/lib/coradoc/asciidoc/parser/table.rb +3 -3
- data/lib/coradoc/asciidoc/transform/from_core_model.rb +4 -6
- data/lib/coradoc/asciidoc/transform/to_core_model.rb +143 -49
- data/lib/coradoc/asciidoc/transform/to_core_model_registrations.rb +1 -2
- data/lib/coradoc/asciidoc/transformer/list_rules.rb +42 -22
- data/lib/coradoc/asciidoc/transformer.rb +4 -1
- data/lib/coradoc/asciidoc/version.rb +1 -1
- data/lib/coradoc/asciidoc.rb +1 -0
- metadata +15 -8
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module AsciiDoc
|
|
5
|
+
class Builder
|
|
6
|
+
autoload :Detection, "#{__dir__}/builder/detection"
|
|
7
|
+
autoload :ListBuilder, "#{__dir__}/builder/list_builder"
|
|
8
|
+
autoload :BlockBuilder, "#{__dir__}/builder/block_builder"
|
|
9
|
+
autoload :TextBuilder, "#{__dir__}/builder/text_builder"
|
|
10
|
+
autoload :ElementBuilder, "#{__dir__}/builder/element_builder"
|
|
11
|
+
|
|
12
|
+
include Detection
|
|
13
|
+
include ListBuilder
|
|
14
|
+
include BlockBuilder
|
|
15
|
+
include TextBuilder
|
|
16
|
+
include ElementBuilder
|
|
17
|
+
|
|
18
|
+
def self.build(ast)
|
|
19
|
+
new.build_document(ast)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def build_document(ast)
|
|
23
|
+
return nil unless ast.is_a?(Hash)
|
|
24
|
+
|
|
25
|
+
if ast.key?(:document)
|
|
26
|
+
build_document_elements(ast[:document])
|
|
27
|
+
else
|
|
28
|
+
build_document_elements(ast)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def build_element(ast)
|
|
33
|
+
return nil unless ast.is_a?(Hash)
|
|
34
|
+
|
|
35
|
+
case detect_element_type(ast)
|
|
36
|
+
when :header
|
|
37
|
+
build_header(ast)
|
|
38
|
+
when :section
|
|
39
|
+
build_section(ast)
|
|
40
|
+
when :block
|
|
41
|
+
build_block(ast)
|
|
42
|
+
when :list
|
|
43
|
+
build_list(ast)
|
|
44
|
+
when :paragraph
|
|
45
|
+
build_paragraph(ast)
|
|
46
|
+
when :inline
|
|
47
|
+
build_inline(ast)
|
|
48
|
+
when :text
|
|
49
|
+
build_text(ast)
|
|
50
|
+
when :attribute
|
|
51
|
+
build_attribute(ast)
|
|
52
|
+
when :document_attributes
|
|
53
|
+
build_document_attributes(ast)
|
|
54
|
+
when :line_break
|
|
55
|
+
build_line_break(ast)
|
|
56
|
+
when :comment_line
|
|
57
|
+
build_comment_line(ast)
|
|
58
|
+
when :comment_block
|
|
59
|
+
build_comment_block(ast)
|
|
60
|
+
when :include
|
|
61
|
+
build_include(ast)
|
|
62
|
+
when :table
|
|
63
|
+
build_table(ast)
|
|
64
|
+
when :unparsed
|
|
65
|
+
build_unparsed(ast)
|
|
66
|
+
when :tag
|
|
67
|
+
build_tag(ast)
|
|
68
|
+
when :bibliography_entry
|
|
69
|
+
build_bibliography_entry(ast)
|
|
70
|
+
else
|
|
71
|
+
build_generic_element(ast)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def build_block(ast)
|
|
76
|
+
block_ast = ast[:block] || ast
|
|
77
|
+
|
|
78
|
+
case detect_block_type(block_ast)
|
|
79
|
+
when :annotation
|
|
80
|
+
build_annotation_block(block_ast)
|
|
81
|
+
when :list
|
|
82
|
+
build_list_block(block_ast)
|
|
83
|
+
else
|
|
84
|
+
build_generic_block(block_ast)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def build_list(ast)
|
|
89
|
+
if ast[:unordered]
|
|
90
|
+
build_unordered_list(ast)
|
|
91
|
+
elsif ast[:ordered]
|
|
92
|
+
build_ordered_list(ast)
|
|
93
|
+
elsif ast[:definition_list]
|
|
94
|
+
build_definition_list(ast)
|
|
95
|
+
else
|
|
96
|
+
build_list_block(ast)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def build_paragraph(ast)
|
|
101
|
+
para_ast = ast[:paragraph] || ast
|
|
102
|
+
|
|
103
|
+
Coradoc::CoreModel::ParagraphBlock.new(
|
|
104
|
+
content: build_paragraph_content(para_ast[:lines]).join("\n"),
|
|
105
|
+
id: para_ast[:id],
|
|
106
|
+
title: para_ast[:title]
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def build_inline(ast)
|
|
111
|
+
format_type = detect_inline_format(ast)
|
|
112
|
+
klass = Coradoc::CoreModel::InlineElement.format_type_class(format_type)
|
|
113
|
+
|
|
114
|
+
klass.new(
|
|
115
|
+
constrained: detect_constrained(ast, format_type),
|
|
116
|
+
content: extract_inline_content(ast, format_type),
|
|
117
|
+
nested_elements: build_nested_inlines(ast)
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def build_attributes(attr_ast)
|
|
122
|
+
return [] unless attr_ast
|
|
123
|
+
|
|
124
|
+
case attr_ast
|
|
125
|
+
when Hash
|
|
126
|
+
attributes = []
|
|
127
|
+
|
|
128
|
+
if attr_ast[:positional]
|
|
129
|
+
Array(attr_ast[:positional]).each do |pos|
|
|
130
|
+
attributes << Coradoc::CoreModel::ElementAttribute.new(
|
|
131
|
+
name: pos.to_s
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
if attr_ast[:named]
|
|
137
|
+
Array(attr_ast[:named]).each do |named|
|
|
138
|
+
next unless named.is_a?(Hash)
|
|
139
|
+
|
|
140
|
+
attributes << Coradoc::CoreModel::ElementAttribute.new(
|
|
141
|
+
name: named[:key] || named[:named_key],
|
|
142
|
+
value: named[:value] || named[:named_value]
|
|
143
|
+
)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
attributes
|
|
148
|
+
when Array
|
|
149
|
+
attr_ast.map { |attr| build_attribute(attr) }.compact
|
|
150
|
+
else
|
|
151
|
+
[]
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def build_document_attributes(ast)
|
|
156
|
+
attrs_ast = ast[:document_attributes] || ast
|
|
157
|
+
|
|
158
|
+
Array(attrs_ast).map do |attr|
|
|
159
|
+
Coradoc::CoreModel::ElementAttribute.new(
|
|
160
|
+
key: attr[:key],
|
|
161
|
+
value: attr[:value]
|
|
162
|
+
)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
private
|
|
167
|
+
|
|
168
|
+
def build_text(ast)
|
|
169
|
+
Coradoc::CoreModel::InlineElement.new(
|
|
170
|
+
content: extract_text_content(ast)
|
|
171
|
+
)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def build_paragraph_content(lines)
|
|
175
|
+
return [] unless lines
|
|
176
|
+
|
|
177
|
+
Array(lines).map { |line| extract_text_content(line) }
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def build_document_elements(ast)
|
|
181
|
+
elements = []
|
|
182
|
+
|
|
183
|
+
elements << build_header(ast) if ast[:header]
|
|
184
|
+
|
|
185
|
+
if ast[:sections]
|
|
186
|
+
elements.concat(
|
|
187
|
+
Array(ast[:sections]).map { |s| build_element(s) }.compact
|
|
188
|
+
)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
elements << build_document_attributes(ast) if ast[:document_attributes]
|
|
192
|
+
|
|
193
|
+
%i[paragraph block list table].each do |key|
|
|
194
|
+
next unless ast[key]
|
|
195
|
+
|
|
196
|
+
Array(ast[key]).each do |item|
|
|
197
|
+
elements << build_element({ key => item })
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
group_document_elements(elements)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def group_document_elements(elements)
|
|
205
|
+
header = elements.find { |e| e.is_a?(CoreModel::HeaderElement) }
|
|
206
|
+
sections = elements.select { |e| e.is_a?(CoreModel::SectionElement) }
|
|
207
|
+
doc_attrs = elements.select { |e| e.is_a?(CoreModel::ElementAttribute) }
|
|
208
|
+
other_content = elements.reject do |e|
|
|
209
|
+
e.is_a?(CoreModel::HeaderElement) ||
|
|
210
|
+
e.is_a?(CoreModel::SectionElement) ||
|
|
211
|
+
e.is_a?(CoreModel::ElementAttribute)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
result = {}
|
|
215
|
+
result[:header] = header if header
|
|
216
|
+
result[:sections] = sections if sections.any?
|
|
217
|
+
result[:content] = other_content if other_content.any?
|
|
218
|
+
result[:document_attributes] = doc_attrs if doc_attrs.any?
|
|
219
|
+
result
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def build_attributes_private(attr_ast)
|
|
223
|
+
build_attributes(attr_ast)
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
@@ -90,13 +90,13 @@ module Coradoc
|
|
|
90
90
|
alias_name = :"#{rule_name}_#{dispatch_hash}"
|
|
91
91
|
Coradoc::AsciiDoc::Parser::Base.class_exec do
|
|
92
92
|
rule(alias_name) do
|
|
93
|
-
|
|
93
|
+
public_send(rule_name, *args, **kwargs)
|
|
94
94
|
end
|
|
95
95
|
end
|
|
96
96
|
@dispatch_data[dispatch_hash] = alias_name
|
|
97
97
|
end
|
|
98
98
|
dispatch_method = @dispatch_data[dispatch_hash]
|
|
99
|
-
|
|
99
|
+
public_send(dispatch_method)
|
|
100
100
|
end
|
|
101
101
|
|
|
102
102
|
def self.config(key)
|
|
@@ -140,7 +140,7 @@ module Coradoc
|
|
|
140
140
|
Coradoc::AsciiDoc::Parser::Base.class_exec do
|
|
141
141
|
alias_method alias_name, rule_name
|
|
142
142
|
rule(rule_name) do
|
|
143
|
-
|
|
143
|
+
public_send(alias_name)
|
|
144
144
|
end
|
|
145
145
|
end
|
|
146
146
|
elsif config(:add_dispatch) && config(:with_params)
|
|
@@ -74,11 +74,11 @@ module Coradoc
|
|
|
74
74
|
end
|
|
75
75
|
|
|
76
76
|
def block_content(n_deep = 3)
|
|
77
|
-
c = block_image
|
|
78
|
-
list |
|
|
79
|
-
text_line(false, unguarded: true) |
|
|
80
|
-
empty_line.as(:line_break)
|
|
77
|
+
c = block_image
|
|
81
78
|
c |= block(n_deep - 1) if n_deep.positive?
|
|
79
|
+
c |= list
|
|
80
|
+
c |= text_line(false, unguarded: true)
|
|
81
|
+
c |= empty_line.as(:line_break)
|
|
82
82
|
c.repeat(1)
|
|
83
83
|
end
|
|
84
84
|
|
|
@@ -111,27 +111,22 @@ module Coradoc
|
|
|
111
111
|
# @param repeater [Integer] Minimum number of delimiter characters (default: 4)
|
|
112
112
|
# @param type [Symbol] Block type for special handling (e.g., :pass)
|
|
113
113
|
def block_style(n_deep = 3, delimiter = '*', repeater = 4, type = nil, verbatim: false)
|
|
114
|
-
|
|
115
|
-
current_delimiter = str(delimiter).repeat(repeater).capture(
|
|
114
|
+
capture_key = :"delimit_#{delimiter}_#{n_deep}"
|
|
115
|
+
current_delimiter = str(delimiter).repeat(repeater).capture(capture_key)
|
|
116
116
|
closing_delimiter = dynamic do |_s, c|
|
|
117
|
-
str(c.captures[
|
|
117
|
+
str(c.captures[capture_key].to_s.strip)
|
|
118
118
|
end
|
|
119
119
|
|
|
120
|
-
# Create a block content parser that respects the closing delimiter
|
|
121
|
-
# This prevents nested blocks from consuming the closing delimiter
|
|
122
120
|
block_content_with_closing = dynamic do |_s, c|
|
|
123
|
-
delim_str = c.captures[
|
|
121
|
+
delim_str = c.captures[capture_key].to_s.strip
|
|
124
122
|
closing_pattern = str(delim_str) >> newline
|
|
125
123
|
|
|
126
|
-
|
|
127
|
-
content
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
content |= block(n_deep - 1)
|
|
132
|
-
end
|
|
124
|
+
content = block_image
|
|
125
|
+
content |= block(n_deep - 1) if n_deep.positive?
|
|
126
|
+
content |= list
|
|
127
|
+
content |= text_line(false, unguarded: true, verbatim: verbatim)
|
|
128
|
+
content |= empty_line.as(:line_break)
|
|
133
129
|
|
|
134
|
-
# Each content element must not start with the closing delimiter
|
|
135
130
|
(closing_pattern.absent? >> content).repeat(1)
|
|
136
131
|
end
|
|
137
132
|
|
|
@@ -142,7 +137,6 @@ module Coradoc
|
|
|
142
137
|
if type == :pass
|
|
143
138
|
(text_line(false, unguarded: true, verbatim: verbatim) | empty_line.as(:line_break)).repeat(1).as(:lines)
|
|
144
139
|
else
|
|
145
|
-
# Use dynamic block content that respects closing delimiter
|
|
146
140
|
block_content_with_closing.as(:lines)
|
|
147
141
|
end >>
|
|
148
142
|
line_start? >>
|
|
@@ -152,18 +146,21 @@ module Coradoc
|
|
|
152
146
|
# Block style parser with EXACT delimiter length (for open blocks)
|
|
153
147
|
# Open blocks use exactly 2 dashes and cannot nest within themselves
|
|
154
148
|
def block_style_exact(n_deep = 3, delimiter = '-', exact_chars = 2, type = nil)
|
|
155
|
-
|
|
149
|
+
capture_key = :"delimit_#{delimiter}_exact_#{exact_chars}_#{n_deep}"
|
|
150
|
+
current_delimiter = str(delimiter).repeat(exact_chars, exact_chars).capture(capture_key)
|
|
156
151
|
closing_delimiter = dynamic do |_s, c|
|
|
157
|
-
str(c.captures[
|
|
152
|
+
str(c.captures[capture_key].to_s.strip)
|
|
158
153
|
end
|
|
159
154
|
|
|
160
|
-
# Create a block content parser that respects the closing delimiter
|
|
161
155
|
block_content_with_closing = dynamic do |_s, c|
|
|
162
|
-
delim_str = c.captures[
|
|
156
|
+
delim_str = c.captures[capture_key].to_s.strip
|
|
163
157
|
closing_pattern = str(delim_str) >> newline
|
|
164
158
|
|
|
165
|
-
content = block_image
|
|
159
|
+
content = block_image
|
|
166
160
|
content |= block(n_deep - 1) if n_deep.positive?
|
|
161
|
+
content |= list
|
|
162
|
+
content |= text_line(false, unguarded: true)
|
|
163
|
+
content |= empty_line.as(:line_break)
|
|
167
164
|
|
|
168
165
|
(closing_pattern.absent? >> content).repeat(1)
|
|
169
166
|
end
|
|
@@ -41,13 +41,10 @@ module Coradoc
|
|
|
41
41
|
|
|
42
42
|
def olist_marker(nesting_level = 1)
|
|
43
43
|
# Don't match table cell format specs like ".2+^.^|"
|
|
44
|
-
# Table cells have format: [colspan][.rowspan][halign][valign][style][*]|
|
|
45
|
-
# If we see a format spec pattern followed by "|", it's a table cell, not a list
|
|
46
44
|
line_start? >>
|
|
45
|
+
(nesting_level > 1 ? literal_space.maybe : str('')) >>
|
|
47
46
|
str('.' * nesting_level) >>
|
|
48
47
|
str('.').absent? >>
|
|
49
|
-
# Don't match if followed by table cell format spec
|
|
50
|
-
# Pattern: digits, dots, plus, alignment chars (^<>), style letters, then |
|
|
51
48
|
(
|
|
52
49
|
(match['0-9.<>^'] | str('+')).repeat(0, 3) >> str('|')
|
|
53
50
|
).absent?
|
|
@@ -75,11 +72,10 @@ module Coradoc
|
|
|
75
72
|
end
|
|
76
73
|
|
|
77
74
|
def ulist_marker(nesting_level = 1)
|
|
78
|
-
# Don't match table delimiters like "|==="
|
|
79
75
|
line_start? >>
|
|
76
|
+
(nesting_level > 1 ? literal_space.maybe : str('')) >>
|
|
80
77
|
str('*' * nesting_level) >>
|
|
81
78
|
str('*').absent? >>
|
|
82
|
-
# Don't match if followed by "===" (table delimiter)
|
|
83
79
|
str('===').absent?
|
|
84
80
|
end
|
|
85
81
|
|
|
@@ -107,8 +103,10 @@ module Coradoc
|
|
|
107
103
|
end
|
|
108
104
|
|
|
109
105
|
def dlist_term(_delimiter)
|
|
110
|
-
|
|
111
|
-
|
|
106
|
+
(element_id_inline.maybe >>
|
|
107
|
+
match("[^\n:]").repeat(1)
|
|
108
|
+
.as(:text)
|
|
109
|
+
).as(:dlist_term) >> dlist_delimiter
|
|
112
110
|
end
|
|
113
111
|
|
|
114
112
|
def dlist_definition
|
|
@@ -142,12 +142,12 @@ module Coradoc
|
|
|
142
142
|
new_row_signal = newline >> (str(delim_char) | format_spec_then_delim)
|
|
143
143
|
|
|
144
144
|
# A single content character - match any char that doesn't signal end of cell
|
|
145
|
+
# Handle escaped delimiter (\|) as a literal delimiter character
|
|
146
|
+
escaped_delimiter = str('\\') >> str(delim_char)
|
|
145
147
|
(
|
|
146
148
|
str(closing_delim).absent? >>
|
|
147
149
|
new_row_signal.absent? >>
|
|
148
|
-
str(delim_char).absent? >>
|
|
149
|
-
format_spec_then_delim.absent? >>
|
|
150
|
-
any
|
|
150
|
+
(escaped_delimiter | (str(delim_char).absent? >> any))
|
|
151
151
|
).repeat(0)
|
|
152
152
|
end
|
|
153
153
|
end
|
|
@@ -66,8 +66,8 @@ module Coradoc
|
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
def transform_structural_element(element)
|
|
69
|
-
case element
|
|
70
|
-
when
|
|
69
|
+
case element
|
|
70
|
+
when CoreModel::DocumentElement
|
|
71
71
|
header = if element.title
|
|
72
72
|
Coradoc::AsciiDoc::Model::Header.new(
|
|
73
73
|
title: Coradoc::AsciiDoc::Model::Title.new(
|
|
@@ -84,7 +84,7 @@ module Coradoc
|
|
|
84
84
|
header: header,
|
|
85
85
|
sections: transform(element.children)
|
|
86
86
|
)
|
|
87
|
-
when
|
|
87
|
+
when CoreModel::SectionElement
|
|
88
88
|
Coradoc::AsciiDoc::Model::Section.new(
|
|
89
89
|
id: element.id,
|
|
90
90
|
level: element.level,
|
|
@@ -257,7 +257,7 @@ module Coradoc
|
|
|
257
257
|
end
|
|
258
258
|
|
|
259
259
|
def transform_inline(inline)
|
|
260
|
-
case inline.
|
|
260
|
+
case inline.resolve_format_type
|
|
261
261
|
when 'bold'
|
|
262
262
|
Coradoc::AsciiDoc::Model::Inline::Bold.new(content: inline.content)
|
|
263
263
|
when 'italic'
|
|
@@ -401,7 +401,6 @@ module Coradoc
|
|
|
401
401
|
}.freeze
|
|
402
402
|
|
|
403
403
|
def resolve_semantic_type(block)
|
|
404
|
-
# Polymorphic dispatch: typed classes override semantic_type
|
|
405
404
|
semantic = block.resolve_semantic_type
|
|
406
405
|
return semantic if semantic
|
|
407
406
|
|
|
@@ -411,7 +410,6 @@ module Coradoc
|
|
|
411
410
|
|
|
412
411
|
case delim
|
|
413
412
|
when 'comment' then :comment
|
|
414
|
-
when 'paragraph' then :paragraph
|
|
415
413
|
when '[verse]' then :verse
|
|
416
414
|
when '>' then :quote
|
|
417
415
|
when "'''", '---', '___', '***' then :horizontal_rule
|