metanorma-document 0.3.0 → 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 (183) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +3 -2
  3. data/Rakefile +30 -5
  4. data/frontend/dist/app.css +1 -0
  5. data/frontend/dist/app.iife.js +24 -0
  6. data/frontend/package-lock.json +7 -505
  7. data/frontend/tailwind.config.js +5 -0
  8. data/lib/metanorma/csa_document/root.rb +27 -0
  9. data/lib/metanorma/csa_document.rb +9 -0
  10. data/lib/metanorma/document/cli.rb +52 -0
  11. data/lib/metanorma/document/components/ancillary_blocks/sourcecode_body.rb +14 -0
  12. data/lib/metanorma/document/components/inline/comma_element.rb +3 -0
  13. data/lib/metanorma/document/components/inline/enum_comma_element.rb +3 -0
  14. data/lib/metanorma/document/components/inline/eref_element.rb +4 -1
  15. data/lib/metanorma/document/components/inline/fmt_termsource_element.rb +4 -2
  16. data/lib/metanorma/document/components/inline/math_element.rb +13 -7
  17. data/lib/metanorma/document/components/inline/vocabulary.rb +4 -0
  18. data/lib/metanorma/document/components/multi_paragraph/admonition_block.rb +6 -1
  19. data/lib/metanorma/document/components/paragraphs/paragraph_block.rb +3 -0
  20. data/lib/metanorma/document/components/reference_elements/callout.rb +3 -2
  21. data/lib/metanorma/document/components/tables/table_cell.rb +3 -0
  22. data/lib/metanorma/document/elements/input.rb +44 -0
  23. data/lib/metanorma/document/elements.rb +1 -0
  24. data/lib/metanorma/document/relaton/address.rb +5 -0
  25. data/lib/metanorma/document/relaton/affiliation.rb +3 -0
  26. data/lib/metanorma/document/relaton/bib_item_size.rb +8 -9
  27. data/lib/metanorma/document/relaton/bib_item_type.rb +3 -0
  28. data/lib/metanorma/document/relaton/bibliographic_date.rb +5 -0
  29. data/lib/metanorma/document/relaton/contact_method.rb +5 -0
  30. data/lib/metanorma/document/relaton/contribution_info.rb +6 -2
  31. data/lib/metanorma/document/relaton/contributor.rb +4 -0
  32. data/lib/metanorma/document/relaton/contributor_role.rb +3 -0
  33. data/lib/metanorma/document/relaton/copyright_association.rb +3 -0
  34. data/lib/metanorma/document/relaton/copyright_owner.rb +3 -0
  35. data/lib/metanorma/document/relaton/date_time.rb +3 -0
  36. data/lib/metanorma/document/relaton/document_identifier.rb +4 -0
  37. data/lib/metanorma/document/relaton/document_relation.rb +8 -0
  38. data/lib/metanorma/document/relaton/document_status.rb +4 -0
  39. data/lib/metanorma/document/relaton/formatted_address.rb +4 -0
  40. data/lib/metanorma/document/relaton/full_name.rb +14 -3
  41. data/lib/metanorma/document/relaton/keyword_type.rb +3 -0
  42. data/lib/metanorma/document/relaton/locality_stack.rb +6 -9
  43. data/lib/metanorma/document/relaton/localized_name.rb +8 -5
  44. data/lib/metanorma/document/relaton/logo_element.rb +5 -0
  45. data/lib/metanorma/document/relaton/medium_type.rb +5 -16
  46. data/lib/metanorma/document/relaton/org_identifier.rb +2 -0
  47. data/lib/metanorma/document/relaton/org_subdivision.rb +2 -0
  48. data/lib/metanorma/document/relaton/org_subdivision_identifier.rb +2 -0
  49. data/lib/metanorma/document/relaton/organization.rb +3 -0
  50. data/lib/metanorma/document/relaton/person.rb +7 -1
  51. data/lib/metanorma/document/relaton/person_identifier.rb +3 -0
  52. data/lib/metanorma/document/relaton/place_type.rb +5 -0
  53. data/lib/metanorma/document/relaton/region_element.rb +3 -9
  54. data/lib/metanorma/document/relaton/relaton_collection.rb +2 -0
  55. data/lib/metanorma/document/relaton/series_type.rb +7 -0
  56. data/lib/metanorma/document/relaton/typed_note.rb +4 -0
  57. data/lib/metanorma/document/relaton/typed_title_string.rb +4 -0
  58. data/lib/metanorma/document/relaton/typed_uri.rb +3 -8
  59. data/lib/metanorma/document/relaton/validity_type.rb +8 -10
  60. data/lib/metanorma/document/relaton/variant_full_name.rb +3 -0
  61. data/lib/metanorma/document/relaton/variant_org_name.rb +3 -0
  62. data/lib/metanorma/document/relaton/version_info.rb +5 -0
  63. data/lib/metanorma/document/relaton/vocab_id_type.rb +6 -12
  64. data/lib/metanorma/document/relaton.rb +0 -14
  65. data/lib/metanorma/document/version.rb +1 -1
  66. data/lib/metanorma/document.rb +1 -0
  67. data/lib/metanorma/html/base_renderer.rb +86 -234
  68. data/lib/metanorma/html/concerns/metadata_extraction.rb +61 -0
  69. data/lib/metanorma/html/concerns/presentation_validation.rb +61 -0
  70. data/lib/metanorma/html/concerns/svg_processing.rb +32 -0
  71. data/lib/metanorma/html/concerns/text_extraction.rb +93 -0
  72. data/lib/metanorma/html/concerns/toc_registry.rb +47 -0
  73. data/lib/metanorma/html/csa_renderer.rb +9 -0
  74. data/lib/metanorma/html/drops/figure_drop.rb +33 -10
  75. data/lib/metanorma/html/drops/sourcecode_drop.rb +3 -6
  76. data/lib/metanorma/html/flavor.rb +6 -1
  77. data/lib/metanorma/html/generator.rb +8 -1
  78. data/lib/metanorma/html/iso_renderer.rb +0 -142
  79. data/lib/metanorma/html/renderers/block_renderer.rb +10 -0
  80. data/lib/metanorma/html/renderers/element_order_traversal.rb +15 -1
  81. data/lib/metanorma/html/renderers/inline_renderer.rb +13 -75
  82. data/lib/metanorma/html/standard_renderer.rb +195 -35
  83. data/lib/metanorma/html.rb +1 -51
  84. data/lib/metanorma/iso_document/sections/iso_preface.rb +27 -13
  85. data/lib/metanorma/mirror/default_registry.rb +21 -0
  86. data/lib/metanorma/mirror/handlers/inline/catalog.rb +7 -12
  87. data/lib/metanorma/mirror/handlers/inline/rich_html_renderer.rb +2 -3
  88. data/lib/metanorma/mirror/handlers/sourcecode.rb +1 -1
  89. data/lib/metanorma/mirror/model/guide.rb +3 -2
  90. data/lib/metanorma/mirror/output/formats/inline_format.rb +64 -3
  91. data/lib/metanorma/mirror/output/pipeline.rb +1 -0
  92. data/lib/metanorma/mirror/output.rb +0 -2
  93. data/lib/metanorma/mirror/rewriter.rb +3 -1
  94. data/lib/metanorma/registers/setup.rb +10 -0
  95. data/lib/metanorma/standard_document/annotation_container.rb +2 -0
  96. data/lib/metanorma/standard_document/elements/input.rb +4 -25
  97. data/lib/metanorma/standard_document/metadata/metanorma_extension.rb +12 -2
  98. data/lib/metanorma/standard_document/metadata/presentation_metadata.rb +107 -0
  99. data/lib/metanorma/standard_document/metadata/semantic_metadata.rb +18 -0
  100. data/lib/metanorma/standard_document/metadata/unitsml/amount_of_substance.rb +19 -0
  101. data/lib/metanorma/standard_document/metadata/unitsml/base_quantity.rb +34 -0
  102. data/lib/metanorma/standard_document/metadata/unitsml/dimension.rb +39 -0
  103. data/lib/metanorma/standard_document/metadata/unitsml/dimension_set.rb +19 -0
  104. data/lib/metanorma/standard_document/metadata/unitsml/electric_current.rb +19 -0
  105. data/lib/metanorma/standard_document/metadata/unitsml/enumerated_root_unit.rb +23 -0
  106. data/lib/metanorma/standard_document/metadata/unitsml/length.rb +19 -0
  107. data/lib/metanorma/standard_document/metadata/unitsml/luminous_intensity.rb +19 -0
  108. data/lib/metanorma/standard_document/metadata/unitsml/mass.rb +19 -0
  109. data/lib/metanorma/standard_document/metadata/unitsml/namespace.rb +17 -0
  110. data/lib/metanorma/standard_document/metadata/unitsml/plane_angle.rb +19 -0
  111. data/lib/metanorma/standard_document/metadata/unitsml/prefix.rb +30 -0
  112. data/lib/metanorma/standard_document/metadata/unitsml/prefix_name.rb +21 -0
  113. data/lib/metanorma/standard_document/metadata/unitsml/prefix_set.rb +19 -0
  114. data/lib/metanorma/standard_document/metadata/unitsml/prefix_symbol.rb +21 -0
  115. data/lib/metanorma/standard_document/metadata/unitsml/quantity.rb +28 -0
  116. data/lib/metanorma/standard_document/metadata/unitsml/quantity_name.rb +21 -0
  117. data/lib/metanorma/standard_document/metadata/unitsml/quantity_set.rb +19 -0
  118. data/lib/metanorma/standard_document/metadata/unitsml/root_units.rb +19 -0
  119. data/lib/metanorma/standard_document/metadata/unitsml/thermodynamic_temperature.rb +19 -0
  120. data/lib/metanorma/standard_document/metadata/unitsml/time.rb +19 -0
  121. data/lib/metanorma/standard_document/metadata/unitsml/unit.rb +32 -0
  122. data/lib/metanorma/standard_document/metadata/unitsml/unit_name.rb +21 -0
  123. data/lib/metanorma/standard_document/metadata/unitsml/unit_set.rb +19 -0
  124. data/lib/metanorma/standard_document/metadata/unitsml/unit_symbol.rb +36 -0
  125. data/lib/metanorma/standard_document/metadata/unitsml/unit_system.rb +23 -0
  126. data/lib/metanorma/standard_document/metadata/unitsml/unitsml_root.rb +28 -0
  127. data/lib/metanorma/standard_document/metadata/unitsml.rb +39 -0
  128. data/lib/metanorma/standard_document/metadata.rb +4 -0
  129. data/lib/metanorma/standard_document/sections/misc_container.rb +8 -6
  130. data/lib/metanorma/standard_document/sections/sections.rb +7 -0
  131. data/lib/metanorma/un_document/blocks/un_admonition_block.rb +86 -0
  132. data/lib/metanorma/un_document/blocks.rb +9 -0
  133. data/lib/metanorma/un_document/root.rb +8 -0
  134. data/lib/metanorma/un_document/sections/un_abstract_section.rb +48 -0
  135. data/lib/metanorma/un_document/sections/un_preface.rb +19 -2
  136. data/lib/metanorma/un_document/sections.rb +1 -0
  137. data/lib/metanorma/un_document/un_text_element.rb +116 -0
  138. data/lib/metanorma/un_document.rb +2 -0
  139. data/script/benchmark_render.rb +35 -0
  140. data/tasks/roundtrip_samples.rake +1 -1
  141. metadata +63 -45
  142. data/.rubocop_todo.yml +0 -220
  143. data/TODO.finalize/01-split-multi-class-files.md +0 -8
  144. data/TODO.finalize/02-remove-data-dist-iso-document-frontend.md +0 -6
  145. data/TODO.finalize/03-align-frontend-iife-and-gha-build.md +0 -11
  146. data/TODO.finalize/04-theme-per-flavor-config-driven.md +0 -9
  147. data/TODO.finalize/05-migrate-all-html-to-liquid.md +0 -16
  148. data/TODO.finalize/06-integrate-pubid-library.md +0 -16
  149. data/TODO.finalize/07-replace-relaton-models-with-relaton-bib.md +0 -39
  150. data/TODO.finalize/08-refactor-renderer-architecture.md +0 -25
  151. data/TODO.finalize/09-eliminate-code-smells.md +0 -14
  152. data/TODO.finalize/10-comprehensive-spec-coverage.md +0 -24
  153. data/TODO.finalize/11-config-driven-flavor-metadata.md +0 -20
  154. data/TODO.finalize/12-add-frozen-string-literal.md +0 -6
  155. data/TODO.finalize/13-extract-composition-from-god-class.md +0 -96
  156. data/TODO.finalize/14-config-driven-section-ordering.md +0 -31
  157. data/TODO.finalize/15-theme-directories-per-flavor.md +0 -46
  158. data/TODO.finalize/16-theme-lutaml-model-schema.md +0 -44
  159. data/docs/html-renderer.adoc +0 -261
  160. data/lib/data/dist/iso_document/frontend/assets/index-mwzbzmnK.js +0 -22
  161. data/lib/data/dist/iso_document/frontend/assets/index-pGvKTNid.css +0 -1
  162. data/lib/data/dist/iso_document/frontend/assets/iso-red.svg +0 -53
  163. data/lib/data/dist/iso_document/frontend/index.html +0 -13
  164. data/lib/metanorma/document/relaton/bib_item_size_type.rb +0 -21
  165. data/lib/metanorma/document/relaton/bibliographic_date_type.rb +0 -22
  166. data/lib/metanorma/document/relaton/document_relation_type.rb +0 -31
  167. data/lib/metanorma/document/relaton/iso4217_code.rb +0 -17
  168. data/lib/metanorma/document/relaton/iso8601_date.rb +0 -17
  169. data/lib/metanorma/document/relaton/personal_identifier_type.rb +0 -21
  170. data/lib/metanorma/document/relaton/region_type.rb +0 -20
  171. data/lib/metanorma/document/relaton/series_type_type.rb +0 -21
  172. data/lib/metanorma/document/relaton/specific_locality_type.rb +0 -25
  173. data/lib/metanorma/document/relaton/title_type.rb +0 -21
  174. data/lib/metanorma/mirror/output/html_renderer.rb +0 -95
  175. data/lib/metanorma/mirror/output/html_renderers/block_renderers.rb +0 -178
  176. data/lib/metanorma/mirror/output/html_renderers/inline_renderer.rb +0 -61
  177. data/lib/metanorma/mirror/output/html_renderers/list_renderers.rb +0 -96
  178. data/lib/metanorma/mirror/output/html_renderers/mark_renderers.rb +0 -41
  179. data/lib/metanorma/mirror/output/html_renderers/section_renderers.rb +0 -129
  180. data/lib/metanorma/mirror/output/html_renderers/structural_renderers.rb +0 -80
  181. data/lib/metanorma/mirror/output/html_renderers/table_renderers.rb +0 -87
  182. data/lib/metanorma/mirror/output/html_renderers.rb +0 -76
  183. 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,10 +131,6 @@ 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
