metanorma-document 0.2.12 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +3 -2
  3. data/README.adoc +1 -1
  4. data/Rakefile +30 -5
  5. data/frontend/dist/app.css +1 -0
  6. data/frontend/dist/app.iife.js +24 -0
  7. data/frontend/package-lock.json +7 -505
  8. data/frontend/tailwind.config.js +5 -0
  9. data/lib/metanorma/csa_document/root.rb +27 -0
  10. data/lib/metanorma/csa_document.rb +9 -0
  11. data/lib/metanorma/document/cli.rb +52 -0
  12. data/lib/metanorma/document/components/ancillary_blocks/sourcecode_body.rb +14 -0
  13. data/lib/metanorma/document/components/inline/comma_element.rb +3 -0
  14. data/lib/metanorma/document/components/inline/enum_comma_element.rb +3 -0
  15. data/lib/metanorma/document/components/inline/eref_element.rb +4 -1
  16. data/lib/metanorma/document/components/inline/fmt_termsource_element.rb +4 -2
  17. data/lib/metanorma/document/components/inline/math_element.rb +13 -7
  18. data/lib/metanorma/document/components/inline/semx_element.rb +7 -8
  19. data/lib/metanorma/document/components/inline/vocabulary.rb +4 -0
  20. data/lib/metanorma/document/components/multi_paragraph/admonition_block.rb +6 -1
  21. data/lib/metanorma/document/components/paragraphs/paragraph_block.rb +3 -0
  22. data/lib/metanorma/document/components/reference_elements/callout.rb +3 -2
  23. data/lib/metanorma/document/components/tables/table_cell.rb +3 -0
  24. data/lib/metanorma/document/elements/input.rb +44 -0
  25. data/lib/metanorma/document/elements.rb +1 -0
  26. data/lib/metanorma/document/relaton/address.rb +5 -0
  27. data/lib/metanorma/document/relaton/affiliation.rb +3 -0
  28. data/lib/metanorma/document/relaton/bib_item_size.rb +8 -9
  29. data/lib/metanorma/document/relaton/bib_item_type.rb +3 -0
  30. data/lib/metanorma/document/relaton/bibliographic_date.rb +5 -0
  31. data/lib/metanorma/document/relaton/contact_method.rb +5 -0
  32. data/lib/metanorma/document/relaton/contribution_info.rb +6 -2
  33. data/lib/metanorma/document/relaton/contributor.rb +4 -0
  34. data/lib/metanorma/document/relaton/contributor_role.rb +3 -0
  35. data/lib/metanorma/document/relaton/copyright_association.rb +3 -0
  36. data/lib/metanorma/document/relaton/copyright_owner.rb +3 -0
  37. data/lib/metanorma/document/relaton/date_time.rb +3 -0
  38. data/lib/metanorma/document/relaton/document_identifier.rb +4 -0
  39. data/lib/metanorma/document/relaton/document_relation.rb +8 -0
  40. data/lib/metanorma/document/relaton/document_status.rb +4 -0
  41. data/lib/metanorma/document/relaton/formatted_address.rb +4 -0
  42. data/lib/metanorma/document/relaton/full_name.rb +14 -3
  43. data/lib/metanorma/document/relaton/keyword_type.rb +3 -0
  44. data/lib/metanorma/document/relaton/locality_stack.rb +6 -9
  45. data/lib/metanorma/document/relaton/localized_name.rb +8 -5
  46. data/lib/metanorma/document/relaton/logo_element.rb +5 -0
  47. data/lib/metanorma/document/relaton/medium_type.rb +5 -16
  48. data/lib/metanorma/document/relaton/org_identifier.rb +2 -0
  49. data/lib/metanorma/document/relaton/org_subdivision.rb +2 -0
  50. data/lib/metanorma/document/relaton/org_subdivision_identifier.rb +2 -0
  51. data/lib/metanorma/document/relaton/organization.rb +3 -0
  52. data/lib/metanorma/document/relaton/person.rb +7 -1
  53. data/lib/metanorma/document/relaton/person_identifier.rb +3 -0
  54. data/lib/metanorma/document/relaton/place_type.rb +5 -0
  55. data/lib/metanorma/document/relaton/region_element.rb +3 -9
  56. data/lib/metanorma/document/relaton/relaton_collection.rb +2 -0
  57. data/lib/metanorma/document/relaton/series_type.rb +7 -0
  58. data/lib/metanorma/document/relaton/typed_note.rb +4 -0
  59. data/lib/metanorma/document/relaton/typed_title_string.rb +4 -0
  60. data/lib/metanorma/document/relaton/typed_uri.rb +3 -8
  61. data/lib/metanorma/document/relaton/validity_type.rb +8 -10
  62. data/lib/metanorma/document/relaton/variant_full_name.rb +3 -0
  63. data/lib/metanorma/document/relaton/variant_org_name.rb +3 -0
  64. data/lib/metanorma/document/relaton/version_info.rb +5 -0
  65. data/lib/metanorma/document/relaton/vocab_id_type.rb +6 -12
  66. data/lib/metanorma/document/relaton.rb +0 -14
  67. data/lib/metanorma/document/version.rb +1 -1
  68. data/lib/metanorma/document.rb +1 -0
  69. data/lib/metanorma/html/base_renderer.rb +133 -307
  70. data/lib/metanorma/html/component/base.rb +2 -9
  71. data/lib/metanorma/html/concerns/metadata_extraction.rb +61 -0
  72. data/lib/metanorma/html/concerns/presentation_validation.rb +61 -0
  73. data/lib/metanorma/html/concerns/svg_processing.rb +32 -0
  74. data/lib/metanorma/html/concerns/text_extraction.rb +93 -0
  75. data/lib/metanorma/html/concerns/toc_registry.rb +47 -0
  76. data/lib/metanorma/html/csa_renderer.rb +9 -0
  77. data/lib/metanorma/html/drops/figure_drop.rb +33 -10
  78. data/lib/metanorma/html/drops/sourcecode_drop.rb +3 -6
  79. data/lib/metanorma/html/flavor.rb +50 -0
  80. data/lib/metanorma/html/flavor_registry.rb +49 -0
  81. data/lib/metanorma/html/generator.rb +113 -29
  82. data/lib/metanorma/html/iso_renderer.rb +0 -142
  83. data/lib/metanorma/html/renderer_delegation.rb +21 -0
  84. data/lib/metanorma/html/renderers/block_renderer.rb +10 -0
  85. data/lib/metanorma/html/renderers/element_order_traversal.rb +48 -0
  86. data/lib/metanorma/html/renderers/inline_renderer.rb +49 -124
  87. data/lib/metanorma/html/renderers/pubid_renderer.rb +4 -26
  88. data/lib/metanorma/html/renderers.rb +2 -0
  89. data/lib/metanorma/html/standard_renderer.rb +198 -36
  90. data/lib/metanorma/html.rb +4 -51
  91. data/lib/metanorma/iso_document/sections/iso_preface.rb +27 -13
  92. data/lib/metanorma/mirror/default_registry.rb +21 -0
  93. data/lib/metanorma/mirror/handlers/inline/catalog.rb +52 -0
  94. data/lib/metanorma/mirror/handlers/inline/rich_html_renderer.rb +34 -35
  95. data/lib/metanorma/mirror/handlers/inline.rb +1 -0
  96. data/lib/metanorma/mirror/handlers/sourcecode.rb +1 -1
  97. data/lib/metanorma/mirror/model/guide.rb +3 -2
  98. data/lib/metanorma/mirror/output/formats/inline_format.rb +64 -3
  99. data/lib/metanorma/mirror/output/pipeline.rb +1 -0
  100. data/lib/metanorma/mirror/output.rb +0 -2
  101. data/lib/metanorma/mirror/rewriter.rb +3 -1
  102. data/lib/metanorma/mirror/transformer.rb +91 -3
  103. data/lib/metanorma/mirror.rb +0 -1
  104. data/lib/metanorma/registers/setup.rb +10 -0
  105. data/lib/metanorma/standard_document/annotation_container.rb +2 -0
  106. data/lib/metanorma/standard_document/elements/input.rb +4 -25
  107. data/lib/metanorma/standard_document/metadata/metanorma_extension.rb +12 -2
  108. data/lib/metanorma/standard_document/metadata/presentation_metadata.rb +107 -0
  109. data/lib/metanorma/standard_document/metadata/semantic_metadata.rb +18 -0
  110. data/lib/metanorma/standard_document/metadata/unitsml/amount_of_substance.rb +19 -0
  111. data/lib/metanorma/standard_document/metadata/unitsml/base_quantity.rb +34 -0
  112. data/lib/metanorma/standard_document/metadata/unitsml/dimension.rb +39 -0
  113. data/lib/metanorma/standard_document/metadata/unitsml/dimension_set.rb +19 -0
  114. data/lib/metanorma/standard_document/metadata/unitsml/electric_current.rb +19 -0
  115. data/lib/metanorma/standard_document/metadata/unitsml/enumerated_root_unit.rb +23 -0
  116. data/lib/metanorma/standard_document/metadata/unitsml/length.rb +19 -0
  117. data/lib/metanorma/standard_document/metadata/unitsml/luminous_intensity.rb +19 -0
  118. data/lib/metanorma/standard_document/metadata/unitsml/mass.rb +19 -0
  119. data/lib/metanorma/standard_document/metadata/unitsml/namespace.rb +17 -0
  120. data/lib/metanorma/standard_document/metadata/unitsml/plane_angle.rb +19 -0
  121. data/lib/metanorma/standard_document/metadata/unitsml/prefix.rb +30 -0
  122. data/lib/metanorma/standard_document/metadata/unitsml/prefix_name.rb +21 -0
  123. data/lib/metanorma/standard_document/metadata/unitsml/prefix_set.rb +19 -0
  124. data/lib/metanorma/standard_document/metadata/unitsml/prefix_symbol.rb +21 -0
  125. data/lib/metanorma/standard_document/metadata/unitsml/quantity.rb +28 -0
  126. data/lib/metanorma/standard_document/metadata/unitsml/quantity_name.rb +21 -0
  127. data/lib/metanorma/standard_document/metadata/unitsml/quantity_set.rb +19 -0
  128. data/lib/metanorma/standard_document/metadata/unitsml/root_units.rb +19 -0
  129. data/lib/metanorma/standard_document/metadata/unitsml/thermodynamic_temperature.rb +19 -0
  130. data/lib/metanorma/standard_document/metadata/unitsml/time.rb +19 -0
  131. data/lib/metanorma/standard_document/metadata/unitsml/unit.rb +32 -0
  132. data/lib/metanorma/standard_document/metadata/unitsml/unit_name.rb +21 -0
  133. data/lib/metanorma/standard_document/metadata/unitsml/unit_set.rb +19 -0
  134. data/lib/metanorma/standard_document/metadata/unitsml/unit_symbol.rb +36 -0
  135. data/lib/metanorma/standard_document/metadata/unitsml/unit_system.rb +23 -0
  136. data/lib/metanorma/standard_document/metadata/unitsml/unitsml_root.rb +28 -0
  137. data/lib/metanorma/standard_document/metadata/unitsml.rb +39 -0
  138. data/lib/metanorma/standard_document/metadata.rb +4 -0
  139. data/lib/metanorma/standard_document/sections/misc_container.rb +8 -6
  140. data/lib/metanorma/standard_document/sections/sections.rb +7 -0
  141. data/lib/metanorma/un_document/blocks/un_admonition_block.rb +86 -0
  142. data/lib/metanorma/un_document/blocks.rb +9 -0
  143. data/lib/metanorma/un_document/root.rb +8 -0
  144. data/lib/metanorma/un_document/sections/un_abstract_section.rb +48 -0
  145. data/lib/metanorma/un_document/sections/un_preface.rb +19 -2
  146. data/lib/metanorma/un_document/sections.rb +1 -0
  147. data/lib/metanorma/un_document/un_text_element.rb +116 -0
  148. data/lib/metanorma/un_document.rb +2 -0
  149. data/script/benchmark_render.rb +35 -0
  150. data/tasks/roundtrip_samples.rake +1 -1
  151. metadata +78 -50
  152. data/.rubocop_todo.yml +0 -327
  153. data/TODO.finalize/01-split-multi-class-files.md +0 -8
  154. data/TODO.finalize/02-remove-data-dist-iso-document-frontend.md +0 -6
  155. data/TODO.finalize/03-align-frontend-iife-and-gha-build.md +0 -11
  156. data/TODO.finalize/04-theme-per-flavor-config-driven.md +0 -9
  157. data/TODO.finalize/05-migrate-all-html-to-liquid.md +0 -16
  158. data/TODO.finalize/06-integrate-pubid-library.md +0 -16
  159. data/TODO.finalize/07-replace-relaton-models-with-relaton-bib.md +0 -39
  160. data/TODO.finalize/08-refactor-renderer-architecture.md +0 -25
  161. data/TODO.finalize/09-eliminate-code-smells.md +0 -14
  162. data/TODO.finalize/10-comprehensive-spec-coverage.md +0 -24
  163. data/TODO.finalize/11-config-driven-flavor-metadata.md +0 -20
  164. data/TODO.finalize/12-add-frozen-string-literal.md +0 -6
  165. data/TODO.finalize/13-extract-composition-from-god-class.md +0 -96
  166. data/TODO.finalize/14-config-driven-section-ordering.md +0 -31
  167. data/TODO.finalize/15-theme-directories-per-flavor.md +0 -46
  168. data/TODO.finalize/16-theme-lutaml-model-schema.md +0 -44
  169. data/docs/html-renderer.adoc +0 -261
  170. data/lib/data/dist/iso_document/frontend/assets/index-mwzbzmnK.js +0 -22
  171. data/lib/data/dist/iso_document/frontend/assets/index-pGvKTNid.css +0 -1
  172. data/lib/data/dist/iso_document/frontend/assets/iso-red.svg +0 -53
  173. data/lib/data/dist/iso_document/frontend/index.html +0 -13
  174. data/lib/metanorma/document/relaton/bib_item_size_type.rb +0 -21
  175. data/lib/metanorma/document/relaton/bibliographic_date_type.rb +0 -22
  176. data/lib/metanorma/document/relaton/document_relation_type.rb +0 -31
  177. data/lib/metanorma/document/relaton/iso4217_code.rb +0 -17
  178. data/lib/metanorma/document/relaton/iso8601_date.rb +0 -17
  179. data/lib/metanorma/document/relaton/personal_identifier_type.rb +0 -21
  180. data/lib/metanorma/document/relaton/region_type.rb +0 -20
  181. data/lib/metanorma/document/relaton/series_type_type.rb +0 -21
  182. data/lib/metanorma/document/relaton/specific_locality_type.rb +0 -25
  183. data/lib/metanorma/document/relaton/title_type.rb +0 -21
  184. data/lib/metanorma/mirror/metanorma_to_mirror.rb +0 -101
  185. data/lib/metanorma/mirror/output/html_renderer.rb +0 -95
  186. data/lib/metanorma/mirror/output/html_renderers/block_renderers.rb +0 -178
  187. data/lib/metanorma/mirror/output/html_renderers/inline_renderer.rb +0 -61
  188. data/lib/metanorma/mirror/output/html_renderers/list_renderers.rb +0 -96
  189. data/lib/metanorma/mirror/output/html_renderers/mark_renderers.rb +0 -64
  190. data/lib/metanorma/mirror/output/html_renderers/section_renderers.rb +0 -129
  191. data/lib/metanorma/mirror/output/html_renderers/structural_renderers.rb +0 -80
  192. data/lib/metanorma/mirror/output/html_renderers/table_renderers.rb +0 -87
  193. data/lib/metanorma/mirror/output/html_renderers.rb +0 -76
  194. data/lib/metanorma/standard_document/annotation.rb +0 -37
