coradoc-mirror 0.1.4 → 0.1.5

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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/lib/coradoc/mirror/handler_registry.rb +19 -30
  3. data/lib/coradoc/mirror/handlers/code_block.rb +17 -24
  4. data/lib/coradoc/mirror/handlers/inline.rb +14 -0
  5. data/lib/coradoc/mirror/node.rb +39 -0
  6. data/lib/coradoc/mirror/reverse_builder/admonition.rb +20 -0
  7. data/lib/coradoc/mirror/reverse_builder/base.rb +42 -0
  8. data/lib/coradoc/mirror/reverse_builder/biblio_entry.rb +22 -0
  9. data/lib/coradoc/mirror/reverse_builder/bibliography.rb +18 -0
  10. data/lib/coradoc/mirror/reverse_builder/blockquote.rb +20 -0
  11. data/lib/coradoc/mirror/reverse_builder/bullet_list.rb +18 -0
  12. data/lib/coradoc/mirror/reverse_builder/caption.rb +19 -0
  13. data/lib/coradoc/mirror/reverse_builder/code_block.rb +22 -0
  14. data/lib/coradoc/mirror/reverse_builder/definition_list.rb +35 -0
  15. data/lib/coradoc/mirror/reverse_builder/document.rb +22 -0
  16. data/lib/coradoc/mirror/reverse_builder/example.rb +20 -0
  17. data/lib/coradoc/mirror/reverse_builder/figure.rb +35 -0
  18. data/lib/coradoc/mirror/reverse_builder/footnote_entry.rb +18 -0
  19. data/lib/coradoc/mirror/reverse_builder/footnote_marker.rb +24 -0
  20. data/lib/coradoc/mirror/reverse_builder/footnotes.rb +20 -0
  21. data/lib/coradoc/mirror/reverse_builder/frontmatter.rb +21 -0
  22. data/lib/coradoc/mirror/reverse_builder/generic_block.rb +26 -0
  23. data/lib/coradoc/mirror/reverse_builder/header.rb +22 -0
  24. data/lib/coradoc/mirror/reverse_builder/horizontal_rule.rb +17 -0
  25. data/lib/coradoc/mirror/reverse_builder/image.rb +25 -0
  26. data/lib/coradoc/mirror/reverse_builder/include.rb +46 -0
  27. data/lib/coradoc/mirror/reverse_builder/inline_text.rb +19 -0
  28. data/lib/coradoc/mirror/reverse_builder/list_item.rb +37 -0
  29. data/lib/coradoc/mirror/reverse_builder/literal_block.rb +22 -0
  30. data/lib/coradoc/mirror/reverse_builder/open_block.rb +17 -0
  31. data/lib/coradoc/mirror/reverse_builder/ordered_list.rb +18 -0
  32. data/lib/coradoc/mirror/reverse_builder/paragraph.rb +17 -0
  33. data/lib/coradoc/mirror/reverse_builder/pass_block.rb +22 -0
  34. data/lib/coradoc/mirror/reverse_builder/preamble.rb +17 -0
  35. data/lib/coradoc/mirror/reverse_builder/raw_inline.rb +17 -0
  36. data/lib/coradoc/mirror/reverse_builder/section.rb +28 -0
  37. data/lib/coradoc/mirror/reverse_builder/sections.rb +19 -0
  38. data/lib/coradoc/mirror/reverse_builder/sidebar.rb +20 -0
  39. data/lib/coradoc/mirror/reverse_builder/soft_break.rb +17 -0
  40. data/lib/coradoc/mirror/reverse_builder/stem_block.rb +22 -0
  41. data/lib/coradoc/mirror/reverse_builder/table.rb +27 -0
  42. data/lib/coradoc/mirror/reverse_builder/table_body.rb +17 -0
  43. data/lib/coradoc/mirror/reverse_builder/table_cell.rb +24 -0
  44. data/lib/coradoc/mirror/reverse_builder/table_head.rb +17 -0
  45. data/lib/coradoc/mirror/reverse_builder/table_row.rb +18 -0
  46. data/lib/coradoc/mirror/reverse_builder/text.rb +24 -0
  47. data/lib/coradoc/mirror/reverse_builder/toc.rb +17 -0
  48. data/lib/coradoc/mirror/reverse_builder/toc_entry.rb +18 -0
  49. data/lib/coradoc/mirror/reverse_builder/verse.rb +20 -0
  50. data/lib/coradoc/mirror/reverse_builder.rb +59 -574
  51. data/lib/coradoc/mirror/version.rb +1 -1
  52. data/lib/coradoc/mirror.rb +2 -0
  53. data/lib/coradoc-mirror.rb +0 -2
  54. metadata +45 -2
  55. data/lib/coradoc/mirror/output.rb +0 -62
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 000b9fd36ba19db9a57b3ad656292b1629eb82a12e7004add2e97b7823060e19
4
- data.tar.gz: f5364e581508705701aac01de53a2d03371213a4224356d4343f197aa37edab0
3
+ metadata.gz: ec671d9e6bbd3654635451d5a9ae2284f7e778f313b56e4e8d87e237e853b15e
4
+ data.tar.gz: 51f63185d93bda861e6f545fdd4bceb0e07a9516db1d8d51011fda4e08236b35
5
5
  SHA512:
