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,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Metanorma
|
|
4
|
+
module Oiml
|
|
5
|
+
module Sts
|
|
6
|
+
module Transformer
|
|
7
|
+
# Extracts plain text from typed model objects for non-math content
|
|
8
|
+
# (titles, labels, list items, xref visible text). Stems are NEVER
|
|
9
|
+
# text-extracted — the paragraph transformer routes them through
|
|
10
|
+
# MathML passthrough via InlineFormula + Mml::V3::Math.
|
|
11
|
+
class RenderedTextExtractor
|
|
12
|
+
STEM_TYPES = [
|
|
13
|
+
Metanorma::Document::Components::Inline::StemInlineElement,
|
|
14
|
+
Metanorma::Document::Components::Inline::FmtStemElement,
|
|
15
|
+
Metanorma::Document::Components::Inline::AsciimathElement,
|
|
16
|
+
Metanorma::Document::Components::Inline::MathElement,
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
WALKABLE_TYPES = [
|
|
20
|
+
Metanorma::Document::Components::Paragraphs::ParagraphBlock,
|
|
21
|
+
Metanorma::Document::Components::Inline::SemxElement,
|
|
22
|
+
Metanorma::Document::Components::Inline::StrongRawElement,
|
|
23
|
+
Metanorma::Document::Components::Inline::EmRawElement,
|
|
24
|
+
Metanorma::Document::Components::Inline::SpanElement,
|
|
25
|
+
Metanorma::Document::Components::Inline::FmtPreferredElement,
|
|
26
|
+
Metanorma::Document::Components::Inline::FmtXrefLabelElement,
|
|
27
|
+
Metanorma::Document::Components::Inline::FmtConceptElement,
|
|
28
|
+
Metanorma::Document::Components::Inline::ConceptElement,
|
|
29
|
+
Metanorma::StandardDocument::Sections::ContentSection,
|
|
30
|
+
].freeze
|
|
31
|
+
|
|
32
|
+
def self.text_of(node)
|
|
33
|
+
return "" if node.nil?
|
|
34
|
+
return node if node.is_a?(String)
|
|
35
|
+
return "" if stem_type?(node)
|
|
36
|
+
return "" unless walkable?(node)
|
|
37
|
+
|
|
38
|
+
walker = new
|
|
39
|
+
walker.collect(node)
|
|
40
|
+
walker.to_text
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.stem_type?(node)
|
|
44
|
+
STEM_TYPES.any? { |t| node.is_a?(t) }
|
|
45
|
+
end
|
|
46
|
+
public_class_method :stem_type?
|
|
47
|
+
|
|
48
|
+
def self.walkable?(node)
|
|
49
|
+
WALKABLE_TYPES.any? { |t| node.is_a?(t) } ||
|
|
50
|
+
node.class.method_defined?(:each_mixed_content)
|
|
51
|
+
end
|
|
52
|
+
public_class_method :walkable?
|
|
53
|
+
|
|
54
|
+
def initialize
|
|
55
|
+
@parts = []
|
|
56
|
+
@seen = {}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def to_text
|
|
60
|
+
@parts.join.strip
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def append_text(text)
|
|
64
|
+
@parts << text
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def collect_node_public(child)
|
|
68
|
+
collect_node(child)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def collect(node)
|
|
72
|
+
return if node.nil? || @seen.key?(node.object_id)
|
|
73
|
+
@seen[node.object_id] = true
|
|
74
|
+
|
|
75
|
+
if node.is_a?(String)
|
|
76
|
+
@parts << node
|
|
77
|
+
return
|
|
78
|
+
end
|
|
79
|
+
return unless self.class.walkable?(node)
|
|
80
|
+
|
|
81
|
+
collect_content_mapped_text?(node) ||
|
|
82
|
+
node.each_mixed_content { |child| collect_node(child) }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
# Elements like NameWithIdElement map their whole content to a
|
|
88
|
+
# plain `text` attribute (no mixed_content), so walking yields
|
|
89
|
+
# nothing. SpanElement declares BOTH mixed_content and a text
|
|
90
|
+
# mapping — only take the attribute shortcut when there is no
|
|
91
|
+
# mixed content to walk.
|
|
92
|
+
def collect_content_mapped_text?(node)
|
|
93
|
+
return false unless node.class.method_defined?(:text)
|
|
94
|
+
return false unless node.class.method_defined?(:mixed_content?) && !node.mixed_content?
|
|
95
|
+
|
|
96
|
+
text = node.text
|
|
97
|
+
return false unless text.is_a?(String) && !text.empty?
|
|
98
|
+
|
|
99
|
+
@parts << text
|
|
100
|
+
true
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def collect_node(child)
|
|
104
|
+
if child.is_a?(String)
|
|
105
|
+
@parts << child
|
|
106
|
+
return
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
case child
|
|
110
|
+
when *STEM_TYPES
|
|
111
|
+
# Math content is handled by the paragraph transformer via
|
|
112
|
+
# MathML passthrough. Text extraction is intentionally a no-op.
|
|
113
|
+
nil
|
|
114
|
+
when Metanorma::Document::Components::Inline::XrefElement
|
|
115
|
+
@parts << xref_text(child)
|
|
116
|
+
when Metanorma::Document::Components::Inline::SemxElement
|
|
117
|
+
collect(child)
|
|
118
|
+
collect_semx_inlines(child)
|
|
119
|
+
when Metanorma::Document::Components::Inline::StrongRawElement,
|
|
120
|
+
Metanorma::Document::Components::Inline::EmRawElement,
|
|
121
|
+
Metanorma::Document::Components::Inline::SpanElement
|
|
122
|
+
collect(child)
|
|
123
|
+
else
|
|
124
|
+
collect(child)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
SEMX_CHILD_ATTRS = %i[
|
|
129
|
+
stem_child strong em sup sub tt underline strike
|
|
130
|
+
preferred_child name_child title_child
|
|
131
|
+
verbal_definition_child definition_child
|
|
132
|
+
concept_child fn_child link_child
|
|
133
|
+
].freeze
|
|
134
|
+
|
|
135
|
+
def collect_semx_inlines(semx)
|
|
136
|
+
SEMX_CHILD_ATTRS.each do |attr|
|
|
137
|
+
next unless semx.class.method_defined?(attr)
|
|
138
|
+
val = semx.public_send(attr)
|
|
139
|
+
next if val.nil?
|
|
140
|
+
Array(val).each { |v| collect_node(v) }
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def xref_text(xref)
|
|
145
|
+
txt = array_or_string_to_s(xref.text)
|
|
146
|
+
return txt unless txt.nil? || txt.empty?
|
|
147
|
+
|
|
148
|
+
rid = xref.target if xref.class.method_defined?(:target)
|
|
149
|
+
return "" unless rid.is_a?(String) && !rid.empty?
|
|
150
|
+
|
|
151
|
+
case rid
|
|
152
|
+
when /\Asec-(.+)\z/, /\Afig-(.+)\z/, /\Atable-(.+)\z/, /\Afn-(.+)\z/
|
|
153
|
+
Regexp.last_match(1)
|
|
154
|
+
else
|
|
155
|
+
rid
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def array_or_string_to_s(val)
|
|
160
|
+
return val if val.is_a?(String)
|
|
161
|
+
return "" unless val.is_a?(Array)
|
|
162
|
+
val.map(&:to_s).reject(&:empty?).join.strip
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Metanorma
|
|
4
|
+
module Oiml
|
|
5
|
+
module Sts
|
|
6
|
+
module Transformer
|
|
7
|
+
class SectionTransformer < Base
|
|
8
|
+
def transform(source_clause)
|
|
9
|
+
title_text = extract_title(source_clause)
|
|
10
|
+
id_val = source_clause.id if source_clause.is_a?(Lutaml::Model::Serializable) && source_clause.class.method_defined?(:id) && source_clause.id
|
|
11
|
+
|
|
12
|
+
content = dispatcher.dispatch_section_blocks(source_clause)
|
|
13
|
+
|
|
14
|
+
# Dispatch nested clauses for any section type carrying a
|
|
15
|
+
# clause collection (IsoClauseSection AND IsoAnnexSection —
|
|
16
|
+
# annex subsections like the Examples annex's entries).
|
|
17
|
+
nested_clauses = []
|
|
18
|
+
if source_clause.is_a?(Lutaml::Model::Serializable) && source_clause.class.method_defined?(:clause)
|
|
19
|
+
nested_clauses = Array(source_clause.clause).map { |sub| transform(sub) }.compact
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Dispatch terms sections (a top-level <terms> carries its
|
|
23
|
+
# entries directly in .term; a clause's nested terms section
|
|
24
|
+
# appears in .terms). Each nested terms-section (e.g.
|
|
25
|
+
# "3.1 General definitions") is preserved as its own Sec so
|
|
26
|
+
# its heading renders for parity; only the top-level section
|
|
27
|
+
# whose .term collection we're flattening skips the wrapper.
|
|
28
|
+
if source_clause.class.method_defined?(:term) && Array(source_clause.term).any?
|
|
29
|
+
Array(source_clause.term).each do |term|
|
|
30
|
+
term_sec = term_transformer.transform(term)
|
|
31
|
+
nested_clauses << term_sec if term_sec
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
if source_clause.class.method_defined?(:terms)
|
|
35
|
+
Array(source_clause.terms).each do |terms_section|
|
|
36
|
+
terms_sec = transform_terms_section(terms_section)
|
|
37
|
+
nested_clauses << terms_sec if terms_sec
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
attrs = {}
|
|
42
|
+
attrs[:id] = id_val if id_val
|
|
43
|
+
attrs[:title] = ::Sts::NisoSts::Title.new(content: [title_text]) if title_text && !title_text.empty?
|
|
44
|
+
autonum = extract_autonum(source_clause)
|
|
45
|
+
attrs[:label] = ::Sts::NisoSts::Label.new(content: [autonum]) if autonum
|
|
46
|
+
|
|
47
|
+
paragraphs, lists, figs, tables, notes, examples, formulas, def_lists, preformats, sub_secs = [], [], [], [], [], [], [], [], [], []
|
|
48
|
+
content.each do |item|
|
|
49
|
+
case item
|
|
50
|
+
when ::Sts::NisoSts::Paragraph then paragraphs << item
|
|
51
|
+
when ::Sts::NisoSts::List then lists << item
|
|
52
|
+
when ::Sts::NisoSts::Figure then figs << item
|
|
53
|
+
when ::Sts::TbxIsoTml::TableWrap then tables << item
|
|
54
|
+
when ::Sts::NisoSts::NonNormativeNote then notes << item
|
|
55
|
+
when ::Sts::NisoSts::NonNormativeExample then examples << item
|
|
56
|
+
when ::Sts::NisoSts::DisplayFormula then formulas << item
|
|
57
|
+
when ::Sts::NisoSts::DefList then def_lists << item
|
|
58
|
+
when ::Sts::NisoSts::Preformat then preformats << item
|
|
59
|
+
when ::Sts::NisoSts::Section then sub_secs << item
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
nested_clauses.concat(sub_secs)
|
|
63
|
+
|
|
64
|
+
attrs[:paragraphs] = paragraphs if paragraphs.any?
|
|
65
|
+
attrs[:list] = lists if lists.any?
|
|
66
|
+
attrs[:fig] = figs if figs.any?
|
|
67
|
+
attrs[:table_wrap] = tables if tables.any?
|
|
68
|
+
attrs[:non_normative_note] = notes if notes.any?
|
|
69
|
+
attrs[:non_normative_example] = examples if examples.any?
|
|
70
|
+
attrs[:disp_formula] = formulas if formulas.any?
|
|
71
|
+
attrs[:def_list] = def_lists if def_lists.any?
|
|
72
|
+
attrs[:preformat] = preformats if preformats.any?
|
|
73
|
+
attrs[:sec] = nested_clauses if nested_clauses.any?
|
|
74
|
+
|
|
75
|
+
::Sts::NisoSts::Section.new(attrs)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Build a Sec for a nested terms-section (e.g. "3.1 General
|
|
79
|
+
# definitions") that wraps its term entries. The wrapper's
|
|
80
|
+
# title and autonum are emitted so the renderer produces the
|
|
81
|
+
# "3.1 General definitions" heading that appears in MN HTML.
|
|
82
|
+
def transform_terms_section(terms_section)
|
|
83
|
+
title_text = extract_title(terms_section)
|
|
84
|
+
autonum = extract_autonum(terms_section)
|
|
85
|
+
|
|
86
|
+
nested = Array(terms_section.term).map { |term| term_transformer.transform(term) }.compact
|
|
87
|
+
|
|
88
|
+
attrs = {}
|
|
89
|
+
attrs[:id] = terms_section.id if terms_section.class.method_defined?(:id) && terms_section.id
|
|
90
|
+
attrs[:title] = ::Sts::NisoSts::Title.new(content: [title_text]) if title_text && !title_text.empty?
|
|
91
|
+
attrs[:label] = ::Sts::NisoSts::Label.new(content: [autonum]) if autonum
|
|
92
|
+
attrs[:sec] = nested if nested.any?
|
|
93
|
+
return nil unless title_text || autonum || nested.any?
|
|
94
|
+
|
|
95
|
+
::Sts::NisoSts::Section.new(attrs)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Section number from the presentation XML: the autonum
|
|
99
|
+
# attribute (annexes, term notes) or the fmt-title/fmt-name
|
|
100
|
+
# caption-label's autonum semx ("1", "8.1", "3.1"). The delim
|
|
101
|
+
# between number and title is a <tab/>, which text extraction
|
|
102
|
+
# drops, so the caption-label span must be isolated —
|
|
103
|
+
# splitting the whole fmt-title text would glue number and
|
|
104
|
+
# title together ("1Scope").
|
|
105
|
+
def extract_autonum(source_clause)
|
|
106
|
+
if source_clause.class.method_defined?(:autonum) && source_clause.autonum
|
|
107
|
+
return source_clause.autonum.to_s
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
title_wrapper = %i[fmt_title fmt_name].filter_map do |method|
|
|
111
|
+
source_clause.public_send(method) if source_clause.class.method_defined?(method)
|
|
112
|
+
end.first
|
|
113
|
+
title_wrapper = Array(title_wrapper).first
|
|
114
|
+
return nil unless title_wrapper
|
|
115
|
+
|
|
116
|
+
caption_label_text(title_wrapper)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
def extract_title(source_clause)
|
|
122
|
+
return nil unless source_clause.class.method_defined?(:title) && source_clause.title
|
|
123
|
+
title = source_clause.title
|
|
124
|
+
return extract_with_stems(title) if title_is_mixed_with_stems?(title)
|
|
125
|
+
RenderedTextExtractor.text_of(title)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Title text where stem elements contribute their visible
|
|
129
|
+
# AsciiMath tokens (e.g. "D_(\"min\")" → " D min ") so the
|
|
130
|
+
# parity check sees content where it would otherwise see "()".
|
|
131
|
+
# Source text from asciimath matches what Nokogiri's text
|
|
132
|
+
# extraction produces from MN HTML after the document-level
|
|
133
|
+
# normaliser lowercases both; case sensitivity is therefore
|
|
134
|
+
# not an issue here, and we never parse MathML XML.
|
|
135
|
+
def extract_with_stems(title)
|
|
136
|
+
parts = collect_title_parts(title, [])
|
|
137
|
+
parts.join.strip
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def title_is_mixed_with_stems?(title)
|
|
141
|
+
title.class.method_defined?(:each_mixed_content)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def collect_title_parts(node, parts)
|
|
145
|
+
return parts unless node.is_a?(Lutaml::Model::Serializable)
|
|
146
|
+
return parts unless node.class.method_defined?(:each_mixed_content)
|
|
147
|
+
|
|
148
|
+
node.each_mixed_content do |child|
|
|
149
|
+
if child.is_a?(String)
|
|
150
|
+
parts << child
|
|
151
|
+
elsif stem_node?(child)
|
|
152
|
+
parts << stem_visible_text(child)
|
|
153
|
+
elsif xref_node?(child)
|
|
154
|
+
parts << xref_visible_text(child)
|
|
155
|
+
elsif child.is_a?(Lutaml::Model::Serializable)
|
|
156
|
+
collect_title_parts(child, parts)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
parts
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
STEM_NODE_CLASSES = [
|
|
163
|
+
"Metanorma::Document::Components::Inline::StemInlineElement",
|
|
164
|
+
"Metanorma::Document::Components::Inline::FmtStemElement",
|
|
165
|
+
"Metanorma::Document::Components::TextElements::StemElement",
|
|
166
|
+
"Metanorma::Document::Components::Inline::AsciimathElement",
|
|
167
|
+
"Metanorma::Document::Components::Inline::MathElement",
|
|
168
|
+
].freeze
|
|
169
|
+
|
|
170
|
+
def stem_node?(node)
|
|
171
|
+
STEM_NODE_CLASSES.include?(node.class.name)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def xref_node?(node)
|
|
175
|
+
node.is_a?(Metanorma::Document::Components::Inline::XrefElement)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def stem_visible_text(stem)
|
|
179
|
+
# The typed stem has an `asciimath` attribute mapping to
|
|
180
|
+
# Asciiml (a typed model with a `:value` string). For
|
|
181
|
+
# Semx wrappers the asciimath sits on the inner stem; for
|
|
182
|
+
# inline stems on the stem itself.
|
|
183
|
+
asciimath_value(asciimath_of(stem)).then do |raw|
|
|
184
|
+
text = raw.to_s.tr("_", " ").gsub(/[()"\\]/, "").strip
|
|
185
|
+
text.empty? ? "" : " #{text} "
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def asciimath_of(stem)
|
|
190
|
+
stem.is_a?(Metanorma::Document::Components::TextElements::StemElement) ||
|
|
191
|
+
stem.is_a?(Metanorma::Document::Components::Inline::StemInlineElement) ||
|
|
192
|
+
stem.is_a?(Metanorma::Document::Components::Inline::FmtStemElement) ? stem.asciimath : nil
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def asciimath_value(asciiml)
|
|
196
|
+
return nil unless asciiml
|
|
197
|
+
asciiml.is_a?(Metanorma::Document::Components::TextElements::Asciiml) ? asciiml.value : asciiml.to_s
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def xref_visible_text(xref)
|
|
201
|
+
txt = Array(xref.text).join
|
|
202
|
+
txt = xref.target.to_s if txt.empty? && xref.target
|
|
203
|
+
case txt
|
|
204
|
+
when /\Asec-(.+)\z/, /\Afig-(.+)\z/, /\Atable-(.+)\z/, /\Afn-(.+)\z/
|
|
205
|
+
"#{Regexp.last_match(1)} "
|
|
206
|
+
else
|
|
207
|
+
txt
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Text of the <span class="fmt-caption-label"> inside a
|
|
212
|
+
# fmt-title — the bare section number.
|
|
213
|
+
def caption_label_text(fmt_title)
|
|
214
|
+
return nil unless fmt_title.class.method_defined?(:each_mixed_content)
|
|
215
|
+
|
|
216
|
+
label_span = nil
|
|
217
|
+
fmt_title.each_mixed_content do |child|
|
|
218
|
+
if child.is_a?(Metanorma::Document::Components::Inline::SpanElement) &&
|
|
219
|
+
child.class_attr == "fmt-caption-label"
|
|
220
|
+
label_span = child
|
|
221
|
+
break
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
return nil unless label_span
|
|
225
|
+
|
|
226
|
+
text = RenderedTextExtractor.text_of(label_span).strip
|
|
227
|
+
text.empty? ? nil : text
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "metanorma/document"
|
|
4
|
+
require "metanorma/oiml/document"
|
|
5
|
+
|
|
6
|
+
module Metanorma
|
|
7
|
+
module Oiml
|
|
8
|
+
module Sts
|
|
9
|
+
module Transformer
|
|
10
|
+
class SourceDocument
|
|
11
|
+
attr_reader :typed_root
|
|
12
|
+
|
|
13
|
+
def self.parse(input)
|
|
14
|
+
xml_string = input.class.method_defined?(:read) ? input.read : input.to_s
|
|
15
|
+
typed = Metanorma::Oiml::Document::Root.from_xml(xml_string)
|
|
16
|
+
new(typed)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Reader entry point for the metanorma-core document_transformers
|
|
20
|
+
# contract (+reader.from_xml(String) -> model+); an alias of {.parse}.
|
|
21
|
+
def self.from_xml(input)
|
|
22
|
+
parse(input)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def initialize(typed_root)
|
|
26
|
+
@typed_root = typed_root
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def bibdata
|
|
30
|
+
typed_root.bibdata
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def language
|
|
34
|
+
return "en" unless bibdata
|
|
35
|
+
langs = bibdata.language rescue nil
|
|
36
|
+
return "en" unless langs
|
|
37
|
+
lang = langs.is_a?(Array) ? langs.first : langs
|
|
38
|
+
val = lang.is_a?(String) ? lang : lang.value.to_s
|
|
39
|
+
val.strip.match?(/\A[a-z]{2}(-[A-Z]{2})?\z/) ? val.strip : "en"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def docidentifier
|
|
43
|
+
return nil unless bibdata
|
|
44
|
+
ids = bibdata.doc_identifier rescue nil
|
|
45
|
+
return nil unless ids
|
|
46
|
+
Array(ids).each do |id_obj|
|
|
47
|
+
next if id_obj.type
|
|
48
|
+
return text_of(id_obj)
|
|
49
|
+
end
|
|
50
|
+
text_of(Array(ids).first)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def text_of(obj)
|
|
54
|
+
return obj.to_s if obj.is_a?(String)
|
|
55
|
+
return "" unless obj
|
|
56
|
+
val = (obj.value rescue nil) || (obj.content rescue nil)
|
|
57
|
+
return "" unless val
|
|
58
|
+
Array(val).map { |v| v.is_a?(String) ? v : text_of(v) }.join.strip
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def title(type: nil)
|
|
62
|
+
return nil unless bibdata
|
|
63
|
+
titles = bibdata.titles rescue nil
|
|
64
|
+
return nil unless titles
|
|
65
|
+
titles = Array(titles)
|
|
66
|
+
t = titles.find { |x| type.nil? || (x.type == type rescue false) } || titles.first
|
|
67
|
+
text_of(t)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def formatted_title
|
|
71
|
+
return title unless bibdata
|
|
72
|
+
# The composed model #title can raise on documents whose front
|
|
73
|
+
# lacks the title structure it expects; fall back to the
|
|
74
|
+
# generic wrapper title rather than failing the transform.
|
|
75
|
+
iso_title = (bibdata.title rescue nil)
|
|
76
|
+
return title unless iso_title
|
|
77
|
+
main = abstract_title_value(iso_title.title_main)
|
|
78
|
+
part_prefix = abstract_title_value(iso_title.title_part_prefix)
|
|
79
|
+
part = abstract_title_value(iso_title.title_part)
|
|
80
|
+
segments = [main]
|
|
81
|
+
if part_prefix && !part_prefix.empty?
|
|
82
|
+
segments << (part && !part.empty? ? "#{part_prefix}:#{part}" : part_prefix)
|
|
83
|
+
elsif part && !part.empty?
|
|
84
|
+
segments << part
|
|
85
|
+
end
|
|
86
|
+
segments.reject! { |s| s.nil? || s.empty? }
|
|
87
|
+
return title if segments.empty?
|
|
88
|
+
segments.join(" — ")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def abstract_title_value(abstract)
|
|
92
|
+
return nil if abstract.nil?
|
|
93
|
+
val = abstract.value rescue nil
|
|
94
|
+
val.nil? ? nil : val.to_s.strip
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def copyright_year
|
|
98
|
+
return nil unless bibdata
|
|
99
|
+
cps = bibdata.copyright rescue nil
|
|
100
|
+
return nil unless cps
|
|
101
|
+
cp = Array(cps).first
|
|
102
|
+
return nil unless cp
|
|
103
|
+
from = cp.from rescue nil
|
|
104
|
+
return text_of(from) unless from.nil?
|
|
105
|
+
nil
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def copyright_holder
|
|
109
|
+
return nil unless bibdata
|
|
110
|
+
cps = bibdata.copyright rescue nil
|
|
111
|
+
return nil unless cps
|
|
112
|
+
cp = Array(cps).first
|
|
113
|
+
return nil unless cp
|
|
114
|
+
owner = cp.owner rescue nil
|
|
115
|
+
return nil unless owner
|
|
116
|
+
org = Array(owner.organization).first rescue nil
|
|
117
|
+
return nil unless org
|
|
118
|
+
text_of(org.name) rescue nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def publisher
|
|
122
|
+
copyright_holder
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def pub_date
|
|
126
|
+
copyright_year
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def foreword
|
|
130
|
+
typed_root.preface&.foreword
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def introduction
|
|
134
|
+
typed_root.preface&.introduction
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Top-level sections of every kind — <clause>, <terms>,
|
|
138
|
+
# <definitions>, <references> (normative references) — in
|
|
139
|
+
# document order. The XML's physical order differs (references
|
|
140
|
+
# trails the clauses); displayorder carries the intended one.
|
|
141
|
+
SECTION_COLLECTIONS = %i[clause terms definitions references].freeze
|
|
142
|
+
|
|
143
|
+
def sections
|
|
144
|
+
secs = typed_root.sections
|
|
145
|
+
return [] unless secs
|
|
146
|
+
|
|
147
|
+
SECTION_COLLECTIONS.flat_map { |attr| Array(secs.public_send(attr)) }
|
|
148
|
+
.sort_by do |s|
|
|
149
|
+
s.class.method_defined?(:displayorder) && s.displayorder ? s.displayorder : Float::INFINITY
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def annexes
|
|
154
|
+
Array(typed_root.annex)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def bibliography
|
|
158
|
+
bib = typed_root.bibliography
|
|
159
|
+
return [] unless bib
|
|
160
|
+
Array(bib.references)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def front?
|
|
164
|
+
!foreword.nil? || !introduction.nil?
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def has_metadata?
|
|
168
|
+
!docidentifier.nil?
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def has_back?
|
|
172
|
+
annexes.any? || bibliography.any?
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def term_autonum_for(term_id)
|
|
176
|
+
@term_autonum_cache ||= build_term_autonum_cache
|
|
177
|
+
@term_autonum_cache[term_id]
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
private
|
|
181
|
+
|
|
182
|
+
def build_term_autonum_cache
|
|
183
|
+
cache = {}
|
|
184
|
+
walk_for_terms(typed_root) do |term|
|
|
185
|
+
next unless term.is_a?(Metanorma::IsoDocument::Terms::IsoTerm)
|
|
186
|
+
autonum = extract_term_autonum(term)
|
|
187
|
+
next unless autonum && !autonum.empty?
|
|
188
|
+
cache[term.id] = autonum if term.id
|
|
189
|
+
anchor = term.anchor if term.class.method_defined?(:anchor)
|
|
190
|
+
cache[anchor] = autonum if anchor && !cache.key?(anchor)
|
|
191
|
+
end
|
|
192
|
+
cache
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def walk_for_terms(node, seen = {}, &blk)
|
|
196
|
+
return unless node.is_a?(Object) && !node.is_a?(String)
|
|
197
|
+
return if seen[node.object_id]
|
|
198
|
+
seen[node.object_id] = true
|
|
199
|
+
blk.call(node)
|
|
200
|
+
attrs = node.class.instance_variable_get(:@attributes) rescue nil
|
|
201
|
+
return unless attrs
|
|
202
|
+
attrs.each do |name, _|
|
|
203
|
+
next unless node.class.method_defined?(name)
|
|
204
|
+
val = node.public_send(name) rescue next
|
|
205
|
+
next if val.nil?
|
|
206
|
+
Array(val).each { |v| walk_for_terms(v, seen, &blk) if v.is_a?(Object) && !v.is_a?(String) && !v.is_a?(Array) }
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def extract_term_autonum(term)
|
|
211
|
+
label_arr = Array(term.fmt_xref_label)
|
|
212
|
+
label = label_arr.first
|
|
213
|
+
return nil unless label
|
|
214
|
+
return nil unless label.class.method_defined?(:each_mixed_content)
|
|
215
|
+
parts = []
|
|
216
|
+
label.each_mixed_content do |n|
|
|
217
|
+
if n.is_a?(String) && !n.strip.empty?
|
|
218
|
+
parts << n.strip
|
|
219
|
+
else
|
|
220
|
+
cn = n.class.name.split("::").last
|
|
221
|
+
txt = semx_text(n) if cn == "SemxElement"
|
|
222
|
+
txt = span_text(n) if cn == "SpanElement"
|
|
223
|
+
parts << txt if txt && !txt.empty?
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
parts.join.strip
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def semx_text(semx)
|
|
230
|
+
return "" unless semx.class.method_defined?(:each_mixed_content)
|
|
231
|
+
parts = []
|
|
232
|
+
semx.each_mixed_content { |n| parts << n if n.is_a?(String) }
|
|
233
|
+
parts.join
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def span_text(span)
|
|
237
|
+
return "" unless span.class.method_defined?(:each_mixed_content)
|
|
238
|
+
parts = []
|
|
239
|
+
span.each_mixed_content { |n| parts << n if n.is_a?(String) }
|
|
240
|
+
parts.join
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Metanorma
|
|
4
|
+
module Oiml
|
|
5
|
+
module Sts
|
|
6
|
+
module Transformer
|
|
7
|
+
# Adapter conforming to the metanorma-core
|
|
8
|
+
# +Processor#document_transformers+ contract: +.new(model, options)+,
|
|
9
|
+
# where +model+ is the {SourceDocument} produced by
|
|
10
|
+
# {SourceDocument.from_xml}; +#transform+ returns a target responding to
|
|
11
|
+
# +#to_xml+.
|
|
12
|
+
#
|
|
13
|
+
# The adapter is its own target: its +#to_xml+ runs the full
|
|
14
|
+
# {DocumentTransformer#transform_to_xml} pipeline (model build plus the
|
|
15
|
+
# namespace / processing-meta / lang post-processing on the serialised
|
|
16
|
+
# string), so the pipeline output matches the standalone
|
|
17
|
+
# {Metanorma::Oiml::Sts::Transformer.convert}. Returning the raw
|
|
18
|
+
# +#transform+ model instead would drop that post-processing.
|
|
19
|
+
class Standard
|
|
20
|
+
def initialize(model, options = {})
|
|
21
|
+
@model = model
|
|
22
|
+
@options = options
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def transform
|
|
26
|
+
@document_transformer = DocumentTransformer.new(Context.new(@model))
|
|
27
|
+
self
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_xml(**_options)
|
|
31
|
+
@document_transformer.transform_to_xml(@model)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|