metanorma-core 0.2.1 → 0.2.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 314a93ed63df1389dfb3e950e63d7ebff4ccf3c1a0e1698d678ddb898ab9fc47
4
- data.tar.gz: 4ef731b709d7f26820209291edea8d576992d457f0161dae1cc3d4c810433f5f
3
+ metadata.gz: 41c7086b0f760b06a87c99ce115f66d31c1a8e30a6f43a182e1ed81a076b0ee6
4
+ data.tar.gz: a55d956d5122b77c0078b9d48adb4c4c3b6c97b1c7638498a8a9d31d342b799b
5
5
  SHA512:
6
- metadata.gz: 03ed6bb9d50427ca61a7659458c8e154329566a968e3cf80f9bf4a9e46c859358bbab811ab1d76481f13797525f6651e80fb2d565eeb0408f41979ae10658c1b
7
- data.tar.gz: 5e69aa811a9385a9938e0707a37334c51929e880633b707fe32590c599787ebe2bd8c5fc6d96e08d948da149079d7bbf52a119441ca27e3d504279286c76ca10
6
+ metadata.gz: c37685805cfcff21fe4bb18e2a17661242c36a7605aa257736875c5735870e875fcfeb399f3769c25cd8ec4515a68561f8bcc8a503b970c805fe04251e2c2f6f
7
+ data.tar.gz: 4ce50e2bba70f304654640489d89a8d3e9fe45b2e42c3b34f634985b8d79db23bdb5f9e2f2da15bda5fdd6da9b0089b344657e9cbc131ce26d80a0011662ac82
data/.rubocop.yml CHANGED
@@ -1,8 +1,31 @@
1
+ # Auto-generated by Cimas: Do not edit it manually!
2
+ # See https://github.com/metanorma/cimas
1
3
  inherit_from:
2
4
  - https://raw.githubusercontent.com/riboseinc/oss-guides/main/ci/rubocop.yml
5
+ # .rubocop_todo.yml MUST be the last entry. inherit_from is last-wins:
6
+ # if listed before oss-guides, the shared config's stricter Metrics/
7
+ # (MethodLength, BlockLength, etc.) rules override the todo's per-file
8
+ # grandfathering, and the todo becomes inert on those cops. Empirically
9
+ # verified on suma 2026-07-06: with todo listed first, 34 Metrics/* offenses
10
+ # remained; moved last, cleared. Every gem in the metanorma-org fleet
11
+ # already ships a `.rubocop_todo.yml` on its live tree (audited 2026-07-06,
12
+ # 54/54 have it), so this reference is safe to emit unconditionally.
13
+ - .rubocop_todo.yml
14
+
15
+ # Rubocop plugins enabled centrally so every metanorma-org gem picks them up
16
+ # on cimas sync — best practice belongs at the shared-template layer, not
17
+ # per-repo. Per ronaldtse feedback on metanorma/ci#332.
18
+ plugins:
19
+ - rubocop-rspec
20
+ - rubocop-performance
21
+ - rubocop-rake
3
22
 
4
23
  # local repo-specific modifications
5
24
  # ...
6
25
 
7
26
  AllCops:
8
- TargetRubyVersion: 3.4
27
+ # 3.3 matches the org-wide minimum being pushed via #274 (Raise minimum
28
+ # Ruby version to 3.3 due to EOL of 3.2 on 2026-03-31). Was 3.4 before
29
+ # this commit — 3.4 was above the org's stated minimum and would have
30
+ # applied Rubocop rules that fail-close on gems still targeting 3.3.
31
+ TargetRubyVersion: 3.3
data/Gemfile CHANGED
@@ -7,5 +7,8 @@ git_source(:github) { |repo| "https://github.com/#{repo}" }
7
7
  gemspec
8
8
 
9
9
  gem "gem-release"
10
+ # top-level, not the gemspec development group: the release job installs
11
+ # without development/test, and bundle exec rake release needs rake
12
+ gem "rake"
10
13
 
11
14
  eval_gemfile("Gemfile.devel") rescue nil
