coradoc 1.1.8 → 2.0.12
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.
Potentially problematic release.
This version of coradoc might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.rspec +1 -1
- data/Rakefile +3 -12
- data/exe/coradoc +21 -2
- data/lib/coradoc/cli.rb +185 -91
- data/lib/coradoc/configurable.rb +527 -0
- data/lib/coradoc/coradoc.rb +463 -0
- data/lib/coradoc/core_model/annotation_block.rb +57 -0
- data/lib/coradoc/core_model/base.rb +172 -0
- data/lib/coradoc/core_model/bibliography.rb +41 -0
- data/lib/coradoc/core_model/bibliography_entry.rb +48 -0
- data/lib/coradoc/core_model/block.rb +63 -0
- data/lib/coradoc/core_model/children_content.rb +53 -0
- data/lib/coradoc/core_model/comment_block.rb +10 -0
- data/lib/coradoc/core_model/definition_item.rb +46 -0
- data/lib/coradoc/core_model/definition_list.rb +28 -0
- data/lib/coradoc/core_model/element_attribute.rb +26 -0
- data/lib/coradoc/core_model/example_block.rb +10 -0
- data/lib/coradoc/core_model/footnote.rb +92 -0
- data/lib/coradoc/core_model/horizontal_rule_block.rb +10 -0
- data/lib/coradoc/core_model/id_generator.rb +16 -0
- data/lib/coradoc/core_model/image.rb +66 -0
- data/lib/coradoc/core_model/inline_element.rb +140 -0
- data/lib/coradoc/core_model/list_block.rb +135 -0
- data/lib/coradoc/core_model/list_item.rb +142 -0
- data/lib/coradoc/core_model/listing_block.rb +13 -0
- data/lib/coradoc/core_model/literal_block.rb +10 -0
- data/lib/coradoc/core_model/metadata.rb +79 -0
- data/lib/coradoc/core_model/open_block.rb +10 -0
- data/lib/coradoc/core_model/paragraph_block.rb +10 -0
- data/lib/coradoc/core_model/pass_block.rb +10 -0
- data/lib/coradoc/core_model/quote_block.rb +12 -0
- data/lib/coradoc/core_model/reviewer_block.rb +10 -0
- data/lib/coradoc/core_model/sidebar_block.rb +10 -0
- data/lib/coradoc/core_model/source_block.rb +10 -0
- data/lib/coradoc/core_model/structural_element.rb +94 -0
- data/lib/coradoc/core_model/table.rb +148 -0
- data/lib/coradoc/core_model/term.rb +53 -0
- data/lib/coradoc/core_model/text_content.rb +22 -0
- data/lib/coradoc/core_model/toc.rb +105 -0
- data/lib/coradoc/core_model/toc_generator.rb +151 -0
- data/lib/coradoc/core_model/verse_block.rb +12 -0
- data/lib/coradoc/core_model.rb +77 -0
- data/lib/coradoc/document_builder.rb +184 -0
- data/lib/coradoc/document_manipulator.rb +203 -0
- data/lib/coradoc/errors.rb +312 -0
- data/lib/coradoc/format_module.rb +49 -0
- data/lib/coradoc/hooks.rb +176 -0
- data/lib/coradoc/input.rb +17 -7
- data/lib/coradoc/logger.rb +54 -0
- data/lib/coradoc/output.rb +17 -6
- data/lib/coradoc/performance_regression.rb +109 -0
- data/lib/coradoc/processor_registry.rb +50 -0
- data/lib/coradoc/query.rb +455 -0
- data/lib/coradoc/registry.rb +156 -0
- data/lib/coradoc/serializer/registry.rb +150 -0
- data/lib/coradoc/transform.rb +11 -0
- data/lib/coradoc/validation.rb +646 -0
- data/lib/coradoc/version.rb +1 -1
- data/lib/coradoc/visitor.rb +283 -0
- data/lib/coradoc.rb +40 -19
- metadata +67 -277
- data/.editorconfig +0 -15
- data/.envrc +0 -1
- data/.irbrc +0 -1
- data/.pryrc.sample +0 -1
- data/.rubocop.yml +0 -14
- data/.rubocop_todo.yml +0 -179
- data/CHANGELOG.md +0 -9
- data/CODE_OF_CONDUCT.md +0 -84
- data/Dockerfile +0 -19
- data/Gemfile +0 -16
- data/LICENSE.txt +0 -21
- data/Makefile +0 -35
- data/README.Docker.adoc +0 -57
- data/README.adoc +0 -119
- data/coradoc.gemspec +0 -40
- data/docker-compose.yml +0 -14
- data/exe/reverse_adoc +0 -81
- data/exe/w2a +0 -60
- data/flake.lock +0 -114
- data/flake.nix +0 -135
- data/lib/coradoc/converter.rb +0 -144
- data/lib/coradoc/document.rb +0 -77
- data/lib/coradoc/element/admonition.rb +0 -18
- data/lib/coradoc/element/attribute.rb +0 -36
- data/lib/coradoc/element/attribute_list.rb +0 -138
- data/lib/coradoc/element/audio.rb +0 -33
- data/lib/coradoc/element/author.rb +0 -24
- data/lib/coradoc/element/base.rb +0 -92
- data/lib/coradoc/element/bibliography.rb +0 -24
- data/lib/coradoc/element/bibliography_entry.rb +0 -24
- data/lib/coradoc/element/block/core.rb +0 -76
- data/lib/coradoc/element/block/example.rb +0 -23
- data/lib/coradoc/element/block/listing.rb +0 -21
- data/lib/coradoc/element/block/literal.rb +0 -21
- data/lib/coradoc/element/block/open.rb +0 -22
- data/lib/coradoc/element/block/pass.rb +0 -21
- data/lib/coradoc/element/block/quote.rb +0 -19
- data/lib/coradoc/element/block/reviewer_comment.rb +0 -19
- data/lib/coradoc/element/block/side.rb +0 -19
- data/lib/coradoc/element/block/sourcecode.rb +0 -21
- data/lib/coradoc/element/block.rb +0 -17
- data/lib/coradoc/element/break.rb +0 -11
- data/lib/coradoc/element/comment_block.rb +0 -22
- data/lib/coradoc/element/comment_line.rb +0 -18
- data/lib/coradoc/element/document_attributes.rb +0 -33
- data/lib/coradoc/element/header.rb +0 -22
- data/lib/coradoc/element/image/block_image.rb +0 -32
- data/lib/coradoc/element/image/core.rb +0 -58
- data/lib/coradoc/element/image/inline_image.rb +0 -12
- data/lib/coradoc/element/image.rb +0 -10
- data/lib/coradoc/element/include.rb +0 -18
- data/lib/coradoc/element/inline/anchor.rb +0 -19
- data/lib/coradoc/element/inline/attribute_reference.rb +0 -19
- data/lib/coradoc/element/inline/bold.rb +0 -25
- data/lib/coradoc/element/inline/cross_reference.rb +0 -46
- data/lib/coradoc/element/inline/footnote.rb +0 -24
- data/lib/coradoc/element/inline/hard_line_break.rb +0 -11
- data/lib/coradoc/element/inline/highlight.rb +0 -25
- data/lib/coradoc/element/inline/italic.rb +0 -25
- data/lib/coradoc/element/inline/link.rb +0 -42
- data/lib/coradoc/element/inline/monospace.rb +0 -25
- data/lib/coradoc/element/inline/quotation.rb +0 -20
- data/lib/coradoc/element/inline/small.rb +0 -19
- data/lib/coradoc/element/inline/span.rb +0 -37
- data/lib/coradoc/element/inline/subscript.rb +0 -20
- data/lib/coradoc/element/inline/superscript.rb +0 -20
- data/lib/coradoc/element/inline/underline.rb +0 -19
- data/lib/coradoc/element/inline.rb +0 -23
- data/lib/coradoc/element/list/core.rb +0 -51
- data/lib/coradoc/element/list/definition.rb +0 -29
- data/lib/coradoc/element/list/ordered.rb +0 -17
- data/lib/coradoc/element/list/unordered.rb +0 -17
- data/lib/coradoc/element/list.rb +0 -13
- data/lib/coradoc/element/list_item.rb +0 -98
- data/lib/coradoc/element/list_item_definition.rb +0 -32
- data/lib/coradoc/element/paragraph.rb +0 -37
- data/lib/coradoc/element/revision.rb +0 -27
- data/lib/coradoc/element/section.rb +0 -62
- data/lib/coradoc/element/table.rb +0 -91
- data/lib/coradoc/element/tag.rb +0 -19
- data/lib/coradoc/element/term.rb +0 -22
- data/lib/coradoc/element/text_element.rb +0 -92
- data/lib/coradoc/element/title.rb +0 -62
- data/lib/coradoc/element/video.rb +0 -50
- data/lib/coradoc/generator.rb +0 -19
- data/lib/coradoc/input/adoc.rb +0 -30
- data/lib/coradoc/input/docx.rb +0 -64
- data/lib/coradoc/input/html/LICENSE.txt +0 -25
- data/lib/coradoc/input/html/README.adoc +0 -308
- data/lib/coradoc/input/html/cleaner.rb +0 -142
- data/lib/coradoc/input/html/config.rb +0 -77
- data/lib/coradoc/input/html/converters/a.rb +0 -52
- data/lib/coradoc/input/html/converters/aside.rb +0 -16
- data/lib/coradoc/input/html/converters/audio.rb +0 -29
- data/lib/coradoc/input/html/converters/base.rb +0 -108
- data/lib/coradoc/input/html/converters/blockquote.rb +0 -22
- data/lib/coradoc/input/html/converters/br.rb +0 -15
- data/lib/coradoc/input/html/converters/bypass.rb +0 -81
- data/lib/coradoc/input/html/converters/code.rb +0 -23
- data/lib/coradoc/input/html/converters/div.rb +0 -19
- data/lib/coradoc/input/html/converters/dl.rb +0 -62
- data/lib/coradoc/input/html/converters/drop.rb +0 -26
- data/lib/coradoc/input/html/converters/em.rb +0 -21
- data/lib/coradoc/input/html/converters/figure.rb +0 -25
- data/lib/coradoc/input/html/converters/h.rb +0 -42
- data/lib/coradoc/input/html/converters/head.rb +0 -23
- data/lib/coradoc/input/html/converters/hr.rb +0 -15
- data/lib/coradoc/input/html/converters/ignore.rb +0 -20
- data/lib/coradoc/input/html/converters/img.rb +0 -110
- data/lib/coradoc/input/html/converters/li.rb +0 -17
- data/lib/coradoc/input/html/converters/mark.rb +0 -19
- data/lib/coradoc/input/html/converters/markup.rb +0 -31
- data/lib/coradoc/input/html/converters/math.rb +0 -38
- data/lib/coradoc/input/html/converters/ol.rb +0 -65
- data/lib/coradoc/input/html/converters/p.rb +0 -23
- data/lib/coradoc/input/html/converters/pass_through.rb +0 -17
- data/lib/coradoc/input/html/converters/pre.rb +0 -55
- data/lib/coradoc/input/html/converters/q.rb +0 -16
- data/lib/coradoc/input/html/converters/strong.rb +0 -20
- data/lib/coradoc/input/html/converters/sub.rb +0 -22
- data/lib/coradoc/input/html/converters/sup.rb +0 -22
- data/lib/coradoc/input/html/converters/table.rb +0 -319
- data/lib/coradoc/input/html/converters/td.rb +0 -81
- data/lib/coradoc/input/html/converters/text.rb +0 -32
- data/lib/coradoc/input/html/converters/th.rb +0 -18
- data/lib/coradoc/input/html/converters/tr.rb +0 -22
- data/lib/coradoc/input/html/converters/video.rb +0 -29
- data/lib/coradoc/input/html/converters.rb +0 -59
- data/lib/coradoc/input/html/errors.rb +0 -14
- data/lib/coradoc/input/html/html_converter.rb +0 -168
- data/lib/coradoc/input/html/plugin.rb +0 -131
- data/lib/coradoc/input/html/plugins/plateau.rb +0 -213
- data/lib/coradoc/input/html/postprocessor.rb +0 -220
- data/lib/coradoc/input/html.rb +0 -61
- data/lib/coradoc/legacy_parser.rb +0 -200
- data/lib/coradoc/oscal.rb +0 -99
- data/lib/coradoc/output/adoc.rb +0 -19
- data/lib/coradoc/output/coradoc_tree_debug.rb +0 -21
- data/lib/coradoc/parser/asciidoc/admonition.rb +0 -24
- data/lib/coradoc/parser/asciidoc/attribute_list.rb +0 -89
- data/lib/coradoc/parser/asciidoc/base.rb +0 -87
- data/lib/coradoc/parser/asciidoc/bibliography.rb +0 -29
- data/lib/coradoc/parser/asciidoc/block.rb +0 -94
- data/lib/coradoc/parser/asciidoc/citation.rb +0 -30
- data/lib/coradoc/parser/asciidoc/content.rb +0 -64
- data/lib/coradoc/parser/asciidoc/document_attributes.rb +0 -25
- data/lib/coradoc/parser/asciidoc/header.rb +0 -29
- data/lib/coradoc/parser/asciidoc/inline.rb +0 -195
- data/lib/coradoc/parser/asciidoc/list.rb +0 -115
- data/lib/coradoc/parser/asciidoc/paragraph.rb +0 -54
- data/lib/coradoc/parser/asciidoc/section.rb +0 -61
- data/lib/coradoc/parser/asciidoc/table.rb +0 -32
- data/lib/coradoc/parser/asciidoc/term.rb +0 -41
- data/lib/coradoc/parser/asciidoc/text.rb +0 -158
- data/lib/coradoc/parser/base.rb +0 -40
- data/lib/coradoc/parser.rb +0 -11
- data/lib/coradoc/reverse_adoc.rb +0 -18
- data/lib/coradoc/transformer.rb +0 -476
- data/lib/coradoc/util.rb +0 -12
- data/lib/reverse_adoc.rb +0 -20
- data/utils/inspect_asciidoc.rb +0 -29
- data/utils/parser_analyzer.rb +0 -66
- data/utils/round_trip.rb +0 -53
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
class DocumentBuilder
|
|
5
|
+
attr_reader :document
|
|
6
|
+
|
|
7
|
+
def self.build(&block)
|
|
8
|
+
builder = new
|
|
9
|
+
builder.instance_eval(&block) if block_given?
|
|
10
|
+
builder
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@document = CoreModel::DocumentElement.new(
|
|
15
|
+
children: []
|
|
16
|
+
)
|
|
17
|
+
@current_context = @document
|
|
18
|
+
@context_stack = []
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def title(text)
|
|
22
|
+
@document.title = text
|
|
23
|
+
self
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def section(title_text, level: 1, &block)
|
|
27
|
+
new_section = CoreModel::SectionElement.new(
|
|
28
|
+
level: level,
|
|
29
|
+
title: title_text,
|
|
30
|
+
children: []
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
@current_context.children << new_section
|
|
34
|
+
|
|
35
|
+
if block_given?
|
|
36
|
+
push_context(new_section)
|
|
37
|
+
instance_eval(&block)
|
|
38
|
+
pop_context
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
self
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def paragraph(text)
|
|
45
|
+
@current_context.children << CoreModel::ParagraphBlock.new(
|
|
46
|
+
content: text
|
|
47
|
+
)
|
|
48
|
+
self
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def code(code_text, language: nil)
|
|
52
|
+
@current_context.children << CoreModel::SourceBlock.new(
|
|
53
|
+
content: code_text,
|
|
54
|
+
language: language
|
|
55
|
+
)
|
|
56
|
+
self
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def blockquote(text, attribution: nil)
|
|
60
|
+
block = CoreModel::QuoteBlock.new(
|
|
61
|
+
content: text,
|
|
62
|
+
attribution: attribution
|
|
63
|
+
)
|
|
64
|
+
@current_context.children << block
|
|
65
|
+
self
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def list(type = :unordered, &block)
|
|
69
|
+
list_items = []
|
|
70
|
+
list_type = type
|
|
71
|
+
|
|
72
|
+
wrapper = Object.new
|
|
73
|
+
wrapper.define_singleton_method(:item) do |text|
|
|
74
|
+
marker = list_type == :ordered ? '1.' : '*'
|
|
75
|
+
list_items << CoreModel::ListItem.new(content: text, marker: marker)
|
|
76
|
+
wrapper
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
wrapper.instance_eval(&block) if block_given?
|
|
80
|
+
|
|
81
|
+
@current_context.children << CoreModel::ListBlock.new(
|
|
82
|
+
marker_type: type.to_s,
|
|
83
|
+
items: list_items
|
|
84
|
+
)
|
|
85
|
+
self
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
alias ordered_list list
|
|
89
|
+
alias unordered_list list
|
|
90
|
+
|
|
91
|
+
def bulleted_list(&block)
|
|
92
|
+
list(:unordered, &block)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def numbered_list(&block)
|
|
96
|
+
list(:ordered, &block)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def image(src, alt: '', title: nil)
|
|
100
|
+
img = CoreModel::Image.new(src: src, alt: alt)
|
|
101
|
+
img.title = title if title
|
|
102
|
+
@current_context.children << img
|
|
103
|
+
self
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def table(headers = [], rows = [])
|
|
107
|
+
table_rows = []
|
|
108
|
+
|
|
109
|
+
if headers.any?
|
|
110
|
+
header_cells = headers.map { |h| CoreModel::TableCell.new(content: h, header: true) }
|
|
111
|
+
table_rows << CoreModel::TableRow.new(cells: header_cells)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
rows.each do |row|
|
|
115
|
+
cells = row.map { |c| CoreModel::TableCell.new(content: c.to_s) }
|
|
116
|
+
table_rows << CoreModel::TableRow.new(cells: cells)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
@current_context.children << CoreModel::Table.new(rows: table_rows)
|
|
120
|
+
self
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def hr
|
|
124
|
+
@current_context.children << CoreModel::HorizontalRuleBlock.new
|
|
125
|
+
self
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def text(text_content)
|
|
129
|
+
@current_context.children << CoreModel::Block.new(
|
|
130
|
+
content: text_content
|
|
131
|
+
)
|
|
132
|
+
self
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def admonition(type, text)
|
|
136
|
+
@current_context.children << CoreModel::AnnotationBlock.new(
|
|
137
|
+
annotation_type: type.to_s,
|
|
138
|
+
content: text
|
|
139
|
+
)
|
|
140
|
+
self
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
%i[note warning tip important caution].each do |admonition_type|
|
|
144
|
+
define_method(admonition_type) do |text|
|
|
145
|
+
admonition(admonition_type, text)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def to_core
|
|
150
|
+
@document
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def to(format, **options)
|
|
154
|
+
Coradoc.serialize(@document, to: format, **options)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def to_html(**options)
|
|
158
|
+
to(:html, **options)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def to_markdown(**options)
|
|
162
|
+
to(:markdown, **options)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def to_asciidoc(**options)
|
|
166
|
+
to(:asciidoc, **options)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
private
|
|
170
|
+
|
|
171
|
+
def push_context(new_context)
|
|
172
|
+
@context_stack << @current_context
|
|
173
|
+
@current_context = new_context
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def pop_context
|
|
177
|
+
@current_context = @context_stack.pop
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def self.build(&block)
|
|
182
|
+
DocumentBuilder.build(&block)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
class DocumentManipulator
|
|
5
|
+
attr_reader :document
|
|
6
|
+
|
|
7
|
+
def initialize(document)
|
|
8
|
+
unless document.is_a?(Coradoc::CoreModel::Base)
|
|
9
|
+
raise ArgumentError,
|
|
10
|
+
"Expected CoreModel::Base, got #{document.class}"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
@document = document
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def query(selector)
|
|
17
|
+
Coradoc::Query.query(@document, selector).to_a
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def select_sections(level: nil, title: nil)
|
|
21
|
+
filtered = filter_sections(@document, level: level, title: title)
|
|
22
|
+
DocumentManipulator.new(filtered)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def transform_text
|
|
26
|
+
return self unless block_given?
|
|
27
|
+
|
|
28
|
+
Visitor::Transformer.new do |element|
|
|
29
|
+
case element
|
|
30
|
+
when CoreModel::InlineElement
|
|
31
|
+
element.content = yield(element.content) if element.content.is_a?(String)
|
|
32
|
+
when CoreModel::Block
|
|
33
|
+
element.content = yield(element.content) if element.content.is_a?(String)
|
|
34
|
+
end
|
|
35
|
+
end.visit(@document)
|
|
36
|
+
self
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def transform_headings
|
|
40
|
+
return self unless block_given?
|
|
41
|
+
|
|
42
|
+
Visitor::Transformer.new do |element|
|
|
43
|
+
element.title = yield(element.title) if element.is_a?(CoreModel::StructuralElement) && element.title.is_a?(String)
|
|
44
|
+
end.visit(@document)
|
|
45
|
+
self
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def add_toc(levels: 3, position: :top)
|
|
49
|
+
sections = collect_sections(@document, max_level: levels)
|
|
50
|
+
toc = CoreModel::TocGenerator.generate(sections)
|
|
51
|
+
|
|
52
|
+
toc_element = CoreModel::Block.new(block_semantic_type: 'toc', content: toc)
|
|
53
|
+
case position
|
|
54
|
+
when :top
|
|
55
|
+
@document.children = [toc_element] + @document.children
|
|
56
|
+
when :bottom
|
|
57
|
+
@document.children = @document.children + [toc_element]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
self
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def remove_elements(element_type)
|
|
64
|
+
Visitor::Transformer.new do |element|
|
|
65
|
+
next unless element.is_a?(CoreModel::StructuralElement) && element.children
|
|
66
|
+
|
|
67
|
+
element.children.reject! do |child|
|
|
68
|
+
match_element_type?(child, element_type)
|
|
69
|
+
end
|
|
70
|
+
end.visit(@document)
|
|
71
|
+
self
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def add_metadata(metadata)
|
|
75
|
+
metadata.each do |key, value|
|
|
76
|
+
@document.set_metadata(key.to_s, value.to_s)
|
|
77
|
+
end
|
|
78
|
+
self
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def set_title(title)
|
|
82
|
+
@document.title = title
|
|
83
|
+
self
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def set_id(id)
|
|
87
|
+
@document.id = id
|
|
88
|
+
self
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def to_html(**options)
|
|
92
|
+
Coradoc.serialize(@document, to: :html, **options)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def to_markdown(**options)
|
|
96
|
+
Coradoc.serialize(@document, to: :markdown, **options)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def to_asciidoc(**options)
|
|
100
|
+
Coradoc.serialize(@document, to: :asciidoc, **options)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def to(format, **options)
|
|
104
|
+
Coradoc.serialize(@document, to: format, **options)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def to_core
|
|
108
|
+
@document
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def clone
|
|
112
|
+
DocumentManipulator.new(deep_clone(@document))
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def match_element_type?(child, element_type)
|
|
118
|
+
return false unless child.is_a?(CoreModel::Block)
|
|
119
|
+
|
|
120
|
+
case element_type
|
|
121
|
+
when :comment_line, :comment_block
|
|
122
|
+
child.is_a?(CoreModel::CommentBlock)
|
|
123
|
+
else
|
|
124
|
+
child.resolve_semantic_type == element_type.to_sym
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def filter_sections(element, level: nil, title: nil)
|
|
129
|
+
if element.is_a?(CoreModel::StructuralElement) && element.children
|
|
130
|
+
element.children = element.children
|
|
131
|
+
.map { |child| filter_sections(child, level: level, title: title) }
|
|
132
|
+
.compact
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
return nil if element.is_a?(CoreModel::StructuralElement) && element.section? && !element.document? && !section_matches?(
|
|
136
|
+
element, level: level, title: title
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
element
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def section_matches?(section, level: nil, title: nil)
|
|
143
|
+
if level
|
|
144
|
+
element_level = section.heading_level
|
|
145
|
+
case level
|
|
146
|
+
when Range then return false unless level.include?(element_level)
|
|
147
|
+
when Integer then return false unless element_level == level
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
if title
|
|
152
|
+
element_title = section.title || ''
|
|
153
|
+
case title
|
|
154
|
+
when String then return false unless element_title.include?(title)
|
|
155
|
+
when Regexp then return false unless element_title&.match?(title)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
true
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def collect_sections(element, max_level: 3, current_level: 1)
|
|
163
|
+
sections = []
|
|
164
|
+
return sections unless element.is_a?(CoreModel::StructuralElement)
|
|
165
|
+
|
|
166
|
+
element.children.each do |child|
|
|
167
|
+
next unless child.is_a?(CoreModel::StructuralElement) &&
|
|
168
|
+
child.section? && (current_level <= max_level)
|
|
169
|
+
|
|
170
|
+
sections << {
|
|
171
|
+
id: child.id,
|
|
172
|
+
title: child.title,
|
|
173
|
+
level: child.level || current_level,
|
|
174
|
+
children: collect_sections(child, max_level: max_level,
|
|
175
|
+
current_level: current_level + 1)
|
|
176
|
+
}
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
sections
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def deep_clone(element)
|
|
183
|
+
case element
|
|
184
|
+
when CoreModel::Base
|
|
185
|
+
cloned = element.class.new
|
|
186
|
+
element.class.attributes.each_key do |name|
|
|
187
|
+
cloned.public_send("#{name}=", deep_clone(element.public_send(name)))
|
|
188
|
+
end
|
|
189
|
+
cloned
|
|
190
|
+
when Array
|
|
191
|
+
element.map { |item| deep_clone(item) }
|
|
192
|
+
when Hash
|
|
193
|
+
element.transform_values { |v| deep_clone(v) }
|
|
194
|
+
else
|
|
195
|
+
begin
|
|
196
|
+
element.dup
|
|
197
|
+
rescue StandardError
|
|
198
|
+
element
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
# Base error class for all Coradoc errors
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Suggestion patterns for common parsing errors
|
|
8
|
+
#
|
|
9
|
+
# These patterns are matched against error messages and source content
|
|
10
|
+
# to provide helpful suggestions for fixing common issues.
|
|
11
|
+
ERROR_SUGGESTIONS = [
|
|
12
|
+
{
|
|
13
|
+
pattern: /unterminated.*string|unexpected.*end.*of.*input|expected.*["']/i,
|
|
14
|
+
suggestion: 'Check for unclosed quotes or strings',
|
|
15
|
+
examples: ["'text'", '"text"']
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
pattern: /unexpected.*indentation|indentation.*error|inconsistent.*indent/i,
|
|
19
|
+
suggestion: 'Check indentation - use consistent spaces or tabs',
|
|
20
|
+
examples: [' indented line', ' nested item']
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
pattern: /missing.*separator|expected.*delimiter|missing.*comma/i,
|
|
24
|
+
suggestion: 'Add missing separator between elements',
|
|
25
|
+
examples: ['item1, item2', 'key: value']
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
pattern: /invalid.*attribute|unknown.*attribute|attribute.*not.*allowed/i,
|
|
29
|
+
suggestion: 'Check attribute spelling and allowed values',
|
|
30
|
+
examples: ['[role=example]', '[source,ruby]']
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
pattern: /invalid.*heading|heading.*level|expected.*heading/i,
|
|
34
|
+
suggestion: 'Use valid heading syntax with = or # markers',
|
|
35
|
+
examples: ['= Level 1', '== Level 2', '### Level 3']
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
pattern: /invalid.*list|list.*marker|expected.*list.*item/i,
|
|
39
|
+
suggestion: 'Use correct list markers (*, -, ., or numbered)',
|
|
40
|
+
examples: ['* bullet', '. ordered', 'term:: definition']
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
pattern: /invalid.*link|malformed.*url|link.*syntax/i,
|
|
44
|
+
suggestion: 'Use correct link syntax: text[url] or link:url[]',
|
|
45
|
+
examples: ['Google[https://google.com]', 'link:file.adoc[]']
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
pattern: /invalid.*table|table.*delimiter|expected.*separator/i,
|
|
49
|
+
suggestion: 'Check table syntax with | delimiters',
|
|
50
|
+
examples: ["|===\n| Cell 1 | Cell 2\n|==="]
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
pattern: /invalid.*block|block.*delimiter|unterminated.*block/i,
|
|
54
|
+
suggestion: 'Ensure block delimiters match (----, ****, ====, etc.)',
|
|
55
|
+
examples: ["----\ncode\n----", "====\nexample\n===="]
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
pattern: /invalid.*macro|unknown.*macro|macro.*syntax/i,
|
|
59
|
+
suggestion: 'Check macro syntax: name:target[attributes]',
|
|
60
|
+
examples: ['include::file.adoc[]', 'image::image.png[]']
|
|
61
|
+
}
|
|
62
|
+
].freeze
|
|
63
|
+
|
|
64
|
+
# Enhanced error classes with source context support
|
|
65
|
+
#
|
|
66
|
+
# These error classes provide additional context such as line numbers,
|
|
67
|
+
# column positions, source snippets, and suggestions to help users debug issues.
|
|
68
|
+
#
|
|
69
|
+
# @example Raising a parse error with context
|
|
70
|
+
# raise ParseError.new(
|
|
71
|
+
# "Unexpected token",
|
|
72
|
+
# source: content,
|
|
73
|
+
# line: 10,
|
|
74
|
+
# column: 5
|
|
75
|
+
# )
|
|
76
|
+
#
|
|
77
|
+
# @example Handling errors with context
|
|
78
|
+
# begin
|
|
79
|
+
# Coradoc.parse(text, format: :markdown)
|
|
80
|
+
# rescue Coradoc::ParseError => e
|
|
81
|
+
# puts e.message_with_context
|
|
82
|
+
# puts e.suggestion if e.suggestion
|
|
83
|
+
# end
|
|
84
|
+
#
|
|
85
|
+
class ParseError < Error
|
|
86
|
+
attr_reader :source, :line, :column, :snippet_lines, :suggestion
|
|
87
|
+
|
|
88
|
+
# Create a new parse error with optional source context
|
|
89
|
+
#
|
|
90
|
+
# @param message [String] The error message
|
|
91
|
+
# @param source [String, nil] The source text being parsed
|
|
92
|
+
# @param line [Integer, nil] The line number (1-indexed)
|
|
93
|
+
# @param column [Integer, nil] The column number (1-indexed)
|
|
94
|
+
# @param snippet_lines [Integer] Number of context lines to show (default: 3)
|
|
95
|
+
# @param suggestion [String, nil] Optional suggestion for fixing the error
|
|
96
|
+
def initialize(message, source: nil, line: nil, column: nil, snippet_lines: 3,
|
|
97
|
+
suggestion: nil)
|
|
98
|
+
@source = source
|
|
99
|
+
@line = line
|
|
100
|
+
@column = column
|
|
101
|
+
@snippet_lines = snippet_lines
|
|
102
|
+
@suggestion = suggestion || find_suggestion(message, source, line)
|
|
103
|
+
super(build_message(message))
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Returns the error message with full context
|
|
107
|
+
#
|
|
108
|
+
# @return [String] Formatted error message with source snippet
|
|
109
|
+
def message_with_context
|
|
110
|
+
return message unless source && line
|
|
111
|
+
|
|
112
|
+
msg = message
|
|
113
|
+
msg += "\n\n"
|
|
114
|
+
msg += source_snippet
|
|
115
|
+
if suggestion
|
|
116
|
+
msg += "\n\n"
|
|
117
|
+
msg += "Suggestion: #{suggestion}"
|
|
118
|
+
end
|
|
119
|
+
msg
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Returns the source snippet around the error location
|
|
123
|
+
#
|
|
124
|
+
# @return [String] Formatted source snippet with line numbers
|
|
125
|
+
def source_snippet
|
|
126
|
+
return '' unless source && line
|
|
127
|
+
|
|
128
|
+
lines = source.lines
|
|
129
|
+
start_line = [1, line - snippet_lines].max
|
|
130
|
+
end_line = [lines.length, line + snippet_lines].min
|
|
131
|
+
|
|
132
|
+
snippet = []
|
|
133
|
+
(start_line..end_line).each do |i|
|
|
134
|
+
prefix = i == line ? '>>> ' : ' '
|
|
135
|
+
snippet_line = lines[i - 1]&.chomp || ''
|
|
136
|
+
snippet << "#{prefix}#{i.to_s.rjust(4)}: #{snippet_line}"
|
|
137
|
+
|
|
138
|
+
# Add column indicator on the error line
|
|
139
|
+
if i == line && column
|
|
140
|
+
indicator = "#{' ' * (prefix.length + 6 + column - 1)}^"
|
|
141
|
+
snippet << indicator
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
snippet.join("\n")
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Returns all suggestions that match this error
|
|
149
|
+
#
|
|
150
|
+
# @return [Array<String>] List of applicable suggestions
|
|
151
|
+
def all_suggestions
|
|
152
|
+
return [] unless message || source
|
|
153
|
+
|
|
154
|
+
suggestions = []
|
|
155
|
+
ERROR_SUGGESTIONS.each do |entry|
|
|
156
|
+
suggestions << format_suggestion(entry) if entry[:pattern].match?(message)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Also check source line if available
|
|
160
|
+
if source && line
|
|
161
|
+
source_line = source.lines[line - 1]
|
|
162
|
+
if source_line
|
|
163
|
+
ERROR_SUGGESTIONS.each do |entry|
|
|
164
|
+
suggestions << format_suggestion(entry) if entry[:pattern].match?(source_line)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
suggestions.uniq
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
private
|
|
173
|
+
|
|
174
|
+
def build_message(message)
|
|
175
|
+
context = []
|
|
176
|
+
context << "line #{line}" if line
|
|
177
|
+
context << "column #{column}" if column
|
|
178
|
+
|
|
179
|
+
if context.any?
|
|
180
|
+
"#{message} (at #{context.join(', ')})"
|
|
181
|
+
else
|
|
182
|
+
message
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def find_suggestion(message, source, line)
|
|
187
|
+
return nil unless message || source
|
|
188
|
+
|
|
189
|
+
# Check message against patterns
|
|
190
|
+
ERROR_SUGGESTIONS.each do |entry|
|
|
191
|
+
return format_suggestion(entry) if entry[:pattern].match?(message)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Check source line if available
|
|
195
|
+
if source && line
|
|
196
|
+
source_line = source.lines[line - 1]
|
|
197
|
+
if source_line
|
|
198
|
+
ERROR_SUGGESTIONS.each do |entry|
|
|
199
|
+
return format_suggestion(entry) if entry[:pattern].match?(source_line)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
nil
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def format_suggestion(entry)
|
|
208
|
+
result = entry[:suggestion]
|
|
209
|
+
result += " (e.g., #{entry[:examples].first(2).join(', ')})" if entry[:examples]&.any?
|
|
210
|
+
result
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Error raised when validation fails
|
|
215
|
+
#
|
|
216
|
+
# @example
|
|
217
|
+
# raise ValidationError.new(
|
|
218
|
+
# "Invalid document structure",
|
|
219
|
+
# errors: ["Missing title", "Empty section"]
|
|
220
|
+
# )
|
|
221
|
+
#
|
|
222
|
+
class ValidationError < Error
|
|
223
|
+
attr_reader :errors
|
|
224
|
+
|
|
225
|
+
# Create a new validation error
|
|
226
|
+
#
|
|
227
|
+
# @param message [String] The error message
|
|
228
|
+
# @param errors [Array<String>] List of specific validation errors
|
|
229
|
+
def initialize(message, errors: [])
|
|
230
|
+
@errors = errors
|
|
231
|
+
super(build_message(message))
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
private
|
|
235
|
+
|
|
236
|
+
def build_message(message)
|
|
237
|
+
return message if errors.empty?
|
|
238
|
+
|
|
239
|
+
"#{message}\n - #{errors.join("\n - ")}"
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Error raised when transformation fails
|
|
244
|
+
#
|
|
245
|
+
# @example
|
|
246
|
+
# raise TransformationError.new(
|
|
247
|
+
# "Cannot convert element",
|
|
248
|
+
# source_type: "Paragraph",
|
|
249
|
+
# target_type: "CoreModel::Block"
|
|
250
|
+
# )
|
|
251
|
+
#
|
|
252
|
+
class TransformationError < Error
|
|
253
|
+
attr_reader :source_type, :target_type
|
|
254
|
+
|
|
255
|
+
# Create a new transformation error
|
|
256
|
+
#
|
|
257
|
+
# @param message [String] The error message
|
|
258
|
+
# @param source_type [String, Class, nil] The source type being transformed
|
|
259
|
+
# @param target_type [String, Class, nil] The target type
|
|
260
|
+
def initialize(message, source_type: nil, target_type: nil)
|
|
261
|
+
@source_type = source_type
|
|
262
|
+
@target_type = target_type
|
|
263
|
+
super(build_message(message))
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
private
|
|
267
|
+
|
|
268
|
+
def build_message(message)
|
|
269
|
+
parts = [message]
|
|
270
|
+
parts << "source: #{source_type}" if source_type
|
|
271
|
+
parts << "target: #{target_type}" if target_type
|
|
272
|
+
parts.join(' (') + (parts.length > 1 ? ')' : '')
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Error raised when a file is not found
|
|
277
|
+
class FileNotFoundError < Error
|
|
278
|
+
attr_reader :path
|
|
279
|
+
|
|
280
|
+
def initialize(path)
|
|
281
|
+
@path = path
|
|
282
|
+
super("File not found: #{path}")
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Error raised when a requested format is not supported
|
|
287
|
+
#
|
|
288
|
+
# @example
|
|
289
|
+
# raise UnsupportedFormatError.new(:docx, available: [:html, :markdown])
|
|
290
|
+
#
|
|
291
|
+
class UnsupportedFormatError < Error
|
|
292
|
+
attr_reader :requested_format, :available_formats
|
|
293
|
+
|
|
294
|
+
# Create a new unsupported format error
|
|
295
|
+
#
|
|
296
|
+
# @param format [Symbol, String] The requested format
|
|
297
|
+
# @param available [Array<Symbol>] List of available formats
|
|
298
|
+
def initialize(format, available: [])
|
|
299
|
+
@requested_format = format
|
|
300
|
+
@available_formats = available
|
|
301
|
+
super(build_message)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
private
|
|
305
|
+
|
|
306
|
+
def build_message
|
|
307
|
+
msg = "Format '#{requested_format}' is not supported"
|
|
308
|
+
msg += ". Available formats: #{available_formats.join(', ')}" if available_formats.any?
|
|
309
|
+
msg
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|