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,322 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Oiml
5
+ module Sts
6
+ module Transformer
7
+ # Factory methods for building sts-ruby model instances.
8
+ # Zero Nokogiri — all models are constructed via lutaml-model.
9
+ module ModelBuilder
10
+ module_function
11
+
12
+ def standard(lang:, dtd_version: "1.2", front: nil, body: nil, back: nil)
13
+ ::Sts::NisoSts::Standard.new(
14
+ lang: lang,
15
+ dtd_version: dtd_version
16
+ ).tap do |std|
17
+ std.front = front if front
18
+ std.body = body if body
19
+ std.back = back if back
20
+ end
21
+ end
22
+
23
+ def front(std_meta: nil)
24
+ ::Sts::NisoSts::Front.new.tap { |f| f.std_meta = std_meta if std_meta }
25
+ end
26
+
27
+ def body(sec: [])
28
+ ::Sts::NisoSts::Body.new(sec: Array(sec))
29
+ end
30
+
31
+ def back(app_group: nil, ref_list: [])
32
+ attrs = {}
33
+ attrs[:ref_list] = Array(ref_list) if ref_list&.any?
34
+ ::Sts::NisoSts::Back.new(attrs).tap do |b|
35
+ b.app_group = app_group if app_group
36
+ end
37
+ end
38
+
39
+ def app_group(app: [])
40
+ ::Sts::NisoSts::AppGroup.new(app: Array(app))
41
+ end
42
+
43
+ def sec(id: nil, label: nil, title: nil, content: [], paragraph: [], sec: [],
44
+ list: [], table_wrap: [], fig: [], non_normative_note: [],
45
+ non_normative_example: [])
46
+ all_content = Array(content) + Array(paragraph)
47
+ distributed = distribute_sec_content(all_content)
48
+
49
+ attrs = {}
50
+ attrs[:id] = id if id
51
+ attrs[:title] = ::Sts::NisoSts::Title.new(content: [title]) if title
52
+ attrs[:paragraphs] = distributed[:paragraph] if distributed[:paragraph].any?
53
+ attrs[:sec] = Array(sec) if sec&.any?
54
+ attrs[:list] = (Array(list) + distributed[:list]) if (list&.any? || distributed[:list].any?)
55
+ attrs[:table_wrap] = (Array(table_wrap) + distributed[:table_wrap]) if (table_wrap&.any? || distributed[:table_wrap].any?)
56
+ attrs[:fig] = (Array(fig) + distributed[:fig]) if (fig&.any? || distributed[:fig].any?)
57
+ attrs[:non_normative_note] = (Array(non_normative_note) + distributed[:note]) if (non_normative_note&.any? || distributed[:note].any?)
58
+ attrs[:non_normative_example] = (Array(non_normative_example) + distributed[:example]) if (non_normative_example&.any? || distributed[:example].any?)
59
+ ::Sts::NisoSts::Section.new(attrs)
60
+ end
61
+
62
+ def distribute_sec_content(content)
63
+ result = Hash.new { |h, k| h[k] = [] }
64
+ content.each do |item|
65
+ case item
66
+ when ::Sts::NisoSts::Paragraph then result[:paragraph] << item
67
+ when ::Sts::NisoSts::List then result[:list] << item
68
+ when ::Sts::NisoSts::Figure then result[:fig] << item
69
+ when ::Sts::TbxIsoTml::TableWrap then result[:table_wrap] << item
70
+ when ::Sts::NisoSts::NonNormativeNote then result[:note] << item
71
+ when ::Sts::NisoSts::NonNormativeExample then result[:example] << item
72
+ when ::Sts::NisoSts::DisplayFormula then result[:formula] << item
73
+ when ::Sts::NisoSts::DefList then result[:def_list] << item
74
+ end
75
+ end
76
+ result
77
+ end
78
+
79
+ def paragraph(id: nil)
80
+ ::Sts::NisoSts::Paragraph.new.tap do |p|
81
+ p.id = id if id
82
+ yield p if block_given?
83
+ end
84
+ end
85
+
86
+ def list(list_type: "bullet", list_item: [])
87
+ ::Sts::NisoSts::List.new(list_type: list_type, list_item: Array(list_item))
88
+ end
89
+
90
+ def def_list(id: nil, def_items: [])
91
+ items = Array(def_items)
92
+ ::Sts::NisoSts::DefList.new.tap do |dl|
93
+ dl.id = id if id
94
+ dl.def_item = items if items.any?
95
+ end
96
+ end
97
+
98
+ def def_item(id: nil, term: nil, defn: nil)
99
+ ::Sts::NisoSts::DefItem.new.tap do |di|
100
+ di.id = id if id
101
+ di.term = term if term
102
+ di.definition = defn if defn
103
+ end
104
+ end
105
+ def term(content: [])
106
+ ::Sts::NisoSts::Term.new(text: Array(content))
107
+ end
108
+
109
+ def def(paragraph: [])
110
+ ::Sts::NisoSts::Def.new(paragraph: Array(paragraph))
111
+ end
112
+
113
+ def list_item(paragraph: [])
114
+ ::Sts::NisoSts::ListItem.new(p: Array(paragraph))
115
+ end
116
+
117
+ def table_wrap(id: nil, caption: nil, table: nil)
118
+ ::Sts::TbxIsoTml::TableWrap.new.tap do |tw|
119
+ tw.id = id if id
120
+ tw.caption = caption if caption
121
+ tw.table = [table] if table
122
+ end
123
+ end
124
+
125
+ def table(thead: nil, tbody: nil)
126
+ ::Sts::TbxIsoTml::Table.new.tap do |t|
127
+ t.thead = thead if thead
128
+ t.tbody = tbody if tbody
129
+ end
130
+ end
131
+
132
+ def thead(tr: [])
133
+ ::Sts::TbxIsoTml::Thead.new(tr: Array(tr))
134
+ end
135
+
136
+ def tbody(tr: [])
137
+ ::Sts::TbxIsoTml::Tbody.new(tr: Array(tr))
138
+ end
139
+
140
+ def tr(th: [], td: [])
141
+ attrs = {}
142
+ attrs[:th] = Array(th) if th&.any?
143
+ attrs[:td] = Array(td) if td&.any?
144
+ ::Sts::TbxIsoTml::Tr.new(attrs)
145
+ end
146
+
147
+ def th(content: nil)
148
+ ::Sts::TbxIsoTml::Th.new.tap { |c| c.content = Array(content) if content }
149
+ end
150
+
151
+ def td(content: nil)
152
+ ::Sts::TbxIsoTml::Td.new.tap { |c| c.content = Array(content) if content }
153
+ end
154
+
155
+ def fig(id: nil, label: nil, caption: nil, graphic: nil)
156
+ ::Sts::NisoSts::Figure.new.tap do |f|
157
+ f.id = id if id
158
+ f.label = ::Sts::NisoSts::Label.new(content: [label]) if label
159
+ f.caption = caption if caption
160
+ f.graphic = [graphic] if graphic
161
+ end
162
+ end
163
+
164
+ def graphic(href: nil)
165
+ ::Sts::NisoSts::Graphic.new(href: xlink_href)
166
+ end
167
+
168
+ def caption(title: nil)
169
+ ::Sts::NisoSts::Caption.new.tap do |c|
170
+ c.title = ::Sts::NisoSts::Title.new(content: [title]) if title
171
+ end
172
+ end
173
+
174
+ def disp_formula(id: nil, label: nil, math: nil)
175
+ ::Sts::NisoSts::DisplayFormula.new.tap do |f|
176
+ f.id = id if id
177
+ f.label = ::Sts::NisoSts::Label.new(content: [label]) if label
178
+ f.math = math if math
179
+ end
180
+ end
181
+
182
+ def inline_formula(math: nil)
183
+ ::Sts::NisoSts::InlineFormula.new(math: math)
184
+ end
185
+
186
+ def non_normative_note(paragraph: [], label: nil)
187
+ ::Sts::NisoSts::NonNormativeNote.new.tap do |n|
188
+ n.p = Array(paragraph) if paragraph&.any?
189
+ n.label = ::Sts::NisoSts::Label.new(content: [label]) if label
190
+ end
191
+ end
192
+
193
+ def non_normative_example(paragraph: [], label: nil)
194
+ ::Sts::NisoSts::NonNormativeExample.new.tap do |e|
195
+ e.p = Array(paragraph) if paragraph&.any?
196
+ e.label = ::Sts::NisoSts::Label.new(content: [label]) if label
197
+ end
198
+ end
199
+
200
+ def ref_list(content_type: nil, title: nil, ref: [])
201
+ ::Sts::NisoSts::ReferenceList.new.tap do |rl|
202
+ rl.content_type = content_type if content_type
203
+ rl.title = ::Sts::NisoSts::Title.new(content: [title]) if title
204
+ rl.ref = Array(ref) if ref&.any?
205
+ end
206
+ end
207
+
208
+ def ref(label: nil, mixed_citation: nil, std: nil)
209
+ ::Sts::NisoSts::Reference.new.tap do |r|
210
+ r.label = ::Sts::NisoSts::Label.new(content: [label]) if label
211
+ r.mixed_citation = ::Sts::NisoSts::MixedCitation.new(content: [mixed_citation]) if mixed_citation
212
+ r.std = std if std
213
+ end
214
+ end
215
+
216
+ # Builds the OIML front-matter metadata block. OIML is not
217
+ # ISO — the NISO-STS-correct container for an OIML publication
218
+ # is <std-meta> (the generic catch-all for non-ISO / non-
219
+ # national / non-regional SDOs). MetadataStd ships the same
220
+ # attribute set as MetadataIso (title_wrap, std_ident,
221
+ # permissions, pub_date, custom_meta_group), but several
222
+ # are collections where MetadataIso's were singular.
223
+ def std_meta(doc_identifier: nil, title: nil, pub_date: nil,
224
+ permissions: nil, custom_meta_group: nil)
225
+ ::Sts::NisoSts::MetadataStd.new.tap do |m|
226
+ m.title_wrap = [::Sts::NisoSts::TitleWrap.new(main: title)] if title
227
+ m.std_ident = std_ident_for(doc_identifier) if doc_identifier
228
+ m.permissions = [permissions] if permissions
229
+ m.pub_date = pub_date.to_s if pub_date
230
+ m.custom_meta_group = [custom_meta_group] if custom_meta_group
231
+ end
232
+ end
233
+
234
+ # <std-ident> per NISO STS (Z39.102-2022): the OIML identifier
235
+ # decomposed into originator + doc-type + doc-number
236
+ # (+ part-number). "OIML B 6-2:2024" → OIML / b / 6 / 2. The
237
+ # publication year is NOT repeated here — pub-date carries it.
238
+ # Identifiers that don't match the OIML pattern fall back to a
239
+ # plain originator string rather than being dropped.
240
+ OIML_ID_PATTERN = /\A(OIML)\s+([A-Z]+)\s+(\d+)(?:-(\d+))?(?::\d{4})?\z/.freeze
241
+
242
+ def std_ident_for(doc_identifier)
243
+ match = OIML_ID_PATTERN.match(doc_identifier.to_s.strip)
244
+ return plain_std_ident(doc_identifier) unless match
245
+
246
+ attrs = {
247
+ originator: match[1],
248
+ doc_type: match[2].downcase,
249
+ doc_number: match[3],
250
+ }
251
+ attrs[:part_number] = match[4] if match[4]
252
+ ::Sts::NisoSts::StandardIdentification.new(attrs)
253
+ end
254
+
255
+ def plain_std_ident(doc_identifier)
256
+ ::Sts::NisoSts::StandardIdentification.new(originator: doc_identifier)
257
+ end
258
+
259
+ def permissions(holder: nil, year: nil)
260
+ attrs = {}
261
+ attrs[:copyright_statement] = "© #{year} #{holder}" if year && holder
262
+ attrs[:copyright_year] = year.to_s if year
263
+ attrs[:copyright_holder] = holder.to_s if holder
264
+ ::Sts::NisoSts::Permissions.new(attrs)
265
+ end
266
+
267
+ def custom_meta_group(name: nil, value: nil)
268
+ ::Sts::NisoSts::CustomMetaGroup.new(
269
+ custom_meta: [::Sts::NisoSts::CustomMeta.new(
270
+ meta_name: name, meta_value: value
271
+ )]
272
+ )
273
+ end
274
+
275
+ def bold(content: [])
276
+ ::Sts::TbxIsoTml::Bold.new(value: Array(content))
277
+ end
278
+
279
+ def italic(content: [])
280
+ ::Sts::TbxIsoTml::Italic.new(value: Array(content))
281
+ end
282
+
283
+ def monospace(content: [])
284
+ ::Sts::NisoSts::Monospace.new(content: Array(content))
285
+ end
286
+
287
+ def sub(content: [])
288
+ ::Sts::NisoSts::Sub.new(content: Array(content))
289
+ end
290
+
291
+ def sup(content: [])
292
+ ::Sts::NisoSts::Sup.new(content: Array(content))
293
+ end
294
+
295
+ def ext_link(xlink_href: nil, content: [])
296
+ ::Sts::NisoSts::ExtLink.new(
297
+ ext_link_type: "uri",
298
+ xlink_href: xlink_href,
299
+ content: Array(content)
300
+ )
301
+ end
302
+
303
+ def xref(rid: nil, ref_type: "other", value: nil)
304
+ ::Sts::TbxIsoTml::Xref.new(rid: rid, ref_type: ref_type, value: value)
305
+ end
306
+
307
+ def fn(id: nil, value: nil)
308
+ ::Sts::TbxIsoTml::Fn.new(id: id, value: value)
309
+ end
310
+
311
+ def title(content: [])
312
+ ::Sts::NisoSts::Title.new(content: Array(content))
313
+ end
314
+
315
+ def label(content: [])
316
+ ::Sts::NisoSts::Label.new(content: Array(content))
317
+ end
318
+ end
319
+ end
320
+ end
321
+ end
322
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+
5
+ module Metanorma
6
+ module Oiml
7
+ module Sts
8
+ module Transformer
9
+ # Converts MN <note> / <example> blocks (outside term entries)
10
+ # to STS <non-normative-note> / <non-normative-example>.
11
+ # Body notes carry NO label — the renderer prepends the kind
12
+ # label ("NOTE" / "EXAMPLE") outside the first paragraph, the
13
+ # way Metanorma marks up body notes ("<span class='note-label'>
14
+ # NOTE</span>&nbsp;<p>…"). Term entries use "Note N to entry:"
15
+ # via TermTransformer.
16
+ class NoteTransformer < Base
17
+ def transform(source_note)
18
+ paragraphs = extract_paragraphs(source_note)
19
+ return nil if paragraphs.empty?
20
+
21
+ if example?(source_note)
22
+ label = example_label(source_note)
23
+ ModelBuilder.non_normative_example(paragraph: paragraphs, label: label)
24
+ else
25
+ ModelBuilder.non_normative_note(paragraph: paragraphs)
26
+ end
27
+ end
28
+
29
+ # MN renders body examples as "<span class='example-label'>
30
+ # EXAMPLE N</span>" — "Example" plus the autonum, even when
31
+ # the source carries no explicit name. The label ends up
32
+ # inside the first paragraph so parity text comparison sees
33
+ # it on both sides.
34
+ def example_label(source_example)
35
+ return nil unless source_example.is_a?(example_block_class)
36
+
37
+ number = source_example.autonum if source_example.class.method_defined?(:autonum)
38
+ number && !number.to_s.empty? ? "Example #{number}" : nil
39
+ end
40
+
41
+ # SourcecodeBlock stores its text in body.content (the bare
42
+ # `content` attribute is nil for parsed documents). The body's
43
+ # content is markup-encoded by the model's roundtrip contract;
44
+ # decoded_content (metanorma-document >= 0.3.1) unwraps it —
45
+ # with a local decode as the fallback for older releases.
46
+ def transform_preformat(source_code)
47
+ text = preformat_text(source_code)
48
+ return nil if text.nil? || text.empty?
49
+
50
+ ::Sts::NisoSts::Preformat.new(content: [text])
51
+ end
52
+
53
+ private
54
+
55
+ def example?(source_note)
56
+ source_note.is_a?(example_block_class)
57
+ end
58
+
59
+ def extract_paragraphs(source_note)
60
+ case source_note
61
+ when example_block_class
62
+ Array(source_note.paragraphs).map { |p| paragraph_transformer.transform(p) }
63
+ when note_block_class
64
+ Array(source_note.content)
65
+ .select { |p| p.is_a?(paragraph_block_class) }
66
+ .map { |p| paragraph_transformer.transform(p) }
67
+ else
68
+ []
69
+ end
70
+ end
71
+
72
+ def preformat_text(source_code)
73
+ return unless source_code.is_a?(sourcecode_block_class)
74
+
75
+ body = source_code.body
76
+ return source_code.text unless body
77
+
78
+ content = body.content if body.is_a?(Lutaml::Model::Serializable)
79
+ return CGI.unescapeHTML(Array(content).join) if content && !content.to_s.empty?
80
+
81
+ source_code.text
82
+ end
83
+
84
+ def note_block_class
85
+ Metanorma::Document::Components::Blocks::NoteBlock
86
+ end
87
+
88
+ def example_block_class
89
+ Metanorma::Document::Components::AncillaryBlocks::ExampleBlock
90
+ end
91
+
92
+ def sourcecode_block_class
93
+ Metanorma::Document::Components::AncillaryBlocks::SourcecodeBlock
94
+ end
95
+
96
+ def paragraph_block_class
97
+ Metanorma::Document::Components::Paragraphs::ParagraphBlock
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end