@@ -0,0 +1,405 @@
1
+ = Implementing STS output for a Metanorma flavour
2
+ :toc: macro
3
+ :toclevels: 3
4
+
5
+ toc::[]
6
+
7
+ == Purpose and audience
8
+
9
+ This is a developer guide for implementing https://www.niso.org/standards-committees/sts[NISO STS] /
10
+ ISO STS output for a Metanorma flavour *from scratch*, using the metanorma-core
11
+ _document-model output leg_.
12
+
13
+ It is written for metanorma-core maintainers and flavour/taste contributors — the
14
+ people who add a new semantic-XML-to-STS output format to a flavour. It is not an
15
+ author guide: authors selecting STS output for an existing flavour need only the
16
+ document attribute (e.g. `:output-extensions:`) documented on metanorma.org.
17
+
18
+ The mechanism described here is the *Feature B* document-model convergence
19
+ (metanorma-core#12): a flavour or taste declares two classes — a *reader* and a
20
+ *transformer* — and metanorma-core drives the read/transform/serialise chain. No
21
+ flavour re-implements the output plumbing, and metanorma-core never depends on the
22
+ STS artefact gem.
23
+
24
+ The worked examples are the two production adoptions:
25
+
26
+ * *OIML* — STS contributed by the OIML *taste* over the ISO base flavour
27
+ (metanorma-oiml + metanorma-taste).
28
+ * *ISO* — STS native to the *flavour* (metanorma-iso).
29
+
30
+ == The big picture
31
+
32
+ Metanorma builds a document in stages:
33
+
34
+ [source]
35
+ ----
36
+ input.adoc --> semantic XML --> (presentation XML) --> output (HTML/PDF/DOCX/STS/...)
37
+ ----
38
+
39
+ Most rendered outputs (HTML, PDF, DOCX) are generated from *presentation XML* — the
40
+ semantic XML enriched with rendering metadata. STS can be generated from *either*
41
+ leg, depending on what your STS transformer consumes. metanorma-core's default treats
42
+ STS as a *semantic*-leg format, and ISO's native `isosts` does exactly that; but a
43
+ flavour or taste whose STS transformer needs the rendered presentation XML can opt
44
+ into the *presentation* leg, as the OIML taste's `oimlsts` does. You declare the leg
45
+ (see <<step-3-make-the-format-selectable>>).
46
+
47
+ The document-model output leg turns that "semantic XML -> STS XML" step into a
48
+ declarative contract. A flavour declares, per output format:
49
+
50
+ * a *reader* that parses the input XML string into a document model, and
51
+ * a *transformer* that maps that model to the STS model and serialises it.
52
+
53
+ metanorma-core's `Processor#output` recognises the format and runs the chain
54
+ (`Processor#render_via_document_model`), so the flavour ships two classes instead of
55
+ bespoke output code.
56
+
57
+ == The contract
58
+
59
+ The contract is `Metanorma::Processor#document_transformers`
60
+ (`metanorma-core/lib/metanorma/processor/processor.rb`). It returns a hash mapping an
61
+ output-format symbol to a spec:
62
+
63
+ [source,ruby]
64
+ ----
65
+ def document_transformers
66
+ {
67
+ myflavoursts: {
68
+ reader: MyFlavour::Sts::SourceDocument, # required
69
+ transformer: MyFlavour::Sts::Standard, # required
70
+ to_xml_options: {}, # default {}
71
+ strip_default_namespace: false, # default false
72
+ post_process: nil, # default identity
73
+ },
74
+ }
75
+ end
76
+ ----
77
+
78
+ Spec keys (metanorma-core contract):
79
+
80
+ `:reader`:: (required) responds to `.from_xml(String) -> model`. Parses the input
81
+ XML into a document model.
82
+
83
+ `:transformer`:: (required) responds to `.new(model, options)`; the instance responds
84
+ to `#transform -> target`, and the target responds to `#to_xml`. See
85
+ <<the-adapter-pattern>>.
86
+
87
+ `:to_xml_options`:: hash splatted into `target.to_xml(**opts)` (default `{}`).
88
+
89
+ `:strip_default_namespace`:: strip `xmlns="..."` from the input XML before
90
+ `from_xml` (default `false`). Set `true` when your reader's model does not expect the
91
+ Metanorma default namespace on the root.
92
+
93
+ `:post_process`:: a callable `(xml, transformer, options) -> xml` run on the
94
+ serialised string (default identity). Use it for namespace / processing-metadata /
95
+ language fix-ups that must happen on the serialised output.
96
+
97
+ Two further keys are read by the *metanorma compile driver* (not by metanorma-core)
98
+ to make the format selectable; see <<step-3-make-the-format-selectable>>:
99
+
100
+ `:suffix`:: the output filename suffix, e.g. `"oiml.sts.xml"`.
101
+
102
+ `:presentation`:: `true` if the format is generated from *presentation* XML;
103
+ omit/`false` for the *semantic* leg. Per-transformer choice: OIML's `oimlsts` sets
104
+ `true`, ISO's native `isosts` uses the semantic leg.
105
+
106
+ === How metanorma-core drives it
107
+
108
+ `Processor#output` routes any registered format through the document-model leg:
109
+
110
+ [source,ruby]
111
+ ----
112
+ def output(isodoc_node, inname, outname, format, options = {})
113
+ options_preprocess(options)
114
+ transformers = effective_document_transformers(options)
115
+ if transformers.key?(format)
116
+ render_via_document_model(isodoc_node, inname, outname, format,
117
+ options, transformers.fetch(format))
118
+ else
119
+ File.open(outname, "w:UTF-8") { |f| f.write(isodoc_node) }
120
+ end
121
+ end
122
+ ----
123
+
124
+ `render_via_document_model` is the whole chain, duck-typed — it never names a
125
+ concrete reader/transformer/model gem, so those stay out of metanorma-core's
126
+ dependencies:
127
+
128
+ [source,ruby]
129
+ ----
130
+ xml = document_model_input_xml(isodoc_node, inname) # semantic or presentation leg
131
+ xml = xml.gsub(/\sxmlns="[^"]*"/, "") if spec[:strip_default_namespace]
132
+ transformer = spec.fetch(:transformer)
133
+ .new(spec.fetch(:reader).from_xml(xml), options)
134
+ out = transformer.transform.to_xml(**(spec[:to_xml_options] || {}))
135
+ out = spec[:post_process].call(out, transformer, options) if spec[:post_process]
136
+ ----
137
+
138
+ The input XML comes from `isodoc_node` (the semantic leg) or, when that is nil, from
139
+ reading `inname` (the presentation leg). Which leg fires is governed by
140
+ `use_presentation_xml`. The metanorma-core default treats STS and RFC as semantic-leg
141
+ formats:
142
+
143
+ [source,ruby]
144
+ ----
145
+ def use_presentation_xml(ext) # metanorma-core default
146
+ case ext
147
+ when :html, :doc, :pdf then true
148
+ else false # STS, RFC, ... default to the semantic leg
149
+ end
150
+ end
151
+ ----
152
+
153
+ A flavour overrides this per format. ISO keeps `isosts` on the semantic leg when its
154
+ native STS is enabled (and falls back to the presentation leg for the legacy path):
155
+
156
+ [source,ruby]
157
+ ----
158
+ def use_presentation_xml(ext) # metanorma-iso
159
+ return false if ext == :isosts && Metanorma::Iso::Sts.enabled? # native STS: semantic
160
+ return true if %i[html_alt sts isosts].include?(ext) # legacy path: presentation
161
+ super
162
+ end
163
+ ----
164
+
165
+ A *taste* declares the leg instead through the `:presentation` key of its spec (Route
166
+ B); OIML sets `presentation: true`, putting `oimlsts` on the presentation leg. Pick
167
+ the leg your transformer actually consumes.
168
+
169
+ [#the-adapter-pattern]
170
+ == Step 1 — Build the STS transformer (in the artefact gem)
171
+
172
+ The reader and transformer live in the flavour's *artefact gem* (e.g.
173
+ metanorma-oiml), never in metanorma-core.
174
+
175
+ === The reader
176
+
177
+ The reader responds to `.from_xml(String) -> model`. In practice this is a thin
178
+ entry point on your source-document model:
179
+
180
+ [source,ruby]
181
+ ----
182
+ module Metanorma::Oiml::Sts::Transformer
183
+ class SourceDocument
184
+ def self.from_xml(input)
185
+ parse(input) # -> a SourceDocument model instance
186
+ end
187
+ end
188
+ end
189
+ ----
190
+
191
+ === The transformer (adapter)
192
+
193
+ The transformer must satisfy: `.new(model, options)`, `#transform -> target`, and
194
+ `target#to_xml`. The cleanest way to meet this while reusing an existing conversion
195
+ pipeline is the *adapter-is-its-own-target* pattern from metanorma-oiml
196
+ (`lib/metanorma/oiml/sts/transformer/standard.rb`):
197
+
198
+ [source,ruby]
199
+ ----
200
+ class Standard
201
+ def initialize(model, options = {})
202
+ @model = model
203
+ @options = options
204
+ end
205
+
206
+ def transform
207
+ @document_transformer = DocumentTransformer.new(Context.new(@model))
208
+ self # the adapter is its own #to_xml target
209
+ end
210
+
211
+ def to_xml(**_options)
212
+ @document_transformer.transform_to_xml(@model) # full existing pipeline
213
+ end
214
+ end
215
+ ----
216
+
217
+ `#to_xml` runs the *full* existing conversion pipeline (model build plus the
218
+ namespace / processing-metadata / language post-processing on the serialised
219
+ string), so the driven output is byte-identical to the gem's standalone converter
220
+ (`Metanorma::Oiml::Sts::Transformer.convert`). Returning the raw `#transform` model
221
+ instead would drop that post-processing — a common trap.
222
+
223
+ TIP: If your gem already has a standalone `convert` (or an `oiml-sts`-style CLI),
224
+ make the adapter's output byte-equal to it and add a byte-equivalence test. That is
225
+ your safety net that the unified path did not change behaviour.
226
+
227
+ == Step 2 — Register the format
228
+
229
+ There are two registration routes. Choose by *what owns the flavour*.
230
+
231
+ === Route A — flavour-native (the flavour has its own Processor)
232
+
233
+ If STS belongs to the flavour itself (ISO), override `document_transformers` on the
234
+ flavour's `Metanorma::Processor` subclass:
235
+
236
+ [source,ruby]
237
+ ----
238
+ module Metanorma::Iso
239
+ class Processor < Metanorma::Processor
240
+ def document_transformers
241
+ {
242
+ isosts: {
243
+ reader: Metanorma::Iso::Sts::SourceDocument,
244
+ transformer: Metanorma::Iso::Sts::Standard,
245
+ },
246
+ }
247
+ end
248
+ end
249
+ end
250
+ ----
251
+
252
+ This is zero-arg and static: the flavour always offers the format.
253
+
254
+ === Route B — taste-contributed (a taste over a base flavour)
255
+
256
+ If STS belongs to a *taste* over a base flavour (OIML is a taste over ISO), the taste
257
+ contributes the spec through the per-taste hook in metanorma-taste, *without*
258
+ metanorma-taste depending on the artefact gem.
259
+
260
+ Declare a `data/<taste>/transformers.rb` shim in the taste gem
261
+ (metanorma-taste/data/oiml/transformers.rb):
262
+
263
+ [source,ruby]
264
+ ----
265
+ Metanorma::TasteRegister.register_document_transformers(:oiml) do
266
+ require "metanorma/oiml/sts" # loaded lazily — only for OIML builds
267
+
268
+ {
269
+ oimlsts: {
270
+ reader: Metanorma::Oiml::Sts::Transformer::SourceDocument,
271
+ transformer: Metanorma::Oiml::Sts::Transformer::Standard,
272
+ strip_default_namespace: false,
273
+ to_xml_options: {},
274
+ suffix: "oiml.sts.xml", # compile-driver keys (Step 3)
275
+ presentation: true,
276
+ },
277
+ }
278
+ end
279
+ ----
280
+
281
+ How the hook works (`metanorma-taste/lib/metanorma/taste_register.rb`):
282
+
283
+ * `register_document_transformers(taste, &block)` stores the block, keyed by taste.
284
+ * `document_transformers_for(taste)` is memoised: on first use it requires the taste's
285
+ `data/<taste>/transformers.rb` shim (`load_transformer_hook`), then calls the block.
286
+ * Because the `require "metanorma/oiml/sts"` sits *inside* the block, the artefact gem
287
+ loads only for builds that use the taste. metanorma-taste keeps no hard dependency
288
+ on it. If the shim declares transformers but the artefact gem is not in the bundle,
289
+ the hook raises `UnknownTasteError` telling the user to add it.
290
+
291
+ The taste-aware `Processor#effective_document_transformers` then merges the taste's
292
+ specs onto the flavour's `document_transformers`, keyed by `options[:supplied_type]`
293
+ (the active taste). Base flavours and tastes without the hook are unaffected:
294
+
295
+ [source,ruby]
296
+ ----
297
+ def effective_document_transformers(options = {})
298
+ base = document_transformers
299
+ taste = options[:supplied_type]
300
+ unless taste && defined?(Metanorma::TasteRegister) &&
301
+ Metanorma::TasteRegister.respond_to?(:document_transformers_for)
302
+ return base
303
+ end
304
+ base.merge(Metanorma::TasteRegister.document_transformers_for(taste) || {})
305
+ end
306
+ ----
307
+
308
+ NOTE: `supplied_type` is the seam that carries taste identity to the shared, stateless
309
+ processor. No per-build state is kept on the processor instance.
310
+
311
+ [#step-3-make-the-format-selectable]
312
+ == Step 3 — Make the format selectable in the compile driver
313
+
314
+ For a user to request the format (e.g. `:output-extensions: oimlsts`), the metanorma
315
+ compile driver must know its suffix and which leg it uses. This is threaded via
316
+ `supplied_type` (`metanorma/lib/metanorma/compile/compile_options.rb`):
317
+
318
+ * `supplied_type` originates from `options[:type]` and is copied into the isodoc
319
+ options (`copy_isodoc_options_attrs`: `ret[:supplied_type] = options[:supplied_type]`),
320
+ so the taste-aware processor sees it.
321
+ * `effective_output_formats` merges the processor's `output_formats` with the
322
+ `:suffix` of each taste-contributed format, so the format passes extension
323
+ validation and gets an output filename:
324
+ +
325
+ [source,ruby]
326
+ ----
327
+ def effective_output_formats(options)
328
+ @processor.output_formats.merge(
329
+ taste_transformers(options).transform_values { |s| s[:suffix] }.compact,
330
+ )
331
+ end
332
+ ----
333
+ * `uses_presentation_xml?` honours both the processor's answer and the taste spec's
334
+ `:presentation` flag:
335
+ +
336
+ [source,ruby]
337
+ ----
338
+ def uses_presentation_xml?(ext, options)
339
+ @processor.use_presentation_xml(ext) ||
340
+ (taste_transformers(options)[ext] || {})[:presentation] == true
341
+ end
342
+ ----
343
+
344
+ For a *flavour-native* format (Route A), the compile driver reads the same keys off
345
+ the flavour's own `document_transformers`; set `:suffix` there too if the format is
346
+ user-selectable, and override `use_presentation_xml` to pick the leg (ISO's `isosts`
347
+ returns `false` for the semantic leg).
348
+
349
+ == Step 4 — Deprecate the standalone converter (optional)
350
+
351
+ If the artefact gem had a standalone STS converter or CLI (e.g. `oiml-sts convert`),
352
+ deprecate it in favour of the unified path once the driver is in place. Keep the code
353
+ (the adapter delegates to it) but emit a deprecation notice from the old CLI entry
354
+ points, and document the metanorma-driven path as canonical. metanorma-oiml#24 is the
355
+ reference.
356
+
357
+ == Conforming to the official STS specifications
358
+
359
+ The transformer must emit *conformant* STS. Source the normative models from:
360
+
361
+ * *NISO STS* — ANSI/NISO Z39.102, the NISO Standards Tag Suite.
362
+ * *ISO STS* — the ISO profile/extension of NISO STS.
363
+
364
+ Model the transformer's output against these tag sets (element/attribute names,
365
+ allowed content models, metadata blocks). Validate generated output against the STS
366
+ DTD/schema as part of your test suite; do not rely on visual inspection of the PDF/
367
+ HTML, which can look correct while the STS XML is non-conformant.
368
+
369
+ == Testing (smoke tests)
370
+
371
+ At minimum:
372
+
373
+ . *Build test* — compile a small document requesting the new format and assert the
374
+ output file is produced with the expected suffix and a well-formed STS root.
375
+ . *Byte-equivalence test* — assert the driven output equals the standalone converter's
376
+ output (guards the adapter, per <<the-adapter-pattern>>).
377
+ . *Registration test* — for a taste, assert `document_transformers_for(:yourtaste)`
378
+ returns the spec and that `effective_document_transformers` merges it under
379
+ `supplied_type`.
380
+ . *Schema validation* — validate the generated STS against the NISO/ISO STS schema.
381
+
382
+ The OIML and ISO STS specs in metanorma-oiml / metanorma-iso are the templates.
383
+
384
+ == Checklist
385
+
386
+ [cols="1,3"]
387
+ |===
388
+ | Artefact gem | `reader` (`.from_xml`) + `transformer` (`Standard` adapter, byte-equal to standalone `convert`)
389
+ | Registration | Route A: override `document_transformers` on the flavour Processor. Route B: `data/<taste>/transformers.rb` shim with a lazy `require`
390
+ | Selectable | `:suffix` (+ `:presentation` only if presentation-leg) so the compile driver exposes it
391
+ | Leg | Declare it per what your transformer consumes: semantic (`use_presentation_xml` `false`, e.g. ISO `isosts`) or presentation (`presentation: true`, e.g. OIML `oimlsts`)
392
+ | Deprecate | old standalone converter/CLI emits a deprecation notice
393
+ | Conformance | output validated against NISO/ISO STS schema
394
+ | Tests | build + byte-equivalence + registration + schema
395
+ |===
396
+
397
+ == Reference
398
+
399
+ * metanorma-core#12 — Feature B document-model convergence (design: Decision + Alignment)
400
+ * metanorma-core#13 — taste-aware `Processor#effective_document_transformers`
401
+ * metanorma/metanorma#589 — compile-driver output selection (`supplied_type`, `effective_output_formats`, `uses_presentation_xml?`)
402
+ * metanorma/metanorma-iso#1610 — mature STS-ISO transformer adoption (flavour-native, Route A)
403
+ * metanorma/metanorma-oiml#24 — STS core-driver adapter + `oiml-sts` CLI deprecation (Route B artefact)
404
+ * metanorma/metanorma-taste#188 — per-taste hook + OIML `:oimlsts` registration
405
+ * NISO STS (ANSI/NISO Z39.102) and ISO STS official specifications
@@ -1,5 +1,5 @@
1
1
  module Metanorma
2
2
  module Core
3
- VERSION = "0.2.1".freeze
3
+ VERSION = "0.2.3".freeze
4
4
  end
5
5
  end
@@ -115,7 +115,7 @@ module Metanorma
115
115
  # attribute is bare (no value).
116
116
  EMPTY_ADOC_OPTIONS_DEFAULT_FALSE =
117
117
  %w(hierarchical-assets break-up-urls-in-tables toc-figures
118
- toc-tables toc-recommendations).freeze
118
+ toc-tables toc-recommendations toc-examples).freeze
119
119
 
