metanorma-core 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/metanorma/asciidoctor_extensions/glob_include_processor.rb +26 -0
- data/lib/metanorma/config/config.rb +24 -0
- data/lib/metanorma/core/boilerplate.rb +278 -0
- data/lib/metanorma/core/flavor_loader.rb +121 -0
- data/lib/metanorma/core/isodoc.rb +131 -0
- data/lib/metanorma/core/version.rb +1 -1
- data/lib/metanorma/input/asciidoc.rb +62 -0
- data/lib/metanorma/input/base.rb +8 -0
- data/lib/metanorma/processor/processor.rb +73 -2
- data/lib/metanorma/util/util.rb +35 -1
- data/lib/metanorma-core.rb +3 -0
- data/metanorma-core.gemspec +4 -3
- metadata +15 -12
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ed26c327953bc5f8571454087342a413f4c5c7e9f1f4030b3f438cccc7e037d1
|
|
4
|
+
data.tar.gz: c7d871e81da7a7c84615ef357a3abea8717ef1a1fb3380af6b5ae436f25eede9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 24192aefbe3788198362fec82be7b6e95336a34ca358aba0ee7b20a85f8d772b79f6f077f300a6a9970c69f24f13ca412805485c0d73e41ab33c5ded8764a55b
|
|
7
|
+
data.tar.gz: 723335b30b227377c3f552506f81081c892c4772c489ddc143a63d8f909257d29795c7b99cfb0a23e996559c5b9dbc77ae5a98ee4500c4929ddcd82f81474a38
|
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
module Metanorma
|
|
2
|
+
# Asciidoctor extensions registered globally when metanorma-core is
|
|
3
|
+
# loaded. Provides the +include::pattern[]+ glob support used across
|
|
4
|
+
# the metanorma stack.
|
|
2
5
|
module AsciidoctorExtensions
|
|
6
|
+
# Asciidoctor +IncludeProcessor+ that expands glob patterns in
|
|
7
|
+
# +include::+ directives. When the +target+ contains a +*+, the
|
|
8
|
+
# files matching the glob (relative to the including reader's
|
|
9
|
+
# +dir+) are included in reverse-sorted order, each separated
|
|
10
|
+
# from the next by a blank line unless the +adjoin-option+
|
|
11
|
+
# attribute is set.
|
|
12
|
+
#
|
|
13
|
+
# @example In an Asciidoc document
|
|
14
|
+
# include::sections/*.adoc[]
|
|
3
15
|
class GlobIncludeProcessor < ::Asciidoctor::Extensions::IncludeProcessor
|
|
16
|
+
# @param _doc [Asciidoctor::Document] containing document (unused).
|
|
17
|
+
# @param reader [Asciidoctor::Reader] the include site's reader.
|
|
18
|
+
# @param target_glob [String] glob pattern relative to
|
|
19
|
+
# +reader.dir+.
|
|
20
|
+
# @param attributes [Hash] include directive attributes;
|
|
21
|
+
# recognises +"adjoin-option"+ to suppress the inter-file
|
|
22
|
+
# blank line.
|
|
23
|
+
# @return [Asciidoctor::Reader] the reader, mutated in place.
|
|
4
24
|
def process(_doc, reader, target_glob, attributes)
|
|
5
25
|
Dir[File.join reader.dir, target_glob].sort.reverse_each do |target|
|
|
6
26
|
content = File.readlines target
|
|
@@ -10,6 +30,12 @@ module Metanorma
|
|
|
10
30
|
reader
|
|
11
31
|
end
|
|
12
32
|
|
|
33
|
+
# Whether this processor handles the given include +target+.
|
|
34
|
+
# Triggers on any target containing +*+ — sufficient for the
|
|
35
|
+
# glob patterns the metanorma stack uses.
|
|
36
|
+
#
|
|
37
|
+
# @param target [String] include directive target.
|
|
38
|
+
# @return [Boolean]
|
|
13
39
|
def handles?(target)
|
|
14
40
|
target.include? "*"
|
|
15
41
|
end
|
|
@@ -1,17 +1,41 @@
|
|
|
1
1
|
module Metanorma
|
|
2
|
+
# Configuration mixin for the +Metanorma+ module. Provides the
|
|
3
|
+
# +Metanorma.configure+ block-based setup pattern and the
|
|
4
|
+
# +Metanorma.configuration+ accessor for read access.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# Metanorma.configure do |c|
|
|
8
|
+
# c.logs = %i[error fatal]
|
|
9
|
+
# end
|
|
2
10
|
module Config
|
|
11
|
+
# Yield the singleton {Metanorma::Configuration} for in-place
|
|
12
|
+
# mutation, e.g. flipping which log severities are printed.
|
|
13
|
+
#
|
|
14
|
+
# @yieldparam [Metanorma::Configuration] the singleton config.
|
|
15
|
+
# @return [Metanorma::Configuration, nil] the config if no block
|
|
16
|
+
# was given (preserving the original semantics), otherwise the
|
|
17
|
+
# block's return value.
|
|
3
18
|
def configure
|
|
4
19
|
if block_given?
|
|
5
20
|
yield configuration
|
|
6
21
|
end
|
|
7
22
|
end
|
|
8
23
|
|
|
24
|
+
# Lazily-instantiated singleton {Metanorma::Configuration} for the
|
|
25
|
+
# +Metanorma+ module.
|
|
26
|
+
#
|
|
27
|
+
# @return [Metanorma::Configuration]
|
|
9
28
|
def configuration
|
|
10
29
|
@configuration ||= Configuration.new
|
|
11
30
|
end
|
|
12
31
|
end
|
|
13
32
|
|
|
33
|
+
# Mutable runtime configuration for the metanorma stack. Currently
|
|
34
|
+
# carries only the +logs+ severity allowlist consumed by
|
|
35
|
+
# {Metanorma::Util#log}; other settings can be added here as
|
|
36
|
+
# cross-cutting needs arise.
|
|
14
37
|
class Configuration
|
|
38
|
+
# @return [Array<Symbol>] severities printed by {Metanorma::Util#log}.
|
|
15
39
|
attr_accessor :logs
|
|
16
40
|
|
|
17
41
|
def initialize
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "asciidoctor"
|
|
4
|
+
require "nokogiri"
|
|
5
|
+
|
|
6
|
+
module Metanorma
|
|
7
|
+
module Core
|
|
8
|
+
# Inline-snippet boilerplate handling shared across metanorma-standoc
|
|
9
|
+
# and metanorma (collection layer). Provides Liquid + inline-Asciidoc
|
|
10
|
+
# substitution into docidentifier-like snippets, plus the
|
|
11
|
+
# option-isolating Asciidoctor convert wrapper used to keep nested
|
|
12
|
+
# conversions from leaking attribute / extension-registry state.
|
|
13
|
+
#
|
|
14
|
+
# Standoc's Cleanup::Boilerplate includes this module and overrides
|
|
15
|
+
# {#boilerplate_snippet_cleanup} to apply standoc-specific namespace
|
|
16
|
+
# cleanup and footnote separation; the metanorma collection layer
|
|
17
|
+
# calls into it as module functions
|
|
18
|
+
# (e.g. +Metanorma::Core::Boilerplate.docidentifier_boilerplate_isodoc+)
|
|
19
|
+
# without including the module — both forms are supported via
|
|
20
|
+
# +extend self+ at the bottom of this module.
|
|
21
|
+
module Boilerplate
|
|
22
|
+
# Asciidoctor attributes that are safe to inherit from an outer
|
|
23
|
+
# conversion context into an isolated nested convert. Anything not
|
|
24
|
+
# in this set is dropped.
|
|
25
|
+
SAFE_SHARED_ATTRIBUTES = {
|
|
26
|
+
"source-highlighter" => "html-pipeline",
|
|
27
|
+
"nofooter" => "",
|
|
28
|
+
"no-header-footer" => "",
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
# Convert a snippet of Asciidoc-with-Liquid text into the
|
|
32
|
+
# localised, cleaned-up XML string suitable for substitution into
|
|
33
|
+
# a surrounding document. Three stages run in order:
|
|
34
|
+
#
|
|
35
|
+
# 1. Liquid substitution via +isodoc.populate_template+.
|
|
36
|
+
# 2. Asciidoc-to-XML conversion via {#adoc2xml} (wraps in a
|
|
37
|
+
# headless dummy document, runs an isolated Asciidoctor
|
|
38
|
+
# convert, extracts the +//sections+ subtree).
|
|
39
|
+
# 3. {#boilerplate_snippet_cleanup} extension hook (default
|
|
40
|
+
# identity; standoc overrides for namespace-cleanup +
|
|
41
|
+
# footnote separation).
|
|
42
|
+
# 4. Localisation via +isodoc.i18n.l10n+.
|
|
43
|
+
#
|
|
44
|
+
# @param adoc [String] Snippet of Asciidoc text, possibly
|
|
45
|
+
# containing Liquid expressions like
|
|
46
|
+
# +{% if seriesabbr %}{{seriesabbr}}{% endif %}+.
|
|
47
|
+
# @param isodoc [#populate_template, #i18n] An isodoc converter
|
|
48
|
+
# instance. Must respond to +populate_template(text, options)+
|
|
49
|
+
# for Liquid substitution and +i18n+ (returning an object
|
|
50
|
+
# responding to +l10n(text, lang, script)+).
|
|
51
|
+
# @param lang [String] BCP-47 language tag (e.g. "en") passed to
|
|
52
|
+
# +l10n+. Owned by the caller because the docidentifier-template
|
|
53
|
+
# pipeline runs outside the isodoc converter's own +@lang+
|
|
54
|
+
# state in the collection use case.
|
|
55
|
+
# @param script [String] ISO-15924 script tag (e.g. "Latn"),
|
|
56
|
+
# passed to +l10n+ for the same reason as +lang+.
|
|
57
|
+
# @param backend [Symbol] Asciidoctor backend symbol
|
|
58
|
+
# (e.g. +:standoc+, +:iso+). Determines which converter
|
|
59
|
+
# Asciidoctor dispatches to for the inner conversion.
|
|
60
|
+
# @param flush_caches [Boolean] If true, the dummy document
|
|
61
|
+
# wrapper includes +:flush-caches:+, telling Asciidoctor to
|
|
62
|
+
# discard cached parse results before running this snippet.
|
|
63
|
+
# Standoc threads its converter-level +@flush_caches+ through
|
|
64
|
+
# here. Defaults to false.
|
|
65
|
+
# @param localdir [String, nil] Filesystem path used as the inner
|
|
66
|
+
# conversion's +:base_dir+ if the caller does not supply one
|
|
67
|
+
# explicitly. Standoc passes its +@localdir+; the collection
|
|
68
|
+
# layer passes the collection's +@dirname+.
|
|
69
|
+
# @return [String] The substituted, cleaned-up XML/text snippet
|
|
70
|
+
# ready to splice into the surrounding document.
|
|
71
|
+
def boilerplate_snippet_convert(adoc, isodoc, lang:, script:, backend:,
|
|
72
|
+
flush_caches: false, localdir: nil)
|
|
73
|
+
b = isodoc.populate_template(adoc, nil)
|
|
74
|
+
node = adoc2xml(b, backend, flush_caches: flush_caches,
|
|
75
|
+
localdir: localdir)
|
|
76
|
+
ret = boilerplate_snippet_cleanup(node)
|
|
77
|
+
isodoc.i18n.l10n(ret.children.to_xml, lang, script).strip
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Extension hook invoked by {#boilerplate_snippet_convert} on the
|
|
81
|
+
# output of {#adoc2xml} before localisation. Default
|
|
82
|
+
# implementation is the identity. Standoc's Cleanup::Boilerplate
|
|
83
|
+
# overrides it to apply boilerplate_xml_cleanup and footnote
|
|
84
|
+
# renumbering; the metanorma collection layer leaves it as
|
|
85
|
+
# identity (no standoc-namespace cleanup needed at that stage).
|
|
86
|
+
#
|
|
87
|
+
# @param node [Nokogiri::XML::Node] The +//sections+ subtree
|
|
88
|
+
# returned by Asciidoctor for the snippet.
|
|
89
|
+
# @return [Nokogiri::XML::Node] The (possibly transformed) node
|
|
90
|
+
# whose children will be serialised as the snippet's output.
|
|
91
|
+
def boilerplate_snippet_cleanup(node)
|
|
92
|
+
node
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Iterate over every +<docidentifier @boilerplate>+ element in
|
|
96
|
+
# +xmldoc+ and replace its content with the Liquid-substituted,
|
|
97
|
+
# Asciidoc-rendered output. Called from standoc's cleanup
|
|
98
|
+
# pipeline (post-processing semantic XML) and from the metanorma
|
|
99
|
+
# collection layer (pre-processing the collection bibdata before
|
|
100
|
+
# MergeBibitems hands it to Relaton — see issue
|
|
101
|
+
# https://github.com/metanorma/metanorma/issues/558).
|
|
102
|
+
#
|
|
103
|
+
# The +@boilerplate+ attribute is removed in all matched cases;
|
|
104
|
+
# substitution is performed only when its value is +"true"+.
|
|
105
|
+
# The output of {#boilerplate_snippet_convert} is a serialised
|
|
106
|
+
# +<sections><p>...</p></sections>+; the inner +<p>+ children
|
|
107
|
+
# are spliced into the docidentifier (or the raw output if no
|
|
108
|
+
# +<p>+ was produced).
|
|
109
|
+
#
|
|
110
|
+
# @param xmldoc [Nokogiri::XML::Document, Nokogiri::XML::Node]
|
|
111
|
+
# The document or subtree to scan. **Mutated in place**: every
|
|
112
|
+
# matched +<docidentifier @boilerplate>+ has its
|
|
113
|
+
# +@boilerplate+ attribute stripped, and (when its value was
|
|
114
|
+
# +"true"+) its content replaced with the substituted output.
|
|
115
|
+
# @param isodoc [#populate_template, #i18n] Isodoc instance, see
|
|
116
|
+
# {#boilerplate_snippet_convert}.
|
|
117
|
+
# @param lang [String] see {#boilerplate_snippet_convert}.
|
|
118
|
+
# @param script [String] see {#boilerplate_snippet_convert}.
|
|
119
|
+
# @param backend [Symbol] see {#boilerplate_snippet_convert}.
|
|
120
|
+
# @param flush_caches [Boolean] see {#boilerplate_snippet_convert}.
|
|
121
|
+
# @param localdir [String, nil] see {#boilerplate_snippet_convert}.
|
|
122
|
+
# @return [Nokogiri::XML::Document, Nokogiri::XML::Node] The
|
|
123
|
+
# input +xmldoc+, mutated in place.
|
|
124
|
+
# @see .docidentifier_templates?
|
|
125
|
+
def docidentifier_boilerplate_isodoc(xmldoc, isodoc, lang:, script:,
|
|
126
|
+
backend:, flush_caches: false,
|
|
127
|
+
localdir: nil)
|
|
128
|
+
xmldoc.xpath("//docidentifier[@boilerplate]").each do |d|
|
|
129
|
+
do_substitute = d["boilerplate"] == "true"
|
|
130
|
+
d.delete("boilerplate")
|
|
131
|
+
do_substitute or next
|
|
132
|
+
id = boilerplate_snippet_convert(
|
|
133
|
+
d.children.to_xml, isodoc,
|
|
134
|
+
lang: lang, script: script, backend: backend,
|
|
135
|
+
flush_caches: flush_caches, localdir: localdir,
|
|
136
|
+
)
|
|
137
|
+
p_node = Nokogiri::XML(id).at("//p")
|
|
138
|
+
new_children = p_node ? p_node.children.to_xml : id
|
|
139
|
+
# If the rendered template is blank (e.g. a Liquid template
|
|
140
|
+
# that gates on a missing docnumeric), drop the
|
|
141
|
+
# <docidentifier> entirely instead of leaving an empty
|
|
142
|
+
# element behind. Downstream Relaton flavours
|
|
143
|
+
# (relaton-iho, relaton-cc, …) eagerly call Pubid::*::Identifier.parse
|
|
144
|
+
# on the docidentifier content; an empty string is truthy
|
|
145
|
+
# in Ruby and would crash that parser, so the empty element
|
|
146
|
+
# must not survive this pass.
|
|
147
|
+
if new_children.to_s.strip.empty?
|
|
148
|
+
d.remove
|
|
149
|
+
else
|
|
150
|
+
d.children = new_children
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
xmldoc
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Predicate: are there any +<docidentifier @boilerplate="true">+
|
|
157
|
+
# nodes in +xmldoc+ that {#docidentifier_boilerplate_isodoc} would
|
|
158
|
+
# substitute? Callers use this to decide whether to refresh
|
|
159
|
+
# downstream state (e.g. re-seed +isodoc.meta+ from the resolved
|
|
160
|
+
# bibdata) after the substitution pass — cheap pre-check, avoids
|
|
161
|
+
# depending on a return-value side channel from the mutating
|
|
162
|
+
# substitution method.
|
|
163
|
+
#
|
|
164
|
+
# @param xmldoc [Nokogiri::XML::Document, Nokogiri::XML::Node]
|
|
165
|
+
# @return [Boolean]
|
|
166
|
+
def docidentifier_templates?(xmldoc)
|
|
167
|
+
xmldoc.xpath("//docidentifier[@boilerplate = 'true']").any?
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Wrap +text+ in the standard headless dummy document used across
|
|
171
|
+
# the metanorma stack and run an isolated Asciidoctor convert
|
|
172
|
+
# against the given backend. Returns the +//sections+ subtree as
|
|
173
|
+
# a Nokogiri node so callers can splice its children into a
|
|
174
|
+
# surrounding document.
|
|
175
|
+
#
|
|
176
|
+
# If +text+ is already valid XML (root element parses), it is
|
|
177
|
+
# returned verbatim — this lets callers stash pre-converted XML
|
|
178
|
+
# alongside Asciidoc snippets without a special case.
|
|
179
|
+
#
|
|
180
|
+
# @param text [String] Asciidoc snippet, or already-converted XML.
|
|
181
|
+
# @param flavour [Symbol] Asciidoctor backend.
|
|
182
|
+
# @param flush_caches [Boolean] Add +:flush-caches:+ to the
|
|
183
|
+
# dummy header. See {#boilerplate_snippet_convert}.
|
|
184
|
+
# @param localdir [String, nil] Forwarded to
|
|
185
|
+
# {#isolated_asciidoctor_convert} via the options hash.
|
|
186
|
+
# @return [Nokogiri::XML::Node, String] +//sections+ subtree
|
|
187
|
+
# for converted Asciidoc; original +text+ if input was XML.
|
|
188
|
+
def adoc2xml(text, flavour, flush_caches: false, localdir: nil)
|
|
189
|
+
Nokogiri::XML(text).root and return text
|
|
190
|
+
f = flush_caches ? ":flush-caches:\n" : ""
|
|
191
|
+
doc = <<~ADOC
|
|
192
|
+
= X
|
|
193
|
+
A
|
|
194
|
+
:semantic-metadata-headless: true
|
|
195
|
+
:no-isobib:
|
|
196
|
+
#{f}:novalid:
|
|
197
|
+
:!sectids:
|
|
198
|
+
|
|
199
|
+
#{text}
|
|
200
|
+
ADOC
|
|
201
|
+
c = isolated_asciidoctor_convert(
|
|
202
|
+
doc, backend: flavour, header_footer: true, localdir: localdir,
|
|
203
|
+
)
|
|
204
|
+
Nokogiri::XML(c).at("//xmlns:sections")
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Run +Asciidoctor.convert+ with curated options so that
|
|
208
|
+
# attributes, +base_dir+, and safe-mode setting do NOT leak in
|
|
209
|
+
# from any outer conversion context. Forces +novalid+ for the
|
|
210
|
+
# inner conversion. The conversion stack is tracked in
|
|
211
|
+
# +@isolated_conversion_stack+ for diagnostics; the +ensure+
|
|
212
|
+
# pop guarantees the marker is balanced even on exception.
|
|
213
|
+
#
|
|
214
|
+
# +localdir+ may be passed inside +options+ as +:localdir+; if
|
|
215
|
+
# so it becomes +:base_dir+ for the inner convert (unless the
|
|
216
|
+
# caller supplied an explicit +:base_dir+). The +:localdir+ key
|
|
217
|
+
# is stripped before delegating to +Asciidoctor.convert+ so it
|
|
218
|
+
# does not appear as an unknown option. Callers that include
|
|
219
|
+
# this module from a class with +@localdir+ get +:base_dir+
|
|
220
|
+
# wired up from there as a fallback.
|
|
221
|
+
#
|
|
222
|
+
# @param content [String] Asciidoc input.
|
|
223
|
+
# @param options [Hash] Asciidoctor convert options. Recognised
|
|
224
|
+
# special key: +:localdir+ (used for +:base_dir+ defaulting
|
|
225
|
+
# and stripped before forwarding).
|
|
226
|
+
# @return [String] Asciidoctor convert output.
|
|
227
|
+
def isolated_asciidoctor_convert(content, options = {})
|
|
228
|
+
@isolated_conversion_stack ||= []
|
|
229
|
+
@isolated_conversion_stack << true
|
|
230
|
+
begin
|
|
231
|
+
preserved = extract_preserved_options(options)
|
|
232
|
+
options = options.dup
|
|
233
|
+
options.delete(:localdir)
|
|
234
|
+
isolated = preserved.merge(options).merge(
|
|
235
|
+
attributes: (preserved[:attributes] || {}).merge(
|
|
236
|
+
"novalid" => "",
|
|
237
|
+
),
|
|
238
|
+
)
|
|
239
|
+
::Asciidoctor.convert(content, isolated)
|
|
240
|
+
ensure
|
|
241
|
+
@isolated_conversion_stack.pop
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Compute the option set carried over from outer conversion
|
|
246
|
+
# state: a curated subset (+:safe+, +:base_dir+) plus the
|
|
247
|
+
# SAFE_SHARED_ATTRIBUTES hash if the caller did not supply
|
|
248
|
+
# +:attributes+. Caller's own option hash takes precedence
|
|
249
|
+
# for everything except +"novalid"+, which the caller of
|
|
250
|
+
# {#isolated_asciidoctor_convert} forces.
|
|
251
|
+
#
|
|
252
|
+
# @param user_opt [Hash] Caller-supplied options. Recognised:
|
|
253
|
+
# +:safe+, +:attributes+, +:base_dir+, +:localdir+.
|
|
254
|
+
# @return [Hash] Preserved options to merge in front of
|
|
255
|
+
# +user_opt+ for the inner +Asciidoctor.convert+.
|
|
256
|
+
def extract_preserved_options(user_opt)
|
|
257
|
+
options = {}
|
|
258
|
+
options[:safe] = user_opt[:safe] if user_opt.key?(:safe)
|
|
259
|
+
localdir = user_opt[:localdir] ||
|
|
260
|
+
(defined?(@localdir) ? @localdir : nil)
|
|
261
|
+
if localdir && !user_opt.key?(:base_dir)
|
|
262
|
+
options[:base_dir] = localdir
|
|
263
|
+
end
|
|
264
|
+
if user_opt[:attributes].nil?
|
|
265
|
+
options[:attributes] = SAFE_SHARED_ATTRIBUTES.dup
|
|
266
|
+
end
|
|
267
|
+
options
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Make every method callable as both
|
|
271
|
+
# +Metanorma::Core::Boilerplate.method+ (for the metanorma
|
|
272
|
+
# collection-layer caller, which does not include the module)
|
|
273
|
+
# and as a regular public instance method (for includers like
|
|
274
|
+
# +Metanorma::Standoc::Cleanup::Boilerplate+).
|
|
275
|
+
extend self
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
|
|
5
|
+
module Metanorma
|
|
6
|
+
module Core
|
|
7
|
+
# Locate and load the right flavor gem (e.g. +metanorma-iso+,
|
|
8
|
+
# +metanorma-itu+) for a given standard type, registering its
|
|
9
|
+
# processor with {Metanorma::Registry} as a side effect.
|
|
10
|
+
#
|
|
11
|
+
# Standard types may be specified as a "taste" name
|
|
12
|
+
# (e.g. +:bipm+, +:icc+) which is mapped to its canonical flavor
|
|
13
|
+
# via {Metanorma::TasteRegister}. Flavor gems follow the
|
|
14
|
+
# +metanorma-<flavor>+ naming convention.
|
|
15
|
+
#
|
|
16
|
+
# The +load_flavor+ entry point both resolves and loads, and is
|
|
17
|
+
# idempotent: if the flavor's processor is already registered,
|
|
18
|
+
# no gem load is attempted.
|
|
19
|
+
module FlavorLoader
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
# Resolve a standard type or taste name to its canonical flavor
|
|
23
|
+
# symbol via the TasteRegister.
|
|
24
|
+
#
|
|
25
|
+
# @param stdtype [Symbol, String] standard type or taste name.
|
|
26
|
+
# @return [Symbol] canonical flavor symbol (the input if no
|
|
27
|
+
# taste alias matched).
|
|
28
|
+
def taste2flavor(stdtype)
|
|
29
|
+
stdtype = stdtype.to_sym
|
|
30
|
+
tastes = Metanorma::TasteRegister.instance.aliases
|
|
31
|
+
tastes[stdtype] and stdtype = tastes[stdtype].to_sym
|
|
32
|
+
stdtype
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Map a canonical flavor symbol to its gem name.
|
|
36
|
+
#
|
|
37
|
+
# @param stdtype [Symbol, String] canonical flavor symbol.
|
|
38
|
+
# @return [String] gem name (e.g. +"metanorma-iso"+).
|
|
39
|
+
def stdtype2flavor_gem(stdtype)
|
|
40
|
+
"metanorma-#{stdtype}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Load the flavor gem for +stdtype+ if its processor is not yet
|
|
44
|
+
# registered, and return the canonical flavor symbol.
|
|
45
|
+
#
|
|
46
|
+
# On a fatal LoadError, an error log file is written and
|
|
47
|
+
# +abort+ is called via {Metanorma::Util#log} (severity +:fatal+).
|
|
48
|
+
# If the gem loads but does not register a processor under the
|
|
49
|
+
# expected canonical name, the same fatal-abort path runs.
|
|
50
|
+
#
|
|
51
|
+
# @param stdtype [Symbol, String] standard type or taste name.
|
|
52
|
+
# @return [Symbol] canonical flavor symbol.
|
|
53
|
+
def load_flavor(stdtype)
|
|
54
|
+
canonical = taste2flavor(stdtype)
|
|
55
|
+
gem_name = stdtype2flavor_gem(canonical)
|
|
56
|
+
registry = Metanorma::Registry.instance
|
|
57
|
+
registry.supported_backends.include?(canonical) or
|
|
58
|
+
require_flavor_gem(gem_name, stdtype)
|
|
59
|
+
registry.supported_backends.include?(canonical) or
|
|
60
|
+
flavor_unsupported(gem_name, stdtype)
|
|
61
|
+
canonical
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Require the flavor gem and log success / failure. On +LoadError+,
|
|
65
|
+
# delegates to {.write_flavor_error_log} which produces a fatal
|
|
66
|
+
# abort. Used internally by {.load_flavor}.
|
|
67
|
+
#
|
|
68
|
+
# @param gem_name [String] gem name (e.g. +"metanorma-iso"+).
|
|
69
|
+
# @param stdtype [Symbol, String] standard type for the user-facing
|
|
70
|
+
# info log line.
|
|
71
|
+
def require_flavor_gem(gem_name, stdtype)
|
|
72
|
+
Metanorma::Util.log("[metanorma] Info: Loading `#{gem_name}` gem " \
|
|
73
|
+
"for standard type `#{stdtype}`.", :info)
|
|
74
|
+
require gem_name
|
|
75
|
+
Metanorma::Util.log("[metanorma] Info: gem `#{gem_name}` loaded.",
|
|
76
|
+
:info)
|
|
77
|
+
rescue LoadError => e
|
|
78
|
+
write_flavor_error_log(e, gem_name)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Write a dated error-log file capturing a failed gem load and
|
|
82
|
+
# abort with a user-facing fatal message that points the user at
|
|
83
|
+
# the metanorma issue tracker.
|
|
84
|
+
#
|
|
85
|
+
# @param err [Exception] the LoadError raised by +require+.
|
|
86
|
+
# @param gem_name [String] gem name that failed to load.
|
|
87
|
+
# @return [void] (calls +abort+ via {Metanorma::Util#log}).
|
|
88
|
+
def write_flavor_error_log(err, gem_name)
|
|
89
|
+
error_log = "#{Date.today}-error.log"
|
|
90
|
+
File.write(error_log, err)
|
|
91
|
+
msg = <<~MSG
|
|
92
|
+
Error: #{err.message}
|
|
93
|
+
Metanorma has encountered an exception.
|
|
94
|
+
|
|
95
|
+
If this problem persists, please report this issue at the following link:
|
|
96
|
+
|
|
97
|
+
* https://github.com/metanorma/metanorma/issues/new
|
|
98
|
+
|
|
99
|
+
Please attach the #{error_log} file.
|
|
100
|
+
Your valuable feedback is very much appreciated!
|
|
101
|
+
|
|
102
|
+
- The Metanorma team
|
|
103
|
+
MSG
|
|
104
|
+
Metanorma::Util.log(msg, :fatal)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Fatal-abort path for the case where the flavor gem loaded but
|
|
108
|
+
# did not register a processor for the requested standard type.
|
|
109
|
+
#
|
|
110
|
+
# @param gem_name [String] gem name that loaded.
|
|
111
|
+
# @param stdtype [Symbol, String] standard type that the gem did
|
|
112
|
+
# not register a processor for.
|
|
113
|
+
# @return [void] (calls +abort+ via {Metanorma::Util#log}).
|
|
114
|
+
def flavor_unsupported(gem_name, stdtype)
|
|
115
|
+
Metanorma::Util.log("[metanorma] Error: The `#{gem_name}` gem does " \
|
|
116
|
+
"not support the standard type #{stdtype}. " \
|
|
117
|
+
"Exiting.", :fatal)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "asciidoctor"
|
|
4
|
+
require_relative "flavor_loader"
|
|
5
|
+
|
|
6
|
+
module Metanorma
|
|
7
|
+
module Core
|
|
8
|
+
# Construction and initialisation of an isodoc converter instance for a
|
|
9
|
+
# given Metanorma flavor. Two entry points:
|
|
10
|
+
#
|
|
11
|
+
# - {.resolve_converter} — pick the right converter class for the
|
|
12
|
+
# flavor + output stage (presentation XML or HTML), instantiate it,
|
|
13
|
+
# and return a fresh, *uninitialised* converter.
|
|
14
|
+
# - {.init} — wire i18n, metadata, and xref state onto an existing
|
|
15
|
+
# converter, returning it ready for use.
|
|
16
|
+
#
|
|
17
|
+
# Callers usually need both: +init(resolve_converter(flavor), ...)+.
|
|
18
|
+
# The collection layer's +Util::isodoc_create+ is the canonical wrapper
|
|
19
|
+
# around that pair.
|
|
20
|
+
module Isodoc
|
|
21
|
+
# Stand-in for the Asciidoctor +node+ argument that converters expect
|
|
22
|
+
# when their +presentation_xml_converter+ / +html_converter+ factory
|
|
23
|
+
# methods are invoked outside an actual Asciidoctor conversion. The
|
|
24
|
+
# +attr+ and +attributes+ stubs return empty values so the factory
|
|
25
|
+
# can run without a real document context.
|
|
26
|
+
class EmptyNode
|
|
27
|
+
# @param _ [String] attribute name (ignored).
|
|
28
|
+
# @return [nil]
|
|
29
|
+
def attr(_)
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @return [Hash] empty attribute set.
|
|
34
|
+
def attributes
|
|
35
|
+
{}
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Initialise the i18n / metadata / xref state on an existing
|
|
40
|
+
# converter. Mutates the converter in place and returns it.
|
|
41
|
+
#
|
|
42
|
+
# The converter must already implement the standard Metanorma
|
|
43
|
+
# converter contract: +#init_i18n+, +#i18n_init+, +#metadata_init+,
|
|
44
|
+
# +#meta+, +#xref_init+, +#xrefs+, and (optionally) +#info+.
|
|
45
|
+
#
|
|
46
|
+
# @param converter [Object] An IsoDoc-style converter instance,
|
|
47
|
+
# typically returned by {.resolve_converter}.
|
|
48
|
+
# @param lang [String] BCP-47 language tag (e.g. "en").
|
|
49
|
+
# @param script [String] ISO-15924 script tag (e.g. "Latn").
|
|
50
|
+
# @param locale [String, nil] Optional BCP-47 locale tag for
|
|
51
|
+
# region-specific overrides.
|
|
52
|
+
# @param i18nyaml [Hash, String, nil] Either a parsed i18n hash or
|
|
53
|
+
# a path to a YAML file. Forwarded to +init_i18n+.
|
|
54
|
+
# @param xml [Nokogiri::XML::Node, nil] If supplied, +converter.info+
|
|
55
|
+
# is invoked against it so document-level metadata can be primed.
|
|
56
|
+
# @param localdir [String, nil] If supplied, wired into both
|
|
57
|
+
# +converter.meta+ and +converter.xrefs.klass+ so file lookups
|
|
58
|
+
# resolve relative to the right directory.
|
|
59
|
+
# @return [Object] the same +converter+, fully initialised.
|
|
60
|
+
def self.init(converter, lang:, script:, locale: nil,
|
|
61
|
+
i18nyaml: nil, xml: nil, localdir: nil)
|
|
62
|
+
converter.init_i18n(i18nyaml: i18nyaml, language: lang,
|
|
63
|
+
script: script, locale: locale)
|
|
64
|
+
i18n = converter.i18n_init(lang, script, locale, i18nyaml)
|
|
65
|
+
converter.metadata_init(lang, script, locale, i18n)
|
|
66
|
+
converter.meta.localdir = localdir if localdir
|
|
67
|
+
converter.xref_init(lang, script, nil, i18n, {})
|
|
68
|
+
converter.xrefs.klass.meta = converter.meta
|
|
69
|
+
converter.xrefs.klass.localdir = localdir if localdir
|
|
70
|
+
converter.info(xml, nil) if xml
|
|
71
|
+
converter
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Resolve the right IsoDoc converter for a given Metanorma flavor
|
|
75
|
+
# and output stage, and return a fresh, *uninitialised* instance
|
|
76
|
+
# of it. The flavor's gem (e.g. metanorma-iso) is autoloaded via
|
|
77
|
+
# {Metanorma::Core::FlavorLoader.load_flavor} if it is not already
|
|
78
|
+
# registered.
|
|
79
|
+
#
|
|
80
|
+
# @param flavor [Symbol, String] Metanorma flavor (e.g. +:iso+,
|
|
81
|
+
# +:standoc+) or a taste name resolvable to one.
|
|
82
|
+
# @param presxml [Boolean] If true (default), return the flavor's
|
|
83
|
+
# presentation-XML converter; otherwise its HTML converter.
|
|
84
|
+
# @return [Object] An IsoDoc-style converter instance, ready to
|
|
85
|
+
# pass to {.init}.
|
|
86
|
+
# @raise [RuntimeError] If no Asciidoctor converter is registered
|
|
87
|
+
# for the resolved flavor, or if the converter does not support
|
|
88
|
+
# the requested output stage.
|
|
89
|
+
def self.resolve_converter(flavor, presxml: true)
|
|
90
|
+
resolved = Metanorma::Core::FlavorLoader.load_flavor(flavor)
|
|
91
|
+
conv_class = ::Asciidoctor::Converter.for(resolved.to_s) ||
|
|
92
|
+
::Asciidoctor::Converter.for(resolved)
|
|
93
|
+
conv_class or
|
|
94
|
+
raise "No Asciidoctor converter registered for #{resolved}"
|
|
95
|
+
conv_instance = conv_class.new(resolved.to_s, {})
|
|
96
|
+
method_name = presxml ? :presentation_xml_converter : :html_converter
|
|
97
|
+
conv_instance.respond_to?(method_name) or
|
|
98
|
+
raise "Flavor #{resolved} does not support " \
|
|
99
|
+
"#{presxml ? 'presentation XML' : 'HTML'} conversion"
|
|
100
|
+
conv_instance.send(method_name, EmptyNode.new)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Convenience wrapper combining {.resolve_converter} and {.init}:
|
|
104
|
+
# resolve the converter for +flavor+ and immediately initialise it
|
|
105
|
+
# with the supplied i18n / metadata kwargs. Most callers want this
|
|
106
|
+
# one-stop helper rather than the two-step form.
|
|
107
|
+
#
|
|
108
|
+
# @param flavor [Symbol, String] flavor or taste name; resolved via
|
|
109
|
+
# {Metanorma::Core::FlavorLoader.load_flavor}.
|
|
110
|
+
# @param lang [String] BCP-47 language tag (e.g. "en").
|
|
111
|
+
# @param script [String] ISO-15924 script tag (e.g. "Latn").
|
|
112
|
+
# @param locale [String, nil] optional BCP-47 locale tag.
|
|
113
|
+
# @param i18nyaml [Hash, String, nil] either a parsed i18n hash or
|
|
114
|
+
# a path to a YAML file. Forwarded to +init_i18n+.
|
|
115
|
+
# @param xml [Nokogiri::XML::Node, nil] if supplied, +converter.info+
|
|
116
|
+
# is invoked against it for metadata priming.
|
|
117
|
+
# @param localdir [String, nil] wired into +converter.meta+ and
|
|
118
|
+
# +converter.xrefs.klass+ for relative file-lookup resolution.
|
|
119
|
+
# @param presxml [Boolean] if true (default), return the flavor's
|
|
120
|
+
# presentation-XML converter; otherwise its HTML converter.
|
|
121
|
+
# @return [Object] a fully initialised IsoDoc-style converter.
|
|
122
|
+
# @see https://github.com/metanorma/metanorma/issues/558
|
|
123
|
+
def self.create(flavor, lang:, script:, locale: nil, i18nyaml: nil,
|
|
124
|
+
xml: nil, localdir: nil, presxml: true)
|
|
125
|
+
conv = resolve_converter(flavor, presxml: presxml)
|
|
126
|
+
init(conv, lang: lang, script: script, locale: locale,
|
|
127
|
+
i18nyaml: i18nyaml, xml: xml, localdir: localdir)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -2,7 +2,25 @@ require "nokogiri"
|
|
|
2
2
|
|
|
3
3
|
module Metanorma
|
|
4
4
|
module Input
|
|
5
|
+
# Asciidoc input processor. Wraps Asciidoctor to convert raw
|
|
6
|
+
# Asciidoc source into the flavor's semantic XML, and parses the
|
|
7
|
+
# document header to extract metanorma- and Asciidoctor-level
|
|
8
|
+
# configuration attributes (used by {Metanorma::Processor#extract_options}
|
|
9
|
+
# and friends).
|
|
5
10
|
class Asciidoc < Base
|
|
11
|
+
# Convert +file+ to the target backend's output by running
|
|
12
|
+
# +Asciidoctor.convert+ with safe-mode and metanorma-specific
|
|
13
|
+
# attributes.
|
|
14
|
+
#
|
|
15
|
+
# @param file [String] raw Asciidoc source.
|
|
16
|
+
# @param filename [String] +docfile+ value, used by Asciidoctor
|
|
17
|
+
# for include-relative path resolution.
|
|
18
|
+
# @param type [Symbol] Asciidoctor backend symbol (e.g. +:iso+,
|
|
19
|
+
# +:standoc+).
|
|
20
|
+
# @param options [Hash] passthrough options. Recognised:
|
|
21
|
+
# +:log+ (Asciidoctor logger), +:novalid+ (skip validation),
|
|
22
|
+
# +:output_dir+ (forwarded as the +output_dir+ attribute).
|
|
23
|
+
# @return [String] Asciidoctor convert output.
|
|
6
24
|
def process(file, filename, type, options = {})
|
|
7
25
|
require "asciidoctor"
|
|
8
26
|
out_opts = { to_file: false, safe: :safe, backend: type,
|
|
@@ -13,12 +31,28 @@ module Metanorma
|
|
|
13
31
|
::Asciidoctor.convert(file, out_opts)
|
|
14
32
|
end
|
|
15
33
|
|
|
34
|
+
# Split a raw Asciidoc file into its header and body. The header
|
|
35
|
+
# is everything up to the first blank line.
|
|
36
|
+
#
|
|
37
|
+
# @param file [String] raw Asciidoc source.
|
|
38
|
+
# @return [Array<(String, String), (nil, nil)>] +[header, body]+;
|
|
39
|
+
# +[nil, nil]+ if +file+ does not split.
|
|
16
40
|
def header(file)
|
|
17
41
|
ret = file.split("\n\n", 2) or return [nil, nil]
|
|
18
42
|
ret[0] and ret[0] += "\n"
|
|
19
43
|
[ret[0], ret[1]]
|
|
20
44
|
end
|
|
21
45
|
|
|
46
|
+
# Read metanorma-specific attributes from an Asciidoc header.
|
|
47
|
+
# Supports both bare (e.g. +:document-class:+) and +mn-+ prefixed
|
|
48
|
+
# forms (e.g. +:mn-document-class:+); +document-class+ and +flavor+
|
|
49
|
+
# are aliases. Returns a +.compact+'d hash so absent keys are
|
|
50
|
+
# omitted entirely.
|
|
51
|
+
#
|
|
52
|
+
# @param file [String] raw Asciidoc source (header is
|
|
53
|
+
# re-extracted internally).
|
|
54
|
+
# @return [Hash] subset of the keys +:type+, +:extensions+,
|
|
55
|
+
# +:relaton+, +:asciimath+, +:novalid+.
|
|
22
56
|
def extract_metanorma_options(file)
|
|
23
57
|
hdr, = header(file)
|
|
24
58
|
/\n:(?:mn-)?(?:document-class|flavor):\s+(?<type>\S[^\n]*)\n/ =~ hdr
|
|
@@ -39,10 +73,20 @@ module Metanorma
|
|
|
39
73
|
}.compact
|
|
40
74
|
end
|
|
41
75
|
|
|
76
|
+
# Normalise a bare-attribute form (e.g. +":use-xinclude:"+) so a
|
|
77
|
+
# value-less attribute reads as +"true"+, while a present-value
|
|
78
|
+
# attribute is left in its natural shape.
|
|
79
|
+
#
|
|
80
|
+
# @param attr [String, nil] line matching +":NAME:"+ ...
|
|
81
|
+
# @param name [String] attribute name (without colons).
|
|
82
|
+
# @return [String, nil] +"true"+ if value-less, the value
|
|
83
|
+
# otherwise; +nil+ if +attr+ was +nil+.
|
|
42
84
|
def empty_attr(attr, name)
|
|
43
85
|
attr&.sub(/^#{name}:\s*$/, "#{name}: true")&.sub(/^#{name}:\s+/, "")
|
|
44
86
|
end
|
|
45
87
|
|
|
88
|
+
# Asciidoc attributes whose presence is parsed as a string value
|
|
89
|
+
# (no boolean default). See {#extract_options}.
|
|
46
90
|
ADOC_OPTIONS =
|
|
47
91
|
%w(htmlstylesheet htmlcoverpage htmlintropage scripts
|
|
48
92
|
scripts-override scripts-pdf wordstylesheet i18nyaml
|
|
@@ -61,19 +105,37 @@ module Metanorma
|
|
|
61
105
|
localize-number iso-word-bg-strip-color modspec-identifier-base)
|
|
62
106
|
.freeze
|
|
63
107
|
|
|
108
|
+
# Boolean Asciidoc attributes that default to +true+ if the
|
|
109
|
+
# attribute is bare (no value).
|
|
64
110
|
EMPTY_ADOC_OPTIONS_DEFAULT_TRUE =
|
|
65
111
|
%w(data-uri-image suppress-asciimath-dup use-xinclude
|
|
66
112
|
source-highlighter).freeze
|
|
67
113
|
|
|
114
|
+
# Boolean Asciidoc attributes that default to +false+ if the
|
|
115
|
+
# attribute is bare (no value).
|
|
68
116
|
EMPTY_ADOC_OPTIONS_DEFAULT_FALSE =
|
|
69
117
|
%w(hierarchical-assets break-up-urls-in-tables toc-figures
|
|
70
118
|
toc-tables toc-recommendations).freeze
|
|
71
119
|
|
|
120
|
+
# Convert an Asciidoc-style attribute name (kebab-case, possibly
|
|
121
|
+
# ending in +-override+ or +-pdf+) into the Ruby symbol used in
|
|
122
|
+
# this gem's option hashes.
|
|
123
|
+
#
|
|
124
|
+
# @param name [String] attribute name from Asciidoc header.
|
|
125
|
+
# @return [Symbol]
|
|
72
126
|
def attr_name_normalise(name)
|
|
73
127
|
name.delete("-").sub(/override$/, "_override").sub(/pdf$/, "_pdf")
|
|
74
128
|
.to_sym
|
|
75
129
|
end
|
|
76
130
|
|
|
131
|
+
# Read processor-relevant options from an Asciidoc header. Three
|
|
132
|
+
# categories are scanned: string-valued ({ADOC_OPTIONS}), boolean
|
|
133
|
+
# default-true ({EMPTY_ADOC_OPTIONS_DEFAULT_TRUE}), and boolean
|
|
134
|
+
# default-false ({EMPTY_ADOC_OPTIONS_DEFAULT_FALSE}). The result
|
|
135
|
+
# is +.compact+'d so absent keys are omitted.
|
|
136
|
+
#
|
|
137
|
+
# @param file [String] raw Asciidoc source.
|
|
138
|
+
# @return [Hash{Symbol => String, Boolean}]
|
|
77
139
|
def extract_options(file)
|
|
78
140
|
hdr, = header(file)
|
|
79
141
|
ret = ADOC_OPTIONS.each_with_object({}) do |w, acc|
|
data/lib/metanorma/input/base.rb
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
module Metanorma
|
|
2
2
|
module Input
|
|
3
|
+
# Abstract base for input processors. Concrete subclasses
|
|
4
|
+
# (e.g. {Metanorma::Input::Asciidoc}) override {#process} to
|
|
5
|
+
# convert raw input text into ISODoc semantic XML.
|
|
3
6
|
class Base
|
|
7
|
+
# @param _file [String] raw input contents.
|
|
8
|
+
# @param _filename [String] source filename for relative
|
|
9
|
+
# path resolution.
|
|
10
|
+
# @param _type [Symbol] target backend symbol.
|
|
11
|
+
# @raise [RuntimeError] abstract base — subclasses must override.
|
|
4
12
|
def process(_file, _filename, _type)
|
|
5
13
|
raise "This is an abstract class"
|
|
6
14
|
end
|
|
@@ -2,13 +2,39 @@
|
|
|
2
2
|
#
|
|
3
3
|
|
|
4
4
|
module Metanorma
|
|
5
|
+
# Abstract base class for Metanorma flavor processors. Each flavor
|
|
6
|
+
# gem (metanorma-iso, metanorma-itu, etc.) defines a subclass and
|
|
7
|
+
# registers it with {Metanorma::Registry}; the registry then
|
|
8
|
+
# dispatches input documents to the appropriate processor based on
|
|
9
|
+
# the document's declared flavor / class.
|
|
10
|
+
#
|
|
11
|
+
# Subclasses MUST set +@short+, +@input_format+, and (if Asciidoctor
|
|
12
|
+
# is the input parser) +@asciidoctor_backend+ in their +initialize+,
|
|
13
|
+
# and MAY override {#output_formats}, {#use_presentation_xml},
|
|
14
|
+
# {#options_preprocess}, and {#output} to customise their output
|
|
15
|
+
# pipeline.
|
|
5
16
|
class Processor
|
|
6
|
-
|
|
17
|
+
# @return [Symbol] short name registered with the Registry
|
|
18
|
+
# (e.g. +:iso+, +:itu+).
|
|
19
|
+
attr_reader :short
|
|
7
20
|
|
|
21
|
+
# @return [Symbol] input parser identifier (typically +:asciidoc+).
|
|
22
|
+
attr_reader :input_format
|
|
23
|
+
|
|
24
|
+
# @return [Symbol, nil] Asciidoctor backend symbol used when the
|
|
25
|
+
# processor's input is Asciidoc (e.g. +:iso+, +:standoc+);
|
|
26
|
+
# +nil+ for non-Asciidoc inputs.
|
|
27
|
+
attr_reader :asciidoctor_backend
|
|
28
|
+
|
|
29
|
+
# @raise [RuntimeError] always — concrete flavors must override.
|
|
8
30
|
def initialize
|
|
9
31
|
raise "This is an abstract class!"
|
|
10
32
|
end
|
|
11
33
|
|
|
34
|
+
# Mapping of output-format name to file extension. Subclasses
|
|
35
|
+
# typically extend this with HTML / Word / PDF entries.
|
|
36
|
+
#
|
|
37
|
+
# @return [Hash{Symbol => String}] format symbol -> file extension.
|
|
12
38
|
def output_formats
|
|
13
39
|
{
|
|
14
40
|
xml: "xml",
|
|
@@ -17,6 +43,17 @@ module Metanorma
|
|
|
17
43
|
}
|
|
18
44
|
end
|
|
19
45
|
|
|
46
|
+
# Convert an input file to Metanorma semantic XML by routing it
|
|
47
|
+
# through the {Metanorma::Input::Asciidoc} processor with this
|
|
48
|
+
# processor's Asciidoctor backend. Override for non-Asciidoc
|
|
49
|
+
# inputs.
|
|
50
|
+
#
|
|
51
|
+
# @param file [String] the raw input contents.
|
|
52
|
+
# @param filename [String] the source filename (used for relative
|
|
53
|
+
# path resolution in includes).
|
|
54
|
+
# @param options [Hash] passthrough options for the input
|
|
55
|
+
# processor; see {Metanorma::Input::Asciidoc#process}.
|
|
56
|
+
# @return [String] Metanorma semantic XML.
|
|
20
57
|
def input_to_isodoc(file, filename, options = {})
|
|
21
58
|
Metanorma::Input::Asciidoc.new.process(file, filename,
|
|
22
59
|
@asciidoctor_backend, options)
|
|
@@ -26,6 +63,13 @@ module Metanorma
|
|
|
26
63
|
# raise "This is an abstract class!"
|
|
27
64
|
# end
|
|
28
65
|
|
|
66
|
+
# Whether the given output extension is downstream of the
|
|
67
|
+
# presentation XML stage (and therefore needs presentation XML
|
|
68
|
+
# generated first). Defaults to true for HTML/Word/PDF. Other formats
|
|
69
|
+
# such as RFC and STS are generated directly from semantic XML
|
|
70
|
+
#
|
|
71
|
+
# @param ext [Symbol] output-format symbol.
|
|
72
|
+
# @return [Boolean]
|
|
29
73
|
def use_presentation_xml(ext)
|
|
30
74
|
case ext
|
|
31
75
|
when :html, :doc, :pdf then true
|
|
@@ -34,21 +78,48 @@ module Metanorma
|
|
|
34
78
|
end
|
|
35
79
|
end
|
|
36
80
|
|
|
81
|
+
# Mutate +options+ in place to ensure +:output_formats+ is set.
|
|
82
|
+
# Override to add other flavor-specific defaults.
|
|
83
|
+
#
|
|
84
|
+
# @param options [Hash] processor options.
|
|
85
|
+
# @return [Hash] +options+ with +:output_formats+ defaulted.
|
|
37
86
|
def options_preprocess(options)
|
|
38
87
|
options[:output_formats] ||= output_formats
|
|
39
88
|
end
|
|
40
89
|
|
|
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.
|
|
93
|
+
#
|
|
94
|
+
# @param isodoc_node [String] rendered output content.
|
|
95
|
+
# @param _inname [String] source filename (unused at this base level).
|
|
96
|
+
# @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).
|
|
41
100
|
def output(isodoc_node, _inname, outname, _format, _options = {})
|
|
42
101
|
File.open(outname, "w:UTF-8") { |f| f.write(isodoc_node) }
|
|
43
102
|
end
|
|
44
103
|
|
|
104
|
+
# Read processor-relevant options from the input file's header
|
|
105
|
+
# via {Metanorma::Input::Asciidoc#extract_options}, merging in
|
|
106
|
+
# this processor's +output_formats+ for downstream stages.
|
|
107
|
+
#
|
|
108
|
+
# @param file [String] raw input contents.
|
|
109
|
+
# @return [Hash] extracted options.
|
|
45
110
|
def extract_options(file)
|
|
46
111
|
Metanorma::Input::Asciidoc.new.extract_options(file)
|
|
47
112
|
.merge(output_formats: output_formats)
|
|
48
113
|
end
|
|
49
114
|
|
|
115
|
+
# Read metanorma-specific options from the input file's header
|
|
116
|
+
# via {Metanorma::Input::Asciidoc#extract_metanorma_options}.
|
|
117
|
+
#
|
|
118
|
+
# @param file [String] raw input contents.
|
|
119
|
+
# @return [Hash] metanorma-specific options
|
|
120
|
+
# (+:type+, +:extensions+, +:relaton+, +:asciimath+, +:novalid+).
|
|
50
121
|
def extract_metanorma_options(file)
|
|
51
122
|
Metanorma::Input::Asciidoc.new.extract_metanorma_options(file)
|
|
52
123
|
end
|
|
53
124
|
end
|
|
54
|
-
end
|
|
125
|
+
end
|
data/lib/metanorma/util/util.rb
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
module Metanorma
|
|
2
|
+
# Cross-cutting helpers used by the metanorma stack: logging gated by
|
|
3
|
+
# {Metanorma::Configuration#logs}, ordering of output-format extensions
|
|
4
|
+
# for execution, and recursive hash key normalisation.
|
|
2
5
|
module Util
|
|
3
6
|
class << self
|
|
7
|
+
# Print +message+ to stdout if +type+ is in
|
|
8
|
+
# {Metanorma::Configuration#logs}; abort the process if
|
|
9
|
+
# +type+ is +:fatal+ regardless of whether it was printed.
|
|
10
|
+
#
|
|
11
|
+
# @param message [String] the message to print.
|
|
12
|
+
# @param type [Symbol] severity, defaults to +:info+. Common
|
|
13
|
+
# values: +:info+, +:warning+, +:error+, +:fatal+. The
|
|
14
|
+
# configured +logs+ list determines which severities are
|
|
15
|
+
# printed; +:fatal+ always aborts on top of (or instead of)
|
|
16
|
+
# printing.
|
|
17
|
+
# @return [void]
|
|
4
18
|
def log(message, type = :info)
|
|
5
19
|
log_types = Metanorma.configuration.logs.map(&:to_s) || []
|
|
6
20
|
|
|
@@ -13,7 +27,14 @@ module Metanorma
|
|
|
13
27
|
end
|
|
14
28
|
end
|
|
15
29
|
|
|
16
|
-
#
|
|
30
|
+
# Sort key used to put output-format extensions in execution order.
|
|
31
|
+
# +xml+ runs first because the semantic XML feeds everything else;
|
|
32
|
+
# +rxl+ second; +presentation+ third (consumed by HTML/Word/PDF);
|
|
33
|
+
# everything else last (alphabetical via the caller's sort
|
|
34
|
+
# stability).
|
|
35
|
+
#
|
|
36
|
+
# @param ext [Symbol] output-format extension symbol.
|
|
37
|
+
# @return [Integer] ordinal for sorting.
|
|
17
38
|
def sort_extensions_execution_ord(ext)
|
|
18
39
|
case ext
|
|
19
40
|
when :xml then 0
|
|
@@ -24,12 +45,25 @@ module Metanorma
|
|
|
24
45
|
end
|
|
25
46
|
end
|
|
26
47
|
|
|
48
|
+
# Stable-sort a list of output-format extensions by execution
|
|
49
|
+
# priority (see {.sort_extensions_execution_ord}).
|
|
50
|
+
#
|
|
51
|
+
# @param ext [Array<Symbol>] extensions to sort.
|
|
52
|
+
# @return [Array<Symbol>] sorted in execution order.
|
|
27
53
|
def sort_extensions_execution(ext)
|
|
28
54
|
ext.sort do |a, b|
|
|
29
55
|
sort_extensions_execution_ord(a) <=> sort_extensions_execution_ord(b)
|
|
30
56
|
end
|
|
31
57
|
end
|
|
32
58
|
|
|
59
|
+
# Recursively normalise the keys of a Hash (and any nested Hashes
|
|
60
|
+
# / Enumerables) to Strings. Used to canonicalise YAML/JSON-derived
|
|
61
|
+
# configuration that may have been parsed with mixed symbol /
|
|
62
|
+
# string keys.
|
|
63
|
+
#
|
|
64
|
+
# @param hash [Hash, Enumerable, Object] the value to normalise.
|
|
65
|
+
# @return [Hash, Array, Object] the normalised value; non-collections
|
|
66
|
+
# are returned unchanged.
|
|
33
67
|
def recursive_string_keys(hash)
|
|
34
68
|
case hash
|
|
35
69
|
when Hash then hash.map do |k, v|
|
data/lib/metanorma-core.rb
CHANGED
|
@@ -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_loader"
|
|
12
|
+
require "metanorma/core/isodoc"
|
|
13
|
+
require "metanorma/core/boilerplate"
|
|
11
14
|
|
|
12
15
|
module Metanorma
|
|
13
16
|
end
|
data/metanorma-core.gemspec
CHANGED
|
@@ -25,9 +25,9 @@ Gem::Specification.new do |spec|
|
|
|
25
25
|
end
|
|
26
26
|
spec.required_ruby_version = Gem::Requirement.new(">= 3.1.0")
|
|
27
27
|
|
|
28
|
-
spec.
|
|
29
|
-
spec.
|
|
30
|
-
spec.
|
|
28
|
+
spec.add_dependency "asciidoctor"
|
|
29
|
+
spec.add_dependency "metanorma-taste", "~> 1.0.0"
|
|
30
|
+
spec.add_dependency "nokogiri"
|
|
31
31
|
|
|
32
32
|
spec.add_development_dependency "debug"
|
|
33
33
|
spec.add_development_dependency "rake", "~> 13.0"
|
|
@@ -35,4 +35,5 @@ Gem::Specification.new do |spec|
|
|
|
35
35
|
spec.add_development_dependency "rubocop", "~> 1"
|
|
36
36
|
spec.add_development_dependency "rubocop-performance"
|
|
37
37
|
spec.add_development_dependency "simplecov", "~> 0.15"
|
|
38
|
+
#spec.metadata["rubygems_mfa_required"] = "true"
|
|
38
39
|
end
|
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.
|
|
4
|
+
version: 0.2.0
|
|
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-
|
|
11
|
+
date: 2026-05-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: asciidoctor
|
|
@@ -25,33 +25,33 @@ dependencies:
|
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '0'
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
|
-
name:
|
|
28
|
+
name: metanorma-taste
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
30
30
|
requirements:
|
|
31
|
-
- - "
|
|
31
|
+
- - "~>"
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version:
|
|
33
|
+
version: 1.0.0
|
|
34
34
|
type: :runtime
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
|
-
- - "
|
|
38
|
+
- - "~>"
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version:
|
|
40
|
+
version: 1.0.0
|
|
41
41
|
- !ruby/object:Gem::Dependency
|
|
42
|
-
name:
|
|
42
|
+
name: nokogiri
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
|
44
44
|
requirements:
|
|
45
|
-
- - "
|
|
45
|
+
- - ">="
|
|
46
46
|
- !ruby/object:Gem::Version
|
|
47
|
-
version:
|
|
47
|
+
version: '0'
|
|
48
48
|
type: :runtime
|
|
49
49
|
prerelease: false
|
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
51
|
requirements:
|
|
52
|
-
- - "
|
|
52
|
+
- - ">="
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
|
-
version:
|
|
54
|
+
version: '0'
|
|
55
55
|
- !ruby/object:Gem::Dependency
|
|
56
56
|
name: debug
|
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -154,6 +154,9 @@ files:
|
|
|
154
154
|
- lib/metanorma/asciidoctor_extensions.rb
|
|
155
155
|
- lib/metanorma/asciidoctor_extensions/glob_include_processor.rb
|
|
156
156
|
- lib/metanorma/config/config.rb
|
|
157
|
+
- lib/metanorma/core/boilerplate.rb
|
|
158
|
+
- lib/metanorma/core/flavor_loader.rb
|
|
159
|
+
- lib/metanorma/core/isodoc.rb
|
|
157
160
|
- lib/metanorma/core/version.rb
|
|
158
161
|
- lib/metanorma/input.rb
|
|
159
162
|
- lib/metanorma/input/asciidoc.rb
|