metanorma-document 0.2.9 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (394) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +72 -81
  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 +12 -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 +284 -1057
  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 -14
  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/flavor.rb +45 -0
  228. data/lib/metanorma/html/flavor_registry.rb +49 -0
  229. data/lib/metanorma/html/generator.rb +108 -31
  230. data/lib/metanorma/html/icc_renderer.rb +0 -39
  231. data/lib/metanorma/html/iec_renderer.rb +1 -44
  232. data/lib/metanorma/html/ieee_renderer.rb +1 -45
  233. data/lib/metanorma/html/ietf_renderer.rb +1 -48
  234. data/lib/metanorma/html/iho_renderer.rb +1 -46
  235. data/lib/metanorma/html/iso_renderer.rb +290 -343
  236. data/lib/metanorma/html/itu_renderer.rb +1 -45
  237. data/lib/metanorma/html/ogc_renderer.rb +15 -76
  238. data/lib/metanorma/html/oiml_renderer.rb +18 -38
  239. data/lib/metanorma/html/pdfa_renderer.rb +0 -39
  240. data/lib/metanorma/html/renderer_delegation.rb +21 -0
  241. data/lib/metanorma/html/renderers/block_renderer.rb +372 -0
  242. data/lib/metanorma/html/renderers/element_order_traversal.rb +34 -0
  243. data/lib/metanorma/html/renderers/inline_renderer.rb +642 -0
  244. data/lib/metanorma/html/renderers/pubid_renderer.rb +47 -0
  245. data/lib/metanorma/html/renderers/section_renderer.rb +193 -0
  246. data/lib/metanorma/html/renderers.rb +10 -0
  247. data/lib/metanorma/html/ribose_renderer.rb +1 -45
  248. data/lib/metanorma/html/standard_renderer.rb +344 -230
  249. data/lib/metanorma/html/templates/_audio.html.liquid +1 -0
  250. data/lib/metanorma/html/templates/_biblio_entry.html.liquid +1 -0
  251. data/lib/metanorma/html/templates/_bipm_cover.html.liquid +27 -0
  252. data/lib/metanorma/html/templates/_boilerplate.html.liquid +1 -0
  253. data/lib/metanorma/html/templates/_bookmark.html.liquid +1 -0
  254. data/lib/metanorma/html/templates/_br.html.liquid +1 -0
  255. data/lib/metanorma/html/templates/_definition_list.html.liquid +6 -0
  256. data/lib/metanorma/html/templates/_element.html.liquid +1 -0
  257. data/lib/metanorma/html/templates/_fn_marker.html.liquid +1 -0
  258. data/lib/metanorma/html/templates/_footnotes.html.liquid +6 -6
  259. data/lib/metanorma/html/templates/_formula.html.liquid +1 -1
  260. data/lib/metanorma/html/templates/_header.html.liquid +1 -1
  261. data/lib/metanorma/html/templates/_heading.html.liquid +1 -0
  262. data/lib/metanorma/html/templates/_image.html.liquid +1 -0
  263. data/lib/metanorma/html/templates/_inline_span.html.liquid +1 -0
  264. data/lib/metanorma/html/templates/_iso_cover.html.liquid +1 -1
  265. data/lib/metanorma/html/templates/_link.html.liquid +1 -0
  266. data/lib/metanorma/html/templates/_list.html.liquid +3 -0
  267. data/lib/metanorma/html/templates/_list_item.html.liquid +1 -0
  268. data/lib/metanorma/html/templates/_main_content.html.liquid +1 -0
  269. data/lib/metanorma/html/templates/_math_container.html.liquid +1 -0
  270. data/lib/metanorma/html/templates/_paragraph.html.liquid +1 -0
  271. data/lib/metanorma/html/templates/_prefatory_section.html.liquid +1 -0
  272. data/lib/metanorma/html/templates/_pubid_identifier.html.liquid +1 -0
  273. data/lib/metanorma/html/templates/_quote.html.liquid +4 -0
  274. data/lib/metanorma/html/templates/_ref_date.html.liquid +1 -0
  275. data/lib/metanorma/html/templates/_ref_title.html.liquid +1 -0
  276. data/lib/metanorma/html/templates/_section.html.liquid +5 -0
  277. data/lib/metanorma/html/templates/_standard_cover.html.liquid +31 -0
  278. data/lib/metanorma/html/templates/_standard_doc_title.html.liquid +3 -0
  279. data/lib/metanorma/html/templates/_stem_span.html.liquid +1 -0
  280. data/lib/metanorma/html/templates/_table.html.liquid +9 -0
  281. data/lib/metanorma/html/templates/_term_domain.html.liquid +1 -0
  282. data/lib/metanorma/html/templates/_term_example.html.liquid +1 -0
  283. data/lib/metanorma/html/templates/_term_note.html.liquid +1 -0
  284. data/lib/metanorma/html/templates/_term_number.html.liquid +1 -0
  285. data/lib/metanorma/html/templates/_term_source.html.liquid +1 -0
  286. data/lib/metanorma/html/templates/_term_text_note.html.liquid +1 -0
  287. data/lib/metanorma/html/templates/_video.html.liquid +1 -0
  288. data/lib/metanorma/html/templates/_wrapped_preface.html.liquid +1 -0
  289. data/lib/metanorma/html/templates/document.html.liquid +4 -0
  290. data/lib/metanorma/html/theme.rb +180 -107
  291. data/lib/metanorma/html.rb +16 -0
  292. data/lib/metanorma/ietf_document/metadata/ietf_bib_data_extension_type.rb +0 -25
  293. data/lib/metanorma/ietf_document/metadata/pi_settings.rb +32 -0
  294. data/lib/metanorma/ietf_document/metadata.rb +2 -0
  295. data/lib/metanorma/iso_document/metadata/doctype_element.rb +20 -0
  296. data/lib/metanorma/iso_document/metadata/iso_bib_data_extension_type.rb +0 -48
  297. data/lib/metanorma/iso_document/metadata/iso_bibliographic_item.rb +0 -72
  298. data/lib/metanorma/iso_document/metadata/iso_document_status.rb +0 -30
  299. data/lib/metanorma/iso_document/metadata/iso_localized_title.rb +4 -1
  300. data/lib/metanorma/iso_document/metadata/language_element.rb +18 -0
  301. data/lib/metanorma/iso_document/metadata/project_number.rb +22 -0
  302. data/lib/metanorma/iso_document/metadata/script_element.rb +18 -0
  303. data/lib/metanorma/iso_document/metadata/stage_element.rb +26 -0
  304. data/lib/metanorma/iso_document/metadata/stagename_element.rb +18 -0
  305. data/lib/metanorma/iso_document/metadata/structured_identifier.rb +0 -17
  306. data/lib/metanorma/iso_document/metadata/title_collection.rb +1 -0
  307. data/lib/metanorma/iso_document/metadata.rb +9 -4
  308. data/lib/metanorma/iso_document/terms/iso_term.rb +0 -13
  309. data/lib/metanorma/iso_document/terms/term_domain_element.rb +18 -0
  310. data/lib/metanorma/iso_document/terms.rb +1 -0
  311. data/lib/metanorma/itu_document/metadata/itu_bib_data_extension_type.rb +0 -37
  312. data/lib/metanorma/itu_document/metadata/meeting_date.rb +18 -0
  313. data/lib/metanorma/itu_document/metadata/meeting_element.rb +18 -0
  314. data/lib/metanorma/itu_document/metadata/study_period.rb +18 -0
  315. data/lib/metanorma/itu_document/metadata.rb +3 -4
  316. data/lib/metanorma/mirror/default_registry.rb +170 -0
  317. data/lib/metanorma/mirror/handler_registry.rb +60 -0
  318. data/lib/metanorma/mirror/handler_result.rb +37 -0
  319. data/lib/metanorma/mirror/handlers/admonition.rb +18 -0
  320. data/lib/metanorma/mirror/handlers/example.rb +21 -0
  321. data/lib/metanorma/mirror/handlers/figure.rb +53 -0
  322. data/lib/metanorma/mirror/handlers/formula.rb +35 -0
  323. data/lib/metanorma/mirror/handlers/inline/catalog.rb +57 -0
  324. data/lib/metanorma/mirror/handlers/inline/rich_html_renderer.rb +159 -0
  325. data/lib/metanorma/mirror/handlers/inline/text_extractor.rb +116 -0
  326. data/lib/metanorma/mirror/handlers/inline.rb +226 -0
  327. data/lib/metanorma/mirror/handlers/list.rb +110 -0
  328. data/lib/metanorma/mirror/handlers/note.rb +26 -0
  329. data/lib/metanorma/mirror/handlers/paragraph.rb +19 -0
  330. data/lib/metanorma/mirror/handlers/quote.rb +16 -0
  331. data/lib/metanorma/mirror/handlers/review.rb +19 -0
  332. data/lib/metanorma/mirror/handlers/section.rb +149 -0
  333. data/lib/metanorma/mirror/handlers/sourcecode.rb +27 -0
  334. data/lib/metanorma/mirror/handlers/structural.rb +68 -0
  335. data/lib/metanorma/mirror/handlers/table.rb +81 -0
  336. data/lib/metanorma/mirror/handlers/term.rb +100 -0
  337. data/lib/metanorma/mirror/handlers.rb +60 -0
  338. data/lib/metanorma/mirror/id_strategy/positional.rb +166 -0
  339. data/lib/metanorma/mirror/id_strategy/preserve.rb +12 -0
  340. data/lib/metanorma/mirror/id_strategy.rb +34 -0
  341. data/lib/metanorma/mirror/math_util.rb +53 -0
  342. data/lib/metanorma/mirror/metadata.rb +37 -0
  343. data/lib/metanorma/mirror/model/container.rb +47 -0
  344. data/lib/metanorma/mirror/model/factory.rb +52 -0
  345. data/lib/metanorma/mirror/model/guide.rb +25 -0
  346. data/lib/metanorma/mirror/model/leaf.rb +17 -0
  347. data/lib/metanorma/mirror/model/mark.rb +53 -0
  348. data/lib/metanorma/mirror/model/node.rb +47 -0
  349. data/lib/metanorma/mirror/model/soft_break.rb +33 -0
  350. data/lib/metanorma/mirror/model/text.rb +34 -0
  351. data/lib/metanorma/mirror/model.rb +16 -0
  352. data/lib/metanorma/mirror/output/builder.rb +39 -0
  353. data/lib/metanorma/mirror/output/formats/base_format.rb +90 -0
  354. data/lib/metanorma/mirror/output/formats/inline_format.rb +81 -0
  355. data/lib/metanorma/mirror/output/formats.rb +53 -0
  356. data/lib/metanorma/mirror/output/html_renderer.rb +95 -0
  357. data/lib/metanorma/mirror/output/html_renderers/block_renderers.rb +178 -0
  358. data/lib/metanorma/mirror/output/html_renderers/inline_renderer.rb +61 -0
  359. data/lib/metanorma/mirror/output/html_renderers/list_renderers.rb +96 -0
  360. data/lib/metanorma/mirror/output/html_renderers/mark_renderers.rb +41 -0
  361. data/lib/metanorma/mirror/output/html_renderers/section_renderers.rb +129 -0
  362. data/lib/metanorma/mirror/output/html_renderers/structural_renderers.rb +80 -0
  363. data/lib/metanorma/mirror/output/html_renderers/table_renderers.rb +87 -0
  364. data/lib/metanorma/mirror/output/html_renderers.rb +76 -0
  365. data/lib/metanorma/mirror/output/pipeline.rb +92 -0
  366. data/lib/metanorma/mirror/output/pipeline_context.rb +22 -0
  367. data/lib/metanorma/mirror/output.rb +14 -0
  368. data/lib/metanorma/mirror/rewriter.rb +121 -0
  369. data/lib/metanorma/mirror/safe_attr.rb +16 -0
  370. data/lib/metanorma/mirror/serialization/json_serializer.rb +25 -0
  371. data/lib/metanorma/mirror/serialization/yaml_serializer.rb +20 -0
  372. data/lib/metanorma/mirror/serialization.rb +10 -0
  373. data/lib/metanorma/mirror/transformer.rb +110 -0
  374. data/lib/metanorma/mirror.rb +31 -0
  375. data/lib/metanorma/standard_document/annotation.rb +37 -0
  376. data/lib/metanorma/standard_document/block_attributes.rb +15 -2
  377. data/lib/metanorma/standard_document/blocks/block_source.rb +0 -23
  378. data/lib/metanorma/standard_document/blocks/source_origin_element.rb +23 -0
  379. data/lib/metanorma/standard_document/blocks.rb +1 -1
  380. data/lib/metanorma/standard_document/terms/term_expression.rb +0 -31
  381. data/lib/metanorma/standard_document/terms/term_name_element.rb +29 -0
  382. data/lib/metanorma/standard_document/terms.rb +1 -0
  383. data/script/render_both.rb +18 -0
  384. metadata +272 -13
  385. data/.github/workflows/rake.yml +0 -21
  386. data/.github/workflows/release.yml +0 -25
  387. data/.github/workflows/roundtrip-samples.yml +0 -132
  388. data/.gitignore +0 -19
  389. data/.rspec +0 -3
  390. data/.rubocop.yml +0 -22
  391. data/Gemfile +0 -28
  392. data/bin/console +0 -15
  393. data/bin/setup +0 -8
  394. data/metanorma-document.gemspec +0 -35