120
120
  # Convert an Asciidoc-style attribute name (kebab-case, possibly
121
121
  # ending in +-override+ or +-pdf+) into the Ruby symbol used in
@@ -43,6 +43,56 @@ module Metanorma
43
43
  }
44
44
  end
45
45
 
46
+ # Per-format "document model" publishing specs. Override in a flavor to
47
+ # register injected reader + transformer classes for a document-model
48
+ # output leg (Presentation/Semantic XML -> reader -> transformer -> XML),
49
+ # so the flavor declares two classes rather than re-implementing the
50
+ # read/transform/serialise chain in {#output}. The classes need not live
51
+ # in the +Metanorma::+ tree.
52
+ #
53
+ # Each value is a Hash with keys:
54
+ # :reader - responds to +.from_xml(String)+ -> model
55
+ # (required)
56
+ # :transformer - responds to +.new(model, options)+; the
57
+ # instance responds to +#transform+ -> target,
58
+ # and the target responds to +#to_xml+
59
+ # (required)
60
+ # :to_xml_options - Hash splatted into +target.to_xml(**opts)+
61
+ # (default +{}+)
62
+ # :strip_default_namespace - strip +xmlns="..."+ before +from_xml+
63
+ # (default +false+)
64
+ # :post_process - callable +(xml, transformer, options)+
65
+ # -> xml (default identity)
66
+ #
67
+ # @return [Hash{Symbol => Hash}] format symbol -> spec. Empty by default,
68
+ # so flavors that do not use this leg are unaffected.
69
+ def document_transformers
70
+ {}
71
+ end
72
+
73
+ # The flavor's own {#document_transformers}, merged with the document-model
74
+ # specs contributed by the active taste (+options[:supplied_type]+). Those
75
+ # specs come from +Metanorma::TasteRegister+ when it supports the per-taste
76
+ # hook; base flavors (no taste), and installed tastes without the hook, are
77
+ # unaffected -- the base map is returned unchanged. {#document_transformers}
78
+ # stays zero-arg (flavors override it) and the taste specs are merged on
79
+ # top. No per-build state is kept on the shared processor instance: the
80
+ # taste identity rides entirely on +options[:supplied_type]+.
81
+ #
82
+ # @param options [Hash] processor options; reads +:supplied_type+.
83
+ # @return [Hash{Symbol => Hash}] merged format symbol -> spec.
84
+ def effective_document_transformers(options = {})
85
+ base = document_transformers
86
+ taste = options[:supplied_type]
87
+ unless taste && defined?(Metanorma::TasteRegister) &&
88
+ Metanorma::TasteRegister.respond_to?(:document_transformers_for)
89
+ return base
90
+ end
91
+
92
+ contributed = Metanorma::TasteRegister.document_transformers_for(taste)
93
+ base.merge(contributed || {})
94
+ end
95
+
46
96
  # Convert an input file to Metanorma semantic XML by routing it