6
- metadata.gz: 8bcd0570c09daf184700080579a5cb17c54a2cccc16d8afb95b3f0598d644d5f5b14790c81024fafdb8306720170ae1e9ddeda893f111446e854b03effcb6826
7
- data.tar.gz: f40e562805ca5466db7847773f7386c8709471c8aa4acbfbc4971db7e8255ede4b32e54b42cec361fa517cd31b1fbed1f283e6c01c3efadcf29ea1b33e75112f
6
+ metadata.gz: 40d4b935ed85cdfb2a0d4845807df32367541498c4921879d5875194f8c1572402ad7bd809055cdb285b9ea2b7457c1847c8039c861c88db9c5094ea42d6edfa
7
+ data.tar.gz: 91b105b79f7eef2230869c1cc386d0ec105b08346a36e66f4e18e95b6931a66382ae786910c9d6bc72c04e287dd9f856a38cc569dadfe3a9f2304f710186e0f7
@@ -8,6 +8,10 @@ module Coradoc
8
8
  # Third-party gems can register additional handlers without modifying
9
9
  # core classes (OCP).
10
10
  #
11
+ # Lookup (including ancestor walking) is delegated to
12
+ # `Coradoc::Dispatch.hierarchical`. This class keeps the Entry shape and
13
+ # the handler-invocation semantics — those are mirror-specific.
14
+ #
11
15
  # @example Registering a handler
12
16
  # registry = Coradoc::Mirror.default_registry
13
17
  # registry.register(MyCustomBlock, MyHandler)
@@ -17,12 +21,11 @@ module Coradoc
17
21
  # registry.register(Coradoc::CoreModel::ParagraphBlock, MyParagraphHandler)
18
22
  #
19
23
  class HandlerRegistry
20
- # Structured entry for a registered handler.
21
24
  Entry = Struct.new(:handler, :method_name, :concat, :extra_kwargs,
22
25
  keyword_init: true)
23
26
 
24
27
  def initialize
25
- @handlers = {}
28
+ @dispatch = Coradoc::Dispatch.hierarchical
26
29
  end
27
30
 
28
31
  # Register a handler for a CoreModel class.
@@ -38,46 +41,32 @@ module Coradoc
38
41
  # to the handler
39
42
  def register(model_class, handler, method_name: :call, concat: false,
40
43
  extra_kwargs: {})
