metanorma-document 0.2.12 → 0.2.13

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 (391) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rake.yml +21 -0
  3. data/.github/workflows/release.yml +25 -0
  4. data/.github/workflows/roundtrip-samples.yml +132 -0
  5. data/.gitignore +19 -0
  6. data/.rspec +3 -0
  7. data/.rubocop.yml +22 -0
  8. data/.rubocop_todo.yml +66 -164
  9. data/CHANGELOG.md +1 -0
  10. data/Gemfile +32 -0
  11. data/README.adoc +0 -144
  12. data/Rakefile +2 -14
  13. data/bin/console +15 -0
  14. data/bin/setup +8 -0
  15. data/data/javascripts/components/mn-lightbox.js +2 -18
  16. data/data/javascripts/components/mn-shortcuts.js +0 -2
  17. data/data/javascripts/core/mn-navigation.js +0 -8
  18. data/data/javascripts/core/mn-theme.js +9 -9
  19. data/data/logos/pdfa-logo.svg +94 -31
  20. data/data/stylesheets/base/_layout.css +0 -3
  21. data/data/stylesheets/base/_reset.css +3 -3
  22. data/data/stylesheets/base/_typography.css +5 -0
  23. data/data/stylesheets/components/bibliography.css +3 -3
  24. data/data/stylesheets/components/cover.css +1 -3
  25. data/data/stylesheets/components/figure.css +1 -13
  26. data/data/stylesheets/components/footnote.css +5 -65
  27. data/data/stylesheets/components/formula.css +0 -44
  28. data/data/stylesheets/components/header.css +2 -17
  29. data/data/stylesheets/components/inline.css +1 -1
  30. data/data/stylesheets/components/table.css +2 -15
  31. data/data/stylesheets/components/term.css +1 -0
  32. data/lib/metanorma/bipm_document/metadata/bipm_bibliographic_item.rb +14 -0
  33. data/lib/metanorma/bipm_document/metadata.rb +1 -1
  34. data/lib/metanorma/document/components/ancillary_blocks/sourcecode_block.rb +65 -0
  35. data/lib/metanorma/document/components/ancillary_blocks.rb +0 -4
  36. data/lib/metanorma/document/components/bib_data/bib_data.rb +0 -7
  37. data/lib/metanorma/document/components/blocks/requirement_model.rb +131 -0
  38. data/lib/metanorma/document/components/blocks.rb +7 -8
  39. data/lib/metanorma/document/components/id_elements/image.rb +0 -2
  40. data/lib/metanorma/document/components/inline/asciimath_element.rb +0 -2
  41. data/lib/metanorma/document/components/inline/attribution_element.rb +0 -2
  42. data/lib/metanorma/document/components/inline/bcp14_element.rb +0 -2
  43. data/lib/metanorma/document/components/inline/biblio_tag_element.rb +0 -2
  44. data/lib/metanorma/document/components/inline/br_element.rb +0 -2
  45. data/lib/metanorma/document/components/inline/comma_element.rb +0 -2
  46. data/lib/metanorma/document/components/inline/concept_element.rb +0 -2
  47. data/lib/metanorma/document/components/inline/display_text_element.rb +0 -2
  48. data/lib/metanorma/document/components/inline/em_raw_element.rb +0 -2
  49. data/lib/metanorma/document/components/inline/enum_comma_element.rb +0 -2
  50. data/lib/metanorma/document/components/inline/eref_element.rb +0 -2
  51. data/lib/metanorma/document/components/inline/fmt_admitted_element.rb +0 -4
  52. data/lib/metanorma/document/components/inline/fmt_annotation_body_element.rb +0 -4
  53. data/lib/metanorma/document/components/inline/fmt_annotation_end_element.rb +0 -4
  54. data/lib/metanorma/document/components/inline/fmt_annotation_start_element.rb +0 -4
  55. data/lib/metanorma/document/components/inline/fmt_concept_element.rb +0 -4
  56. data/lib/metanorma/document/components/inline/fmt_definition_element.rb +0 -4
  57. data/lib/metanorma/document/components/inline/fmt_fn_body_element.rb +0 -4
  58. data/lib/metanorma/document/components/inline/fmt_fn_label_element.rb +0 -4
  59. data/lib/metanorma/document/components/inline/fmt_footnote_container_element.rb +0 -4
  60. data/lib/metanorma/document/components/inline/fmt_identifier_element.rb +0 -4
  61. data/lib/metanorma/document/components/inline/fmt_name_element.rb +0 -4
  62. data/lib/metanorma/document/components/inline/fmt_preferred_element.rb +0 -4
  63. data/lib/metanorma/document/components/inline/fmt_sourcecode_element.rb +0 -4
  64. data/lib/metanorma/document/components/inline/fmt_stem_element.rb +0 -10
  65. data/lib/metanorma/document/components/inline/fmt_termsource_element.rb +0 -4
  66. data/lib/metanorma/document/components/inline/fmt_title_element.rb +0 -4
  67. data/lib/metanorma/document/components/inline/fmt_xref_element.rb +0 -4
  68. data/lib/metanorma/document/components/inline/fmt_xref_label_element.rb +0 -4
  69. data/lib/metanorma/document/components/inline/fn_element.rb +0 -2
  70. data/lib/metanorma/document/components/inline/link_element.rb +0 -2
  71. data/lib/metanorma/document/components/inline/localized_string_element.rb +0 -2
  72. data/lib/metanorma/document/components/inline/localized_strings_element.rb +0 -2
  73. data/lib/metanorma/document/components/inline/math_element.rb +0 -2
  74. data/lib/metanorma/document/components/inline/name_with_id_element.rb +0 -4
  75. data/lib/metanorma/document/components/inline/semx_child_element.rb +0 -2
  76. data/lib/metanorma/document/components/inline/semx_element.rb +1 -13
  77. data/lib/metanorma/document/components/inline/small_cap_element.rb +0 -2
  78. data/lib/metanorma/document/components/inline/span_element.rb +15 -10
  79. data/lib/metanorma/document/components/inline/stem_inline_element.rb +1 -7
  80. data/lib/metanorma/document/components/inline/strong_raw_element.rb +0 -2
  81. data/lib/metanorma/document/components/inline/sub_element.rb +0 -2
  82. data/lib/metanorma/document/components/inline/sup_element.rb +0 -2
  83. data/lib/metanorma/document/components/inline/tab_element.rb +0 -2
  84. data/lib/metanorma/document/components/inline/title_with_annotation_element.rb +0 -4
  85. data/lib/metanorma/document/components/inline/tt_element.rb +0 -2
  86. data/lib/metanorma/document/components/inline/variant_title_element.rb +0 -2
  87. data/lib/metanorma/document/components/inline/xref_element.rb +0 -10
  88. data/lib/metanorma/document/components/inline.rb +0 -12
  89. data/lib/metanorma/document/components/multi_paragraph/quote_block.rb +29 -0
  90. data/lib/metanorma/document/components/multi_paragraph.rb +0 -2
  91. data/lib/metanorma/document/components/reference_elements/source_element.rb +31 -0
  92. data/lib/metanorma/document/components/reference_elements.rb +2 -2
  93. data/lib/metanorma/document/components/tables/table_block.rb +22 -0
  94. data/lib/metanorma/document/components/tables/table_section.rb +25 -0
  95. data/lib/metanorma/document/components/tables.rb +3 -5
  96. data/lib/metanorma/document/relaton/address.rb +27 -0
  97. data/lib/metanorma/document/relaton/bib_item_locality.rb +12 -3
  98. data/lib/metanorma/document/relaton/copyright_association.rb +10 -0
  99. data/lib/metanorma/document/relaton/edition.rb +7 -4
  100. data/lib/metanorma/document/relaton/organization.rb +14 -0
  101. data/lib/metanorma/document/relaton/phone.rb +9 -3
  102. data/lib/metanorma/document/relaton/place_type.rb +13 -0
  103. data/lib/metanorma/document/relaton/price_type.rb +9 -3
  104. data/lib/metanorma/document/relaton.rb +1 -6
  105. data/lib/metanorma/document/version.rb +1 -1
  106. data/lib/metanorma/document.rb +0 -2
  107. data/lib/metanorma/html/asset_pipeline.rb +0 -1
  108. data/lib/metanorma/html/base_renderer.rb +1008 -261
  109. data/lib/metanorma/html/bipm_renderer.rb +144 -15
  110. data/lib/metanorma/html/cc_renderer.rb +45 -1
  111. data/lib/metanorma/html/component/base.rb +7 -2
  112. data/lib/metanorma/html/component/index_section.rb +43 -41
  113. data/lib/metanorma/html/drops/admonition_drop.rb +4 -1
  114. data/lib/metanorma/html/drops/block_element_drop.rb +2 -7
  115. data/lib/metanorma/html/drops/example_drop.rb +4 -1
  116. data/lib/metanorma/html/drops/figure_drop.rb +22 -31
  117. data/lib/metanorma/html/drops/footnote_drop.rb +1 -1
  118. data/lib/metanorma/html/drops/formula_drop.rb +16 -45
  119. data/lib/metanorma/html/drops/note_drop.rb +7 -9
  120. data/lib/metanorma/html/drops/sourcecode_drop.rb +5 -12
  121. data/lib/metanorma/html/drops.rb +0 -2
  122. data/lib/metanorma/html/generator.rb +2 -2
  123. data/lib/metanorma/html/icc_renderer.rb +39 -0
  124. data/lib/metanorma/html/iec_renderer.rb +44 -1
  125. data/lib/metanorma/html/ieee_renderer.rb +45 -1
  126. data/lib/metanorma/html/ietf_renderer.rb +48 -1
  127. data/lib/metanorma/html/iho_renderer.rb +46 -1
  128. data/lib/metanorma/html/iso_renderer.rb +343 -290
  129. data/lib/metanorma/html/itu_renderer.rb +45 -1
  130. data/lib/metanorma/html/ogc_renderer.rb +76 -15
  131. data/lib/metanorma/html/oiml_renderer.rb +38 -18
  132. data/lib/metanorma/html/pdfa_renderer.rb +39 -0
  133. data/lib/metanorma/html/ribose_renderer.rb +45 -1
  134. data/lib/metanorma/html/standard_renderer.rb +229 -341
  135. data/lib/metanorma/html/templates/_footnotes.html.liquid +6 -6
  136. data/lib/metanorma/html/templates/_formula.html.liquid +1 -1
  137. data/lib/metanorma/html/templates/_header.html.liquid +1 -1
  138. data/lib/metanorma/html/templates/_iso_cover.html.liquid +1 -1
  139. data/lib/metanorma/html/templates/document.html.liquid +0 -4
  140. data/lib/metanorma/html/theme.rb +107 -180
  141. data/lib/metanorma/html.rb +3 -14
  142. data/lib/metanorma/ietf_document/metadata/ietf_bib_data_extension_type.rb +25 -0
  143. data/lib/metanorma/ietf_document/metadata.rb +0 -2
  144. data/lib/metanorma/iso_document/metadata/iso_bib_data_extension_type.rb +48 -0
  145. data/lib/metanorma/iso_document/metadata/iso_bibliographic_item.rb +72 -0
  146. data/lib/metanorma/iso_document/metadata/iso_document_status.rb +30 -0
  147. data/lib/metanorma/iso_document/metadata/iso_localized_title.rb +1 -4
  148. data/lib/metanorma/iso_document/metadata/structured_identifier.rb +17 -0
  149. data/lib/metanorma/iso_document/metadata/title_collection.rb +0 -1
  150. data/lib/metanorma/iso_document/metadata.rb +4 -9
  151. data/lib/metanorma/iso_document/terms/iso_term.rb +13 -0
  152. data/lib/metanorma/iso_document/terms.rb +0 -1
  153. data/lib/metanorma/itu_document/metadata/itu_bib_data_extension_type.rb +37 -0
  154. data/lib/metanorma/itu_document/metadata.rb +4 -3
  155. data/lib/metanorma/standard_document/block_attributes.rb +9 -5
  156. data/lib/metanorma/standard_document/blocks/block_source.rb +23 -0
  157. data/lib/metanorma/standard_document/blocks.rb +1 -1
  158. data/lib/metanorma/standard_document/terms/term_expression.rb +31 -0
  159. data/lib/metanorma/standard_document/terms.rb +0 -1
  160. data/metanorma-document.gemspec +35 -0
  161. metadata +13 -262
  162. data/CLAUDE.md +0 -26
  163. data/TODO.finalize/01-split-multi-class-files.md +0 -8
  164. data/TODO.finalize/02-remove-data-dist-iso-document-frontend.md +0 -6
  165. data/TODO.finalize/03-align-frontend-iife-and-gha-build.md +0 -11
  166. data/TODO.finalize/04-theme-per-flavor-config-driven.md +0 -9
  167. data/TODO.finalize/05-migrate-all-html-to-liquid.md +0 -16
  168. data/TODO.finalize/06-integrate-pubid-library.md +0 -16
  169. data/TODO.finalize/07-replace-relaton-models-with-relaton-bib.md +0 -39
  170. data/TODO.finalize/08-refactor-renderer-architecture.md +0 -25
  171. data/TODO.finalize/09-eliminate-code-smells.md +0 -14
  172. data/TODO.finalize/10-comprehensive-spec-coverage.md +0 -24
  173. data/TODO.finalize/11-config-driven-flavor-metadata.md +0 -20
  174. data/TODO.finalize/12-add-frozen-string-literal.md +0 -6
  175. data/TODO.finalize/13-extract-composition-from-god-class.md +0 -96
  176. data/TODO.finalize/14-config-driven-section-ordering.md +0 -31
  177. data/TODO.finalize/15-theme-directories-per-flavor.md +0 -46
  178. data/TODO.finalize/16-theme-lutaml-model-schema.md +0 -44
  179. data/data/javascripts/components/mn-footnotes.js +0 -68
  180. data/data/logos/oiml-logo-full-dark.svg +0 -1
  181. data/data/logos/oiml-logo-full-light.svg +0 -1
  182. data/data/logos/oiml-logo-icon-dark.svg +0 -1
  183. data/data/logos/oiml-logo-icon-light.svg +0 -1
  184. data/data/logos/oiml-logo.svg +0 -1
  185. data/data/themes/bipm.yaml +0 -60
  186. data/data/themes/cc.yaml +0 -43
  187. data/data/themes/icc.yaml +0 -39
  188. data/data/themes/iec.yaml +0 -44
  189. data/data/themes/ieee.yaml +0 -43
  190. data/data/themes/ietf.yaml +0 -45
  191. data/data/themes/iho.yaml +0 -46
  192. data/data/themes/iso.yaml +0 -40
  193. data/data/themes/itu.yaml +0 -45
  194. data/data/themes/ogc.yaml +0 -51
  195. data/data/themes/oiml.yaml +0 -56
  196. data/data/themes/pdfa.yaml +0 -44
  197. data/data/themes/ribose.yaml +0 -43
  198. data/exe/metanorma-document +0 -11
  199. data/frontend/e2e/sample-html.test.ts +0 -256
  200. data/frontend/index.html +0 -12
  201. data/frontend/package-lock.json +0 -4725
  202. data/frontend/package.json +0 -32
  203. data/frontend/postcss.config.js +0 -6
  204. data/frontend/src/App.vue +0 -79
  205. data/frontend/src/app.ts +0 -21
  206. data/frontend/src/components/AppSidebar.vue +0 -77
  207. data/frontend/src/components/DocumentReader.vue +0 -180
  208. data/frontend/src/components/MirrorRenderer.vue +0 -23
  209. data/frontend/src/components/ReadingProgressBar.vue +0 -29
  210. data/frontend/src/components/TextRenderer.vue +0 -131
  211. data/frontend/src/components/TocItem.vue +0 -57
  212. data/frontend/src/components/TopBar.vue +0 -49
  213. data/frontend/src/components/__tests__/MirrorRenderer.test.ts +0 -178
  214. data/frontend/src/components/blocks/AdmonitionBlock.vue +0 -74
  215. data/frontend/src/components/blocks/ClauseBlock.vue +0 -53
  216. data/frontend/src/components/blocks/ExampleBlock.vue +0 -21
  217. data/frontend/src/components/blocks/FallbackBlock.vue +0 -18
  218. data/frontend/src/components/blocks/FigureBlock.vue +0 -26
  219. data/frontend/src/components/blocks/FootnotesBlock.vue +0 -25
  220. data/frontend/src/components/blocks/ListBlocks.vue +0 -62
  221. data/frontend/src/components/blocks/NoteBlock.vue +0 -21
  222. data/frontend/src/components/blocks/ParagraphBlock.vue +0 -25
  223. data/frontend/src/components/blocks/QuoteBlock.vue +0 -25
  224. data/frontend/src/components/blocks/ReviewBlock.vue +0 -39
  225. data/frontend/src/components/blocks/SourcecodeBlock.vue +0 -52
  226. data/frontend/src/components/blocks/TableBlock.vue +0 -48
  227. data/frontend/src/components/blocks/index.ts +0 -36
  228. data/frontend/src/stores/__tests__/documentStore.fixture.test.ts +0 -272
  229. data/frontend/src/stores/__tests__/documentStore.test.ts +0 -143
  230. data/frontend/src/stores/__tests__/ebookStore.test.ts +0 -145
  231. data/frontend/src/stores/__tests__/uiStore.test.ts +0 -42
  232. data/frontend/src/stores/documentStore.ts +0 -153
  233. data/frontend/src/stores/ebookStore.ts +0 -179
  234. data/frontend/src/stores/uiStore.ts +0 -32
  235. data/frontend/src/styles.css +0 -156
  236. data/frontend/src/types/__tests__/rendering-contract.test.ts +0 -172
  237. data/frontend/src/types/rendering-contract.ts +0 -209
  238. data/frontend/src/utils/__tests__/typeMetadata.test.ts +0 -102
  239. data/frontend/src/utils/typeMetadata.ts +0 -121
  240. data/frontend/tailwind.config.js +0 -18
  241. data/frontend/test/fixtures/iso-document.json +0 -271
  242. data/frontend/tmp/debug_e2e.cjs +0 -46
  243. data/frontend/tsconfig.json +0 -25
  244. data/frontend/tsconfig.node.json +0 -11
  245. data/frontend/vite.config.ts +0 -37
  246. data/frontend/vitest.e2e.config.ts +0 -9
  247. data/lib/metanorma/bipm_document/metadata/depiction_element.rb +0 -18
  248. data/lib/metanorma/document/cli.rb +0 -116
  249. data/lib/metanorma/document/components/ancillary_blocks/callout_annotation.rb +0 -27
  250. data/lib/metanorma/document/components/ancillary_blocks/sourcecode_body.rb +0 -18
  251. data/lib/metanorma/document/components/blocks/classification_value.rb +0 -21
  252. data/lib/metanorma/document/components/blocks/permission_model.rb +0 -28
  253. data/lib/metanorma/document/components/blocks/recommendation_model.rb +0 -28
  254. data/lib/metanorma/document/components/blocks/requirement_base.rb +0 -30
  255. data/lib/metanorma/document/components/blocks/requirement_classification.rb +0 -20
  256. data/lib/metanorma/document/components/blocks/requirement_description.rb +0 -43
  257. data/lib/metanorma/document/components/blocks/requirement_inherit.rb +0 -24
  258. data/lib/metanorma/document/components/inline/location_element.rb +0 -24
  259. data/lib/metanorma/document/components/inline/rendered_display.rb +0 -23
  260. data/lib/metanorma/document/components/inline/rendered_math_element.rb +0 -21
  261. data/lib/metanorma/document/components/inline/semantic_content.rb +0 -51
  262. data/lib/metanorma/document/components/inline/semantic_math_element.rb +0 -20
  263. data/lib/metanorma/document/components/inline/vocabulary.rb +0 -184
  264. data/lib/metanorma/document/components/multi_paragraph/quote_author_element.rb +0 -20
  265. data/lib/metanorma/document/components/reference_elements/source_modification.rb +0 -21
  266. data/lib/metanorma/document/components/reference_elements/source_origin.rb +0 -28
  267. data/lib/metanorma/document/components/tables/col_element.rb +0 -18
  268. data/lib/metanorma/document/components/tables/col_group_element.rb +0 -18
  269. data/lib/metanorma/document/components/tables/table_body_section.rb +0 -16
  270. data/lib/metanorma/document/components/tables/table_foot_section.rb +0 -16
  271. data/lib/metanorma/document/components/tables/table_head_section.rb +0 -16
  272. data/lib/metanorma/document/relaton/copyright_owner.rb +0 -16
  273. data/lib/metanorma/document/relaton/formatted_address.rb +0 -29
  274. data/lib/metanorma/document/relaton/logo_element.rb +0 -18
  275. data/lib/metanorma/document/relaton/region_element.rb +0 -18
  276. data/lib/metanorma/html/drops/biblio_entry_drop.rb +0 -50
  277. data/lib/metanorma/html/renderers/block_renderer.rb +0 -372
  278. data/lib/metanorma/html/renderers/inline_renderer.rb +0 -655
  279. data/lib/metanorma/html/renderers/pubid_renderer.rb +0 -69
  280. data/lib/metanorma/html/renderers/section_renderer.rb +0 -193
  281. data/lib/metanorma/html/renderers.rb +0 -8
  282. data/lib/metanorma/html/templates/_audio.html.liquid +0 -1
  283. data/lib/metanorma/html/templates/_biblio_entry.html.liquid +0 -1
  284. data/lib/metanorma/html/templates/_bipm_cover.html.liquid +0 -27
  285. data/lib/metanorma/html/templates/_boilerplate.html.liquid +0 -1
  286. data/lib/metanorma/html/templates/_bookmark.html.liquid +0 -1
  287. data/lib/metanorma/html/templates/_br.html.liquid +0 -1
  288. data/lib/metanorma/html/templates/_definition_list.html.liquid +0 -6
  289. data/lib/metanorma/html/templates/_element.html.liquid +0 -1
  290. data/lib/metanorma/html/templates/_fn_marker.html.liquid +0 -1
  291. data/lib/metanorma/html/templates/_heading.html.liquid +0 -1
  292. data/lib/metanorma/html/templates/_image.html.liquid +0 -1
  293. data/lib/metanorma/html/templates/_inline_span.html.liquid +0 -1
  294. data/lib/metanorma/html/templates/_link.html.liquid +0 -1
  295. data/lib/metanorma/html/templates/_list.html.liquid +0 -3
  296. data/lib/metanorma/html/templates/_list_item.html.liquid +0 -1
  297. data/lib/metanorma/html/templates/_main_content.html.liquid +0 -1
  298. data/lib/metanorma/html/templates/_math_container.html.liquid +0 -1
  299. data/lib/metanorma/html/templates/_paragraph.html.liquid +0 -1
  300. data/lib/metanorma/html/templates/_prefatory_section.html.liquid +0 -1
  301. data/lib/metanorma/html/templates/_pubid_identifier.html.liquid +0 -1
  302. data/lib/metanorma/html/templates/_quote.html.liquid +0 -4
  303. data/lib/metanorma/html/templates/_ref_date.html.liquid +0 -1
  304. data/lib/metanorma/html/templates/_ref_title.html.liquid +0 -1
  305. data/lib/metanorma/html/templates/_section.html.liquid +0 -5
  306. data/lib/metanorma/html/templates/_standard_cover.html.liquid +0 -31
  307. data/lib/metanorma/html/templates/_standard_doc_title.html.liquid +0 -3
  308. data/lib/metanorma/html/templates/_stem_span.html.liquid +0 -1
  309. data/lib/metanorma/html/templates/_table.html.liquid +0 -9
  310. data/lib/metanorma/html/templates/_term_domain.html.liquid +0 -1
  311. data/lib/metanorma/html/templates/_term_example.html.liquid +0 -1
  312. data/lib/metanorma/html/templates/_term_note.html.liquid +0 -1
  313. data/lib/metanorma/html/templates/_term_number.html.liquid +0 -1
  314. data/lib/metanorma/html/templates/_term_source.html.liquid +0 -1
  315. data/lib/metanorma/html/templates/_term_text_note.html.liquid +0 -1
  316. data/lib/metanorma/html/templates/_video.html.liquid +0 -1
  317. data/lib/metanorma/html/templates/_wrapped_preface.html.liquid +0 -1
  318. data/lib/metanorma/ietf_document/metadata/pi_settings.rb +0 -32
  319. data/lib/metanorma/iso_document/metadata/doctype_element.rb +0 -20
  320. data/lib/metanorma/iso_document/metadata/language_element.rb +0 -18
  321. data/lib/metanorma/iso_document/metadata/project_number.rb +0 -22
  322. data/lib/metanorma/iso_document/metadata/script_element.rb +0 -18
  323. data/lib/metanorma/iso_document/metadata/stage_element.rb +0 -26
  324. data/lib/metanorma/iso_document/metadata/stagename_element.rb +0 -18
  325. data/lib/metanorma/iso_document/terms/term_domain_element.rb +0 -18
  326. data/lib/metanorma/itu_document/metadata/meeting_date.rb +0 -18
  327. data/lib/metanorma/itu_document/metadata/meeting_element.rb +0 -18
  328. data/lib/metanorma/itu_document/metadata/study_period.rb +0 -18
  329. data/lib/metanorma/mirror/default_registry.rb +0 -170
  330. data/lib/metanorma/mirror/handler_registry.rb +0 -60
  331. data/lib/metanorma/mirror/handler_result.rb +0 -37
  332. data/lib/metanorma/mirror/handlers/admonition.rb +0 -18
  333. data/lib/metanorma/mirror/handlers/example.rb +0 -21
  334. data/lib/metanorma/mirror/handlers/figure.rb +0 -53
  335. data/lib/metanorma/mirror/handlers/formula.rb +0 -35
  336. data/lib/metanorma/mirror/handlers/inline/rich_html_renderer.rb +0 -159
  337. data/lib/metanorma/mirror/handlers/inline/text_extractor.rb +0 -116
  338. data/lib/metanorma/mirror/handlers/inline.rb +0 -225
  339. data/lib/metanorma/mirror/handlers/list.rb +0 -110
  340. data/lib/metanorma/mirror/handlers/note.rb +0 -26
  341. data/lib/metanorma/mirror/handlers/paragraph.rb +0 -19
  342. data/lib/metanorma/mirror/handlers/quote.rb +0 -16
  343. data/lib/metanorma/mirror/handlers/review.rb +0 -19
  344. data/lib/metanorma/mirror/handlers/section.rb +0 -149
  345. data/lib/metanorma/mirror/handlers/sourcecode.rb +0 -27
  346. data/lib/metanorma/mirror/handlers/structural.rb +0 -68
  347. data/lib/metanorma/mirror/handlers/table.rb +0 -81
  348. data/lib/metanorma/mirror/handlers/term.rb +0 -100
  349. data/lib/metanorma/mirror/handlers.rb +0 -60
  350. data/lib/metanorma/mirror/id_strategy/positional.rb +0 -166
  351. data/lib/metanorma/mirror/id_strategy/preserve.rb +0 -12
  352. data/lib/metanorma/mirror/id_strategy.rb +0 -34
  353. data/lib/metanorma/mirror/math_util.rb +0 -53
  354. data/lib/metanorma/mirror/metadata.rb +0 -37
  355. data/lib/metanorma/mirror/metanorma_to_mirror.rb +0 -101
  356. data/lib/metanorma/mirror/model/container.rb +0 -47
  357. data/lib/metanorma/mirror/model/factory.rb +0 -52
  358. data/lib/metanorma/mirror/model/guide.rb +0 -25
  359. data/lib/metanorma/mirror/model/leaf.rb +0 -17
  360. data/lib/metanorma/mirror/model/mark.rb +0 -53
  361. data/lib/metanorma/mirror/model/node.rb +0 -47
  362. data/lib/metanorma/mirror/model/soft_break.rb +0 -33
  363. data/lib/metanorma/mirror/model/text.rb +0 -34
  364. data/lib/metanorma/mirror/model.rb +0 -16
  365. data/lib/metanorma/mirror/output/builder.rb +0 -39
  366. data/lib/metanorma/mirror/output/formats/base_format.rb +0 -90
  367. data/lib/metanorma/mirror/output/formats/inline_format.rb +0 -81
  368. data/lib/metanorma/mirror/output/formats.rb +0 -53
  369. data/lib/metanorma/mirror/output/html_renderer.rb +0 -95
  370. data/lib/metanorma/mirror/output/html_renderers/block_renderers.rb +0 -178
  371. data/lib/metanorma/mirror/output/html_renderers/inline_renderer.rb +0 -61
  372. data/lib/metanorma/mirror/output/html_renderers/list_renderers.rb +0 -96
  373. data/lib/metanorma/mirror/output/html_renderers/mark_renderers.rb +0 -64
  374. data/lib/metanorma/mirror/output/html_renderers/section_renderers.rb +0 -129
  375. data/lib/metanorma/mirror/output/html_renderers/structural_renderers.rb +0 -80
  376. data/lib/metanorma/mirror/output/html_renderers/table_renderers.rb +0 -87
  377. data/lib/metanorma/mirror/output/html_renderers.rb +0 -76
  378. data/lib/metanorma/mirror/output/pipeline.rb +0 -92
  379. data/lib/metanorma/mirror/output/pipeline_context.rb +0 -22
  380. data/lib/metanorma/mirror/output.rb +0 -14
  381. data/lib/metanorma/mirror/rewriter.rb +0 -121
  382. data/lib/metanorma/mirror/safe_attr.rb +0 -16
  383. data/lib/metanorma/mirror/serialization/json_serializer.rb +0 -25
  384. data/lib/metanorma/mirror/serialization/yaml_serializer.rb +0 -20
  385. data/lib/metanorma/mirror/serialization.rb +0 -10
  386. data/lib/metanorma/mirror/transformer.rb +0 -22
  387. data/lib/metanorma/mirror.rb +0 -32
  388. data/lib/metanorma/standard_document/annotation.rb +0 -37
  389. data/lib/metanorma/standard_document/blocks/source_origin_element.rb +0 -23
  390. data/lib/metanorma/standard_document/terms/term_name_element.rb +0 -29
  391. data/script/render_both.rb +0 -18