@@ -2,8 +2,6 @@
2
2
 
3
3
  module Metanorma
4
4
  module Html
5
- # Renders StandardDocument components to HTML.
6
- # Extends BaseRenderer with terms, bibliography, and standard sections.
7
5
  class StandardRenderer < BaseRenderer
8
6
  register_render Metanorma::StandardDocument::Root,
9
7
  :render_standard_document
@@ -31,46 +29,32 @@ module Metanorma
31
29
  register_render Metanorma::StandardDocument::Blocks::AmendBlock,
32
30
  :render_amend_block
33
31
 
34
- # --- Top-level document rendering ---
35
-
36
32
  def render_standard_document(doc, **_opts)
37
- render_coverpage(doc)
38
-
39
- @output << "<main class=\"main-section\">"
40
-
41
- # Preface sections
42
- render(doc.preface) if doc.preface
43
-
44
- # Document title line
45
- render_doc_title(doc)
46
-
47
- # Main sections
48
- render(doc.sections) if doc.sections
33
+ cover = render_coverpage(doc)
49
34
 
50
- # Annexes
51
- doc.annex&.each { |annex| render(annex) }
35
+ content_parts = []
36
+ content_parts << (render(doc.preface) || "") if doc.preface
37
+ content_parts << (render_doc_title(doc) || "")
38
+ content_parts << (render(doc.sections) || "") if doc.sections
39
+ doc.annex&.each { |annex| content_parts << (render(annex) || "") }
40
+ content_parts << (render(doc.bibliography) || "") if doc.bibliography
52
41
 