47
97
  # through the {Metanorma::Input::Asciidoc} processor with this
48
98
  # processor's Asciidoctor backend. Override for non-Asciidoc
@@ -87,19 +137,80 @@ module Metanorma
87
137
  options[:output_formats] ||= output_formats
88
138
  end
89
139
 
90
- # Default output-writer: dump the rendered output string to a
91
- # UTF-8 file. Override for binary formats (Word, PDF) that need
92
- # different post-processing.
140
+ # Default output-writer. If +format+ is registered in
141
+ # {#document_transformers}, render it through the document-model leg
142
+ # ({#render_via_document_model}); otherwise dump the rendered output
143
+ # string to a UTF-8 file. Flavors override this for binary formats
144
+ # (Word, PDF) that need different post-processing, calling +super+ for
145
+ # the document-model and passthrough cases.
93
146
  #
94
- # @param isodoc_node [String] rendered output content.
95
- # @param _inname [String] source filename (unused at this base level).
147
+ # @param isodoc_node [String, nil] rendered output content, or the
148
+ # semantic XML string on the document-model semantic leg.
149
+ # @param inname [String] source filename.
96
150
  # @param outname [String] destination path.
97
- # @param _format [Symbol] output format (unused at this base level).
98
- # @param _options [Hash] processor options (unused at this base level).
99
- # @return [Integer] bytes written (Ruby's +File#write+ return).
100
- def output(isodoc_node, _inname, outname, _format, _options = {})
101
- File.open(outname, "w:UTF-8") { |f| f.write(isodoc_node) }
151
+ # @param format [Symbol] output format.
152
+ # @param options [Hash] processor options.
153
+ # @return [Object] bytes written (passthrough) or the serialised XML
154
+ # string (document-model leg).
155
+ def output(isodoc_node, inname, outname, format, options = {})
156
+ options_preprocess(options)
157
+ transformers = effective_document_transformers(options)
158
+ if transformers.key?(format)
159
+ render_via_document_model(isodoc_node, inname, outname, format,
160
+ options, transformers.fetch(format))
161
+ else
162
+ File.open(outname, "w:UTF-8") { |f| f.write(isodoc_node) }
163
+ end
164
+ end
165
+
166
+ # Render a document-model output leg: read the input XML into a model via
167
+ # the registered reader, transform it, serialise, optionally post-process,
168
+ # and write. The input XML comes from +isodoc_node+ (semantic leg) or,
169
+ # when that is nil, from reading +inname+ (presentation leg) — so both
170
+ # {#use_presentation_xml} settings are supported transparently. The driver
171
+ # is duck-typed: it never names a concrete reader/transformer/target-model
172
+ # gem, keeping those out of metanorma-core's dependencies.
173
+ #
174
+ # @param isodoc_node [String, nil] semantic XML string, or nil on the
175
+ # presentation leg.
176
+ # @param inname [String] input filename (read on the presentation leg).
177
+ # @param outname [String, nil] destination path; written when non-nil.
178
+ # @param format [Symbol] output-format symbol; must be a key of
179
+ # {#document_transformers}.
180
+ # @param options [Hash] processor options, passed to the transformer and
181
+ # post-processor.
182
+ # @param spec [Hash, nil] pre-resolved transformer spec; when nil it is
183
+ # resolved from {#effective_document_transformers} (so callers that pass
184
+ # the old five-argument signature still work).
185
+ # @return [String] the serialised (and post-processed) output XML.
186
+ def render_via_document_model(isodoc_node, inname, outname, format, options,
187
+ spec = nil)
188
+ spec ||= effective_document_transformers(options).fetch(format)
189
+ xml = document_model_input_xml(isodoc_node, inname)
190
+ xml = xml.gsub(/\sxmlns="[^"]*"/, "") if spec[:strip_default_namespace]
191
+ transformer = spec.fetch(:transformer)
192
+ .new(spec.fetch(:reader).from_xml(xml), options)
193
+ out = transformer.transform.to_xml(**(spec[:to_xml_options] || {}))
194
+ if (post = spec[:post_process])
195
+ out = post.call(out, transformer, options)
196
+ end
197
+ File.open(outname, "w:UTF-8") { |f| f.write(out) } if outname
198
+ out
199
+ end
200
+
201
+ # Resolve the input XML string from whichever leg fired: the semantic
202
+ # leg passes the XML as +isodoc_node+; the presentation leg passes nil
203
+ # and the presentation-XML file path as +inname+.
204
+ #
205
+ # @param isodoc_node [String, nil] semantic XML string, or nil.
206
+ # @param inname [String] presentation-XML file path (used when node nil).
207
+ # @return [String] the input XML.
208
+ def document_model_input_xml(isodoc_node, inname)
209
+ return isodoc_node if isodoc_node
210
+
211
+ File.read(inname, encoding: "UTF-8")
102
212
  end