41
- @handlers[model_class] = Entry.new(
42
- handler: handler,
43
- method_name: method_name,
44
- concat: concat,
45
- extra_kwargs: extra_kwargs
44
+ @dispatch.register(
45
+ model_class,
46
+ Entry.new(
47
+ handler: handler,
48
+ method_name: method_name,
49
+ concat: concat,
50
+ extra_kwargs: extra_kwargs
51
+ )
46
52
  )
47
53
  end
48
54
 
49
55
  # Find the handler entry for a given CoreModel element.
50
56
  #
51
- # Walks the element's class ancestors to find the most specific
52
- # registered handler. This allows registering a handler for a
53
- # base class (e.g., Block) that applies to all subclasses,
54
- # while also registering specific handlers for subclasses.
57
+ # Walks the element's class ancestors (via Dispatch.hierarchical) to
58
+ # find the most specific registered handler. This allows registering
59
+ # a handler for a base class (e.g., Block) that applies to all
60
+ # subclasses, while also registering specific handlers for subclasses.
55
61
  #
56
62
  # @param element [CoreModel::Base] element to find handler for
57
63
  # @return [Entry, nil]
58
- TERMINAL_ANCESTORS = [Object, BasicObject].freeze
59
-
60
64
  def entry_for(element)
61
- entry = @handlers[element.class]
62
- return entry if entry
63
-
64
- element.class.ancestors.each do |ancestor|
65
- next if ancestor == element.class
66
- break if TERMINAL_ANCESTORS.include?(ancestor)
67
-
68
- entry = @handlers[ancestor]
69
- return entry if entry
70
- end
71
-
72
- nil
65
+ @dispatch.lookup(element.class)
73
66
  end
74
67
 
75
- # Check if a handler is registered for a CoreModel class.
76
- #
77
- # @param model_class [Class]
78
- # @return [Boolean]
79
68
  def registered?(model_class)
80
- @handlers.key?(model_class)
69
+ @dispatch.registered?(model_class)
81
70
  end
82
71
 
83
72
  # Invoke the handler for a given element.
@@ -5,49 +5,42 @@ module Coradoc
5
5
  module Handlers
6
6
  module CodeBlock
7
7
  def self.source(element, context:)
8
- build_code_block(element, context)
8
+ build_code_block(element, context, node_class: Node::CodeBlock)
9
9
  end
10
10
 
11
11
  def self.listing(element, context:)
12
- build_code_block(element, context)
12
+ build_code_block(element, context, node_class: Node::CodeBlock)
13
13
  end
14
14
 
15
15
  def self.literal(element, context:)
16
- build_code_block(element, context)
16
+ build_code_block(element, context, node_class: Node::LiteralBlock)
17
17
  end
18
18
 
19
19
  def self.pass(element, context:)
20
- build_code_block(element, context, passthrough: true)
20
+ build_code_block(element, context, node_class: Node::PassBlock, passthrough: true)
21
+ end
22
+
23
+ def self.stem(element, context:)
24
+ build_code_block(element, context, node_class: Node::StemBlock)
21
25
  end
22
26
 
23
27
  class << self
24
28
  private
25
29
 
26
- def build_code_block(element, context, passthrough: false)
30
+ def build_code_block(element, context, node_class:, passthrough: false)
27
31
  text = extract_text(element)
28
32
  js_mode = context.partition_structural
33
+ attrs = Node::CodeBlock::Attrs.new(
34
+ title: element.title,
35
+ language: element.language,
36
+ passthrough: passthrough || nil,
37
+ text: js_mode ? text : nil
38
+ )
29
39
 
30
40
  if js_mode
31
- # @metanorma/mirror JS sourcecode contract: text in attrs.text,
32
- # no children. Pre-formatted text rendered via <pre><code>.
33
- Node::CodeBlock.new(
34
- attrs: Node::CodeBlock::Attrs.new(
35
- title: element.title,
36
- language: element.language,
37
- passthrough: passthrough || nil,
38
- text: text
39
- ),
40
- content: []
41
- )
41
+ node_class.new(attrs: attrs, content: [])
42
42
  else
43
- Node::CodeBlock.new(
44
- attrs: Node::CodeBlock::Attrs.new(
45
- title: element.title,
46
- language: element.language,
47
- passthrough: passthrough || nil
48
- ),
49
- content: [context.text_node(text)]
50
- )
43
+ node_class.new(attrs: attrs, content: [context.text_node(text)])
51
44
  end
52
45
  end
53
46
 
@@ -106,6 +106,8 @@ module Coradoc
106
106
  Node::SoftBreak.new
107
107
  when CoreModel::TextElement
108
108
  build_text_only(element, context)
109
+ when CoreModel::RawInlineElement
110
+ build_raw_inline(element, context)
109
111
  when CoreModel::InlineElement
110
112
  handle_generic_inline(element, context)
111
113
  end
@@ -189,6 +191,18 @@ module Coradoc
189
191
  context.text_node(text)
190
192
  end
191
193
 
194
+ # Passthrough content carries its own raw output-format markup
195
+ # (HTML, XML, etc.). Emit a typed RawInline node so renderers
196
+ # can skip escaping unambiguously — never a plain text node,
197
+ # which would force every downstream consumer to choose between
198
+ # XSS risk and visible literal markup.
199
+ def build_raw_inline(element, _context)
200
+ text = element.content.to_s
201
+ return nil if text.empty?
202
+
203
+ Node::RawInline.new(text: text)
204
+ end
205
+
192
206
  def extract_inline_text(element)
193
207
  return element.content.to_s if element.content && !element.content.to_s.empty?
194
208
 
@@ -162,6 +162,25 @@ module Coradoc
162
162
  end
163
163
  end
164
164
 
165
+ # Raw inline passthrough — content the source format marked as "do
166
+ # not process, emit verbatim." Distinct from `text` so renderers can
167
+ # skip escaping without sniffing content for `<...>`. AsciiDoc's
168
+ # `+++raw+++` is the canonical producer.
169
+ class RawInline < Node
170
+ PM_TYPE = 'raw_inline'
171
+
172
+ attribute :text, :string
173
+
174
+ key_value do
175
+ map 'type', to: :type, render_default: true
176
+ map 'text', to: :text
177
+ end
178
+
179
+ def text_content
180
+ text.to_s
181
+ end
182
+ end
183
+
165
184
  # ── Paragraph / block text ──
166
185
 
167
186
  class Paragraph < Node
@@ -200,6 +219,26 @@ module Coradoc
200
219
  end
201
220
  end
202
221
 
222
+ # Literal block — preformatted text (`....` delimiter). Same shape
223
+ # as CodeBlock but distinguished on the wire so consumers can apply
224
+ # literal-vs-source rendering/styling.
225
+ class LiteralBlock < CodeBlock
226
+ PM_TYPE = 'literal'
227
+ end
228
+
229
+ # Pass block — raw passthrough content (`++++` delimiter). Same
230
+ # shape as CodeBlock; flagged via attrs.passthrough = true.
231
+ class PassBlock < CodeBlock
232
+ PM_TYPE = 'pass'
233
+ end
234
+
235
+ # STEM block — mathematical/scientific markup (`[stem|latexmath|
236
+ # asciimath]\n++++`). Carries a +language+ attr ('latex' default,
237
+ # 'asciimath' alternative).
238
+ class StemBlock < CodeBlock
239
+ PM_TYPE = 'stem'
240
+ end
241
+
203
242
  class Blockquote < Node
204
243
  PM_TYPE = 'quote'
205
244
 
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Coradoc
6
+ module Mirror
7
+ module ReverseBuilder
8
+ class Admonition < Base
9
+ registers 'admonition'
10
+
11
+ def build(node)
12
+ CoreModel::AnnotationBlock.new(
13
+ annotation_type: node.attrs&.admonition_type,
14
+ content: extract_text(node)
15
+ )
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Mirror
5
+ module ReverseBuilder
6
+ # Base class for all reverse builders. Subclasses register one or
7
+ # more Mirror type strings via `registers` and implement `#build`.
8
+ # Shared helpers (build_content, extract_text, apply_mark, ...) are
9
+ # delegated to the context (a MirrorToCoreModel instance), keeping
10
+ # each builder focused on the per-type mapping only (DRY).
11
+ class Base
12
+ attr_reader :context
13
+
14
+ def initialize(context)
15
+ @context = context
16
+ end
17
+
18
+ def build(_node)
19
+ raise NotImplementedError,
20
+ "#{self.class} must implement #build(node)"
21
+ end
22
+
23
+ # Shared helpers — all delegate to the context (DRY).
24
+ def build_content(node) = context.build_content(node)
25
+ def build_inline_children(node) = context.build_inline_children(node)
26
+ def build_node(node) = context.build_node(node)
27
+ def extract_text(node) = context.extract_text(node)
28
+ def apply_mark(inner, mark) = context.apply_mark(inner, mark)
29
+ def inline_content(element) = context.inline_content(element)
30
+
31
+ class << self
32
+ # DSL: declare which Mirror type strings this builder handles.
33
+ # Multiple strings per builder are allowed (e.g. all JS
34
+ # SECTION_TYPES route to the same SectionElement builder).
35
+ def registers(*types)
36
+ types.each { |t| ReverseBuilder.register(t, self) }
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Coradoc
6
+ module Mirror
7
+ module ReverseBuilder
8
+ class BiblioEntry < Base
9
+ registers 'biblio_entry'
10
+
11
+ def build(node)
12
+ attrs = node.attrs
13
+ CoreModel::BibliographyEntry.new(
14
+ anchor_name: attrs&.anchor_name,
15
+ document_id: attrs&.document_id,
16
+ ref_text: extract_text(node)
17
+ )
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Coradoc
6
+ module Mirror
7
+ module ReverseBuilder
8
+ class Bibliography < Base
9
+ registers 'bibliography'
10
+
11
+ def build(node)
12
+ entries = build_content(node).select { |c| c.is_a?(CoreModel::BibliographyEntry) }
13
+ CoreModel::Bibliography.new(title: node.attrs&.title, entries: entries)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Coradoc
6
+ module Mirror
7
+ module ReverseBuilder
8
+ class Blockquote < Base
9
+ registers 'quote'
10
+
11
+ def build(node)
12
+ CoreModel::QuoteBlock.new(
13
+ attribution: node.attrs&.attribution,
14
+ children: build_content(node)
15
+ )
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Coradoc
6
+ module Mirror
7
+ module ReverseBuilder
8
+ class BulletList < Base
9
+ registers 'bullet_list'
10
+
11
+ def build(node)
12
+ items = build_content(node).select { |c| c.is_a?(CoreModel::ListItem) }
13
+ CoreModel::ListBlock.new(marker_type: 'unordered', items: items)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Coradoc
6
+ module Mirror
7
+ module ReverseBuilder
8
+ # Caption only appears as a Figure child. If encountered standalone,
9
+ # extract its text as an inline element so it isn't lost.
10
+ class Caption < Base
11
+ registers 'caption'
12
+
13
+ def build(node)
14
+ CoreModel::InlineElement.new(content: extract_text(node))
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Coradoc
6
+ module Mirror
7
+ module ReverseBuilder
8
+ class CodeBlock < Base
9
+ registers 'sourcecode'
10
+
11
+ def build(node)
12
+ attrs = node.attrs
13
+ CoreModel::SourceBlock.new(
14
+ content: attrs&.text || extract_text(node),
15
+ language: attrs&.language,
16
+ title: attrs&.title
17
+ )
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Coradoc
6
+ module Mirror
7
+ module ReverseBuilder
8
+ class DefinitionList < Base
9
+ registers 'dl'
10
+
11
+ def build(node)
12
+ terms = []
13
+ descriptions = []
14
+ node.content&.each do |child|
15
+ next unless child.is_a?(Node)
16
+
17
+ case child.type
18
+ when 'dt' then terms << build_node(child)
19
+ when 'dd' then descriptions << build_node(child)
20
+ end
21
+ end
22
+
23
+ items = terms.zip(descriptions).map do |term, desc|
24
+ CoreModel::DefinitionItem.new(
25
+ term: inline_content(term),
26
+ definitions: [inline_content(desc)]
27
+ )
28
+ end
29
+
30
+ CoreModel::DefinitionList.new(items: items)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Coradoc
6
+ module Mirror
7
+ module ReverseBuilder
8
+ class Document < Base
9
+ registers 'doc'
10
+
11
+ def build(node)
12
+ attrs = node.attrs
13
+ CoreModel::DocumentElement.new(
14
+ title: attrs&.title,
15
+ id: attrs&.id,
16
+ children: build_content(node)
17
+ )
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Coradoc
6
+ module Mirror
7
+ module ReverseBuilder
8
+ class Example < Base
9
+ registers 'example'
10
+
11
+ def build(node)
12
+ CoreModel::ExampleBlock.new(
13
+ title: node.attrs&.title,
14
+ children: build_content(node)
15
+ )
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Coradoc
6
+ module Mirror
7
+ module ReverseBuilder
8
+ # JS @metanorma/mirror `figure` wraps an image plus an optional
9
+ # caption. Reverse: collapse back to a single CoreModel::Image,
10
+ # promoting the caption child to `caption:` if present.
11
+ class Figure < Base
12
+ registers 'figure'
13
+
14
+ def build(node)
15
+ image_child = node.content&.find { |c| c.is_a?(Node) && c.type == 'image' }
16
+ return nil unless image_child
17
+
18
+ image = build_node(image_child)
19
+ caption = extract_caption(node)
20
+ image.caption = caption if caption && !image.caption
21
+ image
22
+ end
23
+
24
+ private
25
+
26
+ def extract_caption(node)
27
+ caption_node = node.content&.find { |c| c.is_a?(Node) && c.type == 'caption' }
28
+ return nil unless caption_node
29
+
30
+ extract_text(caption_node)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Coradoc
6
+ module Mirror
7
+ module ReverseBuilder
8
+ class FootnoteEntry < Base
9
+ registers 'footnote_entry'
10
+
11
+ def build(node)
12
+ attrs = node.attrs
13
+ CoreModel::Footnote.new(id: attrs&.id, content: extract_text(node))
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Coradoc
6
+ module Mirror
7
+ module ReverseBuilder
8
+ # Inline footnote marker (JS `footnote_marker`). The CoreModel
9
+ # FootnoteReference holds the same id/ref/number triple.
10
+ class FootnoteMarker < Base
11
+ registers 'footnote_marker'
12
+
13
+ def build(node)
14
+ attrs = node.attrs
15
+ CoreModel::FootnoteReference.new(
16
+ id: attrs&.id,
17
+ reference: attrs&.ref_id,
18
+ number: attrs&.number
19
+ )
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Coradoc
6
+ module Mirror
7
+ module ReverseBuilder
8
+ # The `footnotes` block is a structural trailing container; it has
9
+ # no CoreModel equivalent (each entry is built separately). Returns
10
+ # nil so build_content filters it out.
11
+ class Footnotes < Base
12
+ registers 'footnotes'
13
+
14
+ def build(_node)
15
+ nil
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Coradoc
6
+ module Mirror
7
+ module ReverseBuilder
8
+ class Frontmatter < Base
9
+ registers 'frontmatter'
10
+
11
+ def build(node)
12
+ attrs = node.attrs
13
+ CoreModel::FrontmatterBlock.new(
14
+ schema: attrs&.schema,
15
+ data: FrontmatterTreeToHash.to_hash(attrs&.entries || [])
16
+ )
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Coradoc
6
+ module Mirror
7
+ module ReverseBuilder
8
+ # Catch-all for unrecognized block types emitted by the forward
9
+ # direction (`Node::GenericBlock`). Preserves the semantic_type so
10
+ # downstream consumers can dispatch on it.
11
+ class GenericBlock < Base
12
+ registers 'generic_block'
13
+
14
+ def build(node)
15
+ attrs = node.attrs
16
+ CoreModel::Block.new(
17
+ block_semantic_type: attrs&.semantic_type || 'generic',
18
+ title: attrs&.title,
19
+ id: attrs&.id,
20
+ content: extract_text(node)
21
+ )
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end