coradoc-mirror 0.1.1

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 (40) hide show
  1. checksums.yaml +7 -0
  2. data/lib/coradoc/mirror/core_model_to_mirror.rb +181 -0
  3. data/lib/coradoc/mirror/handler_registry.rb +105 -0
  4. data/lib/coradoc/mirror/handlers/admonition.rb +29 -0
  5. data/lib/coradoc/mirror/handlers/bibliography.rb +43 -0
  6. data/lib/coradoc/mirror/handlers/blockquote.rb +19 -0
  7. data/lib/coradoc/mirror/handlers/code_block.rb +69 -0
  8. data/lib/coradoc/mirror/handlers/comment.rb +14 -0
  9. data/lib/coradoc/mirror/handlers/definition_list.rb +69 -0
  10. data/lib/coradoc/mirror/handlers/example.rb +19 -0
  11. data/lib/coradoc/mirror/handlers/footnote.rb +18 -0
  12. data/lib/coradoc/mirror/handlers/frontmatter.rb +71 -0
  13. data/lib/coradoc/mirror/handlers/generic_block.rb +24 -0
  14. data/lib/coradoc/mirror/handlers/horizontal_rule.rb +14 -0
  15. data/lib/coradoc/mirror/handlers/image.rb +58 -0
  16. data/lib/coradoc/mirror/handlers/inline.rb +213 -0
  17. data/lib/coradoc/mirror/handlers/list.rb +80 -0
  18. data/lib/coradoc/mirror/handlers/open_block.rb +16 -0
  19. data/lib/coradoc/mirror/handlers/paragraph.rb +16 -0
  20. data/lib/coradoc/mirror/handlers/reviewer.rb +14 -0
  21. data/lib/coradoc/mirror/handlers/sidebar.rb +19 -0
  22. data/lib/coradoc/mirror/handlers/structural.rb +84 -0
  23. data/lib/coradoc/mirror/handlers/table.rb +82 -0
  24. data/lib/coradoc/mirror/handlers/toc.rb +48 -0
  25. data/lib/coradoc/mirror/handlers/verse.rb +22 -0
  26. data/lib/coradoc/mirror/handlers.rb +38 -0
  27. data/lib/coradoc/mirror/mark.rb +181 -0
  28. data/lib/coradoc/mirror/mark_reverse_builder.rb +142 -0
  29. data/lib/coradoc/mirror/mirror_json_format.rb +42 -0
  30. data/lib/coradoc/mirror/mirror_to_core_model.rb +73 -0
  31. data/lib/coradoc/mirror/mirror_yaml_format.rb +41 -0
  32. data/lib/coradoc/mirror/node.rb +856 -0
  33. data/lib/coradoc/mirror/output.rb +62 -0
  34. data/lib/coradoc/mirror/partitioner.rb +62 -0
  35. data/lib/coradoc/mirror/reverse_builder.rb +600 -0
  36. data/lib/coradoc/mirror/transformer.rb +41 -0
  37. data/lib/coradoc/mirror/version.rb +7 -0
  38. data/lib/coradoc/mirror.rb +161 -0
  39. data/lib/coradoc-mirror.rb +14 -0
  40. metadata +140 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6ac58d41a9214d72f32b3bcc107ddaf463c772598cd9be21e267c56b4dcfee8e