136
  def generate_full_document(document, **)
@@ -130,6 +142,14 @@ module Metanorma
130
142
  assemble_document(body)
131
143
  end
132
144
 
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
152
+
133
153
  # --- Flavor configuration hooks ---
134
154
 
135
155
  def theme
@@ -257,25 +277,6 @@ module Metanorma
257
277
  end.join("\n")
258
278
  end
259
279
 
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
280
  def build_footer
280
281
  mn_logo = load_logo_svg(METANORMA_LOGO, height: 20)
281
282
  render_liquid("_footer.html.liquid", {
@@ -284,22 +285,6 @@ module Metanorma
284
285
  })
285
286
  end
286
287
 
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
288
  # --- Scripts ---
304
289
 
305
290
  def build_scripts
@@ -325,177 +310,28 @@ module Metanorma
325
310
  parts.join("\n")
326
311
  end
327
312
 
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
313
  # --- Dispatch ---
493
314
 
494
315
  def render(node, **)
495
316
  return escape_html(node) if node.is_a?(String)
496
317
 
497
318
  method = lookup_dispatch(node.class, :render_registry)
498
- 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}"
499
335
  end
500
336
 
501
337
  def render_inline_element(element, **)
@@ -527,6 +363,8 @@ module Metanorma
527
363
  :render_note
