metanorma-core 0.2.2 → 0.2.4

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: 7e0fe54371f36297eba65fd7cea8649359c7ca0f50195f388d48c82266bc220f
4
- data.tar.gz: a18e98b49080a74314677f7aae1b382ae64083e4b9ce21c773959cc9e94439b2
3
+ metadata.gz: 68e4e30c0ecd25f66ab9f39d7132b23d05f3266a5afba3a6e67f91916329385c
4
+ data.tar.gz: 3d9d4072b81250ec95cb81d7630eeaaf1115338ba24469ee62a58d25bbfe8910
5
5
  SHA512:
6
- metadata.gz: 6d0a9c5cc7e45bbdfd7c4eafda1028e66c8a904c7d7550d95248d966034215a3a2a2baee6a7bcf54b9aedab8ae984e11e4751f70f4bd9fbff8319bdca56f18f7
7
- data.tar.gz: 4b0244d3496a619dccc2dd2ae4dbfb449b84a86949bd51d052c486a148ba2f321643cfd1fb00c857061ded783fb532c6734fbfe34282e11f3bbb5bdc8d62dd2d
6
+ metadata.gz: 002dd0efbf7c11b30d665b62f377fac848f3636448728ee01012062b332c5e33bec62523f2b6ff194f2c6651efcec93ec24ad33edfeeb11212be9e00fb1b3e90
7
+ data.tar.gz: e2ebc0042409579e2f17312a96b6f8c77a8247235d243500d4f888282738aaf83bda659dfa6a77ab93010fd4675ed1230c426cc50696070e8a1fba740c30f66d
data/README.adoc CHANGED
@@ -11,3 +11,5 @@ image:https://img.shields.io/github/commits-since/metanorma/metanorma-core/lates
11
11
 
12
12
  This is a core repository for plugin dependencies of Metanorma, allowing metanorma-standoc and metanorma to share code without circular dependencies.
13
13
 
14
+
15
+ <!-- canary: automerge stub verification, revert not needed -->
@@ -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
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Core
5
+ # One entry in the flavor/taste table — the single source of truth
6
+ # for flavor identity across the ecosystem.
7
+ #
8
+ # A FLAVOR entry (base_flavor nil): a document model family — model
9
+ # root, processor, per-format renderers, pubid module, gem name.
10
+ # A TASTE entry (base_flavor set): a config-only variant of a flavor
11
+ # — no model of its own; it carries the publisher discriminator,
12
+ # branding directory, and doctype map, and inherits everything else
13
+ # through its base chain.
14
+ #
15
+ # renderers values: a renderer class, or a Proc resolver taking
16
+ # (document, **options) and returning a class — flavor-owned variant
17
+ # selection (publisher/profile/doctype). Returning nil falls through
18
+ # to the next matching entry.
19
+ class Flavor
20
+ attr_reader :name, :base_flavor, :gem, :model_root, :processor,
21
+ :renderers, :pubid_module, :branding_dir,
22
+ :publisher_abbr, :doctype_map, :semantic_input
23
+
24
+ def initialize(name:, base_flavor: nil, gem: nil, model_root: nil,
25
+ processor: nil, renderers: {}, pubid_module: nil,
26
+ branding_dir: nil, publisher_abbr: nil, doctype_map: {},
27
+ semantic_input: false)
28
+ @name = name&.to_sym
29
+ @base_flavor = base_flavor&.to_sym
30
+ @gem = gem || "metanorma-#{name}"
31
+ @model_root = model_root
32
+ @processor = processor
33
+ @renderers = renderers
34
+ @pubid_module = pubid_module
35
+ @branding_dir = branding_dir
36
+ @publisher_abbr = publisher_abbr
37
+ @doctype_map = doctype_map
38
+ @semantic_input = semantic_input
39
+ end
40
+
41
+ def taste? = !!base_flavor
42
+
43
+ def renderer_for(format, document, **options)
44
+ entry = renderers[format]
45
+ return nil unless entry
46
+
47
+ entry.is_a?(Proc) ? entry.call(document, **options) : entry
48
+ end
49
+
50
+ def matches?(document_class)
51
+ return false unless document_class.is_a?(Class)
52
+
53
+ root = model_root_class
54
+ return false unless root
55
+
56
+ !!(document_class <= root)
57
+ end
58
+
59
+ def model_root_class
60
+ return @model_root_class if @model_root_class
61
+
62
+ # Never cache a miss: the flavor gem may load after the first
63
+ # lookup (lazy string roots resolve once the owning gem is
64
+ # required), and a cached nil would poison every later walk.
65
+ klass = if model_root.is_a?(Class)
66
+ model_root
67
+ elsif model_root && Object.const_defined?(model_root)
68
+ Object.const_get(model_root)
69
+ end
70
+ @model_root_class = klass
71
+ klass
72
+ end
73
+
74
+ def pubid_module_const
75
+ return nil unless pubid_module
76
+
77
+ begin
78
+ require "pubid"
79
+ rescue LoadError
80
+ return nil
81
+ end
82
+ Object.const_get(pubid_module.to_s)
83
+ rescue NameError
84
+ raise ArgumentError,
85
+ "Flavor #{name.inspect}: pubid module #{pubid_module} could " "not be resolved — fix the flavor's registration"
86
+ end
87
+ end
88
+ end
89
+ end
@@ -27,6 +27,12 @@ module Metanorma
27
27
  # taste alias matched).