4
+ data.tar.gz: bfd5bce725a65f86cf449cd4e59e9c78539968742237b002d2726dfdc3b37f52
5
+ SHA512:
6
+ metadata.gz: 264dba99b8eecd0e218f52d30dd6498a21b5248c640e837dc5ee4140df6b6f64e7416f75b6c8905c032d4c5fa9a078fbfdfc026c1b3160f0f943f930e5ff6e61
7
+ data.tar.gz: 51639279c428a6e1d1707bb922bd6da2d59576e96d0cfc7d266eef3de96281e3b2dca61fa63afee3fd3bf920799c7258fc1e40ab59782ee6ec06a5dbe2626340
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Mirror
5
+ # Transforms CoreModel documents into ProseMirror-compatible Mirror nodes.
6
+ #
7
+ # Uses a HandlerRegistry for OCP-compliant dispatch: each CoreModel type
8
+ # maps to a handler module/class that produces the corresponding Mirror node.
9
+ class CoreModelToMirror
10
+ attr_reader :registry
11
+
12
+ # Typed struct for pending footnote data (model-driven, not hash bags).
13
+ FootnoteData = Struct.new(:id, :ref_id, :number, :content, keyword_init: true)
14
+
15
+ # Maps element types to their children accessors.
16
+ COLLECTION_ACCESSORS = {
17
+ CoreModel::ListBlock => :items,
18
+ CoreModel::DefinitionList => :items,
19
+ CoreModel::Table => :rows,
20
+ CoreModel::Bibliography => :entries
21
+ }.freeze
22
+
23
+ def initialize(registry: Coradoc::Mirror.default_registry)
24
+ @registry = registry
25
+ @footnote_counter = 0
26
+ @footnotes = []
27
+ @partition_structural = false
28
+ end
29
+
30
+ # Read by Handlers::Structural.section to decide whether to emit
31
+ # generic `section` (legacy) or a JS SECTION_TYPE (`clause`, `annex`,
32
+ # etc.) when partition_structural mode is on.
33
+ attr_accessor :partition_structural
34
+
35
+ def call(document, partition_structural: false)
36
+ @footnote_counter = 0
37
+ @footnotes = []
38
+ @partition_structural = partition_structural
39
+
40
+ content = extract_content(document)
41
+ fn_block = flush_footnotes
42
+ content << fn_block if fn_block
43
+
44
+ attrs = build_document_attrs(document)
45
+ Node::Document.new(
46
+ attrs: Node::Document::Attrs.new(title: attrs[:title], id: attrs[:id]),
47
+ content: partition_structural ? wrap_structural(content) : content
48
+ )
49
+ end
50
+
51
+ # Partitions flat doc children into [preface?, sections?, *bibliography,
52
+ # *trailing] per the @metanorma/mirror JS structural contract. See
53
+ # Partitioner for the bucketing rules.
54
+ def wrap_structural(children)
55
+ partitioned = Partitioner.partition(children)
56
+ wrapped = []
57
+ wrapped << Node::Preamble.new(content: partitioned[:preface]) if partitioned[:preface].any?
58
+ wrapped << Node::Sections.new(content: partitioned[:sections]) if partitioned[:sections].any?
59
+ wrapped.concat(partitioned[:bibliography])
60
+ wrapped.concat(partitioned[:trailing])
61
+ wrapped
62
+ end
63
+
64
+ def extract_content(element)
65
+ children = element_children(element)
66
+
67
+ if children && !children.empty?
68
+ content = []
69
+ children.each { |child| handle_element(child, content) }
70
+ content.compact
71
+ elsif element_has_text_content?(element)
72
+ process_inline_content(element)
73
+ else
74
+ []
75
+ end
76
+ end
77
+
78
+ def process_inline_content(element)
79
+ Handlers::Inline.process(element, context: self)
80
+ end
81
+
82
+ def text_node(text, marks: [])
83
+ Node::Text.new(text: text, marks: marks)
84
+ end
85
+
86
+ def register_footnote(footnote)
87
+ @footnote_counter += 1
88
+ num = @footnote_counter
89
+ fn_id = footnote.id || "fn-#{num}"
90
+ ref_id = "fn-ref-#{num}"
91
+
92
+ fn_content = footnote.content ? [text_node(footnote.content)] : []
93
+ @footnotes << FootnoteData.new(
94
+ id: fn_id, ref_id: ref_id, number: num, content: fn_content
95
+ )
96
+
97
+ Node::FootnoteMarker.new(
98
+ attrs: Node::FootnoteMarker::Attrs.new(
99
+ id: fn_id, ref_id: ref_id, number: num
100
+ )
101
+ )
102
+ end
103
+
104
+ def resolve_footnote_reference(ref)
105
+ target_id = ref.id
106
+ entry = @footnotes.find { |fn| fn.id == target_id } if target_id
107
+
108
+ if entry
109
+ Node::FootnoteMarker.new(
110
+ attrs: Node::FootnoteMarker::Attrs.new(
111
+ id: entry.id,
112
+ ref_id: "fn-ref-#{entry.number}-dup-#{@footnote_counter}",
113
+ number: entry.number
114
+ )
115
+ )
116
+ else
117
+ text_node("[#{target_id || 'footnote'}]")
118
+ end
119
+ end
120
+
121
+ def flush_footnotes
122
+ return nil if @footnotes.empty?
123
+
124
+ entries = @footnotes.map do |fn|
125
+ Node::FootnoteEntry.new(
126
+ attrs: Node::FootnoteEntry::Attrs.new(
127
+ id: fn.id, ref_id: fn.ref_id, number: fn.number
128
+ ),
129
+ content: fn.content
130
+ )
131
+ end
132
+
133
+ @footnotes = []
134
+ Node::Footnotes.new(content: entries)
135
+ end
136
+
137
+ private
138
+
139
+ def element_has_text_content?(element)
140
+ element.is_a?(CoreModel::Block) &&
141
+ element.content &&
142
+ !element.content.to_s.empty?
143
+ end
144
+
145
+ def element_children(element)
146
+ if element.is_a?(Coradoc::CoreModel::HasChildren)
147
+ children = element.children
148
+ return children if children && !children.empty?
149
+ end
150
+
151
+ accessor = COLLECTION_ACCESSORS[element.class]
152
+ element.public_send(accessor) if accessor
153
+ end
154
+
155
+ def handle_element(element, content)
156
+ result = @registry.handle(element, context: self)
157
+ return unless result
158
+
159
+ value, concat = result
160
+ return unless value
161
+
162
+ concat ? content.concat(Array(value)) : content << value
163
+ end
164
+
165
+ def build_document_attrs(document)
166
+ attrs = {}
167
+ attrs[:title] = document.title if document.title
168
+ attrs[:id] = document.id if document.id
169
+
170
+ if document.is_a?(CoreModel::DocumentElement) &&
171
+ document.attributes.is_a?(CoreModel::Metadata)
172
+ document.attributes.entries&.each do |entry|
173
+ attrs[entry.key.to_sym] = entry.value
174
+ end
175
+ end
176
+
177
+ attrs
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Mirror
5
+ # Open registry mapping CoreModel classes to handler modules.
6
+ #
7
+ # Replaces closed case/when dispatch with an extensible registry.
8
+ # Third-party gems can register additional handlers without modifying
9
+ # core classes (OCP).
10
+ #
11
+ # @example Registering a handler
12
+ # registry = Coradoc::Mirror.default_registry
13
+ # registry.register(MyCustomBlock, MyHandler)
14
+ #
15
+ # @example Creating a custom registry
16
+ # registry = Coradoc::Mirror::HandlerRegistry.new
17
+ # registry.register(Coradoc::CoreModel::ParagraphBlock, MyParagraphHandler)
18
+ #
19
+ class HandlerRegistry
20
+ # Structured entry for a registered handler.
21
+ Entry = Struct.new(:handler, :method_name, :concat, :extra_kwargs,
22
+ keyword_init: true)
23
+
24
+ def initialize
25
+ @handlers = {}
26
+ end
27
+
28
+ # Register a handler for a CoreModel class.
29
+ #
30
+ # @param model_class [Class] CoreModel class to handle
31
+ # @param handler [Module, Class, Proc] handler implementation.
32
+ # If a Module/Class, +method_name+ is called on it.
33
+ # If a Proc, called directly with (element, context:).
34
+ # @param method_name [Symbol] method to call on handler (default: :call)
35
+ # @param concat [Boolean] if true, handler result is an array to
36
+ # concat into content rather than a single item to append
37
+ # @param extra_kwargs [Hash] additional keyword arguments passed
38
+ # to the handler
39
+ def register(model_class, handler, method_name: :call, concat: false,
40
+ extra_kwargs: {})
41
+ @handlers[model_class] = Entry.new(
42
+ handler: handler,
43
+ method_name: method_name,
44
+ concat: concat,
45
+ extra_kwargs: extra_kwargs
46
+ )
47
+ end
48
+
49
+ # Find the handler entry for a given CoreModel element.
50
+ #
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.
55
+ #
56
+ # @param element [CoreModel::Base] element to find handler for
57
+ # @return [Entry, nil]
58
+ TERMINAL_ANCESTORS = [Object, BasicObject].freeze
59
+
60
+ 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
73
+ end
74
+
75
+ # Check if a handler is registered for a CoreModel class.
76
+ #
77
+ # @param model_class [Class]
78
+ # @return [Boolean]
79
+ def registered?(model_class)
80
+ @handlers.key?(model_class)
81
+ end
82
+
83
+ # Invoke the handler for a given element.
84
+ #
85
+ # @param element [CoreModel::Base] element to handle
86
+ # @param context [CoreModelToMirror] transformer context
87
+ # @return [Array(result, concat_flag), nil] handler result or nil
88
+ def handle(element, context:)
89
+ entry = entry_for(element)
90
+ return nil unless entry
91
+
92
+ kwargs = { context: context }.merge(entry.extra_kwargs || {})
93
+
94
+ result = case entry.handler
95
+ when Proc
96
+ entry.handler.call(element, context)
97
+ else
98
+ entry.handler.public_send(entry.method_name, element, **kwargs)
99
+ end
100
+
101
+ [result, entry.concat]
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Mirror
5
+ module Handlers
6
+ # Admonition (NOTE, TIP, WARNING, CAUTION, IMPORTANT) handler.
7
+ #
8
+ # Emits a dialect-agnostic `Node::Admonition`. The canonical Ruby
9
+ # attribute is `admonition_type`; the model renames it to the wire
10
+ # name `type` on #to_h unconditionally. No flag, no dialect branch.
11
+ module Admonition
12
+ def self.call(element, context:)
13
+ content = context.extract_content(element)
14
+ return nil if content.empty?
15
+
16
+ Node::Admonition.new(
17
+ attrs: Node::Admonition::Attrs.new(
18
+ admonition_type: element.annotation_type,
19
+ title: element.title,
20
+ label: element.annotation_label,
21
+ id: element.id
22
+ ),
23
+ content: content
24
+ )
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Mirror
5
+ module Handlers
6
+ module Bibliography
7
+ def self.call(element, context:)
8
+ entries = Array(element.entries).filter_map do |entry|
9
+ build_entry(entry, context)
10
+ end
11
+ return nil if entries.empty?
12
+
13
+ Node::Bibliography.new(
14
+ attrs: Node::Bibliography::Attrs.new(
15
+ id: element.id,
16
+ title: element.title,
17
+ level: element.level
18
+ ),
19
+ content: entries
20
+ )
21
+ end
22
+
23
+ class << self
24
+ private
25
+
26
+ def build_entry(entry, context)
27
+ text = entry.display_text
28
+ return nil if text.nil? || text.empty?
29
+
30
+ Node::BibliographyEntry.new(
31
+ attrs: Node::BibliographyEntry::Attrs.new(
32
+ anchor_name: entry.anchor_name,
33
+ document_id: entry.document_id,
34
+ url: entry.url
35
+ ),
36
+ content: [context.text_node(text)]
37
+ )
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Mirror
5
+ module Handlers
6
+ module Blockquote
7
+ def self.call(element, context:)
8
+ content = context.extract_content(element)
9
+ return nil if content.empty?
10
+
11
+ Node::Blockquote.new(
12
+ attrs: Node::Blockquote::Attrs.new(attribution: element.attribution),
13
+ content: content
14
+ )
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Mirror
5
+ module Handlers
6
+ module CodeBlock
7
+ def self.source(element, context:)
8
+ build_code_block(element, context)
9
+ end
10
+
11
+ def self.listing(element, context:)
12
+ build_code_block(element, context)
13
+ end
14
+
15
+ def self.literal(element, context:)
16
+ build_code_block(element, context)
17
+ end
18
+
19
+ def self.pass(element, context:)
20
+ build_code_block(element, context, passthrough: true)
21
+ end
22
+
23
+ class << self
24
+ private
25
+
26
+ def build_code_block(element, context, passthrough: false)
27
+ text = extract_text(element)
28
+ js_mode = context.partition_structural
29
+
30
+ 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
+ )
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
+ )
51
+ end
52
+ end
53
+
54
+ def extract_text(element)
55
+ return '' unless element.is_a?(CoreModel::Block)
56
+
57
+ if element.content && !element.content.to_s.empty?
58
+ element.flat_text || element.content.to_s
59
+ elsif element.lines && !element.lines.empty?
60
+ Array(element.lines).join("\n")
61
+ else
62
+ ''
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Mirror
5
+ module Handlers
6
+ # Handles CommentBlock → omitted (comments are not rendered).
7
+ module Comment
8
+ def self.call(_element, *)
9
+ nil
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Mirror
5
+ module Handlers
6
+ module DefinitionList
7
+ def self.call(element, context:)
8
+ entries = Array(element.items).flat_map do |item|
9
+ definition_entry(item, context)
10
+ end
11
+ return nil if entries.empty?
12
+
13
+ Node::DefinitionList.new(
14
+ attrs: Node::DefinitionList::Attrs.new(id: element.id),
15
+ content: entries
16
+ )
17
+ end
18
+
19
+ class << self
20
+ private
21
+
22
+ def definition_entry(item, context)
23
+ term_node = build_term(item, context)
24
+ desc_node = build_description(item, context)
25
+ return nil unless term_node || desc_node
26
+
27
+ [term_node, desc_node].compact
28
+ end
29
+
30
+ def build_term(item, context)
31
+ term_text = item.term
32
+ return nil unless term_text && !term_text.to_s.empty?
33
+
34
+ term_children = item.term_children if item.is_a?(CoreModel::DefinitionItem)
35
+ if term_children && !term_children.empty?
36
+ inline_nodes = term_children.flat_map do |child|
37
+ Handlers::Inline.process_child(child, context)
38
+ end
39
+ return Node::DefinitionTerm.new(content: inline_nodes) unless inline_nodes.empty?
40
+ end
41
+
42
+ Node::DefinitionTerm.new(content: [context.text_node(term_text.to_s)])
43
+ end
44
+
45
+ def build_description(item, context)
46
+ definitions = item.definitions
47
+ return nil unless definitions && !definitions.empty?
48
+
49
+ desc_nodes = definitions.map do |defn|
50
+ context.text_node(defn.to_s)
51
+ end
52
+
53
+ def_children = item.definition_children if item.is_a?(CoreModel::DefinitionItem)
54
+ if def_children && !def_children.empty?
55
+ def_children.each do |child|
56
+ nodes = Handlers::Inline.process_child(child, context)
57
+ desc_nodes.concat(Array(nodes)) if nodes && !nodes.empty?
58
+ end
59
+ end
60
+
61
+ return nil if desc_nodes.empty?
62
+
63
+ Node::DefinitionDescription.new(content: desc_nodes)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Mirror
5
+ module Handlers
6
+ module Example
7
+ def self.call(element, context:)
8
+ content = context.extract_content(element)
9
+ return nil if content.empty?
10
+
11
+ Node::Example.new(
12
+ attrs: Node::Example::Attrs.new(id: element.id, title: element.title),
13
+ content: content
14
+ )
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Mirror
5
+ module Handlers
6
+ # Handles Footnote and FootnoteReference.
7
+ module Footnote
8
+ def self.call(element, context:)
9
+ context.register_footnote(element)
10
+ end
11
+
12
+ def self.reference(element, context:)
13
+ context.resolve_footnote_reference(element)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Mirror
5
+ module Handlers
6
+ # Handles FrontmatterBlock → frontmatter node.
7
+ #
8
+ # Walks the CoreModel +data+ hash and builds a typed tree of
9
+ # FrontmatterEntry / FrontmatterValue nodes. Dates/times are ISO
10
+ # 8601-encoded so the wire shape is JSON-native.
11
+ module Frontmatter
12
+ def self.call(element, *)
13
+ entries = build_entries(element.data || {})
14
+
15
+ Node::Frontmatter.new(
16
+ attrs: Node::Frontmatter::Attrs.new(
17
+ schema: element.schema,
18
+ entries: entries
19
+ )
20
+ )
21
+ end
22
+
23
+ class << self
24
+ private
25
+
26
+ def build_entries(data)
27
+ data.map { |key, value| build_entry(key.to_s, value) }
28
+ end
29
+
30
+ def build_entry(key, value)
31
+ Node::FrontmatterEntry.new(
32
+ key: key,
33
+ value: build_value(value)
34
+ )
35
+ end
36
+
37
+ def build_value(value)
38
+ case value
39
+ when Hash
40
+ Node::FrontmatterValue.new(
41
+ value_type: 'map',
42
+ entries: build_entries(value)
43
+ )
44
+ when Array
45
+ Node::FrontmatterValue.new(
46
+ value_type: 'array',
47
+ items: value.map { |v| build_value(v) }
48
+ )
49
+ when Integer
50
+ Node::FrontmatterValue.new(value_type: 'integer', integer_value: value)
51
+ when Float
52
+ Node::FrontmatterValue.new(value_type: 'float', float_value: value)
53
+ when TrueClass, FalseClass
54
+ Node::FrontmatterValue.new(value_type: 'boolean', boolean_value: value)
55
+ when Date, DateTime
56
+ Node::FrontmatterValue.new(value_type: 'date', date_value: value)
57
+ when Time
58
+ Node::FrontmatterValue.new(value_type: 'datetime', datetime_value: value)
59
+ when Symbol
60
+ Node::FrontmatterValue.new(value_type: 'symbol', symbol_value: value.to_s)
61
+ when nil
62
+ Node::FrontmatterValue.new(value_type: 'nil')
63
+ else
64
+ Node::FrontmatterValue.new(value_type: 'string', string_value: value.to_s)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Mirror
5
+ module Handlers
6
+ module GenericBlock
7
+ def self.call(element, context:)
8
+ semantic_type = element.resolve_semantic_type
9
+ content = context.extract_content(element)
10
+ return nil if content.empty?
11
+
12
+ Node::GenericBlock.new(
13
+ attrs: Node::GenericBlock::Attrs.new(
14
+ id: element.id,
15
+ title: element.title,
16
+ semantic_type: semantic_type&.to_s
17
+ ),
18
+ content: content
19
+ )
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Mirror
5
+ module Handlers
6
+ # Handles HorizontalRuleBlock → horizontal_rule node.
7
+ module HorizontalRule
8
+ def self.call(_element, *)
9
+ Node::HorizontalRule.new
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end