docbook 0.1.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 (271) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/CLAUDE.md +19 -0
  4. data/CODE_OF_CONDUCT.md +10 -0
  5. data/README.adoc +335 -0
  6. data/Rakefile +12 -0
  7. data/docs/.lycheeignore +33 -0
  8. data/docs/Gemfile +10 -0
  9. data/docs/INDEX.adoc +67 -0
  10. data/docs/_config.yml +186 -0
  11. data/docs/advanced/element-classes.adoc +185 -0
  12. data/docs/advanced/frontend-customization.adoc +193 -0
  13. data/docs/advanced/index.adoc +14 -0
  14. data/docs/advanced/templates.adoc +123 -0
  15. data/docs/features/element-coverage.adoc +373 -0
  16. data/docs/features/html-output/data-model.adoc +285 -0
  17. data/docs/features/html-output/directory-mode.adoc +180 -0
  18. data/docs/features/html-output/index.adoc +90 -0
  19. data/docs/features/html-output/single-file-mode.adoc +125 -0
  20. data/docs/features/index-generation.adoc +197 -0
  21. data/docs/features/index.adoc +63 -0
  22. data/docs/features/numbering.adoc +183 -0
  23. data/docs/features/toc-generation.adoc +150 -0
  24. data/docs/features/xinclude/fragid-schemes.adoc +287 -0
  25. data/docs/features/xinclude/index.adoc +119 -0
  26. data/docs/features/xinclude/text-includes.adoc +123 -0
  27. data/docs/features/xinclude/xml-includes.adoc +167 -0
  28. data/docs/getting-started/index.adoc +50 -0
  29. data/docs/getting-started/installation.adoc +113 -0
  30. data/docs/getting-started/quick-start.adoc +161 -0
  31. data/docs/guides/converting-article.adoc +188 -0
  32. data/docs/guides/converting-book.adoc +192 -0
  33. data/docs/guides/index.adoc +12 -0
  34. data/docs/guides/roundtrip-testing.adoc +129 -0
  35. data/docs/interfaces/cli/format-command.adoc +109 -0
  36. data/docs/interfaces/cli/index.adoc +73 -0
  37. data/docs/interfaces/cli/roundtrip-command.adoc +125 -0
  38. data/docs/interfaces/cli/to-html-command.adoc +178 -0
  39. data/docs/interfaces/cli/validate-command.adoc +104 -0
  40. data/docs/interfaces/index.adoc +101 -0
  41. data/docs/interfaces/ruby-api/html-output.adoc +186 -0
  42. data/docs/interfaces/ruby-api/index.adoc +111 -0
  43. data/docs/interfaces/ruby-api/parsing.adoc +202 -0
  44. data/docs/interfaces/ruby-api/xinclude.adoc +162 -0
  45. data/docs/interfaces/ruby-api/xref-resolution.adoc +156 -0
  46. data/docs/lychee.toml +42 -0
  47. data/docs/reference/cli-options.adoc +155 -0
  48. data/docs/reference/content-block-types.adoc +243 -0
  49. data/docs/reference/glossary.adoc +119 -0
  50. data/docs/reference/index.adoc +12 -0
  51. data/docs/reference/supported-elements.adoc +749 -0
  52. data/docs/understanding/architecture.adoc +145 -0
  53. data/docs/understanding/content-pipeline.adoc +102 -0
  54. data/docs/understanding/data-models.adoc +156 -0
  55. data/docs/understanding/index.adoc +34 -0
  56. data/exe/docbook +7 -0
  57. data/frontend/dist/app.css +1 -0
  58. data/frontend/dist/app.iife.js +24 -0
  59. data/frontend/package-lock.json +1445 -0
  60. data/frontend/package.json +22 -0
  61. data/frontend/src/App.vue +230 -0
  62. data/frontend/src/app.ts +8 -0
  63. data/frontend/src/components/AppSidebar.vue +116 -0
  64. data/frontend/src/components/AppendixSection.vue +39 -0
  65. data/frontend/src/components/BlockRenderer.vue +358 -0
  66. data/frontend/src/components/ChapterSection.vue +32 -0
  67. data/frontend/src/components/ContentRenderer.vue +13 -0
  68. data/frontend/src/components/EbookContainer.vue +147 -0
  69. data/frontend/src/components/EbookTopBar.vue +116 -0
  70. data/frontend/src/components/PartSection.vue +44 -0
  71. data/frontend/src/components/ReferenceEntry.vue +80 -0
  72. data/frontend/src/components/SearchModal.vue +286 -0
  73. data/frontend/src/components/SectionContent.vue +31 -0
  74. data/frontend/src/components/SettingsPanel.vue +236 -0
  75. data/frontend/src/components/TocTreeItem.vue +135 -0
  76. data/frontend/src/composables/useEbookStore.ts +191 -0
  77. data/frontend/src/composables/useSearch.ts +249 -0
  78. data/frontend/src/env.d.ts +7 -0
  79. data/frontend/src/stores/documentStore.ts +221 -0
  80. data/frontend/src/stores/uiStore.ts +98 -0
  81. data/frontend/src/styles.css +253 -0
  82. data/frontend/tsconfig.json +24 -0
  83. data/frontend/tsconfig.node.json +11 -0
  84. data/frontend/vite.config.ts +30 -0
  85. data/lib/docbook/cli.rb +123 -0
  86. data/lib/docbook/document.rb +67 -0
  87. data/lib/docbook/elements/abbrev.rb +16 -0
  88. data/lib/docbook/elements/acknowledgements.rb +22 -0
  89. data/lib/docbook/elements/address.rb +18 -0
  90. data/lib/docbook/elements/alt.rb +16 -0
  91. data/lib/docbook/elements/annotation.rb +18 -0
  92. data/lib/docbook/elements/appendix.rb +34 -0
  93. data/lib/docbook/elements/article.rb +31 -0
  94. data/lib/docbook/elements/att.rb +15 -0
  95. data/lib/docbook/elements/attribution.rb +20 -0
  96. data/lib/docbook/elements/audioobject.rb +18 -0
  97. data/lib/docbook/elements/author.rb +18 -0
  98. data/lib/docbook/elements/bibliography.rb +24 -0
  99. data/lib/docbook/elements/bibliomixed.rb +40 -0
  100. data/lib/docbook/elements/biblioref.rb +20 -0
  101. data/lib/docbook/elements/blockquote.rb +26 -0
  102. data/lib/docbook/elements/book.rb +36 -0
  103. data/lib/docbook/elements/buildtarget.rb +16 -0
  104. data/lib/docbook/elements/callout.rb +22 -0
  105. data/lib/docbook/elements/calloutlist.rb +22 -0
  106. data/lib/docbook/elements/caution.rb +30 -0
  107. data/lib/docbook/elements/chapter.rb +62 -0
  108. data/lib/docbook/elements/citation.rb +16 -0
  109. data/lib/docbook/elements/citerefentry.rb +26 -0
  110. data/lib/docbook/elements/citetitle.rb +20 -0
  111. data/lib/docbook/elements/classname.rb +16 -0
  112. data/lib/docbook/elements/code.rb +16 -0
  113. data/lib/docbook/elements/colophon.rb +22 -0
  114. data/lib/docbook/elements/computeroutput.rb +18 -0
  115. data/lib/docbook/elements/copyright.rb +17 -0
  116. data/lib/docbook/elements/danger.rb +28 -0
  117. data/lib/docbook/elements/date.rb +16 -0
  118. data/lib/docbook/elements/dedication.rb +22 -0
  119. data/lib/docbook/elements/dir.rb +16 -0
  120. data/lib/docbook/elements/emphasis.rb +18 -0
  121. data/lib/docbook/elements/entry.rb +30 -0
  122. data/lib/docbook/elements/entrytbl.rb +22 -0
  123. data/lib/docbook/elements/equation.rb +26 -0
  124. data/lib/docbook/elements/example.rb +30 -0
  125. data/lib/docbook/elements/fieldsynopsis.rb +21 -0
  126. data/lib/docbook/elements/figure.rb +28 -0
  127. data/lib/docbook/elements/filename.rb +16 -0
  128. data/lib/docbook/elements/firstname.rb +16 -0
  129. data/lib/docbook/elements/firstterm.rb +18 -0
  130. data/lib/docbook/elements/footnote.rb +22 -0
  131. data/lib/docbook/elements/footnoteref.rb +21 -0
  132. data/lib/docbook/elements/foreignphrase.rb +18 -0
  133. data/lib/docbook/elements/formalpara.rb +20 -0
  134. data/lib/docbook/elements/function.rb +16 -0
  135. data/lib/docbook/elements/glossary.rb +24 -0
  136. data/lib/docbook/elements/glossdef.rb +18 -0
  137. data/lib/docbook/elements/glossentry.rb +26 -0
  138. data/lib/docbook/elements/glosssee.rb +18 -0
  139. data/lib/docbook/elements/glossseealso.rb +18 -0
  140. data/lib/docbook/elements/glossterm.rb +18 -0
  141. data/lib/docbook/elements/holder.rb +16 -0
  142. data/lib/docbook/elements/honorific.rb +16 -0
  143. data/lib/docbook/elements/imagedata.rb +29 -0
  144. data/lib/docbook/elements/imageobject.rb +22 -0
  145. data/lib/docbook/elements/important.rb +30 -0
  146. data/lib/docbook/elements/index.rb +26 -0
  147. data/lib/docbook/elements/indexdiv.rb +22 -0
  148. data/lib/docbook/elements/indexentry.rb +22 -0
  149. data/lib/docbook/elements/indexterm.rb +34 -0
  150. data/lib/docbook/elements/info.rb +25 -0
  151. data/lib/docbook/elements/informalexample.rb +24 -0
  152. data/lib/docbook/elements/informalfigure.rb +20 -0
  153. data/lib/docbook/elements/inlinemediaobject.rb +22 -0
  154. data/lib/docbook/elements/itemizedlist.rb +21 -0
  155. data/lib/docbook/elements/legalnotice.rb +22 -0
  156. data/lib/docbook/elements/link.rb +26 -0
  157. data/lib/docbook/elements/listitem.rb +32 -0
  158. data/lib/docbook/elements/literal.rb +16 -0
  159. data/lib/docbook/elements/literallayout.rb +22 -0
  160. data/lib/docbook/elements/mediaobject.rb +26 -0
  161. data/lib/docbook/elements/msg.rb +20 -0
  162. data/lib/docbook/elements/msgexplan.rb +22 -0
  163. data/lib/docbook/elements/msgset.rb +22 -0
  164. data/lib/docbook/elements/note.rb +30 -0
  165. data/lib/docbook/elements/orderedlist.rb +23 -0
  166. data/lib/docbook/elements/orgname.rb +16 -0
  167. data/lib/docbook/elements/para.rb +61 -0
  168. data/lib/docbook/elements/paragraph_like.rb +18 -0
  169. data/lib/docbook/elements/parameter.rb +16 -0
  170. data/lib/docbook/elements/part.rb +38 -0
  171. data/lib/docbook/elements/personname.rb +22 -0
  172. data/lib/docbook/elements/phrase.rb +20 -0
  173. data/lib/docbook/elements/preface.rb +58 -0
  174. data/lib/docbook/elements/primary.rb +16 -0
  175. data/lib/docbook/elements/procedure.rb +24 -0
  176. data/lib/docbook/elements/productname.rb +18 -0
  177. data/lib/docbook/elements/productnumber.rb +16 -0
  178. data/lib/docbook/elements/programlisting.rb +28 -0
  179. data/lib/docbook/elements/property.rb +16 -0
  180. data/lib/docbook/elements/pubdate.rb +16 -0
  181. data/lib/docbook/elements/publishername.rb +16 -0
  182. data/lib/docbook/elements/quotation.rb +24 -0
  183. data/lib/docbook/elements/quote.rb +18 -0
  184. data/lib/docbook/elements/refentry.rb +32 -0
  185. data/lib/docbook/elements/refentrytitle.rb +16 -0
  186. data/lib/docbook/elements/reference.rb +26 -0
  187. data/lib/docbook/elements/refmeta.rb +29 -0
  188. data/lib/docbook/elements/refmiscinfo.rb +16 -0
  189. data/lib/docbook/elements/refname.rb +16 -0
  190. data/lib/docbook/elements/refnamediv.rb +22 -0
  191. data/lib/docbook/elements/refpurpose.rb +16 -0
  192. data/lib/docbook/elements/refsect1.rb +22 -0
  193. data/lib/docbook/elements/refsect2.rb +22 -0
  194. data/lib/docbook/elements/refsect3.rb +22 -0
  195. data/lib/docbook/elements/refsection.rb +32 -0
  196. data/lib/docbook/elements/releaseinfo.rb +16 -0
  197. data/lib/docbook/elements/remark.rb +20 -0
  198. data/lib/docbook/elements/replaceable.rb +16 -0
  199. data/lib/docbook/elements/row.rb +15 -0
  200. data/lib/docbook/elements/screen.rb +28 -0
  201. data/lib/docbook/elements/secondary.rb +16 -0
  202. data/lib/docbook/elements/sect1.rb +26 -0
  203. data/lib/docbook/elements/sect2.rb +24 -0
  204. data/lib/docbook/elements/sect3.rb +24 -0
  205. data/lib/docbook/elements/sect4.rb +24 -0
  206. data/lib/docbook/elements/sect5.rb +22 -0
  207. data/lib/docbook/elements/section.rb +66 -0
  208. data/lib/docbook/elements/see.rb +16 -0
  209. data/lib/docbook/elements/seealso.rb +18 -0
  210. data/lib/docbook/elements/set.rb +26 -0
  211. data/lib/docbook/elements/setindex.rb +24 -0
  212. data/lib/docbook/elements/sidebar.rb +22 -0
  213. data/lib/docbook/elements/simplesect.rb +32 -0
  214. data/lib/docbook/elements/step.rb +26 -0
  215. data/lib/docbook/elements/substeps.rb +18 -0
  216. data/lib/docbook/elements/subtitle.rb +16 -0
  217. data/lib/docbook/elements/surname.rb +16 -0
  218. data/lib/docbook/elements/table.rb +24 -0
  219. data/lib/docbook/elements/tag.rb +20 -0
  220. data/lib/docbook/elements/tbody.rb +15 -0
  221. data/lib/docbook/elements/term.rb +37 -0
  222. data/lib/docbook/elements/tertiary.rb +16 -0
  223. data/lib/docbook/elements/textobject.rb +18 -0
  224. data/lib/docbook/elements/tfoot.rb +15 -0
  225. data/lib/docbook/elements/tgroup.rb +21 -0
  226. data/lib/docbook/elements/thead.rb +15 -0
  227. data/lib/docbook/elements/tip.rb +30 -0
  228. data/lib/docbook/elements/title.rb +16 -0
  229. data/lib/docbook/elements/toc.rb +24 -0
  230. data/lib/docbook/elements/tocdiv.rb +22 -0
  231. data/lib/docbook/elements/tocentry.rb +20 -0
  232. data/lib/docbook/elements/topic.rb +26 -0
  233. data/lib/docbook/elements/type.rb +16 -0
  234. data/lib/docbook/elements/uri.rb +16 -0
  235. data/lib/docbook/elements/userinput.rb +18 -0
  236. data/lib/docbook/elements/variable.rb +16 -0
  237. data/lib/docbook/elements/variablelist.rb +19 -0
  238. data/lib/docbook/elements/varlistentry.rb +17 -0
  239. data/lib/docbook/elements/version.rb +16 -0
  240. data/lib/docbook/elements/videoobject.rb +18 -0
  241. data/lib/docbook/elements/warning.rb +30 -0
  242. data/lib/docbook/elements/xref.rb +18 -0
  243. data/lib/docbook/elements/year.rb +16 -0
  244. data/lib/docbook/elements.rb +204 -0
  245. data/lib/docbook/models/document_metadata.rb +30 -0
  246. data/lib/docbook/models/document_root.rb +33 -0
  247. data/lib/docbook/models/index_entry.rb +28 -0
  248. data/lib/docbook/models/reading_position.rb +22 -0
  249. data/lib/docbook/models/section_number.rb +18 -0
  250. data/lib/docbook/models/section_root.rb +29 -0
  251. data/lib/docbook/models/toc_node.rb +24 -0
  252. data/lib/docbook/models.rb +16 -0
  253. data/lib/docbook/output/data.rb +196 -0
  254. data/lib/docbook/output/html.rb +1450 -0
  255. data/lib/docbook/output/index_generator.rb +281 -0
  256. data/lib/docbook/output.rb +8 -0
  257. data/lib/docbook/services/document_builder.rb +258 -0
  258. data/lib/docbook/services/element_to_hash.rb +287 -0
  259. data/lib/docbook/services/index_generator.rb +134 -0
  260. data/lib/docbook/services/numbering_service.rb +186 -0
  261. data/lib/docbook/services/toc_generator.rb +138 -0
  262. data/lib/docbook/services.rb +14 -0
  263. data/lib/docbook/templates/document.html.liquid +20 -0
  264. data/lib/docbook/templates/partials/_head.liquid +39 -0
  265. data/lib/docbook/templates/partials/_scripts.liquid +10 -0
  266. data/lib/docbook/version.rb +5 -0
  267. data/lib/docbook/xinclude_resolver.rb +217 -0
  268. data/lib/docbook/xref_resolver.rb +78 -0
  269. data/lib/docbook.rb +17 -0
  270. data/sig/docbook.rbs +4 -0
  271. metadata +385 -0
