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
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Mirror
5
+ module Handlers
6
+ # Image handler.
7
+ #
8
+ # Two emission shapes:
9
+ # - Ruby legacy (default): bare `image` node, title/caption in attrs.
10
+ # - JS @metanorma/mirror (`partition_structural: true`): when the
11
+ # source image has a title, wrap it in a `figure` node with the
12
+ # image plus a `caption` child, matching the JS schema.
13
+ module Image
14
+ def self.call(element, context:)
15
+ image_node = build_image_node(element)
16
+
17
+ return image_node unless context.partition_structural
18
+ return image_node unless caption_text?(element)
19
+
20
+ Node::Figure.new(
21
+ attrs: Node::Figure::Attrs.new(id: element.id, title: caption_value(element)),
22
+ content: [image_node, Node::Caption.new(content: caption_text_nodes(element, context))]
23
+ )
24
+ end
25
+
26
+ class << self
27
+ private
28
+
29
+ def build_image_node(element)
30
+ Node::Image.new(
31
+ attrs: Node::Image::Attrs.new(
32
+ src: element.src,
33
+ alt: element.alt,
34
+ title: element.title,
35
+ caption: element.caption,
36
+ width: element.width,
37
+ height: element.height,
38
+ inline: element.inline || nil
39
+ )
40
+ )
41
+ end
42
+
43
+ def caption_text?(element)
44
+ !caption_value(element).to_s.empty?
45
+ end
46
+
47
+ def caption_value(element)
48
+ element.caption || element.title
49
+ end
50
+
51
+ def caption_text_nodes(element, context)
52
+ [context.text_node(caption_value(element).to_s)]
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Mirror
5
+ module Handlers
6
+ module Inline
7
+ # Classification of inline handlers.
8
+ SIMPLE_MARK_TYPES = {
9
+ CoreModel::BoldElement => Mark::Bold,
10
+ CoreModel::ItalicElement => Mark::Italic,
11
+ CoreModel::MonospaceElement => Mark::Monospace,
12
+ CoreModel::UnderlineElement => Mark::Underline,
13
+ CoreModel::StrikethroughElement => Mark::Strikethrough,
14
+ CoreModel::SubscriptElement => Mark::Subscript,
15
+ CoreModel::SuperscriptElement => Mark::Superscript,
16
+ CoreModel::HighlightElement => Mark::Highlight,
17
+ CoreModel::TermElement => Mark::Bold
18
+ }.freeze
19
+
20
+ def self.process(element, context:)
21
+ return [] unless element
22
+
23
+ children = inline_children_for(element)
24
+
25
+ children.flat_map do |child|
26
+ process_child(child, context)
27
+ end
28
+ end
29
+
30
+ def self.process_child(child, context)
31
+ case child
32
+ when CoreModel::TextContent
33
+ return [] if child.text.nil? || child.text.empty?
34
+
35
+ [context.text_node(child.text)]
36
+ when CoreModel::InlineElement
37
+ [dispatch_inline(child, context)].compact
38
+ when CoreModel::FootnoteReference
39
+ [context.resolve_footnote_reference(child)]
40
+ when CoreModel::Block, CoreModel::StructuralElement
41
+ result = context.registry.handle(child, context: context)
42
+ return [] unless result
43
+
44
+ value, concat = result
45
+ return [] unless value
46
+
47
+ if concat
48
+ Array(value)
49
+ else
50
+ [value].compact
51
+ end
52
+ when CoreModel::Image
53
+ [Handlers::Image.call(child, context: context)]
54
+ else
55
+ []
56
+ end
57
+ end
58
+
59
+ def self.call(element, context:)
60
+ dispatch_inline(element, context)
61
+ end
62
+
63
+ def self.text_content(element, context:)
64
+ return nil if element.text.nil? || element.text.empty?
65
+
66
+ context.text_node(element.text)
67
+ end
68
+
69
+ class << self
70
+ private
71
+
72
+ def inline_children_for(element)
73
+ if element.is_a?(CoreModel::InlineElement) ||
74
+ element.is_a?(CoreModel::Block) ||
75
+ element.is_a?(CoreModel::TableCell) ||
76
+ element.is_a?(CoreModel::StructuralElement)
77
+ children = element.children
78
+ return children if children && !children.empty?
79
+ end
80
+
81
+ if element.is_a?(CoreModel::InlineElement) ||
82
+ element.is_a?(CoreModel::Block)
83
+ content = element.content
84
+ return [CoreModel::TextContent.new(text: content.to_s)] if content && !content.to_s.empty?
85
+ end
86
+
87
+ []
88
+ end
89
+
90
+ def dispatch_inline(element, context)
91
+ mark_class = SIMPLE_MARK_TYPES[element.class]
92
+ return build_simple_mark(element, context, mark_class) if mark_class
93
+
94
+ case element
95
+ when CoreModel::LinkElement
96
+ build_link_mark(element, context)
97
+ when CoreModel::CrossReferenceElement
98
+ build_xref_mark(element, context)
99
+ when CoreModel::StemElement
100
+ build_stem_mark(element, context)
101
+ when CoreModel::SpanElement
102
+ build_span_mark(element, context)
103
+ when CoreModel::FootnoteElement
104
+ build_footnote_node(element, context)
105
+ when CoreModel::HardLineBreakElement, CoreModel::LineBreakElement
106
+ Node::SoftBreak.new
107
+ when CoreModel::TextElement
108
+ build_text_only(element, context)
109
+ when CoreModel::InlineElement
110
+ handle_generic_inline(element, context)
111
+ end
112
+ end
113
+
114
+ def handle_generic_inline(element, context)
115
+ text = element.content.to_s
116
+ return nil if text.empty?
117
+
118
+ context.text_node(text)
119
+ end
120
+
121
+ def build_simple_mark(element, context, mark_class)
122
+ text = extract_inline_text(element)
123
+ return nil if text.empty?
124
+
125
+ context.text_node(text, marks: [mark_class.new])
126
+ end
127
+
128
+ def build_link_mark(element, context)
129
+ text = extract_inline_text(element)
130
+ text = element.target.to_s if text.empty? && element.target
131
+ return nil if text.empty?
132
+
133
+ context.text_node(text, marks: [
134
+ Mark::Link.new(attrs: Mark::Link::Attrs.new(href: element.target))
135
+ ])
136
+ end
137
+
138
+ def build_xref_mark(element, context)
139
+ text = extract_inline_text(element)
140
+ target = element.target
141
+
142
+ display_text = text.empty? ? (target || '') : text
143
+ return nil if display_text.empty?
144
+
145
+ context.text_node(display_text, marks: [Mark::CrossReference.new(
146
+ attrs: Mark::CrossReference::Attrs.new(
147
+ target: target,
148
+ resolved: text.empty? ? nil : text
149
+ )
150
+ )])
151
+ end
152
+
153
+ def build_stem_mark(element, context)
154
+ text = extract_inline_text(element)
155
+ return nil if text.empty?
156
+
157
+ context.text_node(text, marks: [
158
+ Mark::Stem.new(attrs: Mark::Stem::Attrs.new(stem_type: element.stem_type))
159
+ ])
160
+ end
161
+
162
+ def build_span_mark(element, context)
163
+ text = extract_inline_text(element)
164
+ return nil if text.empty?
165
+
166
+ role = element.attr('role')
167
+ context.text_node(text, marks: [
168
+ Mark::Span.new(attrs: Mark::Span::Attrs.new(role: role))
169
+ ])
170
+ end
171
+
172
+ def build_footnote_node(element, context)
173
+ footnote = nil
174
+ if element.is_a?(CoreModel::InlineElement) && element.content
175
+ fn_id = element.attr('id')
176
+ footnote = CoreModel::Footnote.new(
177
+ id: fn_id,
178
+ content: element.content.to_s
179
+ )
180
+ end
181
+
182
+ context.register_footnote(footnote)
183
+ end
184
+
185
+ def build_text_only(element, context)
186
+ text = extract_inline_text(element)
187
+ return nil if text.empty?
188
+
189
+ context.text_node(text)
190
+ end
191
+
192
+ def extract_inline_text(element)
193
+ return element.content.to_s if element.content && !element.content.to_s.empty?
194
+
195
+ return element.nested_elements.map { |nested| extract_inline_text(nested) }.join if element.is_a?(CoreModel::InlineElement) && element.nested_elements
196
+
197
+ if (element.is_a?(CoreModel::InlineElement) || element.is_a?(CoreModel::Block)) && element.children && !element.children.empty?
198
+ return element.children.map do |child|
199
+ case child
200
+ when CoreModel::TextContent then child.text.to_s
201
+ when CoreModel::InlineElement then extract_inline_text(child)
202
+ else ''
203
+ end
204
+ end.join
205
+ end
206
+
207
+ ''
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Mirror
5
+ module Handlers
6
+ module List
7
+ def self.call(element, context:)
8
+ items = Array(element.items).filter_map do |item|
9
+ list_item(item, context: context)
10
+ end
11
+ return nil if items.empty?
12
+
13
+ node_class = ordered?(element) ? Node::OrderedList : Node::BulletList
14
+ node_class.new(
15
+ attrs: node_class::Attrs.new(
16
+ id: element.id,
17
+ start: element.is_a?(CoreModel::ListBlock) ? element.start : nil
18
+ ),
19
+ content: items
20
+ )
21
+ end
22
+
23
+ class << self
24
+ private
25
+
26
+ def list_item(item, context:)
27
+ content = build_item_content(item, context)
28
+ return nil if content.empty?
29
+
30
+ Node::ListItem.new(
31
+ attrs: Node::ListItem::Attrs.new(id: item.id),
32
+ content: content
33
+ )
34
+ end
35
+
36
+ def build_item_content(item, context)
37
+ content = []
38
+
39
+ has_children = item.is_a?(CoreModel::ListItem) &&
40
+ item.children && !item.children.empty?
41
+
42
+ if has_children
43
+ item.children.each do |child|
44
+ node = dispatch_child(child, context)
45
+ content << node if node
46
+ end
47
+ elsif item.content && !item.content.to_s.empty?
48
+ content << context.text_node(item.content.to_s)
49
+ end
50
+
51
+ if item.is_a?(CoreModel::ListItem) && item.nested_list
52
+ nested = Handlers::List.call(item.nested_list, context: context)
53
+ content << nested if nested
54
+ end
55
+
56
+ content
57
+ end
58
+
59
+ def dispatch_child(child, context)
60
+ case child
61
+ when CoreModel::TextContent
62
+ return nil if child.text.nil? || child.text.empty?
63
+
64
+ context.text_node(child.text)
65
+ when CoreModel::InlineElement
66
+ Handlers::Inline.call(child, context: context)
67
+ when CoreModel::Block, CoreModel::StructuralElement
68
+ result = context.registry.handle(child, context: context)
69
+ result&.first
70
+ end
71
+ end
72
+
73
+ def ordered?(element)
74
+ element.marker_type == 'ordered'
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Mirror
5
+ module Handlers
6
+ module OpenBlock
7
+ def self.call(element, context:)
8
+ content = context.extract_content(element)
9
+ return nil if content.empty?
10
+
11
+ Node::OpenBlock.new(content: content)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Mirror
5
+ module Handlers
6
+ module Paragraph
7
+ def self.call(element, context:)
8
+ content = context.process_inline_content(element)
9
+ return nil if content.empty?
10
+
11
+ Node::Paragraph.new(content: content)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Mirror
5
+ module Handlers
6
+ # Handles ReviewerBlock → omitted (reviewer notes are not rendered).
7
+ module Reviewer
8
+ def self.call(_element, *)
9
+ nil
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Mirror
5
+ module Handlers
6
+ module Sidebar
7
+ def self.call(element, context:)
8
+ content = context.extract_content(element)
9
+ return nil if content.empty?
10
+
11
+ Node::Sidebar.new(
12
+ attrs: Node::Sidebar::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,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Mirror
5
+ module Handlers
6
+ module Structural
7
+ # Top-level document handler. Stays flat; structural partitioning
8
+ # (preface/sections/bibliography) is opted into via the
9
+ # partition_structural: kwarg on CoreModelToMirror#call, which
10
+ # delegates to Mirror::Partitioner.
11
+ def self.document(element, context:)
12
+ content = context.extract_content(element)
13
+ Node::Document.new(
14
+ attrs: Node::Document::Attrs.new(title: element.title, id: element.id),
15
+ content: content
16
+ )
17
+ end
18
+
19
+ # Map CoreModel section style/title hints to JS SECTION_TYPES.
20
+ # When coradoc propagates AsciiDoc style attributes ([appendix],
21
+ # [bibliography], etc.) into SectionElement.attributes, this table
22
+ # is used to pick the right JS section type. Default fallback is
23
+ # `clause` (the JS generic section type).
24
+ SECTION_STYLE_TO_JS_TYPE = {
25
+ 'appendix' => 'annex',
26
+ 'annex' => 'annex',
27
+ 'bibliography' => 'references',
28
+ 'references' => 'references',
29
+ 'abstract' => 'abstract',
30
+ 'foreword' => 'foreword',
31
+ 'introduction' => 'introduction',
32
+ 'acknowledgements' => 'acknowledgements',
33
+ 'terms' => 'terms',
34
+ 'definitions' => 'definitions'
35
+ }.freeze
36
+
37
+ def self.section(element, context:)
38
+ content = context.extract_content(element)
39
+ type = context.partition_structural ? section_type_for(element) : 'section'
40
+
41
+ Node::Section.new(
42
+ type: type,
43
+ attrs: Node::Section::Attrs.new(
44
+ id: element.id,
45
+ title: element.title,
46
+ level: element.heading_level
47
+ ),
48
+ content: content
49
+ )
50
+ end
51
+
52
+ def self.section_type_for(element)
53
+ style = section_style(element)
54
+ SECTION_STYLE_TO_JS_TYPE[style] || 'clause'
55
+ end
56
+
57
+ # Reads `style` then `role` from SectionElement#attributes via the
58
+ # Metadata#[] accessor — no intermediate hash allocation per call.
59
+ def self.section_style(element)
60
+ attrs = element.attributes
61
+ return nil unless attrs.is_a?(Coradoc::CoreModel::Metadata)
62
+
63
+ attrs['style'] || attrs['role']
64
+ end
65
+
66
+ def self.preamble(element, context:)
67
+ content = context.extract_content(element)
68
+ Node::Preamble.new(content: content)
69
+ end
70
+
71
+ def self.header(element, context:)
72
+ content = context.extract_content(element)
73
+ Node::Header.new(
74
+ attrs: Node::Header::Attrs.new(
75
+ title: element.title,
76
+ level: element.heading_level
77
+ ),
78
+ content: content
79
+ )
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Mirror
5
+ module Handlers
6
+ module Table
7
+ def self.call(element, context:)
8
+ rows = Array(element.rows)
9
+ return nil if rows.empty?
10
+
11
+ head_rows, body_rows = partition_rows(rows)
12
+
13
+ content = []
14
+ content << build_table_head(head_rows, context) unless head_rows.empty?
15
+ content << build_table_body(body_rows, context) unless body_rows.empty?
16
+
17
+ return nil if content.empty?
18
+
19
+ Node::Table.new(
20
+ attrs: Node::Table::Attrs.new(
21
+ id: element.id,
22
+ title: element.title,
23
+ width: element.width
24
+ ),
25
+ content: content
26
+ )
27
+ end
28
+
29
+ class << self
30
+ private
31
+
32
+ def partition_rows(rows)
33
+ head = rows.select { |r| r.is_a?(CoreModel::TableRow) && r.header }
34
+ body = rows.reject { |r| r.is_a?(CoreModel::TableRow) && r.header }
35
+ [head, body]
36
+ end
37
+
38
+ def build_table_head(rows, context)
39
+ content = rows.map { |r| build_table_row(r, context) }
40
+ Node::TableHead.new(content: content)
41
+ end
42
+
43
+ def build_table_body(rows, context)
44
+ content = rows.map { |r| build_table_row(r, context) }
45
+ Node::TableBody.new(content: content)
46
+ end
47
+
48
+ def build_table_row(row, context)
49
+ cells = Array(row.cells).map { |c| build_table_cell(c, context) }
50
+ Node::TableRow.new(content: cells)
51
+ end
52
+
53
+ def build_table_cell(cell, context)
54
+ content = build_cell_content(cell, context)
55
+ Node::TableCell.new(
56
+ attrs: Node::TableCell::Attrs.new(
57
+ colspan: cell.colspan,
58
+ rowspan: cell.rowspan,
59
+ alignment: cell.alignment,
60
+ header: cell.header || nil
61
+ ),
62
+ content: content
63
+ )
64
+ end
65
+
66
+ def build_cell_content(cell, context)
67
+ if cell.is_a?(CoreModel::TableCell) && cell.children && !cell.children.empty?
68
+ return cell.children.flat_map do |child|
69
+ Handlers::Inline.process_child(child, context)
70
+ end
71
+ end
72
+
73
+ text = cell.content
74
+ return [] if text.nil? || text.to_s.empty?
75
+
76
+ [context.text_node(text.to_s)]
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Mirror
5
+ module Handlers
6
+ module Toc
7
+ def self.call(element, context:)
8
+ entries = if element.is_a?(CoreModel::Toc) && element.entries
9
+ Array(element.entries).filter_map do |entry|
10
+ build_entry(entry, context)
11
+ end
12
+ else
13
+ []
14
+ end
15
+
16
+ Node::Toc.new(
17
+ attrs: Node::Toc::Attrs.new(title: element.title),
18
+ content: entries
19
+ )
20
+ end
21
+
22
+ class << self
23
+ private
24
+
25
+ def build_entry(entry, context)
26
+ children = if entry.is_a?(CoreModel::TocEntry) && entry.children
27
+ entry.children.filter_map { |c| build_entry(c, context) }
28
+ else
29
+ []
30
+ end
31
+
32
+ content = [context.text_node(entry.title.to_s)] unless children.any?
33
+ content ||= children
34
+
35
+ Node::TocEntry.new(
36
+ attrs: Node::TocEntry::Attrs.new(
37
+ id: entry.is_a?(CoreModel::TocEntry) ? entry.id : nil,
38
+ title: entry.is_a?(CoreModel::TocEntry) ? entry.title : nil,
39
+ level: entry.is_a?(CoreModel::TocEntry) ? entry.level : nil
40
+ ),
41
+ content: content
42
+ )
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Mirror
5
+ module Handlers
6
+ module Verse
7
+ def self.call(element, context:)
8
+ text = if element.content && !element.content.to_s.empty?
9
+ element.flat_text || element.content.to_s
10
+ else
11
+ ''
12
+ end
13
+
14
+ Node::Verse.new(
15
+ attrs: Node::Verse::Attrs.new(attribution: element.attribution),
16
+ content: [context.text_node(text)]
17
+ )
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end