53
- # Bibliography
54
- render(doc.bibliography) if doc.bibliography
55
-
56
- # Index section (from collected index terms)
57
42
  unless @index_term_collector.empty?
58
43
  index_component = Component::IndexSection.new(self)
59
- index_component.render(@index_term_collector)
44
+ content_parts << (index_component.render(@index_term_collector) || "")
60
45
  end
61
46
 
62
- # Index section from indexsect element (presentation XML)
63
- render(doc.indexsect) if doc.indexsect
64
-
65
- # Footnotes section
66
- render_footnotes_section
47
+ content_parts << (render(doc.indexsect) || "") if doc.indexsect
48
+ content_parts << (render_footnotes_section || "")
67
49
 
68
- @output << "</main>"
50
+ cover + render_liquid("_main_content.html.liquid", {
51
+ "content" => content_parts.join,
52
+ })
69
53
  end
70
54
 
71
55
  def render_coverpage(doc)
72
56
  bibdata = doc.bibdata
73
- return unless bibdata
57
+ return "" unless bibdata
74
58
 
75
59
  cover_id = nil
76
60
  bibdata.doc_identifier&.each do |di|
@@ -83,63 +67,46 @@ module Metanorma
83
67
  break
84
68
  end
85
69
 
86
- title_text = nil
87
- if bibdata.is_a?(Metanorma::IsoDocument::Metadata::IsoBibliographicItem)
88
- en_title = bibdata.title_for("en")
89
- title_text = en_title.to_s if en_title
90
- elsif bibdata.is_a?(Metanorma::Document::Components::BibData::BibData)
91
- titles = bibdata.title
92
- if titles && !titles.empty?
93
- en_title = titles.find { |t| t.language == "en" }
94
- title_text = extract_text_value(en_title) if en_title
95
- end
96
- end
70
+ title_text = extract_display_title(bibdata)
97
71
 