@@ -0,0 +1,116 @@
1
+ <template>
2
+ <div class="ebook-topbar" :class="{ 'ebook-topbar--with-sidebar': sidebarOpen }">
3
+ <div class="flex items-center justify-between px-4 py-2">
4
+ <!-- Left: Menu button -->
5
+ <button
6
+ @click="$emit('toggle-toc')"
7
+ class="p-2 rounded-lg hover:bg-black/10 dark:hover:bg-white/10 text-gray-700 dark:text-gray-300 transition-colors"
8
+ >
9
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
10
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
11
+ </svg>
12
+ </button>
13
+
14
+ <!-- Center: Title -->
15
+ <div class="flex-1 text-center px-4">
16
+ <h1 class="text-sm font-medium text-gray-700 dark:text-gray-300 truncate">
17
+ {{ title }}
18
+ </h1>
19
+ </div>
20
+
21
+ <!-- Right: Actions -->
22
+ <div class="flex items-center gap-1">
23
+ <!-- Font settings -->
24
+ <button
25
+ @click="$emit('toggle-settings')"
26
+ class="p-2 rounded-lg hover:bg-black/10 dark:hover:bg-white/10 text-gray-700 dark:text-gray-300 transition-colors"
27
+ title="Display settings"
28
+ >
29
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
30
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7"/>
31
+ </svg>
32
+ </button>
33
+
34
+ <!-- Theme toggle -->
35
+ <button
36
+ @click="cycleTheme"
37
+ class="p-2 rounded-lg hover:bg-black/10 dark:hover:bg-white/10 text-gray-700 dark:text-gray-300 transition-colors"
38
+ :title="`Theme: ${currentTheme}`"
39
+ >
40
+ <svg v-if="currentTheme === 'day'" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
41
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/>
42
+ </svg>
43
+ <svg v-else-if="currentTheme === 'sepia'" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
44
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
45
+ </svg>
46
+ <svg v-else-if="currentTheme === 'night'" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
47
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"/>
48
+ </svg>
49
+ <svg v-else class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
50
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
51
+ </svg>
52
+ </button>
53
+ </div>
54
+ </div>
55
+ </div>
56
+ </template>
57
+
58
+ <script setup lang="ts">
59
+ import { computed } from 'vue'
60
+ import { useEbookStore, type Theme } from '@/composables/useEbookStore'
61
+
62
+ defineProps<{
63
+ title?: string
64
+ sidebarOpen?: boolean
65
+ }>()
66
+
67
+ defineEmits<{
68
+ 'toggle-toc': []
69
+ 'toggle-settings': []
70
+ }>()
71
+
72
+ const ebookStore = useEbookStore()
73
+ const currentTheme = computed(() => ebookStore.theme.value)
74
+
75
+ const themeOrder: Theme[] = ['day', 'sepia', 'night', 'oled']
76
+
77
+ function cycleTheme() {
78
+ const currentIndex = themeOrder.indexOf(ebookStore.theme.value)
79
+ const nextIndex = (currentIndex + 1) % themeOrder.length
80
+ ebookStore.setTheme(themeOrder[nextIndex])
81
+ }
82
+ </script>
83
+
84
+ <style scoped>
85
+ .ebook-topbar {
86
+ position: fixed;
87
+ top: 0;
88
+ left: 0;
89
+ right: 0;
90
+ z-index: 30;
91
+ background: rgba(255, 255, 255, 0.9);
92
+ backdrop-filter: blur(8px);
93
+ border-bottom: 1px solid rgba(0, 0, 0, 0.05);
94
+ transition: left 0.2s ease;
95
+ }
96
+
97
+ :global(.dark) .ebook-topbar {
98
+ background: rgba(17, 24, 39, 0.9);
99
+ border-bottom-color: rgba(255, 255, 255, 0.1);
100
+ }
101
+
102
+ .ebook-topbar--with-sidebar {
103
+ left: 280px;
104
+ }
105
+
106
+ @media (max-width: 1023px) {
107
+ .ebook-topbar--with-sidebar {
108
+ left: 0;
109
+ }
110
+ }
111
+
112
+ .ebook-topbar button {
113
+ min-width: 44px;
114
+ min-height: 44px;
115
+ }
116
+ </style>
@@ -0,0 +1,44 @@
1
+ <template>
2
+ <section :id="section.id" class="mb-12 scroll-mt-20">
3
+ <h1 class="text-2xl font-bold mb-6 pb-2 border-b-2 border-gray-300 dark:border-gray-600">
4
+ {{ section.title }}
5
+ </h1>
6
+ <BlockRenderer v-if="sectionContent" :blocks="sectionContent.blocks" />
7
+ <!-- Render children -->
8
+ <template v-if="section.children && section.children.length > 0">
9
+ <div v-for="child in section.children" :key="child.id">
10
+ <component :is="getChildComponent(child.type)" :section="child" />
11
+ </div>
12
+ </template>
13
+ </section>
14
+ </template>
15
+
16
+ <script setup lang="ts">
17
+ import { computed, markRaw } from 'vue'
18
+ import { useDocumentStore, type TocItem } from '@/stores/documentStore'
19
+ import SectionContent from '@/components/SectionContent.vue'
20
+ import ChapterSection from '@/components/ChapterSection.vue'
21
+ import AppendixSection from '@/components/AppendixSection.vue'
22
+ import BlockRenderer from '@/components/BlockRenderer.vue'
23
+
24
+ interface Props {
25
+ section: TocItem
26
+ }
27
+
28
+ const props = defineProps<Props>()
29
+ const documentStore = useDocumentStore()
30
+
31
+ const sectionContent = computed(() => documentStore.getSectionContent(props.section.id))
32
+
33
+ function getChildComponent(type: string) {
34
+ switch (type) {
35
+ case 'chapter':
36
+ case 'reference':
37
+ return markRaw(ChapterSection)
38
+ case 'appendix':
39
+ return markRaw(AppendixSection)
40
+ default:
41
+ return markRaw(SectionContent)
42
+ }
43
+ }
44
+ </script>
@@ -0,0 +1,80 @@
1
+ <template>
2
+ <div class="reference-entry mb-8 border-l-4 border-cyan-600 pl-4 py-3 bg-cyan-50/50 dark:bg-cyan-900/10 rounded-r-lg">
3
+ <!-- Reference meta (from refmeta - subtle metadata at top) -->
4
+ <template v-for="(child, idx) in block.children" :key="'meta-' + idx">
5
+ <span
6
+ v-if="child.type === 'reference_meta'"
7
+ class="reference-meta text-xs text-gray-400 dark:text-gray-500 block mb-1"
8
+ >
9
+ {{ child.text }}
10
+ </span>
11
+ </template>
12
+
13
+ <!-- Reference badge (from refnamediv.refclass - e.g., "pi") -->
14
+ <template v-for="(child, idx) in block.children" :key="'badge-' + idx">
15
+ <span
16
+ v-if="child.type === 'reference_badge'"
17
+ class="reference-badge inline-block text-[0.65rem] font-semibold uppercase tracking-wide px-2 py-0.5 rounded-full bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 mb-2"
18
+ >
19
+ {{ child.text }}
20
+ </span>
21
+ </template>
22
+
23
+ <!-- Reference name (from refnamediv.refname - THE HEADWORD) -->
24
+ <template v-for="(child, idx) in block.children" :key="'name-' + idx">
25
+ <h2
26
+ v-if="child.type === 'reference_name'"
27
+ class="reference-name text-2xl font-bold font-mono text-cyan-700 dark:text-cyan-400 mb-1"
28
+ >
29
+ {{ child.text }}
30
+ </h2>
31
+ </template>
32
+
33
+ <!-- Reference definition (from refnamediv.refpurpose - THE DEFINITION) -->
34
+ <template v-for="(child, idx) in block.children" :key="'def-' + idx">
35
+ <p
36
+ v-if="child.type === 'reference_definition'"
37
+ class="reference-definition text-base text-gray-600 dark:text-gray-400 italic mb-4"
38
+ >
39
+ {{ child.text }}
40
+ </p>
41
+ </template>
42
+
43
+ <!-- Description section (from refsection) -->
44
+ <template v-for="(child, idx) in block.children" :key="'section-' + idx">
45
+ <div
46
+ v-if="child.type === 'description_section'"
47
+ class="description-section mt-4 pt-4 border-t border-gray-200 dark:border-gray-700"
48
+ >
49
+ <BlockRenderer :blocks="child.children || []" />
50
+ </div>
51
+ </template>
52
+ </div>
53
+ </template>
54
+
55
+ <script setup lang="ts">
56
+ import type { ContentBlock } from '../stores/documentStore'
57
+ import BlockRenderer from './BlockRenderer.vue'
58
+
59
+ defineProps<{
60
+ block: ContentBlock
61
+ }>()
62
+ </script>
63
+
64
+ <style scoped>
65
+ .reference-entry {
66
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
67
+ }
68
+
69
+ .reference-name {
70
+ font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
71
+ }
72
+
73
+ .reference-badge {
74
+ letter-spacing: 0.05em;
75
+ }
76
+
77
+ .description-section {
78
+ /* Container styling handled by parent */
79
+ }
80
+ </style>
@@ -0,0 +1,286 @@
1
+ <template>
2
+ <Teleport to="body">
3
+ <div
4
+ v-if="uiStore.searchOpen"
5
+ class="fixed inset-0 bg-black/50 z-50 flex items-start justify-center pt-[10vh] transition-opacity duration-200"
6
+ @click.self="uiStore.closeSearch"
7
+ >
8
+ <div class="w-full max-w-xl bg-white dark:bg-gray-800 rounded-xl shadow-xl overflow-hidden transition-transform duration-200">
9
+ <!-- Search Input -->
10
+ <div class="flex items-center gap-3 p-4 border-b border-gray-200 dark:border-gray-700">
11
+ <svg class="w-5 h-5 text-gray-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
13
+ </svg>
14
+ <input
15
+ ref="inputRef"
16
+ v-model="searchQuery"
17
+ type="text"
18
+ class="flex-1 bg-transparent border-none outline-none text-gray-900 dark:text-gray-100 placeholder-gray-400 text-base"
19
+ placeholder="Search headings and content..."
20
+ @keydown="handleKeydown"
21
+ />
22
+ <button
23
+ v-if="searchQuery"
24
+ @click="searchQuery = ''"
25
+ class="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-400"
26
+ >
27
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
28
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
29
+ </svg>
30
+ </button>
31
+ </div>
32
+
33
+ <!-- Search Results -->
34
+ <div class="max-h-80 overflow-y-auto">
35
+ <div v-if="isSearching" class="p-8 text-center text-gray-500">
36
+ Searching...
37
+ </div>
38
+ <div v-else-if="searchQuery && results.length === 0" class="p-8 text-center text-gray-500">
39
+ No results found for "{{ searchQuery }}"
40
+ </div>
41
+ <div v-else-if="results.length > 0" class="p-2">
42
+ <a
43
+ v-for="(result, index) in results"
44
+ :key="result.id"
45
+ :href="'#' + result.id"
46
+ @click="selectResult(result)"
47
+ class="flex flex-col gap-1 px-3 py-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer"
48
+ :class="{ 'bg-gray-100 dark:bg-gray-700': focusedIndex === index }"
49
+ @mouseenter="focusedIndex = index"
50
+ >
51
+ <div class="flex items-center gap-3">
52
+ <span class="text-xs px-2 py-0.5 rounded bg-gray-200 dark:bg-gray-600 text-gray-600 dark:text-gray-300">
53
+ {{ result.type }}
54
+ </span>
55
+ <span class="flex-1 text-gray-900 dark:text-gray-100" v-html="highlightMatch(result.title)"></span>
56
+ </div>
57
+ <div v-if="result.snippet" class="text-xs text-gray-500 dark:text-gray-400 pl-[3.5rem] line-clamp-2" v-html="highlightMatch(result.snippet)"></div>
58
+ </a>
59
+ </div>
60
+ <div v-else class="p-8 text-center text-gray-400 text-sm">
61
+ Type to search...
62
+ </div>
63
+ </div>
64
+
65
+ <!-- Footer hints -->
66
+ <div class="px-4 py-3 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
67
+ <div class="flex gap-4 text-xs text-gray-400">
68
+ <span><kbd class="px-1.5 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-gray-600 dark:text-gray-300">↑</kbd><kbd class="px-1.5 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-gray-600 dark:text-gray-300 ml-0.5">↓</kbd> Navigate</span>
69
+ <span><kbd class="px-1.5 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-gray-600 dark:text-gray-300">Enter</kbd> Select</span>
70
+ <span><kbd class="px-1.5 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-gray-600 dark:text-gray-300">Esc</kbd> Close</span>
71
+ </div>
72
+ </div>
73
+ </div>
74
+ </div>
75
+ </Teleport>
76
+ </template>
77
+
78
+ <script setup lang="ts">
79
+ import { ref, watch, onMounted, nextTick } from 'vue'
80
+ import { useDocumentStore, type TocItem, type ContentBlock } from '@/stores/documentStore'
81
+ import { useUiStore } from '@/stores/uiStore'
82
+ import FlexSearch from 'flexsearch'
83
+
84
+ const documentStore = useDocumentStore()
85
+ const uiStore = useUiStore()
86
+
87
+ interface SearchResult {
88
+ id: string
89
+ title: string
90
+ type: 'chapter' | 'appendix' | 'part' | 'section' | 'glossary' | 'bibliography' | 'index' | 'reference' | 'preface'
91
+ children?: TocItem[]
92
+ snippet?: string
93
+ }
94
+
95
+ const searchQuery = ref('')
96
+ const results = ref<SearchResult[]>([])
97
+ const isSearching = ref(false)
98
+ const focusedIndex = ref(0)
99
+ const inputRef = ref<HTMLInputElement | null>(null)
100
+
101
+ // FlexSearch Document index for titles
102
+ const index = new (FlexSearch as any).Document({
103
+ document: {
104
+ id: 'id',
105
+ index: ['title'],
106
+ store: ['id', 'title', 'type']
107
+ },
108
+ tokenize: 'forward',
109
+ resolution: 9
110
+ })
111
+
112
+ function buildIndexes() {
113
+ const sections = documentStore.sections
114
+ addToIndex(sections)
115
+ }
116
+
117
+ function addToIndex(items: TocItem[]) {
118
+ items.forEach(item => {
119
+ index.add({
120
+ id: item.id,
121
+ title: item.title,
122
+ type: item.type
123
+ })
124
+ if (item.children && item.children.length > 0) {
125
+ addToIndex(item.children)
126
+ }
127
+ })
128
+ }
129
+
130
+ function extractTextFromBlocks(blocks: ContentBlock[]): string {
131
+ return blocks.map(block => {
132
+ let text = block.text || ''
133
+ if (block.children) {
134
+ text += ' ' + extractTextFromBlocks(block.children)
135
+ }
136
+ return text
137
+ }).join(' ')
138
+ }
139
+
140
+ function searchContent(query: string): SearchResult[] {
141
+ const content = documentStore.content
142
+ const searchResults: SearchResult[] = []
143
+ const lowerQuery = query.toLowerCase()
144
+
145
+ for (const [id, sectionContent] of Object.entries(content)) {
146
+ const text = extractTextFromBlocks(sectionContent.blocks || []).toLowerCase()
147
+ if (text.includes(lowerQuery)) {
148
+ // Find the snippet around the match
149
+ const matchPos = text.indexOf(lowerQuery)
150
+ const start = Math.max(0, matchPos - 50)
151
+ const end = Math.min(text.length, matchPos + query.length + 100)
152
+ let snippet = extractTextFromBlocks(sectionContent.blocks || []).substring(start, end)
153
+ if (start > 0) snippet = '...' + snippet
154
+ if (end < text.length) snippet = snippet + '...'
155
+
156
+ // Get section info from TOC
157
+ const section = findSection(documentStore.sections, id)
158
+ searchResults.push({
159
+ id,
160
+ title: section?.title || id,
161
+ type: (section?.type || 'section') as SearchResult['type'],
162
+ snippet
163
+ })
164
+ }
165
+ }
166
+ return searchResults
167
+ }
168
+
169
+ function findSection(items: TocItem[], id: string): TocItem | null {
170
+ for (const item of items) {
171
+ if (item.id === id) return item
172
+ if (item.children && item.children.length > 0) {
173
+ const found = findSection(item.children, id)
174
+ if (found) return found
175
+ }
176
+ }
177
+ return null
178
+ }
179
+
180
+ function search() {
181
+ if (!searchQuery.value.trim()) {
182
+ results.value = []
183
+ return
184
+ }
185
+
186
+ isSearching.value = true
187
+
188
+ // Search titles with FlexSearch
189
+ const searchResults = index.search(searchQuery.value, { limit: 30, enrich: true })
190
+
191
+ const titleResults: Map<string, TocItem> = new Map()
192
+ const seen = new Set<string>()
193
+
194
+ if (Array.isArray(searchResults)) {
195
+ searchResults.forEach((field: { result: Array<{ id: string; doc?: TocItem }> }) => {
196
+ if (field.result && Array.isArray(field.result)) {
197
+ field.result.forEach((r: { id: string; doc?: TocItem }) => {
198
+ if (r.doc && !seen.has(r.id)) {
199
+ seen.add(r.id)
200
+ titleResults.set(r.id, r.doc)
201
+ }
202
+ })
203
+ }
204
+ })
205
+ }
206
+
207
+ // Search full content
208
+ const contentResults = searchContent(searchQuery.value)
209
+
210
+ // Combine results, prioritizing title matches
211
+ const combined: SearchResult[] = []
212
+
213
+ // First add title matches
214
+ titleResults.forEach((tocItem) => {
215
+ combined.push({
216
+ id: tocItem.id,
217
+ title: tocItem.title,
218
+ type: tocItem.type,
219
+ children: tocItem.children
220
+ })
221
+ })
222
+
223
+ // Then add content-only matches
224
+ contentResults.forEach((cr) => {
225
+ if (!seen.has(cr.id)) {
226
+ seen.add(cr.id)
227
+ combined.push(cr)
228
+ }
229
+ })
230
+
231
+ results.value = combined.slice(0, 50)
232
+ isSearching.value = false
233
+ }
234
+
235
+ // Debounced search
236
+ let debounceTimer: ReturnType<typeof setTimeout> | null = null
237
+ watch(searchQuery, () => {
238
+ if (debounceTimer) clearTimeout(debounceTimer)
239
+ debounceTimer = setTimeout(search, 150)
240
+ })
241
+
242
+ // Watch for document data changes to rebuild indexes
243
+ watch(
244
+ () => documentStore.documentData,
245
+ () => {
246
+ buildIndexes()
247
+ },
248
+ { immediate: true }
249
+ )
250
+
251
+ onMounted(() => {
252
+ nextTick(() => inputRef.value?.focus())
253
+ })
254
+
255
+ function handleKeydown(e: KeyboardEvent) {
256
+ if (e.key === 'ArrowDown') {
257
+ e.preventDefault()
258
+ focusedIndex.value = Math.min(focusedIndex.value + 1, results.value.length - 1)
259
+ } else if (e.key === 'ArrowUp') {
260
+ e.preventDefault()
261
+ focusedIndex.value = Math.max(focusedIndex.value - 1, 0)
262
+ } else if (e.key === 'Enter' && results.value[focusedIndex.value]) {
263
+ e.preventDefault()
264
+ selectResult(results.value[focusedIndex.value])
265
+ } else if (e.key === 'Escape') {
266
+ uiStore.closeSearch()
267
+ }
268
+ }
269
+
270
+ function selectResult(result: SearchResult) {
271
+ // Scroll to section and close search
272
+ const element = document.getElementById(result.id)
273
+ if (element) {
274
+ element.scrollIntoView({ behavior: 'smooth' })
275
+ uiStore.closeSearch()
276
+ searchQuery.value = ''
277
+ results.value = []
278
+ }
279
+ }
280
+
281
+ function highlightMatch(text: string): string {
282
+ if (!searchQuery.value) return text
283
+ const regex = new RegExp(`(${searchQuery.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi')
284
+ return text.replace(regex, '<mark class="bg-yellow-200 dark:bg-yellow-600 rounded px-0.5">$1</mark>')
285
+ }
286
+ </script>
@@ -0,0 +1,31 @@
1
+ <template>
2
+ <section :id="section.id" class="mb-8 scroll-mt-20">
3
+ <h3 class="text-lg font-semibold mb-3 text-gray-800 dark:text-gray-200">
4
+ <span v-if="numbering" class="text-gray-500 dark:text-gray-400 mr-2">{{ numbering }}</span>
5
+ {{ section.title }}
6
+ </h3>
7
+ <BlockRenderer v-if="sectionContent" :blocks="sectionContent.blocks" />
8
+ <!-- Render children -->
9
+ <template v-if="section.children && section.children.length > 0">
10
+ <div v-for="child in section.children" :key="child.id" class="ml-4">
11
+ <SectionContent :section="child" />
12
+ </div>
13
+ </template>
14
+ </section>
15
+ </template>
16
+
17
+ <script setup lang="ts">
18
+ import { computed } from 'vue'
19
+ import { useDocumentStore, type TocItem } from '@/stores/documentStore'
20
+ import BlockRenderer from '@/components/BlockRenderer.vue'
21
+
22
+ interface Props {
23
+ section: TocItem
24
+ }
25
+
26
+ const props = defineProps<Props>()
27
+ const documentStore = useDocumentStore()
28
+
29
+ const numbering = computed(() => documentStore.getNumbering(props.section.id))
30
+ const sectionContent = computed(() => documentStore.getSectionContent(props.section.id))
31
+ </script>