metanorma-oiml 0.1.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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.adoc +291 -0
- data/data/themes/oiml/assets/oiml-logo-full-dark.svg +1 -0
- data/data/themes/oiml/assets/oiml-logo-full-light.svg +1 -0
- data/data/themes/oiml/assets/oiml-logo-icon-dark.svg +1 -0
- data/data/themes/oiml/assets/oiml-logo-icon-light.svg +1 -0
- data/data/themes/oiml/assets/oiml-logo.svg +1 -0
- data/data/themes/oiml/theme.yaml +56 -0
- data/exe/oiml-sts +10 -0
- data/lib/metanorma/oiml/document/root.rb +29 -0
- data/lib/metanorma/oiml/document.rb +35 -0
- data/lib/metanorma/oiml/html/renderer.rb +14 -0
- data/lib/metanorma/oiml/html.rb +14 -0
- data/lib/metanorma/oiml/sts/cli.rb +89 -0
- data/lib/metanorma/oiml/sts/html_renderer/assets/metanorma-icon-dark.svg +1 -0
- data/lib/metanorma/oiml/sts/html_renderer/assets/metanorma-icon-light.svg +1 -0
- data/lib/metanorma/oiml/sts/html_renderer/assets/oiml-logo-dark.svg +1 -0
- data/lib/metanorma/oiml/sts/html_renderer/assets/oiml-logo.svg +1 -0
- data/lib/metanorma/oiml/sts/html_renderer/assets/oiml-smart-logo-dark.svg +1 -0
- data/lib/metanorma/oiml/sts/html_renderer/assets/oiml-smart-logo.svg +1 -0
- data/lib/metanorma/oiml/sts/html_renderer/assets/page.js +145 -0
- data/lib/metanorma/oiml/sts/html_renderer/assets/theme.css +509 -0
- data/lib/metanorma/oiml/sts/html_renderer/ruby.rb +1071 -0
- data/lib/metanorma/oiml/sts/html_renderer/templates/_element.html.liquid +1 -0
- data/lib/metanorma/oiml/sts/html_renderer/templates/_footer.html.liquid +21 -0
- data/lib/metanorma/oiml/sts/html_renderer/templates/_hero.html.liquid +9 -0
- data/lib/metanorma/oiml/sts/html_renderer/templates/_img.html.liquid +1 -0
- data/lib/metanorma/oiml/sts/html_renderer/templates/_link.html.liquid +1 -0
- data/lib/metanorma/oiml/sts/html_renderer/templates/_meta_header.html.liquid +1 -0
- data/lib/metanorma/oiml/sts/html_renderer/templates/document.html.liquid +43 -0
- data/lib/metanorma/oiml/sts/html_renderer.rb +19 -0
- data/lib/metanorma/oiml/sts/mathml_builder.rb +93 -0
- data/lib/metanorma/oiml/sts/parity_validator/normaliser.rb +95 -0
- data/lib/metanorma/oiml/sts/parity_validator/report.rb +137 -0
- data/lib/metanorma/oiml/sts/parity_validator.rb +23 -0
- data/lib/metanorma/oiml/sts/transformer/back_transformer.rb +55 -0
- data/lib/metanorma/oiml/sts/transformer/base.rb +71 -0
- data/lib/metanorma/oiml/sts/transformer/block_dispatcher.rb +145 -0
- data/lib/metanorma/oiml/sts/transformer/body_transformer.rb +23 -0
- data/lib/metanorma/oiml/sts/transformer/context.rb +27 -0
- data/lib/metanorma/oiml/sts/transformer/dl_transformer.rb +49 -0
- data/lib/metanorma/oiml/sts/transformer/document_transformer.rb +65 -0
- data/lib/metanorma/oiml/sts/transformer/figure_transformer.rb +49 -0
- data/lib/metanorma/oiml/sts/transformer/footnote_collector.rb +33 -0
- data/lib/metanorma/oiml/sts/transformer/formula_transformer.rb +45 -0
- data/lib/metanorma/oiml/sts/transformer/front_transformer.rb +84 -0
- data/lib/metanorma/oiml/sts/transformer/id_generator.rb +39 -0
- data/lib/metanorma/oiml/sts/transformer/inline_transformer.rb +14 -0
- data/lib/metanorma/oiml/sts/transformer/list_transformer.rb +82 -0
- data/lib/metanorma/oiml/sts/transformer/model_builder.rb +322 -0
- data/lib/metanorma/oiml/sts/transformer/note_transformer.rb +103 -0
- data/lib/metanorma/oiml/sts/transformer/paragraph_transformer.rb +574 -0
- data/lib/metanorma/oiml/sts/transformer/reference_transformer.rb +107 -0
- data/lib/metanorma/oiml/sts/transformer/rendered_text_extractor.rb +168 -0
- data/lib/metanorma/oiml/sts/transformer/section_transformer.rb +233 -0
- data/lib/metanorma/oiml/sts/transformer/source_document.rb +246 -0
- data/lib/metanorma/oiml/sts/transformer/standard.rb +37 -0
- data/lib/metanorma/oiml/sts/transformer/table_transformer.rb +290 -0
- data/lib/metanorma/oiml/sts/transformer/term_transformer.rb +236 -0
- data/lib/metanorma/oiml/sts/transformer.rb +47 -0
- data/lib/metanorma/oiml/sts/validation_report.rb +43 -0
- data/lib/metanorma/oiml/sts/validator.rb +126 -0
- data/lib/metanorma/oiml/sts/version.rb +9 -0
- data/lib/metanorma/oiml/sts.rb +84 -0
- data/lib/metanorma-oiml.rb +10 -0
- data/schematron/oiml-sts.sch +70 -0
- metadata +221 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module Metanorma
|
|
3
|
+
module Oiml
|
|
4
|
+
module Sts
|
|
5
|
+
module Transformer
|
|
6
|
+
class TableTransformer < Base
|
|
7
|
+
def transform(source_table)
|
|
8
|
+
attrs = {}
|
|
9
|
+
attrs[:id] = source_table.id if source_table.class.method_defined?(:id) && source_table.id
|
|
10
|
+
label = table_label(source_table)
|
|
11
|
+
attrs[:label] = label if label
|
|
12
|
+
caption = table_caption(source_table)
|
|
13
|
+
attrs[:caption] = caption if caption
|
|
14
|
+
table = build_table(source_table)
|
|
15
|
+
attrs[:table] = [table] if table
|
|
16
|
+
::Sts::TbxIsoTml::TableWrap.new(attrs)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
# "Table 2" from the presentation XML autonum attribute.
|
|
22
|
+
def table_label(source_table)
|
|
23
|
+
return nil unless source_table.class.method_defined?(:autonum) && source_table.autonum
|
|
24
|
+
|
|
25
|
+
"Table #{source_table.autonum}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def table_caption(source_table)
|
|
29
|
+
return nil unless source_table.class.method_defined?(:name) && source_table.name
|
|
30
|
+
|
|
31
|
+
text = RenderedTextExtractor.text_of(source_table.name).strip
|
|
32
|
+
return nil if text.empty?
|
|
33
|
+
|
|
34
|
+
::Sts::NisoSts::Caption.new(
|
|
35
|
+
title: ::Sts::NisoSts::Title.new(content: [text])
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def build_table(source_table)
|
|
40
|
+
attrs = {}
|
|
41
|
+
if source_table.class.method_defined?(:thead) && source_table.thead
|
|
42
|
+
attrs[:thead] = build_section(source_table.thead, ::Sts::TbxIsoTml::Thead)
|
|
43
|
+
end
|
|
44
|
+
if source_table.class.method_defined?(:tbody) && source_table.tbody
|
|
45
|
+
attrs[:tbody] = build_section(source_table.tbody, ::Sts::TbxIsoTml::Tbody)
|
|
46
|
+
end
|
|
47
|
+
::Sts::TbxIsoTml::Table.new(attrs)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def build_section(source_section, klass)
|
|
51
|
+
trs = Array(source_section.tr).map { |tr| build_tr(tr) }
|
|
52
|
+
klass.new(tr: trs)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def build_tr(tr)
|
|
56
|
+
th_cells = Array(tr.th).map { |c| build_cell(c, ::Sts::TbxIsoTml::Th) }
|
|
57
|
+
td_cells = Array(tr.td).map { |c| build_cell(c, ::Sts::TbxIsoTml::Td) }
|
|
58
|
+
attrs = {}
|
|
59
|
+
attrs[:th] = th_cells if th_cells.any?
|
|
60
|
+
attrs[:td] = td_cells if td_cells.any?
|
|
61
|
+
::Sts::TbxIsoTml::Tr.new(attrs)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def build_cell(cell, klass)
|
|
65
|
+
entries = cell_entries(cell)
|
|
66
|
+
fns = cell_footnotes(cell)
|
|
67
|
+
klass.new do |c|
|
|
68
|
+
entries.each do |kind, value|
|
|
69
|
+
case kind
|
|
70
|
+
when :text then c.content value
|
|
71
|
+
when :inline_formula then c.inline_formula value
|
|
72
|
+
when :monospace then c.monospace value
|
|
73
|
+
when :bold then c.bold value
|
|
74
|
+
when :italic then c.italic value
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
fns.each { |fn| c.fn fn }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Extracts <fn> footnotes from the cell. MN table cells with
|
|
82
|
+
# footnote:[...] in AsciiDoc produce an inline <fn> carrying
|
|
83
|
+
# the footnote body (with paragraphs and math). STS preserves
|
|
84
|
+
# the footnote as a sibling <fn> element inside the cell so
|
|
85
|
+
# the renderer can show its content.
|
|
86
|
+
def cell_footnotes(cell)
|
|
87
|
+
return [] unless cell.class.method_defined?(:each_mixed_content)
|
|
88
|
+
|
|
89
|
+
cell.each_mixed_content.filter_map do |node|
|
|
90
|
+
next if node.is_a?(String)
|
|
91
|
+
next unless node.is_a?(Metanorma::Document::Components::Inline::FnElement)
|
|
92
|
+
|
|
93
|
+
build_fn(node)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def build_fn(fn_element)
|
|
98
|
+
paragraphs = Array(fn_element.p).map.with_index { |p, i| build_fn_paragraph(p, fn_element, first: i.zero?) }
|
|
99
|
+
return nil if paragraphs.empty?
|
|
100
|
+
|
|
101
|
+
attrs = { id: fn_element.id, p: paragraphs }
|
|
102
|
+
label = fn_label(fn_element)
|
|
103
|
+
attrs[:label] = ::Sts::NisoSts::Label.new(content: [label]) if label
|
|
104
|
+
::Sts::TbxIsoTml::Fn.new(attrs)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Fn#p is typed as Sts::NisoSts::Paragraph (uses :text attr).
|
|
108
|
+
# Our paragraph_transformer produces Sts::NisoSts::Paragraph.
|
|
109
|
+
# Convert by extracting text — footnote bodies in MN table
|
|
110
|
+
# cells drop inline math, so we accept text-only. The fn's
|
|
111
|
+
# reference letter prefixes the first paragraph to match
|
|
112
|
+
# MN's "<sup>a</sup> body" rendering.
|
|
113
|
+
def build_fn_paragraph(mn_p, fn_element, first:)
|
|
114
|
+
iso_p = paragraph_transformer.transform(mn_p)
|
|
115
|
+
text = extract_paragraph_text(iso_p)
|
|
116
|
+
label = fn_label(fn_element)
|
|
117
|
+
text = "#{label} #{text}" if first && label && !text.empty?
|
|
118
|
+
::Sts::NisoSts::Paragraph.new(text: [text])
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def extract_paragraph_text(iso_paragraph)
|
|
122
|
+
parts = []
|
|
123
|
+
iso_paragraph.element_order.each do |entry|
|
|
124
|
+
if entry.node_type == :text
|
|
125
|
+
parts << entry.text_content.to_s
|
|
126
|
+
elsif entry.node_type == :element && entry.name == "inline-formula"
|
|
127
|
+
# Best-effort: extract any mi/mn/mo text from the math
|
|
128
|
+
parts << extract_formula_text(iso_paragraph)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
parts.join.strip
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def extract_formula_text(iso_paragraph)
|
|
135
|
+
Array(iso_paragraph.inline_formula).map do |f|
|
|
136
|
+
math = f.math
|
|
137
|
+
next "" unless math
|
|
138
|
+
extract_math_text(math.to_xml)
|
|
139
|
+
end.join
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Strip comments and MathML/XML tags from a math element's
|
|
143
|
+
# serialised XML so the parity validator compares only the
|
|
144
|
+
# visible text. Comment stripping uses String#index (linear,
|
|
145
|
+
# correct for multi-char terminators); tag stripping uses
|
|
146
|
+
# simple non-nested character classes.
|
|
147
|
+
def extract_math_text(math_xml)
|
|
148
|
+
stripped = strip_xml_comments(math_xml.to_s)
|
|
149
|
+
stripped
|
|
150
|
+
.gsub(%r{</?\w+:[^>]*>}, " ")
|
|
151
|
+
.gsub(/<\/?[^>]*>/, " ")
|
|
152
|
+
.gsub(/\s+/, " ")
|
|
153
|
+
.strip
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Removes every `<!-- ... -->` comment from +str+. Linear in
|
|
157
|
+
# the input size; correct for comments containing `>` (which
|
|
158
|
+
# the previous regex-based stripper would prematurely match).
|
|
159
|
+
# An unterminated comment runs to end-of-string.
|
|
160
|
+
def strip_xml_comments(str)
|
|
161
|
+
result = String.new
|
|
162
|
+
pos = 0
|
|
163
|
+
while (open_idx = str.index("<!--", pos))
|
|
164
|
+
result << str[pos, open_idx - pos]
|
|
165
|
+
close_idx = str.index("-->", open_idx + 4)
|
|
166
|
+
break if close_idx.nil?
|
|
167
|
+
pos = close_idx + 3
|
|
168
|
+
end
|
|
169
|
+
result << str[pos..] unless pos.nil? || pos >= str.length
|
|
170
|
+
result
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def fn_label(fn_element)
|
|
174
|
+
ref = fn_element.reference
|
|
175
|
+
ref.to_s unless ref.nil? || ref.to_s.empty?
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Walks the cell's mixed content in document order and produces
|
|
179
|
+
# an array of [:text, String] | [:inline_formula, InlineFormula] |
|
|
180
|
+
# [:monospace|:bold|:italic, model] tuples. The builder in
|
|
181
|
+
# build_cell routes them to the correct typed collection on the
|
|
182
|
+
# Td/Th model, preserving cross-type ordering via lutaml-model's
|
|
183
|
+
# mixed_content element_order.
|
|
184
|
+
def cell_entries(cell)
|
|
185
|
+
entries = []
|
|
186
|
+
current_text = nil
|
|
187
|
+
flush = lambda do
|
|
188
|
+
return if current_text.nil?
|
|
189
|
+
entries << [:text, current_text]
|
|
190
|
+
current_text = nil
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
walk_cell(cell) do |kind, node, text|
|
|
194
|
+
case kind
|
|
195
|
+
when :text
|
|
196
|
+
current_text = (current_text || "") + text
|
|
197
|
+
when :stem
|
|
198
|
+
formula = build_inline_formula(node)
|
|
199
|
+
if formula
|
|
200
|
+
flush.call
|
|
201
|
+
entries << [:inline_formula, formula]
|
|
202
|
+
else
|
|
203
|
+
mirror_text = cell_mirror_text(node)
|
|
204
|
+
current_text = (current_text || "") + mirror_text if mirror_text && !mirror_text.empty?
|
|
205
|
+
end
|
|
206
|
+
when :monospace
|
|
207
|
+
flush.call
|
|
208
|
+
entries << [:monospace, ::Sts::NisoSts::Monospace.new(content: [text])]
|
|
209
|
+
when :bold
|
|
210
|
+
flush.call
|
|
211
|
+
entries << [:bold, ::Sts::TbxIsoTml::Bold.new(value: [text])]
|
|
212
|
+
when :italic
|
|
213
|
+
flush.call
|
|
214
|
+
entries << [:italic, ::Sts::TbxIsoTml::Italic.new(value: [text])]
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
flush.call
|
|
218
|
+
entries
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def walk_cell(cell)
|
|
222
|
+
return unless cell.class.method_defined?(:each_mixed_content)
|
|
223
|
+
|
|
224
|
+
@cell_stem_mirrors = build_cell_stem_mirrors(cell)
|
|
225
|
+
|
|
226
|
+
cell.each_mixed_content do |node|
|
|
227
|
+
if node.is_a?(String)
|
|
228
|
+
yield(:text, node, node)
|
|
229
|
+
next
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
case node.class.name&.split("::")&.last
|
|
233
|
+
when "StemInlineElement", "StemBlockElement"
|
|
234
|
+
yield(:stem, node, nil)
|
|
235
|
+
when "FmtStemElement"
|
|
236
|
+
next
|
|
237
|
+
when "AsciimathElement"
|
|
238
|
+
next
|
|
239
|
+
when "TtElement", "MonospaceElement"
|
|
240
|
+
yield(:monospace, node, RenderedTextExtractor.text_of(node))
|
|
241
|
+
when "StrongRawElement", "StrongElement"
|
|
242
|
+
yield(:bold, node, RenderedTextExtractor.text_of(node))
|
|
243
|
+
when "EmRawElement", "EmElement"
|
|
244
|
+
yield(:italic, node, RenderedTextExtractor.text_of(node))
|
|
245
|
+
else
|
|
246
|
+
text = RenderedTextExtractor.text_of(node)
|
|
247
|
+
yield(:text, node, text) if text && !text.empty?
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def build_inline_formula(stem_node)
|
|
253
|
+
mirror = @cell_stem_mirrors && @cell_stem_mirrors[stem_node.id]
|
|
254
|
+
source = mirror || stem_node
|
|
255
|
+
::Metanorma::Oiml::Sts::MathmlBuilder.inline_formula_from_stem(
|
|
256
|
+
source,
|
|
257
|
+
klass: ::Sts::NisoSts::InlineFormula
|
|
258
|
+
)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def build_cell_stem_mirrors(cell)
|
|
262
|
+
map = {}
|
|
263
|
+
return map unless cell.class.method_defined?(:each_mixed_content)
|
|
264
|
+
|
|
265
|
+
cell.each_mixed_content do |node|
|
|
266
|
+
next unless node.is_a?(Metanorma::Document::Components::Inline::FmtStemElement)
|
|
267
|
+
Array(node.semx).each do |semx|
|
|
268
|
+
source = semx.source if semx.is_a?(Metanorma::Document::Components::Inline::SemxElement)
|
|
269
|
+
map[source] = node if source && !source.empty?
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
map
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def cell_mirror_text(stem_node)
|
|
276
|
+
mirror = @cell_stem_mirrors && @cell_stem_mirrors[stem_node.id]
|
|
277
|
+
return nil unless mirror
|
|
278
|
+
|
|
279
|
+
Array(mirror.semx).map do |semx|
|
|
280
|
+
next nil unless semx.is_a?(Metanorma::Document::Components::Inline::SemxElement)
|
|
281
|
+
semx.text
|
|
282
|
+
end.compact.join
|
|
283
|
+
rescue StandardError
|
|
284
|
+
nil
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# frozen_string: true
|
|
2
|
+
|
|
3
|
+
module Metanorma
|
|
4
|
+
module Oiml
|
|
5
|
+
module Sts
|
|
6
|
+
module Transformer
|
|
7
|
+
class TermTransformer < Base
|
|
8
|
+
def transform(source_term)
|
|
9
|
+
content = []
|
|
10
|
+
|
|
11
|
+
term_number = extract_term_number(source_term)
|
|
12
|
+
add_preferred_name(content, source_term)
|
|
13
|
+
add_definition(content, source_term)
|
|
14
|
+
add_standalone_paragraphs(content, source_term)
|
|
15
|
+
add_lists(content, source_term)
|
|
16
|
+
add_term_notes(content, source_term)
|
|
17
|
+
add_term_examples(content, source_term)
|
|
18
|
+
add_sources(content, source_term)
|
|
19
|
+
|
|
20
|
+
attrs = {}
|
|
21
|
+
attrs[:id] = source_term.id if source_term.id
|
|
22
|
+
# The term number ("3.1") becomes the section's <title>,
|
|
23
|
+
# which the renderer emits as a TermNum heading matching
|
|
24
|
+
# MN's "<h3 class='TermNum'>3.5.12</h3>" layout. Previously
|
|
25
|
+
# it was emitted as a leading <p>, which the parity check
|
|
26
|
+
# did not see as a heading.
|
|
27
|
+
attrs[:title] = ::Sts::NisoSts::Title.new(content: [term_number]) if term_number
|
|
28
|
+
paragraphs, lists, notes = partition_content(content)
|
|
29
|
+
attrs[:paragraphs] = paragraphs if paragraphs.any?
|
|
30
|
+
attrs[:list] = lists if lists.any?
|
|
31
|
+
attrs[:non_normative_note] = notes if notes.any?
|
|
32
|
+
|
|
33
|
+
nested = Array(source_term.term).map { |sub| transform(sub) }
|
|
34
|
+
attrs[:sec] = nested if nested.any?
|
|
35
|
+
|
|
36
|
+
::Sts::NisoSts::Section.new(attrs)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def partition_content(content)
|
|
40
|
+
paragraphs = content.grep(::Sts::NisoSts::Paragraph)
|
|
41
|
+
lists = content.grep(::Sts::NisoSts::List)
|
|
42
|
+
notes = content.grep(::Sts::NisoSts::NonNormativeNote)
|
|
43
|
+
others = content.reject do |c|
|
|
44
|
+
[::Sts::NisoSts::Paragraph, ::Sts::NisoSts::List,
|
|
45
|
+
::Sts::NisoSts::NonNormativeNote].any? { |k| c.is_a?(k) }
|
|
46
|
+
end
|
|
47
|
+
[paragraphs + others, lists, notes]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
# The term number ("3.1") from the term's fmt-name caption label.
|
|
53
|
+
def extract_term_number(source_term)
|
|
54
|
+
section_transformer.extract_autonum(source_term)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def add_preferred_name(content, source_term)
|
|
58
|
+
para = preferred_name_paragraph(source_term)
|
|
59
|
+
content << para if para
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Preferred names often contain math (e.g. "maximum capacity
|
|
63
|
+
# (E_max)"). The typed TermNameElement drops stems, so we prefer
|
|
64
|
+
# the rendered fmt_preferred tree and route it through the
|
|
65
|
+
# paragraph transformer so MathML passes through verbatim.
|
|
66
|
+
# Falls back to text-only paragraph when no rendered tree exists.
|
|
67
|
+
def preferred_name_paragraph(source_term)
|
|
68
|
+
fmt = Array(source_term.fmt_preferred).first if source_term.class.method_defined?(:fmt_preferred)
|
|
69
|
+
if fmt && fmt.class.method_defined?(:p)
|
|
70
|
+
p = Array(fmt.p).first
|
|
71
|
+
return paragraph_transformer.transform(p) if p
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
text = preferred_name_text(source_term)
|
|
75
|
+
return nil if text.nil? || text.empty?
|
|
76
|
+
|
|
77
|
+
::Sts::NisoSts::Paragraph.new(text: [text])
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def preferred_name_text(source_term)
|
|
81
|
+
preferred = Array(source_term.preferred).first
|
|
82
|
+
return nil unless preferred
|
|
83
|
+
|
|
84
|
+
expression = preferred.expression
|
|
85
|
+
return RenderedTextExtractor.text_of(preferred) unless expression
|
|
86
|
+
|
|
87
|
+
names = Array(expression.name)
|
|
88
|
+
return RenderedTextExtractor.text_of(preferred) if names.empty?
|
|
89
|
+
|
|
90
|
+
RenderedTextExtractor.text_of(names.first)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def add_definition(content, source_term)
|
|
94
|
+
definition = Array(source_term.definition).first
|
|
95
|
+
return unless definition
|
|
96
|
+
|
|
97
|
+
vd = definition.verbal_definition
|
|
98
|
+
return unless vd
|
|
99
|
+
|
|
100
|
+
Array(vd.p).each { |p| content << paragraph_transformer.transform(p) }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def add_standalone_paragraphs(content, source_term)
|
|
104
|
+
Array(source_term.p).each { |p| content << paragraph_transformer.transform(p) }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def add_lists(content, source_term)
|
|
108
|
+
definition = Array(source_term.definition).first
|
|
109
|
+
return unless definition
|
|
110
|
+
|
|
111
|
+
vd = definition.verbal_definition
|
|
112
|
+
return unless vd
|
|
113
|
+
|
|
114
|
+
Array(vd.ul).each { |ul| content << list_transformer.transform(ul) }
|
|
115
|
+
Array(vd.ol).each { |ol| content << list_transformer.transform(ol) }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def add_list_items(content, list)
|
|
119
|
+
Array(list.listitem).each do |li|
|
|
120
|
+
Array(li.paragraphs).each { |p| content << paragraph_transformer.transform(p) }
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Term notes become <non-normative-note> carrying their own
|
|
125
|
+
# label ("Note 1 to entry:") — plain "Note" notes leave the
|
|
126
|
+
# kind label to the renderer. Nested <ul>/<ol> inside the
|
|
127
|
+
# termnote become sibling <list> children of the note so the
|
|
128
|
+
# renderer emits them after the paragraphs.
|
|
129
|
+
def add_term_notes(content, source_term)
|
|
130
|
+
Array(source_term.termnote).each do |tn|
|
|
131
|
+
paragraphs = Array(tn.p).map { |p| paragraph_transformer.transform(p) }
|
|
132
|
+
lists = Array(tn.ul).map { |ul| list_transformer.transform(ul) } +
|
|
133
|
+
Array(tn.ol).map { |ol| list_transformer.transform(ol) }
|
|
134
|
+
next if paragraphs.empty? && lists.empty?
|
|
135
|
+
|
|
136
|
+
attrs = { p: paragraphs }
|
|
137
|
+
attrs[:list] = lists if lists.any?
|
|
138
|
+
label = term_note_label(tn)
|
|
139
|
+
attrs[:label] = ::Sts::NisoSts::Label.new(content: [label]) if label
|
|
140
|
+
content << ::Sts::NisoSts::NonNormativeNote.new(attrs)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# MN renders term notes "<span class='term-note-label'>
|
|
145
|
+
# Term notes become <non-normative-note> with a "Note:" label
|
|
146
|
+
# — matching MN's "<span class='termnote_label'>Note: </span>"
|
|
147
|
+
# prefix inside the note paragraph. The entity's autonum
|
|
148
|
+
# ("Note 1 to entry:") isn't surfaced in MN's term notes, so
|
|
149
|
+
# we mirror that here.
|
|
150
|
+
def term_note_label(_note_obj)
|
|
151
|
+
"Note:"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def add_term_examples(content, source_term)
|
|
155
|
+
Array(source_term.termexample).each { |te| add_typed_note_paragraphs(content, te, "Example", colon: true) }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def add_typed_note_paragraphs(content, note_obj, label, colon: false)
|
|
159
|
+
suffix = colon ? ": " : " "
|
|
160
|
+
Array(note_obj.p).each_with_index do |p, i|
|
|
161
|
+
para = paragraph_transformer.transform(p)
|
|
162
|
+
if i.zero?
|
|
163
|
+
existing = para.content.to_a
|
|
164
|
+
para.content = ["#{label}#{suffix}#{existing.first || ''}"] + existing[1..]
|
|
165
|
+
end
|
|
166
|
+
content << para
|
|
167
|
+
end
|
|
168
|
+
Array(note_obj.ul).each { |ul| content << list_transformer.transform(ul) }
|
|
169
|
+
Array(note_obj.ol).each { |ol| content << list_transformer.transform(ol) }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def build_list(list, list_type)
|
|
173
|
+
list_transformer.transform_with_type(list, list_type)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def add_sources(content, source_term)
|
|
177
|
+
Array(source_term.source).each { |s| content << build_source(s) }
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def build_source(source)
|
|
181
|
+
origin = source.origin
|
|
182
|
+
return nil unless origin
|
|
183
|
+
|
|
184
|
+
citeas = origin.citeas
|
|
185
|
+
locality = extract_locality(origin)
|
|
186
|
+
ordinal = bib_ordinal(origin)
|
|
187
|
+
|
|
188
|
+
text = "[SOURCE: #{citeas}"
|
|
189
|
+
if locality
|
|
190
|
+
text << ", #{locality}"
|
|
191
|
+
text << "[#{ordinal}]" if ordinal
|
|
192
|
+
text << " "
|
|
193
|
+
end
|
|
194
|
+
text << "]"
|
|
195
|
+
::Sts::NisoSts::Paragraph.new(text: [text])
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def extract_locality(origin)
|
|
199
|
+
ls_arr = origin.locality_stack
|
|
200
|
+
return nil unless ls_arr.is_a?(Array) && ls_arr.any?
|
|
201
|
+
|
|
202
|
+
parts = []
|
|
203
|
+
ls_arr.each do |ls|
|
|
204
|
+
Array(ls.bib_locality).each do |loc|
|
|
205
|
+
from = loc.reference_from
|
|
206
|
+
val = case from
|
|
207
|
+
when String then from
|
|
208
|
+
when nil then ""
|
|
209
|
+
else
|
|
210
|
+
cv = from.content rescue nil
|
|
211
|
+
cv ? Array(cv).first.to_s : from.to_s
|
|
212
|
+
end
|
|
213
|
+
parts << val if val && !val.empty?
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
parts.empty? ? nil : parts.join(", ")
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def bib_ordinal(origin)
|
|
220
|
+
bibitemid = origin.bibitemid
|
|
221
|
+
return nil unless bibitemid
|
|
222
|
+
|
|
223
|
+
bib = context.source.typed_root.bibliography
|
|
224
|
+
return nil unless bib
|
|
225
|
+
|
|
226
|
+
items = Array(bib.references).flat_map { |rs| Array(rs.references) }
|
|
227
|
+
items.each_with_index do |item, i|
|
|
228
|
+
return (i + 1).to_s if item.id == bibitemid || item.anchor == bibitemid
|
|
229
|
+
end
|
|
230
|
+
nil
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string: true
|
|
2
|
+
|
|
3
|
+
# OIML Metanorma presentation XML → OIML NISO STS XML transformer.
|
|
4
|
+
#
|
|
5
|
+
# Architecture:
|
|
6
|
+
# - {SourceDocument}: parses input via Metanorma::Oiml::Document::Root
|
|
7
|
+
# (typed model, on the metanorma-document framework). Zero Nokogiri.
|
|
8
|
+
# - {Context}: shared state during one conversion run.
|
|
9
|
+
# - {ModelBuilder}: factory methods for sts-ruby model instances.
|
|
10
|
+
# - {DocumentTransformer}: orchestrator. Produces a Sts::NisoSts::Standard
|
|
11
|
+
# root via sts-ruby models and serializes via lutaml-model.
|
|
12
|
+
# - Per-element transformers walk typed-model source objects and emit
|
|
13
|
+
# sts-ruby model instances. Zero Nokogiri.
|
|
14
|
+
# - {RenderedTextExtractor}: unified text extraction from typed models.
|
|
15
|
+
# - {BlockDispatcher}: Open/Closed dispatch by typed model class name.
|
|
16
|
+
module Metanorma::Oiml::Sts::Transformer
|
|
17
|
+
autoload :SourceDocument, "metanorma/oiml/sts/transformer/source_document"
|
|
18
|
+
autoload :Standard, "metanorma/oiml/sts/transformer/standard"
|
|
19
|
+
autoload :Context, "metanorma/oiml/sts/transformer/context"
|
|
20
|
+
autoload :ModelBuilder, "metanorma/oiml/sts/transformer/model_builder"
|
|
21
|
+
autoload :IdGenerator, "metanorma/oiml/sts/transformer/id_generator"
|
|
22
|
+
autoload :FootnoteCollector, "metanorma/oiml/sts/transformer/footnote_collector"
|
|
23
|
+
autoload :Base, "metanorma/oiml/sts/transformer/base"
|
|
24
|
+
autoload :DocumentTransformer, "metanorma/oiml/sts/transformer/document_transformer"
|
|
25
|
+
autoload :FrontTransformer, "metanorma/oiml/sts/transformer/front_transformer"
|
|
26
|
+
autoload :BodyTransformer, "metanorma/oiml/sts/transformer/body_transformer"
|
|
27
|
+
autoload :BackTransformer, "metanorma/oiml/sts/transformer/back_transformer"
|
|
28
|
+
autoload :SectionTransformer, "metanorma/oiml/sts/transformer/section_transformer"
|
|
29
|
+
autoload :ParagraphTransformer, "metanorma/oiml/sts/transformer/paragraph_transformer"
|
|
30
|
+
autoload :InlineTransformer, "metanorma/oiml/sts/transformer/inline_transformer"
|
|
31
|
+
autoload :ListTransformer, "metanorma/oiml/sts/transformer/list_transformer"
|
|
32
|
+
autoload :TableTransformer, "metanorma/oiml/sts/transformer/table_transformer"
|
|
33
|
+
autoload :FigureTransformer, "metanorma/oiml/sts/transformer/figure_transformer"
|
|
34
|
+
autoload :FormulaTransformer, "metanorma/oiml/sts/transformer/formula_transformer"
|
|
35
|
+
autoload :NoteTransformer, "metanorma/oiml/sts/transformer/note_transformer"
|
|
36
|
+
autoload :ReferenceTransformer, "metanorma/oiml/sts/transformer/reference_transformer"
|
|
37
|
+
autoload :TermTransformer, "metanorma/oiml/sts/transformer/term_transformer"
|
|
38
|
+
autoload :DlTransformer, "metanorma/oiml/sts/transformer/dl_transformer"
|
|
39
|
+
autoload :BlockDispatcher, "metanorma/oiml/sts/transformer/block_dispatcher"
|
|
40
|
+
autoload :RenderedTextExtractor, "metanorma/oiml/sts/transformer/rendered_text_extractor"
|
|
41
|
+
|
|
42
|
+
def self.convert(input)
|
|
43
|
+
source = SourceDocument.parse(input)
|
|
44
|
+
context = Context.new(source)
|
|
45
|
+
DocumentTransformer.new(context).transform_to_xml(source)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
|
|
5
|
+
module Metanorma
|
|
6
|
+
module Oiml
|
|
7
|
+
module Sts
|
|
8
|
+
# Immutable value object holding the result of validating an OIML STS
|
|
9
|
+
# XML document. `errors` is an array of {Finding} structs.
|
|
10
|
+
class ValidationReport
|
|
11
|
+
Finding = Struct.new(:rule_id, :message, :xpath, keyword_init: true)
|
|
12
|
+
|
|
13
|
+
attr_reader :errors
|
|
14
|
+
|
|
15
|
+
def initialize(errors: [])
|
|
16
|
+
@errors = errors
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def valid?
|
|
20
|
+
errors.empty?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def add(rule_id:, message:, xpath: nil)
|
|
24
|
+
@errors << Finding.new(rule_id: rule_id, message: message, xpath: xpath)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def freeze
|
|
28
|
+
@errors.freeze
|
|
29
|
+
super
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def to_s
|
|
33
|
+
if valid?
|
|
34
|
+
"OIML STS validation passed (0 errors)"
|
|
35
|
+
else
|
|
36
|
+
"OIML STS validation FAILED (#{errors.size} errors):\n" \
|
|
37
|
+
"#{errors.map { |e| " [#{e.rule_id}] #{e.message}#{' at ' + e.xpath if e.xpath}" }.join("\n")}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|