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.
@@ -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
@@ -138,8 +138,7 @@ module Coradoc
138
138
  sections << element
139
139
 
140
140
  else
141
- warn "Unknown element type: #{element.class}"
142
- warn "Element: #{element.inspect}"
141
+ next if element.is_a?(String)
143
142
  end
144
143
  end
145
144
 
@@ -7,6 +7,7 @@ module Coradoc
7
7
  # Define the DSL for defining mappings in Asciidoc format
8
8
  class AsciidocMapping < Lutaml::Model::Mapping
9
9
  attr_reader :mappings
10
+ attr_writer :mappings
10
11
 
11
12
  def initialize
12
13
  super
@@ -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
- send(rule_name, *args, **kwargs)
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
- send(dispatch_method)
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
- send(alias_name)
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
- # repeat(repeater,) means repeater or more characters
115
- current_delimiter = str(delimiter).repeat(repeater).capture(:delimit)
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[:delimit].to_s.strip)
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[:delimit].to_s.strip
121
+ delim_str = c.captures[capture_key].to_s.strip
124
122
  closing_pattern = str(delim_str) >> newline
125
123
 
126
- # Build content that doesn't match the closing delimiter
127
- content = block_image | list | text_line(false, unguarded: true,
128
- verbatim: verbatim) | empty_line.as(:line_break)
129
- if n_deep.positive?
130
- # For nested blocks, also prevent them from consuming the closing delimiter
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
- current_delimiter = str(delimiter).repeat(exact_chars, exact_chars).capture(:delimit)
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[:delimit].to_s.strip)
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[:delimit].to_s.strip
156
+ delim_str = c.captures[capture_key].to_s.strip
163
157
  closing_pattern = str(delim_str) >> newline
164
158
 
165
- content = block_image | list | text_line(false, unguarded: true) | empty_line.as(:line_break)
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
- match("[^\n:]").repeat(1) # >> empty_line.repeat(0)
111
- .as(:dlist_term) >> dlist_delimiter
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.element_type
70
- when 'document'
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 'section'
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.format_type
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