@@ -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
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Html
5
+ module Concerns
6
+ # Plain-text extraction from model nodes, mixed into BaseRenderer.
7
+ # Used wherever rendered output needs a text-only form (titles,
8
+ # labels, captions, ToC entries). Walks `element_order` and typed
9
+ # attributes directly on the model — never strips tags from
10
+ # rendered HTML.
11
+ module TextExtraction
12
+ def extract_plain_text(node)
13
+ return node.to_s if node.is_a?(String)
14
+ return extract_text_value(node).to_s unless node.is_a?(Lutaml::Model::Serializable)
15
+
16
+ parts = []
17
+ xml_mapping = node.class.mappings_for(:xml, node.lutaml_register)
18
+
19
+ if node.element_order.is_a?(Array) && xml_mapping
20
+ element_to_attr =
21
+ Renderers::ElementOrderTraversal.element_to_attr_map(xml_mapping)
22
+
23
+ indices = Hash.new(0)
24
+ node.element_order.each do |el|
25
+ next unless el.is_a?(Lutaml::Xml::Element)
26
+
27
+ if el.text?
28
+ parts << el.text_content.to_s
29
+ elsif el.name == "tab"
30
+ parts << " "
31
+ # rubocop:disable Lint/DuplicateBranch
32
+ elsif el.name == "br"
33
+ parts << " "
34
+ # rubocop:enable Lint/DuplicateBranch
35
+ elsif el.element?
36
+ attr_name = element_to_attr[el.name]
37
+ if attr_name
38
+ coll = node.public_send(attr_name)
39
+ obj = if coll.is_a?(Array)
40
+ idx = indices[attr_name]
41
+ indices[attr_name] += 1
42
+ coll[idx]
43
+ else
44
+ coll
45
+ end
46
+ text = extract_plain_text(obj)
47
+ parts << (text.empty? ? " " : text)
48
+ elsif el.name == "span"
49
+ parts << " "
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ if parts.join.strip.empty?
56
+ t = safe_attr(node, :text)
57
+ parts << (t.is_a?(Array) ? t.join : t.to_s) if t
58
+ end
59
+
60
+ parts.join.strip.gsub(" ", " ")
61
+ end
62
+
63
+ def extract_text_value(val)
64
+ return nil if val.nil?
65
+ return val if val.is_a?(String)
66
+
67
+ if val.is_a?(Array)
68
+ val.map { |v| extract_text_value(v) }.join
69
+ elsif val.is_a?(Lutaml::Model::Serializable)
70
+ c = safe_attr(val, :content)
71
+ if c && !c.equal?(val)
72
+ extract_text_value(c)
73
+ else
74
+ t = safe_attr(val, :text)
75
+ if t
76
+ extract_text_value(t)
77
+ else
78
+ v = safe_attr(val, :value)
79
+ if v
80
+ extract_text_value(v)
81
+ else
82
+ val.to_s
83
+ end
84
+ end
85
+ end
86
+ else
87
+ val.to_s
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Html
5
+ module Concerns
6
+ # Table-of-contents entry collection and ToC rendering, mixed into
7
+ # BaseRenderer. Sub-renderers register section/figure/table entries
8
+ # as they walk the document; assemble_document renders the collected
9
+ # entries through the _toc.html.liquid template.
10
+ module TocRegistry
11
+ def toc_entries
12
+ @toc_entries
13
+ end
14
+
15
+ def build_toc_html(entries)
16
+ entry_drops = entries.map { |e| Drops::TocEntryDrop.new(e) }
17
+ figure_drops = @figure_entries.map { |f| Drops::FigureListEntryDrop.new(f) }
18
+ table_drops = @table_entries.map { |t| Drops::FigureListEntryDrop.new(t) }
19
+ has_special_lists = !@figure_entries.empty? || !@table_entries.empty?
20
+
21
+ render_liquid("_toc.html.liquid", {
22
+ "entries" => entry_drops,
23
+ "figures" => figure_drops,
24
+ "tables" => table_drops,
25
+ "has_special_lists" => has_special_lists,
26
+ })
27
+ end
28
+
29
+ def register_toc_entry(id:, level:, text:)
30
+ @toc_entries << { id: id, level: level, text: text }
31
+ end
32
+
33
+ def register_figure_entry(id:, text:)
34
+ @figure_entries << { id: id, text: text }
35
+ end
36
+
37
+ def figure_entries
38
+ @figure_entries
39
+ end
40
+
41
+ def register_table_entry(id:, text:)
42
+ @table_entries << { id: id, text: text }
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Html
5
+ class CsaRenderer < IsoRenderer
6
+ register_render Metanorma::CsaDocument::Root, :render_document
7
+ end
8
+ end
9
+ end
@@ -47,24 +47,47 @@ module Metanorma
47
47
  end