28
28
  def taste2flavor(stdtype)
29
29
  stdtype = stdtype.to_sym
30
+ entry = Flavors.find(stdtype)
31
+ return entry.base_flavor if entry&.taste?
32
+
33
+ # Transitional: taste entries land in the table with
34
+ # metanorma-taste's restructure PR; until then fall back to
35
+ # TasteRegister.aliases. Removed in this branch's final commit.
30
36
  tastes = Metanorma::TasteRegister.instance.aliases
31
37
  tastes[stdtype] and stdtype = tastes[stdtype].to_sym
32
38
  stdtype
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Core
5
+ # The process-wide flavor/taste table. Registered most-specific-last;
6
+ # resolution walks most-specific-first so flavor gems (loaded after
7
+ # the harness defaults) win, and taste entries fall through to their
8
+ # base chain by yielding nil.
9
+ class FlavorRegistry
10
+ include Enumerable
11
+
12
+ def initialize
13
+ @flavors = []
14
+ end
15
+
16
+ def register(flavor)
17
+ @flavors << flavor
18
+ self
19
+ end
20
+
21
+ def each(&) = @flavors.each(&)
22
+
23
+ def find_by_name(name)
24
+ @flavors.reverse_each.find { |f| f.name == name.to_sym }
25
+ end
26
+
27
+ def flavor_for(document)
28
+ @flavors.reverse_each.find { |f| f.matches?(document.class) }
29
+ end
30
+
31
+ # Renderer class for (document, format): walk matching entries
32
+ # most-specific-first; a taste without its own renderer for the
33
+ # format yields nil and falls through to the base flavor entry.
34
+ def renderer_for(format, document, **options)
35
+ @flavors.reverse_each.each do |flavor|
36
+ next unless flavor.matches?(document.class)
37
+
38
+ renderer = flavor.renderer_for(format, document, **options)
39
+ return renderer if renderer
40
+ end
41
+ nil
42
+ end
43
+
44
+ # Canonical FLAVOR entry for a stdtype/taste name: follows the
45
+ # base_flavor chain so compile loads the base flavor's gem and
46
+ # processor while the taste supplies config.
47
+ def canonical(name)
48
+ entry = find_by_name(name)
49
+ return nil unless entry
50
+
51
+ entry = find_by_name(entry.base_flavor) while entry&.base_flavor
52
+ entry
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Core
5
+ # The composition-layer API over the flavor/taste table. Replaces
6
+ # the former scattered mechanisms: Registry-only processor dispatch,
7
+ # FlavorLoader.taste2flavor, CLI hand-merged dictionaries, and the
8
+ # renderer registries the harness once carried.
9
+ module Flavors
10
+ class << self
11
+ def table
12
+ @table ||= FlavorRegistry.new
13
+ end
14
+
15
+ def register(flavor)
16
+ table.register(flavor)
17
+ end
18
+
19
+ # Compile-time: canonical flavor entry for a stdtype name
20
+ # (taste chains resolved), loading its gem if needed.
21
+ def resolve(type)
22
+ canonical = table.canonical(type)
23
+ return nil unless canonical
24
+
25
+ unless backends.include?(canonical.name)
26
+ require canonical.gem
27
+ end
28
+ canonical
29
+ end
30
+
31
+ # Render-time: renderer class for (document, format).
32
+ def renderer_for(document, format:, **options)
33
+ table.renderer_for(format, document, **options)
34
+ end
35
+
36
+ def flavor_for(document)
37
+ table.flavor_for(document)
38
+ end
39
+
40
+ def find(name)
41
+ table.find_by_name(name)
42
+ end
43
+
44
+ # Tastes of a given base flavor (for dictionaries/projections).
45
+ def tastes_of(base)
46
+ table.select { |f| f.taste? && f.base_flavor == base.to_sym }
47
+ end
48
+
49
+ def available_tastes
50
+ table.select(&:taste?).map(&:name)
51
+ end
52
+
53
+ private
54
+
55
+ def backends
56
+ Metanorma::Registry.instance.supported_backends
57
+ rescue NameError
58
+ []
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -1,5 +1,5 @@
1
1
  module Metanorma
