metanorma-mirror 1.0.0

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 (58) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +25 -0
  3. data/README.adoc +41 -0
  4. data/lib/metanorma/mirror/class_table.rb +50 -0
  5. data/lib/metanorma/mirror/default_registry.rb +23 -0
  6. data/lib/metanorma/mirror/handler_registry.rb +60 -0
  7. data/lib/metanorma/mirror/handler_result.rb +37 -0
  8. data/lib/metanorma/mirror/handlers/admonition.rb +18 -0
  9. data/lib/metanorma/mirror/handlers/example.rb +21 -0
  10. data/lib/metanorma/mirror/handlers/figure.rb +53 -0
  11. data/lib/metanorma/mirror/handlers/formula.rb +35 -0
  12. data/lib/metanorma/mirror/handlers/imagemap.rb +43 -0
  13. data/lib/metanorma/mirror/handlers/inline/catalog.rb +36 -0
  14. data/lib/metanorma/mirror/handlers/inline/rich_html_renderer.rb +132 -0
  15. data/lib/metanorma/mirror/handlers/inline/text_extractor.rb +114 -0
  16. data/lib/metanorma/mirror/handlers/inline.rb +155 -0
  17. data/lib/metanorma/mirror/handlers/list.rb +110 -0
  18. data/lib/metanorma/mirror/handlers/note.rb +26 -0
  19. data/lib/metanorma/mirror/handlers/paragraph.rb +19 -0
  20. data/lib/metanorma/mirror/handlers/quote.rb +16 -0
  21. data/lib/metanorma/mirror/handlers/review.rb +19 -0
  22. data/lib/metanorma/mirror/handlers/section.rb +149 -0
  23. data/lib/metanorma/mirror/handlers/sourcecode.rb +27 -0
  24. data/lib/metanorma/mirror/handlers/structural.rb +68 -0
  25. data/lib/metanorma/mirror/handlers/svgmap.rb +52 -0
  26. data/lib/metanorma/mirror/handlers/table.rb +81 -0
  27. data/lib/metanorma/mirror/handlers/term.rb +100 -0
  28. data/lib/metanorma/mirror/handlers.rb +62 -0
  29. data/lib/metanorma/mirror/id_strategy/positional.rb +151 -0
  30. data/lib/metanorma/mirror/id_strategy/preserve.rb +12 -0
  31. data/lib/metanorma/mirror/id_strategy.rb +34 -0
  32. data/lib/metanorma/mirror/math_util.rb +53 -0
  33. data/lib/metanorma/mirror/metadata.rb +37 -0
  34. data/lib/metanorma/mirror/model/container.rb +50 -0
  35. data/lib/metanorma/mirror/model/factory.rb +34 -0
  36. data/lib/metanorma/mirror/model/guide.rb +29 -0
  37. data/lib/metanorma/mirror/model/leaf.rb +17 -0
  38. data/lib/metanorma/mirror/model/mark.rb +39 -0
  39. data/lib/metanorma/mirror/model/node.rb +42 -0
  40. data/lib/metanorma/mirror/model/soft_break.rb +32 -0
  41. data/lib/metanorma/mirror/model/text.rb +27 -0
  42. data/lib/metanorma/mirror/model.rb +16 -0
  43. data/lib/metanorma/mirror/output/builder.rb +39 -0
  44. data/lib/metanorma/mirror/output/formats/base_format.rb +90 -0
  45. data/lib/metanorma/mirror/output/formats/inline_format.rb +142 -0
  46. data/lib/metanorma/mirror/output/formats.rb +53 -0
  47. data/lib/metanorma/mirror/output/pipeline.rb +109 -0
  48. data/lib/metanorma/mirror/output/pipeline_context.rb +22 -0
  49. data/lib/metanorma/mirror/output.rb +12 -0
  50. data/lib/metanorma/mirror/rewriter.rb +123 -0
  51. data/lib/metanorma/mirror/safe_attr.rb +16 -0
  52. data/lib/metanorma/mirror/serialization/json_serializer.rb +27 -0
  53. data/lib/metanorma/mirror/serialization/yaml_serializer.rb +20 -0
  54. data/lib/metanorma/mirror/serialization.rb +10 -0
  55. data/lib/metanorma/mirror/transformer.rb +110 -0
  56. data/lib/metanorma/mirror/version.rb +7 -0
  57. data/lib/metanorma/mirror.rb +98 -0
  58. metadata +197 -0
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Mirror
5
+ module Handlers
6
+ module Inline
7
+ module TextExtractor
8
+ def self.extract_element_text(element)
9
+ return "" unless element.is_a?(Lutaml::Model::Serializable)
10
+
11
+ text = SafeAttr.read(element, :text)
12
+ if text.is_a?(Array)
13
+ joined = text.join.strip
14
+ return joined unless joined.empty?
15
+ elsif text.is_a?(String) && !text.strip.empty?
16
+ return text
17
+ end
18
+
19
+ content = SafeAttr.read(element, :content)
20
+ if content.is_a?(Array)
21
+ parts = content.filter_map do |c|
22
+ next c.to_s if c.is_a?(String)
23
+ next extract_element_text(c) if c.is_a?(Lutaml::Model::Serializable)
24
+
25
+ nil
26
+ end
27
+ joined = parts.join.strip
28
+ return joined unless joined.empty?
29
+ elsif content.is_a?(String) && !content.strip.empty?
30
+ return content
31
+ end
32
+
33
+ ""
34
+ end
35
+
36
+ def self.extract_formatted_text(element)
37
+ return "" unless element
38
+ return element.to_s unless element.is_a?(Lutaml::Model::Serializable)
39
+
40
+ parts = []
41
+ element.each_mixed_content do |node|
42
+ case node
43
+ when String
44
+ parts << node
45
+ when Lutaml::Model::Serializable
46
+ inner = extract_formatted_text(node)
47
+ parts << inner if inner && !inner.empty?
48
+ end
49
+ end
50
+ result = parts.join.strip
51
+ result.empty? ? extract_element_text(element) : result
52
+ end
53
+
54
+ def self.extract_text_from_model(node)
55
+ substitution = Mirror.inline_text_substitutions.lookup(node)
56
+ if substitution
57
+ substitution
58
+ else
59
+ text = extract_formatted_text(node)
60
+ text.empty? ? extract_element_text(node) : text
61
+ end
62
+ end
63
+
64
+ def self.extract_fn_label(element)
65
+ label = SafeAttr.read(element, :fmt_fn_label)
66
+ return extract_element_text(element) unless label
67
+
68
+ extract_formatted_text(label)
69
+ end
70
+
71
+ def self.extract_stem_text(element)
72
+ asciimath = MathUtil.asciimath_from_stem(element)
73
+ return asciimath if asciimath
74
+
75
+ extract_element_text(element)
76
+ end
77
+
78
+ def self.extract_span_text(element)
79
+ text = extract_element_text(element)
80
+ return text unless text.strip.empty?
81
+
82
+ extract_formatted_text(element)
83
+ end
84
+
85
+ def self.extract_name_text(name)
86
+ return name if name.is_a?(String)
87
+
88
+ text = SafeAttr.read(name, :text)
89
+ return text.to_s if text.is_a?(String) && !text.strip.empty?
90
+
91
+ stems = SafeAttr.read(name, :stem)
92
+ if stems.is_a?(Array) && !stems.empty?
93
+ parts = Array(text).dup
94
+ stems.each_with_index do |s, i|
95
+ stem_text = MathUtil.text_from_stem(s)
96
+ parts.insert(i + 1, stem_text) if stem_text && !stem_text.empty?
97
+ end
98
+ joined = parts.join.strip
99
+ return joined unless joined.empty?
100
+ end
101
+
102
+ return Array(text).join if text.is_a?(Array) && !text.empty?
103
+
104
+ ""
105
+ end
106
+
107
+ def self.extract_bibdata_title(bibdata)
108
+ Metadata.title_from_bibdata(bibdata)
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Mirror
5
+ module Handlers
6
+ module Inline
7
+ autoload :TextExtractor, "#{__dir__}/inline/text_extractor"
8
+ autoload :RichHtmlRenderer, "#{__dir__}/inline/rich_html_renderer"
9
+ autoload :Catalog, "#{__dir__}/inline/catalog"
10
+
11
+ SEMX_MARK_CONFIG = {
12
+ "xref" => { mark_type: "xref", target_attr: :target,
13
+ fmt_attr: :fmt_xref },
14
+ "eref" => { mark_type: "eref", target_attr: :target,
15
+ fmt_attr: :fmt_xref },
16
+ "link" => { mark_type: "link", target_attr: :target,
17
+ fmt_attr: :fmt_link },
18
+ }.freeze
19
+
20
+ CROSSREF_MARKS = %w[xref eref link].to_set.freeze
21
+
22
+ # Delegates to TextExtractor
23
+ def self.extract_element_text(element)
24
+ TextExtractor.extract_element_text(element)
25
+ end
26
+
27
+ def self.extract_formatted_text(element)
28
+ TextExtractor.extract_formatted_text(element)
29
+ end
30
+
31
+ def self.extract_text_from_model(node)
32
+ TextExtractor.extract_text_from_model(node)
33
+ end
34
+
35
+ # Delegates to RichHtmlRenderer
36
+ def self.extract_rich_html(element)
37
+ RichHtmlRenderer.extract(element)
38
+ end
39
+
40
+ # Primary entry point: extract inline content from a mixed-content element.
41
+ # Iterates semantic children only — `fmt-*` rendered siblings are skipped
42
+ # to avoid emitting duplicate marks for the same logical content.
43
+ def self.extract_inline(element, context:)
44
+ nodes = []
45
+ iterator = Mirror.inline_content_iterator or
46
+ raise Error, "no inline content iterator registered; " \
47
+ "the document seed must provide one"
48
+ iterator.each(element) do |node|
49
+ case node
50
+ when String
51
+ nodes << context.text_node(node) unless node.empty?
52
+ else
53
+ handle_inline_element(node, nodes, context:)
54
+ end
55
+ end
56
+ filter_empty_crossrefs(nodes)
57
+ end
58
+
59
+ def self.handle_inline_element(element, nodes, context:)
60
+ if semx_element?(element)
61
+ handle_semx(element, nodes, context:)
62
+ return
63
+ end
64
+
65
+ if block_in_inline?(element, context)
66
+ handle_block_in_inline(element, nodes, context:)
67
+ return
68
+ end
69
+
70
+ mark_builder = Mirror.mark_builders.lookup(element)
71
+ if mark_builder
72
+ mark = mark_builder.call(element)
73
+ text = extract_marked_text(mark.type, element)
74
+ nodes << context.text_node(text, marks: [mark])
75
+ else
76
+ handle_structured_inline(element, nodes, context:)
77
+ end
78
+ end
79
+
80
+ def self.handle_semx(element, nodes, context:)
81
+ config = SEMX_MARK_CONFIG[element.element_attr]
82
+ if config
83
+ handle_semx_crossref(element, config, nodes, context:)
84
+ else
85
+ handle_structured_inline(element, nodes, context:)
86
+ end
87
+ end
88
+
89
+ def self.handle_semx_crossref(element, config, nodes, context:)
90
+ fmt_children = SafeAttr.read(element, config[:fmt_attr])
91
+ fmt_child = fmt_children&.first
92
+
93
+ mark_attrs = {}
94
+ if fmt_child && config[:target_attr]
95
+ mark_attrs[config[:target_attr]] = SafeAttr.read(fmt_child, :target)
96
+ end
97
+
98
+ if element.element_attr == "eref" && fmt_child
99
+ mark_attrs[:citeas] = extract_formatted_text(fmt_child)
100
+ end
101
+
102
+ text = extract_formatted_text(fmt_child || element)
103
+ mark = Handlers.build_mark(config[:mark_type],
104
+ attrs: mark_attrs.compact)
105
+ nodes << context.text_node(text, marks: [mark])
106
+ end
107
+
108
+ def self.handle_block_in_inline(element, nodes, context:)
109
+ result = context.registry.handle(element, context: context)
110
+ result.append_to(nodes)
111
+ end
112
+
113
+ def self.handle_structured_inline(element, nodes, context:)
114
+ if element.is_a?(Lutaml::Model::Serializable)
115
+ inner_nodes = extract_inline(element, context:)
116
+ nodes.concat(inner_nodes)
117
+ else
118
+ text = element.text
119
+ nodes << context.text_node(text) if text.is_a?(String) && !text.strip.empty?
120
+ end
121
+ end
122
+
123
+ def self.mark_text_extractors
124
+ @mark_text_extractors ||= {
125
+ "footnote" => TextExtractor.method(:extract_fn_label),
126
+ "stem" => TextExtractor.method(:extract_stem_text),
127
+ "span" => TextExtractor.method(:extract_span_text),
128
+ }
129
+ end
130
+
131
+ def self.extract_marked_text(mark_type, element)
132
+ extractor = mark_text_extractors[mark_type]
133
+ return extractor.call(element) if extractor
134
+
135
+ TextExtractor.extract_element_text(element)
136
+ end
137
+
138
+ def self.semx_element?(element)
139
+ !Mirror.semx_elements.lookup(element).nil?
140
+ end
141
+
142
+ def self.block_in_inline?(element, context)
143
+ context.registry.registered?(element.class)
144
+ end
145
+
146
+ def self.filter_empty_crossrefs(nodes)
147
+ nodes.reject do |n|
148
+ n.is_a?(Model::Text) && n.text.strip.empty? &&
149
+ n.marks.any? { |m| CROSSREF_MARKS.include?(m.type) }
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Mirror
5
+ module Handlers
6
+ module List
7
+ def self.bullet(element, context:)
8
+ attrs = list_attrs(element)
9
+ items = extract_items(element, context:)
10
+
11
+ Handlers.build_node("bullet_list", attrs: attrs, content: items)
12
+ end
13
+
14
+ def self.ordered(element, context:)
15
+ attrs = list_attrs(element)
16
+ attrs[:type] = SafeAttr.read(element, :type)
17
+ attrs[:start] = SafeAttr.read(element, :start)
18
+ attrs[:group] = SafeAttr.read(element, :group)
19
+ items = extract_items(element, context:)
20
+
21
+ Handlers.build_node("ordered_list", attrs: attrs.compact,
22
+ content: items)
23
+ end
24
+
25
+ def self.definition(element, context:)
26
+ attrs = list_attrs(element)
27
+ attrs[:key] = SafeAttr.read(element, :key)
28
+ items = extract_definition_items(element, context:)
29
+
30
+ Handlers.build_node("dl", attrs: attrs.compact, content: items)
31
+ end
32
+
33
+ def self.list_item(element, context:)
34
+ attrs = {}
35
+ attrs[:id] = SafeAttr.read(element, :id)
36
+ attrs[:checkbox] = SafeAttr.read(element, :checkbox)
37
+ attrs[:checkedcheckbox] = SafeAttr.read(element, :checkedcheckbox)
38
+
39
+ content = []
40
+ text = SafeAttr.read(element, :text)
41
+ if text.is_a?(Array)
42
+ text.each do |t|
43
+ next if t.is_a?(String) && t.strip.empty?
44
+
45
+ content << context.text_node(t.to_s)
46
+ end
47
+ end
48
+
49
+ %i[paragraphs unordered_lists ordered_lists sourcecode figure example
50
+ note quote table].each do |attr|
51
+ collection = SafeAttr.read(element, attr)
52
+ next unless collection
53
+
54
+ collection.each do |child|
55
+ result = context.registry.handle(child, context: context)
56
+ result.append_to(content)
57
+ end
58
+ end
59
+
60
+ Handlers.build_node("list_item", attrs: attrs.compact,
61
+ content: content)
62
+ end
63
+
64
+ def self.extract_items(element, context:)
65
+ Array(element.listitem).filter_map do |li|
66
+ result = context.registry.handle(li, context: context)
67
+ result.nodes
68
+ end
69
+ end
70
+
71
+ def self.extract_definition_items(element, context:)
72
+ items = []
73
+ dts = Array(element.dt)
74
+ dds = Array(element.dd)
75
+
76
+ dts.each_with_index do |dt, idx|
77
+ dt_attrs = {}
78
+ dt_attrs[:id] = SafeAttr.read(dt, :id)
79
+ dt_content = Inline.extract_inline(dt, context:)
80
+ items << Handlers.build_node("dt", attrs: dt_attrs.compact,
81
+ content: dt_content)
82
+
83
+ dd = dds[idx]
84
+ next unless dd
85
+
86
+ dd_attrs = {}
87
+ dd_attrs[:id] = SafeAttr.read(dd, :id)
88
+ dd_content = context.extract_named_collections(dd,
89
+ %i[p ul ol
90
+ sourcecode figure example note table formula quote dl])
91
+ items << Handlers.build_node("dd", attrs: dd_attrs.compact,
92
+ content: dd_content)
93
+ end
94
+ items
95
+ end
96
+
97
+ def self.list_attrs(element)
98
+ attrs = {}
99
+ attrs[:id] = SafeAttr.read(element, :id)
100
+ attrs[:nobullet] = SafeAttr.read(element, :nobullet)
101
+ attrs[:bare] = SafeAttr.read(element, :bare)
102
+ attrs[:spacing] = SafeAttr.read(element, :spacing)
103
+ attrs[:indent] = SafeAttr.read(element, :indent)
104
+ attrs[:semx_id] = SafeAttr.read(element, :semx_id)
105
+ attrs.compact
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Mirror
5
+ module Handlers
6
+ module Note
7
+ EXTRA = { type_attr: :type_attr, remove_in_rfc: nil }.freeze
8
+
9
+ def self.call(element, context:)
10
+ attrs = Handlers.extract_attrs(element, extra_attrs: EXTRA)
11
+
12
+ content = []
13
+ element_content = SafeAttr.read(element, :content)
14
+ if element_content.is_a?(Array)
15
+ element_content.each do |p|
16
+ result = context.registry.handle(p, context: context)
17
+ result.append_to(content)
18
+ end
19
+ end
20
+
21
+ Handlers.build_node("note", attrs: attrs, content: content)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Mirror
5
+ module Handlers
6
+ module Paragraph
7
+ EXTRA = { alignment: nil, keep_with_next: nil,
8
+ keep_with_previous: nil }.freeze
9
+
10
+ def self.call(element, context:)
11
+ attrs = Handlers.extract_attrs(element, extra_attrs: EXTRA)
12
+ content = Inline.extract_inline(element, context:)
13
+
14
+ Handlers.build_node("paragraph", attrs: attrs, content: content)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Mirror
5
+ module Handlers
6
+ module Quote
7
+ def self.call(element, context:)
8
+ attrs = Handlers.extract_attrs(element)
9
+ content = context.extract_blocks(element)
10
+
11
+ Handlers.build_node("quote", attrs: attrs, content: content)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Mirror
5
+ module Handlers
6
+ module Review
7
+ EXTRA = { date: nil, from: nil, to: nil, reviewer: nil,
8
+ display: nil }.freeze
9
+
10
+ def self.call(element, context:)
11
+ attrs = Handlers.extract_attrs(element, extra_attrs: EXTRA)
12
+ content = context.extract_blocks(element)
13
+
14
+ Handlers.build_node("review", attrs: attrs, content: content)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Mirror
5
+ module Handlers
6
+ module Section
7
+ def self.clause(element, context:)
8
+ attrs = section_attrs(element, context:)
9
+ content = context.extract_blocks(element)
10
+
11
+ Handlers.build_node("clause", attrs: attrs, content: content)
12
+ end
13
+
14
+ def self.annex(element, context:)
15
+ attrs = section_attrs(element, context:)
16
+ attrs[:commentary] = SafeAttr.read(element, :commentary)
17
+ attrs[:language] = SafeAttr.read(element, :language)
18
+ attrs[:script] = SafeAttr.read(element, :script)
19
+
20
+ content = context.extract_blocks(element)
21
+
22
+ Handlers.build_node("annex", attrs: attrs.compact, content: content)
23
+ end
24
+
25
+ def self.content_section(element, context:)
26
+ attrs = section_attrs(element, context:)
27
+ content = context.extract_blocks(element)
28
+
29
+ subsection = SafeAttr.read(element, :subsection)
30
+ subsection&.each do |sub|
31
+ result = context.registry.handle(sub, context: context)
32
+ result.append_to(content)
33
+ end
34
+
35
+ Handlers.build_node("content_section", attrs: attrs, content: content)
36
+ end
37
+
38
+ def self.terms(element, context:)
39
+ attrs = section_attrs(element, context:)
40
+ content = context.extract_named_collections(element,
41
+ %i[p term dl example
42
+ admonition])
43
+ children = context.extract_section_children(element)
44
+
45
+ Handlers.build_node("terms", attrs: attrs,
46
+ content: content + children)
47
+ end
48
+
49
+ def self.definitions(element, context:)
50
+ attrs = section_attrs(element, context:)
51
+ content = context.extract_blocks(element)
52
+
53
+ Handlers.build_node("definitions", attrs: attrs, content: content)
54
+ end
55
+
56
+ def self.references(element, context:)
57
+ attrs = section_attrs(element, context:)
58
+ attrs[:normative] = SafeAttr.read(element, :normative)
59
+ attrs[:hidden] = SafeAttr.read(element, :hidden)
60
+
61
+ # Prefatory paragraphs (e.g. "The following documents are referred to...")
62
+ content = context.extract_named_collections(element,
63
+ %i[p])
64
+
65
+ # Bibliographic items
66
+ refs = SafeAttr.read(element, :references)
67
+ refs&.each do |ref|
68
+ text = extract_biblio_text(ref)
69
+ next if text.strip.empty?
70
+
71
+ content << Handlers.build_node("paragraph",
72
+ attrs: { id: SafeAttr.read(ref,
73
+ :id) }.compact,
74
+ content: [context.text_node(text)])
75
+ end
76
+
77
+ Handlers.build_node("references", attrs: attrs, content: content)
78
+ end
79
+
80
+ def self.extract_biblio_text(ref)
81
+ parts = []
82
+
83
+ tag = SafeAttr.read(ref, :biblio_tag)
84
+ if tag
85
+ tag_text = Inline.extract_formatted_text(tag).strip
86
+ parts << tag_text unless tag_text.empty?
87
+ end
88
+
89
+ formatted = SafeAttr.read(ref, :formatted_ref)
90
+ if formatted
91
+ fmt_text = Inline.extract_formatted_text(formatted).strip
92
+ parts << fmt_text unless fmt_text.empty?
93
+ end
94
+
95
+ return parts.join(" ") unless parts.empty?
96
+
97
+ docid = SafeAttr.read(ref, :docidentifier)
98
+ if docid
99
+ text = Inline.extract_formatted_text(docid)
100
+ return text unless text.strip.empty?
101
+ end
102
+
103
+ ""
104
+ end
105
+
106
+ def self.floating_title(element, context:)
107
+ attrs = {}
108
+ attrs[:id] = context.id_strategy.assign_id(element)
109
+ attrs[:depth] = SafeAttr.read(element, :depth)
110
+ attrs[:semx_id] = SafeAttr.read(element, :semx_id)
111
+
112
+ title = extract_title(element)
113
+ attrs[:title] = title if title
114
+
115
+ Handlers.build_node("floating_title", attrs: attrs.compact)
116
+ end
117
+
118
+ def self.section_attrs(element, context:)
119
+ attrs = {}
120
+ attrs[:id] = context.id_strategy.assign_id(element)
121
+ attrs[:number] = SafeAttr.read(element, :number)
122
+ attrs[:obligation] = SafeAttr.read(element, :obligation)
123
+ attrs[:unnumbered] = SafeAttr.read(element, :unnumbered)
124
+ attrs[:toc] = SafeAttr.read(element, :toc)
125
+ attrs[:type] = SafeAttr.read(element, :type)
126
+ attrs[:semx_id] = SafeAttr.read(element, :semx_id)
127
+ attrs[:autonum] = SafeAttr.read(element, :autonum)
128
+ attrs[:displayorder] = SafeAttr.read(element, :displayorder)
129
+
130
+ title = extract_title(element)
131
+ attrs[:title] = title if title
132
+ attrs.compact
133
+ end
134
+
135
+ def self.extract_title(element)
136
+ title = SafeAttr.read(element, :title)
137
+ return nil unless title
138
+
139
+ case title
140
+ when String then title
141
+ else
142
+ rich = Inline.extract_rich_html(title)
143
+ rich.empty? ? Inline.extract_element_text(title) : rich
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Mirror
5
+ module Handlers
6
+ module Sourcecode
7
+ EXTRA = { language: :lang, filename: nil, linenums: nil }.freeze
8
+
9
+ def self.call(element, context:)
10
+ attrs = Handlers.extract_attrs(element, extra_attrs: EXTRA)
11
+
12
+ body = SafeAttr.read(element, :body)
13
+ text = if body
14
+ body.decoded_content
15
+ elsif SafeAttr.read(element, :content).is_a?(String)
16
+ SafeAttr.read(element, :content)
17
+ else
18
+ ""
19
+ end
20
+ attrs[:text] = text
21
+
22
+ Handlers.build_node("sourcecode", attrs: attrs.compact)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end