metanorma-document 0.2.9 → 0.2.12

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 (390) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +164 -66
  3. data/CLAUDE.md +26 -0
  4. data/README.adoc +144 -0
  5. data/Rakefile +14 -2
  6. data/TODO.finalize/01-split-multi-class-files.md +8 -0
  7. data/TODO.finalize/02-remove-data-dist-iso-document-frontend.md +6 -0
  8. data/TODO.finalize/03-align-frontend-iife-and-gha-build.md +11 -0
  9. data/TODO.finalize/04-theme-per-flavor-config-driven.md +9 -0
  10. data/TODO.finalize/05-migrate-all-html-to-liquid.md +16 -0
  11. data/TODO.finalize/06-integrate-pubid-library.md +16 -0
  12. data/TODO.finalize/07-replace-relaton-models-with-relaton-bib.md +39 -0
  13. data/TODO.finalize/08-refactor-renderer-architecture.md +25 -0
  14. data/TODO.finalize/09-eliminate-code-smells.md +14 -0
  15. data/TODO.finalize/10-comprehensive-spec-coverage.md +24 -0
  16. data/TODO.finalize/11-config-driven-flavor-metadata.md +20 -0
  17. data/TODO.finalize/12-add-frozen-string-literal.md +6 -0
  18. data/TODO.finalize/13-extract-composition-from-god-class.md +96 -0
  19. data/TODO.finalize/14-config-driven-section-ordering.md +31 -0
  20. data/TODO.finalize/15-theme-directories-per-flavor.md +46 -0
  21. data/TODO.finalize/16-theme-lutaml-model-schema.md +44 -0
  22. data/data/javascripts/components/mn-footnotes.js +68 -0
  23. data/data/javascripts/components/mn-lightbox.js +18 -2
  24. data/data/javascripts/components/mn-shortcuts.js +2 -0
  25. data/data/javascripts/core/mn-navigation.js +8 -0
  26. data/data/javascripts/core/mn-theme.js +9 -9
  27. data/data/logos/oiml-logo-full-dark.svg +1 -0
  28. data/data/logos/oiml-logo-full-light.svg +1 -0
  29. data/data/logos/oiml-logo-icon-dark.svg +1 -0
  30. data/data/logos/oiml-logo-icon-light.svg +1 -0
  31. data/data/logos/oiml-logo.svg +1 -0
  32. data/data/logos/pdfa-logo.svg +31 -94
  33. data/data/stylesheets/base/_layout.css +3 -0
  34. data/data/stylesheets/base/_reset.css +3 -3
  35. data/data/stylesheets/base/_typography.css +0 -5
  36. data/data/stylesheets/components/bibliography.css +3 -3
  37. data/data/stylesheets/components/cover.css +3 -1
  38. data/data/stylesheets/components/figure.css +13 -1
  39. data/data/stylesheets/components/footnote.css +65 -5
  40. data/data/stylesheets/components/formula.css +44 -0
  41. data/data/stylesheets/components/header.css +17 -2
  42. data/data/stylesheets/components/inline.css +1 -1
  43. data/data/stylesheets/components/table.css +15 -2
  44. data/data/stylesheets/components/term.css +0 -1
  45. data/data/themes/bipm.yaml +60 -0
  46. data/data/themes/cc.yaml +43 -0
  47. data/data/themes/icc.yaml +39 -0
  48. data/data/themes/iec.yaml +44 -0
  49. data/data/themes/ieee.yaml +43 -0
  50. data/data/themes/ietf.yaml +45 -0
  51. data/data/themes/iho.yaml +46 -0
  52. data/data/themes/iso.yaml +40 -0
  53. data/data/themes/itu.yaml +45 -0
  54. data/data/themes/ogc.yaml +51 -0
  55. data/data/themes/oiml.yaml +56 -0
  56. data/data/themes/pdfa.yaml +44 -0
  57. data/data/themes/ribose.yaml +43 -0
  58. data/exe/metanorma-document +11 -0
  59. data/frontend/e2e/sample-html.test.ts +256 -0
  60. data/frontend/index.html +12 -0
  61. data/frontend/package-lock.json +4725 -0
  62. data/frontend/package.json +32 -0
  63. data/frontend/postcss.config.js +6 -0
  64. data/frontend/src/App.vue +79 -0
  65. data/frontend/src/app.ts +21 -0
  66. data/frontend/src/components/AppSidebar.vue +77 -0
  67. data/frontend/src/components/DocumentReader.vue +180 -0
  68. data/frontend/src/components/MirrorRenderer.vue +23 -0
  69. data/frontend/src/components/ReadingProgressBar.vue +29 -0
  70. data/frontend/src/components/TextRenderer.vue +131 -0
  71. data/frontend/src/components/TocItem.vue +57 -0
  72. data/frontend/src/components/TopBar.vue +49 -0
  73. data/frontend/src/components/__tests__/MirrorRenderer.test.ts +178 -0
  74. data/frontend/src/components/blocks/AdmonitionBlock.vue +74 -0
  75. data/frontend/src/components/blocks/ClauseBlock.vue +53 -0
  76. data/frontend/src/components/blocks/ExampleBlock.vue +21 -0
  77. data/frontend/src/components/blocks/FallbackBlock.vue +18 -0
  78. data/frontend/src/components/blocks/FigureBlock.vue +26 -0
  79. data/frontend/src/components/blocks/FootnotesBlock.vue +25 -0
  80. data/frontend/src/components/blocks/ListBlocks.vue +62 -0
  81. data/frontend/src/components/blocks/NoteBlock.vue +21 -0
  82. data/frontend/src/components/blocks/ParagraphBlock.vue +25 -0
  83. data/frontend/src/components/blocks/QuoteBlock.vue +25 -0
  84. data/frontend/src/components/blocks/ReviewBlock.vue +39 -0
  85. data/frontend/src/components/blocks/SourcecodeBlock.vue +52 -0
  86. data/frontend/src/components/blocks/TableBlock.vue +48 -0
  87. data/frontend/src/components/blocks/index.ts +36 -0
  88. data/frontend/src/stores/__tests__/documentStore.fixture.test.ts +272 -0
  89. data/frontend/src/stores/__tests__/documentStore.test.ts +143 -0
  90. data/frontend/src/stores/__tests__/ebookStore.test.ts +145 -0
  91. data/frontend/src/stores/__tests__/uiStore.test.ts +42 -0
  92. data/frontend/src/stores/documentStore.ts +153 -0
  93. data/frontend/src/stores/ebookStore.ts +179 -0
  94. data/frontend/src/stores/uiStore.ts +32 -0
  95. data/frontend/src/styles.css +156 -0
  96. data/frontend/src/types/__tests__/rendering-contract.test.ts +172 -0
  97. data/frontend/src/types/rendering-contract.ts +209 -0
  98. data/frontend/src/utils/__tests__/typeMetadata.test.ts +102 -0
  99. data/frontend/src/utils/typeMetadata.ts +121 -0
  100. data/frontend/tailwind.config.js +18 -0
  101. data/frontend/test/fixtures/iso-document.json +271 -0
  102. data/frontend/tmp/debug_e2e.cjs +46 -0
  103. data/frontend/tsconfig.json +25 -0
  104. data/frontend/tsconfig.node.json +11 -0
  105. data/frontend/vite.config.ts +37 -0
  106. data/frontend/vitest.e2e.config.ts +9 -0
  107. data/lib/metanorma/bipm_document/metadata/bipm_bibliographic_item.rb +0 -14
  108. data/lib/metanorma/bipm_document/metadata/depiction_element.rb +18 -0
  109. data/lib/metanorma/bipm_document/metadata.rb +1 -1
  110. data/lib/metanorma/document/cli.rb +116 -0
  111. data/lib/metanorma/document/components/ancillary_blocks/callout_annotation.rb +27 -0
  112. data/lib/metanorma/document/components/ancillary_blocks/sourcecode_block.rb +0 -65
  113. data/lib/metanorma/document/components/ancillary_blocks/sourcecode_body.rb +18 -0
  114. data/lib/metanorma/document/components/ancillary_blocks.rb +4 -0
  115. data/lib/metanorma/document/components/bib_data/bib_data.rb +7 -0
  116. data/lib/metanorma/document/components/blocks/classification_value.rb +21 -0
  117. data/lib/metanorma/document/components/blocks/permission_model.rb +28 -0
  118. data/lib/metanorma/document/components/blocks/recommendation_model.rb +28 -0
  119. data/lib/metanorma/document/components/blocks/requirement_base.rb +30 -0
  120. data/lib/metanorma/document/components/blocks/requirement_classification.rb +20 -0
  121. data/lib/metanorma/document/components/blocks/requirement_description.rb +43 -0
  122. data/lib/metanorma/document/components/blocks/requirement_inherit.rb +24 -0
  123. data/lib/metanorma/document/components/blocks/requirement_model.rb +0 -131
  124. data/lib/metanorma/document/components/blocks.rb +8 -7
  125. data/lib/metanorma/document/components/id_elements/image.rb +2 -0
  126. data/lib/metanorma/document/components/inline/asciimath_element.rb +2 -0
  127. data/lib/metanorma/document/components/inline/attribution_element.rb +2 -0
  128. data/lib/metanorma/document/components/inline/bcp14_element.rb +2 -0
  129. data/lib/metanorma/document/components/inline/biblio_tag_element.rb +2 -0
  130. data/lib/metanorma/document/components/inline/br_element.rb +2 -0
  131. data/lib/metanorma/document/components/inline/comma_element.rb +2 -0
  132. data/lib/metanorma/document/components/inline/concept_element.rb +2 -0
  133. data/lib/metanorma/document/components/inline/display_text_element.rb +2 -0
  134. data/lib/metanorma/document/components/inline/em_raw_element.rb +2 -0
  135. data/lib/metanorma/document/components/inline/enum_comma_element.rb +2 -0
  136. data/lib/metanorma/document/components/inline/eref_element.rb +2 -0
  137. data/lib/metanorma/document/components/inline/fmt_admitted_element.rb +4 -0
  138. data/lib/metanorma/document/components/inline/fmt_annotation_body_element.rb +4 -0
  139. data/lib/metanorma/document/components/inline/fmt_annotation_end_element.rb +4 -0
  140. data/lib/metanorma/document/components/inline/fmt_annotation_start_element.rb +4 -0
  141. data/lib/metanorma/document/components/inline/fmt_concept_element.rb +4 -0
  142. data/lib/metanorma/document/components/inline/fmt_definition_element.rb +4 -0
  143. data/lib/metanorma/document/components/inline/fmt_fn_body_element.rb +4 -0
  144. data/lib/metanorma/document/components/inline/fmt_fn_label_element.rb +4 -0
  145. data/lib/metanorma/document/components/inline/fmt_footnote_container_element.rb +4 -0
  146. data/lib/metanorma/document/components/inline/fmt_identifier_element.rb +4 -0
  147. data/lib/metanorma/document/components/inline/fmt_name_element.rb +4 -0
  148. data/lib/metanorma/document/components/inline/fmt_preferred_element.rb +4 -0
  149. data/lib/metanorma/document/components/inline/fmt_sourcecode_element.rb +4 -0
  150. data/lib/metanorma/document/components/inline/fmt_stem_element.rb +10 -0
  151. data/lib/metanorma/document/components/inline/fmt_termsource_element.rb +4 -0
  152. data/lib/metanorma/document/components/inline/fmt_title_element.rb +4 -0
  153. data/lib/metanorma/document/components/inline/fmt_xref_element.rb +4 -0
  154. data/lib/metanorma/document/components/inline/fmt_xref_label_element.rb +4 -0
  155. data/lib/metanorma/document/components/inline/fn_element.rb +2 -0
  156. data/lib/metanorma/document/components/inline/link_element.rb +2 -0
  157. data/lib/metanorma/document/components/inline/localized_string_element.rb +2 -0
  158. data/lib/metanorma/document/components/inline/localized_strings_element.rb +2 -0
  159. data/lib/metanorma/document/components/inline/location_element.rb +24 -0
  160. data/lib/metanorma/document/components/inline/math_element.rb +2 -0
  161. data/lib/metanorma/document/components/inline/name_with_id_element.rb +4 -0
  162. data/lib/metanorma/document/components/inline/rendered_display.rb +23 -0
  163. data/lib/metanorma/document/components/inline/rendered_math_element.rb +21 -0
  164. data/lib/metanorma/document/components/inline/semantic_content.rb +51 -0
  165. data/lib/metanorma/document/components/inline/semantic_math_element.rb +20 -0
  166. data/lib/metanorma/document/components/inline/semx_child_element.rb +2 -0
  167. data/lib/metanorma/document/components/inline/semx_element.rb +13 -1
  168. data/lib/metanorma/document/components/inline/small_cap_element.rb +2 -0
  169. data/lib/metanorma/document/components/inline/span_element.rb +10 -15
  170. data/lib/metanorma/document/components/inline/stem_inline_element.rb +7 -1
  171. data/lib/metanorma/document/components/inline/strong_raw_element.rb +2 -0
  172. data/lib/metanorma/document/components/inline/sub_element.rb +2 -0
  173. data/lib/metanorma/document/components/inline/sup_element.rb +2 -0
  174. data/lib/metanorma/document/components/inline/tab_element.rb +2 -0
  175. data/lib/metanorma/document/components/inline/title_with_annotation_element.rb +4 -0
  176. data/lib/metanorma/document/components/inline/tt_element.rb +2 -0
  177. data/lib/metanorma/document/components/inline/variant_title_element.rb +2 -0
  178. data/lib/metanorma/document/components/inline/vocabulary.rb +184 -0
  179. data/lib/metanorma/document/components/inline/xref_element.rb +10 -0
  180. data/lib/metanorma/document/components/inline.rb +12 -0
  181. data/lib/metanorma/document/components/multi_paragraph/quote_author_element.rb +20 -0
  182. data/lib/metanorma/document/components/multi_paragraph/quote_block.rb +0 -29
  183. data/lib/metanorma/document/components/multi_paragraph.rb +2 -0
  184. data/lib/metanorma/document/components/reference_elements/source_element.rb +0 -31
  185. data/lib/metanorma/document/components/reference_elements/source_modification.rb +21 -0
  186. data/lib/metanorma/document/components/reference_elements/source_origin.rb +28 -0
  187. data/lib/metanorma/document/components/reference_elements.rb +2 -2
  188. data/lib/metanorma/document/components/tables/col_element.rb +18 -0
  189. data/lib/metanorma/document/components/tables/col_group_element.rb +18 -0
  190. data/lib/metanorma/document/components/tables/table_block.rb +0 -22
  191. data/lib/metanorma/document/components/tables/table_body_section.rb +16 -0
  192. data/lib/metanorma/document/components/tables/table_foot_section.rb +16 -0
  193. data/lib/metanorma/document/components/tables/table_head_section.rb +16 -0
  194. data/lib/metanorma/document/components/tables/table_section.rb +0 -25
  195. data/lib/metanorma/document/components/tables.rb +5 -3
  196. data/lib/metanorma/document/relaton/address.rb +0 -27
  197. data/lib/metanorma/document/relaton/bib_item_locality.rb +3 -12
  198. data/lib/metanorma/document/relaton/copyright_association.rb +0 -10
  199. data/lib/metanorma/document/relaton/copyright_owner.rb +16 -0
  200. data/lib/metanorma/document/relaton/edition.rb +4 -7
  201. data/lib/metanorma/document/relaton/formatted_address.rb +29 -0
  202. data/lib/metanorma/document/relaton/logo_element.rb +18 -0
  203. data/lib/metanorma/document/relaton/organization.rb +0 -14
  204. data/lib/metanorma/document/relaton/phone.rb +3 -9
  205. data/lib/metanorma/document/relaton/place_type.rb +0 -13
  206. data/lib/metanorma/document/relaton/price_type.rb +3 -9
  207. data/lib/metanorma/document/relaton/region_element.rb +18 -0
  208. data/lib/metanorma/document/relaton.rb +6 -1
  209. data/lib/metanorma/document/version.rb +1 -1
  210. data/lib/metanorma/document.rb +2 -0
  211. data/lib/metanorma/html/asset_pipeline.rb +1 -0
  212. data/lib/metanorma/html/base_renderer.rb +261 -1008
  213. data/lib/metanorma/html/bipm_renderer.rb +15 -144
  214. data/lib/metanorma/html/cc_renderer.rb +1 -45
  215. data/lib/metanorma/html/component/base.rb +2 -7
  216. data/lib/metanorma/html/component/index_section.rb +41 -43
  217. data/lib/metanorma/html/drops/admonition_drop.rb +1 -4
  218. data/lib/metanorma/html/drops/biblio_entry_drop.rb +50 -0
  219. data/lib/metanorma/html/drops/block_element_drop.rb +7 -2
  220. data/lib/metanorma/html/drops/example_drop.rb +1 -4
  221. data/lib/metanorma/html/drops/figure_drop.rb +31 -22
  222. data/lib/metanorma/html/drops/footnote_drop.rb +1 -1
  223. data/lib/metanorma/html/drops/formula_drop.rb +45 -16
  224. data/lib/metanorma/html/drops/note_drop.rb +9 -7
  225. data/lib/metanorma/html/drops/sourcecode_drop.rb +12 -5
  226. data/lib/metanorma/html/drops.rb +2 -0
  227. data/lib/metanorma/html/generator.rb +2 -2
  228. data/lib/metanorma/html/icc_renderer.rb +0 -39
  229. data/lib/metanorma/html/iec_renderer.rb +1 -44
  230. data/lib/metanorma/html/ieee_renderer.rb +1 -45
  231. data/lib/metanorma/html/ietf_renderer.rb +1 -48
  232. data/lib/metanorma/html/iho_renderer.rb +1 -46
  233. data/lib/metanorma/html/iso_renderer.rb +290 -343
  234. data/lib/metanorma/html/itu_renderer.rb +1 -45
  235. data/lib/metanorma/html/ogc_renderer.rb +15 -76
  236. data/lib/metanorma/html/oiml_renderer.rb +18 -38
  237. data/lib/metanorma/html/pdfa_renderer.rb +0 -39
  238. data/lib/metanorma/html/renderers/block_renderer.rb +372 -0
  239. data/lib/metanorma/html/renderers/inline_renderer.rb +655 -0
  240. data/lib/metanorma/html/renderers/pubid_renderer.rb +69 -0
  241. data/lib/metanorma/html/renderers/section_renderer.rb +193 -0
  242. data/lib/metanorma/html/renderers.rb +8 -0
  243. data/lib/metanorma/html/ribose_renderer.rb +1 -45
  244. data/lib/metanorma/html/standard_renderer.rb +341 -229
  245. data/lib/metanorma/html/templates/_audio.html.liquid +1 -0
  246. data/lib/metanorma/html/templates/_biblio_entry.html.liquid +1 -0
  247. data/lib/metanorma/html/templates/_bipm_cover.html.liquid +27 -0
  248. data/lib/metanorma/html/templates/_boilerplate.html.liquid +1 -0
  249. data/lib/metanorma/html/templates/_bookmark.html.liquid +1 -0
  250. data/lib/metanorma/html/templates/_br.html.liquid +1 -0
  251. data/lib/metanorma/html/templates/_definition_list.html.liquid +6 -0
  252. data/lib/metanorma/html/templates/_element.html.liquid +1 -0
  253. data/lib/metanorma/html/templates/_fn_marker.html.liquid +1 -0
  254. data/lib/metanorma/html/templates/_footnotes.html.liquid +6 -6
  255. data/lib/metanorma/html/templates/_formula.html.liquid +1 -1
  256. data/lib/metanorma/html/templates/_header.html.liquid +1 -1
  257. data/lib/metanorma/html/templates/_heading.html.liquid +1 -0
  258. data/lib/metanorma/html/templates/_image.html.liquid +1 -0
  259. data/lib/metanorma/html/templates/_inline_span.html.liquid +1 -0
  260. data/lib/metanorma/html/templates/_iso_cover.html.liquid +1 -1
  261. data/lib/metanorma/html/templates/_link.html.liquid +1 -0
  262. data/lib/metanorma/html/templates/_list.html.liquid +3 -0
  263. data/lib/metanorma/html/templates/_list_item.html.liquid +1 -0
  264. data/lib/metanorma/html/templates/_main_content.html.liquid +1 -0
  265. data/lib/metanorma/html/templates/_math_container.html.liquid +1 -0
  266. data/lib/metanorma/html/templates/_paragraph.html.liquid +1 -0
  267. data/lib/metanorma/html/templates/_prefatory_section.html.liquid +1 -0
  268. data/lib/metanorma/html/templates/_pubid_identifier.html.liquid +1 -0
  269. data/lib/metanorma/html/templates/_quote.html.liquid +4 -0
  270. data/lib/metanorma/html/templates/_ref_date.html.liquid +1 -0
  271. data/lib/metanorma/html/templates/_ref_title.html.liquid +1 -0
  272. data/lib/metanorma/html/templates/_section.html.liquid +5 -0
  273. data/lib/metanorma/html/templates/_standard_cover.html.liquid +31 -0
  274. data/lib/metanorma/html/templates/_standard_doc_title.html.liquid +3 -0
  275. data/lib/metanorma/html/templates/_stem_span.html.liquid +1 -0
  276. data/lib/metanorma/html/templates/_table.html.liquid +9 -0
  277. data/lib/metanorma/html/templates/_term_domain.html.liquid +1 -0
  278. data/lib/metanorma/html/templates/_term_example.html.liquid +1 -0
  279. data/lib/metanorma/html/templates/_term_note.html.liquid +1 -0
  280. data/lib/metanorma/html/templates/_term_number.html.liquid +1 -0
  281. data/lib/metanorma/html/templates/_term_source.html.liquid +1 -0
  282. data/lib/metanorma/html/templates/_term_text_note.html.liquid +1 -0
  283. data/lib/metanorma/html/templates/_video.html.liquid +1 -0
  284. data/lib/metanorma/html/templates/_wrapped_preface.html.liquid +1 -0
  285. data/lib/metanorma/html/templates/document.html.liquid +4 -0
  286. data/lib/metanorma/html/theme.rb +180 -107
  287. data/lib/metanorma/html.rb +13 -0
  288. data/lib/metanorma/ietf_document/metadata/ietf_bib_data_extension_type.rb +0 -25
  289. data/lib/metanorma/ietf_document/metadata/pi_settings.rb +32 -0
  290. data/lib/metanorma/ietf_document/metadata.rb +2 -0
  291. data/lib/metanorma/iso_document/metadata/doctype_element.rb +20 -0
  292. data/lib/metanorma/iso_document/metadata/iso_bib_data_extension_type.rb +0 -48
  293. data/lib/metanorma/iso_document/metadata/iso_bibliographic_item.rb +0 -72
  294. data/lib/metanorma/iso_document/metadata/iso_document_status.rb +0 -30
  295. data/lib/metanorma/iso_document/metadata/iso_localized_title.rb +4 -1
  296. data/lib/metanorma/iso_document/metadata/language_element.rb +18 -0
  297. data/lib/metanorma/iso_document/metadata/project_number.rb +22 -0
  298. data/lib/metanorma/iso_document/metadata/script_element.rb +18 -0
  299. data/lib/metanorma/iso_document/metadata/stage_element.rb +26 -0
  300. data/lib/metanorma/iso_document/metadata/stagename_element.rb +18 -0
  301. data/lib/metanorma/iso_document/metadata/structured_identifier.rb +0 -17
  302. data/lib/metanorma/iso_document/metadata/title_collection.rb +1 -0
  303. data/lib/metanorma/iso_document/metadata.rb +9 -4
  304. data/lib/metanorma/iso_document/terms/iso_term.rb +0 -13
  305. data/lib/metanorma/iso_document/terms/term_domain_element.rb +18 -0
  306. data/lib/metanorma/iso_document/terms.rb +1 -0
  307. data/lib/metanorma/itu_document/metadata/itu_bib_data_extension_type.rb +0 -37
  308. data/lib/metanorma/itu_document/metadata/meeting_date.rb +18 -0
  309. data/lib/metanorma/itu_document/metadata/meeting_element.rb +18 -0
  310. data/lib/metanorma/itu_document/metadata/study_period.rb +18 -0
  311. data/lib/metanorma/itu_document/metadata.rb +3 -4
  312. data/lib/metanorma/mirror/default_registry.rb +170 -0
  313. data/lib/metanorma/mirror/handler_registry.rb +60 -0
  314. data/lib/metanorma/mirror/handler_result.rb +37 -0
  315. data/lib/metanorma/mirror/handlers/admonition.rb +18 -0
  316. data/lib/metanorma/mirror/handlers/example.rb +21 -0
  317. data/lib/metanorma/mirror/handlers/figure.rb +53 -0
  318. data/lib/metanorma/mirror/handlers/formula.rb +35 -0
  319. data/lib/metanorma/mirror/handlers/inline/rich_html_renderer.rb +159 -0
  320. data/lib/metanorma/mirror/handlers/inline/text_extractor.rb +116 -0
  321. data/lib/metanorma/mirror/handlers/inline.rb +225 -0
  322. data/lib/metanorma/mirror/handlers/list.rb +110 -0
  323. data/lib/metanorma/mirror/handlers/note.rb +26 -0
  324. data/lib/metanorma/mirror/handlers/paragraph.rb +19 -0
  325. data/lib/metanorma/mirror/handlers/quote.rb +16 -0
  326. data/lib/metanorma/mirror/handlers/review.rb +19 -0
  327. data/lib/metanorma/mirror/handlers/section.rb +149 -0
  328. data/lib/metanorma/mirror/handlers/sourcecode.rb +27 -0
  329. data/lib/metanorma/mirror/handlers/structural.rb +68 -0
  330. data/lib/metanorma/mirror/handlers/table.rb +81 -0
  331. data/lib/metanorma/mirror/handlers/term.rb +100 -0
  332. data/lib/metanorma/mirror/handlers.rb +60 -0
  333. data/lib/metanorma/mirror/id_strategy/positional.rb +166 -0
  334. data/lib/metanorma/mirror/id_strategy/preserve.rb +12 -0
  335. data/lib/metanorma/mirror/id_strategy.rb +34 -0
  336. data/lib/metanorma/mirror/math_util.rb +53 -0
  337. data/lib/metanorma/mirror/metadata.rb +37 -0
  338. data/lib/metanorma/mirror/metanorma_to_mirror.rb +101 -0
  339. data/lib/metanorma/mirror/model/container.rb +47 -0
  340. data/lib/metanorma/mirror/model/factory.rb +52 -0
  341. data/lib/metanorma/mirror/model/guide.rb +25 -0
  342. data/lib/metanorma/mirror/model/leaf.rb +17 -0
  343. data/lib/metanorma/mirror/model/mark.rb +53 -0
  344. data/lib/metanorma/mirror/model/node.rb +47 -0
  345. data/lib/metanorma/mirror/model/soft_break.rb +33 -0
  346. data/lib/metanorma/mirror/model/text.rb +34 -0
  347. data/lib/metanorma/mirror/model.rb +16 -0
  348. data/lib/metanorma/mirror/output/builder.rb +39 -0
  349. data/lib/metanorma/mirror/output/formats/base_format.rb +90 -0
  350. data/lib/metanorma/mirror/output/formats/inline_format.rb +81 -0
  351. data/lib/metanorma/mirror/output/formats.rb +53 -0
  352. data/lib/metanorma/mirror/output/html_renderer.rb +95 -0
  353. data/lib/metanorma/mirror/output/html_renderers/block_renderers.rb +178 -0
  354. data/lib/metanorma/mirror/output/html_renderers/inline_renderer.rb +61 -0
  355. data/lib/metanorma/mirror/output/html_renderers/list_renderers.rb +96 -0
  356. data/lib/metanorma/mirror/output/html_renderers/mark_renderers.rb +64 -0
  357. data/lib/metanorma/mirror/output/html_renderers/section_renderers.rb +129 -0
  358. data/lib/metanorma/mirror/output/html_renderers/structural_renderers.rb +80 -0
  359. data/lib/metanorma/mirror/output/html_renderers/table_renderers.rb +87 -0
  360. data/lib/metanorma/mirror/output/html_renderers.rb +76 -0
  361. data/lib/metanorma/mirror/output/pipeline.rb +92 -0
  362. data/lib/metanorma/mirror/output/pipeline_context.rb +22 -0
  363. data/lib/metanorma/mirror/output.rb +14 -0
  364. data/lib/metanorma/mirror/rewriter.rb +121 -0
  365. data/lib/metanorma/mirror/safe_attr.rb +16 -0
  366. data/lib/metanorma/mirror/serialization/json_serializer.rb +25 -0
  367. data/lib/metanorma/mirror/serialization/yaml_serializer.rb +20 -0
  368. data/lib/metanorma/mirror/serialization.rb +10 -0
  369. data/lib/metanorma/mirror/transformer.rb +22 -0
  370. data/lib/metanorma/mirror.rb +32 -0
  371. data/lib/metanorma/standard_document/annotation.rb +37 -0
  372. data/lib/metanorma/standard_document/block_attributes.rb +15 -2
  373. data/lib/metanorma/standard_document/blocks/block_source.rb +0 -23
  374. data/lib/metanorma/standard_document/blocks/source_origin_element.rb +23 -0
  375. data/lib/metanorma/standard_document/blocks.rb +1 -1
  376. data/lib/metanorma/standard_document/terms/term_expression.rb +0 -31
  377. data/lib/metanorma/standard_document/terms/term_name_element.rb +29 -0
  378. data/lib/metanorma/standard_document/terms.rb +1 -0
  379. data/script/render_both.rb +18 -0
  380. metadata +262 -13
  381. data/.github/workflows/rake.yml +0 -21
  382. data/.github/workflows/release.yml +0 -25
  383. data/.github/workflows/roundtrip-samples.yml +0 -132
  384. data/.gitignore +0 -19
  385. data/.rspec +0 -3
  386. data/.rubocop.yml +0 -22
  387. data/Gemfile +0 -28
  388. data/bin/console +0 -15
  389. data/bin/setup +0 -8
  390. data/metanorma-document.gemspec +0 -35