48
48
  sub_figures_html = sub_figures_parts.join
49
49
 
50
- key_parts = []
51
- renderer.safe_attr(figure, :note)&.each do |n|
52
- key_parts << (renderer.render_note(n) || "")
53
- end
54
- renderer.safe_attr(figure, :dl)&.then do |dl|
55
- key_parts << (renderer.render_definition_list(dl) || "")
56
- end
57
- key_html = key_parts.join
58
-
59
50
  new(
60
51
  id: id,
61
52
  image_html: image_html,
62
53
  caption_html: caption_html,
63
- key_html: key_html,
54
+ key_html: build_key_html(figure, renderer),
64
55
  sub_figures_html: sub_figures_html,
65
56
  css_class: "figure",
66
57
  )
67
58
  end
59
+
60
+ class << self
61
+ private
62
+
63
+ # Notes, plain definition lists, and figure.key (e.g.
64
+ # <key class="formula_dl">) — symbol/footnote definitions
65
+ # belonging to the figure.
66
+ def build_key_html(figure, renderer)
67
+ parts = []
68
+ renderer.safe_attr(figure, :note)&.each do |n|
69
+ parts << (renderer.render_note(n) || "")
70
+ end
71
+ renderer.safe_attr(figure, :dl)&.then do |dl|
72
+ parts << (renderer.render_definition_list(dl) || "")
73
+ end
74
+ append_figure_key(parts, renderer.safe_attr(figure, :key), renderer)
75
+ parts.join
76
+ end
77
+
78
+ def append_figure_key(parts, key, renderer)
79
+ return unless key
80
+
81
+ name = renderer.safe_attr(key, :name)
82
+ parts << (renderer.render_inline_element(name) || "") if name
83
+ if renderer.safe_attr(key, :dl)
84
+ parts << (renderer.render_definition_list(key.dl) || "")
85
+ end
86
+ renderer.safe_attr(key, :p)&.each do |para|
87
+ parts << (renderer.render_paragraph(para) || "")
88
+ end
89
+ end
90
+ end
68
91
  end
