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.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.adoc +291 -0
  4. data/data/themes/oiml/assets/oiml-logo-full-dark.svg +1 -0
  5. data/data/themes/oiml/assets/oiml-logo-full-light.svg +1 -0
  6. data/data/themes/oiml/assets/oiml-logo-icon-dark.svg +1 -0
  7. data/data/themes/oiml/assets/oiml-logo-icon-light.svg +1 -0
  8. data/data/themes/oiml/assets/oiml-logo.svg +1 -0
  9. data/data/themes/oiml/theme.yaml +56 -0
  10. data/exe/oiml-sts +10 -0
  11. data/lib/metanorma/oiml/document/root.rb +29 -0
  12. data/lib/metanorma/oiml/document.rb +35 -0
  13. data/lib/metanorma/oiml/html/renderer.rb +14 -0
  14. data/lib/metanorma/oiml/html.rb +14 -0
  15. data/lib/metanorma/oiml/sts/cli.rb +89 -0
  16. data/lib/metanorma/oiml/sts/html_renderer/assets/metanorma-icon-dark.svg +1 -0
  17. data/lib/metanorma/oiml/sts/html_renderer/assets/metanorma-icon-light.svg +1 -0
  18. data/lib/metanorma/oiml/sts/html_renderer/assets/oiml-logo-dark.svg +1 -0
  19. data/lib/metanorma/oiml/sts/html_renderer/assets/oiml-logo.svg +1 -0
  20. data/lib/metanorma/oiml/sts/html_renderer/assets/oiml-smart-logo-dark.svg +1 -0
  21. data/lib/metanorma/oiml/sts/html_renderer/assets/oiml-smart-logo.svg +1 -0
  22. data/lib/metanorma/oiml/sts/html_renderer/assets/page.js +145 -0
  23. data/lib/metanorma/oiml/sts/html_renderer/assets/theme.css +509 -0
  24. data/lib/metanorma/oiml/sts/html_renderer/ruby.rb +1071 -0
  25. data/lib/metanorma/oiml/sts/html_renderer/templates/_element.html.liquid +1 -0
  26. data/lib/metanorma/oiml/sts/html_renderer/templates/_footer.html.liquid +21 -0
  27. data/lib/metanorma/oiml/sts/html_renderer/templates/_hero.html.liquid +9 -0
  28. data/lib/metanorma/oiml/sts/html_renderer/templates/_img.html.liquid +1 -0
  29. data/lib/metanorma/oiml/sts/html_renderer/templates/_link.html.liquid +1 -0
  30. data/lib/metanorma/oiml/sts/html_renderer/templates/_meta_header.html.liquid +1 -0
  31. data/lib/metanorma/oiml/sts/html_renderer/templates/document.html.liquid +43 -0
  32. data/lib/metanorma/oiml/sts/html_renderer.rb +19 -0
  33. data/lib/metanorma/oiml/sts/mathml_builder.rb +93 -0
  34. data/lib/metanorma/oiml/sts/parity_validator/normaliser.rb +95 -0
  35. data/lib/metanorma/oiml/sts/parity_validator/report.rb +137 -0
  36. data/lib/metanorma/oiml/sts/parity_validator.rb +23 -0
  37. data/lib/metanorma/oiml/sts/transformer/back_transformer.rb +55 -0
  38. data/lib/metanorma/oiml/sts/transformer/base.rb +71 -0
  39. data/lib/metanorma/oiml/sts/transformer/block_dispatcher.rb +145 -0
  40. data/lib/metanorma/oiml/sts/transformer/body_transformer.rb +23 -0
  41. data/lib/metanorma/oiml/sts/transformer/context.rb +27 -0
  42. data/lib/metanorma/oiml/sts/transformer/dl_transformer.rb +49 -0
  43. data/lib/metanorma/oiml/sts/transformer/document_transformer.rb +65 -0
  44. data/lib/metanorma/oiml/sts/transformer/figure_transformer.rb +49 -0
  45. data/lib/metanorma/oiml/sts/transformer/footnote_collector.rb +33 -0
  46. data/lib/metanorma/oiml/sts/transformer/formula_transformer.rb +45 -0
  47. data/lib/metanorma/oiml/sts/transformer/front_transformer.rb +84 -0
  48. data/lib/metanorma/oiml/sts/transformer/id_generator.rb +39 -0
  49. data/lib/metanorma/oiml/sts/transformer/inline_transformer.rb +14 -0
  50. data/lib/metanorma/oiml/sts/transformer/list_transformer.rb +82 -0
  51. data/lib/metanorma/oiml/sts/transformer/model_builder.rb +322 -0
  52. data/lib/metanorma/oiml/sts/transformer/note_transformer.rb +103 -0
  53. data/lib/metanorma/oiml/sts/transformer/paragraph_transformer.rb +574 -0
  54. data/lib/metanorma/oiml/sts/transformer/reference_transformer.rb +107 -0
  55. data/lib/metanorma/oiml/sts/transformer/rendered_text_extractor.rb +168 -0
  56. data/lib/metanorma/oiml/sts/transformer/section_transformer.rb +233 -0
  57. data/lib/metanorma/oiml/sts/transformer/source_document.rb +246 -0
  58. data/lib/metanorma/oiml/sts/transformer/standard.rb +37 -0
  59. data/lib/metanorma/oiml/sts/transformer/table_transformer.rb +290 -0
  60. data/lib/metanorma/oiml/sts/transformer/term_transformer.rb +236 -0
  61. data/lib/metanorma/oiml/sts/transformer.rb +47 -0
  62. data/lib/metanorma/oiml/sts/validation_report.rb +43 -0
  63. data/lib/metanorma/oiml/sts/validator.rb +126 -0
  64. data/lib/metanorma/oiml/sts/version.rb +9 -0
  65. data/lib/metanorma/oiml/sts.rb +84 -0
  66. data/lib/metanorma-oiml.rb +10 -0
  67. data/schematron/oiml-sts.sch +70 -0
  68. metadata +221 -0
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Oiml
5
+ module Sts
6
+ module Transformer
7
+ class BlockDispatcher < Base
8
+ HANDLERS = {
9
+ "IsoClauseSection" => :dispatch_via_section,
10
+ "ClauseSection" => :dispatch_via_section,
11
+ "IsoForewordSection" => :dispatch_via_section,
12
+ "IsoIntroductionSection" => :dispatch_via_section,
13
+ "IsoTermsSection" => :dispatch_via_section,
14
+ "DefinitionSection" => :dispatch_via_section,
15
+ "IsoAnnexSection" => :dispatch_via_section,
16
+ "ParagraphBlock" => :dispatch_via_paragraph,
17
+ "ParagraphWithFootnote" => :dispatch_via_paragraph,
18
+ "UnorderedList" => :dispatch_via_list,
19
+ "OrderedList" => :dispatch_via_list,
20
+ "DefinitionList" => :dispatch_via_dl,
21
+ "TableBlock" => :dispatch_via_table,
22
+ "FigureBlock" => :dispatch_via_figure,
23
+ "FormulaBlock" => :dispatch_via_formula,
24
+ "NoteBlock" => :dispatch_via_note,
25
+ "ExampleBlock" => :dispatch_via_note,
26
+ "AdmonitionBlock" => :dispatch_via_note,
27
+ "QuoteBlock" => :dispatch_via_note,
28
+ "SourcecodeBlock" => :dispatch_via_sourcecode,
29
+ "StandardReferencesSection" => :dispatch_via_references,
30
+ "BibliographySection" => :dispatch_via_references
31
+ }.freeze
32
+
33
+ PRESENTATION_CLASSES = %w[
34
+ FmtTitleElement FmtXrefLabelElement FmtNameElement FmtStemElement
35
+ FmtFnLabelElement FmtConceptElement SemxElement LocalizedString
36
+ LocalizedStrings FmtVal AsciiMath BrElement TabElement Bookmark
37
+ SpanElement
38
+ ].freeze
39
+
40
+ def dispatch(source_obj)
41
+ return nil if source_obj.nil? || source_obj.is_a?(String)
42
+ class_name = source_obj.class.name&.split("::")&.last
43
+ return nil if PRESENTATION_CLASSES.include?(class_name)
44
+
45
+ handler = HANDLERS[class_name]
46
+ return nil unless handler
47
+
48
+ send(handler, source_obj)
49
+ end
50
+
51
+ SECTION_CLASS_NAMES = %w[
52
+ IsoClauseSection ClauseSection IsoForewordSection
53
+ IsoIntroductionSection IsoTermsSection DefinitionSection
54
+ IsoAnnexSection AnnexSection
55
+ ].freeze
56
+
57
+ # Typed attribute → handler method mapping for ContentSection.
58
+ # These are NOT yielded by each_mixed_content; we iterate them
59
+ # explicitly to dispatch tables, lists, notes, etc.
60
+ TYPED_ATTR_DISPATCH = {
61
+ paragraphs: :dispatch_via_paragraph,
62
+ unordered_lists: :dispatch_via_list,
63
+ ordered_lists: :dispatch_via_list,
64
+ tables: :dispatch_via_table,
65
+ figures: :dispatch_via_figure,
66
+ formulas: :dispatch_via_formula,
67
+ notes: :dispatch_via_note,
68
+ examples: :dispatch_via_note,
69
+ admonitions: :dispatch_via_note,
70
+ sourcecode_blocks: :dispatch_via_sourcecode,
71
+ quote_blocks: :dispatch_via_note,
72
+ definition_lists: :dispatch_via_dl,
73
+ definitions: :dispatch_via_section,
74
+ }.freeze
75
+
76
+ def dispatch_section_blocks(section)
77
+ results = []
78
+
79
+ # Walk typed collection attributes for all content types.
80
+ # each_mixed_content is NOT used because it overlaps with
81
+ # typed attributes (causing duplication) and misses some
82
+ # content types (tables, lists, notes).
83
+ TYPED_ATTR_DISPATCH.each do |attr, handler|
84
+ next unless section.class.method_defined?(attr)
85
+ collection = section.public_send(attr)
86
+ Array(collection).each do |item|
87
+ result = send(handler, item)
88
+ results << result if result
89
+ dispatch_paragraph_siblings(item, results)
90
+ end
91
+ end
92
+
93
+ results
94
+ end
95
+
96
+ # MN's typed model attaches "block siblings" as attributes on
97
+ # the preceding block (ParagraphBlock.note holds notes that
98
+ # semantically follow the paragraph; TableBlock.note holds
99
+ # table notes, which render right after the table). Walk those
100
+ # sibling collections and dispatch each, preserving document
101
+ # order. Restricted to ParagraphBlock and TableBlock to avoid
102
+ # firing for lists/etc. that have their own note sub-collections
103
+ # with different semantics.
104
+ def dispatch_paragraph_siblings(item, results)
105
+ return unless paragraph_block?(item) || table_block?(item)
106
+
107
+ SIBLING_ATTRS.each do |attr, handler|
108
+ next unless item.class.method_defined?(attr)
109
+ Array(item.public_send(attr)).each do |sibling|
110
+ result = send(handler, sibling)
111
+ results << result if result
112
+ end
113
+ end
114
+ end
115
+
116
+ def paragraph_block?(item)
117
+ item.is_a?(Metanorma::Document::Components::Paragraphs::ParagraphBlock)
118
+ end
119
+
120
+ def table_block?(item)
121
+ item.is_a?(Metanorma::Document::Components::Tables::TableBlock)
122
+ end
123
+
124
+ SIBLING_ATTRS = {
125
+ note: :dispatch_via_note,
126
+ example: :dispatch_via_note,
127
+ }.freeze
128
+
129
+ private
130
+
131
+ def dispatch_via_section(obj); section_transformer.transform(obj); end
132
+ def dispatch_via_paragraph(obj); paragraph_transformer.transform(obj); end
133
+ def dispatch_via_list(obj); list_transformer.transform(obj); end
134
+ def dispatch_via_dl(obj); dl_transformer.transform(obj); end
135
+ def dispatch_via_table(obj); table_transformer.transform(obj); end
136
+ def dispatch_via_figure(obj); figure_transformer.transform(obj); end
137
+ def dispatch_via_formula(obj); formula_transformer.transform(obj); end
138
+ def dispatch_via_note(obj); note_transformer.transform(obj); end
139
+ def dispatch_via_sourcecode(obj); note_transformer.transform_preformat(obj); end
140
+ def dispatch_via_references(obj); reference_transformer.transform_section(obj); end
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+ module Metanorma
3
+ module Oiml
4
+ module Sts
5
+ module Transformer
6
+ class BodyTransformer < Base
7
+ def transform(source)
8
+ sections = []
9
+
10
+ sections += front_transformer.preface_sections(source) if source.front?
11
+
12
+ source.sections.each do |clause|
13
+ sec = dispatcher.dispatch(clause)
14
+ sections << sec if sec
15
+ end
16
+
17
+ ModelBuilder.body(sec: sections)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Oiml
5
+ module Sts
6
+ module Transformer
7
+ class Context
8
+ attr_reader :source, :id_generator, :footnote_collector
9
+
10
+ def initialize(source)
11
+ @source = source
12
+ @id_generator = IdGenerator.new
13
+ @footnote_collector = FootnoteCollector.new
14
+ end
15
+
16
+ def language
17
+ source.language || "en"
18
+ end
19
+
20
+ def docidentifier
21
+ source.docidentifier
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Oiml
5
+ module Sts
6
+ module Transformer
7
+ # Converts MN `<dl>`/`<dt>`/`<dd>` to STS `<def-list>`/`<def-item>`.
8
+ # Follows mnconvert's pattern (mn2xml.xsl lines 5327-5528): each
9
+ # dt/dd pair becomes a def-item with term + def children. Notes
10
+ # inside dl are extracted as siblings after the def-list.
11
+ class DlTransformer < Base
12
+ def transform(source_dl)
13
+ def_items = pair_dt_dd(source_dl)
14
+ ModelBuilder.def_list(id: source_dl.id, def_items: def_items)
15
+ end
16
+
17
+ private
18
+
19
+ def pair_dt_dd(source_dl)
20
+ dts = Array(source_dl.dt)
21
+ dds = Array(source_dl.dd)
22
+ dts.each_with_index.map do |dt, i|
23
+ build_def_item(dt, dds[i])
24
+ end
25
+ end
26
+
27
+ def build_def_item(dt, dd)
28
+ attrs = {}
29
+ attrs[:id] = dt.id if dt.class.method_defined?(:id) && dt.id
30
+
31
+ term_text = RenderedTextExtractor.text_of(dt)
32
+ term = term_text.nil? || term_text.empty? ? nil : ModelBuilder.term(content: [term_text])
33
+
34
+ defn = dd ? build_def(dd) : nil
35
+
36
+ attrs[:term] = term if term
37
+ attrs[:defn] = defn if defn
38
+ ModelBuilder.def_item(**attrs)
39
+ end
40
+
41
+ def build_def(dd)
42
+ paragraphs = Array(dd.p).map { |p| paragraph_transformer.transform(p) }
43
+ ModelBuilder.def(paragraph: paragraphs)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Oiml
5
+ module Sts
6
+ module Transformer
7
+ class DocumentTransformer < Base
8
+ def transform(source)
9
+ front_model = build_front(source)
10
+ body_model = build_body(source)
11
+ back_model = build_back(source)
12
+
13
+ std = ModelBuilder.standard(
14
+ lang: context.language,
15
+ dtd_version: "1.2",
16
+ front: front_model,
17
+ body: body_model,
18
+ back: back_model
19
+ )
20
+ std
21
+ end
22
+
23
+ def transform_to_xml(source)
24
+ std = transform(source)
25
+ xml = std.to_xml
26
+ inject_namespaces(xml)
27
+ .then { |x| inject_processing_meta(x) }
28
+ .then { |x| fix_lang_attribute(x) }
29
+ end
30
+
31
+ private
32
+
33
+ def build_front(source)
34
+ return nil unless source.has_metadata?
35
+ front_transformer.transform(source)
36
+ end
37
+
38
+ def build_body(source)
39
+ BodyTransformer.new(context).transform(source)
40
+ end
41
+
42
+ def build_back(source)
43
+ return nil unless source.has_back?
44
+ BackTransformer.new(context).transform(source)
45
+ end
46
+
47
+ def inject_namespaces(xml)
48
+ return xml if xml.include?("xmlns:xlink")
49
+ xml.sub("<standard", '<standard xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:mml="http://www.w3.org/1998/Math/MathML"')
50
+ end
51
+
52
+ def inject_processing_meta(xml)
53
+ return xml if xml.include?("<processing-meta")
54
+ meta = '<processing-meta tagset-family="sts" base-tagset="interchange" table-model="xhtml" mathml="MathML 3.0" terminology-model="tbx"/>'
55
+ xml.sub(/(<standard[^>]*>)/, "\\1\n #{meta}")
56
+ end
57
+
58
+ def fix_lang_attribute(xml)
59
+ xml.gsub(/\slang="([a-z-]+)"/, ' xml:lang="\\1"')
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+ module Metanorma
3
+ module Oiml
4
+ module Sts
5
+ module Transformer
6
+ class FigureTransformer < Base
7
+ def transform(source_fig)
8
+ attrs = {}
9
+ attrs[:id] = source_fig.id if source_fig.class.method_defined?(:id) && source_fig.id
10
+
11
+ label_text = extract_label(source_fig)
12
+ caption_text = extract_title(source_fig)
13
+ graphic = extract_graphic(source_fig)
14
+
15
+ ModelBuilder.fig(id: attrs[:id], label: label_text, caption: ModelBuilder.caption(title: caption_text), graphic: graphic)
16
+ end
17
+
18
+ private
19
+
20
+ # "Figure 1" from the presentation XML autonum attribute —
21
+ # same pattern as TableTransformer's label.
22
+ def extract_label(source_fig)
23
+ return nil unless source_fig.class.method_defined?(:autonum) && source_fig.autonum
24
+
25
+ "Figure #{source_fig.autonum}"
26
+ end
27
+
28
+ def extract_title(source_fig)
29
+ return nil unless source_fig.class.method_defined?(:name) && source_fig.name
30
+
31
+ text = RenderedTextExtractor.text_of(source_fig.name).strip
32
+ text.empty? ? nil : text
33
+ end
34
+
35
+ def extract_graphic(source_fig)
36
+ return nil unless source_fig.class.method_defined?(:image)
37
+ images = Array(source_fig.image)
38
+ return nil if images.empty?
39
+ img = images.first
40
+ href = img.src if img.class.method_defined?(:src)
41
+ href ||= img.href if img.class.method_defined?(:href)
42
+ return nil unless href
43
+ ModelBuilder.graphic(xlink_href: href)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Oiml
5
+ module Sts
6
+ module Transformer
7
+ # Collects footnotes during conversion so they can be flushed to the
8
+ # right location in the STS output.
9
+ class FootnoteCollector
10
+ def initialize
11
+ @seen = {}
12
+ @counter = 0
13
+ end
14
+
15
+ def register(source_id)
16
+ return @seen[source_id] if @seen.key?(source_id)
17
+
18
+ @counter += 1
19
+ @seen[source_id] = "fn_#{@counter}"
20
+ end
21
+
22
+ def known?(source_id)
23
+ @seen.key?(source_id)
24
+ end
25
+
26
+ def size
27
+ @seen.size
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+ module Metanorma
3
+ module Oiml
4
+ module Sts
5
+ module Transformer
6
+ class FormulaTransformer < Base
7
+ def transform(source_formula)
8
+ id = source_formula.id if source_formula.class.method_defined?(:id) && source_formula.id
9
+ label = extract_label(source_formula)
10
+ math = extract_math(source_formula)
11
+ ModelBuilder.disp_formula(id: id, label: label, math: math)
12
+ end
13
+
14
+ private
15
+
16
+ def extract_label(source_formula)
17
+ return nil unless source_formula.class.method_defined?(:fmt_name)
18
+ fmt_name = Array(source_formula.fmt_name).first
19
+ return nil unless fmt_name
20
+ RenderedTextExtractor.text_of(fmt_name)
21
+ end
22
+
23
+ # MathML passthrough: extract <math> from the formula's stem and
24
+ # parse into Mml::V3::Math. Delegates to MathmlBuilder so the
25
+ # math extraction logic lives in one place. Zero Nokogiri.
26
+ def extract_math(source_formula)
27
+ stem = first_stem(source_formula)
28
+ return nil unless stem
29
+
30
+ ::Metanorma::Oiml::Sts::MathmlBuilder.math_from_stem(stem)
31
+ end
32
+
33
+ def first_stem(source_formula)
34
+ if source_formula.class.method_defined?(:stem)
35
+ Array(source_formula.stem).first
36
+ elsif source_formula.class.method_defined?(:formula)
37
+ formula_el = Array(source_formula.formula).first
38
+ Array(formula_el.stem).first if formula_el
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+ module Metanorma
3
+ module Oiml
4
+ module Sts
5
+ module Transformer
6
+ class FrontTransformer < Base
7
+ def transform(source)
8
+ std_meta = build_std_meta(source)
9
+ ModelBuilder.front(std_meta: std_meta)
10
+ end
11
+
12
+ def preface_sections(source)
13
+ sections = []
14
+ sections += build_boilerplate(source)
15
+ sections += build_foreword(source) if source.foreword
16
+ sections += build_introduction(source) if source.introduction
17
+ sections
18
+ end
19
+
20
+ private
21
+
22
+ def build_std_meta(source)
23
+ title = source.formatted_title || source.title(type: "main") || source.title
24
+ ModelBuilder.std_meta(
25
+ doc_identifier: source.docidentifier,
26
+ title: title,
27
+ pub_date: source.pub_date,
28
+ permissions: ModelBuilder.permissions(
29
+ holder: source.copyright_holder || source.publisher || "OIML",
30
+ year: source.copyright_year || source.pub_date
31
+ ),
32
+ custom_meta_group: build_custom_meta(source)
33
+ )
34
+ end
35
+
36
+ def build_custom_meta(source)
37
+ series = source.docidentifier.to_s.match(/OIML\s+([A-Z])\s/) { |m| m[1] }
38
+ return nil unless series
39
+ ModelBuilder.custom_meta_group(name: "oiml-doc-series", value: series)
40
+ end
41
+
42
+ def build_boilerplate(source)
43
+ typed = source.typed_root
44
+ bp = typed.boilerplate
45
+ return [] unless bp
46
+
47
+ sections = []
48
+ Array(bp.copyright_statement).each do |cs|
49
+ paragraphs = []
50
+ cs.each_mixed_content do |inner|
51
+ next if inner.is_a?(String)
52
+ next unless inner.class.method_defined?(:paragraphs)
53
+ Array(inner.paragraphs).each { |p| paragraphs << paragraph_transformer.transform(p) }
54
+ end
55
+ next if paragraphs.empty?
56
+ sections << ModelBuilder.sec(title: "Copyright", paragraph: paragraphs)
57
+ end
58
+ Array(bp.feedback_statement).each do |fs|
59
+ paragraphs = []
60
+ fs.each_mixed_content do |inner|
61
+ next if inner.is_a?(String)
62
+ next unless inner.class.method_defined?(:paragraphs)
63
+ Array(inner.paragraphs).each { |p| paragraphs << paragraph_transformer.transform(p) }
64
+ end
65
+ next if paragraphs.empty?
66
+ sections << ModelBuilder.sec(title: "Feedback", paragraph: paragraphs)
67
+ end
68
+ sections
69
+ end
70
+
71
+ def build_foreword(source)
72
+ content = dispatcher.dispatch_section_blocks(source.foreword)
73
+ [ModelBuilder.sec(title: "Foreword", paragraph: content)]
74
+ end
75
+
76
+ def build_introduction(source)
77
+ content = dispatcher.dispatch_section_blocks(source.introduction)
78
+ [ModelBuilder.sec(title: "Introduction", paragraph: content)]
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Oiml
5
+ module Sts
6
+ module Transformer
7
+ class IdGenerator
8
+ def initialize
9
+ @registry = {}
10
+ @counter = 0
11
+ end
12
+
13
+ def register(source_id, sts_id)
14
+ @registry[source_id] = sts_id
15
+ end
16
+
17
+ def remap(source_id)
18
+ @registry[source_id]
19
+ end
20
+
21
+ def id_for(obj, prefix: "sec")
22
+ return nil unless obj
23
+ source_id = obj.id if obj.is_a?(Lutaml::Model::Serializable) && obj.class.method_defined?(:id)
24
+ source_id ||= nil
25
+
26
+ if source_id && !source_id.empty?
27
+ return @registry[source_id] if @registry.key?(source_id)
28
+ register(source_id, source_id)
29
+ return source_id
30
+ end
31
+
32
+ @counter += 1
33
+ "#{prefix}_#{@counter}"
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+ module Metanorma
3
+ module Oiml
4
+ module Sts
5
+ module Transformer
6
+ class InlineTransformer < Base
7
+ def transform(source_node)
8
+ paragraph_transformer.transform(source_node)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+ module Metanorma
3
+ module Oiml
4
+ module Sts
5
+ module Transformer
6
+ class ListTransformer < Base
7
+ def transform(source_list)
8
+ list_type = source_list.is_a?(Metanorma::Document::Components::Lists::OrderedList) ? "order" : "bullet"
9
+ items = Array(source_list.listitem).map { |li| build_list_item(li) }
10
+ ModelBuilder.list(list_type: list_type, list_item: items)
11
+ end
12
+
13
+ def transform_def_list(source_dl)
14
+ items = Array(source_dl.dt).zip(Array(source_dl.dd)).map do |dt, dd|
15
+ build_def_item(dt, dd)
16
+ end.compact
17
+ ::Sts::NisoSts::DefList.new(def_item: items)
18
+ end
19
+
20
+ private
21
+
22
+ def build_list_item(li)
23
+ nested_lists = build_nested_lists(li)
24
+
25
+ ::Sts::NisoSts::ListItem.new do |item|
26
+ label = item_label(li)
27
+ item.label ::Sts::NisoSts::Label.new(content: [label]) if label
28
+ Array(li.paragraphs).each { |p| item.p paragraph_transformer.transform(p) }
29
+ nested_lists.each { |l| item.list l }
30
+ end
31
+ end
32
+
33
+ # The item's marker from the presentation XML (its fmt-name /
34
+ # autonum, e.g. "—" or "1."), nil when absent.
35
+ def item_label(li)
36
+ return nil unless li.class.method_defined?(:fmt_name) && li.fmt_name
37
+
38
+ text = RenderedTextExtractor.text_of(li.fmt_name).strip
39
+ text.empty? ? nil : text
40
+ end
41
+
42
+ # Recurses into nested <ul>/<ol> children of a list item.
43
+ # MN stores them as typed collection attrs on ListItem.
44
+ def build_nested_lists(li)
45
+ results = []
46
+ [:unordered_lists, :ordered_lists].each do |attr|
47
+ next unless li.class.method_defined?(attr)
48
+ Array(li.public_send(attr)).each do |nested|
49
+ results << transform(nested)
50
+ end
51
+ end
52
+ results
53
+ end
54
+
55
+ def build_def_item(dt, dd)
56
+ term_text = RenderedTextExtractor.text_of(dt)
57
+ desc_text = extract_dd_text(dd)
58
+ return nil unless term_text || desc_text
59
+
60
+ attrs = {}
61
+ attrs[:term] = ::Sts::NisoSts::Term.new(text: [term_text]) if term_text && !term_text.empty?
62
+ if desc_text && !desc_text.empty?
63
+ attrs[:def] = ::Sts::NisoSts::Def.new(
64
+ paragraph: [::Sts::NisoSts::Paragraph.new(content: [desc_text])]
65
+ )
66
+ end
67
+ ::Sts::NisoSts::DefItem.new(attrs)
68
+ end
69
+
70
+ def extract_dd_text(dd)
71
+ return nil unless dd
72
+ ps = dd.p if dd.is_a?(Lutaml::Model::Serializable) && dd.class.method_defined?(:p)
73
+ ps = Array(ps)
74
+ return RenderedTextExtractor.text_of(dd) if ps.empty?
75
+ texts = ps.map { |p| RenderedTextExtractor.text_of(p) }.reject(&:empty?)
76
+ texts.empty? ? nil : texts.join(" ")
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end