98
- @output << render_liquid("_cover.html.liquid", {
99
- "doc_id" => cover_id,
100
- "title" => title_text,
101
- })
72
+ render_liquid("_cover.html.liquid", {
73
+ "doc_id" => cover_id,
74
+ "title" => title_text,
75
+ })
102
76
  end
103
77
 
104
78
  def render_doc_title(doc)
105
79
  bibdata = doc.bibdata
106
- return unless bibdata
80
+ return nil unless bibdata
107
81
 
108
- if bibdata.is_a?(Metanorma::IsoDocument::Metadata::IsoBibliographicItem)
109
- en_title = bibdata.title_for("en")
110
- return unless en_title
111
- elsif bibdata.is_a?(Metanorma::Document::Components::BibData::BibData)
112
- titles = bibdata.title
113
- return unless titles && !titles.empty?
82
+ title = extract_display_title(bibdata)
83
+ return nil unless title
114
84
 
115
- en_title = titles.find { |t| t.language == "en" }
116
- return unless en_title
117
-
118
- en_title = extract_text_value(en_title)
119
- else
120
- return
121
- end
122
-
123
- @output << render_liquid("_doc_title.html.liquid", {
124
- "title" => en_title,
125
- })
85
+ render_liquid("_doc_title.html.liquid", {
86
+ "title" => title,
87
+ })
126
88
  end
127
89
 
128
- # --- Section rendering ---
129
-
130
90
  def render_section(section, level: 1, title_class: nil,
131
- with_subsections: false, with_terms: false)
91
+ with_subsections: false, with_terms: false)
132
92
  attrs = element_attrs(id: safe_attr(section, :id))
133
- tag("div", attrs) do
134
- if title_class
135
- render_standard_title(section, level, default_class: title_class)
136
- else
137
- render_standard_title(section, level)
138
- end
139
- render_standard_section_blocks(section, level)
140
- render_subsections(section, level) if with_subsections
141
- section.terms&.each { |term| render_term(term) } if with_terms
93
+ parts = []
94
+ parts << if title_class
95
+ render_standard_title(section, level,
96
+ default_class: title_class) || ""
97
+ else
98
+ render_standard_title(section, level) || ""
99
+ end
100
+ parts << (render_standard_section_blocks(section, level) || "")
101
+ parts << (render_subsections(section, level) || "") if with_subsections
102
+ if with_terms
103
+ section.terms&.each { |term| parts << (render_term(term) || "") }
142
104
  end
105
+ render_liquid("_element.html.liquid", {
106
+ "tag" => "div",
107
+ "extra_attrs" => attrs,
108
+ "content" => parts.join,
109
+ })
143
110
  end
144
111
 
145
112
  def render_clause_section(section, level: 1, **)
@@ -174,70 +141,94 @@ with_subsections: false, with_terms: false)
174
141
  level = safe_attr(title_node, :level) || 1
175
142
  h = "h#{[[level, 6].min, 1].max}"
176
143
  attrs = element_attrs(id: safe_attr(title_node, :id))
177
- @output << "<#{h}#{attrs}>#{escape_html(title_node.text.to_s)}</#{h}>"
144
+ render_liquid("_heading.html.liquid", {
145
+ "tag" => h,
146
+ "class_attr" => attrs,
147
+ "content" => escape_html(title_node.text.to_s),
148
+ })
178
149
  end
179
150
 
180
151
  def render_amend_block(amend, **_opts)
181
152
  attrs = element_attrs(id: safe_attr(amend, :id))
182
- tag("div", attrs) do
183
- render_mixed_inline(amend)
184
- end
153
+ content = render_mixed_inline(amend)
154
+ render_liquid("_element.html.liquid", {
155
+ "tag" => "div",
156
+ "extra_attrs" => attrs,
157
+ "content" => content,
158
+ })
185
159
  end
186
160
 
187
161
  # --- Term rendering ---
188
162
 
189
163
  def render_term(term, **_opts)
190
164
  attrs = element_attrs(id: safe_attr(term, :id))
191
- tag("div", attrs) do
192
- term.preferred&.each { |d| render_term_designation(d, "preferred") }
193
- term.admitted&.each { |d| render_term_designation(d, "admitted") }
194
- term.deprecates&.each { |d| render_term_designation(d, "deprecated") }
195
-
196
- # Domain
197
- if term.domain
198
- domain_text = if term.domain.is_a?(String)
199
- term.domain
200
- else
201
- safe_attr(
202
- term.domain, :text
203
- ).to_s
204
- end
205
- @output << "<p class=\"term-domain\">&lt;#{escape_html(domain_text)}&gt;</p>" unless domain_text.to_s.empty?
206
- end
165
+ parts = []
166
+ term.preferred&.each do |d|
167
+ parts << (render_term_designation(d, "preferred") || "")
168
+ end
169
+ term.admitted&.each do |d|
170
+ parts << (render_term_designation(d, "admitted") || "")
171
+ end
172
+ term.deprecates&.each do |d|
173
+ parts << (render_term_designation(d, "deprecated") || "")
174
+ end
207
175
 
208
- # Definitions
209
- if term.definition
210
- Array(term.definition).each do |defn|
211
- render_term_definition(defn)
212
- end
176
+ if term.domain
177
+ domain_text = if term.domain.is_a?(String)
178
+ term.domain
179
+ else
180
+ safe_attr(term.domain, :text).to_s
181
+ end
182
+ unless domain_text.to_s.empty?
183
+ parts << render_liquid("_term_domain.html.liquid", {
184
+ "text" => escape_html(domain_text),
185
+ }).to_s
213
186
  end
187
+ end
214
188
 
