metanorma-document 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. checksums.yaml +4 -4
  2. data/BUGS.sts/08-list-items-duplicate-bullets.md +45 -0
  3. data/CLAUDE.md +3 -2
  4. data/Rakefile +30 -5
  5. data/data/stylesheets/components/header.css +3 -5
  6. data/data/stylesheets/components/lists.css +14 -0
  7. data/frontend/dist/app.css +1 -0
  8. data/frontend/dist/app.iife.js +24 -0
  9. data/frontend/package-lock.json +7 -505
  10. data/frontend/tailwind.config.js +5 -0
  11. data/lib/metanorma/{oiml_document → csa_document}/root.rb +2 -2
  12. data/lib/metanorma/csa_document.rb +9 -0
  13. data/lib/metanorma/document/cli.rb +52 -0
  14. data/lib/metanorma/document/components/ancillary_blocks/sourcecode_body.rb +14 -0
  15. data/lib/metanorma/document/components/inline/comma_element.rb +3 -0
  16. data/lib/metanorma/document/components/inline/enum_comma_element.rb +3 -0
  17. data/lib/metanorma/document/components/inline/eref_element.rb +4 -1
  18. data/lib/metanorma/document/components/inline/fmt_termsource_element.rb +4 -2
  19. data/lib/metanorma/document/components/inline/math_element.rb +13 -7
  20. data/lib/metanorma/document/components/inline/vocabulary.rb +4 -0
  21. data/lib/metanorma/document/components/multi_paragraph/admonition_block.rb +6 -1
  22. data/lib/metanorma/document/components/paragraphs/paragraph_block.rb +3 -0
  23. data/lib/metanorma/document/components/reference_elements/callout.rb +3 -2
  24. data/lib/metanorma/document/components/tables/table_cell.rb +3 -0
  25. data/lib/metanorma/document/elements/input.rb +44 -0
  26. data/lib/metanorma/document/elements.rb +1 -0
  27. data/lib/metanorma/document/relaton/address.rb +5 -0
  28. data/lib/metanorma/document/relaton/affiliation.rb +3 -0
  29. data/lib/metanorma/document/relaton/bib_item_size.rb +8 -9
  30. data/lib/metanorma/document/relaton/bib_item_type.rb +3 -0
  31. data/lib/metanorma/document/relaton/bibliographic_date.rb +5 -0
  32. data/lib/metanorma/document/relaton/contact_method.rb +5 -0
  33. data/lib/metanorma/document/relaton/contribution_info.rb +6 -2
  34. data/lib/metanorma/document/relaton/contributor.rb +4 -0
  35. data/lib/metanorma/document/relaton/contributor_role.rb +3 -0
  36. data/lib/metanorma/document/relaton/copyright_association.rb +3 -0
  37. data/lib/metanorma/document/relaton/copyright_owner.rb +3 -0
  38. data/lib/metanorma/document/relaton/date_time.rb +3 -0
  39. data/lib/metanorma/document/relaton/document_identifier.rb +4 -0
  40. data/lib/metanorma/document/relaton/document_relation.rb +8 -0
  41. data/lib/metanorma/document/relaton/document_status.rb +4 -0
  42. data/lib/metanorma/document/relaton/formatted_address.rb +4 -0
  43. data/lib/metanorma/document/relaton/full_name.rb +14 -3
  44. data/lib/metanorma/document/relaton/keyword_type.rb +3 -0
  45. data/lib/metanorma/document/relaton/locality_stack.rb +6 -9
  46. data/lib/metanorma/document/relaton/localized_name.rb +8 -5
  47. data/lib/metanorma/document/relaton/logo_element.rb +5 -0
  48. data/lib/metanorma/document/relaton/medium_type.rb +5 -16
  49. data/lib/metanorma/document/relaton/org_identifier.rb +2 -0
  50. data/lib/metanorma/document/relaton/org_subdivision.rb +2 -0
  51. data/lib/metanorma/document/relaton/org_subdivision_identifier.rb +2 -0
  52. data/lib/metanorma/document/relaton/organization.rb +3 -0
  53. data/lib/metanorma/document/relaton/person.rb +7 -1
  54. data/lib/metanorma/document/relaton/person_identifier.rb +3 -0
  55. data/lib/metanorma/document/relaton/place_type.rb +5 -0
  56. data/lib/metanorma/document/relaton/region_element.rb +3 -9
  57. data/lib/metanorma/document/relaton/relaton_collection.rb +2 -0
  58. data/lib/metanorma/document/relaton/series_type.rb +7 -0
  59. data/lib/metanorma/document/relaton/typed_note.rb +4 -0
  60. data/lib/metanorma/document/relaton/typed_title_string.rb +4 -0
  61. data/lib/metanorma/document/relaton/typed_uri.rb +3 -8
  62. data/lib/metanorma/document/relaton/validity_type.rb +8 -10
  63. data/lib/metanorma/document/relaton/variant_full_name.rb +3 -0
  64. data/lib/metanorma/document/relaton/variant_org_name.rb +3 -0
  65. data/lib/metanorma/document/relaton/version_info.rb +5 -0
  66. data/lib/metanorma/document/relaton/vocab_id_type.rb +6 -12
  67. data/lib/metanorma/document/relaton.rb +0 -14
  68. data/lib/metanorma/document/version.rb +1 -1
  69. data/lib/metanorma/document.rb +2 -1
  70. data/lib/metanorma/html/asset_pipeline.rb +1 -0
  71. data/lib/metanorma/html/base_renderer.rb +106 -236
  72. data/lib/metanorma/html/concerns/metadata_extraction.rb +61 -0
  73. data/lib/metanorma/html/concerns/presentation_validation.rb +61 -0
  74. data/lib/metanorma/html/concerns/svg_processing.rb +32 -0
  75. data/lib/metanorma/html/concerns/text_extraction.rb +93 -0
  76. data/lib/metanorma/html/concerns/toc_registry.rb +47 -0
  77. data/lib/metanorma/html/csa_renderer.rb +9 -0
  78. data/lib/metanorma/html/drops/figure_drop.rb +33 -10
  79. data/lib/metanorma/html/drops/sourcecode_drop.rb +3 -6
  80. data/lib/metanorma/html/flavor.rb +6 -1
  81. data/lib/metanorma/html/generator.rb +8 -8
  82. data/lib/metanorma/html/iso_renderer.rb +0 -142
  83. data/lib/metanorma/html/renderers/block_renderer.rb +41 -5
  84. data/lib/metanorma/html/renderers/element_order_traversal.rb +15 -1
  85. data/lib/metanorma/html/renderers/inline_renderer.rb +16 -76
  86. data/lib/metanorma/html/standard_renderer.rb +195 -35
  87. data/lib/metanorma/html/templates/_list_item.html.liquid +1 -1
  88. data/lib/metanorma/html/theme.rb +38 -11
  89. data/lib/metanorma/html.rb +1 -52
  90. data/lib/metanorma/iso_document/sections/iso_preface.rb +27 -13
  91. data/lib/metanorma/mirror/default_registry.rb +21 -0
  92. data/lib/metanorma/mirror/handlers/inline/catalog.rb +7 -12
  93. data/lib/metanorma/mirror/handlers/inline/rich_html_renderer.rb +2 -3
  94. data/lib/metanorma/mirror/handlers/sourcecode.rb +1 -1
  95. data/lib/metanorma/mirror/model/guide.rb +3 -2
  96. data/lib/metanorma/mirror/output/formats/inline_format.rb +64 -3
  97. data/lib/metanorma/mirror/output/pipeline.rb +1 -0
  98. data/lib/metanorma/mirror/output.rb +0 -2
  99. data/lib/metanorma/mirror/rewriter.rb +3 -1
  100. data/lib/metanorma/registers/setup.rb +6 -2
  101. data/lib/metanorma/standard_document/annotation_container.rb +2 -0
  102. data/lib/metanorma/standard_document/elements/input.rb +4 -25
  103. data/lib/metanorma/standard_document/metadata/metanorma_extension.rb +12 -2
  104. data/lib/metanorma/standard_document/metadata/presentation_metadata.rb +107 -0
  105. data/lib/metanorma/standard_document/metadata/semantic_metadata.rb +18 -0
  106. data/lib/metanorma/standard_document/metadata/unitsml/amount_of_substance.rb +19 -0
  107. data/lib/metanorma/standard_document/metadata/unitsml/base_quantity.rb +34 -0
  108. data/lib/metanorma/standard_document/metadata/unitsml/dimension.rb +39 -0
  109. data/lib/metanorma/standard_document/metadata/unitsml/dimension_set.rb +19 -0
  110. data/lib/metanorma/standard_document/metadata/unitsml/electric_current.rb +19 -0
  111. data/lib/metanorma/standard_document/metadata/unitsml/enumerated_root_unit.rb +23 -0
  112. data/lib/metanorma/standard_document/metadata/unitsml/length.rb +19 -0
  113. data/lib/metanorma/standard_document/metadata/unitsml/luminous_intensity.rb +19 -0
  114. data/lib/metanorma/standard_document/metadata/unitsml/mass.rb +19 -0
  115. data/lib/metanorma/standard_document/metadata/unitsml/namespace.rb +17 -0
  116. data/lib/metanorma/standard_document/metadata/unitsml/plane_angle.rb +19 -0
  117. data/lib/metanorma/standard_document/metadata/unitsml/prefix.rb +30 -0
  118. data/lib/metanorma/standard_document/metadata/unitsml/prefix_name.rb +21 -0
  119. data/lib/metanorma/standard_document/metadata/unitsml/prefix_set.rb +19 -0
  120. data/lib/metanorma/standard_document/metadata/unitsml/prefix_symbol.rb +21 -0
  121. data/lib/metanorma/standard_document/metadata/unitsml/quantity.rb +28 -0
  122. data/lib/metanorma/standard_document/metadata/unitsml/quantity_name.rb +21 -0
  123. data/lib/metanorma/standard_document/metadata/unitsml/quantity_set.rb +19 -0
  124. data/lib/metanorma/standard_document/metadata/unitsml/root_units.rb +19 -0
  125. data/lib/metanorma/standard_document/metadata/unitsml/thermodynamic_temperature.rb +19 -0
  126. data/lib/metanorma/standard_document/metadata/unitsml/time.rb +19 -0
  127. data/lib/metanorma/standard_document/metadata/unitsml/unit.rb +32 -0
  128. data/lib/metanorma/standard_document/metadata/unitsml/unit_name.rb +21 -0
  129. data/lib/metanorma/standard_document/metadata/unitsml/unit_set.rb +19 -0
  130. data/lib/metanorma/standard_document/metadata/unitsml/unit_symbol.rb +36 -0
  131. data/lib/metanorma/standard_document/metadata/unitsml/unit_system.rb +23 -0
  132. data/lib/metanorma/standard_document/metadata/unitsml/unitsml_root.rb +28 -0
  133. data/lib/metanorma/standard_document/metadata/unitsml.rb +39 -0
  134. data/lib/metanorma/standard_document/metadata.rb +4 -0
  135. data/lib/metanorma/standard_document/sections/misc_container.rb +8 -6
  136. data/lib/metanorma/standard_document/sections/sections.rb +7 -0
  137. data/lib/metanorma/un_document/blocks/un_admonition_block.rb +86 -0
  138. data/lib/metanorma/un_document/blocks.rb +9 -0
  139. data/lib/metanorma/un_document/root.rb +8 -0
  140. data/lib/metanorma/un_document/sections/un_abstract_section.rb +48 -0
  141. data/lib/metanorma/un_document/sections/un_preface.rb +19 -2
  142. data/lib/metanorma/un_document/sections.rb +1 -0
  143. data/lib/metanorma/un_document/un_text_element.rb +116 -0
  144. data/lib/metanorma/un_document.rb +2 -0
  145. data/script/benchmark_render.rb +35 -0
  146. data/tasks/roundtrip_samples.rake +2 -2
  147. metadata +65 -54
  148. data/.rubocop_todo.yml +0 -220
  149. data/TODO.finalize/01-split-multi-class-files.md +0 -8
  150. data/TODO.finalize/02-remove-data-dist-iso-document-frontend.md +0 -6
  151. data/TODO.finalize/03-align-frontend-iife-and-gha-build.md +0 -11
  152. data/TODO.finalize/04-theme-per-flavor-config-driven.md +0 -9
  153. data/TODO.finalize/05-migrate-all-html-to-liquid.md +0 -16
  154. data/TODO.finalize/06-integrate-pubid-library.md +0 -16
  155. data/TODO.finalize/07-replace-relaton-models-with-relaton-bib.md +0 -39
  156. data/TODO.finalize/08-refactor-renderer-architecture.md +0 -25
  157. data/TODO.finalize/09-eliminate-code-smells.md +0 -14
  158. data/TODO.finalize/10-comprehensive-spec-coverage.md +0 -24
  159. data/TODO.finalize/11-config-driven-flavor-metadata.md +0 -20
  160. data/TODO.finalize/12-add-frozen-string-literal.md +0 -6
  161. data/TODO.finalize/13-extract-composition-from-god-class.md +0 -96
  162. data/TODO.finalize/14-config-driven-section-ordering.md +0 -31
  163. data/TODO.finalize/15-theme-directories-per-flavor.md +0 -46
  164. data/TODO.finalize/16-theme-lutaml-model-schema.md +0 -44
  165. data/data/logos/oiml-logo-full-dark.svg +0 -1
  166. data/data/logos/oiml-logo-full-light.svg +0 -1
  167. data/data/logos/oiml-logo-icon-dark.svg +0 -1
  168. data/data/logos/oiml-logo-icon-light.svg +0 -1
  169. data/data/logos/oiml-logo.svg +0 -1
  170. data/data/themes/oiml.yaml +0 -56
  171. data/docs/html-renderer.adoc +0 -261
  172. data/lib/data/dist/iso_document/frontend/assets/index-mwzbzmnK.js +0 -22
  173. data/lib/data/dist/iso_document/frontend/assets/index-pGvKTNid.css +0 -1
  174. data/lib/data/dist/iso_document/frontend/assets/iso-red.svg +0 -53
  175. data/lib/data/dist/iso_document/frontend/index.html +0 -13
  176. data/lib/metanorma/document/relaton/bib_item_size_type.rb +0 -21
  177. data/lib/metanorma/document/relaton/bibliographic_date_type.rb +0 -22
  178. data/lib/metanorma/document/relaton/document_relation_type.rb +0 -31
  179. data/lib/metanorma/document/relaton/iso4217_code.rb +0 -17
  180. data/lib/metanorma/document/relaton/iso8601_date.rb +0 -17
  181. data/lib/metanorma/document/relaton/personal_identifier_type.rb +0 -21
  182. data/lib/metanorma/document/relaton/region_type.rb +0 -20
  183. data/lib/metanorma/document/relaton/series_type_type.rb +0 -21
  184. data/lib/metanorma/document/relaton/specific_locality_type.rb +0 -25
  185. data/lib/metanorma/document/relaton/title_type.rb +0 -21
  186. data/lib/metanorma/html/oiml_renderer.rb +0 -33
  187. data/lib/metanorma/mirror/output/html_renderer.rb +0 -95
  188. data/lib/metanorma/mirror/output/html_renderers/block_renderers.rb +0 -178
  189. data/lib/metanorma/mirror/output/html_renderers/inline_renderer.rb +0 -61
  190. data/lib/metanorma/mirror/output/html_renderers/list_renderers.rb +0 -96
  191. data/lib/metanorma/mirror/output/html_renderers/mark_renderers.rb +0 -41
  192. data/lib/metanorma/mirror/output/html_renderers/section_renderers.rb +0 -129
  193. data/lib/metanorma/mirror/output/html_renderers/structural_renderers.rb +0 -80
  194. data/lib/metanorma/mirror/output/html_renderers/table_renderers.rb +0 -87
  195. data/lib/metanorma/mirror/output/html_renderers.rb +0 -76
  196. data/lib/metanorma/oiml_document.rb +0 -9
  197. data/lib/metanorma/standard_document/annotation.rb +0 -37