@@ -10,47 +10,6 @@ module Metanorma
10
10
  class IsoRenderer < StandardRenderer
11
11
  # --- Public hooks for flavor customization ---
12
12
 
13
- def flavor_publishers(_doc_id)
14
- ["ISO"]
15
- end
16
-
17
- def flavor_publisher_name
18
- "ISO"
19
- end
20
-
21
- def publisher_logo_map
22
- { "ISO" => "iso-logo.svg" }
23
- end
24
-
25
- # ISO brand: #e3000f red from logo
26
- def theme
27
- @theme ||= Theme.new.tap do |t|
28
- t.primary = "#b3000c"
29
- t.accent = "#e3000f"
30
- t.accent_deep = "#b3000c"
31
- t.gradient = "linear-gradient(135deg, #8a0009 0%, #b3000c 50%, #e3000f 100%)"
32
- t.primary_light = "#fef0f1"
33
- t.accent_light = "#fde8ea"
34
- t.warm = "#1a1a1a"
35
- t.warm_light = "#f5f5f5"
36
- t.header_background = "linear-gradient(135deg, #8a0009 0%, #b3000c 40%, #e3000f 100%)"
37
- t.cover_background = "linear-gradient(175deg, #5a0006 0%, #8a0009 25%, #b3000c 50%, #e3000f 80%, #ff4d4d 100%)"
38
- t.cover_before_bg = "background: radial-gradient(ellipse at 25% 20%, rgba(255,77,77,0.15) 0%, transparent 50%), radial-gradient(ellipse at 75% 80%, rgba(26,26,26,0.1) 0%, transparent 40%)"
39
- t.cover_after_bg = "height: 3px; background: linear-gradient(90deg, transparent, #e3000f, #1a1a1a, transparent)"
40
- t.progress_bar_color = "#e3000f"
41
- t.note_border = "#e3000f"
42
- t.note_bg = "#fde8ea"
43
- t.note_color = "#e3000f"
44
- t.example_border = "#b3000c"
45
- t.example_bg = "#fef0f1"
46
- t.example_color = "#b3000c"
47
- t.admonition_border = "#1a1a1a"
48
- t.admonition_color = "#1a1a1a"
49
- t.footer_border_color = "#e3000f"
50
- t.cover_separator_color = "rgba(227,0,15,0.25)"
51
- end
52
- end
53
-
54
13
  register_render Metanorma::IsoDocument::Root, :render_document
