metanorma-document 0.3.0 → 0.5.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/BUGS.sts/08-list-items-duplicate-bullets.md +45 -0
- data/CLAUDE.md +3 -2
- data/Rakefile +30 -5
- data/data/stylesheets/components/header.css +3 -5
- data/data/stylesheets/components/lists.css +14 -0
- data/frontend/dist/app.css +1 -0
- data/frontend/dist/app.iife.js +24 -0
- data/frontend/package-lock.json +7 -505
- data/frontend/tailwind.config.js +5 -0
- data/lib/metanorma/{oiml_document → csa_document}/root.rb +2 -2
- data/lib/metanorma/csa_document.rb +9 -0
- data/lib/metanorma/document/cli.rb +52 -0
- data/lib/metanorma/document/components/ancillary_blocks/sourcecode_body.rb +14 -0
- data/lib/metanorma/document/components/inline/comma_element.rb +3 -0
- data/lib/metanorma/document/components/inline/enum_comma_element.rb +3 -0
- data/lib/metanorma/document/components/inline/eref_element.rb +4 -1
- data/lib/metanorma/document/components/inline/fmt_termsource_element.rb +4 -2
- data/lib/metanorma/document/components/inline/math_element.rb +13 -7
- data/lib/metanorma/document/components/inline/vocabulary.rb +4 -0
- data/lib/metanorma/document/components/multi_paragraph/admonition_block.rb +6 -1
- data/lib/metanorma/document/components/paragraphs/paragraph_block.rb +3 -0
- data/lib/metanorma/document/components/reference_elements/callout.rb +3 -2
- data/lib/metanorma/document/components/tables/table_cell.rb +3 -0
- data/lib/metanorma/document/elements/input.rb +44 -0
- data/lib/metanorma/document/elements.rb +1 -0
- data/lib/metanorma/document/relaton/address.rb +5 -0
- data/lib/metanorma/document/relaton/affiliation.rb +3 -0
- data/lib/metanorma/document/relaton/bib_item_size.rb +8 -9
- data/lib/metanorma/document/relaton/bib_item_type.rb +3 -0
- data/lib/metanorma/document/relaton/bibliographic_date.rb +5 -0
- data/lib/metanorma/document/relaton/contact_method.rb +5 -0
- data/lib/metanorma/document/relaton/contribution_info.rb +6 -2
- data/lib/metanorma/document/relaton/contributor.rb +4 -0
- data/lib/metanorma/document/relaton/contributor_role.rb +3 -0
- data/lib/metanorma/document/relaton/copyright_association.rb +3 -0
- data/lib/metanorma/document/relaton/copyright_owner.rb +3 -0
- data/lib/metanorma/document/relaton/date_time.rb +3 -0
- data/lib/metanorma/document/relaton/document_identifier.rb +4 -0
- data/lib/metanorma/document/relaton/document_relation.rb +8 -0
- data/lib/metanorma/document/relaton/document_status.rb +4 -0
- data/lib/metanorma/document/relaton/formatted_address.rb +4 -0
- data/lib/metanorma/document/relaton/full_name.rb +14 -3
- data/lib/metanorma/document/relaton/keyword_type.rb +3 -0
- data/lib/metanorma/document/relaton/locality_stack.rb +6 -9
- data/lib/metanorma/document/relaton/localized_name.rb +8 -5
- data/lib/metanorma/document/relaton/logo_element.rb +5 -0
- data/lib/metanorma/document/relaton/medium_type.rb +5 -16
- data/lib/metanorma/document/relaton/org_identifier.rb +2 -0
- data/lib/metanorma/document/relaton/org_subdivision.rb +2 -0
- data/lib/metanorma/document/relaton/org_subdivision_identifier.rb +2 -0
- data/lib/metanorma/document/relaton/organization.rb +3 -0
- data/lib/metanorma/document/relaton/person.rb +7 -1
- data/lib/metanorma/document/relaton/person_identifier.rb +3 -0
- data/lib/metanorma/document/relaton/place_type.rb +5 -0
- data/lib/metanorma/document/relaton/region_element.rb +3 -9
- data/lib/metanorma/document/relaton/relaton_collection.rb +2 -0
- data/lib/metanorma/document/relaton/series_type.rb +7 -0
- data/lib/metanorma/document/relaton/typed_note.rb +4 -0
- data/lib/metanorma/document/relaton/typed_title_string.rb +4 -0
- data/lib/metanorma/document/relaton/typed_uri.rb +3 -8
- data/lib/metanorma/document/relaton/validity_type.rb +8 -10
- data/lib/metanorma/document/relaton/variant_full_name.rb +3 -0
- data/lib/metanorma/document/relaton/variant_org_name.rb +3 -0
- data/lib/metanorma/document/relaton/version_info.rb +5 -0
- data/lib/metanorma/document/relaton/vocab_id_type.rb +6 -12
- data/lib/metanorma/document/relaton.rb +0 -14
- data/lib/metanorma/document/version.rb +1 -1
- data/lib/metanorma/document.rb +2 -1
- data/lib/metanorma/html/asset_pipeline.rb +1 -0
- data/lib/metanorma/html/base_renderer.rb +106 -236
- data/lib/metanorma/html/concerns/metadata_extraction.rb +61 -0
- data/lib/metanorma/html/concerns/presentation_validation.rb +61 -0
- data/lib/metanorma/html/concerns/svg_processing.rb +32 -0
- data/lib/metanorma/html/concerns/text_extraction.rb +93 -0
- data/lib/metanorma/html/concerns/toc_registry.rb +47 -0
- data/lib/metanorma/html/csa_renderer.rb +9 -0
- data/lib/metanorma/html/drops/figure_drop.rb +33 -10
- data/lib/metanorma/html/drops/sourcecode_drop.rb +3 -6
- data/lib/metanorma/html/flavor.rb +6 -1
- data/lib/metanorma/html/generator.rb +8 -8
- data/lib/metanorma/html/iso_renderer.rb +0 -142
- data/lib/metanorma/html/renderers/block_renderer.rb +41 -5
- data/lib/metanorma/html/renderers/element_order_traversal.rb +15 -1
- data/lib/metanorma/html/renderers/inline_renderer.rb +16 -76
- data/lib/metanorma/html/standard_renderer.rb +195 -35
- data/lib/metanorma/html/templates/_list_item.html.liquid +1 -1
- data/lib/metanorma/html/theme.rb +38 -11
- data/lib/metanorma/html.rb +1 -52
- data/lib/metanorma/iso_document/sections/iso_preface.rb +27 -13
- data/lib/metanorma/mirror/default_registry.rb +21 -0
- data/lib/metanorma/mirror/handlers/inline/catalog.rb +7 -12
- data/lib/metanorma/mirror/handlers/inline/rich_html_renderer.rb +2 -3
- data/lib/metanorma/mirror/handlers/sourcecode.rb +1 -1
- data/lib/metanorma/mirror/model/guide.rb +3 -2
- data/lib/metanorma/mirror/output/formats/inline_format.rb +64 -3
- data/lib/metanorma/mirror/output/pipeline.rb +1 -0
- data/lib/metanorma/mirror/output.rb +0 -2
- data/lib/metanorma/mirror/rewriter.rb +3 -1
- data/lib/metanorma/registers/setup.rb +6 -2
- data/lib/metanorma/standard_document/annotation_container.rb +2 -0
- data/lib/metanorma/standard_document/elements/input.rb +4 -25
- data/lib/metanorma/standard_document/metadata/metanorma_extension.rb +12 -2
- data/lib/metanorma/standard_document/metadata/presentation_metadata.rb +107 -0
- data/lib/metanorma/standard_document/metadata/semantic_metadata.rb +18 -0
- data/lib/metanorma/standard_document/metadata/unitsml/amount_of_substance.rb +19 -0
- data/lib/metanorma/standard_document/metadata/unitsml/base_quantity.rb +34 -0
- data/lib/metanorma/standard_document/metadata/unitsml/dimension.rb +39 -0
- data/lib/metanorma/standard_document/metadata/unitsml/dimension_set.rb +19 -0
- data/lib/metanorma/standard_document/metadata/unitsml/electric_current.rb +19 -0
- data/lib/metanorma/standard_document/metadata/unitsml/enumerated_root_unit.rb +23 -0
- data/lib/metanorma/standard_document/metadata/unitsml/length.rb +19 -0
- data/lib/metanorma/standard_document/metadata/unitsml/luminous_intensity.rb +19 -0
- data/lib/metanorma/standard_document/metadata/unitsml/mass.rb +19 -0
- data/lib/metanorma/standard_document/metadata/unitsml/namespace.rb +17 -0
- data/lib/metanorma/standard_document/metadata/unitsml/plane_angle.rb +19 -0
- data/lib/metanorma/standard_document/metadata/unitsml/prefix.rb +30 -0
- data/lib/metanorma/standard_document/metadata/unitsml/prefix_name.rb +21 -0
- data/lib/metanorma/standard_document/metadata/unitsml/prefix_set.rb +19 -0
- data/lib/metanorma/standard_document/metadata/unitsml/prefix_symbol.rb +21 -0
- data/lib/metanorma/standard_document/metadata/unitsml/quantity.rb +28 -0
- data/lib/metanorma/standard_document/metadata/unitsml/quantity_name.rb +21 -0
- data/lib/metanorma/standard_document/metadata/unitsml/quantity_set.rb +19 -0
- data/lib/metanorma/standard_document/metadata/unitsml/root_units.rb +19 -0
- data/lib/metanorma/standard_document/metadata/unitsml/thermodynamic_temperature.rb +19 -0
- data/lib/metanorma/standard_document/metadata/unitsml/time.rb +19 -0
- data/lib/metanorma/standard_document/metadata/unitsml/unit.rb +32 -0
- data/lib/metanorma/standard_document/metadata/unitsml/unit_name.rb +21 -0
- data/lib/metanorma/standard_document/metadata/unitsml/unit_set.rb +19 -0
- data/lib/metanorma/standard_document/metadata/unitsml/unit_symbol.rb +36 -0
- data/lib/metanorma/standard_document/metadata/unitsml/unit_system.rb +23 -0
- data/lib/metanorma/standard_document/metadata/unitsml/unitsml_root.rb +28 -0
- data/lib/metanorma/standard_document/metadata/unitsml.rb +39 -0
- data/lib/metanorma/standard_document/metadata.rb +4 -0
- data/lib/metanorma/standard_document/sections/misc_container.rb +8 -6
- data/lib/metanorma/standard_document/sections/sections.rb +7 -0
- data/lib/metanorma/un_document/blocks/un_admonition_block.rb +86 -0
- data/lib/metanorma/un_document/blocks.rb +9 -0
- data/lib/metanorma/un_document/root.rb +8 -0
- data/lib/metanorma/un_document/sections/un_abstract_section.rb +48 -0
- data/lib/metanorma/un_document/sections/un_preface.rb +19 -2
- data/lib/metanorma/un_document/sections.rb +1 -0
- data/lib/metanorma/un_document/un_text_element.rb +116 -0
- data/lib/metanorma/un_document.rb +2 -0
- data/script/benchmark_render.rb +35 -0
- data/tasks/roundtrip_samples.rake +2 -2
- metadata +65 -54
- data/.rubocop_todo.yml +0 -220
- data/TODO.finalize/01-split-multi-class-files.md +0 -8
- data/TODO.finalize/02-remove-data-dist-iso-document-frontend.md +0 -6
- data/TODO.finalize/03-align-frontend-iife-and-gha-build.md +0 -11
- data/TODO.finalize/04-theme-per-flavor-config-driven.md +0 -9
- data/TODO.finalize/05-migrate-all-html-to-liquid.md +0 -16
- data/TODO.finalize/06-integrate-pubid-library.md +0 -16
- data/TODO.finalize/07-replace-relaton-models-with-relaton-bib.md +0 -39
- data/TODO.finalize/08-refactor-renderer-architecture.md +0 -25
- data/TODO.finalize/09-eliminate-code-smells.md +0 -14
- data/TODO.finalize/10-comprehensive-spec-coverage.md +0 -24
- data/TODO.finalize/11-config-driven-flavor-metadata.md +0 -20
- data/TODO.finalize/12-add-frozen-string-literal.md +0 -6
- data/TODO.finalize/13-extract-composition-from-god-class.md +0 -96
- data/TODO.finalize/14-config-driven-section-ordering.md +0 -31
- data/TODO.finalize/15-theme-directories-per-flavor.md +0 -46
- data/TODO.finalize/16-theme-lutaml-model-schema.md +0 -44
- data/data/logos/oiml-logo-full-dark.svg +0 -1
- data/data/logos/oiml-logo-full-light.svg +0 -1
- data/data/logos/oiml-logo-icon-dark.svg +0 -1
- data/data/logos/oiml-logo-icon-light.svg +0 -1
- data/data/logos/oiml-logo.svg +0 -1
- data/data/themes/oiml.yaml +0 -56
- data/docs/html-renderer.adoc +0 -261
- data/lib/data/dist/iso_document/frontend/assets/index-mwzbzmnK.js +0 -22
- data/lib/data/dist/iso_document/frontend/assets/index-pGvKTNid.css +0 -1
- data/lib/data/dist/iso_document/frontend/assets/iso-red.svg +0 -53
- data/lib/data/dist/iso_document/frontend/index.html +0 -13
- data/lib/metanorma/document/relaton/bib_item_size_type.rb +0 -21
- data/lib/metanorma/document/relaton/bibliographic_date_type.rb +0 -22
- data/lib/metanorma/document/relaton/document_relation_type.rb +0 -31
- data/lib/metanorma/document/relaton/iso4217_code.rb +0 -17
- data/lib/metanorma/document/relaton/iso8601_date.rb +0 -17
- data/lib/metanorma/document/relaton/personal_identifier_type.rb +0 -21
- data/lib/metanorma/document/relaton/region_type.rb +0 -20
- data/lib/metanorma/document/relaton/series_type_type.rb +0 -21
- data/lib/metanorma/document/relaton/specific_locality_type.rb +0 -25
- data/lib/metanorma/document/relaton/title_type.rb +0 -21
- data/lib/metanorma/html/oiml_renderer.rb +0 -33
- data/lib/metanorma/mirror/output/html_renderer.rb +0 -95
- data/lib/metanorma/mirror/output/html_renderers/block_renderers.rb +0 -178
- data/lib/metanorma/mirror/output/html_renderers/inline_renderer.rb +0 -61
- data/lib/metanorma/mirror/output/html_renderers/list_renderers.rb +0 -96
- data/lib/metanorma/mirror/output/html_renderers/mark_renderers.rb +0 -41
- data/lib/metanorma/mirror/output/html_renderers/section_renderers.rb +0 -129
- data/lib/metanorma/mirror/output/html_renderers/structural_renderers.rb +0 -80
- data/lib/metanorma/mirror/output/html_renderers/table_renderers.rb +0 -87
- data/lib/metanorma/mirror/output/html_renderers.rb +0 -76
- data/lib/metanorma/oiml_document.rb +0 -9
- data/lib/metanorma/standard_document/annotation.rb +0 -37
|
@@ -15,7 +15,23 @@ module Metanorma
|
|
|
15
15
|
"metanorma/html/renderers/element_order_traversal"
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
module Concerns
|
|
19
|
+
autoload :MetadataExtraction,
|
|
20
|
+
"metanorma/html/concerns/metadata_extraction"
|
|
21
|
+
autoload :PresentationValidation,
|
|
22
|
+
"metanorma/html/concerns/presentation_validation"
|
|
23
|
+
autoload :SvgProcessing, "metanorma/html/concerns/svg_processing"
|
|
24
|
+
autoload :TextExtraction, "metanorma/html/concerns/text_extraction"
|
|
25
|
+
autoload :TocRegistry, "metanorma/html/concerns/toc_registry"
|
|
26
|
+
end
|
|
27
|
+
|
|
18
28
|
class BaseRenderer
|
|
29
|
+
include Concerns::MetadataExtraction
|
|
30
|
+
include Concerns::PresentationValidation
|
|
31
|
+
include Concerns::SvgProcessing
|
|
32
|
+
include Concerns::TextExtraction
|
|
33
|
+
include Concerns::TocRegistry
|
|
34
|
+
|
|
19
35
|
LOGO_DIR = File.expand_path("../../../data/logos", __dir__)
|
|
20
36
|
|
|
21
37
|
SPAN_ROLE_CLASSES = {
|
|
@@ -115,14 +131,11 @@ module Metanorma
|
|
|
115
131
|
@renderer_context ||= RendererContext.new(self)
|
|
116
132
|
end
|
|
117
133
|
|
|
118
|
-
def toc_entries
|
|
119
|
-
@toc_entries
|
|
120
|
-
end
|
|
121
|
-
|
|
122
134
|
attr_writer :document, :theme
|
|
123
135
|
|
|
124
|
-
def generate_full_document(document, **)
|
|
136
|
+
def generate_full_document(document, theme: nil, **)
|
|
125
137
|
@document = document
|
|
138
|
+
@theme_override = theme
|
|
126
139
|
validate_presentation_xml!
|
|
127
140
|
|
|
128
141
|
body = render(@document) || ""
|
|
@@ -130,6 +143,14 @@ module Metanorma
|
|
|
130
143
|
assemble_document(body)
|
|
131
144
|
end
|
|
132
145
|
|
|
146
|
+
# Renders only the document body content (no html/head/styles
|
|
147
|
+
# assembly) — for embedding classic-rendered content into a host page.
|
|
148
|
+
def generate_body(document, **)
|
|
149
|
+
@document = document
|
|
150
|
+
validate_presentation_xml!
|
|
151
|
+
render(@document) || ""
|
|
152
|
+
end
|
|
153
|
+
|
|
133
154
|
# --- Flavor configuration hooks ---
|
|
134
155
|
|
|
135
156
|
def theme
|
|
@@ -137,10 +158,27 @@ module Metanorma
|
|
|
137
158
|
end
|
|
138
159
|
|
|
139
160
|
def resolve_theme
|
|
161
|
+
return load_theme_override(@theme_override) if @theme_override
|
|
162
|
+
|
|
140
163
|
flavor = flavor_name
|
|
141
164
|
flavor ? Theme.load(flavor) : Theme.new
|
|
142
165
|
end
|
|
143
166
|
|
|
167
|
+
# A theme override given to Generator.generate: a Theme instance,
|
|
168
|
+
# a path to a theme directory (theme.yaml with assets/, templates/
|
|
169
|
+
# and custom.css alongside), or a path to a bare theme.yaml file.
|
|
170
|
+
# This is how an organization customizes the output without
|
|
171
|
+
# changing any code.
|
|
172
|
+
def load_theme_override(override)
|
|
173
|
+
return override if override.is_a?(Theme)
|
|
174
|
+
|
|
175
|
+
if File.directory?(override)
|
|
176
|
+
Theme.from_theme_dir(override)
|
|
177
|
+
else
|
|
178
|
+
Theme.from_file(override)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
144
182
|
def flavor_name
|
|
145
183
|
return nil unless defined?(@document) && @document
|
|
146
184
|
|
|
@@ -257,25 +295,6 @@ module Metanorma
|
|
|
257
295
|
end.join("\n")
|
|
258
296
|
end
|
|
259
297
|
|
|
260
|
-
def load_logo_svg(filename, height: 32)
|
|
261
|
-
path = theme.resolve_asset(filename) || File.join(LOGO_DIR, filename)
|
|
262
|
-
return nil unless File.exist?(path)
|
|
263
|
-
|
|
264
|
-
svg = File.read(path)
|
|
265
|
-
svg = svg.sub(/\A<\?xml[^?]*\?>\s*/, "")
|
|
266
|
-
svg = svg.sub(/\A\s*<!--.*?-->\s*/m, "")
|
|
267
|
-
svg = svg.sub(/<svg\s/, '<svg class="header-logo" ')
|
|
268
|
-
svg = if svg.match?(/<svg[^>]*\sheight="[^"]*"/)
|
|
269
|
-
svg.sub(/(<svg[^>]*?)(\sheight="[^"]*")/,
|
|
270
|
-
"\\1 height=\"#{height}\"")
|
|
271
|
-
else
|
|
272
|
-
svg.sub(/(<svg\b)/, "\\1 height=\"#{height}\"")
|
|
273
|
-
end
|
|
274
|
-
svg.sub(/(<svg[^>]*?)\swidth="[^"]*"/, '\1')
|
|
275
|
-
rescue StandardError
|
|
276
|
-
nil
|
|
277
|
-
end
|
|
278
|
-
|
|
279
298
|
def build_footer
|
|
280
299
|
mn_logo = load_logo_svg(METANORMA_LOGO, height: 20)
|
|
281
300
|
render_liquid("_footer.html.liquid", {
|
|
@@ -284,22 +303,6 @@ module Metanorma
|
|
|
284
303
|
})
|
|
285
304
|
end
|
|
286
305
|
|
|
287
|
-
# --- ToC generation ---
|
|
288
|
-
|
|
289
|
-
def build_toc_html(entries)
|
|
290
|
-
entry_drops = entries.map { |e| Drops::TocEntryDrop.new(e) }
|
|
291
|
-
figure_drops = @figure_entries.map { |f| Drops::FigureListEntryDrop.new(f) }
|
|
292
|
-
table_drops = @table_entries.map { |t| Drops::FigureListEntryDrop.new(t) }
|
|
293
|
-
has_special_lists = !@figure_entries.empty? || !@table_entries.empty?
|
|
294
|
-
|
|
295
|
-
render_liquid("_toc.html.liquid", {
|
|
296
|
-
"entries" => entry_drops,
|
|
297
|
-
"figures" => figure_drops,
|
|
298
|
-
"tables" => table_drops,
|
|
299
|
-
"has_special_lists" => has_special_lists,
|
|
300
|
-
})
|
|
301
|
-
end
|
|
302
|
-
|
|
303
306
|
# --- Scripts ---
|
|
304
307
|
|
|
305
308
|
def build_scripts
|
|
@@ -325,177 +328,28 @@ module Metanorma
|
|
|
325
328
|
parts.join("\n")
|
|
326
329
|
end
|
|
327
330
|
|
|
328
|
-
# --- Validation ---
|
|
329
|
-
|
|
330
|
-
def validate_presentation_xml!
|
|
331
|
-
has_presentation = check_presentation_markers(@document)
|
|
332
|
-
return if has_presentation
|
|
333
|
-
|
|
334
|
-
raise ArgumentError,
|
|
335
|
-
"HTML generation requires Presentation XML input. " \
|
|
336
|
-
"Semantic XML does not contain formatting data needed for HTML. " \
|
|
337
|
-
"Use a '.presentation.xml' file instead."
|
|
338
|
-
end
|
|
339
|
-
|
|
340
|
-
def check_presentation_markers(node)
|
|
341
|
-
return false unless node
|
|
342
|
-
return false if node.is_a?(String)
|
|
343
|
-
|
|
344
|
-
if node.is_a?(Lutaml::Model::Serializable)
|
|
345
|
-
node_attrs = node.class.attributes
|
|
346
|
-
if node_attrs.key?(:type) && node.type == "presentation"
|
|
347
|
-
return true
|
|
348
|
-
end
|
|
349
|
-
if node_attrs.key?(:fmt_title) && node.fmt_title
|
|
350
|
-
return true
|
|
351
|
-
end
|
|
352
|
-
if node_attrs.key?(:displayorder) && node.displayorder
|
|
353
|
-
return true
|
|
354
|
-
end
|
|
355
|
-
|
|
356
|
-
%i[preface sections annex bibliography].each do |attr|
|
|
357
|
-
next unless node_attrs.key?(attr)
|
|
358
|
-
|
|
359
|
-
val = node.public_send(attr)
|
|
360
|
-
next unless val
|
|
361
|
-
|
|
362
|
-
Array(val).each { |v| return true if check_presentation_markers(v) }
|
|
363
|
-
end
|
|
364
|
-
|
|
365
|
-
node.each_mixed_content do |child|
|
|
366
|
-
next if child.is_a?(String)
|
|
367
|
-
return true if check_presentation_markers(child)
|
|
368
|
-
end
|
|
369
|
-
end
|
|
370
|
-
|
|
371
|
-
false
|
|
372
|
-
end
|
|
373
|
-
|
|
374
|
-
# --- Metadata extraction ---
|
|
375
|
-
|
|
376
|
-
def language
|
|
377
|
-
bibdata = @document.bibdata
|
|
378
|
-
return "en" unless bibdata
|
|
379
|
-
|
|
380
|
-
langs = bibdata.language
|
|
381
|
-
if langs && !langs.empty?
|
|
382
|
-
lang = langs.find { |l| l.current == "true" } || langs.first
|
|
383
|
-
lang.value || lang.to_s
|
|
384
|
-
else
|
|
385
|
-
"en"
|
|
386
|
-
end
|
|
387
|
-
end
|
|
388
|
-
|
|
389
|
-
def html_title
|
|
390
|
-
extract_display_title(@document.bibdata) || "Document"
|
|
391
|
-
end
|
|
392
|
-
|
|
393
|
-
def extract_display_title(bibdata)
|
|
394
|
-
return nil unless bibdata
|
|
395
|
-
|
|
396
|
-
title = bibdata.title_for("en") if bibdata.is_a?(Metanorma::Document::Components::BibData::BibData)
|
|
397
|
-
return title.to_s if title && !title.to_s.empty?
|
|
398
|
-
|
|
399
|
-
titles = safe_attr(bibdata, :title)
|
|
400
|
-
return nil unless titles && !titles.is_a?(String) && !titles.empty?
|
|
401
|
-
|
|
402
|
-
en = titles.find { |t| safe_attr(t, :language) == "en" }
|
|
403
|
-
found = en || titles.first
|
|
404
|
-
extract_text_value(found).to_s
|
|
405
|
-
end
|
|
406
|
-
|
|
407
|
-
def extract_primary_doc_id
|
|
408
|
-
bibdata = @document.bibdata
|
|
409
|
-
return nil unless bibdata
|
|
410
|
-
|
|
411
|
-
identifiers = bibdata.doc_identifier
|
|
412
|
-
return nil unless identifiers && !identifiers.empty?
|
|
413
|
-
|
|
414
|
-
first_id = identifiers.first
|
|
415
|
-
text = if first_id.is_a?(String)
|
|
416
|
-
first_id
|
|
417
|
-
elsif first_id.is_a?(Lutaml::Model::Serializable)
|
|
418
|
-
Array(first_id.value).join
|
|
419
|
-
else
|
|
420
|
-
first_id.to_s
|
|
421
|
-
end
|
|
422
|
-
text.strip.empty? ? nil : text.strip
|
|
423
|
-
end
|
|
424
|
-
|
|
425
|
-
# --- Registration helpers ---
|
|
426
|
-
|
|
427
|
-
def register_toc_entry(id:, level:, text:)
|
|
428
|
-
@toc_entries << { id: id, level: level, text: text }
|
|
429
|
-
end
|
|
430
|
-
|
|
431
|
-
def register_figure_entry(id:, text:)
|
|
432
|
-
@figure_entries << { id: id, text: text }
|
|
433
|
-
end
|
|
434
|
-
|
|
435
|
-
def figure_entries
|
|
436
|
-
@figure_entries
|
|
437
|
-
end
|
|
438
|
-
|
|
439
|
-
def register_table_entry(id:, text:)
|
|
440
|
-
@table_entries << { id: id, text: text }
|
|
441
|
-
end
|
|
442
|
-
|
|
443
|
-
def extract_plain_text(node)
|
|
444
|
-
return node.to_s if node.is_a?(String)
|
|
445
|
-
return extract_text_value(node).to_s unless node.is_a?(Lutaml::Model::Serializable)
|
|
446
|
-
|
|
447
|
-
parts = []
|
|
448
|
-
xml_mapping = node.class.mappings_for(:xml, node.lutaml_register)
|
|
449
|
-
|
|
450
|
-
if node.element_order.is_a?(Array) && xml_mapping
|
|
451
|
-
element_to_attr =
|
|
452
|
-
Renderers::ElementOrderTraversal.element_to_attr_map(xml_mapping)
|
|
453
|
-
|
|
454
|
-
indices = Hash.new(0)
|
|
455
|
-
node.element_order.each do |el|
|
|
456
|
-
next unless el.is_a?(Lutaml::Xml::Element)
|
|
457
|
-
|
|
458
|
-
if el.text?
|
|
459
|
-
parts << el.text_content.to_s
|
|
460
|
-
elsif el.name == "tab"
|
|
461
|
-
parts << " "
|
|
462
|
-
elsif el.name == "br"
|
|
463
|
-
parts << " "
|
|
464
|
-
elsif el.element?
|
|
465
|
-
attr_name = element_to_attr[el.name]
|
|
466
|
-
if attr_name
|
|
467
|
-
coll = node.public_send(attr_name)
|
|
468
|
-
obj = if coll.is_a?(Array)
|
|
469
|
-
idx = indices[attr_name]
|
|
470
|
-
indices[attr_name] += 1
|
|
471
|
-
coll[idx]
|
|
472
|
-
else
|
|
473
|
-
coll
|
|
474
|
-
end
|
|
475
|
-
text = extract_plain_text(obj)
|
|
476
|
-
parts << (text.empty? ? " " : text)
|
|
477
|
-
elsif el.name == "span"
|
|
478
|
-
parts << " "
|
|
479
|
-
end
|
|
480
|
-
end
|
|
481
|
-
end
|
|
482
|
-
end
|
|
483
|
-
|
|
484
|
-
if parts.join.strip.empty?
|
|
485
|
-
t = safe_attr(node, :text)
|
|
486
|
-
parts << (t.is_a?(Array) ? t.join : t.to_s) if t
|
|
487
|
-
end
|
|
488
|
-
|
|
489
|
-
parts.join.strip.gsub(" ", " ")
|
|
490
|
-
end
|
|
491
|
-
|
|
492
331
|
# --- Dispatch ---
|
|
493
332
|
|
|
494
333
|
def render(node, **)
|
|
495
334
|
return escape_html(node) if node.is_a?(String)
|
|
496
335
|
|
|
497
336
|
method = lookup_dispatch(node.class, :render_registry)
|
|
498
|
-
|
|
337
|
+
return public_send(method, node, **) if method
|
|
338
|
+
|
|
339
|
+
record_render_warning(
|
|
340
|
+
"no renderer registered for #{node.class} — content skipped",
|
|
341
|
+
)
|
|
342
|
+
""
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# Emit a render warning once per distinct message. Warnings mark
|
|
346
|
+
# content that was skipped or degraded rather than rendered.
|
|
347
|
+
def record_render_warning(message)
|
|
348
|
+
@render_warnings ||= {}
|
|
349
|
+
return if @render_warnings.key?(message)
|
|
350
|
+
|
|
351
|
+
@render_warnings[message] = true
|
|
352
|
+
warn "metanorma-document: #{message}"
|
|
499
353
|
end
|
|
500
354
|
|
|
501
355
|
def render_inline_element(element, **)
|
|
@@ -527,6 +381,8 @@ module Metanorma
|
|
|
527
381
|
:render_note
|
|
528
382
|
register_render Metanorma::Document::Components::AncillaryBlocks::ExampleBlock,
|
|
529
383
|
:render_example
|
|
384
|
+
register_render Metanorma::StandardDocument::Blocks::Form,
|
|
385
|
+
:render_form
|
|
530
386
|
register_render Metanorma::Document::Components::AncillaryBlocks::SourcecodeBlock,
|
|
531
387
|
:render_sourcecode
|
|
532
388
|
register_render Metanorma::Document::Components::AncillaryBlocks::FormulaBlock,
|
|
@@ -592,6 +448,8 @@ module Metanorma
|
|
|
592
448
|
:render_fmt_stem
|
|
593
449
|
register_inline_render Metanorma::Document::Components::Inline::CommaElement,
|
|
594
450
|
:render_comma
|
|
451
|
+
register_inline_render Metanorma::StandardDocument::Elements::Input,
|
|
452
|
+
:render_input
|
|
595
453
|
register_inline_render Metanorma::Document::Components::Inline::EnumCommaElement,
|
|
596
454
|
:render_comma
|
|
597
455
|
register_inline_render Metanorma::Document::Components::IdElements::Bookmark,
|
|
@@ -643,7 +501,31 @@ module Metanorma
|
|
|
643
501
|
register_inline_render klass, :render_mixed_inline
|
|
644
502
|
end
|
|
645
503
|
|
|
504
|
+
# Inline-ish classes that are also reached through the BLOCK
|
|
505
|
+
# dispatch (as direct children of block containers, e.g. fmt-title
|
|
506
|
+
# as a child of clause). fmt-title and variant-title duplicate the
|
|
507
|
+
# semantic `title` attribute that sections render — skip them.
|
|
508
|
+
register_render Metanorma::Document::Components::Inline::FmtTitleElement,
|
|
509
|
+
:render_noop
|
|
510
|
+
register_render Metanorma::Document::Components::Inline::VariantTitleElement,
|
|
511
|
+
:render_noop
|
|
512
|
+
# fmt-xref-label carries the cross-reference link label; render it
|
|
513
|
+
# through when it appears in block context.
|
|
514
|
+
register_render Metanorma::Document::Components::Inline::FmtXrefLabelElement,
|
|
515
|
+
:render_block_inline_content
|
|
516
|
+
|
|
646
517
|
def lookup_dispatch(type_class, registry_method)
|
|
518
|
+
# Registry contents are fixed once renderer classes are loaded, so
|
|
519
|
+
# the resolved method per (registry, node class) is memoized for
|
|
520
|
+
# the lifetime of this renderer instance.
|
|
521
|
+
cache = (@dispatch_cache ||= {})
|
|
522
|
+
key = [registry_method, type_class]
|
|
523
|
+
return cache[key] if cache.key?(key)
|
|
524
|
+
|
|
525
|
+
cache[key] = resolve_dispatch(type_class, registry_method)
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
def resolve_dispatch(type_class, registry_method)
|
|
647
529
|
self.class.ancestors.each do |ancestor|
|
|
648
530
|
next unless ancestor.is_a?(Class) && (ancestor == BaseRenderer || ancestor < BaseRenderer)
|
|
649
531
|
|
|
@@ -658,6 +540,13 @@ module Metanorma
|
|
|
658
540
|
""
|
|
659
541
|
end
|
|
660
542
|
|
|
543
|
+
# Block-dispatch entry point for inline content reached as a direct
|
|
544
|
+
# child of a block container (render passes keyword args; the inline
|
|
545
|
+
# pipeline does not accept them).
|
|
546
|
+
def render_block_inline_content(el, **)
|
|
547
|
+
render_mixed_inline(el)
|
|
548
|
+
end
|
|
549
|
+
|
|
661
550
|
def render_noop_inline(*)
|
|
662
551
|
nil
|
|
663
552
|
end
|
|
@@ -690,6 +579,7 @@ module Metanorma
|
|
|
690
579
|
def render_semx_inline(el) = @inline_renderer.render_semx_inline(el)
|
|
691
580
|
def render_fmt_xref(el) = @inline_renderer.render_fmt_xref(el)
|
|
692
581
|
def render_comma(*) = @inline_renderer.render_comma
|
|
582
|
+
def render_input(el) = @inline_renderer.render_input(el)
|
|
693
583
|
def render_math(el) = @inline_renderer.render_math(el)
|
|
694
584
|
def render_asciimath(el) = @inline_renderer.render_asciimath(el)
|
|
695
585
|
def render_index(el) = @inline_renderer.render_index(el)
|
|
@@ -707,7 +597,7 @@ module Metanorma
|
|
|
707
597
|
def render_fn(fn) = @inline_renderer.render_fn(fn)
|
|
708
598
|
def render_concept(concept) = @inline_renderer.render_concept(concept)
|
|
709
599
|
def render_fmt_stem(fmt_stem) = @inline_renderer.render_fmt_stem(fmt_stem)
|
|
710
|
-
def render_mixed_content_in_order(node) = @inline_renderer.render_mixed_content_in_order(node)
|
|
600
|
+
def render_mixed_content_in_order(node, **) = @inline_renderer.render_mixed_content_in_order(node, **)
|
|
711
601
|
|
|
712
602
|
# Block rendering delegation
|
|
713
603
|
def render_paragraph(p,
|
|
@@ -750,6 +640,8 @@ module Metanorma
|
|
|
750
640
|
@block_renderer.render_example(example, **)
|
|
751
641
|
end
|
|
752
642
|
|
|
643
|
+
def render_form(form, **) = @block_renderer.render_form(form, **)
|
|
644
|
+
|
|
753
645
|
def render_sourcecode(sc,
|
|
754
646
|
**)
|
|
755
647
|
@block_renderer.render_sourcecode(sc, **)
|
|
@@ -861,7 +753,12 @@ level = 1)
|
|
|
861
753
|
end
|
|
862
754
|
|
|
863
755
|
obj.public_send(method_name)
|
|
864
|
-
rescue NoMethodError
|
|
756
|
+
rescue NoMethodError => e
|
|
757
|
+
# Only swallow "method missing on this object" (duck-typing across
|
|
758
|
+
# model classes). A NoMethodError raised *inside* the getter is a
|
|
759
|
+
# real bug — let it surface.
|
|
760
|
+
raise unless e.receiver.equal?(obj)
|
|
761
|
+
|
|
865
762
|
nil
|
|
866
763
|
end
|
|
867
764
|
|
|
@@ -876,7 +773,8 @@ level = 1)
|
|
|
876
773
|
target_id: @current_section_id,
|
|
877
774
|
target_text: @current_section_number,
|
|
878
775
|
)
|
|
879
|
-
rescue StandardError
|
|
776
|
+
rescue StandardError => e
|
|
777
|
+
record_render_warning("index term dropped: #{e.class}: #{e.message}")
|
|
880
778
|
nil
|
|
881
779
|
end
|
|
882
780
|
|
|
@@ -916,34 +814,6 @@ level = 1)
|
|
|
916
814
|
CGI.escapeHTML(text.to_s)
|
|
917
815
|
end
|
|
918
816
|
|
|
919
|
-
def extract_text_value(val)
|
|
920
|
-
return nil if val.nil?
|
|
921
|
-
return val if val.is_a?(String)
|
|
922
|
-
|
|
923
|
-
if val.is_a?(Array)
|
|
924
|
-
val.map { |v| extract_text_value(v) }.join
|
|
925
|
-
elsif val.is_a?(Lutaml::Model::Serializable)
|
|
926
|
-
c = safe_attr(val, :content)
|
|
927
|
-
if c && !c.equal?(val)
|
|
928
|
-
extract_text_value(c)
|
|
929
|
-
else
|
|
930
|
-
t = safe_attr(val, :text)
|
|
931
|
-
if t
|
|
932
|
-
extract_text_value(t)
|
|
933
|
-
else
|
|
934
|
-
v = safe_attr(val, :value)
|
|
935
|
-
if v
|
|
936
|
-
extract_text_value(v)
|
|
937
|
-
else
|
|
938
|
-
val.to_s
|
|
939
|
-
end
|
|
940
|
-
end
|
|
941
|
-
end
|
|
942
|
-
else
|
|
943
|
-
val.to_s
|
|
944
|
-
end
|
|
945
|
-
end
|
|
946
|
-
|
|
947
817
|
def render_footnotes_section
|
|
948
818
|
return nil if @footnote_collector.empty?
|
|
949
819
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Metanorma
|
|
4
|
+
module Html
|
|
5
|
+
module Concerns
|
|
6
|
+
# Document metadata extraction from bibdata, mixed into
|
|
7
|
+
# BaseRenderer: page language, HTML title, and the primary document
|
|
8
|
+
# identifier used in the header.
|
|
9
|
+
module MetadataExtraction
|
|
10
|
+
def language
|
|
11
|
+
bibdata = @document.bibdata
|
|
12
|
+
return "en" unless bibdata
|
|
13
|
+
|
|
14
|
+
langs = bibdata.language
|
|
15
|
+
if langs && !langs.empty?
|
|
16
|
+
lang = langs.find { |l| l.current == "true" } || langs.first
|
|
17
|
+
lang.value || lang.to_s
|
|
18
|
+
else
|
|
19
|
+
"en"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def html_title
|
|
24
|
+
extract_display_title(@document.bibdata) || "Document"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def extract_display_title(bibdata)
|
|
28
|
+
return nil unless bibdata
|
|
29
|
+
|
|
30
|
+
title = bibdata.title_for("en") if bibdata.is_a?(Metanorma::Document::Components::BibData::BibData)
|
|
31
|
+
return title.to_s if title && !title.to_s.empty?
|
|
32
|
+
|
|
33
|
+
titles = safe_attr(bibdata, :title)
|
|
34
|
+
return nil unless titles && !titles.is_a?(String) && !titles.empty?
|
|
35
|
+
|
|
36
|
+
en = titles.find { |t| safe_attr(t, :language) == "en" }
|
|
37
|
+
found = en || titles.first
|
|
38
|
+
extract_text_value(found).to_s
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def extract_primary_doc_id
|
|
42
|
+
bibdata = @document.bibdata
|
|
43
|
+
return nil unless bibdata
|
|
44
|
+
|
|
45
|
+
identifiers = bibdata.doc_identifier
|
|
46
|
+
return nil unless identifiers && !identifiers.empty?
|
|
47
|
+
|
|
48
|
+
first_id = identifiers.first
|
|
49
|
+
text = if first_id.is_a?(String)
|
|
50
|
+
first_id
|
|
51
|
+
elsif first_id.is_a?(Lutaml::Model::Serializable)
|
|
52
|
+
Array(first_id.value).join
|
|
53
|
+
else
|
|
54
|
+
first_id.to_s
|
|
55
|
+
end
|
|
56
|
+
text.strip.empty? ? nil : text.strip
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Metanorma
|
|
4
|
+
module Html
|
|
5
|
+
module Concerns
|
|
6
|
+
# Presentation-XML validation mixed into BaseRenderer. HTML
|
|
7
|
+
# generation requires Presentation XML input — semantic XML does not
|
|
8
|
+
# contain the formatting data needed for HTML — so the document is
|
|
9
|
+
# checked for presentation markers before rendering begins.
|
|
10
|
+
module PresentationValidation
|
|
11
|
+
def validate_presentation_xml!
|
|
12
|
+
has_presentation = check_presentation_markers(@document)
|
|
13
|
+
return if has_presentation
|
|
14
|
+
|
|
15
|
+
raise ArgumentError,
|
|
16
|
+
"HTML generation requires Presentation XML input. " \
|
|
17
|
+
"Semantic XML does not contain formatting data needed for HTML. " \
|
|
18
|
+
"Use a '.presentation.xml' file instead."
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Name kept for API stability (public renderer API); the boolean
|
|
22
|
+
# return predates the Naming/PredicateMethod convention.
|
|
23
|
+
# rubocop:disable Naming/PredicateMethod
|
|
24
|
+
def check_presentation_markers(node)
|
|
25
|
+
return false unless node
|
|
26
|
+
return false if node.is_a?(String)
|
|
27
|
+
|
|
28
|
+
if node.is_a?(Lutaml::Model::Serializable)
|
|
29
|
+
node_attrs = node.class.attributes
|
|
30
|
+
if node_attrs.key?(:type) && node.type == "presentation"
|
|
31
|
+
return true
|
|
32
|
+
end
|
|
33
|
+
if node_attrs.key?(:fmt_title) && node.fmt_title
|
|
34
|
+
return true
|
|
35
|
+
end
|
|
36
|
+
if node_attrs.key?(:displayorder) && node.displayorder
|
|
37
|
+
return true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
%i[preface sections annex bibliography].each do |attr|
|
|
41
|
+
next unless node_attrs.key?(attr)
|
|
42
|
+
|
|
43
|
+
val = node.public_send(attr)
|
|
44
|
+
next unless val
|
|
45
|
+
|
|
46
|
+
Array(val).each { |v| return true if check_presentation_markers(v) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
node.each_mixed_content do |child|
|
|
50
|
+
next if child.is_a?(String)
|
|
51
|
+
return true if check_presentation_markers(child)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
false
|
|
56
|
+
end
|
|
57
|
+
# rubocop:enable Naming/PredicateMethod
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Metanorma
|
|
4
|
+
module Html
|
|
5
|
+
module Concerns
|
|
6
|
+
# SVG logo loading and normalization mixed into BaseRenderer.
|
|
7
|
+
# Logos ship as standalone SVG files; for inline embedding the xml
|
|
8
|
+
# prolog and leading comments are stripped, a CSS class is added,
|
|
9
|
+
# and width/height are normalized to the requested display height.
|
|
10
|
+
module SvgProcessing
|
|
11
|
+
def load_logo_svg(filename, height: 32)
|
|
12
|
+
path = theme.resolve_asset(filename) || File.join(BaseRenderer::LOGO_DIR, filename)
|
|
13
|
+
return nil unless File.exist?(path)
|
|
14
|
+
|
|
15
|
+
svg = File.read(path)
|
|
16
|
+
svg = svg.sub(/\A<\?xml[^?]*\?>\s*/, "")
|
|
17
|
+
svg = svg.sub(/\A\s*<!--.*?-->\s*/m, "")
|
|
18
|
+
svg = svg.sub(/<svg\s/, '<svg class="header-logo" ')
|
|
19
|
+
svg = if svg.match?(/<svg[^>]*\sheight="[^"]*"/)
|
|
20
|
+
svg.sub(/(<svg[^>]*?)(\sheight="[^"]*")/,
|
|
21
|
+
"\\1 height=\"#{height}\"")
|
|
22
|
+
else
|
|
23
|
+
svg.sub(/(<svg\b)/, "\\1 height=\"#{height}\"")
|
|
24
|
+
end
|
|
25
|
+
svg.sub(/(<svg[^>]*?)\swidth="[^"]*"/, '\1')
|
|
26
|
+
rescue StandardError
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|