69
92
  end
70
93
  end
@@ -23,22 +23,19 @@ css_class: nil)
23
23
  renderer.render_inline_element(sc.name)
24
24
  end
25
25
 
26
- code_text = if sc.body&.content
27
- Array(sc.body.content).join
26
+ code_text = if sc.body
27
+ sc.body.decoded_content
28
28
  elsif sc.content
29
29
  Array(sc.content).join
30
30
  else
31
31
  ""
32
32
  end
33
- raw_text = code_text.gsub("&lt;", "<").gsub("&gt;", ">").gsub("&amp;", "&").gsub(
34
- "&quot;", "\""
35
- )
36
33
 
37
34
  new(
38
35
  id: id,
39
36
  lang: lang,
40
37
  name_html: name_html,
41
- code_html: renderer.escape_html(raw_text),
38
+ code_html: renderer.escape_html(code_text),
42
39
  css_class: "sourcecode",
43
40
  )
44
41
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pubid"
4
+
5
+ module Metanorma
6
+ module Html
7
+ # A flavor ties together the four concepts that identify a Metanorma
8
+ # document variant:
9
+ # - a symbolic name (used for theme loading, CSS class names)
10
+ # - a model class (the typed root class for the flavor)
11
+ # - a renderer class (the HTML renderer that handles the flavor)
12
+ # - an optional Pubid module (for parsing identifiers like ISO/IEC/etc.)
13
+ #
14
+ # Flavor centralizes flavor identity so BaseRenderer, PubidRenderer,
15
+ # and Generator all read from the same source of truth. Adding a new
16
+ # flavor is one Flavor entry in the registry, not three coordinated
17
+ # edits across three files.
18
+ class Flavor
19
+ attr_reader :name, :model_class, :renderer_class, :pubid_module
20
+
21
+ def initialize(name:, model_class:, renderer_class:, pubid_module: nil)
22
+ @name = name
23
+ @model_class = model_class
24
+ @renderer_class = renderer_class
25
+ @pubid_module = pubid_module
26
+ end
27
+
28
+ # Match by ancestry: a document whose class is the flavor's model
29
+ # class, or any descendant of it, belongs to this flavor. Walks
30
+ # parent classes via is_a? to support subclassing.
31
+ def matches?(document_class)
32
+ return false unless document_class.is_a?(Class)
33
+
34
+ # `<=` returns nil for unrelated classes; coerce to boolean.
35
+ !!(document_class <= model_class)
36
+ end
37
+
38
+ def pubid_module_const
39
+ return nil unless pubid_module
40
+
41
+ Object.const_get(pubid_module.to_s)
42
+ rescue NameError
43
+ raise ArgumentError,
44
+ "Flavor #{name.inspect}: pubid module #{pubid_module} could " \
45
+ "not be resolved — fix the registration in " \
46
+ "Html::Generator.build_flavor_registry"
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Html
5
+ # Registry of known flavors. Queried by:
6
+ # - Generator — to pick the renderer for a document
7
+ # - BaseRenderer — to resolve flavor_name for theme loading
8
+ # - PubidRenderer — to resolve the Pubid module
9
+ #
10
+ # Single source of truth for flavor identity. Adding a flavor is
11
+ # one Flavor entry via #register.
12
+ class FlavorRegistry
13
+ include Enumerable
14
+
15
+ def initialize
16
+ @flavors = []
17
+ end
18
+
19
+ def register(flavor)
20
+ @flavors << flavor
21
+ self
22
+ end
23
+
24
+ def each(&)
25
+ @flavors.each(&)
26
+ end
27
+
28
+ # Returns the most-specific Flavor whose model_class matches the
29
+ # given document class (by ancestry). Most-specific = registered
30
+ # last. We scan in reverse so general flavors (registered first)
31
+ # only match if no more specific flavor does.
32
+ def find_for(document_class)
33
+ @flavors.reverse_each.find { |f| f.matches?(document_class) }
34
+ end
35
+
36
+ def name_for(document_class)
37
+ find_for(document_class)&.name
38
+ end
39
+
40
+ def renderer_for(document_class)
41
+ find_for(document_class)&.renderer_class
42
+ end
43
+
44
+ def pubid_module_for(document_class)
45
+ find_for(document_class)&.pubid_module_const
46
+ end
47
+ end
48
+ end
49
+ end
@@ -3,18 +3,22 @@
3
3
  module Metanorma