@@ -6,16 +6,15 @@ require "cgi"
6
6
 
7
7
  module Metanorma
8
8
  module Html
9
- module Renderers
10
- autoload :InlineRenderer, "metanorma/html/renderers/inline_renderer"
11
- autoload :BlockRenderer, "metanorma/html/renderers/block_renderer"
12
- autoload :SectionRenderer, "metanorma/html/renderers/section_renderer"
13
- autoload :PubidRenderer, "metanorma/html/renderers/pubid_renderer"
14
- end
15
-
9
+ # Renders BasicDocument components to HTML.
10
+ # Subclassed by StandardRenderer and flavor-specific renderers.
11
+ # Owns the full HTML document generation pipeline: body content, header,
12
+ # footer, ToC sidebar, CSS (via Theme), and JavaScript.
16
13
  class BaseRenderer
17
14
  LOGO_DIR = File.expand_path("../../../data/logos", __dir__)
18
15
 
16
+ # HTML-specific class names for inline spans, keyed by the XML span role.
17
+ # The XML class_attr is INPUT only — we never emit it in HTML.
19
18
  SPAN_ROLE_CLASSES = {
20
19
  "boldtitle" => "title-text",
21
20
  "nonboldtitle" => "subtitle-text",
@@ -45,6 +44,8 @@ module Metanorma
45
44
 
46
45
  METANORMA_LOGO = "metanorma-logo.svg"
47
46
 
47
+ # Type-to-method registry for OCP dispatch.
48
+ # Each subclass gets its own hash; lookup traverses ancestors.
48
49
  class << self
49
50
  def render_registry
50
51
  @render_registry ||= {}
@@ -63,12 +64,8 @@ module Metanorma
63
64
  end
64
65
  end
65
66
 
66
- attr_reader :inline_renderer, :block_renderer, :section_renderer,
67
- :pubid_renderer, :index_term_collector, :footnote_collector
68
- private :inline_renderer, :block_renderer, :section_renderer,
69
- :pubid_renderer
70
-
71
67
  def initialize
68
+ @output = +""
72
69
  @toc_entries = []
73
70
  @figure_entries = []
74
71
  @table_entries = []
@@ -76,107 +73,86 @@ module Metanorma
76
73
  @footnote_collector = Component::FootnoteCollector.new
77
74
  @current_section_id = nil
78
75
  @current_section_number = nil
79
-
80
- @inline_renderer = Renderers::InlineRenderer.new(self)
81
- @block_renderer = Renderers::BlockRenderer.new(self)
82
- @section_renderer = Renderers::SectionRenderer.new(self)
83
- @pubid_renderer = Renderers::PubidRenderer.new(self)
84
76
  end
85
77
 
78
+ # --- Public API ---
79
+
80
+ # Facade object for Drops to call renderer methods without exposing
81
+ # the full private interface. Delegates via method_missing with an
82
+ # explicit allowlist — adding new delegations only requires updating
83
+ # DELEGATED_METHODS, not writing a new one-liner.
86
84
  class RendererContext
85
+ DELEGATED_METHODS = %i[
86
+ safe_attr escape_html extract_block_label extract_plain_text
87
+ capture_output render_paragraph render_mixed_inline
88
+ render_inline_element render_unordered_list render_ordered_list
89
+ render_definition_list render_sourcecode render_table
90
+ render_figure render_quote render_formula render_note
91
+ render_image render_stem_content register_figure_entry
92
+ render_liquid render_block_children
93
+ ].freeze
94
+
87
95
  def initialize(renderer)
88
96
  @renderer = renderer
89
97
  end
90
98
 
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
- def render_paragraph(...) = @renderer.render_paragraph(...)
96
- def render_mixed_inline(...)= @renderer.render_mixed_inline(...)
97
- def render_inline_element(...)= @renderer.render_inline_element(...)
98
- def render_unordered_list(...)= @renderer.render_unordered_list(...)
99
- def render_ordered_list(...)= @renderer.render_ordered_list(...)
100
- def render_definition_list(...)= @renderer.render_definition_list(...)
101
- def render_sourcecode(...) = @renderer.render_sourcecode(...)
102
- def render_table(...) = @renderer.render_table(...)
103
- def render_figure(...) = @renderer.render_figure(...)
104
- def render_quote(...) = @renderer.render_quote(...)
105
- def render_formula(...) = @renderer.render_formula(...)
106
- def render_note(...) = @renderer.render_note(...)
107
- def render_image(...) = @renderer.render_image(...)
108
- def render_stem_content(...)= @renderer.render_stem_content(...)
109
- def register_figure_entry(...)= @renderer.register_figure_entry(...)
110
- def render_liquid(...) = @renderer.render_liquid(...)
111
- def render_note_children(...) = @renderer.render_note_children(...)
112
- def render_simple_children(...) = @renderer.render_simple_children(...)
113
- def render_full_block_children(...) = @renderer.render_full_block_children(...)
99
+ def respond_to_missing?(method_name, include_private = false)
100
+ DELEGATED_METHODS.include?(method_name) || super
101
+ end
102
+
103
+ private
104
+
105
+ def method_missing(method_name, ...)
106
+ if DELEGATED_METHODS.include?(method_name)
107
+ @renderer.public_send(method_name, ...)
108
+ else
109
+ super
110
+ end
111
+ end
114
112
  end
115
113
 
116
114
  def renderer_context
117
115
  @renderer_context ||= RendererContext.new(self)
118
116
  end
119
117
 
118
+ def to_html
119
+ @output
120
+ end
121
+
120
122
  def toc_entries
121
123
  @toc_entries
122
124
  end
123
125
 
124
- attr_writer :document, :theme
125
-
126
- def generate_full_document(document, **)
126
+ # Generate a complete HTML document from a presentation XML document.
127
+ def generate_full_document(document)
127
128
  @document = document
128
129
  validate_presentation_xml!
129
130
 
130
- body = render(@document) || ""
131
+ # First pass: render body content (collects ToC entries as side effect)
132
+ render(@document)
133
+ body = @output
131
134
 
132
135
  assemble_document(body)
133
136
  end
134
137
 
135
- # --- Flavor configuration hooks ---
136
-
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
138
+ # --- Flavor configuration hooks (override in subclasses) ---
152
139
 
153
140
  def theme
154
- @theme ||= resolve_theme
155
- end
156
-
157
- def resolve_theme
158
- flavor = flavor_name
159
- flavor ? Theme.load(flavor) : Theme.new
160
- end
161
-
162
- def flavor_name
163
- return nil unless defined?(@document) && @document
164
-
165
- @document.class.name&.split("::")&.detect do |ns|
166
- FLAVOR_MAP.key?(ns)
167
- end&.then { |ns| FLAVOR_MAP[ns] }
141
+ @theme ||= Theme.new
168
142
  end
169
143
 
170
144
  def flavor_publishers(_doc_id)
171
- theme.publishers
145
+ []
172
146
  end
173
147
 
174
148
  def flavor_publisher_name
175
- theme.publisher_name
149
+ pubs = flavor_publishers(extract_primary_doc_id)
150
+ pubs.empty? ? nil : pubs.join("/")
176
151
  end
177
152
 
153
+ # Map of publisher name => logo filename. Override in flavor renderers.
178
154
  def publisher_logo_map
179
- theme.logos_light
155
+ {}
180
156
  end
181
157
 
182
158
  def flavor_font_url
@@ -189,7 +165,7 @@ module Metanorma
189
165
  TEMPLATE_CACHE_MUTEX = Mutex.new
190
166
 
191
167
  def render_liquid(template_name, assigns)
192
- template_path = theme.resolve_template(template_name)
168
+ template_path = File.join(TEMPLATES_ROOT, template_name)
193
169
  template = TEMPLATE_CACHE_MUTEX.synchronize do
194
170
  TEMPLATE_CACHE[template_path] ||= Liquid::Template.parse(File.read(template_path))
195
171
  end
@@ -235,7 +211,7 @@ module Metanorma
235
211
  end
236
212
 
237
213
  def header_title_text
238
- raw = html_title.to_s.split(" — ").first.to_s
214
+ raw = html_title.to_s.split(" — ").first.to_s.gsub(/<[^>]+>/, "")
239
215
  raw.length > 60 ? "#{raw[0, 57]}..." : raw
240
216
  end
241
217
 
@@ -244,7 +220,7 @@ module Metanorma
244
220
  logo_map = publisher_logo_map
245
221
  return "" if publishers.empty? && logo_map.empty?
246
222
 
247
- dark_logo_map = theme.logos_dark
223
+ # Use flavor-declared publishers; fall back to logo map keys
248
224
  display_pubs = publishers.empty? ? logo_map.keys : publishers
249
225
 
250
226
  display_pubs.filter_map do |pub|
@@ -254,28 +230,18 @@ module Metanorma
254
230
  svg = load_logo_svg(filename, height: 26)
255
231
  next unless svg
256
232
 
257
- light_span = "<span class=\"brand-logo brand-logo-light\" aria-label=\"#{pub} logo\">#{svg}</span>"
258
-
259
- dark_span = ""
260
- dark_filename = dark_logo_map[pub]
261
- if dark_filename
262
- dark_svg = load_logo_svg(dark_filename, height: 26)
263
- if dark_svg
264
- dark_span = "<span class=\"brand-logo brand-logo-dark\" aria-label=\"#{pub} logo\">#{dark_svg}</span>"
265
- end
266
- end
267
-
268
- "#{light_span}\n#{dark_span}"
233
+ "<span class=\"brand-logo\" aria-label=\"#{pub} logo\">#{svg}</span>"
269
234
  end.join("\n")
270
235
  end
271
236
 
272
237
  def load_logo_svg(filename, height: 32)
273
- path = theme.resolve_asset(filename) || File.join(LOGO_DIR, filename)
238
+ path = File.join(LOGO_DIR, filename)
274
239
  return nil unless File.exist?(path)
275
240
 
276
241
  svg = File.read(path)
277
242
  svg = svg.sub(/\A<\?xml[^?]*\?>\s*/, "")
278
243
  svg = svg.sub(/\A\s*<!--.*?-->\s*/m, "")
244
+ svg = svg.sub(/<path[^>]*style="fill:#e3000f[^"]*"[^>]*\/>/, "")
279
245
  svg = svg.sub(/<svg\s/, '<svg class="header-logo" ')
280
246
  svg = if svg.match?(/<svg[^>]*\sheight="[^"]*"/)
281
247
  svg.sub(/(<svg[^>]*?)(\sheight="[^"]*")/,
@@ -331,10 +297,7 @@ module Metanorma
331
297
  def build_styles
332
298
  pipeline = AssetPipeline.new
333
299
  css = pipeline.compile_css(flavor_css: flavor_css_module)
334
- parts = [theme.to_css_root, css, theme.to_css_extras]
335
- custom_css_path = theme.theme_css_path
336
- parts << File.read(custom_css_path) if custom_css_path
337
- parts.join("\n")
300
+ "#{theme.to_css_root}\n#{css}\n#{theme.to_css_extras}"
338
301
  end
339
302
 
340
303
  # --- Validation ---
@@ -353,11 +316,12 @@ module Metanorma
353
316
  return false unless node
354
317
  return false if node.is_a?(String)
355
318
 
319
+ if node.is_a?(Metanorma::Document::Root) && node.type == "presentation"
320
+ return true
321
+ end
322
+
356
323
  if node.is_a?(Lutaml::Model::Serializable)
357
324
  node_attrs = node.class.attributes
358
- if node_attrs.key?(:type) && node.type == "presentation"
359
- return true
360
- end
361
325
  if node_attrs.key?(:fmt_title) && node.fmt_title
362
326
  return true
363
327
  end
@@ -399,21 +363,16 @@ module Metanorma
399
363
  end
400
364
 
401
365
  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?
366
+ bibdata = @document.bibdata
367
+ return "Document" unless bibdata
413
368
 
414
- en = titles.find { |t| safe_attr(t, :language) == "en" }
415
- found = en || titles.first
416
- extract_text_value(found).to_s
369
+ titles = bibdata.titles
370
+ if titles
371
+ title = bibdata.title_for("en")
372
+ title.to_s
373
+ else
374
+ "Document"
375
+ end
417
376
  end
418
377
 
419
378
  def extract_primary_doc_id
@@ -434,7 +393,7 @@ module Metanorma
434
393
  text.strip.empty? ? nil : text.strip
435
394
  end
436
395
 
437
- # --- Registration helpers ---
396
+ # --- CSS loader ---
438
397
 
439
398
  def register_toc_entry(id:, level:, text:)
440
399
  @toc_entries << { id: id, level: level, text: text }
@@ -444,14 +403,12 @@ module Metanorma
444
403
  @figure_entries << { id: id, text: text }
445
404
  end
446
405
 
447
- def figure_entries
448
- @figure_entries
449
- end
450
-
451
406
  def register_table_entry(id:, text:)
452
407
  @table_entries << { id: id, text: text }
453
408
  end
454
409
 
410
+ attr_reader :index_term_collector, :footnote_collector
411
+
455
412
  def extract_plain_text(node)
456
413
  return node.to_s if node.is_a?(String)
457
414
  return extract_text_value(node).to_s unless node.is_a?(Lutaml::Model::Serializable)
@@ -474,39 +431,38 @@ module Metanorma
474
431
  if el.text?
475
432
  parts << el.text_content.to_s
476
433
  elsif el.name == "tab"
477
- parts << " "
434
+ parts << "\u00A0\u00A0"
478
435
  elsif el.name == "br"
479
436
  parts << " "
480
437
  elsif el.element?
481
438
  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
439
+ next unless attr_name
440
+
441
+ coll = node.public_send(attr_name)
442
+ obj = if coll.is_a?(Array)
443
+ idx = indices[attr_name]
444
+ indices[attr_name] += 1
445
+ coll[idx]
446
+ else
447
+ coll
448
+ end
449
+ parts << extract_plain_text(obj) if obj
496
450
  end
497
451
  end
498
452
  end
499
453
 
454
+ # Fallback: try .text
500
455
  if parts.join.strip.empty?
501
456
  t = safe_attr(node, :text)
502
457
  parts << (t.is_a?(Array) ? t.join : t.to_s) if t
503
458
  end
504
459
 
505
- parts.join.strip.gsub(" ", " ")
460
+ parts.join.strip.gsub("\u00A0", " ")
506
461
  end
507
462
 
508
- # --- Dispatch ---
509
-
463
+ # Dispatch to the appropriate render method via type registry.
464
+ # Lookups traverse the ancestor chain so subclasses inherit
465
+ # parent registrations and can override them independently.
510
466
  def render(node, **)
511
467
  return escape_html(node) if node.is_a?(String)
512
468
 
@@ -514,18 +470,20 @@ module Metanorma
514
470
  method ? public_send(method, node, **) : ""
515
471
  end
516
472
 
473
+ # Dispatch to the appropriate inline render method via type registry.
517
474
  def render_inline_element(element, **)
518
- @inline_renderer.render_inline_element(element)
519
- end
520
-
521
- def is_title_element?(node, section)
522
- title = safe_attr(section, :title)
523
- return false unless title
524
-
525
- node.equal?(title)
475
+ return "" if element.nil?
476
+ return escape_html(element) if element.is_a?(String)
477
+
478
+ method = lookup_dispatch(element.class, :inline_registry)
479
+ if method
480
+ public_send(method, element)
481
+ elsif element.is_a?(Lutaml::Model::Serializable) && element.mixed?
482
+ render_mixed_inline(element)
483
+ end
526
484
  end
527
485
 
528
- # --- Type registrations ---
486
+ # --- Type registrations (class-level, evaluated at class load time) ---
529
487
 
530
488
  register_render Metanorma::Document::Components::Paragraphs::ParagraphBlock,
531
489
  :render_paragraph
@@ -624,6 +582,7 @@ module Metanorma
624
582
  :render_index
625
583
  register_inline_render Metanorma::Document::Components::Blocks::NoteBlock,
626
584
  :render_note_inline
585
+ # All Fmt* elements delegate to render_mixed_inline
627
586
  register_inline_render Metanorma::Document::Components::Inline::FmtNameElement,
628
587
  :render_mixed_inline
629
588
  register_inline_render Metanorma::Document::Components::Inline::FmtTitleElement,
@@ -688,163 +647,941 @@ module Metanorma
688
647
  nil
689
648
  end
690
649
 
691
- # --- Delegation to sub-renderers ---
650
+ # --- Block-level rendering ---
651
+
652
+ def render_paragraph(p, **_opts)
653
+ attrs = element_attrs(id: safe_attr(p, :id),
654
+ style: alignment_style(safe_attr(
655
+ p, :alignment
656
+ )))
657
+ tag("p", attrs) { render_mixed_inline(p) }
658
+ end
659
+
660
+ def render_table(table, **_opts)
661
+ attrs = element_attrs(id: safe_attr(table, :id), class: "table-block")
662
+ table_id = safe_attr(table, :id)
663
+ name_el = safe_attr(table, :fmt_name) || safe_attr(table, :name)
664
+ if table_id && name_el
665
+ register_table_entry(id: table_id, text: extract_plain_text(name_el))
666
+ end
667
+ col_count = table_column_count(table)
668
+ @output << "<div class=\"table-scroll-wrapper\">"
669
+ tag("table", attrs) do
670
+ name_el = safe_attr(table, :fmt_name) || safe_attr(table, :name)
671
+ if name_el
672
+ @output << "<caption>"
673
+ render_inline_element(name_el)
674
+ @output << "</caption>"
675
+ end
676
+ @output << "<colgroup>" if table.colgroup
677
+ render_table_colgroup(table.colgroup) if table.colgroup
678
+ @output << "</colgroup>" if table.colgroup
679
+ render_table_section(table.thead, "thead") if table.thead
680
+ render_table_section(table.tbody, "tbody") if table.tbody
681
+ if table.tfoot || (table.note && !table.note.empty?)
682
+ @output << "<tfoot>"
683
+ render_table_section_rows(table.tfoot) if table.tfoot
684
+ if table.note && !table.note.empty?
685
+ @output << "<tr><td colspan=\"#{col_count}\" class=\"table-notes\">"
686
+ table.note.each { |n| render_note(n) }
687
+ @output << "</td></tr>"
688
+ end
689
+ @output << "</tfoot>"
690
+ end
691
+ end
692
+ @output << "</div>"
693
+ end
694
+
695
+ def table_column_count(table)
696
+ if table.colgroup&.col && !table.colgroup.col.empty?
697
+ return table.colgroup.col.size
698
+ end
699
+
700
+ # Walk all rows to find max column count, accounting for colspan
701
+ max_cols = 0
702
+ %i[thead tbody tfoot].each do |section|
703
+ sec = table.public_send(section)
704
+ next unless sec&.tr
705
+
706
+ sec.tr.each do |tr|
707
+ cols = 0
708
+ Array(tr.th).each do |th|
709
+ cols += th.colspan && th.colspan > 1 ? th.colspan : 1
710
+ end
711
+ Array(tr.td).each do |td|
712
+ cols += td.colspan && td.colspan > 1 ? td.colspan : 1
713
+ end
714
+ max_cols = cols if cols > max_cols
715
+ end
716
+ end
717
+ max_cols.positive? ? max_cols : 1
718
+ end
719
+
720
+ def render_table_colgroup(colgroup)
721
+ colgroup.col&.each do |col|
722
+ attrs = element_attrs(style: col.width ? "width: #{col.width}" : nil)
723
+ @output << "<col#{attrs}>"
724
+ end
725
+ end
726
+
727
+ def render_table_section(section, tag_name)
728
+ @output << "<#{tag_name}>"
729
+ render_table_section_rows(section)
730
+ @output << "</#{tag_name}>"
731
+ end
732
+
733
+ def render_table_section_rows(section)
734
+ section.tr&.each do |tr|
735
+ @output << "<tr>"
736
+ walked = walk_ordered(tr) do |type, obj|
737
+ next unless type == :element
738
+
739
+ render_table_cell(obj)
740
+ end
741
+ unless walked
742
+ Array(tr.th).each { |th| render_table_cell(th, "th") }
743
+ Array(tr.td).each { |td| render_table_cell(td, "td") }
744
+ end
745
+ @output << "</tr>"
746
+ end
747
+ end
748
+
749
+ def render_unordered_list(ul, **_opts)
750
+ attrs = element_attrs(id: safe_attr(ul, :id))
751
+ tag("ul", attrs) do
752
+ ul.listitem&.each { |li| render_list_item(li) }
753
+ end
754
+ end
755
+
756
+ def render_table_cell(cell, force_tag = nil)
757
+ tag_name = force_tag || (cell.is_a?(Metanorma::Document::Components::Tables::HeaderTableCell) ? "th" : "td")
758
+ attrs = element_attrs(
759
+ colspan: safe_attr(cell, :colspan),
760
+ rowspan: safe_attr(cell, :rowspan),
761
+ align: safe_attr(cell, :alignment),
762
+ valign: safe_attr(cell, :vertical_alignment),
763
+ )
764
+ @output << "<#{tag_name}#{attrs}>"
765
+ render_cell_content(cell)
766
+ @output << "</#{tag_name}>"
767
+ end
768
+
769
+ def render_ordered_list(ol, **_opts)
770
+ attrs = element_attrs(id: safe_attr(ol, :id),
771
+ start: safe_attr(ol, :start), type: safe_attr(ol, :type_attr))
772
+ tag("ol", attrs) do
773
+ ol.listitem&.each { |li| render_list_item(li) }
774
+ end
775
+ end
776
+
777
+ def render_list_item(li)
778
+ li_id = safe_attr(li, :id)
779
+ attrs = li_id ? %( id="#{escape_html(li_id)}") : ""
780
+ @output << "<li#{attrs}>"
781
+ render_mixed_content_in_order(li)
782
+ @output << "</li>"
783
+ end
784
+
785
+ # Render all children of a mixed-content node in document order,
786
+ # dispatching block elements to their render methods.
787
+ def render_mixed_content_in_order(node)
788
+ node.each_mixed_content do |child|
789
+ case child
790
+ when String
791
+ @output << escape_html(child)
792
+ else
793
+ if block_element?(child)
794
+ render(child)
795
+ else
796
+ render_inline_element(child)
797
+ end
798
+ end
799
+ end
800
+ end
801
+
802
+ # Collect all renderable children from a node in document order,
803
+ # sorted by displayorder when available. Uses walk_ordered to traverse
804
+ # element_order, and also gathers typed attributes that may not appear
805
+ # in element_order (e.g. terms, definitions on section models).
806
+ def collect_ordered_children(section)
807
+ children = []
808
+
809
+ walk_ordered(section) do |type, obj|
810
+ next if %i[text tab].include?(type)
811
+
812
+ children << obj
813
+ end
814
+
815
+ # Gather typed attributes that may not appear in element_order
816
+ supplementary_attrs = %i[terms definitions]
817
+ supplementary_attrs.each do |attr|
818
+ val = safe_attr(section, attr)
819
+ next if val.nil?
820
+
821
+ Array(val).each do |v|
822
+ children << v unless children.include?(v)
823
+ end
824
+ end
825
+
826
+ children.compact!
827
+ sort_by_displayorder(children)
828
+ end
829
+
830
+ # Render children of a section in displayorder, skipping title elements.
831
+ def render_ordered_content(section, level = 1)
832
+ children = collect_ordered_children(section)
833
+ children.each do |node|
834
+ next if node.is_a?(String)
835
+ next if is_title_element?(node, section)
836
+
837
+ render(node, level: level + 1)
838
+ end
839
+ end
840
+
841
+ def sort_by_displayorder(children)
842
+ children.sort_by do |node|
843
+ order = if node.is_a?(Lutaml::Model::Serializable) &&
844
+ node.class.attributes.key?(:displayorder)
845
+ node.displayorder
846
+ end
847
+ order &&= order.to_i
848
+ order || Float::INFINITY
849
+ end
850
+ end
851
+
852
+ def render_definition_list(dl, **_opts)
853
+ attrs = element_attrs(id: safe_attr(dl, :id))
854
+ tag("dl", attrs) do
855
+ dl.dt&.each_with_index do |dt, i|
856
+ @output << "<dt>"
857
+ render_mixed_inline(dt)
858
+ @output << "</dt>"
859
+ dd = dl.dd&.[](i)
860
+ if dd
861
+ @output << "<dd>"
862
+ render_mixed_inline(dd)
863
+ @output << "</dd>"
864
+ end
865
+ end
866
+ end
867
+ end
868
+
869
+ def render_figure(figure, **_opts)
870
+ drop = Drops::FigureDrop.from_model(figure, renderer: renderer_context)
871
+ @output << render_liquid("_figure.html.liquid", { "block" => drop })
872
+ end
873
+
874
+ def render_image(image)
875
+ src_val = safe_attr(image, :src) || safe_attr(image, :source)
876
+ attrs = element_attrs(
877
+ id: safe_attr(image, :id),
878
+ src: src_val,
879
+ alt: safe_attr(image, :alt),
880
+ height: safe_attr(image, :height),
881
+ width: safe_attr(image, :width),
882
+ )
883
+ @output << "<img#{attrs} />"
884
+ end
885
+
886
+ def render_video(video)
887
+ attrs = element_attrs(
888
+ id: safe_attr(video, :id),
889
+ src: safe_attr(video, :src),
890
+ )
891
+ @output << "<video#{attrs} controls></video>"
892
+ end
893
+
894
+ def render_audio(audio)
895
+ attrs = element_attrs(
896
+ id: safe_attr(audio, :id),
897
+ src: safe_attr(audio, :src),
898
+ )
899
+ @output << "<audio#{attrs} controls></audio>"
900
+ end
901
+
902
+ def render_note(note, **_opts)
903
+ drop = Drops::NoteDrop.from_model(note, renderer: renderer_context)
904
+ @output << render_liquid("_note.html.liquid", { "block" => drop })
905
+ end
906
+
907
+ def render_example(example, **_opts)
908
+ drop = Drops::ExampleDrop.from_model(example,
909
+ renderer: renderer_context)
910
+ @output << render_liquid("_example.html.liquid", { "block" => drop })
911
+ end
912
+
913
+ def render_sourcecode(sc, **_opts)
914
+ drop = Drops::SourcecodeDrop.from_model(sc, renderer: renderer_context)
915
+ @output << render_liquid("_sourcecode.html.liquid", { "block" => drop })
916
+ end
917
+
918
+ def render_formula(formula, **_opts)
919
+ drop = Drops::FormulaDrop.from_model(formula,
920
+ renderer: renderer_context)
921
+ @output << render_liquid("_formula.html.liquid", { "block" => drop })
922
+ end
923
+
924
+ def render_quote(quote, **_opts)
925
+ attrs = element_attrs(id: safe_attr(quote, :id), class: "quote")
926
+ tag("blockquote", attrs) do
927
+ quote.paragraphs&.each { |para| render_paragraph(para) }
928
+ quote.ul&.each { |ul| render_unordered_list(ul) }
929
+ quote.ol&.each { |ol| render_ordered_list(ol) }
930
+ if quote.attribution
931
+ @output << "<footer>"
932
+ render_mixed_inline(quote.attribution)
933
+ @output << "</footer>"
934
+ end
935
+ end
936
+ end
937
+
938
+ def render_admonition(admonition, **_opts)
939
+ drop = Drops::AdmonitionDrop.from_model(admonition,
940
+ renderer: renderer_context)
941
+ @output << render_liquid("_admonition.html.liquid", { "block" => drop })
942
+ end
943
+
944
+ def render_bookmark(bookmark, **_opts)
945
+ @output << %(<a id="#{escape_html(safe_attr(bookmark, :id).to_s)}"></a>)
946
+ end
947
+
948
+ # Renders the typed child collections of a block element (paragraphs,
949
+ # lists, nested blocks) in a standard order. Used by Drops and section
950
+ # rendering to avoid duplicating the enumeration in each consumer.
951
+ #
952
+ # +children+ is a Hash mapping attr names to render-method symbols:
953
+ # { paragraphs: :render_paragraph, ul: :render_unordered_list, ... }
954
+ #
955
+ # Each key is sent to +model+ via safe_attr; non-nil results are
956
+ # dispatched to the corresponding render method.
957
+ def render_block_children(model, children:)
958
+ children.each do |attr, render_method|
959
+ values = safe_attr(model, attr)
960
+ next if values.nil?
961
+
962
+ Array(values).each { |v| public_send(render_method, v) }
963
+ end
964
+ end
965
+
966
+ # Standard child set for container blocks (example, note, etc.)
967
+ BLOCK_CHILDREN = {
968
+ paragraphs: :render_paragraph,
969
+ ul: :render_unordered_list,
970
+ ol: :render_ordered_list,
971
+ dl: :render_definition_list,
972
+ sourcecode: :render_sourcecode,
973
+ table: :render_table,
974
+ figure: :render_figure,
975
+ quote: :render_quote,
976
+ formula: :render_formula,
977
+ }.freeze
978
+
979
+ # Minimal child set for simple blocks (admonition, note with content only)
980
+ SIMPLE_CHILDREN = {
981
+ paragraphs: :render_paragraph,
982
+ }.freeze
983
+
984
+ # Note-style child set (paragraphs + lists + dl, no nested blocks)
985
+ NOTE_CHILDREN = {
986
+ paragraphs: :render_paragraph,
987
+ ul: :render_unordered_list,
988
+ ol: :render_ordered_list,
989
+ dl: :render_definition_list,
990
+ quote: :render_quote,
991
+ }.freeze
992
+
993
+ # --- Section rendering ---
994
+
995
+ def render_basic_section(section, level: 1, **_opts)
996
+ attrs = element_attrs(id: safe_attr(section, :id))
997
+ tag("div", attrs) do
998
+ render_section_title(section, level)
999
+ section.blocks&.each { |block| render(block) }
1000
+ section.notes&.each { |note| render_note(note) }
1001
+ end
1002
+ end
1003
+
1004
+ def render_hierarchical_section(section, level: 1, **_opts)
1005
+ attrs = element_attrs(id: safe_attr(section, :id))
1006
+ tag("div", attrs) do
1007
+ render_section_title(section, level)
1008
+ render_section_content(section, level)
1009
+ section.subsections&.each { |sub| render(sub, level: level + 1) }
1010
+ end
1011
+ end
1012
+
1013
+ def render_content_section(section, level: 1, **_opts)
1014
+ render_hierarchical_section(section, level: level, **_opts)
1015
+ end
1016
+
1017
+ def render_section_title(section, level)
1018
+ titles = section.title
1019
+ return unless titles && !titles.empty?
1020
+
1021
+ h = "h#{[[level, 6].min, 1].max}"
1022
+ title_text = extract_title_text(titles)
1023
+ @output << "<#{h}>#{escape_html(title_text)}</#{h}>"
1024
+ end
1025
+
1026
+ def render_section_content(section, _level)
1027
+ section.blocks&.each { |block| render(block) }
1028
+ section.notes&.each { |note| render_note(note) }
1029
+ end
1030
+
1031
+ # --- Inline content rendering ---
1032
+
1033
+ # Walk element_order in document order, resolving each element to its
1034
+ # Ruby object via the XML mapping, and yielding (element_order_entry, resolved_object)
1035
+ # to the given block. Handles tab elements as nbsp. Skips elements not in the mapping.
1036
+ #
1037
+ # This is the single ordered-walk primitive used by all mixed content renderers:
1038
+ # - render_mixed_inline (inline-only)
1039
+ # - render_cell_content (mixed block/inline)
1040
+ # - render_semx_content (filtered display attrs only)
1041
+ def walk_ordered(node, allow_filter: nil)
1042
+ return false unless node.is_a?(Lutaml::Model::Serializable)
1043
+ return false unless node.element_order.is_a?(Array) && !node.element_order.empty?
1044
+
1045
+ xml_mapping = node.class.mappings_for(:xml, node.lutaml_register)
1046
+ return false unless xml_mapping
1047
+
1048
+ element_to_attr = {}
1049
+ xml_mapping.mapping_elements_hash.each_value do |rule_or_array|
1050
+ Array(rule_or_array).each do |rule|
1051
+ element_to_attr[rule.name] = rule.to
1052
+ element_to_attr[rule.name.to_s] = rule.to if rule.name.is_a?(Symbol)
1053
+ end
1054
+ end
1055
+
1056
+ skip_indices = build_semx_skip_set(node)
692
1057
 
693
- # Inline rendering delegation
694
- def walk_ordered(node, allow_filter: nil, &)
695
- @inline_renderer.walk_ordered(node, allow_filter: allow_filter, &)
1058
+ indices = Hash.new(0)
1059
+
1060
+ node.element_order.each_with_index do |el, i|
1061
+ next if skip_indices.include?(i)
1062
+
1063
+ if el.text?
1064
+ text = el.text_content
1065
+ yield :text, text if text && block_given?
1066
+ elsif el.element?
1067
+ # Handle <tab/> elements
1068
+ if el.name == "tab"
1069
+ yield :tab, nil if block_given?
1070
+ next
1071
+ end
1072
+
1073
+ attr_name = element_to_attr[el.name]
1074
+ next unless attr_name
1075
+
1076
+ # Apply optional filter (used by semx to skip semantic attrs)
1077
+ next if allow_filter && !allow_filter.include?(attr_name)
1078
+
1079
+ coll = node.public_send(attr_name)
1080
+ obj = if coll.is_a?(Array)
1081
+ idx = indices[attr_name]
1082
+ indices[attr_name] += 1
1083
+ coll[idx]
1084
+ else
1085
+ coll
1086
+ end
1087
+ yield :element, obj if obj && block_given?
1088
+ end
1089
+ end
1090
+ true
1091
+ end
1092
+
1093
+ # In presentation XML, semantic elements are followed by <semx> wrappers
1094
+ # or <fmt-*> display elements. Skip source elements to avoid duplicates.
1095
+ def build_semx_skip_set(node)
1096
+ skip_after = {
1097
+ "link" => "semx",
1098
+ "xref" => "semx",
1099
+ "eref" => "semx",
1100
+ "stem" => nil,
1101
+ "concept" => "fmt-concept",
1102
+ "refterm" => nil,
1103
+ "renderterm" => nil,
1104
+ "origin" => "semx",
1105
+ }
1106
+ skip = {}
1107
+ node.element_order.each_with_index do |el, i|
1108
+ next unless el.element?
1109
+
1110
+ next_tag = skip_after[el.name]
1111
+ next unless next_tag
1112
+
1113
+ next_el = node.element_order[i + 1]
1114
+ if next_tag.nil?
1115
+ skip[i] = true
1116
+ elsif next_el&.element? && next_el.name == next_tag
1117
+ skip[i] = true
1118
+ end
1119
+ end
1120
+ skip
1121
+ end
1122
+
1123
+ # Table cells can contain both block-level content (p, ul, ol, dl)
1124
+ # and inline content (text, em, strong, etc.) in document order.
1125
+ def render_cell_content(cell)
1126
+ walked = walk_ordered(cell) do |type, obj|
1127
+ case type
1128
+ when :text
1129
+ @output << escape_html(obj)
1130
+ when :tab
1131
+ @output << "\u00a0\u00a0"
1132
+ when :element
1133
+ if block_element?(obj)
1134
+ render(obj)
1135
+ else
1136
+ render_inline_element(obj)
1137
+ end
1138
+ end
1139
+ end
1140
+ unless walked
1141
+ render_mixed_content_in_order(cell)
1142
+ end
696
1143
  end
697
1144
 
698
1145
  def render_mixed_inline(node)
699
- @inline_renderer.render_mixed_inline(node)
1146
+ # Models using map_all_content: raw XML in content
1147
+ if raw_content_node?(node)
1148
+ raw = node.content
1149
+ if raw.is_a?(String) && !raw.strip.empty?
1150
+ @output << render_raw_content(raw)
1151
+ return
1152
+ end
1153
+ end
1154
+
1155
+ if node.is_a?(Lutaml::Model::Serializable) && node.element_order && !node.element_order.empty?
1156
+ render_ordered_inline(node)
1157
+ elsif node.is_a?(Lutaml::Model::Serializable)
1158
+ node.each_mixed_content do |child|
1159
+ case child
1160
+ when String
1161
+ @output << escape_html(child)
1162
+ else
1163
+ render_inline_element(child)
1164
+ end
1165
+ end
1166
+ else
1167
+ render_inline_collections(node)
1168
+ end
700
1169
  end
701
1170
 
702
- def render_cell_content(cell) = @inline_renderer.render_cell_content(cell)
703
- def render_em(el) = @inline_renderer.render_em(el)
704
- def render_strong(el) = @inline_renderer.render_strong(el)
705
- def render_tt(el) = @inline_renderer.render_tt(el)
706
- def render_sub(el) = @inline_renderer.render_sub(el)
707
- def render_sup(el) = @inline_renderer.render_sup(el)
708
- def render_small_caps(el) = @inline_renderer.render_small_caps(el)
709
- def render_underline(el) = @inline_renderer.render_underline(el)
710
- def render_strike(el) = @inline_renderer.render_strike(el)
711
- def render_br(*) = @inline_renderer.render_br
712
- def render_tab(*) = @inline_renderer.render_tab
713
- def render_span(el) = @inline_renderer.render_span(el)
714
- def render_fn_inline(el) = @inline_renderer.render_fn_inline(el)
715
- def render_stem(el) = @inline_renderer.render_stem(el)
716
- def render_semx_inline(el) = @inline_renderer.render_semx_inline(el)
717
- def render_fmt_xref(el) = @inline_renderer.render_fmt_xref(el)
718
- def render_comma(*) = @inline_renderer.render_comma
719
- def render_math(el) = @inline_renderer.render_math(el)
720
- def render_asciimath(el) = @inline_renderer.render_asciimath(el)
721
- def render_index(el) = @inline_renderer.render_index(el)
722
- def render_note_inline(el) = @inline_renderer.render_note_inline(el)
1171
+ def raw_content_node?(node)
1172
+ node.is_a?(Lutaml::Model::Serializable) &&
1173
+ node.content.is_a?(String)
1174
+ rescue NoMethodError
1175
+ false
1176
+ end
723
1177
 
724
- def render_semx_content(el,
725
- **)
726
- @inline_renderer.render_semx_content(el, **)
1178
+ # Iterate element_order directly, preserving whitespace text nodes
1179
+ # that each_mixed_content drops (it skips text where text.strip.empty?)
1180
+ def render_ordered_inline(node)
1181
+ walked = walk_ordered(node) do |type, obj|
1182
+ case type
1183
+ when :text
1184
+ @output << escape_html(obj)
1185
+ when :tab
1186
+ @output << "\u00a0\u00a0"
1187
+ when :element
1188
+ render_inline_element(obj)
1189
+ end
1190
+ end
1191
+ unless walked
1192
+ node.each_mixed_content do |child|
1193
+ case child
1194
+ when String
1195
+ @output << escape_html(child)
1196
+ else
1197
+ render_inline_element(child)
1198
+ end
1199
+ end
1200
+ end
727
1201
  end
728
1202
 
729
- def render_stem_content(stem) = @inline_renderer.render_stem_content(stem)
730
- def render_link(link) = @inline_renderer.render_link(link)
731
- def render_xref(xref) = @inline_renderer.render_xref(xref)
732
- def render_eref(eref) = @inline_renderer.render_eref(eref)
733
- def render_fn(fn) = @inline_renderer.render_fn(fn)
734
- def render_concept(concept) = @inline_renderer.render_concept(concept)
735
- def render_fmt_stem(fmt_stem) = @inline_renderer.render_fmt_stem(fmt_stem)
736
- def render_mixed_content_in_order(node) = @inline_renderer.render_mixed_content_in_order(node)
1203
+ # Inline adapter methods for registry dispatch
1204
+
1205
+ def render_em(el)
1206
+ render_inline_tag("em", el)
1207
+ end
737
1208
 
738
- # Block rendering delegation
739
- def render_paragraph(p,
740
- **)
741
- @block_renderer.render_paragraph(p, **)
1209
+ def render_strong(el)
1210
+ render_inline_tag("strong", el)
742
1211
  end
743
1212
 
744
- def render_table(table,
745
- **)
746
- @block_renderer.render_table(table, **)
1213
+ def render_tt(el)
1214
+ render_inline_tag("tt", el)
747
1215
  end
748
1216
 
749
- def render_unordered_list(ul,
750
- **)
751
- @block_renderer.render_unordered_list(ul, **)
1217
+ def render_sub(el)
1218
+ render_inline_tag("sub", el)
752
1219
  end
753
1220
 
754
- def render_ordered_list(ol,
755
- **)
756
- @block_renderer.render_ordered_list(ol, **)
1221
+ def render_sup(el)
1222
+ render_inline_tag("sup", el)
757
1223
  end
758
1224
 
759
- def render_definition_list(dl,
760
- **)
761
- @block_renderer.render_definition_list(dl, **)
1225
+ def render_small_caps(el)
1226
+ render_inline_tag("span", el, class: "small-caps")
762
1227
  end
763
1228
 
764
- def render_figure(figure,
765
- **)
766
- @block_renderer.render_figure(figure, **)
1229
+ def render_underline(el)
1230
+ render_inline_tag("u", el)
767
1231
  end
768
1232
 
769
- def render_image(image) = @block_renderer.render_image(image)
770
- def render_video(video) = @block_renderer.render_video(video)
771
- def render_audio(audio) = @block_renderer.render_audio(audio)
772
- def render_note(note, **) = @block_renderer.render_note(note, **)
1233
+ def render_strike(el)
1234
+ render_inline_tag("s", el)
1235
+ end
773
1236
 
774
- def render_example(example,
775
- **)
776
- @block_renderer.render_example(example, **)
1237
+ def render_br(*)
1238
+ @output << "<br />"
777
1239
  end
778
1240
 
779
- def render_sourcecode(sc,
780
- **)
781
- @block_renderer.render_sourcecode(sc, **)
1241
+ def render_tab(*)
1242
+ @output << "\u00a0\u00a0"
782
1243
  end
783
1244
 
784
- def render_formula(formula,
785
- **)
786
- @block_renderer.render_formula(formula, **)
1245
+ def render_span(el)
1246
+ xml_class = safe_attr(el, :class_attr).to_s
1247
+ html_class = html_class_for_span(xml_class) unless xml_class.empty?
1248
+ attrs = element_attrs(style: safe_attr(el, :style), class: html_class)
1249
+ tag("span", attrs) { render_mixed_inline(el) }
787
1250
  end
788
1251
 
789
- def render_quote(quote,
790
- **)
791
- @block_renderer.render_quote(quote, **)
1252
+ def render_fn_inline(el)
1253
+ render_fn(el)
792
1254
  end
793
1255
 
794
- def render_admonition(admonition,
795
- **)
796
- @block_renderer.render_admonition(admonition, **)
1256
+ def render_stem(el)
1257
+ @output << render_stem_content(el)
797
1258
  end
798
1259
 
799
- def render_bookmark(bookmark,
800
- **)
801
- @block_renderer.render_bookmark(bookmark, **)
1260
+ def render_semx_inline(el)
1261
+ render_semx_content(el)
802
1262
  end
803
1263
 
804
- def render_block_children(model,
805
- children:)
806
- @block_renderer.render_block_children(model, children: children)
1264
+ def render_fmt_xref(el)
1265
+ target = safe_attr(el, :target) || safe_attr(el, :to_attr)
1266
+ if target
1267
+ attrs = element_attrs(href: "##{escape_html(target)}", class: "xref")
1268
+ tag("a", attrs) { render_mixed_inline(el) }
1269
+ else
1270
+ render_mixed_inline(el)
1271
+ end
807
1272
  end
808
1273
 
809
- def render_note_children(model) = @block_renderer.render_note_children(model)
810
- def render_simple_children(model) = @block_renderer.render_simple_children(model)
811
- def render_full_block_children(model) = @block_renderer.render_full_block_children(model)
1274
+ def render_comma(*)
1275
+ @output << ", "
1276
+ end
812
1277
 
813
- # Section rendering delegation
814
- def render_basic_section(section,
815
- **)
816
- @section_renderer.render_basic_section(section, **)
1278
+ def render_math(el)
1279
+ @output << el.content.to_s
817
1280
  end
818
1281
 
819
- def render_hierarchical_section(section,
820
- **)
821
- @section_renderer.render_hierarchical_section(section, **)
1282
+ def render_asciimath(el)
1283
+ @output << %(<span class="stem">#{escape_html(Array(el.text).join)}</span>)
822
1284
  end
823
1285
 
824
- def render_content_section(section,
825
- **)
826
- @section_renderer.render_content_section(section, **)
1286
+ def render_index(el)
1287
+ collect_index_term(el)
1288
+ ""
827
1289
  end
828
1290
 
829
- def render_ordered_content(section,
830
- level = 1)
831
- @section_renderer.render_ordered_content(section, level)
1291
+ def render_note_inline(el)
1292
+ render_note(el)
832
1293
  end
833
1294
 
834
- def collect_ordered_children(section) = @section_renderer.collect_ordered_children(section)
835
- def sort_by_displayorder(children) = @section_renderer.sort_by_displayorder(children)
1295
+ def render_inline_collections(node)
1296
+ texts = node.text
1297
+ if texts.is_a?(Array)
1298
+ texts.each do |t|
1299
+ if t.is_a?(String)
1300
+ @output << escape_html(t)
1301
+ else
1302
+ render_inline_element(t)
1303
+ end
1304
+ end
1305
+ elsif texts.is_a?(String)
1306
+ @output << escape_html(texts)
1307
+ end
836
1308
 
837
- def render_preface(preface,
838
- **)
839
- @section_renderer.render_preface(preface, **)
1309
+ inline_attrs = %i[em strong smallcap sub sup tt underline strike
1310
+ xref eref link span stem concept fn br tab keyword
1311
+ fmt_annotation_start fmt_annotation_end
1312
+ fmt_stem fmt_fn_label fmt_concept
1313
+ bookmark image semx fmt_xref_label]
1314
+ inline_attrs.each do |attr|
1315
+ values = safe_attr(node, attr)
1316
+ next if values.nil?
1317
+
1318
+ Array(values).each { |v| render_inline_element(v) }
1319
+ end
840
1320
  end
841
1321
 
842
- # Pubid rendering delegation
843
- def parse_pubid(docidentifier_string) = @pubid_renderer.parse_pubid(docidentifier_string)
844
- def pubid_to_html(identifier) = @pubid_renderer.pubid_to_html(identifier)
1322
+ # Render SemxElement display content only, skipping semantic linkage.
1323
+ # semx wraps both semantic data (origin, xref, source, etc.) and
1324
+ # display content (fmt-xref, span, strong, etc.). Only render display.
1325
+ def render_semx_content(element, **_opts)
1326
+ display_attrs = %i[text fmt_xref fmt_link fmt_concept span strong em sup p semx
1327
+ asciimath math sub_child tt_child br_child tab_child
1328
+ stem_child figure_child formula_child sourcecode_child]
1329
+ label_stripped = false
1330
+
1331
+ walked = walk_ordered(element,
1332
+ allow_filter: display_attrs) do |type, obj|
1333
+ case type
1334
+ when :text
1335
+ text = obj
1336
+ if !label_stripped
1337
+ text = deduplicate_semx_text(text, @output)
1338
+ label_stripped = true
1339
+ end
1340
+ @output << escape_html(text)
1341
+ when :element
1342
+ if obj.is_a?(Metanorma::Document::Components::Paragraphs::ParagraphBlock)
1343
+ render_paragraph(obj)
1344
+ else
1345
+ render_inline_element(obj)
1346
+ end
1347
+ end
1348
+ end
1349
+
1350
+ unless walked
1351
+ display_attrs.each do |attr|
1352
+ val = safe_attr(element, attr)
1353
+ next if val.nil?
1354
+
1355
+ if val.is_a?(Array)
1356
+ val.each do |v|
1357
+ if v.is_a?(Metanorma::Document::Components::Paragraphs::ParagraphBlock)
1358
+ render_paragraph(v)
1359
+ else
1360
+ render_inline_element(v)
1361
+ end
1362
+ end
1363
+ elsif val.is_a?(String)
1364
+ @output << escape_html(val)
1365
+ else
1366
+ render_inline_element(val)
1367
+ end
1368
+ end
1369
+ end
1370
+ end
1371
+
1372
+ def deduplicate_semx_text(semx_text, output)
1373
+ first_word = semx_text[/\A\s*(\S+)/, 1]
1374
+ return semx_text unless first_word
1375
+
1376
+ tail = output[-200..]
1377
+ return semx_text unless tail&.rstrip&.end_with?(first_word)
1378
+
1379
+ semx_text.sub(/\A\s*#{Regexp.escape(first_word)}\s*/, "")
1380
+ end
1381
+
1382
+ def render_inline_tag(tag_name, element, **extra_attrs)
1383
+ tag(tag_name, element_attrs(**extra_attrs)) do
1384
+ render_mixed_inline(element)
1385
+ end
1386
+ end
1387
+
1388
+ # Process raw XML content from map_all_content models.
1389
+ # Strips source elements (xref, eref, stem) that have a following <semx>
1390
+ # wrapper, keeping only the semx display content.
1391
+ def render_raw_content(raw_xml)
1392
+ doc = Nokogiri::XML.fragment(raw_xml)
1393
+ # Convert fmt-link elements to HTML <a> tags before stripping wrappers
1394
+ doc.css("fmt-link").each do |el|
1395
+ target = el["target"] || el["href"]
1396
+ if target
1397
+ display_text = target.delete_prefix("mailto:")
1398
+ a = doc.document.create_element("a", display_text, "href" => target)
1399
+ el.replace(a)
1400
+ else
1401
+ el.replace(el.children)
1402
+ end
1403
+ end
1404
+ # Remove source elements that precede a <semx> sibling,
1405
+ # deduplicating any label text that appears in both the source
1406
+ # paragraph and the semx display content.
1407
+ doc.traverse do |node|
1408
+ next unless node.element?
1409
+ next unless %w[xref eref stem link].include?(node.name)
1410
+
1411
+ next_sib = node.next_sibling
1412
+ while next_sib.is_a?(Nokogiri::XML::Text) && next_sib.text.strip.empty?
1413
+ next_sib = next_sib.next_sibling
1414
+ end
1415
+ next unless next_sib&.element? && next_sib.name == "semx"
1416
+
1417
+ deduplicate_semx_label(node, next_sib)
1418
+ node.remove
1419
+ end
1420
+ # Strip presentation wrappers, keeping inner content
1421
+ %w[semx fmt-xref].each do |tag|
1422
+ doc.css(tag).each { |el| el.replace(el.children) }
1423
+ end
1424
+ # Remap XML class names to HTML-specific class names
1425
+ doc.css("[class]").each do |el|
1426
+ el["class"] = el["class"].split(/\s+/).map do |c|
1427
+ html_class_for_span(c)
1428
+ end.join(" ")
1429
+ end
1430
+ doc.inner_html
1431
+ end
1432
+
1433
+ def deduplicate_semx_label(source_node, semx_node)
1434
+ first_text = semx_node.children.find do |c|
1435
+ c.text? && !c.text.strip.empty?
1436
+ end
1437
+ return unless first_text
1438
+
1439
+ semx_prefix = first_text.text[/\A(\s*\S+)/, 1]
1440
+ return unless semx_prefix && !semx_prefix.strip.empty?
1441
+
1442
+ prev = source_node.previous_sibling
1443
+ return unless prev.is_a?(Nokogiri::XML::Text)
1444
+
1445
+ label = semx_prefix.strip
1446
+ prev_text = prev.text.rstrip
1447
+ return unless prev_text.end_with?(label)
1448
+
1449
+ prev.content = prev_text.sub(/#{Regexp.escape(label)}\s*\z/, "")
1450
+ first_text.content = first_text.text.sub(
1451
+ /\A\s*#{Regexp.escape(label)}\s*/, ""
1452
+ )
1453
+ end
1454
+
1455
+ def render_link(link)
1456
+ target = safe_attr(link, :target) || safe_attr(link, :href)
1457
+ attrs = element_attrs(href: target, id: safe_attr(link, :id))
1458
+ tag("a", attrs) do
1459
+ content = safe_attr(link, :content)
1460
+ if content && !Array(content).join.strip.empty?
1461
+ render_mixed_inline(link)
1462
+ else
1463
+ display_text = target.to_s.delete_prefix("mailto:")
1464
+ @output << escape_html(display_text)
1465
+ end
1466
+ end
1467
+ end
1468
+
1469
+ def render_xref(xref)
1470
+ target = safe_attr(xref, :target) || safe_attr(xref, :to_attr)
1471
+ attrs = element_attrs(href: "##{escape_html(target)}",
1472
+ id: safe_attr(xref,
1473
+ :id))
1474
+ tag("a", attrs) { render_mixed_inline(xref) }
1475
+ end
1476
+
1477
+ def render_eref(eref)
1478
+ citeas = safe_attr(eref, :citeas)
1479
+ if citeas
1480
+ @output << escape_html(citeas)
1481
+ else
1482
+ render_mixed_inline(eref)
1483
+ end
1484
+ end
1485
+
1486
+ def render_fn(fn)
1487
+ fn_id = safe_attr(fn, :id)
1488
+ number = @footnote_collector.register(fn)
1489
+
1490
+ attrs = element_attrs(id: fn_id, class: "fn-marker")
1491
+ tag("span", attrs) do
1492
+ label = safe_attr(fn, :fn_label) || safe_attr(fn, :reference)
1493
+ if label
1494
+ @output << "<sup><a href=\"##{escape_html("footnote-#{number}")}\" " \
1495
+ "class=\"fn-link\" id=\"#{escape_html("fnref-#{number}")}\">" \
1496
+ "#{escape_html(label.to_s)}</a></sup>"
1497
+ end
1498
+ end
1499
+ end
1500
+
1501
+ def render_concept(concept)
1502
+ render_mixed_inline(concept)
1503
+ end
1504
+
1505
+ # --- Stem/math rendering ---
1506
+
1507
+ def render_fmt_stem(fmt_stem)
1508
+ semx_items = Array(fmt_stem.semx)
1509
+ return if semx_items.empty?
1510
+
1511
+ semx = semx_items.first
1512
+ math_items = Array(semx.math)
1513
+ ascii_items = Array(semx.asciimath)
1514
+
1515
+ # Collect source formats for interactive copy dropdown
1516
+ source_formats = {}
1517
+ if ascii_items.any?
1518
+ ascii_text = ascii_items.map { |a| a.text.to_s.strip }.join
1519
+ source_formats["asciimath"] = ascii_text unless ascii_text.empty?
1520
+ end
1521
+
1522
+ data_attrs = ""
1523
+ unless source_formats.empty?
1524
+ data_attrs = " data-stem-formats='#{escape_html(source_formats.to_json)}'"
1525
+ end
1526
+
1527
+ if math_items.any?
1528
+ content = math_items.map { |m| m.content.to_s }.join
1529
+ unless content.empty?
1530
+ @output << "<span class=\"math-container\"#{data_attrs}>"
1531
+ @output << "<math xmlns=\"http://www.w3.org/1998/Math/MathML\">#{content}</math>"
1532
+ @output << "</span>"
1533
+ end
1534
+ elsif ascii_items.any?
1535
+ # No MathML — render asciimath as fallback
1536
+ text = ascii_items.map { |a| a.text.to_s.strip }.join
1537
+ @output << "<span class=\"stem\"#{data_attrs}>#{escape_html(text)}</span>"
1538
+ end
1539
+ end
1540
+
1541
+ def render_stem_content(stem)
1542
+ return "" if stem.nil?
1543
+
1544
+ # StemInlineElement — source element, skip (rendered via FmtStemElement)
1545
+ if stem.is_a?(Metanorma::Document::Components::Inline::StemInlineElement)
1546
+ return ""
1547
+ end
1548
+
1549
+ # FmtStemElement — already handled by render_fmt_stem
1550
+ if stem.is_a?(Metanorma::Document::Components::Inline::FmtStemElement)
1551
+ return ""
1552
+ end
1553
+
1554
+ # TextElements::StemElement — block math (no fmt- counterpart for display formulas)
1555
+ if stem.is_a?(Metanorma::Document::Components::TextElements::StemElement)
1556
+ if stem.math
1557
+ math_val = stem.math
1558
+ return math_val.to_xml if math_val.is_a?(Nokogiri::XML::Node)
1559
+ return math_val.to_xml if defined?(Moxml::Document) && math_val.is_a?(Moxml::Document)
1560
+
1561
+ math_items = Array(math_val)
1562
+ math_items.join
1563
+ elsif stem.asciimath
1564
+ text = extract_text_value(stem.asciimath)
1565
+ return %(<span class="stem">#{escape_html(text)}</span>)
1566
+ end
1567
+ if stem.latexmath
1568
+ text = extract_text_value(stem.latexmath)
1569
+ return %(<span class="stem">#{escape_html(text)}</span>)
1570
+ end
1571
+ end
1572
+
1573
+ text = extract_text_value(stem)
1574
+ text.empty? ? "" : %(<span class="stem">#{escape_html(text)}</span>)
1575
+ end
845
1576
 
846
1577
  # --- Helper methods ---
847
1578
 
1579
+ def tag(name, attrs_str)
1580
+ @output << "<#{name}#{attrs_str}>"
1581
+ yield
1582
+ @output << "</#{name}>"
1583
+ end
1584
+
848
1585
  def element_attrs(**attrs)
849
1586
  parts = []
850
1587
  attrs.each do |k, v|
@@ -907,6 +1644,7 @@ level = 1)
907
1644
  end
908
1645
 
909
1646
  def extract_block_label(block, default)
1647
+ # Presentation XML puts label in <name> child element
910
1648
  names = safe_attr(block, :name)
911
1649
  if names && !names.empty?
912
1650
  name = names.is_a?(Array) ? names.first : names
@@ -914,6 +1652,7 @@ level = 1)
914
1652
  return text unless text.to_s.strip.empty?
915
1653
  end
916
1654
 
1655
+ # Fallback: autonum XML attribute
917
1656
  autonum = safe_attr(block, :autonum)
918
1657
  if autonum && !autonum.to_s.empty?
919
1658
  number = autonum.to_s
@@ -930,10 +1669,9 @@ level = 1)
930
1669
  end
931
1670
 
932
1671
  def extract_title_text(titles)
933
- return "" if titles.nil?
934
- return extract_text_value(titles).to_s unless titles.is_a?(Array)
935
- return "" if titles.empty?
1672
+ return "" if titles.nil? || titles.empty?
936
1673
 
1674
+ titles = Array(titles)
937
1675
  title = titles.first
938
1676
  extract_text_value(title).to_s
939
1677
  end
@@ -971,20 +1709,29 @@ level = 1)
971
1709
  end
972
1710
 
973
1711
  def render_footnotes_section
974
- return nil if @footnote_collector.empty?
1712
+ return if @footnote_collector.empty?
975
1713
 
976
1714
  drops = @footnote_collector.to_a.map do |entry|
977
1715
  content_html = ""
978
1716
  if entry.content && !entry.content.empty?
979
- content_html = Array(entry.content).filter_map do |p|
980
- render_paragraph(p)
981
- end.join
1717
+ content_html = capture_output do
1718
+ Array(entry.content).each { |p| render_paragraph(p) }
1719
+ end
982
1720
  end
983
1721
  Drops::FootnoteDrop.new(entry, content_html)
984
1722
  end
985
1723
 
986
- render_liquid("_footnotes.html.liquid",
987
- { "footnotes" => drops })
1724
+ @output << render_liquid("_footnotes.html.liquid",
1725
+ { "footnotes" => drops })
1726
+ end
1727
+
1728
+ def capture_output
1729
+ old_output = @output
1730
+ @output = +""
1731
+ yield
1732
+ result = @output
1733
+ @output = old_output
1734
+ result
988
1735
  end
989
1736
  end
990
1737
  end