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
@@ -11,9 +11,27 @@ module Metanorma
11
11
  autoload :BlockRenderer, "metanorma/html/renderers/block_renderer"
12
12
  autoload :SectionRenderer, "metanorma/html/renderers/section_renderer"
13
13
  autoload :PubidRenderer, "metanorma/html/renderers/pubid_renderer"
14
+ autoload :ElementOrderTraversal,
15
+ "metanorma/html/renderers/element_order_traversal"
16
+ end
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"
14
26
  end
15
27
 
16
28
  class BaseRenderer
29
+ include Concerns::MetadataExtraction
30
+ include Concerns::PresentationValidation
31
+ include Concerns::SvgProcessing
32
+ include Concerns::TextExtraction
33
+ include Concerns::TocRegistry
34
+
17
35
  LOGO_DIR = File.expand_path("../../../data/logos", __dir__)
18
36
 
19
37
  SPAN_ROLE_CLASSES = {
@@ -84,16 +102,13 @@ module Metanorma
84
102
  end
85
103
 
86
104
  class RendererContext
105
+ include RendererDelegation
106
+
87
107
  def initialize(renderer)
88
108
  @renderer = renderer
89
109
  end
90
110
 
91
- def safe_attr(...) = @renderer.safe_attr(...)
92
- def escape_html(...) = @renderer.escape_html(...)
93
- def extract_block_label(...)= @renderer.extract_block_label(...)
94
- def extract_plain_text(...)= @renderer.extract_plain_text(...)
95
111
  def render_paragraph(...) = @renderer.render_paragraph(...)
96
- def render_mixed_inline(...)= @renderer.render_mixed_inline(...)
97
112
  def render_inline_element(...)= @renderer.render_inline_element(...)
98
113
  def render_unordered_list(...)= @renderer.render_unordered_list(...)
99
114
  def render_ordered_list(...)= @renderer.render_ordered_list(...)
@@ -107,7 +122,6 @@ module Metanorma
107
122
  def render_image(...) = @renderer.render_image(...)
108
123
  def render_stem_content(...)= @renderer.render_stem_content(...)
109
124
  def register_figure_entry(...)= @renderer.register_figure_entry(...)
110
- def render_liquid(...) = @renderer.render_liquid(...)
111
125
  def render_note_children(...) = @renderer.render_note_children(...)
112
126
  def render_simple_children(...) = @renderer.render_simple_children(...)
113
127
  def render_full_block_children(...) = @renderer.render_full_block_children(...)
@@ -117,10 +131,6 @@ module Metanorma
117
131
  @renderer_context ||= RendererContext.new(self)
118
132
  end
119
133
 
120
- def toc_entries
121
- @toc_entries
122
- end
123
-
124
134
  attr_writer :document, :theme
125
135
 
126
136
  def generate_full_document(document, **)
@@ -132,23 +142,15 @@ module Metanorma
132
142
  assemble_document(body)
133
143
  end
134
144
 
135
- # --- Flavor configuration hooks ---
145
+ # Renders only the document body content (no html/head/styles
146
+ # assembly) — for embedding classic-rendered content into a host page.
147
+ def generate_body(document, **)
148
+ @document = document
149
+ validate_presentation_xml!
150
+ render(@document) || ""
151
+ end
136
152
 
137
- FLAVOR_MAP = {
138
- "IsoDocument" => :iso,
139
- "IecDocument" => :iec,
140
- "IeeeDocument" => :ieee,
141
- "IetfDocument" => :ietf,
142
- "ItuDocument" => :itu,
143
- "IhoDocument" => :iho,
144
- "BipmDocument" => :bipm,
145
- "OgcDocument" => :ogc,
146
- "OimlDocument" => :oiml,
147
- "CcDocument" => :cc,
148
- "IccDocument" => :icc,
149
- "RiboseDocument" => :ribose,
150
- "PdfaDocument" => :pdfa,
151
- }.freeze
153
+ # --- Flavor configuration hooks ---
152
154
 
153
155
  def theme
154
156
  @theme ||= resolve_theme
@@ -162,9 +164,15 @@ module Metanorma
162
164
  def flavor_name
163
165
  return nil unless defined?(@document) && @document
164
166
 
165
- @document.class.name&.split("::")&.detect do |ns|
166
- FLAVOR_MAP.key?(ns)
167
- end&.then { |ns| FLAVOR_MAP[ns] }
167
+ Metanorma::Html::Generator.flavors.name_for(@document.class)
168
+ end
169
+
170
+ # Pubid module for the current document's flavor, or nil if the
171
+ # flavor has no Pubid support. Queries the shared FlavorRegistry.
172
+ def pubid_module
173
+ return nil unless defined?(@document) && @document
174
+
175
+ Metanorma::Html::Generator.flavors.pubid_module_for(@document.class)
168
176
  end
169
177
 
170
178
  def flavor_publishers(_doc_id)
@@ -269,25 +277,6 @@ module Metanorma
269
277
  end.join("\n")
270
278
  end
271
279
 
272
- def load_logo_svg(filename, height: 32)
273
- path = theme.resolve_asset(filename) || File.join(LOGO_DIR, filename)
274
- return nil unless File.exist?(path)
275
-
276
- svg = File.read(path)
277
- svg = svg.sub(/\A<\?xml[^?]*\?>\s*/, "")
278
- svg = svg.sub(/\A\s*<!--.*?-->\s*/m, "")
279
- svg = svg.sub(/<svg\s/, '<svg class="header-logo" ')
280
- svg = if svg.match?(/<svg[^>]*\sheight="[^"]*"/)
281
- svg.sub(/(<svg[^>]*?)(\sheight="[^"]*")/,
282
- "\\1 height=\"#{height}\"")
283
- else
284
- svg.sub(/(<svg\b)/, "\\1 height=\"#{height}\"")
285
- end
286
- svg.sub(/(<svg[^>]*?)\swidth="[^"]*"/, '\1')
287
- rescue StandardError
288
- nil
289
- end
290
-
291
280
  def build_footer
292
281
  mn_logo = load_logo_svg(METANORMA_LOGO, height: 20)
293
282
  render_liquid("_footer.html.liquid", {
@@ -296,22 +285,6 @@ module Metanorma
296
285
  })
297
286
  end
298
287
 
299
- # --- ToC generation ---
300
-
301
- def build_toc_html(entries)
302
- entry_drops = entries.map { |e| Drops::TocEntryDrop.new(e) }
303
- figure_drops = @figure_entries.map { |f| Drops::FigureListEntryDrop.new(f) }
304
- table_drops = @table_entries.map { |t| Drops::FigureListEntryDrop.new(t) }
305
- has_special_lists = !@figure_entries.empty? || !@table_entries.empty?
306
-
307
- render_liquid("_toc.html.liquid", {
308
- "entries" => entry_drops,
309
- "figures" => figure_drops,
310
- "tables" => table_drops,
311
- "has_special_lists" => has_special_lists,
312
- })
313
- end
314
-
315
288
  # --- Scripts ---
316
289
 
317
290
  def build_scripts
@@ -337,181 +310,28 @@ module Metanorma
337
310
  parts.join("\n")
338
311
  end
339
312
 
340
- # --- Validation ---
341
-
342
- def validate_presentation_xml!
343
- has_presentation = check_presentation_markers(@document)
344
- return if has_presentation
345
-
346
- raise ArgumentError,
347
- "HTML generation requires Presentation XML input. " \
348
- "Semantic XML does not contain formatting data needed for HTML. " \
349
- "Use a '.presentation.xml' file instead."
350
- end
351
-
352
- def check_presentation_markers(node)
353
- return false unless node
354
- return false if node.is_a?(String)
355
-
356
- if node.is_a?(Lutaml::Model::Serializable)
357
- node_attrs = node.class.attributes
358
- if node_attrs.key?(:type) && node.type == "presentation"
359
- return true
360
- end
361
- if node_attrs.key?(:fmt_title) && node.fmt_title
362
- return true
363
- end
364
- if node_attrs.key?(:displayorder) && node.displayorder
365
- return true
366
- end
367
-
368
- %i[preface sections annex bibliography].each do |attr|
369
- next unless node_attrs.key?(attr)
370
-
371
- val = node.public_send(attr)
372
- next unless val
373
-
374
- Array(val).each { |v| return true if check_presentation_markers(v) }
375
- end
376
-
377
- node.each_mixed_content do |child|
378
- next if child.is_a?(String)
379
- return true if check_presentation_markers(child)
380
- end
381
- end
382
-
383
- false
384
- end
385
-
386
- # --- Metadata extraction ---
387
-
388
- def language
389
- bibdata = @document.bibdata
390
- return "en" unless bibdata
391
-
392
- langs = bibdata.language
393
- if langs && !langs.empty?
394
- lang = langs.find { |l| l.current == "true" } || langs.first
395
- lang.value || lang.to_s
396
- else
397
- "en"
398
- end
399
- end
400
-
401
- def html_title
402
- extract_display_title(@document.bibdata) || "Document"
403
- end
404
-
405
- def extract_display_title(bibdata)
406
- return nil unless bibdata
407
-
408
- title = bibdata.title_for("en") if bibdata.is_a?(Metanorma::Document::Components::BibData::BibData)
409
- return title.to_s if title && !title.to_s.empty?
410
-
411
- titles = safe_attr(bibdata, :title)
412
- return nil unless titles && !titles.is_a?(String) && !titles.empty?
413
-
414
- en = titles.find { |t| safe_attr(t, :language) == "en" }
415
- found = en || titles.first
416
- extract_text_value(found).to_s
417
- end
418
-
419
- def extract_primary_doc_id
420
- bibdata = @document.bibdata
421
- return nil unless bibdata
422
-
423
- identifiers = bibdata.doc_identifier
424
- return nil unless identifiers && !identifiers.empty?
425
-
426
- first_id = identifiers.first
427
- text = if first_id.is_a?(String)
428
- first_id
429
- elsif first_id.is_a?(Lutaml::Model::Serializable)
430
- Array(first_id.value).join
431
- else
432
- first_id.to_s
433
- end
434
- text.strip.empty? ? nil : text.strip
435
- end
436
-
437
- # --- Registration helpers ---
438
-
439
- def register_toc_entry(id:, level:, text:)
440
- @toc_entries << { id: id, level: level, text: text }
441
- end
442
-
443
- def register_figure_entry(id:, text:)
444
- @figure_entries << { id: id, text: text }
445
- end
446
-
447
- def figure_entries
448
- @figure_entries
449
- end
450
-
451
- def register_table_entry(id:, text:)
452
- @table_entries << { id: id, text: text }
453
- end
454
-
455
- def extract_plain_text(node)
456
- return node.to_s if node.is_a?(String)
457
- return extract_text_value(node).to_s unless node.is_a?(Lutaml::Model::Serializable)
458
-
459
- parts = []
460
- xml_mapping = node.class.mappings_for(:xml, node.lutaml_register)
461
-
462
- if node.element_order.is_a?(Array) && xml_mapping
463
- element_to_attr = {}
464
- xml_mapping.mapping_elements_hash.each_value do |rule_or_array|
465
- Array(rule_or_array).each do |rule|
466
- element_to_attr[rule.name.to_s] = rule.to
467
- end
468
- end
469
-
470
- indices = Hash.new(0)
471
- node.element_order.each do |el|
472
- next unless el.is_a?(Lutaml::Xml::Element)
473
-
474
- if el.text?
475
- parts << el.text_content.to_s
476
- elsif el.name == "tab"
477
- parts << " "
478
- elsif el.name == "br"
479
- parts << " "
480
- elsif el.element?
481
- attr_name = element_to_attr[el.name]
482
- if attr_name
483
- coll = node.public_send(attr_name)
484
- obj = if coll.is_a?(Array)
485
- idx = indices[attr_name]
486
- indices[attr_name] += 1
487
- coll[idx]
488
- else
489
- coll
490
- end
491
- text = extract_plain_text(obj)
492
- parts << (text.empty? ? " " : text)
493
- elsif el.name == "span"
494
- parts << " "
495
- end
496
- end
497
- end
498
- end
499
-
500
- if parts.join.strip.empty?
501
- t = safe_attr(node, :text)
502
- parts << (t.is_a?(Array) ? t.join : t.to_s) if t
503
- end
504
-
505
- parts.join.strip.gsub(" ", " ")
506
- end
507
-
508
313
  # --- Dispatch ---
509
314
 
510
315
  def render(node, **)
511
316
  return escape_html(node) if node.is_a?(String)
512
317
 
513
318
  method = lookup_dispatch(node.class, :render_registry)
514
- method ? public_send(method, node, **) : ""
319
+ return public_send(method, node, **) if method
320
+
321
+ record_render_warning(
322
+ "no renderer registered for #{node.class} — content skipped",
323
+ )
324
+ ""
325
+ end
326
+
327
+ # Emit a render warning once per distinct message. Warnings mark
328
+ # content that was skipped or degraded rather than rendered.
329
+ def record_render_warning(message)
330
+ @render_warnings ||= {}
331
+ return if @render_warnings.key?(message)
332
+
333
+ @render_warnings[message] = true
334
+ warn "metanorma-document: #{message}"
515
335
  end
516
336
 
517
337
  def render_inline_element(element, **)
@@ -543,6 +363,8 @@ module Metanorma
543
363
  :render_note
544
364
  register_render Metanorma::Document::Components::AncillaryBlocks::ExampleBlock,
545
365
  :render_example
366
+ register_render Metanorma::StandardDocument::Blocks::Form,
367
+ :render_form
546
368
  register_render Metanorma::Document::Components::AncillaryBlocks::SourcecodeBlock,
547
369
  :render_sourcecode
548
370
  register_render Metanorma::Document::Components::AncillaryBlocks::FormulaBlock,
@@ -608,6 +430,8 @@ module Metanorma
608
430
  :render_fmt_stem
609
431
  register_inline_render Metanorma::Document::Components::Inline::CommaElement,
610
432
  :render_comma
433
+ register_inline_render Metanorma::StandardDocument::Elements::Input,
434
+ :render_input
611
435
  register_inline_render Metanorma::Document::Components::Inline::EnumCommaElement,
612
436
  :render_comma
613
437
  register_inline_render Metanorma::Document::Components::IdElements::Bookmark,
@@ -624,52 +448,66 @@ module Metanorma
624
448
  :render_index
625
449
  register_inline_render Metanorma::Document::Components::Blocks::NoteBlock,
626
450
  :render_note_inline
627
- register_inline_render Metanorma::Document::Components::Inline::FmtNameElement,
628
- :render_mixed_inline
629
- register_inline_render Metanorma::Document::Components::Inline::FmtTitleElement,
630
- :render_mixed_inline
631
- register_inline_render Metanorma::Document::Components::Inline::FmtXrefLabelElement,
632
- :render_mixed_inline
633
- register_inline_render Metanorma::Document::Components::Inline::FmtFnLabelElement,
634
- :render_mixed_inline
635
- register_inline_render Metanorma::Document::Components::Inline::FmtConceptElement,
636
- :render_mixed_inline
637
- register_inline_render Metanorma::Document::Components::Inline::FmtAnnotationStartElement,
638
- :render_mixed_inline
639
- register_inline_render Metanorma::Document::Components::Inline::FmtAnnotationEndElement,
640
- :render_mixed_inline
641
- register_inline_render Metanorma::Document::Components::Inline::FmtAnnotationBodyElement,
642
- :render_mixed_inline
643
- register_inline_render Metanorma::Document::Components::Inline::VariantTitleElement,
644
- :render_mixed_inline
645
- register_inline_render Metanorma::Document::Components::Inline::LocalizedStringElement,
646
- :render_mixed_inline
647
- register_inline_render Metanorma::Document::Components::Inline::TitleWithAnnotationElement,
648
- :render_mixed_inline
649
- register_inline_render Metanorma::Document::Components::Inline::BiblioTagElement,
650
- :render_mixed_inline
651
- register_inline_render Metanorma::Document::Components::Inline::NameWithIdElement,
652
- :render_mixed_inline
653
- register_inline_render Metanorma::Document::Components::Inline::DisplayTextElement,
654
- :render_mixed_inline
655
- register_inline_render Metanorma::Document::Components::Inline::FmtFootnoteContainerElement,
656
- :render_mixed_inline
657
- register_inline_render Metanorma::Document::Components::Inline::FmtFnBodyElement,
658
- :render_mixed_inline
659
- register_inline_render Metanorma::Document::Components::Inline::FmtPreferredElement,
660
- :render_mixed_inline
661
- register_inline_render Metanorma::Document::Components::Inline::FmtDefinitionElement,
662
- :render_mixed_inline
663
- register_inline_render Metanorma::Document::Components::Inline::FmtTermsourceElement,
664
- :render_mixed_inline
665
- register_inline_render Metanorma::Document::Components::Inline::FmtAdmittedElement,
666
- :render_mixed_inline
667
- register_inline_render Metanorma::Document::Components::Inline::FmtIdentifierElement,
668
- :render_mixed_inline
669
- register_inline_render Metanorma::Document::Components::Inline::FmtSourcecodeElement,
670
- :render_mixed_inline
451
+
452
+ # Transparent inline wrappers: element classes whose render
453
+ # semantics are "iterate mixed_content and render each child".
454
+ # Grouped here so adding a new transparent wrapper is one entry,
455
+ # not another register_inline_render line. Classes with a more
456
+ # specific handler register themselves above.
457
+ TRANSPARENT_INLINE_WRAPPERS = [
458
+ Metanorma::Document::Components::Inline::FmtNameElement,
459
+ Metanorma::Document::Components::Inline::FmtTitleElement,
460
+ Metanorma::Document::Components::Inline::FmtXrefLabelElement,
461
+ Metanorma::Document::Components::Inline::FmtFnLabelElement,
462
+ Metanorma::Document::Components::Inline::FmtConceptElement,
463
+ Metanorma::Document::Components::Inline::FmtAnnotationStartElement,
464
+ Metanorma::Document::Components::Inline::FmtAnnotationEndElement,
465
+ Metanorma::Document::Components::Inline::FmtAnnotationBodyElement,
466
+ Metanorma::Document::Components::Inline::VariantTitleElement,
467
+ Metanorma::Document::Components::Inline::LocalizedStringElement,
468
+ Metanorma::Document::Components::Inline::TitleWithAnnotationElement,
469
+ Metanorma::Document::Components::Inline::BiblioTagElement,
470
+ Metanorma::Document::Components::Inline::NameWithIdElement,
471
+ Metanorma::Document::Components::Inline::DisplayTextElement,
472
+ Metanorma::Document::Components::Inline::FmtFootnoteContainerElement,
473
+ Metanorma::Document::Components::Inline::FmtFnBodyElement,
474
+ Metanorma::Document::Components::Inline::FmtPreferredElement,
475
+ Metanorma::Document::Components::Inline::FmtDefinitionElement,
476
+ Metanorma::Document::Components::Inline::FmtTermsourceElement,
477
+ Metanorma::Document::Components::Inline::FmtAdmittedElement,
478
+ Metanorma::Document::Components::Inline::FmtIdentifierElement,
479
+ Metanorma::Document::Components::Inline::FmtSourcecodeElement,
480
+ ].freeze
481
+
482
+ TRANSPARENT_INLINE_WRAPPERS.each do |klass|
483
+ register_inline_render klass, :render_mixed_inline
484
+ end
485
+
486
+ # Inline-ish classes that are also reached through the BLOCK
487
+ # dispatch (as direct children of block containers, e.g. fmt-title
488
+ # as a child of clause). fmt-title and variant-title duplicate the
489
+ # semantic `title` attribute that sections render — skip them.
490
+ register_render Metanorma::Document::Components::Inline::FmtTitleElement,
491
+ :render_noop
492
+ register_render Metanorma::Document::Components::Inline::VariantTitleElement,
493
+ :render_noop
494
+ # fmt-xref-label carries the cross-reference link label; render it
495
+ # through when it appears in block context.
496
+ register_render Metanorma::Document::Components::Inline::FmtXrefLabelElement,
497
+ :render_block_inline_content
671
498
 
672
499
  def lookup_dispatch(type_class, registry_method)
500
+ # Registry contents are fixed once renderer classes are loaded, so
501
+ # the resolved method per (registry, node class) is memoized for
502
+ # the lifetime of this renderer instance.
503
+ cache = (@dispatch_cache ||= {})
504
+ key = [registry_method, type_class]
505
+ return cache[key] if cache.key?(key)
506
+
507
+ cache[key] = resolve_dispatch(type_class, registry_method)
508
+ end
509
+
510
+ def resolve_dispatch(type_class, registry_method)
673
511
  self.class.ancestors.each do |ancestor|
674
512
  next unless ancestor.is_a?(Class) && (ancestor == BaseRenderer || ancestor < BaseRenderer)
675
513
 
@@ -684,6 +522,13 @@ module Metanorma
684
522
  ""
685
523
  end
686
524
 
525
+ # Block-dispatch entry point for inline content reached as a direct
526
+ # child of a block container (render passes keyword args; the inline
527
+ # pipeline does not accept them).
528
+ def render_block_inline_content(el, **)
529
+ render_mixed_inline(el)
530
+ end
531
+
687
532
  def render_noop_inline(*)
688
533
  nil
689
534
  end
@@ -716,6 +561,7 @@ module Metanorma
716
561
  def render_semx_inline(el) = @inline_renderer.render_semx_inline(el)
717
562
  def render_fmt_xref(el) = @inline_renderer.render_fmt_xref(el)
718
563
  def render_comma(*) = @inline_renderer.render_comma
564
+ def render_input(el) = @inline_renderer.render_input(el)
719
565
  def render_math(el) = @inline_renderer.render_math(el)
720
566
  def render_asciimath(el) = @inline_renderer.render_asciimath(el)
721
567
  def render_index(el) = @inline_renderer.render_index(el)
@@ -776,6 +622,8 @@ module Metanorma
776
622
  @block_renderer.render_example(example, **)
777
623
  end
778
624
 
625
+ def render_form(form, **) = @block_renderer.render_form(form, **)
626
+
779
627
  def render_sourcecode(sc,
780
628
  **)
781
629
  @block_renderer.render_sourcecode(sc, **)
@@ -887,7 +735,12 @@ level = 1)
887
735
  end