215
- # Notes (mapped from <termnote>)
216
- term.note&.each_with_index do |note, i|
217
- if note.is_a?(String)
218
- @output << "<div class=\"note-block\"><p><span class=\"term-note-label\">Note #{i + 1} to entry: </span>#{escape_html(note)}</p></div>"
219
- else
220
- render_note(note)
221
- end
189
+ if term.definition
190
+ Array(term.definition).each do |defn|
191
+ parts << (render_term_definition(defn) || "")
222
192
  end
193
+ end
223
194
 
224
- # Examples (mapped from <termexample>)
225
- term.example&.each { |ex| render_paragraph(ex) }
226
-
227
- # Source references
228
- term.source&.each { |src| render_term_source(src) }
195
+ term.note&.each_with_index do |note, i|
196
+ if note.is_a?(String)
197
+ label = "Note #{i + 1} to entry: "
198
+ parts << render_liquid("_term_text_note.html.liquid", {
199
+ "label" => label,
200
+ "content" => escape_html(note),
201
+ }).to_s
202
+ else
203
+ parts << (render_note(note) || "")
204
+ end
229
205
  end
206
+
207
+ term.example&.each { |ex| parts << (render_paragraph(ex) || "") }
208
+ term.source&.each { |src| parts << (render_term_source(src) || "") }
209
+ render_liquid("_element.html.liquid", {
210
+ "tag" => "div",
211
+ "extra_attrs" => attrs,
212
+ "content" => parts.join,
213
+ })
230
214
  end
231
215
 
232
216
  def render_term_designation(designation, _type)
233
217
  name = extract_designation_name(designation)
234
- return unless name
235
-
236
- tag("p", " class=\"term-name\" style=\"text-align:left;\"") do
237
- tag("b") do
238
- tag("dfn") { @output << escape_html(name) }
239
- end
240
- end
218
+ return nil unless name
219
+
220
+ inner = escape_html(name)
221
+ dfn = render_liquid("_element.html.liquid",
222
+ { "tag" => "dfn", "extra_attrs" => "",
223
+ "content" => inner })
224
+ bold = render_liquid("_element.html.liquid",
225
+ { "tag" => "b", "extra_attrs" => "",
226
+ "content" => dfn })
227
+ render_liquid("_element.html.liquid", {
228
+ "tag" => "p",
229
+ "extra_attrs" => " class=\"term-name\" style=\"text-align:left;\"",
230
+ "content" => bold,
231
+ })
241
232
  end
242
233
 
243
234
  def extract_designation_name(designation)
@@ -254,143 +245,224 @@ with_subsections: false, with_terms: false)
254
245
  end
255
246
 
256
247
  def render_term_definition(definition)
257
- return unless definition
258
- return unless definition.is_a?(Metanorma::StandardDocument::Terms::TermDefinition)
248
+ return nil unless definition
249
+ return nil unless definition.is_a?(Metanorma::StandardDocument::Terms::TermDefinition)
259
250
 
260
251
  ve = definition.verbalexpression
261
- return unless ve
252
+ return nil unless ve
262
253
 
263
- ve.paragraph&.each { |para| render_paragraph(para) }
254
+ parts = []
255
+ ve.paragraph&.each { |para| parts << (render_paragraph(para) || "") }
256
+ parts.join
264
257
  end
265
258
 
266
259
  def render_term_note(note)
267
260
  attrs = element_attrs(id: safe_attr(note, :id), class: "note-block")
268
- tag("div", attrs) do
269
- label = extract_termnote_label(note)
270
- @output << "<p><span class=\"term-note-label\">#{escape_html(label)}: </span>"
271
- note.p&.each { |para| render_mixed_inline(para) }
272
- @output << "</p>"
273
- note.ul&.each { |ul| render_unordered_list(ul) }
274
- note.ol&.each { |ol| render_ordered_list(ol) }
275
- note.dl&.then { |dl| render_definition_list(dl) }
261
+ label = extract_termnote_label(note)
262
+ parts = []
263
+ note_content_parts = []
264
+ note.p&.each do |para|
265
+ note_content_parts << (render_mixed_inline(para) || "")
276
266
  end
267
+ note_content = note_content_parts.join
268
+ parts << render_liquid("_term_note.html.liquid", {
269
+ "label" => escape_html(label),
270
+ "content" => note_content,
271
+ })
272
+ note.ul&.each { |ul| parts << (render_unordered_list(ul) || "") }
273
+ note.ol&.each { |ol| parts << (render_ordered_list(ol) || "") }
274
+ note.dl&.then { |dl| parts << (render_definition_list(dl) || "") }
275
+ render_liquid("_element.html.liquid", {
276
+ "tag" => "div",
277
+ "extra_attrs" => attrs,
278
+ "content" => parts.join,
279
+ })
277
280
  end
278
281
 
279
282
  def render_term_example(example)
280
283
  attrs = element_attrs(id: safe_attr(example, :id), class: "example")
281
- tag("div", attrs) do
282
- label = extract_block_label(example, "EXAMPLE")
283
- @output << "<p><span class=\"example-label\">#{escape_html(label)}</span>&nbsp;"
284
- example.p&.each { |para| render_mixed_inline(para) }
285
- @output << "</p>"
286
- example.ul&.each { |ul| render_unordered_list(ul) }
287
- example.ol&.each { |ol| render_ordered_list(ol) }
288
- example.dl&.then { |dl| render_definition_list(dl) }
284
+ label = extract_block_label(example, "EXAMPLE")
285
+ parts = []
286
+ ex_content_parts = []
287
+ example.p&.each do |para|
288
+ ex_content_parts << (render_mixed_inline(para) || "")
289
289
  end