4
4
  module Html
5
5
  class Generator
6
- @renderers = []
7
6
  @tastes = []
8
7
  @setup = false
9
8
 
10
9
  class << self
11
- def register(model_class, renderer_class)
12
- @renderers << [model_class, renderer_class]
10
+ # Returns the full FlavorRegistry built from setup!. Read-only
11
+ # access for BaseRenderer, PubidRenderer, and any consumer that
12
+ # needs flavor identity.
13
+ def flavors
14
+ setup! unless @setup
15
+ @flavors
13
16
  end
14
17
 
15
- # Register a taste: same document model, different renderer based on publisher.
16
- # When the document's first author publisher abbreviation matches,
17
- # the taste renderer takes precedence over the model-based renderer.
18
+ # Register a taste: same document model, different renderer based
19
+ # on publisher. When the document's first author publisher
20
+ # abbreviation matches, the taste renderer takes precedence over
21
+ # the model-based renderer.
18
22
  def register_taste(model_class, publisher_abbrev, renderer_class)
19
23
  @tastes << [model_class, publisher_abbrev, renderer_class]
20
24
  end
@@ -27,15 +31,11 @@ module Metanorma
27
31
  def renderer_for(document)
28
32
  setup! unless @setup
29
33
 
30
- # Check tastes first (publisher-based dispatch)
31
34
  taste_renderer = find_taste(document)