2
2
  module Core
3
- VERSION = "0.2.2".freeze
3
+ VERSION = "0.2.4".freeze
4
4
  end
5
5
  end
@@ -70,6 +70,29 @@ module Metanorma
70
70
  {}
71
71
  end
72
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
+
73
96
  # Convert an input file to Metanorma semantic XML by routing it
74
97
  # through the {Metanorma::Input::Asciidoc} processor with this
75
98
  # processor's Asciidoctor backend. Override for non-Asciidoc
@@ -131,9 +154,10 @@ module Metanorma
131
154
  # string (document-model leg).
132
155
  def output(isodoc_node, inname, outname, format, options = {})
133
156
  options_preprocess(options)
134
- if document_transformers.key?(format)
157
+ transformers = effective_document_transformers(options)
158
+ if transformers.key?(format)
135
159
  render_via_document_model(isodoc_node, inname, outname, format,
136
- options)
160
+ options, transformers.fetch(format))
137
161
  else
138
162
  File.open(outname, "w:UTF-8") { |f| f.write(isodoc_node) }
139
163
  end
@@ -155,9 +179,13 @@ module Metanorma
155
179
  # {#document_transformers}.
156
180
  # @param options [Hash] processor options, passed to the transformer and
157
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).
158
185
  # @return [String] the serialised (and post-processed) output XML.
159
- def render_via_document_model(isodoc_node, inname, outname, format, options)
160
- spec = document_transformers.fetch(format)
186
+ def render_via_document_model(isodoc_node, inname, outname, format, options,
187
+ spec = nil)
188
+ spec ||= effective_document_transformers(options).fetch(format)
161
189
  xml = document_model_input_xml(isodoc_node, inname)
162
190
  xml = xml.gsub(/\sxmlns="[^"]*"/, "") if spec[:strip_default_namespace]
163
191
  transformer = spec.fetch(:transformer)
@@ -8,6 +8,9 @@ require "metanorma/input"
8
8
  require "metanorma/processor/processor"
9
9
  require "metanorma/registry/registry"
10
10
  require "metanorma/asciidoctor_extensions"
11
+ require "metanorma/core/flavor"
12
+ require "metanorma/core/flavor_registry"
13
+ require "metanorma/core/flavors"
11
14
  require "metanorma/core/flavor_loader"
12
15
  require "metanorma/core/isodoc"
13
16
  require "metanorma/core/boilerplate"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: metanorma-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-21 00:00:00.000000000 Z
11
+ date: 2026-09-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: asciidoctor
@@ -164,12 +164,16 @@ files:
164
164
  - Gemfile
165
165
  - LICENSE
166
166
  - README.adoc
167
+ - docs/implementing-sts-output-for-a-flavour.adoc
167
168
  - lib/metanorma-core.rb
168
169
  - lib/metanorma/asciidoctor_extensions.rb
169
170
  - lib/metanorma/asciidoctor_extensions/glob_include_processor.rb
170
171
  - lib/metanorma/config/config.rb
171
172
  - lib/metanorma/core/boilerplate.rb
173
+ - lib/metanorma/core/flavor.rb
172
174
  - lib/metanorma/core/flavor_loader.rb
175
+ - lib/metanorma/core/flavor_registry.rb
176
+ - lib/metanorma/core/flavors.rb
173
177
  - lib/metanorma/core/isodoc.rb
174
178
  - lib/metanorma/core/version.rb
175
179
  - lib/metanorma/input.rb