290
+ ex_content = ex_content_parts.join
291
+ parts << render_liquid("_term_example.html.liquid", {
292
+ "label" => escape_html(label),
293
+ "content" => ex_content,
294
+ })
295
+ example.ul&.each { |ul| parts << (render_unordered_list(ul) || "") }
296
+ example.ol&.each { |ol| parts << (render_ordered_list(ol) || "") }
297
+ example.dl&.then { |dl| parts << (render_definition_list(dl) || "") }
298
+ render_liquid("_element.html.liquid", {
299
+ "tag" => "div",
300
+ "extra_attrs" => attrs,
301
+ "content" => parts.join,
302
+ })
290
303
  end
291
304
 
292
305
  def render_term_source(source)
293
- return unless source
306
+ return nil unless source
294
307
 
295
- @output << "<p class=\"term-source\">[SOURCE: "
308
+ parts = []
296
309
  termsource = safe_attr(source, :termsource)
297
310
  origin = safe_attr(source, :origin)
298
311
 
299
312
  if termsource
300
- render_mixed_inline(termsource)
313
+ parts << (render_mixed_inline(termsource) || "")
301
314
  elsif origin
302
315
  citeas = safe_attr(origin, :citeas)
303
316
  bibitemid = safe_attr(origin, :bibitemid)
304
317
 
305
- if citeas && !citeas.to_s.empty?
306
- @output << if bibitemid && !bibitemid.to_s.empty?
307
- "<a href=\"##{escape_html(bibitemid.to_s)}\" class=\"bibref\">#{escape_html(citeas.to_s)}</a>"
308
- else
309
- escape_html(citeas.to_s)
310
- end
311
- else
312
- render_mixed_inline(origin)
313
- end
318
+ parts << if citeas && !citeas.to_s.empty?
319
+ if bibitemid && !bibitemid.to_s.empty?
320
+ render_liquid("_link.html.liquid", {
321
+ "attrs" => element_attrs(
322
+ href: "##{escape_html(bibitemid.to_s)}", class: "bibref",
323
+ ),
324
+ "content" => escape_html(citeas.to_s),
325
+ })
326
+ else
327
+ escape_html(citeas.to_s)
328
+ end
329
+ else
330
+ render_mixed_inline(origin) || ""
331
+ end
314
332
 
315
333
  modification = safe_attr(source, :modification)
316
334
  if modification && !modification.to_s.empty?
317
- @output << ", modified — #{escape_html(modification.to_s)}"
335
+ parts << ", modified — #{escape_html(modification.to_s)}"
318
336
  end
319
337
  else
320
- render_mixed_inline(source)
338
+ parts << (render_mixed_inline(source) || "")
321
339
  end
322
- @output << "]</p>"
340
+
341
+ render_liquid("_term_source.html.liquid", {
342
+ "content" => parts.join,
343
+ })
323
344
  end
324
345
 
325
346
  def render_term_source_element(element)
326
- return unless element
347
+ return nil unless element
327
348
 
328
- @output << "<p>"
329
- render_mixed_inline(element)
330
- @output << "</p>"
349
+ content = render_mixed_inline(element)
350
+ render_liquid("_paragraph.html.liquid", {
351
+ "attrs" => "",
352
+ "content" => content,
353
+ })
331
354
  end
332
355
 
333
356
  # --- Bibliography / References ---
334
357
 
335
358
  def render_bibliography(bib, level: 1, **_opts)
336
- tag("div", "") do
337
- bib.references&.each do |ref|
338
- render_references_section(ref, level: level)
339
- end
340
- bib.clause&.each { |cl| render(cl, level: level) }
359
+ parts = []
360
+ bib.references&.each do |ref|
361
+ parts << (render_references_section(ref, level: level) || "")
341
362
  end
363
+ bib.clause&.each { |cl| parts << (render(cl, level: level) || "") }
364
+ render_liquid("_element.html.liquid", {
365
+ "tag" => "div",
366
+ "extra_attrs" => "",
367
+ "content" => parts.join,
368
+ })
342
369
  end
343
370
 
344
371
  def render_references_section(section, level: 1, **_opts)
345
372
  is_normative = safe_attr(section, :normative) == "true"
346
373
  attrs = element_attrs(id: safe_attr(section, :id))
347
- tag("div", attrs) do
348
- render_standard_title(section, level,
349
- default_class: is_normative ? "" : "section-sub")
350
- section.p&.each { |para| render_paragraph(para) }
351
- section.note&.each { |note| render_paragraph(note) }
352
- section.references&.each_with_index do |bibitem, i|
353
- render_bibitem(bibitem, i + 1, normative: is_normative)
354
- end
355
- section.table&.each { |t| render_table(t) }
374
+ parts = []
375
+ parts << (render_standard_title(section, level,
376
+ default_class: is_normative ? "" : "section-sub") || "")
377
+ section.p&.each { |para| parts << (render_paragraph(para) || "") }
378
+ section.note&.each { |note| parts << (render_paragraph(note) || "") }
379
+ section.references&.each_with_index do |bibitem, i|
380
+ parts << (render_bibitem(bibitem, i + 1,
381
+ normative: is_normative) || "")
356
382
  end
383
+ section.table&.each { |t| parts << (render_table(t) || "") }
384
+ render_liquid("_element.html.liquid", {
385
+ "tag" => "div",
386
+ "extra_attrs" => attrs,
387
+ "content" => parts.join,
388
+ })
357
389
  end
358
390
 
359
391
  def render_bibitem(item, index, normative: false)
360
392
  css_class = normative ? "norm-ref-entry" : "biblio-entry"
361
- attrs = element_attrs(id: safe_attr(item, :id), class: css_class)
393
+ item_id = safe_attr(item, :id)
362
394
  url = bibitem_url(item)
363
395
 