@@ -104,13 +104,6 @@ module Metanorma
104
104
  end
105
105
 
106
106
  def render_mixed_inline(node)
107
- if raw_content_node?(node)
108
- raw = node.content
109
- if raw.is_a?(String) && !raw.strip.empty?
110
- return render_raw_content(raw)
111
- end
112
- end
113
-
114
107
  if node.is_a?(Lutaml::Model::Serializable) && node.element_order && !node.element_order.empty?
115
108
  render_ordered_inline(node)
116
109
  elsif node.is_a?(Lutaml::Model::Serializable)
@@ -129,13 +122,6 @@ module Metanorma
129
122
  end
130
123
  end
131
124
 
132
- def raw_content_node?(node)
133
- node.is_a?(Lutaml::Model::Serializable) &&
134
- node.content.is_a?(String)
135
- rescue NoMethodError
136
- false
137
- end
138
-
139
125
  def render_ordered_inline(node)
140
126
  parts = []
141
127
  walked = walk_ordered(node) do |type, obj|
@@ -243,8 +229,19 @@ module Metanorma
243
229
  ", "
244
230
  end
245
231
 
232
+ # Form inputs render as disabled HTML inputs — this is a document
233
+ # rendering, not an interactive form.
234
+ def render_input(el)
235
+ attrs = element_attrs(
236
+ type: safe_attr(el, :type) || "text",
237
+ disabled: "disabled",
238
+ checked: safe_attr(el, :checked) ? "checked" : nil,
239
+ )
240
+ "<input#{attrs}>"
241
+ end
242
+
246
243
  def render_math(el)