32
35
  return taste_renderer if taste_renderer
33
36
 
34
- # Fall back to model-based dispatch (most specific last)
35
- @renderers.reverse_each do |model_class, renderer_class|
36
- return renderer_class if document.is_a?(model_class)
37
- end
38
- BaseRenderer
37
+ flavor = @flavors.find_for(document.class)
38
+ flavor&.renderer_class || BaseRenderer
39
39
  end
40
40
 
41
41
  private
@@ -79,6 +79,7 @@ module Metanorma
79
79
  return if @setup
80
80
 
81
81
  @setup = true
82
+ @flavors = build_flavor_registry
82
83
 
83
84
  # Trigger autoloads by referencing constants
84
85
  BaseRenderer
@@ -86,6 +87,7 @@ module Metanorma
86
87
  IsoRenderer
87
88
  BipmRenderer
88
89
  CcRenderer
90
+ CsaRenderer
89
91
  IccRenderer
90
92
  IecRenderer
91
93
  IeeeRenderer
@@ -97,27 +99,109 @@ module Metanorma
97
99
  PdfaRenderer
98
100
  RiboseRenderer
99
101
 
100
- # Register renderers (most general first, most specific last)
101
- register Metanorma::Document::Root, BaseRenderer
102
- register Metanorma::StandardDocument::Root, StandardRenderer
103
- register Metanorma::IsoDocument::Root, IsoRenderer
104
- register Metanorma::BipmDocument::Root, BipmRenderer
105
- register Metanorma::CcDocument::Root, CcRenderer
106
- register Metanorma::IecDocument::Root, IecRenderer
107
- register Metanorma::IeeeDocument::Root, IeeeRenderer
108
- register Metanorma::IetfDocument::Root, IetfRenderer
109
- register Metanorma::IhoDocument::Root, IhoRenderer
110
- register Metanorma::ItuDocument::Root, ItuRenderer
111
- register Metanorma::OgcDocument::Root, OgcRenderer
112
- register Metanorma::OimlDocument::Root, OimlRenderer
113
- register Metanorma::RiboseDocument::Root, RiboseRenderer
114
-
115
102
  # Register tastes (publisher-based dispatch within same model)
