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
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Mirror
|
|
5
|
+
# OCP-compliant registry for Mirror node -> CoreModel transformation.
|
|
6
|
+
#
|
|
7
|
+
# Adding support for a new Mirror node type is purely additive:
|
|
8
|
+
#
|
|
9
|
+
# module ReverseBuilder
|
|
10
|
+
# class Figure < Base
|
|
11
|
+
# registers 'figure'
|
|
12
|
+
#
|
|
13
|
+
# def build(node)
|
|
14
|
+
# CoreModel::Image.new(src: node.src, ...)
|
|
15
|
+
# end
|
|
16
|
+
# end
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# No edits to MirrorToCoreModel or any other existing class. The
|
|
20
|
+
# registry is the single source of truth for "which type string maps
|
|
21
|
+
# to which builder" (MECE).
|
|
22
|
+
#
|
|
23
|
+
# This file is the autoload target for the ReverseBuilder constant
|
|
24
|
+
# (see coradoc/mirror.rb). All built-in Builder subclasses live here
|
|
25
|
+
# so their `registers` calls run at load time and the REGISTRY is
|
|
26
|
+
# full before any caller references it. Mirror-level mark dispatch
|
|
27
|
+
# lives in MarkReverseBuilder (mark_reverse_builder.rb).
|
|
28
|
+
module ReverseBuilder
|
|
29
|
+
REGISTRY = {}
|
|
30
|
+
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
def register(type, builder_class)
|
|
34
|
+
REGISTRY[type] = builder_class
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def lookup(type)
|
|
38
|
+
REGISTRY[type]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def registered_types
|
|
42
|
+
REGISTRY.keys
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Base class for all reverse builders. Subclasses register one or
|
|
46
|
+
# more Mirror type strings via `registers` and implement `#build`.
|
|
47
|
+
# Shared helpers (build_content, extract_text, apply_mark, ...) are
|
|
48
|
+
# delegated to the context (a MirrorToCoreModel instance), keeping
|
|
49
|
+
# each builder focused on the per-type mapping only (DRY).
|
|
50
|
+
class Base
|
|
51
|
+
attr_reader :context
|
|
52
|
+
|
|
53
|
+
def initialize(context)
|
|
54
|
+
@context = context
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def build(_node)
|
|
58
|
+
raise NotImplementedError,
|
|
59
|
+
"#{self.class} must implement #build(node)"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Shared helpers — all delegate to the context (DRY).
|
|
63
|
+
def build_content(node) = context.build_content(node)
|
|
64
|
+
def build_inline_children(node) = context.build_inline_children(node)
|
|
65
|
+
def build_node(node) = context.build_node(node)
|
|
66
|
+
def extract_text(node) = context.extract_text(node)
|
|
67
|
+
def apply_mark(inner, mark) = context.apply_mark(inner, mark)
|
|
68
|
+
def inline_content(element) = context.inline_content(element)
|
|
69
|
+
|
|
70
|
+
class << self
|
|
71
|
+
# DSL: declare which Mirror type strings this builder handles.
|
|
72
|
+
# Multiple strings per builder are allowed (e.g. all JS
|
|
73
|
+
# SECTION_TYPES route to the same SectionElement builder).
|
|
74
|
+
def registers(*types)
|
|
75
|
+
types.each { |t| ReverseBuilder.register(t, self) }
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# ── Structural ──
|
|
81
|
+
|
|
82
|
+
class Document < Base
|
|
83
|
+
registers 'doc'
|
|
84
|
+
|
|
85
|
+
def build(node)
|
|
86
|
+
attrs = node.attrs
|
|
87
|
+
CoreModel::DocumentElement.new(
|
|
88
|
+
title: attrs&.title,
|
|
89
|
+
id: attrs&.id,
|
|
90
|
+
children: build_content(node)
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# All JS SECTION_TYPES reverse to a generic SectionElement. Style
|
|
96
|
+
# information lives in the original AsciiDoc and is preserved only
|
|
97
|
+
# on the forward side; the reverse side collapses them.
|
|
98
|
+
class Section < Base
|
|
99
|
+
registers 'section', 'clause', 'annex', 'content_section',
|
|
100
|
+
'abstract', 'foreword', 'introduction',
|
|
101
|
+
'acknowledgements', 'terms', 'definitions', 'references'
|
|
102
|
+
|
|
103
|
+
def build(node)
|
|
104
|
+
attrs = node.attrs
|
|
105
|
+
CoreModel::SectionElement.new(
|
|
106
|
+
title: attrs&.title,
|
|
107
|
+
level: attrs&.level,
|
|
108
|
+
id: attrs&.id,
|
|
109
|
+
children: build_content(node)
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
class Header < Base
|
|
115
|
+
registers 'floating_title', 'heading'
|
|
116
|
+
|
|
117
|
+
def build(node)
|
|
118
|
+
attrs = node.attrs
|
|
119
|
+
CoreModel::HeaderElement.new(
|
|
120
|
+
title: attrs&.title,
|
|
121
|
+
level: attrs&.level,
|
|
122
|
+
children: build_content(node)
|
|
123
|
+
)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
class Preamble < Base
|
|
128
|
+
registers 'preface'
|
|
129
|
+
|
|
130
|
+
def build(node)
|
|
131
|
+
CoreModel::PreambleElement.new(children: build_content(node))
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# `sections` is a structural container only — unwrap directly into
|
|
136
|
+
# an array. MirrorToCoreModel#build_content flattens arrays.
|
|
137
|
+
class Sections < Base
|
|
138
|
+
registers 'sections'
|
|
139
|
+
|
|
140
|
+
def build(node)
|
|
141
|
+
build_content(node)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# ── Blocks ──
|
|
146
|
+
|
|
147
|
+
class Paragraph < Base
|
|
148
|
+
registers 'paragraph'
|
|
149
|
+
|
|
150
|
+
def build(node)
|
|
151
|
+
CoreModel::ParagraphBlock.new(children: build_inline_children(node))
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
class CodeBlock < Base
|
|
156
|
+
registers 'sourcecode'
|
|
157
|
+
|
|
158
|
+
def build(node)
|
|
159
|
+
attrs = node.attrs
|
|
160
|
+
CoreModel::SourceBlock.new(
|
|
161
|
+
content: attrs&.text || extract_text(node),
|
|
162
|
+
language: attrs&.language,
|
|
163
|
+
title: attrs&.title
|
|
164
|
+
)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
class Blockquote < Base
|
|
169
|
+
registers 'quote'
|
|
170
|
+
|
|
171
|
+
def build(node)
|
|
172
|
+
CoreModel::QuoteBlock.new(
|
|
173
|
+
attribution: node.attrs&.attribution,
|
|
174
|
+
children: build_content(node)
|
|
175
|
+
)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
class Example < Base
|
|
180
|
+
registers 'example'
|
|
181
|
+
|
|
182
|
+
def build(node)
|
|
183
|
+
CoreModel::ExampleBlock.new(
|
|
184
|
+
title: node.attrs&.title,
|
|
185
|
+
children: build_content(node)
|
|
186
|
+
)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
class Sidebar < Base
|
|
191
|
+
registers 'sidebar'
|
|
192
|
+
|
|
193
|
+
def build(node)
|
|
194
|
+
CoreModel::SidebarBlock.new(
|
|
195
|
+
title: node.attrs&.title,
|
|
196
|
+
children: build_content(node)
|
|
197
|
+
)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
class OpenBlock < Base
|
|
202
|
+
registers 'open_block'
|
|
203
|
+
|
|
204
|
+
def build(node)
|
|
205
|
+
CoreModel::OpenBlock.new(children: build_content(node))
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
class Verse < Base
|
|
210
|
+
registers 'verse'
|
|
211
|
+
|
|
212
|
+
def build(node)
|
|
213
|
+
CoreModel::VerseBlock.new(
|
|
214
|
+
content: extract_text(node),
|
|
215
|
+
attribution: node.attrs&.attribution
|
|
216
|
+
)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
class HorizontalRule < Base
|
|
221
|
+
registers 'horizontal_rule', 'thematic_break'
|
|
222
|
+
|
|
223
|
+
def build(_node)
|
|
224
|
+
CoreModel::HorizontalRuleBlock.new
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
class Frontmatter < Base
|
|
229
|
+
registers 'frontmatter'
|
|
230
|
+
|
|
231
|
+
def build(node)
|
|
232
|
+
attrs = node.attrs
|
|
233
|
+
CoreModel::FrontmatterBlock.new(
|
|
234
|
+
schema: attrs&.schema,
|
|
235
|
+
data: FrontmatterTreeToHash.to_hash(attrs&.entries || [])
|
|
236
|
+
)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
class Admonition < Base
|
|
241
|
+
registers 'admonition'
|
|
242
|
+
|
|
243
|
+
def build(node)
|
|
244
|
+
CoreModel::AnnotationBlock.new(
|
|
245
|
+
annotation_type: node.attrs&.admonition_type,
|
|
246
|
+
content: extract_text(node)
|
|
247
|
+
)
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# ── Lists ──
|
|
252
|
+
|
|
253
|
+
class BulletList < Base
|
|
254
|
+
registers 'bullet_list'
|
|
255
|
+
|
|
256
|
+
def build(node)
|
|
257
|
+
items = build_content(node).select { |c| c.is_a?(CoreModel::ListItem) }
|
|
258
|
+
CoreModel::ListBlock.new(marker_type: 'unordered', items: items)
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
class OrderedList < Base
|
|
263
|
+
registers 'ordered_list'
|
|
264
|
+
|
|
265
|
+
def build(node)
|
|
266
|
+
items = build_content(node).select { |c| c.is_a?(CoreModel::ListItem) }
|
|
267
|
+
CoreModel::ListBlock.new(marker_type: 'ordered', items: items)
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
class ListItem < Base
|
|
272
|
+
registers 'list_item'
|
|
273
|
+
|
|
274
|
+
def build(node)
|
|
275
|
+
children = build_inline_children(node)
|
|
276
|
+
text = children.map { |c| c.is_a?(CoreModel::TextContent) ? c.text : '' }.join
|
|
277
|
+
|
|
278
|
+
CoreModel::ListItem.new(
|
|
279
|
+
content: text,
|
|
280
|
+
children: children,
|
|
281
|
+
nested_list: find_nested_list(node)
|
|
282
|
+
)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
private
|
|
286
|
+
|
|
287
|
+
def find_nested_list(node)
|
|
288
|
+
node.content&.each do |child|
|
|
289
|
+
next unless child.is_a?(Node)
|
|
290
|
+
return build_node(child) if LIST_TYPES.include?(child.type)
|
|
291
|
+
end
|
|
292
|
+
nil
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
class DefinitionList < Base
|
|
297
|
+
registers 'dl'
|
|
298
|
+
|
|
299
|
+
def build(node)
|
|
300
|
+
terms = []
|
|
301
|
+
descriptions = []
|
|
302
|
+
node.content&.each do |child|
|
|
303
|
+
next unless child.is_a?(Node)
|
|
304
|
+
|
|
305
|
+
case child.type
|
|
306
|
+
when 'dt' then terms << build_node(child)
|
|
307
|
+
when 'dd' then descriptions << build_node(child)
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
items = terms.zip(descriptions).map do |term, desc|
|
|
312
|
+
CoreModel::DefinitionItem.new(
|
|
313
|
+
term: inline_content(term),
|
|
314
|
+
definitions: [inline_content(desc)]
|
|
315
|
+
)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
CoreModel::DefinitionList.new(items: items)
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
class InlineText < Base
|
|
323
|
+
registers 'dt', 'dd'
|
|
324
|
+
|
|
325
|
+
def build(node)
|
|
326
|
+
children = build_inline_children(node)
|
|
327
|
+
text = children.map { |c| c.is_a?(CoreModel::TextContent) ? c.text : '' }.join
|
|
328
|
+
CoreModel::InlineElement.new(content: text)
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# ── Media ──
|
|
333
|
+
|
|
334
|
+
class Image < Base
|
|
335
|
+
registers 'image'
|
|
336
|
+
|
|
337
|
+
def build(node)
|
|
338
|
+
attrs = node.attrs
|
|
339
|
+
CoreModel::Image.new(
|
|
340
|
+
src: attrs&.src,
|
|
341
|
+
alt: attrs&.alt,
|
|
342
|
+
title: attrs&.title,
|
|
343
|
+
caption: attrs&.caption,
|
|
344
|
+
width: attrs&.width,
|
|
345
|
+
height: attrs&.height
|
|
346
|
+
)
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# JS @metanorma/mirror `figure` wraps an image plus an optional
|
|
351
|
+
# caption. Reverse: collapse back to a single CoreModel::Image,
|
|
352
|
+
# promoting the caption child to `caption:` if present.
|
|
353
|
+
class Figure < Base
|
|
354
|
+
registers 'figure'
|
|
355
|
+
|
|
356
|
+
def build(node)
|
|
357
|
+
image_child = node.content&.find { |c| c.is_a?(Node) && c.type == 'image' }
|
|
358
|
+
return nil unless image_child
|
|
359
|
+
|
|
360
|
+
image = build_node(image_child)
|
|
361
|
+
caption = extract_caption(node)
|
|
362
|
+
image.caption = caption if caption && !image.caption
|
|
363
|
+
image
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
private
|
|
367
|
+
|
|
368
|
+
def extract_caption(node)
|
|
369
|
+
caption_node = node.content&.find { |c| c.is_a?(Node) && c.type == 'caption' }
|
|
370
|
+
return nil unless caption_node
|
|
371
|
+
|
|
372
|
+
extract_text(caption_node)
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Caption only appears as a Figure child. If encountered standalone,
|
|
377
|
+
# extract its text as an inline element so it isn't lost.
|
|
378
|
+
class Caption < Base
|
|
379
|
+
registers 'caption'
|
|
380
|
+
|
|
381
|
+
def build(node)
|
|
382
|
+
CoreModel::InlineElement.new(content: extract_text(node))
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# ── Tables ──
|
|
387
|
+
|
|
388
|
+
class Table < Base
|
|
389
|
+
registers 'table'
|
|
390
|
+
|
|
391
|
+
def build(node)
|
|
392
|
+
rows = []
|
|
393
|
+
node.content&.each do |child|
|
|
394
|
+
next unless child.is_a?(Node)
|
|
395
|
+
next unless %w[table_head table_body].include?(child.type)
|
|
396
|
+
|
|
397
|
+
child.content&.each do |row_node|
|
|
398
|
+
rows << build_node(row_node) if row_node.is_a?(Node)
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
CoreModel::Table.new(title: node.attrs&.title, rows: rows)
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
class TableHead < Base
|
|
407
|
+
registers 'table_head'
|
|
408
|
+
|
|
409
|
+
def build(node)
|
|
410
|
+
build_content(node).first || CoreModel::TableRow.new
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
class TableBody < Base
|
|
415
|
+
registers 'table_body'
|
|
416
|
+
|
|
417
|
+
def build(node)
|
|
418
|
+
build_content(node).first || CoreModel::TableRow.new
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
class TableRow < Base
|
|
423
|
+
registers 'table_row'
|
|
424
|
+
|
|
425
|
+
def build(node)
|
|
426
|
+
cells = build_content(node).select { |c| c.is_a?(CoreModel::TableCell) }
|
|
427
|
+
CoreModel::TableRow.new(cells: cells)
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
class TableCell < Base
|
|
432
|
+
registers 'table_cell'
|
|
433
|
+
|
|
434
|
+
def build(node)
|
|
435
|
+
attrs = node.attrs
|
|
436
|
+
CoreModel::TableCell.new(
|
|
437
|
+
content: extract_text(node),
|
|
438
|
+
header: attrs&.header || false,
|
|
439
|
+
colspan: attrs&.colspan,
|
|
440
|
+
rowspan: attrs&.rowspan,
|
|
441
|
+
alignment: attrs&.alignment
|
|
442
|
+
)
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# ── Bibliography ──
|
|
447
|
+
|
|
448
|
+
class Bibliography < Base
|
|
449
|
+
registers 'bibliography'
|
|
450
|
+
|
|
451
|
+
def build(node)
|
|
452
|
+
entries = build_content(node).select { |c| c.is_a?(CoreModel::BibliographyEntry) }
|
|
453
|
+
CoreModel::Bibliography.new(title: node.attrs&.title, entries: entries)
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
class BiblioEntry < Base
|
|
458
|
+
registers 'biblio_entry'
|
|
459
|
+
|
|
460
|
+
def build(node)
|
|
461
|
+
attrs = node.attrs
|
|
462
|
+
CoreModel::BibliographyEntry.new(
|
|
463
|
+
anchor_name: attrs&.anchor_name,
|
|
464
|
+
document_id: attrs&.document_id,
|
|
465
|
+
ref_text: extract_text(node)
|
|
466
|
+
)
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
# ── Footnotes ──
|
|
471
|
+
|
|
472
|
+
# The `footnotes` block is a structural trailing container; it has
|
|
473
|
+
# no CoreModel equivalent (each entry is built separately). Returns
|
|
474
|
+
# nil so build_content filters it out.
|
|
475
|
+
class Footnotes < Base
|
|
476
|
+
registers 'footnotes'
|
|
477
|
+
|
|
478
|
+
def build(_node)
|
|
479
|
+
nil
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
class FootnoteEntry < Base
|
|
484
|
+
registers 'footnote_entry'
|
|
485
|
+
|
|
486
|
+
def build(node)
|
|
487
|
+
attrs = node.attrs
|
|
488
|
+
CoreModel::Footnote.new(id: attrs&.id, content: extract_text(node))
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
# Inline footnote marker (JS `footnote_marker`). The CoreModel
|
|
493
|
+
# FootnoteReference holds the same id/ref/number triple.
|
|
494
|
+
class FootnoteMarker < Base
|
|
495
|
+
registers 'footnote_marker'
|
|
496
|
+
|
|
497
|
+
def build(node)
|
|
498
|
+
attrs = node.attrs
|
|
499
|
+
CoreModel::FootnoteReference.new(
|
|
500
|
+
id: attrs&.id,
|
|
501
|
+
reference: attrs&.ref_id,
|
|
502
|
+
number: attrs&.number
|
|
503
|
+
)
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
# ── TOC ──
|
|
508
|
+
|
|
509
|
+
class Toc < Base
|
|
510
|
+
registers 'toc'
|
|
511
|
+
|
|
512
|
+
def build(_node)
|
|
513
|
+
CoreModel::Toc.new
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
class TocEntry < Base
|
|
518
|
+
registers 'toc_entry'
|
|
519
|
+
|
|
520
|
+
def build(node)
|
|
521
|
+
attrs = node.attrs
|
|
522
|
+
CoreModel::TocEntry.new(id: attrs&.id, title: attrs&.title)
|
|
523
|
+
end
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
# ── Inline ──
|
|
527
|
+
|
|
528
|
+
class Text < Base
|
|
529
|
+
registers 'text'
|
|
530
|
+
|
|
531
|
+
def build(node)
|
|
532
|
+
text = node.text || ''
|
|
533
|
+
marks = node.marks || []
|
|
534
|
+
|
|
535
|
+
return CoreModel::TextContent.new(text: text) if marks.empty?
|
|
536
|
+
|
|
537
|
+
marks.reduce(CoreModel::TextContent.new(text: text)) do |current, mark|
|
|
538
|
+
apply_mark(current, mark)
|
|
539
|
+
end
|
|
540
|
+
end
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
class SoftBreak < Base
|
|
544
|
+
registers 'soft_break'
|
|
545
|
+
|
|
546
|
+
def build(_node)
|
|
547
|
+
CoreModel::LineBreakElement.new
|
|
548
|
+
end
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
# Catch-all for unrecognized block types emitted by the forward
|
|
552
|
+
# direction (`Node::GenericBlock`). Preserves the semantic_type so
|
|
553
|
+
# downstream consumers can dispatch on it.
|
|
554
|
+
class GenericBlock < Base
|
|
555
|
+
registers 'generic_block'
|
|
556
|
+
|
|
557
|
+
def build(node)
|
|
558
|
+
attrs = node.attrs
|
|
559
|
+
CoreModel::Block.new(
|
|
560
|
+
block_semantic_type: attrs&.semantic_type || 'generic',
|
|
561
|
+
title: attrs&.title,
|
|
562
|
+
id: attrs&.id,
|
|
563
|
+
content: extract_text(node)
|
|
564
|
+
)
|
|
565
|
+
end
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
LIST_TYPES = %w[bullet_list ordered_list].freeze
|
|
569
|
+
private_constant :LIST_TYPES
|
|
570
|
+
|
|
571
|
+
# Walks a typed FrontmatterEntry / FrontmatterValue tree and
|
|
572
|
+
# rebuilds the CoreModel `data` hash. Inverse of
|
|
573
|
+
# Handlers::Frontmatter.build_value.
|
|
574
|
+
module FrontmatterTreeToHash
|
|
575
|
+
module_function
|
|
576
|
+
|
|
577
|
+
def to_hash(entries)
|
|
578
|
+
entries.each_with_object({}) do |entry, result|
|
|
579
|
+
result[entry.key] = unwrap_value(entry.value)
|
|
580
|
+
end
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
def unwrap_value(value)
|
|
584
|
+
case value.value_type
|
|
585
|
+
when 'map' then to_hash(value.entries || [])
|
|
586
|
+
when 'array' then (value.items || []).map { |v| unwrap_value(v) }
|
|
587
|
+
when 'integer' then value.integer_value
|
|
588
|
+
when 'float' then value.float_value
|
|
589
|
+
when 'boolean' then value.boolean_value
|
|
590
|
+
when 'date' then value.date_value
|
|
591
|
+
when 'datetime' then value.datetime_value
|
|
592
|
+
when 'symbol' then value.symbol_value&.to_sym
|
|
593
|
+
when 'nil' then nil
|
|
594
|
+
else value.string_value
|
|
595
|
+
end
|
|
596
|
+
end
|
|
597
|
+
end
|
|
598
|
+
end
|
|
599
|
+
end
|
|
600
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Mirror
|
|
5
|
+
# Bidirectional facade for CoreModel ↔ Mirror transformation.
|
|
6
|
+
#
|
|
7
|
+
# Delegates to CoreModelToMirror (forward) and MirrorToCoreModel (reverse).
|
|
8
|
+
#
|
|
9
|
+
# @example Forward transformation
|
|
10
|
+
# transformer = Coradoc::Mirror::Transformer.new
|
|
11
|
+
# mirror_doc = transformer.from_core_model(document)
|
|
12
|
+
#
|
|
13
|
+
# @example Reverse transformation
|
|
14
|
+
# core_doc = transformer.to_core_model(mirror_doc)
|
|
15
|
+
#
|
|
16
|
+
class Transformer
|
|
17
|
+
def initialize(registry: Coradoc::Mirror.default_registry)
|
|
18
|
+
@registry = registry
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Convert CoreModel document to Mirror node tree.
|
|
22
|
+
#
|
|
23
|
+
# @param document [CoreModel::Base] CoreModel document
|
|
24
|
+
# @param partition_structural [Boolean] wrap doc.content in
|
|
25
|
+
# preface/sections/bibliography containers per the @metanorma/mirror
|
|
26
|
+
# JS structural contract (default: false).
|
|
27
|
+
# @return [Node::Document] mirror document root
|
|
28
|
+
def from_core_model(document, partition_structural: false)
|
|
29
|
+
CoreModelToMirror.new(registry: @registry).call(document, partition_structural: partition_structural)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Convert Mirror node tree to CoreModel document.
|
|
33
|
+
#
|
|
34
|
+
# @param mirror_node [Node] mirror document root
|
|
35
|
+
# @return [CoreModel::Base] CoreModel document
|
|
36
|
+
def to_core_model(mirror_node)
|
|
37
|
+
MirrorToCoreModel.new.call(mirror_node)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|