247
- el.content.to_s
244
+ el.to_xml
248
245
  end
249
246
 
250
247
  def render_asciimath(el)
@@ -374,65 +371,6 @@ module Metanorma
374
371
  })
375
372
  end
376
373
 
377
- def render_raw_content(raw_xml)
378
- doc = Nokogiri::XML.fragment(raw_xml)
379
- doc.css("fmt-link").each do |el|
380
- target = el["target"] || el["href"]
381
- if target
382
- display_text = target.delete_prefix("mailto:")
383
- a = doc.document.create_element("a", display_text,
384
- "href" => target)
385
- el.replace(a)
386
- else
387
- el.replace(el.children)
388
- end
389
- end
390
- doc.traverse do |node|
391
- next unless node.element?
392
- next unless %w[xref eref stem link].include?(node.name)
393
-
394
- next_sib = node.next_sibling
395
- while next_sib.is_a?(Nokogiri::XML::Text) && next_sib.text.strip.empty?
396
- next_sib = next_sib.next_sibling
397
- end
398
- next unless next_sib&.element? && next_sib.name == "semx"
399
-
400
- deduplicate_semx_label(node, next_sib)
401
- node.remove
402
- end
403
- %w[semx fmt-xref].each do |tag|
404
- doc.css(tag).each { |el| el.replace(el.children) }
405
- end
406
- doc.css("[class]").each do |el|
407
- el["class"] = el["class"].split(/\s+/).map do |c|
408
- html_class_for_span(c)
409
- end.join(" ")
410
- end
411
- doc.inner_html
412
- end
413
-
414
- def deduplicate_semx_label(source_node, semx_node)
415
- first_text = semx_node.children.find do |c|
416
- c.text? && !c.text.strip.empty?
417
- end
418
- return unless first_text
419
-
420
- semx_prefix = first_text.text[/\A(\s*\S+)/, 1]
421
- return unless semx_prefix && !semx_prefix.strip.empty?
422
-
423
- prev = source_node.previous_sibling
424
- return unless prev.is_a?(Nokogiri::XML::Text)
425
-
426
- label = semx_prefix.strip
427
- prev_text = prev.text.rstrip
428
- return unless prev_text.end_with?(label)
429
-
430
- prev.content = prev_text.sub(/#{Regexp.escape(label)}\s*\z/, "")
431
- first_text.content = first_text.text.sub(
432
- /\A\s*#{Regexp.escape(label)}\s*/, ""
433
- )
434
- end
435
-
436
374
  def render_link(link)
437
375
  target = safe_attr(link, :target) || safe_attr(link, :href)
438
376
  attrs = element_attrs(href: target, id: safe_attr(link, :id))
@@ -517,7 +455,7 @@ module Metanorma
517
455
  end
518
456
 
519
457
  if math_items.any?
520
- content = math_items.map { |m| m.content.to_s }.join
458
+ content = math_items.map(&:to_xml).join
521
459
  unless content.empty?
522
460
  return render_liquid("_math_container.html.liquid", {
523
461
  "data_attrs" => data_attrs,
@@ -592,9 +530,11 @@ module Metanorma
592
530
  end
593
531
  end
594
532
 
595
- def render_mixed_content_in_order(node)
533
+ def render_mixed_content_in_order(node, skip_classes: nil)
596
534
  parts = []
597
535
  node.each_mixed_content do |child|
536
+ next if skip_classes&.any? { |klass| child.is_a?(klass) }
537
+
598
538
  parts << case child
599
539
  when String
600
540
  escape_html(child)
@@ -160,57 +160,217 @@ module Metanorma
160
160
 
161
161
  # --- Term rendering ---
162
162
 
163
+ # Unified term entry renderer. Handles both semantic-only term models
164
+ # (StandardDocument::Terms::Term) and presentation-aware models with
165
+ # fmt-* attributes (IsoDocument::Terms::IsoTerm): safe_attr returns nil
166
+ # for attributes a model does not declare, so the fmt-* branches are
167
+ # skipped automatically on semantic-only models.
163
168
  def render_term(term, **_opts)
164
- attrs = element_attrs(id: safe_attr(term, :id))
169
+ attrs = element_attrs(id: safe_attr(term, :id), **term_data_attrs(term))
170
+ fmt_definition = safe_attr(term, :fmt_definition)
171
+
165
172
  parts = []
166
- term.preferred&.each do |d|
167
- parts << (render_term_designation(d, "preferred") || "")
173
+ parts << (render_term_number(term) || "")
174
+ parts << render_term_designations(term, :fmt_preferred, :preferred,
175
+ "preferred")
176
+ parts << render_term_designations(term, :fmt_admitted, :admitted,
177
+ "admitted")
178
+ parts << render_term_designations(term, :fmt_deprecates, :deprecates,
179
+ "deprecated")
180
+ parts << (render_term_domain(term, fmt_definition) || "")
181
+ parts << render_term_definitions(term, fmt_definition)
182
+ parts << render_term_note_parts(term)
183
+ parts << render_term_example_parts(term)
184
+ parts << render_term_source_parts(term)
185
+ safe_attr(term, :admonition)&.each do |admonition|
186
+ parts << (render_admonition(admonition) || "")
168
187
  end
169
- term.admitted&.each do |d|
170
- parts << (render_term_designation(d, "admitted") || "")
188
+ safe_attr(term, :term)&.each { |sub| parts << (render_term(sub) || "") }
189
+ render_liquid("_element.html.liquid", {
190
+ "tag" => "div",
191
+ "extra_attrs" => attrs,
192
+ "content" => parts.join,
193
+ })
194
+ end
195
+
196
+ def term_data_attrs(term)
197
+ term_name = extract_term_name(term)
198
+ term_definition = extract_term_definition(term)
199
+ data_attrs = {}
200
+ if term_name && !term_name.empty?
201
+ data_attrs["data-term-name"] = term_name
171
202
  end
172
- term.deprecates&.each do |d|
173
- parts << (render_term_designation(d, "deprecated") || "")
203
+ if term_definition && !term_definition.empty?
204
+ data_attrs["data-term-definition"] = term_definition
174
205
  end
175
-
176
- if term.domain
177
- domain_text = if term.domain.is_a?(String)
178
- term.domain
206
+ data_attrs
207
+ end
208
+
209
+ def render_term_number(term)
210
+ fmt_name = safe_attr(term, :fmt_name)
211
+ term_number = safe_attr(term, :term_number)
212
+ if fmt_name
213
+ render_liquid("_term_number.html.liquid", {
214
+ "content" => render_inline_element(fmt_name),
215
+ })
216
+ elsif term_number
217
+ number_text = if term_number.is_a?(String)
218
+ term_number
179
219
  else
180
- safe_attr(term.domain, :text).to_s
220
+ extract_text_value(term_number)
181
221
  end
182
- unless domain_text.to_s.empty?
183
- parts << render_liquid("_term_domain.html.liquid", {
184
- "text" => escape_html(domain_text),
185
- }).to_s
222
+ render_liquid("_term_number.html.liquid", {
223
+ "content" => escape_html(number_text),
224
+ })
225
+ end
226
+ end
227
+
228
+ # Designations (preferred/admitted/deprecated): fmt-* presentation
229
+ # elements win when present; otherwise render semantic designations.
230
+ def render_term_designations(term, fmt_attr, semantic_attr, type)
231
+ fmt = safe_attr(term, fmt_attr)
232
+ fmt_list = fmt.is_a?(Array) ? fmt : [fmt].compact
233
+ parts = []
234
+ if fmt_list.empty?
235
+ term.public_send(semantic_attr)&.each do |designation|
236
+ parts << (render_term_designation(designation, type) || "")
237
+ end
238
+ else
239
+ fmt_list.each do |element|
240
+ element.p&.each { |para| parts << (render_paragraph(para) || "") }
186
241
  end
187
242
  end
243
+ parts.join
244
+ end
245
+
246
+ def render_term_domain(term, fmt_definition)
247
+ return nil if fmt_definition
248
+
249
+ domain = term.domain
250
+ return nil unless domain
251
+
252
+ domain_text = domain.is_a?(String) ? domain : safe_attr(domain, :text).to_s
253
+ return nil if domain_text.empty?
254
+
255
+ render_liquid("_term_domain.html.liquid", {
256
+ "text" => escape_html(domain_text),
257
+ }).to_s
258
+ end
259
+
260
+ def render_term_definitions(term, fmt_definition)
261
+ return render_ordered_content(fmt_definition) || "" if fmt_definition
262
+
263
+ parts = []
264
+ safe_attr(term, :p)&.each do |para|
265
+ parts << (render_paragraph(para) || "")
266
+ end
267
+ term.definition&.each do |definition|
268
+ parts << (render_term_definition(definition) || "")
269
+ end
270
+ parts.join
271
+ end
272
+
273
+ def render_term_note_parts(term)
274
+ notes = safe_attr(term, :termnote) || Array(safe_attr(term, :note))
275
+ parts = []
276
+ notes.each_with_index do |note, i|
277
+ parts << (render_term_note_item(note, i) || "")
278
+ end
279
+ parts.join
280
+ end
281
+
282
+ # A term note can be a raw String (rendered with a "Note N to entry"
283
+ # label), a termnote element (labelled wrapper), or a plain block
284
+ # (rendered as a generic note).
285
+ def render_term_note_item(note, index)
286
+ if note.is_a?(String)
287
+ render_liquid("_term_text_note.html.liquid", {
288
+ "label" => "Note #{index + 1} to entry: ",
289
+ "content" => escape_html(note),
290
+ }).to_s
291
+ elsif term_scoped_block?(note)
292
+ render_term_note(note)
293
+ else
294
+ render_note(note)
295
+ end
296
+ end
297
+
298
+ def render_term_example_parts(term)
299
+ examples = safe_attr(term, :termexample) ||
300
+ Array(safe_attr(term, :example))
301
+ examples.map do |example|
302
+ render_term_example_item(example) || ""
303
+ end.join
304
+ end
305
+
306
+ # A term example can be a termexample element (labelled wrapper), a
307
+ # raw String, or a plain paragraph block (both rendered as paragraphs).
308
+ # render_paragraph cannot take a bare String (render_mixed_inline
309
+ # expects a model), so Strings go through the paragraph template
310
+ # directly — same markup render_paragraph would emit.
311
+ def render_term_example_item(example)
312
+ if example.is_a?(String)
313
+ return render_liquid("_paragraph.html.liquid", {
314
+ "attrs" => "",
315
+ "content" => escape_html(example),
316
+ })
317
+ end
318
+ return render_term_example(example) if term_scoped_block?(example)
319
+
320
+ render_paragraph(example)
321
+ end
188
322
 
189
- if term.definition
190
- Array(term.definition).each do |defn|
191
- parts << (render_term_definition(defn) || "")
323
+ # Termnote/termexample elements wrap block children (`p`, lists);
324
+ # plain paragraph blocks and Strings do not declare a `p` attribute.
325
+ def term_scoped_block?(node)
326
+ node.is_a?(Lutaml::Model::Serializable) &&
327
+ node.class.attributes.key?(:p)
328
+ end
329
+
330
+ def render_term_source_parts(term)
331
+ fmt_termsource = safe_attr(term, :fmt_termsource)
332
+ parts = []
333
+ if fmt_termsource && !fmt_termsource.empty?
334
+ fmt_termsource.each do |source|
335
+ parts << render_liquid("_paragraph.html.liquid", {
336
+ "attrs" => " class=\"term-source\"",
337
+ "content" => render_mixed_inline(source),
338
+ })
339
+ end
340
+ else
341
+ term.source&.each { |src| parts << (render_term_source(src) || "") }
342
+ safe_attr(term, :termsource)&.each do |source|
343
+ parts << (render_term_source_element(source) || "")
192
344
  end
193
345
  end
346
+ parts.join
347
+ end
194
348
 
195
- term.note&.each_with_index do |note, i|
196
- if note.is_a?(String)
197
- label = "Note #{i + 1} to entry: "
198
- parts << render_liquid("_term_text_note.html.liquid", {
199
- "label" => label,
200
- "content" => escape_html(note),
201
- }).to_s
202
- else
203
- parts << (render_note(note) || "")
349
+ def extract_term_name(term)
350
+ fmt_pref = safe_attr(term, :fmt_preferred)
351
+ if fmt_pref && !fmt_pref.empty?
352
+ fp = fmt_pref.first
353
+ if fp.p && !fp.p.empty?
354
+ return extract_plain_text(fp.p.first)
204
355
  end
205
356
  end
357
+ if term.preferred && !term.preferred.empty?
358
+ return extract_designation_name(term.preferred.first).to_s
359
+ end
206
360
 
207
- term.example&.each { |ex| parts << (render_paragraph(ex) || "") }
208
- term.source&.each { |src| parts << (render_term_source(src) || "") }
209
- render_liquid("_element.html.liquid", {
210
- "tag" => "div",
211
- "extra_attrs" => attrs,
212
- "content" => parts.join,
213
- })
361
+ safe_attr(term, :id).to_s.delete_prefix("term-")
362
+ end
363
+
364
+ def extract_term_definition(term)
365
+ fmt_def = safe_attr(term, :fmt_definition)
366
+ return extract_plain_text(fmt_def) if fmt_def
367
+
368
+ term_p = safe_attr(term, :p)
369
+ if term_p && !term_p.empty?
370
+ text = term_p.map { |para| extract_plain_text(para) }.join(" ")
371
+ return text.strip unless text.strip.empty?
372
+ end
373
+ nil
214
374
  end
215
375
 
216
376
  def render_term_designation(designation, _type)
@@ -1 +1 @@
1
- <li{{ attrs }}>{{ content }}</li>
1
+ <li{{ attrs }}>{% if label %}<span class="li-label">{{ label }}</span>{% endif %}{{ content }}</li>
@@ -120,26 +120,53 @@ module Metanorma
120
120
  # --- Non-YAML state (set programmatically) ---
121
121
  attr_accessor :theme_dir
122
122
 
123
+ # Additional theme directories registered by flavor gems (e.g.
124
+ # metanorma-oiml). Searched before the built-in THEMES_DIR so a
125
+ # flavor can ship its own `themes/<flavor>/theme.yaml` (directory
126
+ # form, with assets/templates/custom.css alongside) outside this gem.
127
+ @extra_themes_dirs = []
128
+
129
+ class << self
130
+ attr_reader :extra_themes_dirs
131
+
132
+ def register_themes_dir(dir)
133
+ @extra_themes_dirs.unshift(File.expand_path(dir))
134
+ end
135
+
136
+ def themes_dirs
137
+ @extra_themes_dirs + [THEMES_DIR]
138
+ end
139
+ end
140
+
123
141
  def self.load(flavor)
124
- dir_theme = File.join(THEMES_DIR, flavor.to_s, "theme.yaml")
125
- flat_theme = File.join(THEMES_DIR, "#{flavor}.yaml")
126
-
127
- if File.exist?(dir_theme)
128
- from_directory(dir_theme, flavor)
129
- elsif File.exist?(flat_theme)
130
- from_file(flat_theme)
131
- else
132
- new
142
+ themes_dirs.each do |themes_dir|
143
+ dir_theme = File.join(themes_dir, flavor.to_s, "theme.yaml")
144
+ flat_theme = File.join(themes_dir, "#{flavor}.yaml")
145
+
146
+ return from_directory(dir_theme, flavor, themes_dir) if File.exist?(dir_theme)
147
+ return from_file(flat_theme) if File.exist?(flat_theme)
133
148
  end
149
+
150
+ new
134
151
  end
135
152
 
136
153
  def self.from_file(path)
137
154
  from_yaml(File.read(path))
138
155
  end
139
156
 
140
- def self.from_directory(path, flavor)
157
+ def self.from_directory(path, flavor, themes_dir = THEMES_DIR)
141
158
  theme = from_yaml(File.read(path))
142
- theme.theme_dir = File.join(THEMES_DIR, flavor.to_s)
159
+ theme.theme_dir = File.join(themes_dir, flavor.to_s)
160
+ theme
161
+ end
162
+
163
+ # Load a theme from an external theme directory: `dir/theme.yaml`
164
+ # with optional `assets/`, `templates/` and `custom.css` resolved
165
+ # relative to `dir`. This is the entry point for organization-local
166
+ # themes living outside any gem.
167
+ def self.from_theme_dir(dir)
168
+ theme = from_yaml(File.read(File.join(dir, "theme.yaml")))
169
+ theme.theme_dir = dir
143
170
  theme
144
171
  end
145
172
 
@@ -11,57 +11,6 @@ module Metanorma
11
11
  Liquid::Environment.default.file_system = Liquid::LocalFileSystem.new(
12
12
  TEMPLATES_ROOT, "_%s.html.liquid"
13
13
  )
14
-
15
- Moxml::Adapter::Nokogiri.class_eval do
16
- class << self
17
- alias_method :_original_children_patched, :children
18
- remove_method :children
19
- end
20
-
21
- def self.children(node)
22
- node.children.to_a
23
- end
24
- end
25
- end
26
-
27
- # Patch missing Moxml module method used by lutaml-model XML serializer
28
- ::Moxml.class_eval do
29
- def self.preprocess_entities(text)
30
- if block_given?
31
- yield text
32
- else
33
- text
34
- end
35
- end
36
- end
37
-
38
- module Html
39
- Lutaml::Xml::XmlElement.class_eval do
40
- remove_method :order
41
-
42
- def order
43
- return @order_cache if @order_cache
44
-
45
- @order_cache = children.filter_map do |child|
46
- if child.text?
47
- Lutaml::Xml::Element.new("Text", "text",
48
- text_content: child.text,
49
- node_type: :text)
50
- elsif child.cdata?
51
- Lutaml::Xml::Element.new("Text", "#cdata-section",
52
- text_content: child.text,
53
- node_type: :cdata)
54
- elsif child.comment?
55
- nil
56
- else
57
- Lutaml::Xml::Element.new("Element", child.unprefixed_name,
58
- node_type: :element,
59
- namespace_uri: child.namespace_uri,
60
- namespace_prefix: child.namespace_prefix)
61
- end
62
- end
63
- end
64
- end
65
14
  end
66
15
 
67
16
  autoload :BaseRenderer, "metanorma/html/base_renderer"
@@ -77,6 +26,7 @@ module Metanorma
77
26
  autoload :IsoRenderer, "metanorma/html/iso_renderer"
78
27
  autoload :BipmRenderer, "metanorma/html/bipm_renderer"
79
28
  autoload :CcRenderer, "metanorma/html/cc_renderer"
29
+ autoload :CsaRenderer, "metanorma/html/csa_renderer"
80
30
  autoload :IccRenderer, "metanorma/html/icc_renderer"
81
31
  autoload :PdfaRenderer, "metanorma/html/pdfa_renderer"
82
32
  autoload :IecRenderer, "metanorma/html/iec_renderer"
@@ -85,7 +35,6 @@ module Metanorma
85
35
  autoload :IhoRenderer, "metanorma/html/iho_renderer"
86
36
  autoload :ItuRenderer, "metanorma/html/itu_renderer"
87
37
  autoload :OgcRenderer, "metanorma/html/ogc_renderer"
88
- autoload :OimlRenderer, "metanorma/html/oiml_renderer"
89
38
  autoload :RiboseRenderer, "metanorma/html/ribose_renderer"
90
39
  end
91
40
  end
@@ -4,30 +4,44 @@ module Metanorma
4
4
  module IsoDocument
5
5
  module Sections
6
6
  # Prefatory clauses in an ISO/IEC document.
7
- # Extends StandardDocument::Preface with ISO-specific section types:
8
- # abstract IsoAbstractSection
9
- # foreword → IsoForewordSection
10
- # introduction IsoClauseSection
11
- # ISO preface mandates foreword; isodoc preface is permissive.
12
- class IsoPreface < Metanorma::StandardDocument::Sections::Preface
13
- # Override: ISO uses specific section types
7
+ # Corresponds to isostandard.rng:
8
+ # preface = element preface { abstract?, foreword, introduction? }
9
+ #
10
+ # This class deliberately does NOT inherit StandardDocument::Preface:
11
+ # lutaml-model deep-duplicates a parent's XML mappings into subclasses,
12
+ # so inheriting would keep the isodoc-level acknowledgements and
13
+ # executivesummary mappings that the ISO grammar forbids. Attributes
14
+ # are composed directly instead.
15
+ #
16
+ # The `clause` mapping is a documented extension required by real ISO
17
+ # presentation output, which emits front-matter clauses such as
18
+ # `<clause type="toc">` ahead of the foreword. `content` mirrors
19
+ # `clause` for the generic preface-content consumers (e.g. the mirror
20
+ # renderer), matching the runtime behavior of the previous inherited
21
+ # dual mapping.
22
+ class IsoPreface < Lutaml::Model::Serializable
14
23
  attribute :abstract, IsoAbstractSection
15
24
  attribute :foreword, IsoForewordSection
16
25
  attribute :introduction, IsoClauseSection
17
26
 
18
- # Generic clauses in preface use IsoClauseSection
27
+ # Presentation front-matter clauses (e.g. table of contents placeholder)
19
28
  attribute :clause, IsoClauseSection, collection: true
29
+ attribute :content, Metanorma::StandardDocument::Sections::ContentSection,
30
+ collection: true
20
31
 
21
- # Acknowledgements and executivesummary use IsoClauseSection
22
- attribute :acknowledgements, IsoClauseSection
23
- attribute :executivesummary, IsoClauseSection
32
+ # Presentation-specific attributes
33
+ attribute :semx_id, :string
34
+ attribute :displayorder, :integer
24
35
 
25
36
  xml do
26
37
  element "preface"
27
38
  ordered
28
39
 
29
- Metanorma::StandardDocument::SectionXmlMapping.apply_preface_elements(self)
30
- map_element "clause", to: :clause
40
+ map_element "abstract", to: :abstract
41
+ map_element "foreword", to: :foreword
42
+ map_element "introduction", to: :introduction
43
+ map_element "clause", to: :clause
44
+ map_element "clause", to: :content
31
45
 
32
46
  Metanorma::StandardDocument::SectionXmlMapping.apply_preface_attributes(self)
33
47
  end
@@ -153,6 +153,27 @@ module Metanorma
153
153
  Handlers::Structural,
154
154
  method_name: :preface,
155
155
  )
156
+ # IsoPreface and UnPreface deliberately do not inherit
157
+ # StandardDocument::Preface (grammar-strict classes compose from
158
+ # mixins), so they need explicit registrations — the registry
159
+ # resolves handlers through class ancestry.
160
+ registry.register(
161
+ Metanorma::IsoDocument::Sections::IsoPreface,
162
+ Handlers::Structural,
163
+ method_name: :preface,
164
+ )
165
+ registry.register(
166
+ Metanorma::UnDocument::Sections::UnPreface,
167
+ Handlers::Structural,
168
+ method_name: :preface,
169
+ )
170
+ # UnAbstractSection likewise composes instead of inheriting
171
+ # ContentSection.
172
+ registry.register(
173
+ Metanorma::UnDocument::Sections::UnAbstractSection,
174
+ Handlers::Section,
175
+ method_name: :content_section,
176
+ )
156
177
  registry.register(
157
178
  Metanorma::StandardDocument::Sections::Sections,
158
179
  Handlers::Structural,
@@ -5,19 +5,14 @@ module Metanorma
5
5
  module Handlers
6
6
  module Inline
7
7
  # Shared declarative table of inline marks whose HTML rendering is
8
- # a fixed tag with optional static attrs. Two adapters consume it:
8
+ # a fixed tag with optional static attrs. It is consumed by
9
+ # RichHtmlRenderer, which renders Metanorma XML elements to HTML
10
+ # directly (the fallback / attribute-value path) during the
11
+ # forward transform into the mirror IR.
9
12
  #
10
- # - RichHtmlRenderer: renders Metanorma XML elements to HTML
11
- # directly (the fallback / attribute-value path).
12
- # - Output::HtmlRenderers::MarkRenderers: renders mirror Model marks
13
- # to HTML (the primary SSR path).
14
- #
15
- # Both adapters must produce visually equivalent HTML for the same
16
- # mark type — the table is the single source of truth for the
17
- # mark-name to tag-and-attrs mapping. Marks whose attrs vary per
18
- # instance (link, xref, eref, span) carry data in mark.attrs and
19
- # are not in this table; each adapter keeps custom handlers for
20
- # those.
13
+ # Marks whose attrs vary per instance (link, xref, eref, span)
14
+ # carry data in mark.attrs and are not in this table; each
15
+ # consumer keeps custom handlers for those.
21
16
  module Catalog
22
17
  SIMPLE_WRAPS = {
23
18
  "emphasis" => { tag: :em },
@@ -12,9 +12,8 @@ module Metanorma
12
12
  # Nokogiri::HTML5::Builder to guarantee well-formed, escaped output.
13
13
  #
14
14
  # Simple-wrap elements (em, strong, sub, sup, code, u, s, smallcap,
15
- # bcp14, concept) share their tag-and-attrs mapping with the Model-side
16
- # renderer via Inline::Catalog, so the XML→HTML and Model→HTML paths
17
- # produce visually equivalent output for the same mark type.
15
+ # bcp14, concept) take their tag-and-attrs mapping from the shared
16
+ # Inline::Catalog table.
18
17
  module RichHtmlRenderer
19
18
  # Complex element handlers — these have per-instance attrs or
20
19
  # multi-step rendering and bypass the shared Catalog.
@@ -11,7 +11,7 @@ module Metanorma
11
11
 
12
12
  body = SafeAttr.read(element, :body)
13
13
  text = if body
14
- Array(body.content).join
14
+ body.decoded_content
15
15
  elsif SafeAttr.read(element, :content).is_a?(String)
16
16
  SafeAttr.read(element, :content)
17
17
  else