213
+ private :document_model_input_xml
103
214
 
104
215
  # Read processor-relevant options from the input file's header
105
216
  # via {Metanorma::Input::Asciidoc#extract_options}, merging in
@@ -23,10 +23,10 @@ Gem::Specification.new do |spec|
23
23
  f.match(%r{^(test|spec|features|bin|.github)/}) \
24
24
  || f.match(%r{Rakefile|bin/rspec})
25
25
  end
26
- spec.required_ruby_version = Gem::Requirement.new(">= 3.1.0")
26
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.3.0")
27
27
 
28
28
  spec.add_dependency "asciidoctor"
29
- spec.add_dependency "metanorma-taste", "~> 1.0.0"
29
+ spec.add_dependency "metanorma-taste", "~> 1.1.0"
30
30
  spec.add_dependency "metanorma-utils"
31
31
  spec.add_dependency "nokogiri"
32
32
 
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: metanorma-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-05-25 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: asciidoctor
@@ -30,14 +29,14 @@ dependencies:
30
29
  requirements:
31
30
  - - "~>"
32
31
  - !ruby/object:Gem::Version
33
- version: 1.0.0
32
+ version: 1.1.0
34
33
  type: :runtime
35
34
  prerelease: false
36
35
  version_requirements: !ruby/object:Gem::Requirement