888
736
 
889
737
  obj.public_send(method_name)
890
- rescue NoMethodError
738
+ rescue NoMethodError => e
739
+ # Only swallow "method missing on this object" (duck-typing across
740
+ # model classes). A NoMethodError raised *inside* the getter is a
741
+ # real bug — let it surface.
742
+ raise unless e.receiver.equal?(obj)
743
+
891
744
  nil
892
745
  end
893
746
 
@@ -902,7 +755,8 @@ level = 1)
902
755
  target_id: @current_section_id,
903
756
  target_text: @current_section_number,
904
757
  )
905
- rescue StandardError
758
+ rescue StandardError => e
759
+ record_render_warning("index term dropped: #{e.class}: #{e.message}")
906
760
  nil
907
761
  end
908
762
 
@@ -942,34 +796,6 @@ level = 1)
942
796
  CGI.escapeHTML(text.to_s)
943
797
  end
944
798
 
945
- def extract_text_value(val)
946
- return nil if val.nil?
947
- return val if val.is_a?(String)
948
-
949
- if val.is_a?(Array)
950
- val.map { |v| extract_text_value(v) }.join
951
- elsif val.is_a?(Lutaml::Model::Serializable)
952
- c = safe_attr(val, :content)
953
- if c && !c.equal?(val)
954
- extract_text_value(c)
955
- else
956
- t = safe_attr(val, :text)
957
- if t
958
- extract_text_value(t)
959
- else
960
- v = safe_attr(val, :value)
961
- if v
962
- extract_text_value(v)
963
- else
964
- val.to_s
965
- end
966
- end
967
- end
968
- else
969
- val.to_s
970
- end
971
- end
972
-
973
799
  def render_footnotes_section