55
14
  register_render Metanorma::IsoDocument::Sections::IsoPreface,
56
15
  :render_preface
@@ -77,9 +36,12 @@ module Metanorma
77
36
 
78
37
  def render_term_origin(element)
79
38
  text = extract_text_value(element)
80
- return unless text
39
+ return nil unless text
81
40
 
82
- @output << "<span class=\"term-source\">#{escape_html(text)}</span>"
41
+ render_liquid("_inline_span.html.liquid", {
42
+ "attrs" => " class=\"term-source\"",
43
+ "content" => escape_html(text),
44
+ })
83
45
  end
84
46
 
85
47
  def extract_display_title(bibdata)
@@ -107,6 +69,7 @@ module Metanorma
107
69
  raw_id = extract_text_value(identifiers.first).to_s.strip
108
70
  return nil if raw_id.empty?
109
71
 
72
+ raw_id = strip_doc_id_prefix(raw_id)
110
73
  pub = flavor_publisher_name
111
74
  if pub && !raw_id.start_with?(pub)
112
75
  "#{pub} #{raw_id}"
@@ -115,21 +78,25 @@ module Metanorma
115
78
  end
116
79
  end
117
80
 
118
- # Extract English stage text, deduplicating duplicate language variants.
81
+ def strip_doc_id_prefix(raw_id)
82
+ prefix = theme.doc_id_strip_prefix
83
+ return raw_id unless prefix
84
+
85
+ raw_id.to_s.gsub(/\A#{Regexp.escape(prefix)}\s+/, "").strip
86
+ end
87
+
119
88
  def extract_stage(bibdata)
120
89
  return nil unless bibdata.status&.stage
121
90
 
122
91
  stages = Array(bibdata.status.stage)
123
92
  return nil if stages.empty?
124
93
 
125
- # Prefer English-language stage
126
94
  en_stage = stages.find do |s|
127
95
  lang = safe_attr(s, :language)
128
96
  lang == "en" if lang
129
97
  end
130
98
  return Array(en_stage.value).join.strip if en_stage&.value
131
99
 
132
- # Fallback: first non-empty, deduplicated
133
100
  seen = Set.new
134
101
  stage_text = stages.filter_map do |s|
135
102
  val = Array(s.value).join.strip
@@ -142,7 +109,6 @@ module Metanorma
142
109
  stage_text.empty? ? nil : stage_text
143
110
  end
144
111
 
145
- # Extract document type from ext.doctype.
146
112
  def extract_doctype(bibdata)
147
113
  return nil unless bibdata.is_a?(Metanorma::IsoDocument::Metadata::IsoBibliographicItem)
148
114
 
@@ -152,14 +118,12 @@ module Metanorma
152
118
  doctypes = ext.doctype
153
119
  return nil unless doctypes && !doctypes.empty?
154
120
 
155
- # Prefer English-language doctype
156
121
  en_dt = doctypes.find do |d|
157
122
  lang = safe_attr(d, :language)
158
123
  lang == "en" if lang
159
124
  end
160
125
  return en_dt.value.to_s if en_dt&.value
161
126
 
162
- # Fallback: first doctype
163
127
  dt = doctypes.first
164
128
  val = dt&.value.to_s
165
129
  val.strip.empty? ? nil : val
@@ -168,38 +132,33 @@ module Metanorma
168
132
  # --- Top-level document rendering ---
169
133
 
170
134
  def render_document(doc, **_opts)
171
- render_coverpage(doc)
172
- render_boilerplate_section(doc)
173
-
174
- @output << "<main class=\"main-section\">"
175
-
176
- # Preface sections
177
- render(doc.preface) if doc.preface
135
+ parts = []
136
+ parts << (render_coverpage(doc) || "")
137
+ parts << (render_boilerplate_section(doc) || "")
178
138
 
179
- # Document title line
180
- render_doc_title(doc)
139
+ parts << (render(doc.preface) || "") if doc.preface
140
+ parts << (render_doc_title(doc) || "")
181
141
 
182
- # Collect ALL top-level content (sections + normative refs + annexes + bibliography)
183
- # and render in displayorder for correct document order.
184
142
  all_items = collect_document_children(doc)
185
-
186
143
  all_items.each do |node|
187
144
  next if node.is_a?(String)
188
145
  next if is_title_element?(node, doc.sections)
189
146
 
190
- render(node)
147
+ parts << (render(node) || "")
191
148
  end
192
149
 
193
- render_footnotes_section
150
+ parts << (render_footnotes_section || "")
194
151
 
195
- @output << "</main>"
152
+ render_liquid("_main_content.html.liquid", {
153
+ "content" => parts.join,
154
+ })
196
155
  end
197
156
 
198
157
  # --- Cover page ---
199
158
 
200
159
  def render_coverpage(doc)
201
160
  bibdata = doc.bibdata
202
- return unless bibdata
161
+ return "" unless bibdata
203
162
 
204
163
  logos = publisher_logos_html(doc) || []
205
164
  doc_id = formatted_doc_id(bibdata)
@@ -223,141 +182,150 @@ module Metanorma
223
182
  title_text = nil
224
183
  if bibdata.titles
225
184
  en_title = bibdata.title_for("en")
226
- title_text = en_title.to_s if en_title
185
+ if en_title
186
+ title_text = if en_title.is_a?(Metanorma::IsoDocument::Metadata::AbstractTitle) && en_title.value
187
+ en_title.value.to_s
188
+ else
189
+ en_title.to_s
190
+ end
191
+ end
227
192
  end
228
193
 
229
194
  stage_text = extract_stage(bibdata)
230
195
 
231
- @output << render_liquid("_iso_cover.html.liquid", {
232
- "publisher_logos" => logos,
233
- "doc_id" => doc_id,
234
- "pub_date" => pub_date,
235
- "doctype" => doctype,
236
- "title" => title_text,
237
- "stage" => stage_text,
238
- })
196
+ render_liquid("_standard_cover.html.liquid", {
197
+ "publisher_logos" => logos,
198
+ "doc_id" => doc_id,
199
+ "pub_date" => pub_date,
200
+ "doctype" => doctype,
201
+ "title" => title_text,
202
+ "stage" => stage_text,
203
+ })
239
204
  end
240
205
 
241
206
  def render_doc_title(doc)
242
207
  bibdata = doc.bibdata
243
- return unless bibdata
208
+ return nil unless bibdata
244
209
 
245
210
  titles = bibdata.titles
246
- return unless titles
211
+ return nil unless titles
247
212
 
248
213
  en_title = bibdata.title_for("en")
249
- return unless en_title
214
+ return nil unless en_title
250
215
 
251
- @output << render_liquid("_iso_doc_title.html.liquid", {
252
- "title" => en_title.to_s,
253
- })
216
+ render_liquid("_standard_doc_title.html.liquid", {
217
+ "title" => en_title.to_s,
218
+ })
254
219
  end
255
220
 
256
221
  def render_boilerplate_section(doc)
257
- return unless doc.boilerplate
222
+ return nil unless doc.boilerplate
258
223
 
259
- @output << "<div class=\"prefatory-section\">"
260
- render(doc.boilerplate)
261
- @output << "</div><hr class=\"cover-separator\" />"
262
- end
263
-
264
- def render_preface(preface, **_opts)
265
- render(preface.foreword) if preface.foreword
266
- render(preface.introduction) if preface.introduction
267
- render(preface.abstract) if preface.abstract
268
- preface.clause&.each { |cl| render(cl, level: 1) }
269
- render(preface.acknowledgements) if preface.acknowledgements
270
- render(preface.executivesummary) if preface.executivesummary
224
+ content = render(doc.boilerplate)
225
+ render_liquid("_prefatory_section.html.liquid", {
226
+ "content" => content,
227
+ })
271
228
  end
272
229
 
273
230
  def render_foreword(fw, level: 1, **_opts)
274
231
  attrs = element_attrs(id: safe_attr(fw, :id))
275
- tag("div", attrs) do
276
- title = safe_attr(fw, :fmt_title) || safe_attr(fw, :title)
277
- if title
278
- fw_id = safe_attr(fw, :id)
279
- title_text = extract_plain_text(title)
280
- register_toc_entry(id: fw_id, level: level, text: title_text)
281
- @output << "<h1 class=\"foreword-title\">"
282
- render_mixed_inline(title)
283
- @output << "</h1>"
284
- end
285
- render_ordered_content(fw)
232
+ title = safe_attr(fw, :fmt_title) || safe_attr(fw, :title)
233
+ parts = []
234
+ if title
235
+ fw_id = safe_attr(fw, :id)
236
+ title_content = render_mixed_inline(title)
237
+ register_toc_entry(id: fw_id, level: level,
238
+ text: extract_plain_text(title))
239
+ parts << render_liquid("_heading.html.liquid", {
240
+ "tag" => "h1",
241
+ "class_attr" => " class=\"foreword-title\"",
242
+ "content" => title_content,
243
+ })
286
244
  end
245
+ parts << (render_ordered_content(fw) || "")
246
+ render_liquid("_element.html.liquid", "tag" => "div",
247
+ "extra_attrs" => attrs, "content" => parts.join)
287
248
  end
288
249
 
289
250
  def render_abstract(section, level: 1, **_opts)
290
251
  attrs = element_attrs(id: safe_attr(section, :id))
291
- tag("div", attrs) do
292
- title = safe_attr(section, :fmt_title) || safe_attr(section, :title)
293
- if title
294
- sec_id = safe_attr(section, :id)
295
- title_text = extract_plain_text(title)
296
- register_toc_entry(id: sec_id, level: level, text: title_text)
297
- @output << "<h1 class=\"intro-title\">"
298
- render_mixed_inline(title)
299
- @output << "</h1>"
300
- end
301
- render_ordered_content(section)
252
+ title = safe_attr(section, :fmt_title) || safe_attr(section, :title)
253
+ parts = []
254
+ if title
255
+ sec_id = safe_attr(section, :id)
256
+ title_content = render_mixed_inline(title)
257
+ register_toc_entry(id: sec_id, level: level,
258
+ text: extract_plain_text(title))
259
+ parts << render_liquid("_heading.html.liquid", {
260
+ "tag" => "h1",
261
+ "class_attr" => " class=\"intro-title\"",
262
+ "content" => title_content,
263
+ })
302
264
  end
265
+ parts << (render_ordered_content(section) || "")
266
+ render_liquid("_element.html.liquid", "tag" => "div",
267
+ "extra_attrs" => attrs, "content" => parts.join)
303
268
  end
304
269
 
305
270
  # --- Main sections rendering ---
306
271
 
307
272
  def render_sections(sections, **_opts)
308
- # Collect ALL section-level children (from mixed content AND typed attributes)
309
- # then sort by displayorder for correct document order.
310
273
  children = collect_ordered_children(sections)
274
+ parts = []
311
275
  children.each do |node|
312
276
  next if node.is_a?(String)
313
277
  next if is_title_element?(node, sections)
314
278
 
315
- render(node, level: 1)
279
+ parts << (render(node, level: 1) || "")
316
280
  end
281
+ parts.join
317
282
  end
318
283
 
319
284
  # --- Clause rendering ---
320
285
 
321
286
  def render_clause(clause, level: 1, **_opts)
322
287
  attrs = element_attrs(id: safe_attr(clause, :id))
323
- tag("div", attrs) do
324
- render_title(clause, level)
325
- render_ordered_content(clause, level)
326
- end
288
+ title = render_title(clause, level)
289
+ content = render_ordered_content(clause, level)
290
+ render_liquid("_element.html.liquid", "tag" => "div",
291
+ "extra_attrs" => attrs, "content" => "#{title}#{content}")
327
292
  end
328
293
 
329
294
  # --- Annex rendering ---
330
295
 
331
296
  def render_annex(annex, level: 1, **_opts)
332
297
  attrs = element_attrs(id: safe_attr(annex, :id), class: "section-sub")
333
- tag("div", attrs) do
334
- render_annex_title(annex, level)
335
- render_ordered_content(annex, level)
336
- end
298
+ title = render_annex_title(annex, level)
299
+ content = render_ordered_content(annex, level)
300
+ render_liquid("_element.html.liquid", "tag" => "div",
301
+ "extra_attrs" => attrs, "content" => "#{title}#{content}")
337
302
  end
338
303
 
339
304
  def render_annex_title(annex, level)
340
305
  title_element = safe_attr(annex, :fmt_title) || safe_attr(annex, :title)
341
- return unless title_element
306
+ return nil unless title_element
342
307
 
343
308
  annex_id = safe_attr(annex, :id)
344
- title_text = extract_plain_text(title_element)
345
- register_toc_entry(id: annex_id, level: level, text: title_text)
309
+ title_content = render_mixed_inline(title_element)
310
+ register_toc_entry(id: annex_id, level: level,
311
+ text: extract_plain_text(title_element))
346
312
 
347
313
  h = "h#{[[level, 6].min, 1].max}"
348
- @output << "<#{h} class=\"annex-title\">"
349
- render_mixed_inline(title_element)
350
- @output << "</#{h}>"
314
+ render_liquid("_heading.html.liquid", {
315
+ "tag" => h,
316
+ "class_attr" => " class=\"annex-title\"",
317
+ "content" => title_content,
318
+ })
351
319
  end
352
320
 
353
321
  # --- Terms section rendering ---
354
322
 
355
323
  def render_terms_section(terms, level: 1, **_opts)
356
324
  attrs = element_attrs(id: safe_attr(terms, :id))
357
- tag("div", attrs) do
358
- render_title(terms, level)
359
- render_ordered_content(terms, level)
360
- end
325
+ title = render_title(terms, level)
326
+ content = render_ordered_content(terms, level)
327
+ render_liquid("_element.html.liquid", "tag" => "div",
328
+ "extra_attrs" => attrs, "content" => "#{title}#{content}")
361
329
  end
362
330
 
363
331
  # --- ISO Term rendering ---
@@ -367,112 +335,120 @@ module Metanorma
367
335
  term_def = extract_term_definition(term)
368
336
  data_attrs = {}
369
337
  if term_name && !term_name.empty?
370
- data_attrs["data-term-name"] =
371
- term_name
338
+ data_attrs["data-term-name"] = term_name
372
339
  end
373
340
  if term_def && !term_def.empty?
374
- data_attrs["data-term-definition"] =
375
- term_def
341
+ data_attrs["data-term-definition"] = term_def
376
342
  end
377
343
  attrs = element_attrs(id: safe_attr(term, :id), **data_attrs)
378
- tag("div", attrs) do
379
- # In presentation mode, use fmt_* elements
380
- if term.fmt_name
381
- # Render term number (e.g. "3.1")
382
- @output << "<p class=\"term-number\">"
383
- render_inline_element(term.fmt_name)
384
- @output << "</p>"
385
- elsif term.term_number
386
- tn = term.term_number
387
- tn_text = if tn.is_a?(String)
388
- tn
389
- else
390
- extract_text_value(tn)
391
- end
392
- @output << "<p class=\"term-number\">#{escape_html(tn_text)}</p>"
393
- end
344
+ fmt_name = safe_attr(term, :fmt_name)
345
+ fmt_preferred = safe_attr(term, :fmt_preferred)
346
+ fmt_admitted = safe_attr(term, :fmt_admitted)
347
+ fmt_deprecates = safe_attr(term, :fmt_deprecates)
348
+ fmt_definition = safe_attr(term, :fmt_definition)
349
+ fmt_termsource = safe_attr(term, :fmt_termsource)
350
+ term_number = safe_attr(term, :term_number)
351
+ term_p = safe_attr(term, :p)
352
+ term_note = safe_attr(term, :termnote) || Array(safe_attr(term, :note))
353
+ term_example = safe_attr(term,
354
+ :termexample) || Array(safe_attr(term,
355
+ :example))
356
+ term_termsource = safe_attr(term, :termsource)
357
+ term_admonition = safe_attr(term, :admonition)
358
+ term_nested = safe_attr(term, :term)
394
359
 
395
- # Preferred designations — use fmt-preferred if available
396
- if term.fmt_preferred && !term.fmt_preferred.empty?
397
- term.fmt_preferred.each do |fp|
398
- fp.p&.each do |para|
399
- render_paragraph(para)
400
- end
401
- end
402
- elsif term.preferred && !term.preferred.empty?
403
- term.preferred&.each do |designation|
404
- render_term_designation(designation, "preferred")
405
- end
360
+ parts = []
361
+ if fmt_name
362
+ tn_content = render_inline_element(fmt_name)
363
+ parts << render_liquid("_term_number.html.liquid",
364
+ { "content" => tn_content })
365
+ elsif term_number
366
+ tn_text = if term_number.is_a?(String)
367
+ term_number
368
+ else
369
+ extract_text_value(term_number)
370
+ end
371
+ parts << render_liquid("_term_number.html.liquid",
372
+ { "content" => escape_html(tn_text) })
373
+ end
374
+
375
+ if fmt_preferred && !fmt_preferred.empty?
376
+ fmt_preferred.each do |fp|
377
+ fp.p&.each { |para| parts << (render_paragraph(para) || "") }
406
378
  end
407
-
408
- # Admitted designations — use fmt-admitted if available
409
- if term.fmt_admitted && !term.fmt_admitted.empty?
410
- term.fmt_admitted.each do |fa|
411
- fa.p&.each do |para|
412
- render_paragraph(para)
413
- end
414
- end
415
- elsif term.admitted && !term.admitted.empty?
416
- term.admitted&.each do |designation|
417
- render_term_designation(designation, "admitted")
418
- end
379
+ elsif term.preferred && !term.preferred.empty?
380
+ term.preferred&.each do |designation|
381
+ parts << (render_term_designation(designation, "preferred") || "")
419
382
  end
383
+ end
420
384
 
421
- # Deprecated designations — use fmt-deprecates if available
422
- if term.fmt_deprecates
423
- term.fmt_deprecates.p&.each { |para| render_paragraph(para) }
424
- elsif term.deprecates && !term.deprecates.empty?
425
- term.deprecates&.each do |designation|
426
- render_term_designation(designation, "deprecated")
427
- end
385
+ if fmt_admitted && !fmt_admitted.empty?
386
+ fmt_admitted.each do |fa|
387
+ fa.p&.each { |para| parts << (render_paragraph(para) || "") }
428
388
  end
389
+ elsif term.admitted && !term.admitted.empty?
390
+ term.admitted&.each do |designation|
391
+ parts << (render_term_designation(designation, "admitted") || "")
392
+ end
393
+ end
429
394
 
430
- # Domain — only render separately when fmt-definition is absent
431
- # (fmt-definition already includes domain text in its content)
432
- if term.domain && !term.fmt_definition
433
- domain_text = safe_attr(term.domain, :text)
434
- @output << "<p class=\"term-domain\">&lt;#{escape_html(domain_text)}&gt;</p>" if domain_text
395
+ if fmt_deprecates
396
+ fmt_deprecates.p&.each do |para|
397
+ parts << (render_paragraph(para) || "")
398
+ end
399
+ elsif term.deprecates && !term.deprecates.empty?
400
+ term.deprecates&.each do |designation|
401
+ parts << (render_term_designation(designation, "deprecated") || "")
435
402
  end
403
+ end
436
404
 
437
- # Definition use fmt-definition if available
438
- if term.fmt_definition
439
- render_ordered_content(term.fmt_definition)
440
- else
441
- # Definition paragraphs
442
- term.p&.each { |para| render_paragraph(para) }
443
- # Structured definitions
444
- term.definition&.each { |defn| render_term_definition(defn) }
405
+ if term.domain && !fmt_definition
406
+ domain_text = safe_attr(term.domain, :text)
407
+ if domain_text
408
+ parts << render_liquid("_term_domain.html.liquid", {
409
+ "text" => escape_html(domain_text),
410
+ })
445
411
  end
412
+ end
446
413
 
447
- # Term notes
448
- term.termnote&.each { |note| render_term_note(note) }
414
+ if fmt_definition
415
+ parts << (render_ordered_content(fmt_definition) || "")
416
+ else
417
+ term_p&.each { |para| parts << (render_paragraph(para) || "") }
418
+ term.definition&.each do |defn|
419
+ parts << (render_term_definition(defn) || "")
420
+ end
421
+ end
449
422
 
450
- # Term examples
451
- term.termexample&.each { |ex| render_term_example(ex) }
423
+ term_note&.each { |note| parts << (render_term_note(note) || "") }
424
+ term_example&.each { |ex| parts << (render_term_example(ex) || "") }
452
425
 
453
- # Source references — use fmt-termsource if available
454
- if term.fmt_termsource && !term.fmt_termsource.empty?
455
- term.fmt_termsource.each do |fts|
456
- @output << "<p class=\"term-source\">"
457
- render_mixed_inline(fts)
458
- @output << "</p>"
459
- end
460
- else
461
- term.source&.each { |src| render_term_source(src) }
462
- term.termsource&.each { |src| render_term_source_element(src) }
426
+ if fmt_termsource && !fmt_termsource.empty?
427
+ fmt_termsource.each do |fts|
428
+ src_content = render_mixed_inline(fts)
429
+ parts << render_liquid("_paragraph.html.liquid", {
430
+ "attrs" => " class=\"term-source\"",
431
+ "content" => src_content,
432
+ })
433
+ end
434
+ else
435
+ term.source&.each { |src| parts << (render_term_source(src) || "") }
436
+ term_termsource&.each do |src|
437
+ parts << (render_term_source_element(src) || "")
463
438
  end
439
+ end
464
440
 
465
- # Admonitions
466
- term.admonition&.each { |adm| render_admonition(adm) }
441
+ term_admonition&.each { |adm| parts << (render_admonition(adm) || "") }
442
+ term_nested&.each { |sub| parts << (render_term(sub) || "") }
467
443
 
468
- # Nested terms
469
- term.term&.each { |sub| render_term(sub) }
470
- end
444
+ render_liquid("_element.html.liquid", "tag" => "div",
445
+ "extra_attrs" => attrs, "content" => parts.join)
471
446
  end
472
447
 
473
448
  def extract_term_name(term)
474
- if term.fmt_preferred && !term.fmt_preferred.empty?
475
- fp = term.fmt_preferred.first
449
+ fmt_pref = safe_attr(term, :fmt_preferred)
450
+ if fmt_pref && !fmt_pref.empty?
451
+ fp = fmt_pref.first
476
452
  if fp.p && !fp.p.empty?
477
453
  return extract_plain_text(fp.p.first)
478
454
  end
@@ -485,49 +461,47 @@ module Metanorma
485
461
  end
486
462
 
487
463
  def extract_term_definition(term)
488
- if term.fmt_definition
489
- rendered = capture_output do
490
- render_ordered_content(term.fmt_definition)
491
- end
492
- return strip_html(rendered).gsub(/\s+/, " ").strip
493
- end
494
- if term.p && !term.p.empty?
495
- rendered = capture_output do
496
- term.p.each do |para|
497
- render_paragraph(para)
498
- end
499
- end
500
- return strip_html(rendered).gsub(/\s+/, " ").strip
464
+ fmt_def = safe_attr(term, :fmt_definition)
465
+ return extract_plain_text(fmt_def) if fmt_def
466
+
467
+ term_p = safe_attr(term, :p)
468
+ if term_p && !term_p.empty?
469
+ text = term_p.map { |para| extract_plain_text(para) }.join(" ")
470
+ return text.strip unless text.strip.empty?
501
471
  end
502
472
  nil
503
473
  end
504
474
 
505
- def strip_html(html)
506
- html.gsub(/<[^>]+>/, "").gsub("&lt;", "<").gsub("&gt;", ">")
507
- .gsub("&amp;", "&").gsub("&nbsp;", " ")
508
- end
509
-
510
475
  def render_term_designation(designation, type)
511
476
  css_class = type == "deprecated" ? "term-deprecated" : "term-name"
512
- @output << "<p class=\"#{css_class}\" style=\"text-align:left;\">"
513
- @output << "<del>" if type == "deprecated"
514
- @output << "<b><dfn>"
515
-
516
477
  name = extract_designation_name(designation)
517
- if name
518
- @output << escape_html(name)
519
- else
520
- # Fallback: render mixed content (e.g. <strong> element)
521
- render_mixed_inline(designation)
478
+ inner = if name
479
+ escape_html(name)
480
+ else
481
+ render_mixed_inline(designation) || ""
482
+ end
483
+
484
+ dfn_content = render_liquid("_element.html.liquid",
485
+ { "tag" => "dfn", "extra_attrs" => "",
486
+ "content" => inner })
487
+ b_content = render_liquid("_element.html.liquid",
488
+ { "tag" => "b", "extra_attrs" => "",
489
+ "content" => dfn_content })
490
+
491
+ if type == "deprecated"
492
+ b_content = render_liquid("_element.html.liquid",
493
+ { "tag" => "del", "extra_attrs" => "",
494
+ "content" => b_content })
522
495
  end
523
496
 
524
- @output << "</dfn></b>"
525
- @output << "</del>" if type == "deprecated"
526
- @output << "</p>"
497
+ render_liquid("_element.html.liquid", {
498
+ "tag" => "p",
499
+ "extra_attrs" => " class=\"#{css_class}\" style=\"text-align:left;\"",
500
+ "content" => b_content,
501
+ })
527
502
  end
528
503
 
529
504
  def extract_designation_name(designation)
530
- # Try expression.name first (TermNameElement objects with text)
531
505
  expr = designation.expression
532
506
  if expr.is_a?(Metanorma::IsoDocument::Terms::TermExpression) && expr.name
533
507
  names = Array(expr.name)
@@ -535,7 +509,6 @@ module Metanorma
535
509
  return text unless text.strip.empty?
536
510
  end
537
511
 
538
- # Try text attribute from mixed content
539
512
  texts = safe_attr(designation, :text)
540
513
  if texts
541
514
  joined = texts.is_a?(Array) ? texts.join : texts.to_s
@@ -558,113 +531,82 @@ module Metanorma
558
531
  end
559
532
 
560
533
  def render_term_definition(definition)
561
- return unless definition
534
+ return nil unless definition
562
535
 
536
+ parts = []
563
537
  rendered = false
564
538
 
565
- # Try verbal_definition first (ISO semantic format)
566
- vd = definition.verbal_definition
539
+ vd = safe_attr(definition,
540
+ :verbal_definition) || safe_attr(definition,
541
+ :verbalexpression)
567
542
  if vd
568
- vd.p&.each { |para| render_paragraph(para) }
569
- vd.ul&.each { |ul| render_unordered_list(ul) }
570
- vd.ol&.each { |ol| render_ordered_list(ol) }
543
+ vd_p = safe_attr(vd, :p) || safe_attr(vd, :paragraph)
544
+ vd_p&.each { |para| parts << (render_paragraph(para) || "") }
545
+ vd.ul&.each { |ul| parts << (render_unordered_list(ul) || "") }
546
+ vd.ol&.each { |ol| parts << (render_ordered_list(ol) || "") }
571
547
  rendered = true
572
548
  end
573
549
 
574
- # Try direct p (presentation XML format where <p> is inside <definition>)
575
550
  p_children = safe_attr(definition, :p)
576
551
  if !rendered && p_children && !p_children.empty?
577
- p_children.each { |para| render_paragraph(para) }
552
+ p_children.each { |para| parts << (render_paragraph(para) || "") }
578
553
  rendered = true
579
554
  end
580
555
 
581
- # Fallback: render via element_order
582
556
  unless rendered
583
- render_ordered_content(definition)
557
+ parts << (render_ordered_content(definition) || "")
584
558
  end
559
+ parts.join
585
560
  end
586
561
 
587
562
  # --- Boilerplate rendering ---
588
563
 
589
564
  def render_boilerplate(boilerplate, **_opts)
590
- return unless boilerplate
591
-
592
- content = boilerplate_to_xml(boilerplate)
593
- return unless content
594
-
595
- @output << "<div class=\"boilerplate-copyright\">"
565
+ return nil unless boilerplate
596
566
 
597
- raw = content.to_s
598
- clean = convert_boilerplate_links(raw)
599
- .gsub(/<fmt-title[^>]*>.*?<\/fmt-title>/m, "")
600
- .gsub(/<semx[^>]*>.*?<\/semx>/m, "")
601
- .gsub(/<title[^>]*>.*?<\/title>/m, "")
602
- .gsub(/<fmt-xref-label[^>]*>.*?<\/fmt-xref-label>/m, "")
603
- .gsub(/<variant-title[^>]*>.*?<\/variant-title>/m, "")
604
- .gsub(/<\/?(?:copyright-statement|clause)[^>]*>/, "")
567
+ parts = []
568
+ parts << (render_boilerplate_items(boilerplate.copyright_statement) || "")
569
+ parts << (render_boilerplate_items(boilerplate.license_statement) || "")
570
+ parts << (render_boilerplate_items(boilerplate.legal_statement) || "")
571
+ parts << (render_boilerplate_items(boilerplate.feedback_statement) || "")
572
+ parts << (render_boilerplate_items(boilerplate.clause) || "")
605
573
 
606
- # Remap XML class names to HTML-specific class names
607
- boilerplate_doc = Nokogiri::HTML::DocumentFragment.parse(clean)
608
- boilerplate_doc.css("[class]").each do |el|
609
- el["class"] = el["class"].split(/\s+/).map do |c|
610
- html_class_for_span(c)
611
- end.join(" ")
612
- end
574
+ render_liquid("_boilerplate.html.liquid", content: parts.join)
575
+ end
613
576
 
614
- @output << boilerplate_doc.inner_html.strip
577
+ def render_boilerplate_items(items)
578
+ return nil unless items
615
579
 
616
- @output << "</div>"
580
+ Array(items).filter_map { |item| render_boilerplate_clause(item) }.join
617
581
  end
618
582
 
619
- def boilerplate_to_xml(boilerplate)
620
- parts = []
621
- %i[copyright_statement license_statement legal_statement
622
- feedback_statement clause].each do |attr|
623
- items = boilerplate.public_send(attr)
624
- next unless items
625
-
626
- tag = attr.to_s.gsub("_", "-")
627
- items.each do |item|
628
- inner = item.to_xml
629
- next if !inner || inner.strip.empty?
630
-
631
- # Strip the outer <clause> wrapper — the inner content is what matters
632
- stripped = inner.strip
633
- stripped = stripped.sub(%r{^<clause[^>]*>\n?}, "").sub(%r{\n?</clause>$}, "")
634
- parts << "<#{tag}>#{stripped}</#{tag}>"
635
- end
636
- end
637
- return nil if parts.empty?
583
+ def render_boilerplate_clause(item)
584
+ return nil unless item
585
+ return nil unless item.is_a?(Lutaml::Model::Serializable)
638
586
 
639
- parts.join("\n")
640
- end
587
+ inner = safe_attr(item, :subsection)
588
+ targets = if inner && !Array(inner).empty?
589
+ Array(inner)
590
+ else
591
+ [item]
592
+ end
641
593
 
642
- def convert_boilerplate_links(raw)
643
- doc = Nokogiri::XML.fragment(raw)
644
- doc.css("link").each do |link|
645
- target = link["target"] || link["href"]
646
- next_sib = link.next_sibling
647
- while next_sib.is_a?(Nokogiri::XML::Text) && next_sib.text.strip.empty?
648
- next_sib = next_sib.next_sibling
649
- end
594
+ targets.filter_map do |section|
595
+ render_boilerplate_clause_content(section)
596
+ end.join
597
+ end
650
598
 
651
- display_text = if next_sib&.element? && next_sib.name == "semx"
652
- fmt_link = next_sib.at_css("fmt-link")
653
- if fmt_link
654
- fmt_target = fmt_link["target"] || fmt_link["href"] || target
655
- display_text = fmt_target.to_s.delete_prefix("mailto:")
656
- next_sib.remove
657
- display_text
658
- end
659
- end
599
+ def render_boilerplate_clause_content(section)
600
+ fmt_title = safe_attr(section, :fmt_title)
601
+ parts = []
602
+ section.each_mixed_content do |child|
603
+ next if child.is_a?(String)
604
+ next if is_title_element?(child, section)
605
+ next if child.equal?(fmt_title)
660
606
 
661
- display_text ||= target.to_s.delete_prefix("mailto:")
662
- a_tag = Nokogiri::HTML::DocumentFragment.parse(
663
- "<a href=\"#{CGI.escapeHTML(target.to_s)}\">#{CGI.escapeHTML(display_text)}</a>",
664
- )
665
- link.replace(a_tag)
607
+ parts << (render(child) || "")
666
608
  end
667
- doc.inner_html
609
+ parts.join
668
610
  end
669
611
 
670
612
  # --- Helpers ---
@@ -672,22 +614,18 @@ module Metanorma
672
614
  def render_title(section, level)
673
615
  title_element = safe_attr(section,
674
616
  :fmt_title) || safe_attr(section, :title)
675
- return unless title_element
617
+ return nil unless title_element
676
618
 
677
619
  section_id = safe_attr(section, :id)
678
- title_text = extract_plain_text(title_element)
679
- register_toc_entry(id: section_id, level: level, text: title_text)
620
+ title_content = render_mixed_inline(title_element)
621
+ register_toc_entry(id: section_id, level: level,
622
+ text: extract_plain_text(title_element))
680
623
 
681
624
  h = "h#{[[level, 6].min, 1].max}"
682
- @output << "<#{h}>"
683
- render_mixed_inline(title_element)
684
- @output << "</#{h}>"
625
+ render_liquid("_heading.html.liquid", tag: h, class_attr: "",
626
+ content: title_content)
685
627
  end
686
628
 
687
- # Collect all document-level children (sections, normative refs, annexes,
688
- # bibliography) sorted by displayorder for correct document order.
689
- # Top-level paragraphs in sections (title paragraphs) are excluded —
690
- # they are rendered separately by render_doc_title.
691
629
  def collect_document_children(doc)
692
630
  items = []
693
631
 
@@ -711,6 +649,7 @@ module Metanorma
711
649
  logo_map = publisher_logo_map
712
650
  return [] if publishers.empty? && logo_map.empty?
713
651
 
652
+ dark_logo_map = theme.logos_dark
714
653
  display_pubs = publishers.empty? ? logo_map.keys : publishers
715
654
  display_pubs.filter_map do |pub|
716
655
  filename = logo_map[pub]
@@ -719,10 +658,18 @@ module Metanorma
719
658
  svg = load_logo_svg(filename, height: 48)
720
659
  next unless svg
721
660
 
722
- # White fill for dark cover background
723
- svg = svg.gsub("fill:#00b1ff", "fill:white") if pub == "OGC"
724
- svg = svg.gsub("fill:#e3000f", "fill:white") if pub == "ISO"
725
- svg
661
+ light_span = "<span class=\"cover-logo cover-logo-light\">#{svg}</span>"
662
+
663
+ dark_span = ""
664
+ dark_filename = dark_logo_map[pub]
665
+ if dark_filename
666
+ dark_svg = load_logo_svg(dark_filename, height: 48)
667
+ if dark_svg
668
+ dark_span = "<span class=\"cover-logo cover-logo-dark\">#{dark_svg}</span>"
669
+ end
670
+ end
671
+
672
+ dark_span.empty? ? light_span : "#{light_span}\n#{dark_span}"
726
673
  end
727
674
  end
728
675
  end