37
36
  requirements:
38
37
  - - "~>"
39
38
  - !ruby/object:Gem::Version
40
- version: 1.0.0
39
+ version: 1.1.0
41
40
  - !ruby/object:Gem::Dependency
42
41
  name: metanorma-utils
43
42
  requirement: !ruby/object:Gem::Requirement
@@ -164,6 +163,7 @@ files:
164
163
  - Gemfile
165
164
  - LICENSE
166
165
  - README.adoc
166
+ - docs/implementing-sts-output-for-a-flavour.adoc
167
167
  - lib/metanorma-core.rb
168
168
  - lib/metanorma/asciidoctor_extensions.rb
169
169
  - lib/metanorma/asciidoctor_extensions/glob_include_processor.rb
@@ -184,7 +184,6 @@ homepage: https://github.com/metanorma/metanorma-core
184
184
  licenses:
185
185
  - BSD-2-Clause
186
186
  metadata: {}
187
- post_install_message:
188
187
  rdoc_options: []
189
188
  require_paths:
190
189
  - lib
@@ -192,15 +191,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
192
191
  requirements:
193
192
  - - ">="
194
193
  - !ruby/object:Gem::Version
195
- version: 3.1.0
194
+ version: 3.3.0
196
195
  required_rubygems_version: !ruby/object:Gem::Requirement
197
196
  requirements:
198
197
  - - ">="
199
198
  - !ruby/object:Gem::Version
200
199
  version: '0'
201
200
  requirements: []
202
- rubygems_version: 3.5.22
203
- signing_key:
201
+ rubygems_version: 4.0.16
204
202
  specification_version: 4
205
203
  summary: Metanorma Core
206
204
  test_files: []