116
- register_taste Metanorma::IsoDocument::Root, "ICC",
117
- IccRenderer
103
+ register_taste Metanorma::IsoDocument::Root, "ICC", IccRenderer
118
104
  register_taste Metanorma::RiboseDocument::Root, "PDF Association",
119
105
  PdfaRenderer
120
106
  end
107
+
108
+ # Single source of truth for flavor identity. Each Flavor ties
109
+ # together the model class, the renderer, the symbolic name, and
110
+ # (if applicable) the Pubid module. Adding a new flavor = one
111
+ # entry here.
112
+ def build_flavor_registry
113
+ FlavorRegistry.new.tap do |registry|
114
+ registry.register(Flavor.new(
115
+ name: nil,
116
+ model_class: Metanorma::Document::Root,
117
+ renderer_class: BaseRenderer,
118
+ ))
119
+ registry.register(Flavor.new(
120
+ name: nil,
121
+ model_class: Metanorma::StandardDocument::Root,
122
+ renderer_class: StandardRenderer,
123
+ ))
124
+ registry.register(Flavor.new(
125
+ name: :iso,
126
+ model_class: Metanorma::IsoDocument::Root,
127
+ renderer_class: IsoRenderer,
128
+ pubid_module: :"Pubid::Iso",
129
+ ))
130
+ registry.register(Flavor.new(
131
+ name: :bipm,
132
+ model_class: Metanorma::BipmDocument::Root,
133
+ renderer_class: BipmRenderer,
134
+ ))
135
+ registry.register(Flavor.new(
136
+ name: :cc,
137
+ model_class: Metanorma::CcDocument::Root,
138
+ renderer_class: CcRenderer,
139
+ ))
140
+ registry.register(Flavor.new(
141
+ name: :csa,
142
+ model_class: Metanorma::CsaDocument::Root,
143
+ renderer_class: CsaRenderer,
144
+ pubid_module: :"Pubid::Csa",
145
+ ))
146
+ registry.register(Flavor.new(
147
+ name: :iec,
148
+ model_class: Metanorma::IecDocument::Root,
149
+ renderer_class: IecRenderer,
150
+ pubid_module: :"Pubid::Iec",
151
+ ))
152
+ registry.register(Flavor.new(
153
+ name: :ieee,
154
+ model_class: Metanorma::IeeeDocument::Root,
155
+ renderer_class: IeeeRenderer,
156
+ pubid_module: :"Pubid::Ieee",
157
+ ))
158
+ registry.register(Flavor.new(
159
+ name: :ietf,
160
+ model_class: Metanorma::IetfDocument::Root,
161
+ renderer_class: IetfRenderer,
162
+ ))
163
+ registry.register(Flavor.new(
164
+ name: :iho,
165
+ model_class: Metanorma::IhoDocument::Root,
166
+ renderer_class: IhoRenderer,
167
+ pubid_module: :"Pubid::Iho",
168
+ ))
169
+ registry.register(Flavor.new(
170
+ name: :itu,
171
+ model_class: Metanorma::ItuDocument::Root,
172
+ renderer_class: ItuRenderer,
173
+ pubid_module: :"Pubid::Itu",
174
+ ))
175
+ registry.register(Flavor.new(
176
+ name: :ogc,
177
+ model_class: Metanorma::OgcDocument::Root,
178
+ renderer_class: OgcRenderer,
179
+ ))
180
+ registry.register(Flavor.new(
181
+ name: :oiml,
182
+ model_class: Metanorma::OimlDocument::Root,
183
+ renderer_class: OimlRenderer,
184
+ pubid_module: :"Pubid::Oiml",
185
+ ))
186
+ registry.register(Flavor.new(
187
+ name: :pdfa,
188
+ model_class: Metanorma::RiboseDocument::Root,
189
+ renderer_class: PdfaRenderer,
190
+ ))
191
+ registry.register(Flavor.new(
192
+ name: :ribose,
193
+ model_class: Metanorma::RiboseDocument::Root,
194
+ renderer_class: RiboseRenderer,
195
+ ))
196
+ end
197
+ end
198
+
199
+ def safe_attr(node, attr_name)
200
+ return nil unless node.is_a?(Lutaml::Model::Serializable)
201
+ return nil unless node.class.attributes.key?(attr_name)
202
+
203
+ node.public_send(attr_name)
204
+ end
121
205
  end
122
206
  end
123
207
  end