528
364
  register_render Metanorma::Document::Components::AncillaryBlocks::ExampleBlock,
529
365
  :render_example
366
+ register_render Metanorma::StandardDocument::Blocks::Form,
367
+ :render_form
530
368
  register_render Metanorma::Document::Components::AncillaryBlocks::SourcecodeBlock,
531
369
  :render_sourcecode
532
370
  register_render Metanorma::Document::Components::AncillaryBlocks::FormulaBlock,
@@ -592,6 +430,8 @@ module Metanorma
592
430
  :render_fmt_stem
593
431
  register_inline_render Metanorma::Document::Components::Inline::CommaElement,
594
432
  :render_comma
433
+ register_inline_render Metanorma::StandardDocument::Elements::Input,
434
+ :render_input
595
435
  register_inline_render Metanorma::Document::Components::Inline::EnumCommaElement,
596
436
  :render_comma
597
437
  register_inline_render Metanorma::Document::Components::IdElements::Bookmark,
@@ -643,7 +483,31 @@ module Metanorma
643
483
  register_inline_render klass, :render_mixed_inline
644
484
  end
645
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
498
+
646
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)
647
511
  self.class.ancestors.each do |ancestor|
648
512
  next unless ancestor.is_a?(Class) && (ancestor == BaseRenderer || ancestor < BaseRenderer)
