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.

Files changed (225) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -1
  3. data/Rakefile +3 -12
  4. data/exe/coradoc +21 -2
  5. data/lib/coradoc/cli.rb +185 -91
  6. data/lib/coradoc/configurable.rb +527 -0
  7. data/lib/coradoc/coradoc.rb +463 -0
  8. data/lib/coradoc/core_model/annotation_block.rb +57 -0
  9. data/lib/coradoc/core_model/base.rb +172 -0
  10. data/lib/coradoc/core_model/bibliography.rb +41 -0
  11. data/lib/coradoc/core_model/bibliography_entry.rb +48 -0
  12. data/lib/coradoc/core_model/block.rb +63 -0
  13. data/lib/coradoc/core_model/children_content.rb +53 -0
  14. data/lib/coradoc/core_model/comment_block.rb +10 -0
  15. data/lib/coradoc/core_model/definition_item.rb +46 -0
  16. data/lib/coradoc/core_model/definition_list.rb +28 -0
  17. data/lib/coradoc/core_model/element_attribute.rb +26 -0
  18. data/lib/coradoc/core_model/example_block.rb +10 -0
  19. data/lib/coradoc/core_model/footnote.rb +92 -0
  20. data/lib/coradoc/core_model/horizontal_rule_block.rb +10 -0
  21. data/lib/coradoc/core_model/id_generator.rb +16 -0
  22. data/lib/coradoc/core_model/image.rb +66 -0
  23. data/lib/coradoc/core_model/inline_element.rb +140 -0
  24. data/lib/coradoc/core_model/list_block.rb +135 -0
  25. data/lib/coradoc/core_model/list_item.rb +142 -0
  26. data/lib/coradoc/core_model/listing_block.rb +13 -0
  27. data/lib/coradoc/core_model/literal_block.rb +10 -0
  28. data/lib/coradoc/core_model/metadata.rb +79 -0
  29. data/lib/coradoc/core_model/open_block.rb +10 -0
  30. data/lib/coradoc/core_model/paragraph_block.rb +10 -0
  31. data/lib/coradoc/core_model/pass_block.rb +10 -0
  32. data/lib/coradoc/core_model/quote_block.rb +12 -0
  33. data/lib/coradoc/core_model/reviewer_block.rb +10 -0
  34. data/lib/coradoc/core_model/sidebar_block.rb +10 -0
  35. data/lib/coradoc/core_model/source_block.rb +10 -0
  36. data/lib/coradoc/core_model/structural_element.rb +94 -0
  37. data/lib/coradoc/core_model/table.rb +148 -0
  38. data/lib/coradoc/core_model/term.rb +53 -0
  39. data/lib/coradoc/core_model/text_content.rb +22 -0
  40. data/lib/coradoc/core_model/toc.rb +105 -0
  41. data/lib/coradoc/core_model/toc_generator.rb +151 -0
  42. data/lib/coradoc/core_model/verse_block.rb +12 -0
  43. data/lib/coradoc/core_model.rb +77 -0
  44. data/lib/coradoc/document_builder.rb +184 -0
  45. data/lib/coradoc/document_manipulator.rb +203 -0
  46. data/lib/coradoc/errors.rb +312 -0
  47. data/lib/coradoc/format_module.rb +49 -0
  48. data/lib/coradoc/hooks.rb +176 -0
  49. data/lib/coradoc/input.rb +17 -7
  50. data/lib/coradoc/logger.rb +54 -0
  51. data/lib/coradoc/output.rb +17 -6
  52. data/lib/coradoc/performance_regression.rb +109 -0
  53. data/lib/coradoc/processor_registry.rb +50 -0
  54. data/lib/coradoc/query.rb +455 -0
  55. data/lib/coradoc/registry.rb +156 -0
  56. data/lib/coradoc/serializer/registry.rb +150 -0
  57. data/lib/coradoc/transform.rb +11 -0
  58. data/lib/coradoc/validation.rb +646 -0
  59. data/lib/coradoc/version.rb +1 -1
  60. data/lib/coradoc/visitor.rb +283 -0
  61. data/lib/coradoc.rb +40 -19
  62. metadata +67 -277
  63. data/.editorconfig +0 -15
  64. data/.envrc +0 -1
  65. data/.irbrc +0 -1
  66. data/.pryrc.sample +0 -1
  67. data/.rubocop.yml +0 -14
  68. data/.rubocop_todo.yml +0 -179
  69. data/CHANGELOG.md +0 -9
  70. data/CODE_OF_CONDUCT.md +0 -84
  71. data/Dockerfile +0 -19
  72. data/Gemfile +0 -16
  73. data/LICENSE.txt +0 -21
  74. data/Makefile +0 -35
  75. data/README.Docker.adoc +0 -57
  76. data/README.adoc +0 -119
  77. data/coradoc.gemspec +0 -40
  78. data/docker-compose.yml +0 -14
  79. data/exe/reverse_adoc +0 -81
  80. data/exe/w2a +0 -60
  81. data/flake.lock +0 -114
  82. data/flake.nix +0 -135
  83. data/lib/coradoc/converter.rb +0 -144
  84. data/lib/coradoc/document.rb +0 -77
  85. data/lib/coradoc/element/admonition.rb +0 -18
  86. data/lib/coradoc/element/attribute.rb +0 -36
  87. data/lib/coradoc/element/attribute_list.rb +0 -138
  88. data/lib/coradoc/element/audio.rb +0 -33
  89. data/lib/coradoc/element/author.rb +0 -24
  90. data/lib/coradoc/element/base.rb +0 -92
  91. data/lib/coradoc/element/bibliography.rb +0 -24
  92. data/lib/coradoc/element/bibliography_entry.rb +0 -24
  93. data/lib/coradoc/element/block/core.rb +0 -76
  94. data/lib/coradoc/element/block/example.rb +0 -23
  95. data/lib/coradoc/element/block/listing.rb +0 -21
  96. data/lib/coradoc/element/block/literal.rb +0 -21
  97. data/lib/coradoc/element/block/open.rb +0 -22
  98. data/lib/coradoc/element/block/pass.rb +0 -21
  99. data/lib/coradoc/element/block/quote.rb +0 -19
  100. data/lib/coradoc/element/block/reviewer_comment.rb +0 -19
  101. data/lib/coradoc/element/block/side.rb +0 -19
  102. data/lib/coradoc/element/block/sourcecode.rb +0 -21
  103. data/lib/coradoc/element/block.rb +0 -17
  104. data/lib/coradoc/element/break.rb +0 -11
  105. data/lib/coradoc/element/comment_block.rb +0 -22
  106. data/lib/coradoc/element/comment_line.rb +0 -18
  107. data/lib/coradoc/element/document_attributes.rb +0 -33
  108. data/lib/coradoc/element/header.rb +0 -22
  109. data/lib/coradoc/element/image/block_image.rb +0 -32
  110. data/lib/coradoc/element/image/core.rb +0 -58
  111. data/lib/coradoc/element/image/inline_image.rb +0 -12
  112. data/lib/coradoc/element/image.rb +0 -10
  113. data/lib/coradoc/element/include.rb +0 -18
  114. data/lib/coradoc/element/inline/anchor.rb +0 -19
  115. data/lib/coradoc/element/inline/attribute_reference.rb +0 -19
  116. data/lib/coradoc/element/inline/bold.rb +0 -25
  117. data/lib/coradoc/element/inline/cross_reference.rb +0 -46
  118. data/lib/coradoc/element/inline/footnote.rb +0 -24
  119. data/lib/coradoc/element/inline/hard_line_break.rb +0 -11
  120. data/lib/coradoc/element/inline/highlight.rb +0 -25
  121. data/lib/coradoc/element/inline/italic.rb +0 -25
  122. data/lib/coradoc/element/inline/link.rb +0 -42
  123. data/lib/coradoc/element/inline/monospace.rb +0 -25
  124. data/lib/coradoc/element/inline/quotation.rb +0 -20
  125. data/lib/coradoc/element/inline/small.rb +0 -19
  126. data/lib/coradoc/element/inline/span.rb +0 -37
  127. data/lib/coradoc/element/inline/subscript.rb +0 -20
  128. data/lib/coradoc/element/inline/superscript.rb +0 -20
  129. data/lib/coradoc/element/inline/underline.rb +0 -19
  130. data/lib/coradoc/element/inline.rb +0 -23
  131. data/lib/coradoc/element/list/core.rb +0 -51
  132. data/lib/coradoc/element/list/definition.rb +0 -29
  133. data/lib/coradoc/element/list/ordered.rb +0 -17
  134. data/lib/coradoc/element/list/unordered.rb +0 -17
  135. data/lib/coradoc/element/list.rb +0 -13
  136. data/lib/coradoc/element/list_item.rb +0 -98
  137. data/lib/coradoc/element/list_item_definition.rb +0 -32
  138. data/lib/coradoc/element/paragraph.rb +0 -37
  139. data/lib/coradoc/element/revision.rb +0 -27
  140. data/lib/coradoc/element/section.rb +0 -62
  141. data/lib/coradoc/element/table.rb +0 -91
  142. data/lib/coradoc/element/tag.rb +0 -19
  143. data/lib/coradoc/element/term.rb +0 -22
  144. data/lib/coradoc/element/text_element.rb +0 -92
  145. data/lib/coradoc/element/title.rb +0 -62
  146. data/lib/coradoc/element/video.rb +0 -50
  147. data/lib/coradoc/generator.rb +0 -19
  148. data/lib/coradoc/input/adoc.rb +0 -30
  149. data/lib/coradoc/input/docx.rb +0 -64
  150. data/lib/coradoc/input/html/LICENSE.txt +0 -25
  151. data/lib/coradoc/input/html/README.adoc +0 -308
  152. data/lib/coradoc/input/html/cleaner.rb +0 -142
  153. data/lib/coradoc/input/html/config.rb +0 -77
  154. data/lib/coradoc/input/html/converters/a.rb +0 -52
  155. data/lib/coradoc/input/html/converters/aside.rb +0 -16
  156. data/lib/coradoc/input/html/converters/audio.rb +0 -29
  157. data/lib/coradoc/input/html/converters/base.rb +0 -108
  158. data/lib/coradoc/input/html/converters/blockquote.rb +0 -22
  159. data/lib/coradoc/input/html/converters/br.rb +0 -15
  160. data/lib/coradoc/input/html/converters/bypass.rb +0 -81
  161. data/lib/coradoc/input/html/converters/code.rb +0 -23
  162. data/lib/coradoc/input/html/converters/div.rb +0 -19
  163. data/lib/coradoc/input/html/converters/dl.rb +0 -62
  164. data/lib/coradoc/input/html/converters/drop.rb +0 -26
  165. data/lib/coradoc/input/html/converters/em.rb +0 -21
  166. data/lib/coradoc/input/html/converters/figure.rb +0 -25
  167. data/lib/coradoc/input/html/converters/h.rb +0 -42
  168. data/lib/coradoc/input/html/converters/head.rb +0 -23
  169. data/lib/coradoc/input/html/converters/hr.rb +0 -15
  170. data/lib/coradoc/input/html/converters/ignore.rb +0 -20
  171. data/lib/coradoc/input/html/converters/img.rb +0 -110
  172. data/lib/coradoc/input/html/converters/li.rb +0 -17
  173. data/lib/coradoc/input/html/converters/mark.rb +0 -19
  174. data/lib/coradoc/input/html/converters/markup.rb +0 -31
  175. data/lib/coradoc/input/html/converters/math.rb +0 -38
  176. data/lib/coradoc/input/html/converters/ol.rb +0 -65
  177. data/lib/coradoc/input/html/converters/p.rb +0 -23
  178. data/lib/coradoc/input/html/converters/pass_through.rb +0 -17
  179. data/lib/coradoc/input/html/converters/pre.rb +0 -55
  180. data/lib/coradoc/input/html/converters/q.rb +0 -16
  181. data/lib/coradoc/input/html/converters/strong.rb +0 -20
  182. data/lib/coradoc/input/html/converters/sub.rb +0 -22
  183. data/lib/coradoc/input/html/converters/sup.rb +0 -22
  184. data/lib/coradoc/input/html/converters/table.rb +0 -319
  185. data/lib/coradoc/input/html/converters/td.rb +0 -81
  186. data/lib/coradoc/input/html/converters/text.rb +0 -32
  187. data/lib/coradoc/input/html/converters/th.rb +0 -18
  188. data/lib/coradoc/input/html/converters/tr.rb +0 -22
  189. data/lib/coradoc/input/html/converters/video.rb +0 -29
  190. data/lib/coradoc/input/html/converters.rb +0 -59
  191. data/lib/coradoc/input/html/errors.rb +0 -14
  192. data/lib/coradoc/input/html/html_converter.rb +0 -168
  193. data/lib/coradoc/input/html/plugin.rb +0 -131
  194. data/lib/coradoc/input/html/plugins/plateau.rb +0 -213
  195. data/lib/coradoc/input/html/postprocessor.rb +0 -220
  196. data/lib/coradoc/input/html.rb +0 -61
  197. data/lib/coradoc/legacy_parser.rb +0 -200
  198. data/lib/coradoc/oscal.rb +0 -99
  199. data/lib/coradoc/output/adoc.rb +0 -19
  200. data/lib/coradoc/output/coradoc_tree_debug.rb +0 -21
  201. data/lib/coradoc/parser/asciidoc/admonition.rb +0 -24
  202. data/lib/coradoc/parser/asciidoc/attribute_list.rb +0 -89
  203. data/lib/coradoc/parser/asciidoc/base.rb +0 -87
  204. data/lib/coradoc/parser/asciidoc/bibliography.rb +0 -29
  205. data/lib/coradoc/parser/asciidoc/block.rb +0 -94
  206. data/lib/coradoc/parser/asciidoc/citation.rb +0 -30
  207. data/lib/coradoc/parser/asciidoc/content.rb +0 -64
  208. data/lib/coradoc/parser/asciidoc/document_attributes.rb +0 -25
  209. data/lib/coradoc/parser/asciidoc/header.rb +0 -29
  210. data/lib/coradoc/parser/asciidoc/inline.rb +0 -195
  211. data/lib/coradoc/parser/asciidoc/list.rb +0 -115
  212. data/lib/coradoc/parser/asciidoc/paragraph.rb +0 -54
  213. data/lib/coradoc/parser/asciidoc/section.rb +0 -61
  214. data/lib/coradoc/parser/asciidoc/table.rb +0 -32
  215. data/lib/coradoc/parser/asciidoc/term.rb +0 -41
  216. data/lib/coradoc/parser/asciidoc/text.rb +0 -158
  217. data/lib/coradoc/parser/base.rb +0 -40
  218. data/lib/coradoc/parser.rb +0 -11
  219. data/lib/coradoc/reverse_adoc.rb +0 -18
  220. data/lib/coradoc/transformer.rb +0 -476
  221. data/lib/coradoc/util.rb +0 -12
  222. data/lib/reverse_adoc.rb +0 -20
  223. data/utils/inspect_asciidoc.rb +0 -29
  224. data/utils/parser_analyzer.rb +0 -66
  225. 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