364
- tag("p", attrs) do
365
- if url
366
- @output << "<a href=\"#{escape_html(url)}\" target=\"_blank\" rel=\"noopener\" class=\"biblio-link\">"
367
- end
396
+ if item.biblio_tag
397
+ prefix, rest = split_biblio_tag(item.biblio_tag)
398
+ ordinal_html = prefix.empty? ? nil : escape_html(prefix)
399
+ pubid_html = rest&.filter_map do |child|
400
+ render_inline_element(child)
401
+ end&.join
402
+ else
403
+ ordinal_html = "[#{index}]"
404
+ pubid_html = nil
405
+ end
368
406
 
369
- # Use biblio-tag from presentation XML if available
370
- if item.biblio_tag
371
- render_mixed_inline(item.biblio_tag)
372
- @output << "&nbsp;"
373
- else
374
- @output << "[#{index}]&nbsp;"
375
- end
407
+ content_html = render_bibitem_content(item)
408
+
409
+ drop = Drops::BiblioEntryDrop.new(
410
+ id: item_id,
411
+ css_class: css_class,
412
+ ordinal_html: ordinal_html,
413
+ pubid_html: pubid_html,
414
+ url: url ? escape_html(url) : nil,
415
+ content_html: content_html,
416
+ )
417
+ render_liquid("_biblio_entry.html.liquid", { "entry" => drop })
418
+ end
376
419
 
377
- render_bibitem_content(item)
420
+ def split_biblio_tag(tag)
421
+ children = []
422
+ tag.each_mixed_content { |c| children << c }
378
423
 
379
- if url
380
- @output << "</a>"
424
+ prefix = +""
425
+ rest = []
426
+ found_boundary = false
427
+
428
+ children.each do |child|
429
+ if found_boundary
430
+ rest << child
431
+ else
432
+ case child
433
+ when Metanorma::Document::Components::Inline::TabElement
434
+ found_boundary = true
435
+ next
436
+ when String
437
+ stripped = child.strip
438
+ if stripped.match?(/\A\[\d+\]\z/)
439
+ prefix << child
440
+ elsif child.match?(/\A\s*\z/)
441
+ prefix << child
442
+ else
443
+ found_boundary = true
444
+ rest << child
445
+ end
446
+ else
447
+ found_boundary = true
448
+ rest << child
449
+ end
381
450
  end
382
451
  end
452
+
453
+ prefix_text = prefix.strip
454
+ [prefix_text, rest.empty? ? nil : rest]
383
455
  end
384
456
 
457
+ PREFERRED_LINK_TYPES = %w[src citation].freeze
458
+
385
459
  def bibitem_url(item)
386
460
  links = Array(item.link)
387
461
  return nil if links.empty?
388
462
 
389
- # Prefer src or citation type, skip RSS feeds
390
- preferred = links.find { |l| ["src", "citation"].include?(l.type) }
463
+ preferred = links.find { |l| PREFERRED_LINK_TYPES.include?(l.type) }
391
464
  return preferred.content.to_s if preferred && !preferred.content.to_s.empty?
392
465
 
393
- # Fallback: first non-RSS link
394
466
  non_rss = links.find do |l|
395
467
  !l.content.to_s.include?(".rss") && !l.content.to_s.empty?
396
468
  end
@@ -398,25 +470,15 @@ with_subsections: false, with_terms: false)
398
470
  end
399
471
 
400
472
  def render_bibitem_content(item)
401
- # Use formatted reference if available (presentation XML)
473
+ parts = []
402
474
  if item.formatted_ref
403
- render_mixed_inline(item.formatted_ref)
404
- return
475
+ parts << (render_mixed_inline(item.formatted_ref) || "")
476
+ return parts.join
405
477
  end
406
478
 
407
- # Fallback: construct from parts
408
- if item.docidentifier && !item.docidentifier.empty?
409
- docids = Array(item.docidentifier)
410
- # Skip bracket-number identifiers (e.g. "[1]") and iso-reference/URN variants;
411
- # render only the primary doc identifier
412
- primary = docids.find do |di|
413
- val = extract_text_value(di).to_s
414
- val.match?(/\A\[?\d+\]?\z/) ? false : !val.match?(/\A(?:iso-reference|URN)\s/)
415
- end
416
- if primary
417
- id_val = extract_text_value(primary)
418
- @output << "<span class=\"ref-doc-number\">#{escape_html(id_val)}</span>" unless id_val.to_s.empty?
419
- end
479
+ rendered_pubid = render_pubid_identifier(item)
480
+ unless rendered_pubid
481
+ render_docidentifier_fallback_into(parts, item)
420
482
  end
421
483
 
422
484
  if item.date && !item.date.empty?
@@ -424,7 +486,10 @@ with_subsections: false, with_terms: false)
424
486
  date_on = date.is_a?(Metanorma::Document::Relaton::BibliographicDate) ? date.on : nil
425
487
  date_val = extract_text_value(date_on || safe_attr(date, :text))
426
488
  if date_val && !date_val.to_s.empty?
427
- @output << ":<span class=\"ref-year\">#{escape_html(date_val.to_s)}</span>"
489
+ parts << render_liquid("_ref_date.html.liquid", {
490
+ "prefix" => ":",
491
+ "year" => escape_html(date_val.to_s),
492
+ })
428
493
  end
429
494
  end
430
495
  end
@@ -435,11 +500,51 @@ with_subsections: false, with_terms: false)
435
500
  safe_attr(t, :type) == "main"
436
501
  end || titles.first
437
502
  if main_title
438
- @output << ", <i>"
439
- render_mixed_inline(main_title)
440
- @output << "</i>"
503
+ title_content = render_mixed_inline(main_title)
504
+ parts << render_liquid("_ref_title.html.liquid", {
505
+ "content" => title_content,
506
+ })
441
507
  end
442
508
  end
