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.
- checksums.yaml +7 -0
- data/lib/coradoc/mirror/core_model_to_mirror.rb +181 -0
- data/lib/coradoc/mirror/handler_registry.rb +105 -0
- data/lib/coradoc/mirror/handlers/admonition.rb +29 -0
- data/lib/coradoc/mirror/handlers/bibliography.rb +43 -0
- data/lib/coradoc/mirror/handlers/blockquote.rb +19 -0
- data/lib/coradoc/mirror/handlers/code_block.rb +69 -0
- data/lib/coradoc/mirror/handlers/comment.rb +14 -0
- data/lib/coradoc/mirror/handlers/definition_list.rb +69 -0
- data/lib/coradoc/mirror/handlers/example.rb +19 -0
- data/lib/coradoc/mirror/handlers/footnote.rb +18 -0
- data/lib/coradoc/mirror/handlers/frontmatter.rb +71 -0
- data/lib/coradoc/mirror/handlers/generic_block.rb +24 -0
- data/lib/coradoc/mirror/handlers/horizontal_rule.rb +14 -0
- data/lib/coradoc/mirror/handlers/image.rb +58 -0
- data/lib/coradoc/mirror/handlers/inline.rb +213 -0
- data/lib/coradoc/mirror/handlers/list.rb +80 -0
- data/lib/coradoc/mirror/handlers/open_block.rb +16 -0
- data/lib/coradoc/mirror/handlers/paragraph.rb +16 -0
- data/lib/coradoc/mirror/handlers/reviewer.rb +14 -0
- data/lib/coradoc/mirror/handlers/sidebar.rb +19 -0
- data/lib/coradoc/mirror/handlers/structural.rb +84 -0
- data/lib/coradoc/mirror/handlers/table.rb +82 -0
- data/lib/coradoc/mirror/handlers/toc.rb +48 -0
- data/lib/coradoc/mirror/handlers/verse.rb +22 -0
- data/lib/coradoc/mirror/handlers.rb +38 -0
- data/lib/coradoc/mirror/mark.rb +181 -0
- data/lib/coradoc/mirror/mark_reverse_builder.rb +142 -0
- data/lib/coradoc/mirror/mirror_json_format.rb +42 -0
- data/lib/coradoc/mirror/mirror_to_core_model.rb +73 -0
- data/lib/coradoc/mirror/mirror_yaml_format.rb +41 -0
- data/lib/coradoc/mirror/node.rb +856 -0
- data/lib/coradoc/mirror/output.rb +62 -0
- data/lib/coradoc/mirror/partitioner.rb +62 -0
- data/lib/coradoc/mirror/reverse_builder.rb +600 -0
- data/lib/coradoc/mirror/transformer.rb +41 -0
- data/lib/coradoc/mirror/version.rb +7 -0
- data/lib/coradoc/mirror.rb +161 -0
- data/lib/coradoc-mirror.rb +14 -0
- 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,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
|