974
800
  return nil if @footnote_collector.empty?
975
801
 
@@ -4,6 +4,8 @@ module Metanorma
4
4
  module Html
5
5
  module Component
6
6
  class Base
7
+ include Metanorma::Html::RendererDelegation
8
+
7
9
  attr_reader :renderer
8
10
 
9
11
  def initialize(renderer)
@@ -38,18 +40,9 @@ module Metanorma
38
40
  raise NotImplementedError, "#{self.class}#render not implemented"
39
41
  end
40
42
 
41
- def escape_html(...) = renderer.escape_html(...)
42
- def render_mixed_inline(...) = renderer.render_mixed_inline(...)
43
43
  def render_mixed_content_in_order(...) = renderer.render_mixed_content_in_order(...)
44
44
  def element_attrs(...) = renderer.element_attrs(...)
45
- def safe_attr(obj, method_name) = renderer.safe_attr(obj, method_name)
46
- def extract_plain_text(node) = renderer.extract_plain_text(node)
47
45
  def extract_text_value(val) = renderer.extract_text_value(val)
48
- def render_liquid(...) = renderer.render_liquid(...)
49
-
50
- def extract_block_label(block, default)
51
- renderer.extract_block_label(block, default)
52
- end
53
46
 
54
47
  def self.register_in(registry)
55
48
  handled_classes.each do |klass|
@@ -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