509
+ parts.join
510
+ end
511
+
512
+ def render_pubid_identifier(item)
513
+ return nil unless item.docidentifier && !item.docidentifier.empty?
514
+
515
+ docids = Array(item.docidentifier)
516
+ primary = docids.find do |di|
517
+ val = extract_text_value(di).to_s
518
+ val.match?(/\A\[?\d+\]?\z/) ? false : !val.match?(/\A(?:iso-reference|URN)\s/)
519
+ end
520
+ return nil unless primary
521
+
522
+ id_string = extract_text_value(primary).to_s
523
+ return nil if id_string.empty?
524
+
525
+ identifier = parse_pubid(id_string)
526
+ return nil unless identifier
527
+
528
+ pubid_to_html(identifier)
529
+ end
530
+
531
+ def render_docidentifier_fallback_into(parts, item)
532
+ return unless item.docidentifier && !item.docidentifier.empty?
533
+
534
+ docids = Array(item.docidentifier)
535
+ primary = docids.find do |di|
536
+ val = extract_text_value(di).to_s
537
+ val.match?(/\A\[?\d+\]?\z/) ? false : !val.match?(/\A(?:iso-reference|URN)\s/)
538
+ end
539
+ return unless primary
540
+
541
+ id_val = extract_text_value(primary)
542
+ unless id_val.to_s.empty?
543
+ parts << render_liquid("_inline_span.html.liquid", {
544
+ "attrs" => " class=\"ref-doc-number\"",
545
+ "content" => escape_html(id_val),
546
+ })
547
+ end
443
548
  end
444
549
 
445
550
  # --- Standard section helpers ---
@@ -447,9 +552,10 @@ with_subsections: false, with_terms: false)
447
552
  def render_standard_title(section, level, default_class: "")
448
553
  title_element = safe_attr(section,
449
554
  :fmt_title) || safe_attr(section, :title)
450
- return unless title_element
555
+ return nil unless title_element
451
556
 
452
557
  section_id = safe_attr(section, :id)
558
+ title_content = render_mixed_inline(title_element)
453
559
  title_text = extract_plain_text(title_element)
454
560
  register_toc_entry(id: section_id, level: level, text: title_text)
455
561
 
@@ -458,58 +564,67 @@ with_subsections: false, with_terms: false)
458
564
 
459
565
  h = "h#{[[level, 6].min, 1].max}"
460
566
  title_class = default_class.empty? ? "" : " class=\"#{default_class}\""
461
- @output << "<#{h}#{title_class}>"
462
- render_mixed_inline(title_element)
463
- @output << "</#{h}>"
567
+ render_liquid("_heading.html.liquid", {
568
+ "tag" => h,
569
+ "class_attr" => title_class,
570
+ "content" => title_content,
571
+ })
464
572
  end
465
573
 
466
574
  def render_standard_section_blocks(section, level)
467
575
  if section.is_a?(Lutaml::Model::Serializable) && section.mixed?
576
+ parts = []
468
577
  section.each_mixed_content do |node|
469
578
  next if node.is_a?(String)
470
579
  next if is_title_element?(node, section)
471
580
 
472
- render(node, level: level + 1)
581
+ parts << (render(node, level: level + 1) || "")
473
582
  end
583
+ parts.join
474
584
  else
475
585
  render_section_block_collections(section, level)
476
586
  end
477
587
  end
478
588
 
479
589
  def render_section_block_collections(section, level)
480
- # Paragraphs
590
+ parts = []
481
591
  paragraphs = safe_attr(section, :paragraphs) || safe_attr(section, :p)
482
- Array(paragraphs).each { |p| render_paragraph(p) } if paragraphs
592
+ if paragraphs
593
+ Array(paragraphs).each do |p|
594
+ parts << (render_paragraph(p) || "")
595
+ end
596
+ end
483
597
 
484
- # Lists
485
598
  %i[unordered_lists ordered_lists definition_lists].each do |attr|
486
599
  values = safe_attr(section, attr)
487
- Array(values).each { |v| render(v, level: level + 1) } if values
600
+ if values
601
+ Array(values).each do |v|
602
+ parts << (render(v, level: level + 1) || "")
603
+ end
604
+ end
488
605
  end
489
606
 
490
- # Block elements
491
607
  %i[tables figures formulas examples notes admonitions sourcecode_blocks
492
608
  quote_blocks].each do |attr|
493
609
  values = safe_attr(section, attr)
494
- Array(values).each { |v| render(v, level: level + 1) } if values
610
+ if values
611
+ Array(values).each do |v|
612
+ parts << (render(v, level: level + 1) || "")
613
+ end
614
+ end
495
615
  end
616
+ parts.join
496
617
  end
497
618
 
498
619
  def render_subsections(section, level)
499
620
  clauses = safe_attr(section,
500
621
  :clause) || safe_attr(section, :subsections)
501
- Array(clauses).each { |cl| render(cl, level: level + 1) } if clauses
502
- end
503
-
504
- def is_title_element?(node, section)
505
- title = safe_attr(section, :title)
506
- return false unless title
622
+ return nil unless clauses
507
623
 
508
- node.equal?(title)
624
+ clauses.filter_map { |cl| render(cl, level: level + 1) }.join
509
625
  end
510
626
 
511
627
  def extract_termnote_label(note)
512
- # Presentation XML: <name>Note 1 to entry</name>
513
628
  names = safe_attr(note, :name)
514
629
  if names && !names.empty?
515
630
  name = names.is_a?(Array) ? names.first : names
@@ -517,7 +632,6 @@ with_subsections: false, with_terms: false)
517
632
  return text unless text.to_s.strip.empty?
518
633
  end
519
634
 
520
- # Fallback: autonum attribute
521
635
  autonum = safe_attr(note, :autonum)
522
636
  return "Note #{autonum} to entry" if autonum && !autonum.to_s.empty?
523
637