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
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "metanorma-frontend",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "build": "vite build",
8
+ "dev": "vite",
9
+ "test": "vitest run",
10
+ "test:watch": "vitest",
11
+ "typecheck": "vue-tsc --noEmit"
12
+ },
13
+ "dependencies": {
14
+ "@types/prismjs": "^1.26.6",
15
+ "pinia": "^2.1.7",
16
+ "prismjs": "^1.30.0",
17
+ "vue": "^3.4.21"
18
+ },
19
+ "devDependencies": {
20
+ "@vitejs/plugin-vue": "^5.0.4",
21
+ "@vue/test-utils": "^2.4.8",
22
+ "autoprefixer": "^10.5.0",
23
+ "happy-dom": "^20.9.0",
24
+ "postcss": "^8.5.9",
25
+ "puppeteer": "^25.1.0",
26
+ "tailwindcss": "^3.4.19",
27
+ "typescript": "^5.4.0",
28
+ "vite": "^5.2.0",
29
+ "vitest": "^4.1.5",
30
+ "vue-tsc": "^2.0.6"
31
+ }
32
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
@@ -0,0 +1,79 @@
1
+ <template>
2
+ <div :class="['h-screen overflow-hidden', ebookStore.getThemeClass()]" :style="ebookStore.getCssVariables()">
3
+ <div id="metanorma-error" class="error-fallback" style="display:none">
4
+ <div class="error-content">
5
+ <svg class="error-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
6
+ <h2>Something went wrong</h2>
7
+ <p class="error-message">An error occurred while rendering this document.</p>
8
+ <button @click="reload" class="error-retry">Reload page</button>
9
+ </div>
10
+ </div>
11
+ <DocumentReader />
12
+ </div>
13
+ </template>
14
+
15
+ <script setup lang="ts">
16
+ import { onMounted } from 'vue'
17
+ import { useEbookStore } from '@/stores/ebookStore'
18
+ import { useDocumentStore } from '@/stores/documentStore'
19
+ import DocumentReader from '@/components/DocumentReader.vue'
20
+
21
+ const ebookStore = useEbookStore()
22
+ const documentStore = useDocumentStore()
23
+
24
+ documentStore.loadFromWindow()
25
+
26
+ onMounted(() => {
27
+ ebookStore.applyTheme()
28
+ })
29
+
30
+ function reload() {
31
+ window.location.reload()
32
+ }
33
+ </script>
34
+
35
+ <style>
36
+ .error-fallback {
37
+ position: fixed;
38
+ inset: 0;
39
+ z-index: 9999;
40
+ background: #fefce8;
41
+ color: #1a1a1a;
42
+ display: flex;
43
+ align-items: center;
44
+ justify-content: center;
45
+ }
46
+ .error-content {
47
+ text-align: center;
48
+ max-width: 400px;
49
+ padding: 2rem;
50
+ }
51
+ .error-icon {
52
+ width: 48px;
53
+ height: 48px;
54
+ margin: 0 auto 1rem;
55
+ color: #f59e0b;
56
+ }
57
+ .error-content h2 {
58
+ font-size: 1.25rem;
59
+ font-weight: 700;
60
+ margin-bottom: 0.5rem;
61
+ }
62
+ .error-message {
63
+ font-size: 0.9rem;
64
+ color: #6b7280;
65
+ margin-bottom: 1.5rem;
66
+ }
67
+ .error-retry {
68
+ padding: 8px 20px;
69
+ font-size: 0.85rem;
70
+ font-weight: 600;
71
+ border-radius: 8px;
72
+ background: #0d9488;
73
+ color: #fff;
74
+ cursor: pointer;
75
+ }
76
+ .error-retry:hover {
77
+ background: #0f766e;
78
+ }
79
+ </style>
@@ -0,0 +1,21 @@
1
+ import { createApp } from 'vue'
2
+ import { createPinia } from 'pinia'
3
+ import App from './App.vue'
4
+ import './styles.css'
5
+
6
+ const pinia = createPinia()
7
+
8
+ const app = createApp(App)
9
+ app.use(pinia)
10
+
11
+ app.config.errorHandler = (err, _instance, info) => {
12
+ console.error(`[Metanorma] ${info}:`, err)
13
+ const el = document.getElementById('metanorma-error')
14
+ if (el) {
15
+ el.style.display = 'flex'
16
+ const msg = el.querySelector('.error-message')
17
+ if (msg) msg.textContent = err instanceof Error ? err.message : String(err)
18
+ }
19
+ }
20
+
21
+ app.mount('#metanorma-app')
@@ -0,0 +1,77 @@
1
+ <template>
2
+ <aside :class="['sidebar', { 'sidebar-open': uiStore.sidebarOpen }]">
3
+ <div class="sidebar-header">
4
+ <h2 class="sidebar-title">{{ documentStore.title }}</h2>
5
+ <button @click="uiStore.closeSidebar" class="sidebar-close" title="Close sidebar">
6
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
7
+ </button>
8
+ </div>
9
+ <nav class="sidebar-nav">
10
+ <ul class="toc-list">
11
+ <TocItem v-for="item in documentStore.sections" :key="item.id" :item="item" />
12
+ </ul>
13
+ </nav>
14
+ </aside>
15
+ </template>
16
+
17
+ <script setup lang="ts">
18
+ import { useDocumentStore } from '@/stores/documentStore'
19
+ import { useUiStore } from '@/stores/uiStore'
20
+ import TocItem from './TocItem.vue'
21
+
22
+ const documentStore = useDocumentStore()
23
+ const uiStore = useUiStore()
24
+ </script>
25
+
26
+ <style scoped>
27
+ .sidebar {
28
+ position: fixed;
29
+ top: 0;
30
+ left: 0;
31
+ width: 280px;
32
+ height: 100vh;
33
+ background: var(--chrome-bg);
34
+ border-right: 1px solid var(--chrome-border);
35
+ transform: translateX(-100%);
36
+ transition: transform 0.2s ease;
37
+ z-index: 50;
38
+ display: flex;
39
+ flex-direction: column;
40
+ overflow: hidden;
41
+ }
42
+ .sidebar-open {
43
+ transform: translateX(0);
44
+ }
45
+ @media (min-width: 1024px) {
46
+ .sidebar-open { transform: translateX(0); }
47
+ }
48
+ .sidebar-header {
49
+ display: flex;
50
+ align-items: center;
51
+ justify-content: space-between;
52
+ padding: 12px 16px;
53
+ border-bottom: 1px solid var(--chrome-border);
54
+ min-height: 56px;
55
+ }
56
+ .sidebar-title {
57
+ font-size: 0.85rem;
58
+ font-weight: 700;
59
+ color: var(--chrome-text);
60
+ overflow: hidden;
61
+ text-overflow: ellipsis;
62
+ white-space: nowrap;
63
+ max-width: 200px;
64
+ }
65
+ .sidebar-close {
66
+ display: flex;
67
+ align-items: center;
68
+ justify-content: center;
69
+ color: var(--chrome-text-dim);
70
+ padding: 4px;
71
+ border-radius: 6px;
72
+ cursor: pointer;
73
+ }
74
+ .sidebar-close:hover { background: var(--chrome-bg-hover); }
75
+ .sidebar-nav { flex: 1; overflow-y: auto; padding: 8px 0; }
76
+ .toc-list { list-style: none; padding: 0; margin: 0; }
77
+ </style>
@@ -0,0 +1,180 @@
1
+ <template>
2
+ <a href="#main-content" class="skip-link" @click.prevent="skipToContent">Skip to content</a>
3
+
4
+ <ReadingProgressBar :progress="readingProgress" :visible="ebookStore.showProgress" />
5
+
6
+ <div v-if="uiStore.sidebarOpen" @click="uiStore.closeSidebar" class="fixed inset-0 bg-black/50 z-40 lg:hidden"></div>
7
+
8
+ <AppSidebar v-show="!ebookStore.focusMode || uiStore.sidebarOpen" />
9
+
10
+ <div :class="['focus-mode-topbar', { 'focus-mode-topbar--hidden': ebookStore.focusMode && !uiStore.sidebarOpen }]">
11
+ <TopBar
12
+ :title="documentStore.title"
13
+ :sidebar-open="uiStore.sidebarOpen"
14
+ @toggle-toc="ebookStore.toggleToc"
15
+ @toggle-settings="ebookStore.toggleSettings"
16
+ />
17
+ </div>
18
+
19
+ <main ref="mainContent" id="main-content" role="main" :class="[
20
+ 'h-screen overflow-y-auto transition-all duration-200',
21
+ !ebookStore.focusMode ? 'pt-14' : 'pt-0',
22
+ uiStore.sidebarOpen ? 'lg:pl-[280px]' : '',
23
+ ]" @scroll="onScroll">
24
+ <div class="px-6 py-10 lg:px-8 lg:py-12 mx-auto transition-[max-width] duration-300 ease-out" :style="contentStyle">
25
+ <div v-if="documentStore.loadError" class="error-state">
26
+ <svg class="error-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
27
+ <h2>Failed to load document</h2>
28
+ <p class="error-detail">{{ documentStore.loadError }}</p>
29
+ <button @click="retryLoad" class="error-retry">Retry</button>
30
+ </div>
31
+
32
+ <div v-else-if="!documentStore.mirrorDocument" class="loading-state">
33
+ <div class="loading-skeleton">
34
+ <div class="skeleton-title"></div>
35
+ <div class="skeleton-line w-3/4"></div>
36
+ <div class="skeleton-line w-full"></div>
37
+ <div class="skeleton-line w-5/6"></div>
38
+ </div>
39
+ </div>
40
+
41
+ <template v-else>
42
+ <div v-if="documentStore.mirrorDocument" class="mn-content" :class="{ 'mn-content--reveal': contentRevealed }">
43
+ <MirrorRenderer :blocks="documentStore.mirrorDocument.content || []" :top="true" />
44
+ </div>
45
+
46
+ <footer class="doc-fin">
47
+ <div class="doc-fin-rule"></div>
48
+ <h3 class="doc-fin-title">End of document</h3>
49
+ <button @click="scrollToTop" class="doc-fin-btn">Back to top</button>
50
+ <p class="doc-fin-credit">Generated by <a href="https://github.com/metanorma" class="doc-fin-credit-link">Metanorma</a></p>
51
+ </footer>
52
+ </template>
53
+ </div>
54
+ </main>
55
+
56
+ <Transition name="back-to-top-fade">
57
+ <button v-if="showBackToTop" class="back-to-top" @click="scrollToTop" title="Back to top">
58
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
59
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"/>
60
+ </svg>
61
+ </button>
62
+ </Transition>
63
+ </template>
64
+
65
+ <script setup lang="ts">
66
+ import { onMounted, onUnmounted, computed, ref, watch, nextTick } from 'vue'
67
+ import { useDocumentStore } from '@/stores/documentStore'
68
+ import { useUiStore } from '@/stores/uiStore'
69
+ import { useEbookStore } from '@/stores/ebookStore'
70
+ import AppSidebar from '@/components/AppSidebar.vue'
71
+ import MirrorRenderer from '@/components/MirrorRenderer.vue'
72
+ import TopBar from '@/components/TopBar.vue'
73
+ import ReadingProgressBar from '@/components/ReadingProgressBar.vue'
74
+
75
+ const documentStore = useDocumentStore()
76
+ const uiStore = useUiStore()
77
+ const ebookStore = useEbookStore()
78
+ const mainContent = ref<HTMLElement | null>(null)
79
+
80
+ const contentRevealed = ref(false)
81
+ const readingProgress = ref(0)
82
+ const showBackToTop = ref(false)
83
+
84
+ const contentStyle = computed(() => {
85
+ const vars = ebookStore.getCssVariables()
86
+ return { maxWidth: vars['--ebook-max-width'], margin: '0 auto' }
87
+ })
88
+
89
+ watch(() => documentStore.mirrorDocument, (doc) => {
90
+ if (doc) {
91
+ nextTick(() => { contentRevealed.value = true })
92
+ }
93
+ }, { immediate: true })
94
+
95
+ function onScroll() {
96
+ if (!mainContent.value) return
97
+ const el = mainContent.value
98
+ const progress = el.scrollTop / (el.scrollHeight - el.clientHeight) * 100
99
+ readingProgress.value = Math.min(100, Math.max(0, progress))
100
+ showBackToTop.value = el.scrollTop > 400
101
+
102
+ const sections = el.querySelectorAll('[id]')
103
+ for (let i = sections.length - 1; i >= 0; i--) {
104
+ const section = sections[i] as HTMLElement
105
+ if (section.getBoundingClientRect().top <= 120) {
106
+ uiStore.setActiveSection(section.id)
107
+ break
108
+ }
109
+ }
110
+ }
111
+
112
+ function scrollToTop() {
113
+ mainContent.value?.scrollTo({ top: 0, behavior: 'smooth' })
114
+ }
115
+
116
+ function skipToContent() {
117
+ mainContent.value?.focus()
118
+ mainContent.value?.scrollTo({ top: 0 })
119
+ }
120
+
121
+ function retryLoad() {
122
+ documentStore.loadFromWindow()
123
+ }
124
+
125
+ onMounted(() => {
126
+ document.addEventListener('keydown', handleKeydown)
127
+ if (window.innerWidth >= 1024) uiStore.openSidebar()
128
+ })
129
+
130
+ onUnmounted(() => {
131
+ document.removeEventListener('keydown', handleKeydown)
132
+ })
133
+
134
+ function handleKeydown(e: KeyboardEvent) {
135
+ if (e.key === 'Escape') {
136
+ ebookStore.closeAll()
137
+ } else if (e.key === 'f' && !e.ctrlKey && !e.metaKey) {
138
+ const tag = (e.target as HTMLElement).tagName
139
+ if (tag !== 'INPUT' && tag !== 'TEXTAREA') {
140
+ ebookStore.toggleFocusMode()
141
+ }
142
+ }
143
+ }
144
+ </script>
145
+
146
+ <style scoped>
147
+ .skip-link { position: absolute; top: -100px; left: 0; background: var(--chrome-accent, #0d9488); color: #fff; padding: 8px 16px; z-index: 100; font-size: 0.85rem; font-weight: 600; border-radius: 0 0 8px 0; text-decoration: none; transition: top 0.1s; }
148
+ .skip-link:focus { top: 0; }
149
+ :deep(.mn-content--reveal) { animation: content-reveal 0.5s cubic-bezier(0.16, 1, 0.3, 1) both; }
150
+ @keyframes content-reveal { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
151
+
152
+ .doc-fin { margin-top: 6rem; padding: 3rem 1rem 2rem; display: flex; flex-direction: column; align-items: center; text-align: center; }
153
+ .doc-fin-rule { width: 100px; height: 1px; background: linear-gradient(to right, transparent, color-mix(in srgb, var(--ebook-accent) 40%, var(--ebook-border)), transparent); margin-bottom: 2rem; }
154
+ .doc-fin-title { font-family: ui-monospace, monospace; font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.14em; color: var(--ebook-text-muted); margin: 0 0 1.5rem; }
155
+ .doc-fin-btn { padding: 9px 20px; font-size: 0.78rem; font-weight: 500; border-radius: 8px; border: 1px solid var(--ebook-border); color: var(--ebook-text); background: transparent; cursor: pointer; }
156
+ .doc-fin-btn:hover { background: var(--ebook-bg-secondary); }
157
+ .doc-fin-credit { font-size: 0.6rem; color: var(--ebook-text-muted); opacity: 0.4; margin: 1rem 0 0; }
158
+ .doc-fin-credit-link { color: var(--ebook-text-muted); text-decoration: none; }
159
+ .doc-fin-credit-link:hover { text-decoration: underline; }
160
+
161
+ .error-state { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 50vh; gap: 0.75rem; text-align: center; }
162
+ .error-icon { width: 40px; height: 40px; color: #f59e0b; }
163
+ .error-state h2 { font-size: 1.1rem; font-weight: 600; color: var(--chrome-text); }
164
+ .error-detail { font-size: 0.85rem; color: var(--chrome-text-dim); max-width: 300px; }
165
+ .error-retry { margin-top: 0.5rem; padding: 8px 20px; font-size: 0.8rem; font-weight: 600; border-radius: 8px; background: var(--chrome-accent, #0d9488); color: #fff; cursor: pointer; }
166
+ .error-retry:hover { opacity: 0.9; }
167
+
168
+ .loading-skeleton { max-width: 500px; margin: 0 auto; padding-top: 2rem; }
169
+ .skeleton-title { height: 32px; width: 60%; border-radius: 6px; background: var(--chrome-bg-hover); margin-bottom: 1.5rem; animation: skeleton-pulse 1.5s ease-in-out infinite; }
170
+ .skeleton-line { height: 14px; border-radius: 4px; background: var(--chrome-bg-hover); margin-bottom: 0.6rem; animation: skeleton-pulse 1.5s ease-in-out infinite; }
171
+ @keyframes skeleton-pulse { 0%, 100% { opacity: 0.4; } 50% { opacity: 1; } }
172
+
173
+ .focus-mode-topbar { transition: opacity 0.2s ease; }
174
+ .focus-mode-topbar--hidden { opacity: 0; pointer-events: none; }
175
+
176
+ .back-to-top { position: fixed; bottom: 24px; right: 24px; width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; background: var(--chrome-bg-glass); color: var(--chrome-text); border: 1px solid var(--chrome-border); backdrop-filter: blur(8px); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12); cursor: pointer; z-index: 35; transition: background 0.15s ease, transform 0.15s ease; }
177
+ .back-to-top:hover { background: var(--chrome-bg-hover); transform: translateY(-2px); }
178
+ .back-to-top-fade-enter-active, .back-to-top-fade-leave-active { transition: opacity 0.2s ease, transform 0.2s ease; }
179
+ .back-to-top-fade-enter-from, .back-to-top-fade-leave-to { opacity: 0; transform: translateY(16px); }
180
+ </style>
@@ -0,0 +1,23 @@
1
+ <template>
2
+ <div class="content-blocks">
3
+ <template v-for="(block, index) in blocks" :key="index">
4
+ <MirrorRenderer v-if="block.type === 'doc'" :blocks="block.content || []" />
5
+ <TextRenderer v-else-if="block.type === 'text'" :node="block as any" />
6
+ <ClauseBlock v-else-if="isSectionType(block.type)" :block="block" />
7
+ <br v-else-if="block.type === 'soft_break'" />
8
+ <component v-else :is="getBlockComponent(block.type)" :block="block" />
9
+ </template>
10
+ </div>
11
+ </template>
12
+
13
+ <script setup lang="ts">
14
+ import type { MirrorBlockNode, MirrorTextNode } from '@/stores/documentStore'
15
+ import ClauseBlock from './blocks/ClauseBlock.vue'
16
+ import TextRenderer from './TextRenderer.vue'
17
+ import { getBlockComponent } from './blocks'
18
+ import { isSectionType } from '@/utils/typeMetadata'
19
+
20
+ defineProps<{
21
+ blocks: (MirrorBlockNode | MirrorTextNode)[]
22
+ }>()
23
+ </script>
@@ -0,0 +1,29 @@
1
+ <template>
2
+ <div v-if="visible" class="reading-progress">
3
+ <div class="reading-progress-bar" :style="{ width: `${progress}%` }"></div>
4
+ </div>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ defineProps<{
9
+ progress: number
10
+ visible: boolean
11
+ }>()
12
+ </script>
13
+
14
+ <style scoped>
15
+ .reading-progress {
16
+ position: fixed;
17
+ top: 56px;
18
+ left: 0;
19
+ right: 0;
20
+ height: 3px;
21
+ background: var(--chrome-border);
22
+ z-index: 39;
23
+ }
24
+ .reading-progress-bar {
25
+ height: 100%;
26
+ background: var(--chrome-accent, #0d9488);
27
+ transition: width 0.1s ease-out;
28
+ }
29
+ </style>
@@ -0,0 +1,131 @@
1
+ <template>
2
+ <component :is="wrapInMarks(node.marks || [])">
3
+ {{ node.text }}
4
+ </component>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import { h, type VNode, type Component } from 'vue'
9
+
10
+ interface Mark {
11
+ type: string
12
+ attrs?: Record<string, any>
13
+ }
14
+
15
+ interface MirrorTextNode {
16
+ type: 'text'
17
+ text: string
18
+ marks?: Mark[]
19
+ }
20
+
21
+ defineProps<{
22
+ node: MirrorTextNode
23
+ }>()
24
+
25
+ function markToTag(mark: Mark): string {
26
+ switch (mark.type) {
27
+ case 'emphasis':
28
+ return 'em'
29
+ case 'strong':
30
+ return 'strong'
31
+ case 'subscript':
32
+ return 'sub'
33
+ case 'superscript':
34
+ return 'sup'
35
+ case 'code':
36
+ return 'code'
37
+ case 'underline':
38
+ return 'u'
39
+ case 'strike':
40
+ return 's'
41
+ case 'link':
42
+ case 'xref':
43
+ case 'eref':
44
+ return 'a'
45
+ case 'footnote':
46
+ return 'sup'
47
+ case 'stem':
48
+ case 'concept':
49
+ case 'bcp14':
50
+ case 'span':
51
+ default:
52
+ return 'span'
53
+ }
54
+ }
55
+
56
+ function markAttrs(mark: Mark): Record<string, any> {
57
+ switch (mark.type) {
58
+ case 'link':
59
+ return {
60
+ href: mark.attrs?.href || '#',
61
+ class: 'link-text hover:underline',
62
+ target: '_blank',
63
+ rel: 'noopener noreferrer',
64
+ }
65
+ case 'xref':
66
+ return {
67
+ href: `#${mark.attrs?.target || ''}`,
68
+ class: 'xref link-text',
69
+ }
70
+ case 'eref':
71
+ return {
72
+ class: 'eref link-text',
73
+ cite: mark.attrs?.citeas || '',
74
+ }
75
+ case 'emphasis':
76
+ return { class: 'italic ebook-text' }
77
+ case 'strong':
78
+ return { class: 'font-bold heading-text' }
79
+ case 'code':
80
+ return { class: 'inline-code px-1.5 py-0.5 rounded text-sm font-mono border' }
81
+ case 'footnote':
82
+ return { class: 'footnote-inline' }
83
+ case 'smallcap':
84
+ return { class: 'smallcap' }
85
+ case 'span': {
86
+ const cls = mark.attrs?.class_attr
87
+ return cls ? { class: cls } : {}
88
+ }
89
+ default:
90
+ return {}
91
+ }
92
+ }
93
+
94
+ function wrapInMarks(marks: Mark[]): Component {
95
+ return {
96
+ setup(_, { slots }) {
97
+ return () => {
98
+ if (marks.length === 0) {
99
+ return h('span', {}, slots.default?.())
100
+ }
101
+
102
+ let inner: VNode | null = null
103
+ for (let i = marks.length - 1; i >= 0; i--) {
104
+ const mark = marks[i]
105
+ const tag = markToTag(mark)
106
+ const attrs = markAttrs(mark)
107
+
108
+ if (inner) {
109
+ inner = h(tag, attrs, inner)
110
+ } else {
111
+ inner = h(tag, attrs, slots.default?.())
112
+ }
113
+ }
114
+
115
+ return inner || h('span', {}, slots.default?.())
116
+ }
117
+ },
118
+ }
119
+ }
120
+ </script>
121
+
122
+ <style scoped>
123
+ .ebook-text { color: var(--ebook-text); }
124
+ .heading-text { color: var(--ebook-text-heading); }
125
+ .link-text { color: var(--ebook-link-color); transition: color 0.15s ease; }
126
+ .xref { text-decoration: none; border-bottom: 1px dashed color-mix(in srgb, var(--ebook-link-color) 40%, transparent); }
127
+ .xref:hover { border-bottom-color: var(--ebook-link-color); border-bottom-style: solid; }
128
+ .inline-code { background: var(--ebook-inline-code-bg); color: var(--ebook-inline-code-text); border-color: var(--ebook-inline-code-border); }
129
+ .footnote-inline { font-size: 0.75em; }
130
+ .smallcap { font-variant: small-caps; }
131
+ </style>
@@ -0,0 +1,57 @@
1
+ <template>
2
+ <li>
3
+ <a
4
+ :href="`#${item.id}`"
5
+ :class="['toc-link', { 'toc-active': isActive }]"
6
+ @click.prevent="navigate"
7
+ >
8
+ <span v-if="number" class="toc-number">{{ number }}</span>
9
+ {{ item.title }}
10
+ </a>
11
+ <ul v-if="item.children.length > 0" class="toc-list">
12
+ <TocItem v-for="child in item.children" :key="child.id" :item="child" />
13
+ </ul>
14
+ </li>
15
+ </template>
16
+
17
+ <script setup lang="ts">
18
+ import { computed } from 'vue'
19
+ import { useDocumentStore, type TocItem as TocItemType } from '@/stores/documentStore'
20
+ import { useUiStore } from '@/stores/uiStore'
21
+
22
+ const props = defineProps<{ item: TocItemType }>()
23
+
24
+ const documentStore = useDocumentStore()
25
+ const uiStore = useUiStore()
26
+
27
+ const isActive = computed(() => uiStore.activeSectionId === props.item.id)
28
+ const number = computed(() => documentStore.getNumbering(props.item.id))
29
+
30
+ function navigate() {
31
+ const el = document.getElementById(props.item.id)
32
+ if (el) {
33
+ el.scrollIntoView({ behavior: 'smooth', block: 'start' })
34
+ history.replaceState(null, '', `#${props.item.id}`)
35
+ }
36
+ if (window.innerWidth < 1024) uiStore.closeSidebar()
37
+ }
38
+ </script>
39
+
40
+ <style scoped>
41
+ .toc-list { list-style: none; padding-left: 12px; margin: 0; }
42
+ .toc-link {
43
+ display: flex;
44
+ align-items: baseline;
45
+ gap: 6px;
46
+ padding: 6px 16px;
47
+ font-size: 0.82rem;
48
+ color: var(--chrome-text-dim);
49
+ text-decoration: none;
50
+ border-radius: 6px;
51
+ transition: background 0.1s ease, color 0.1s ease;
52
+ cursor: pointer;
53
+ }
54
+ .toc-link:hover { background: var(--chrome-bg-hover); color: var(--chrome-text); }
55
+ .toc-active { color: var(--chrome-accent, #0d9488); background: color-mix(in srgb, var(--chrome-accent, #0d9488) 15%, transparent); }
56
+ .toc-number { font-family: ui-monospace, monospace; font-size: 0.75rem; color: var(--chrome-text-muted); min-width: 2em; }
57
+ </style>