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,574 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module Metanorma
|
|
3
|
+
module Oiml
|
|
4
|
+
module Sts
|
|
5
|
+
module Transformer
|
|
6
|
+
class ParagraphTransformer < Base
|
|
7
|
+
PRESENTATION_CLASSES = %w[
|
|
8
|
+
FmtTitleElement FmtXrefLabelElement FmtNameElement FmtFnLabelElement
|
|
9
|
+
FmtAnnotationStartElement FmtAnnotationEndElement FmtConceptElement
|
|
10
|
+
FmtStemElement
|
|
11
|
+
LocalizedString LocalizedStrings FmtVal AsciiMath
|
|
12
|
+
BrElement TabElement Bookmark
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
def transform(source_para)
|
|
16
|
+
id_val = nil
|
|
17
|
+
if source_para.is_a?(Lutaml::Model::Serializable) && source_para.class.method_defined?(:id)
|
|
18
|
+
id_val = source_para.id
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
@seen_nodes = {}
|
|
22
|
+
@stem_mirrors = build_stem_mirror_map(source_para)
|
|
23
|
+
entries = []
|
|
24
|
+
current_text = nil
|
|
25
|
+
flush_text = lambda do
|
|
26
|
+
return if current_text.nil?
|
|
27
|
+
entries << [:text, clean_text(current_text)]
|
|
28
|
+
current_text = nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
walk_inline(source_para) do |kind, node, text|
|
|
32
|
+
case kind
|
|
33
|
+
when :text
|
|
34
|
+
current_text = (current_text || "") + text
|
|
35
|
+
when :bold
|
|
36
|
+
flush_text.call
|
|
37
|
+
entries << [:bold, text]
|
|
38
|
+
when :italic
|
|
39
|
+
flush_text.call
|
|
40
|
+
entries << [:italic, text]
|
|
41
|
+
when :sub
|
|
42
|
+
flush_text.call
|
|
43
|
+
entries << [:sub, text]
|
|
44
|
+
when :sup
|
|
45
|
+
flush_text.call
|
|
46
|
+
entries << [:sup, text]
|
|
47
|
+
when :tt
|
|
48
|
+
flush_text.call
|
|
49
|
+
entries << [:monospace, text]
|
|
50
|
+
when :stem
|
|
51
|
+
formula = build_inline_formula(node)
|
|
52
|
+
if formula
|
|
53
|
+
flush_text.call
|
|
54
|
+
entries << [:inline_formula, formula]
|
|
55
|
+
else
|
|
56
|
+
text = mirror_text(node)
|
|
57
|
+
current_text = (current_text || "") + text if text && !text.empty?
|
|
58
|
+
end
|
|
59
|
+
when :xref
|
|
60
|
+
x = build_xref(node)
|
|
61
|
+
if x
|
|
62
|
+
flush_text.call
|
|
63
|
+
entries << [:xref, x]
|
|
64
|
+
end
|
|
65
|
+
when :link
|
|
66
|
+
l = build_link(node)
|
|
67
|
+
if l
|
|
68
|
+
flush_text.call
|
|
69
|
+
entries << [:ext_link, l]
|
|
70
|
+
end
|
|
71
|
+
when :eref
|
|
72
|
+
if text && !text.empty?
|
|
73
|
+
current_text = (current_text || "") + text
|
|
74
|
+
end
|
|
75
|
+
when :concept
|
|
76
|
+
if text && !text.empty?
|
|
77
|
+
current_text = (current_text || "") + text
|
|
78
|
+
end
|
|
79
|
+
when :fn
|
|
80
|
+
fn = build_fn(node)
|
|
81
|
+
if fn
|
|
82
|
+
flush_text.call
|
|
83
|
+
entries << [:fn, fn]
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
flush_text.call
|
|
88
|
+
|
|
89
|
+
::Sts::NisoSts::Paragraph.new(id: id_val) do |p|
|
|
90
|
+
entries.each do |kind, value|
|
|
91
|
+
case kind
|
|
92
|
+
when :text then p.text value
|
|
93
|
+
when :bold then p.bold value
|
|
94
|
+
when :italic then p.italic value
|
|
95
|
+
when :sub then p.sub value
|
|
96
|
+
when :sup then p.sup value
|
|
97
|
+
when :monospace then p.monospace value
|
|
98
|
+
when :inline_formula then p.inline_formula value
|
|
99
|
+
when :xref then p.xref value
|
|
100
|
+
when :ext_link then p.ext_link value
|
|
101
|
+
when :fn then p.fn value
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def walk_inline(obj, &block)
|
|
110
|
+
return unless obj.class.method_defined?(:each_mixed_content)
|
|
111
|
+
|
|
112
|
+
nodes = obj.each_mixed_content.to_a
|
|
113
|
+
nodes.each_with_index do |node, index|
|
|
114
|
+
following = nodes[index + 1]
|
|
115
|
+
if semantic_with_mirror?(node, following)
|
|
116
|
+
emit_mirror(node, following, &block)
|
|
117
|
+
next
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
walk_node(node, &block)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# A semantic element (<link>, <eref>, ...) immediately followed
|
|
125
|
+
# by its <semx> presentation mirror (semx@source == element@id):
|
|
126
|
+
# the pair is emitted as ONE unit — the mirror carries the
|
|
127
|
+
# renderable form, the semantic original lends its kind
|
|
128
|
+
# (style/type) when the mirror doesn't declare one (NISO STS
|
|
129
|
+
# @ref-type maps from Metanorma's @type/@style).
|
|
130
|
+
def semantic_with_mirror?(node, following)
|
|
131
|
+
return false if node.is_a?(String)
|
|
132
|
+
return false unless following.is_a?(Metanorma::Document::Components::Inline::SemxElement)
|
|
133
|
+
return false unless following.class.method_defined?(:source) && following.source
|
|
134
|
+
|
|
135
|
+
node.class.method_defined?(:id) && node.id && node.id == following.source
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def emit_mirror(node, mirror, &block)
|
|
139
|
+
@mirror_style = node_style(node)
|
|
140
|
+
walk_node(mirror, &block)
|
|
141
|
+
ensure
|
|
142
|
+
@mirror_style = nil
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def node_style(node)
|
|
146
|
+
%i[style type].each do |attr|
|
|
147
|
+
next unless node.class.method_defined?(attr)
|
|
148
|
+
|
|
149
|
+
val = node.public_send(attr).to_s
|
|
150
|
+
return val unless val.empty?
|
|
151
|
+
end
|
|
152
|
+
nil
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Walks a single node yielded by each_mixed_content. Recurses
|
|
156
|
+
# into SemxElement wrappers so fmt-preferred / fmt-concept trees
|
|
157
|
+
# (which wrap content in semx) walk the same way as body
|
|
158
|
+
# paragraphs (which usually have direct inline children).
|
|
159
|
+
# Semx mirrors inline children into named attributes as well as
|
|
160
|
+
# its mixed content — @seen_nodes prevents double-yielding.
|
|
161
|
+
def walk_node(node, &block)
|
|
162
|
+
if node.is_a?(String)
|
|
163
|
+
yield(:text, node, node)
|
|
164
|
+
return
|
|
165
|
+
end
|
|
166
|
+
return if @seen_nodes[node.object_id]
|
|
167
|
+
|
|
168
|
+
@seen_nodes[node.object_id] = true
|
|
169
|
+
|
|
170
|
+
class_name = node.class.name&.split("::")&.last
|
|
171
|
+
return if PRESENTATION_CLASSES.include?(class_name)
|
|
172
|
+
return if presentation_span?(node)
|
|
173
|
+
|
|
174
|
+
case class_name
|
|
175
|
+
when "SemxElement"
|
|
176
|
+
# Mixed content first; named child attributes only as a
|
|
177
|
+
# fallback for fmt trees that store inlines there (the two
|
|
178
|
+
# views mirror each other — walking both duplicates links.
|
|
179
|
+
yield_count = 0
|
|
180
|
+
counting = lambda do |kind, node, text|
|
|
181
|
+
yield_count += 1
|
|
182
|
+
yield(kind, node, text)
|
|
183
|
+
end
|
|
184
|
+
walk_inline(node, &counting)
|
|
185
|
+
walk_semx_named_children(node, &counting) if yield_count.zero?
|
|
186
|
+
when "EmRawElement", "EmElement"
|
|
187
|
+
yield(:italic, node, build_inline_container(node, ::Sts::TbxIsoTml::Italic))
|
|
188
|
+
when "StrongRawElement", "StrongElement"
|
|
189
|
+
yield(:bold, node, build_inline_container(node, ::Sts::TbxIsoTml::Bold))
|
|
190
|
+
when "SubElement"
|
|
191
|
+
yield(:sub, node, build_inline_container(node, ::Sts::NisoSts::Sub))
|
|
192
|
+
when "SupElement"
|
|
193
|
+
yield(:sup, node, build_inline_container(node, ::Sts::NisoSts::Sup))
|
|
194
|
+
when "TtElement", "MonospaceElement"
|
|
195
|
+
yield(:tt, node, build_inline_container(node, ::Sts::NisoSts::Monospace))
|
|
196
|
+
when "StemInlineElement", "StemBlockElement"
|
|
197
|
+
yield(:stem, node, nil)
|
|
198
|
+
when "AsciimathElement"
|
|
199
|
+
nil
|
|
200
|
+
when "SpanElement"
|
|
201
|
+
walk_inline(node, &block)
|
|
202
|
+
when "LinkElement"
|
|
203
|
+
yield(:link, node, text_of(node))
|
|
204
|
+
when "XrefElement"
|
|
205
|
+
yield(:xref, node, nil)
|
|
206
|
+
when "FmtXrefElement", "FmtErefElement"
|
|
207
|
+
yield(:xref, node, RenderedTextExtractor.text_of(node))
|
|
208
|
+
when "ErefElement"
|
|
209
|
+
yield(:eref, node, eref_text(node))
|
|
210
|
+
when "ConceptElement"
|
|
211
|
+
yield(:concept, node, concept_text(node))
|
|
212
|
+
when "FnElement"
|
|
213
|
+
yield(:fn, node, nil)
|
|
214
|
+
else
|
|
215
|
+
# Never silently drop content: unhandled inline classes
|
|
216
|
+
# degrade to their extracted text.
|
|
217
|
+
fallback_text = RenderedTextExtractor.text_of(node)
|
|
218
|
+
yield(:text, node, fallback_text) if fallback_text && !fallback_text.empty?
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Named child attributes on SemxElement that store inline
|
|
223
|
+
# content (strong_child, em_child, fmt_xref, etc.). Walked
|
|
224
|
+
# when each_mixed_content yields nothing — fmt-* mirrors land
|
|
225
|
+
# here because the typed model promotes them out of mixed
|
|
226
|
+
# content into dedicated typed collections.
|
|
227
|
+
SEMX_NAMED_CHILDREN = %i[
|
|
228
|
+
stem_child strong em sup sub tt underline strike
|
|
229
|
+
preferred_child name_child title_child
|
|
230
|
+
verbal_definition_child definition_child
|
|
231
|
+
concept_child fn_child link_child
|
|
232
|
+
fmt_xref fmt_eref fmt_concept fmt_link fmt_stem fmt_preferred
|
|
233
|
+
fmt_definition fmt_termsource fmt_name fmt_fn_body
|
|
234
|
+
].freeze
|
|
235
|
+
|
|
236
|
+
def walk_semx_named_children(semx, &block)
|
|
237
|
+
SEMX_NAMED_CHILDREN.each do |attr|
|
|
238
|
+
next unless semx.class.method_defined?(attr)
|
|
239
|
+
val = semx.public_send(attr)
|
|
240
|
+
next if val.nil?
|
|
241
|
+
Array(val).each { |v| walk_node(v, &block) }
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Build a typed Sts inline container (Bold/Italic/Sub/Sup/
|
|
246
|
+
# Monospace) whose content is the recursively-walked children
|
|
247
|
+
# of `node`. Used for inline elements that wrap further inline
|
|
248
|
+
# content — most importantly <strong>max capacity (<stem>
|
|
249
|
+
# E_max </stem>)</strong>, where the stem must survive as an
|
|
250
|
+
# inline-formula INSIDE the bold (not as a sibling text drop).
|
|
251
|
+
def build_inline_container(node, klass)
|
|
252
|
+
children = build_inline_children(node)
|
|
253
|
+
klass.new do |container|
|
|
254
|
+
children.each { |child| attach_inline_child(container, child) }
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Routes a child to the matching typed-collection attribute on
|
|
259
|
+
# the container. NisoSts stores text on a different attribute
|
|
260
|
+
# per class (Paragraph#text, TbxIsoTml::Bold/Italic#value,
|
|
261
|
+
# everything else has #content). Sub/Sup only declare content,
|
|
262
|
+
# so non-string children degrade to their #to_s form.
|
|
263
|
+
def attach_inline_child(container, child)
|
|
264
|
+
case container
|
|
265
|
+
when ::Sts::NisoSts::Paragraph
|
|
266
|
+
attach_to_paragraph(container, child)
|
|
267
|
+
when ::Sts::TbxIsoTml::Bold, ::Sts::TbxIsoTml::Italic
|
|
268
|
+
attach_to_rich_inline(container, child)
|
|
269
|
+
else
|
|
270
|
+
container.content child.to_s
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def attach_to_rich_inline(container, child)
|
|
275
|
+
case child
|
|
276
|
+
when String then container.value child
|
|
277
|
+
when ::Sts::NisoSts::InlineFormula then container.value inline_formula_text(child)
|
|
278
|
+
when ::Sts::TbxIsoTml::Bold then container.bold child
|
|
279
|
+
when ::Sts::TbxIsoTml::Italic then container.italic child
|
|
280
|
+
when ::Sts::NisoSts::Sub then container.sub child
|
|
281
|
+
when ::Sts::NisoSts::Sup then container.sup child
|
|
282
|
+
when ::Sts::NisoSts::Monospace then container.monospace child
|
|
283
|
+
when ::Sts::TbxIsoTml::Xref then container.xref child
|
|
284
|
+
when ::Sts::NisoSts::ExtLink then container.ext_link child
|
|
285
|
+
else
|
|
286
|
+
container.value child.to_s
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# sts 0.6.0's TbxIsoTml::Bold/Italic don't declare an
|
|
291
|
+
# inline_formula collection, so a formula nested inside a
|
|
292
|
+
# <strong>/<em>wider container</strong> can't keep its MathML.
|
|
293
|
+
# Fall back to the formula's visible text — matches what a
|
|
294
|
+
# text-only reader sees in the source (e.g. "E_max" inside
|
|
295
|
+
# "<strong>max capacity (<stem>E_max</stem>)</strong>").
|
|
296
|
+
# Leading/trailing whitespace is preserved so "(<stem>v</stem>)"
|
|
297
|
+
# renders as "( v )" matching MN's text extraction.
|
|
298
|
+
def inline_formula_text(formula)
|
|
299
|
+
math = formula.math
|
|
300
|
+
return "" unless math
|
|
301
|
+
|
|
302
|
+
text = math.to_xml.to_s
|
|
303
|
+
.gsub(/<[^>]+>/, " ")
|
|
304
|
+
.gsub(/\s+/, " ")
|
|
305
|
+
text = " #{text.strip} " unless text.strip.empty?
|
|
306
|
+
text
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def attach_to_paragraph(container, child)
|
|
310
|
+
case child
|
|
311
|
+
when String then container.text child
|
|
312
|
+
when ::Sts::NisoSts::InlineFormula then container.inline_formula child
|
|
313
|
+
when ::Sts::TbxIsoTml::Bold then container.bold child
|
|
314
|
+
when ::Sts::TbxIsoTml::Italic then container.italic child
|
|
315
|
+
when ::Sts::NisoSts::Sub then container.sub child
|
|
316
|
+
when ::Sts::NisoSts::Sup then container.sup child
|
|
317
|
+
when ::Sts::NisoSts::Monospace then container.monospace child
|
|
318
|
+
when ::Sts::TbxIsoTml::Xref then container.xref child
|
|
319
|
+
when ::Sts::NisoSts::ExtLink then container.ext_link child
|
|
320
|
+
when ::Sts::NisoSts::Fn then container.fn child
|
|
321
|
+
else
|
|
322
|
+
container.text child.to_s
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Recursively walks node's children, returning an array of
|
|
327
|
+
# strings and Sts inline model instances (InlineFormula, Xref,
|
|
328
|
+
# ExtLink, Bold, Italic, Sub, Sup) in document order. Stems
|
|
329
|
+
# become InlineFormula via MathML passthrough; nested strong/
|
|
330
|
+
# em become nested Bold/Italic.
|
|
331
|
+
def build_inline_children(node)
|
|
332
|
+
return [] unless node.class.method_defined?(:each_mixed_content)
|
|
333
|
+
|
|
334
|
+
entries = []
|
|
335
|
+
current_text = nil
|
|
336
|
+
|
|
337
|
+
walk_inline(node) do |kind, child, payload|
|
|
338
|
+
case kind
|
|
339
|
+
when :text
|
|
340
|
+
current_text = (current_text || "") + payload
|
|
341
|
+
when :bold, :italic, :sub, :sup, :tt
|
|
342
|
+
if current_text
|
|
343
|
+
entries << clean_text(current_text)
|
|
344
|
+
current_text = nil
|
|
345
|
+
end
|
|
346
|
+
entries << payload
|
|
347
|
+
when :stem
|
|
348
|
+
formula = build_inline_formula(child)
|
|
349
|
+
next unless formula
|
|
350
|
+
|
|
351
|
+
if current_text
|
|
352
|
+
entries << clean_text(current_text)
|
|
353
|
+
current_text = nil
|
|
354
|
+
end
|
|
355
|
+
entries << formula
|
|
356
|
+
when :xref
|
|
357
|
+
x = build_xref(child)
|
|
358
|
+
next unless x
|
|
359
|
+
|
|
360
|
+
if current_text
|
|
361
|
+
entries << clean_text(current_text)
|
|
362
|
+
current_text = nil
|
|
363
|
+
end
|
|
364
|
+
entries << x
|
|
365
|
+
when :link
|
|
366
|
+
l = build_link(child)
|
|
367
|
+
next unless l
|
|
368
|
+
|
|
369
|
+
if current_text
|
|
370
|
+
entries << clean_text(current_text)
|
|
371
|
+
current_text = nil
|
|
372
|
+
end
|
|
373
|
+
entries << l
|
|
374
|
+
when :eref, :concept
|
|
375
|
+
current_text = (current_text || "") + payload if payload && !payload.empty?
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
entries << clean_text(current_text) if current_text
|
|
379
|
+
entries
|
|
380
|
+
end
|
|
381
|
+
# MathML passthrough: parse the stem's <math> into Mml::V3::Math
|
|
382
|
+
# and wrap it in an InlineFormula. Prefers the formatted mirror
|
|
383
|
+
# (fmt-stem) when one exists for this stem's id — the formatted
|
|
384
|
+
# version carries rendered MathML (European decimal commas,
|
|
385
|
+
# applied symbol rules) matching MN's HTML output.
|
|
386
|
+
def build_inline_formula(stem_node)
|
|
387
|
+
mirror = lookup_stem_mirror(stem_node)
|
|
388
|
+
source_for_math = mirror || stem_node
|
|
389
|
+
::Metanorma::Oiml::Sts::MathmlBuilder.inline_formula_from_stem(source_for_math)
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# Footnote bodies become a sibling <fn> element inside the
|
|
393
|
+
# paragraph. Each fn paragraph is run through paragraph_
|
|
394
|
+
# transformer; the result is flattened to text because
|
|
395
|
+
# Sts::TbxIsoTml::Fn#p is typed as NisoSts::Paragraph (uses
|
|
396
|
+
# :text, not :content) and the math roundtrip drops msub.
|
|
397
|
+
def build_fn(fn_element)
|
|
398
|
+
paragraphs = Array(fn_element.p).map.with_index do |p, i|
|
|
399
|
+
build_fn_paragraph_text(p, fn_element, first: i.zero?)
|
|
400
|
+
end
|
|
401
|
+
return nil if paragraphs.empty?
|
|
402
|
+
|
|
403
|
+
attrs = { id: fn_element.id, paragraph: paragraphs }
|
|
404
|
+
label = fn_element.reference if fn_element.is_a?(Metanorma::Document::Components::Inline::FnElement)
|
|
405
|
+
attrs[:label] = ::Sts::NisoSts::Label.new(content: [label]) if label && !label.to_s.empty?
|
|
406
|
+
::Sts::NisoSts::Fn.new(attrs)
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def build_fn_paragraph_text(mn_p, fn_element, first:)
|
|
410
|
+
text = RenderedTextExtractor.text_of(mn_p)
|
|
411
|
+
label = fn_element.reference if fn_element.is_a?(Metanorma::Document::Components::Inline::FnElement)
|
|
412
|
+
text = "#{label})#{text}" if first && label && !label.to_s.empty? && !text.empty?
|
|
413
|
+
::Sts::NisoSts::Paragraph.new(text: [text])
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# Builds a map of stem_id → FmtStemElement mirror by scanning
|
|
417
|
+
# the paragraph's descendant fmt-stem elements. Each fmt-stem's
|
|
418
|
+
# inner semx carries a `source` matching the original stem's id.
|
|
419
|
+
def build_stem_mirror_map(paragraph)
|
|
420
|
+
map = {}
|
|
421
|
+
return map unless paragraph.class.method_defined?(:each_mixed_content)
|
|
422
|
+
|
|
423
|
+
walk_all(paragraph) do |node|
|
|
424
|
+
next unless node.is_a?(Metanorma::Document::Components::Inline::FmtStemElement)
|
|
425
|
+
Array(node.semx).each do |semx|
|
|
426
|
+
source = semx.source if semx.class.method_defined?(:source)
|
|
427
|
+
map[source] = node if source && !source.empty?
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
map
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def lookup_stem_mirror(stem_node)
|
|
434
|
+
return nil unless stem_node.class.method_defined?(:id)
|
|
435
|
+
return nil unless stem_node.id
|
|
436
|
+
|
|
437
|
+
@stem_mirrors ? @stem_mirrors[stem_node.id] : nil
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# When a fmt-stem mirror carries plain text (no MathML — e.g.
|
|
441
|
+
# numbers with thousands separator: "3 000", "1 000"), MN
|
|
442
|
+
# renders them as text rather than math. Expose the mirror's
|
|
443
|
+
# text so the caller can emit it as a plain string instead
|
|
444
|
+
# of dropping the content.
|
|
445
|
+
def mirror_text(stem_node)
|
|
446
|
+
mirror = lookup_stem_mirror(stem_node)
|
|
447
|
+
return nil unless mirror
|
|
448
|
+
|
|
449
|
+
Array(mirror.semx).map do |semx|
|
|
450
|
+
next nil unless semx.is_a?(Metanorma::Document::Components::Inline::SemxElement)
|
|
451
|
+
semx.text
|
|
452
|
+
end.compact.join
|
|
453
|
+
rescue StandardError
|
|
454
|
+
nil
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
# Depth-first walk of every node under `root` (no dedup — we
|
|
458
|
+
# need to find every fmt-stem even when one is nested inside
|
|
459
|
+
# another's semx).
|
|
460
|
+
def walk_all(root, &block)
|
|
461
|
+
queue = [root]
|
|
462
|
+
until queue.empty?
|
|
463
|
+
node = queue.shift
|
|
464
|
+
next if node.nil?
|
|
465
|
+
if node.is_a?(String)
|
|
466
|
+
block.call(node)
|
|
467
|
+
next
|
|
468
|
+
end
|
|
469
|
+
block.call(node)
|
|
470
|
+
node.class.attributes.each do |attr_name, _|
|
|
471
|
+
next unless node.class.method_defined?(attr_name)
|
|
472
|
+
val = node.public_send(attr_name) rescue next
|
|
473
|
+
next if val.nil?
|
|
474
|
+
Array(val).each { |v| queue << v if v.is_a?(Lutaml::Model::Serializable) }
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def build_xref(node)
|
|
480
|
+
rid = nil
|
|
481
|
+
[:target, :refid, :to].each do |attr|
|
|
482
|
+
next unless node.class.method_defined?(attr)
|
|
483
|
+
val = node.public_send(attr)
|
|
484
|
+
rid = val if val.is_a?(String) && !val.empty?
|
|
485
|
+
break if rid
|
|
486
|
+
end
|
|
487
|
+
return nil unless rid
|
|
488
|
+
|
|
489
|
+
visible = xref_visible_text(node, rid)
|
|
490
|
+
ModelBuilder.xref(rid: rid, ref_type: ref_type_for(node), value: visible)
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
def xref_visible_text(node, rid)
|
|
494
|
+
explicit = text_of(node)
|
|
495
|
+
return explicit unless explicit.empty?
|
|
496
|
+
case rid
|
|
497
|
+
when /\Asec-(.+)\z/, /\Afig-(.+)\z/, /\Atable-(.+)\z/, /\Afn-(.+)\z/
|
|
498
|
+
Regexp.last_match(1)
|
|
499
|
+
else
|
|
500
|
+
rid
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
def ref_type_for(node)
|
|
505
|
+
style = node.style if node.class.method_defined?(:style)
|
|
506
|
+
style = style.to_s if style
|
|
507
|
+
style = @mirror_style if (style.nil? || style.empty?) && @mirror_style
|
|
508
|
+
case style
|
|
509
|
+
when "clause", "section" then "sec"
|
|
510
|
+
when "table" then "table"
|
|
511
|
+
when "figure", "fig" then "fig"
|
|
512
|
+
when "fn", "footnote" then "fn"
|
|
513
|
+
else "other"
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
def build_link(node)
|
|
518
|
+
href = nil
|
|
519
|
+
[:target, :href].each do |attr|
|
|
520
|
+
next unless node.class.method_defined?(attr)
|
|
521
|
+
val = node.public_send(attr)
|
|
522
|
+
href = val if val.is_a?(String) && !val.empty?
|
|
523
|
+
break if href
|
|
524
|
+
end
|
|
525
|
+
return nil unless href
|
|
526
|
+
|
|
527
|
+
visible = href.start_with?("mailto:") ? href.sub("mailto:", "") : link_text(node)
|
|
528
|
+
ModelBuilder.ext_link(xlink_href: href, content: [visible])
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
def link_text(node)
|
|
532
|
+
c = node.content if node.class.method_defined?(:content)
|
|
533
|
+
c = Array(c) unless c.is_a?(Array)
|
|
534
|
+
joined = c.map(&:to_s).join.strip
|
|
535
|
+
joined.empty? ? text_of(node) : joined
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def eref_text(node)
|
|
539
|
+
citeas = node.citeas if node.class.method_defined?(:citeas)
|
|
540
|
+
return citeas.to_s if citeas && !citeas.to_s.empty?
|
|
541
|
+
text_of(node)
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
def concept_text(node)
|
|
545
|
+
renderterm = Array(node.renderterm).first.to_s if node.class.method_defined?(:renderterm)
|
|
546
|
+
xref_node = Array(node.xref).first if node.class.method_defined?(:xref)
|
|
547
|
+
return renderterm unless xref_node
|
|
548
|
+
|
|
549
|
+
rid = xref_node.target if xref_node.class.method_defined?(:target)
|
|
550
|
+
autonum = source.term_autonum_for(rid) if rid
|
|
551
|
+
return "#{renderterm} (#{autonum})" if autonum && !autonum.empty?
|
|
552
|
+
"#{renderterm} (#{rid})"
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
def presentation_span?(node)
|
|
556
|
+
return false unless node.is_a?(Metanorma::Document::Components::Inline::SpanElement)
|
|
557
|
+
cls = node.class_attr
|
|
558
|
+
cls.is_a?(String) && %w[fmt-autonum-delim fmt-caption-delim citesec citefig citetbl stdpublisher stddocNumber stdyear].any? { |c| cls.include?(c) }
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def text_of(obj)
|
|
562
|
+
return obj.to_s if obj.is_a?(String)
|
|
563
|
+
return "" unless obj
|
|
564
|
+
RenderedTextExtractor.text_of(obj)
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
def clean_text(text)
|
|
568
|
+
text.to_s.gsub(/link:mailto:([^\s\[]+)/, '\1').gsub(/link:([^\s\[]+)/, '\1')
|
|
569
|
+
end
|
|
570
|
+
end
|
|
571
|
+
end
|
|
572
|
+
end
|
|
573
|
+
end
|
|
574
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module Metanorma
|
|
3
|
+
module Oiml
|
|
4
|
+
module Sts
|
|
5
|
+
module Transformer
|
|
6
|
+
class ReferenceTransformer < Base
|
|
7
|
+
def transform_section(ref_section)
|
|
8
|
+
bibitems = extract_bibitems(ref_section)
|
|
9
|
+
title_text = extract_title(ref_section)
|
|
10
|
+
|
|
11
|
+
paragraphs = intro_paragraphs(ref_section)
|
|
12
|
+
label = section_transformer.extract_autonum(ref_section)
|
|
13
|
+
normative = paragraphs.any? || !label.nil?
|
|
14
|
+
# Bibliography entries are numbered ([1], [2], …); normative
|
|
15
|
+
# reference entries are not (ISO/Metanorma convention).
|
|
16
|
+
refs = bibitems.map.with_index { |b, i| transform_bibitem(b, normative ? nil : i + 1) }.compact
|
|
17
|
+
|
|
18
|
+
unless normative
|
|
19
|
+
# Back-matter bibliography: a bare ref-list.
|
|
20
|
+
return ModelBuilder.ref_list(content_type: "bibliography", title: title_text, ref: refs)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# In-body normative references: a numbered <sec> whose intro
|
|
24
|
+
# paragraphs precede the (untitled) ref-list.
|
|
25
|
+
ref_list = ModelBuilder.ref_list(content_type: "normative-refs", title: nil, ref: refs)
|
|
26
|
+
attrs = {}
|
|
27
|
+
attrs[:id] = ref_section.id if ref_section.class.method_defined?(:id) && ref_section.id
|
|
28
|
+
attrs[:label] = ::Sts::NisoSts::Label.new(content: [label]) if label
|
|
29
|
+
attrs[:title] = ::Sts::NisoSts::Title.new(content: [title_text]) if title_text && !title_text.empty?
|
|
30
|
+
attrs[:paragraphs] = paragraphs if paragraphs.any?
|
|
31
|
+
attrs[:ref_list] = [ref_list] if ref_list
|
|
32
|
+
::Sts::NisoSts::Section.new(attrs)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def transform_bibitem(bibitem, ordinal = nil)
|
|
36
|
+
identifier = extract_docidentifier(bibitem)
|
|
37
|
+
formattedref = extract_formattedref(bibitem)
|
|
38
|
+
label_text = ordinal ? "[#{ordinal}]" : nil
|
|
39
|
+
|
|
40
|
+
content_parts = []
|
|
41
|
+
content_parts << identifier if identifier
|
|
42
|
+
content_parts << formattedref if formattedref
|
|
43
|
+
|
|
44
|
+
ModelBuilder.ref(
|
|
45
|
+
label: label_text,
|
|
46
|
+
mixed_citation: content_parts.join(", "),
|
|
47
|
+
std: build_std(identifier, formattedref)
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# Every bibliographic <std> carries a <title> (OIML X 999
|
|
54
|
+
# 5.4); the publication year rides inside the <std-ref>
|
|
55
|
+
# designation, which is type="dated" when the identifier
|
|
56
|
+
# embeds a year ("…:2012", "…-2022") and "undated" otherwise
|
|
57
|
+
# (NISO STS idiom — <std> has no <pub-date> in the base tag
|
|
58
|
+
# suite).
|
|
59
|
+
def build_std(identifier, formattedref)
|
|
60
|
+
return nil unless identifier || formattedref
|
|
61
|
+
|
|
62
|
+
type = identifier.to_s.match?(/(:\d{4}|-\d{4}\b)/) ? "dated" : "undated"
|
|
63
|
+
attrs = { type: type }
|
|
64
|
+
attrs[:std_ref] = ::Sts::NisoSts::StandardRef.new(type: type, value: identifier) if identifier
|
|
65
|
+
attrs[:title] = formattedref.to_s if formattedref
|
|
66
|
+
::Sts::NisoSts::ReferenceStandard.new(attrs)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def extract_bibitems(ref_section)
|
|
70
|
+
refs = ref_section.class.method_defined?(:references) ? ref_section.references : nil
|
|
71
|
+
return Array(refs) if refs
|
|
72
|
+
[]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Boilerplate paragraphs of an in-body references section
|
|
76
|
+
# ("The following documents are referred to ...").
|
|
77
|
+
def intro_paragraphs(ref_section)
|
|
78
|
+
return [] unless ref_section.class.method_defined?(:p)
|
|
79
|
+
|
|
80
|
+
Array(ref_section.p).map { |p| paragraph_transformer.transform(p) }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def extract_title(ref_section)
|
|
84
|
+
return nil unless ref_section.class.method_defined?(:title) && ref_section.title
|
|
85
|
+
RenderedTextExtractor.text_of(ref_section.title)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def extract_docidentifier(bibitem)
|
|
89
|
+
return nil unless bibitem.class.method_defined?(:docidentifier)
|
|
90
|
+
ids = Array(bibitem.docidentifier)
|
|
91
|
+
primary = ids.find { |i| !(i.class.method_defined?(:type) && i.type) } || ids.first
|
|
92
|
+
return nil unless primary
|
|
93
|
+
val = primary.class.method_defined?(:id) ? primary.id : primary.to_s
|
|
94
|
+
val.to_s.strip
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def extract_formattedref(bibitem)
|
|
98
|
+
fr = bibitem.class.method_defined?(:formatted_ref) ? bibitem.formatted_ref : nil
|
|
99
|
+
return nil unless fr
|
|
100
|
+
text = RenderedTextExtractor.text_of(fr)
|
|
101
|
+
text.empty? ? nil : text
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|