649
513
 
@@ -658,6 +522,13 @@ module Metanorma
658
522
  ""
659
523
  end
660
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
+
661
532
  def render_noop_inline(*)
662
533
  nil
663
534
  end
@@ -690,6 +561,7 @@ module Metanorma
690
561
  def render_semx_inline(el) = @inline_renderer.render_semx_inline(el)
691
562
  def render_fmt_xref(el) = @inline_renderer.render_fmt_xref(el)
692
563
  def render_comma(*) = @inline_renderer.render_comma
564
+ def render_input(el) = @inline_renderer.render_input(el)
693
565
  def render_math(el) = @inline_renderer.render_math(el)
694
566
  def render_asciimath(el) = @inline_renderer.render_asciimath(el)
695
567
  def render_index(el) = @inline_renderer.render_index(el)
@@ -750,6 +622,8 @@ module Metanorma
750
622
  @block_renderer.render_example(example, **)
751
623
  end
752
624
 
625
+ def render_form(form, **) = @block_renderer.render_form(form, **)
626
+
753
627
  def render_sourcecode(sc,
754
628
  **)
755
629
  @block_renderer.render_sourcecode(sc, **)
@@ -861,7 +735,12 @@ level = 1)
861
735
  end
862
736
 
863
737
  obj.public_send(method_name)
864
- 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
+
865
744
  nil
866
745
  end
867
746
 
@@ -876,7 +755,8 @@ level = 1)
876
755
  target_id: @current_section_id,
877
756
  target_text: @current_section_number,
878
757
  )
879
- rescue StandardError
758
+ rescue StandardError => e
759
+ record_render_warning("index term dropped: #{e.class}: #{e.message}")
880
760
  nil
881
761
  end
882
762
 
@@ -916,34 +796,6 @@ level = 1)
916
796
  CGI.escapeHTML(text.to_s)
917
797
  end
918
798
 
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
799
  def render_footnotes_section
948
800
  return nil if @footnote_collector.empty?
949
801
 
@@ -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
@@ -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