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,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docbook
4
+ module Services
5
+ # Generates hierarchical TOC from document sections
6
+ class TocGenerator
7
+ def initialize(document)
8
+ @document = document
9
+ end
10
+
11
+ def generate
12
+ root_elements = get_root_elements
13
+ root_elements.map { |element| build_toc_node(element) }.compact
14
+ end
15
+
16
+ private
17
+
18
+ def get_root_elements
19
+ case @document
20
+ when Elements::Book
21
+ roots = []
22
+ roots.concat(Array(@document.part))
23
+ roots.concat(Array(@document.chapter))
24
+ roots.concat(Array(@document.appendix))
25
+ roots.concat(Array(@document.preface))
26
+ roots
27
+ when Elements::Article
28
+ roots = []
29
+ roots.concat(Array(@document.section))
30
+ roots.concat(Array(@document.article))
31
+ roots
32
+ else
33
+ Array(@document.elements)
34
+ end
35
+ end
36
+
37
+ def build_toc_node(element)
38
+ return nil unless can_have_title?(element)
39
+
40
+ node = Models::TocNode.new(
41
+ id: get_id(element),
42
+ title: get_title(element),
43
+ type: element_type(element),
44
+ number: nil # Numbering added separately
45
+ )
46
+
47
+ # Recursively add children
48
+ children = get_child_sections(element)
49
+ if children.any?
50
+ node.children = children.map { |child| build_toc_node(child) }.compact
51
+ end
52
+
53
+ node
54
+ end
55
+
56
+ def can_have_title?(element)
57
+ element.respond_to?(:title) ||
58
+ element.respond_to?(:info) ||
59
+ element.respond_to?(:refnamediv)
60
+ end
61
+
62
+ def get_title(element)
63
+ case element
64
+ when Elements::RefEntry
65
+ # Use refname from refnamediv as title
66
+ if element.refnamediv&.refname
67
+ element.refnamediv.refname.map(&:content).join(" ")
68
+ elsif element.refmeta&.refentrytitle
69
+ element.refmeta.refentrytitle.content
70
+ else
71
+ "Untitled"
72
+ end
73
+ when Elements::Reference
74
+ element.info&.title&.content ||
75
+ element.title&.content ||
76
+ "Reference"
77
+ when Elements::Book, Elements::Article, Elements::Chapter, Elements::Appendix,
78
+ Elements::Preface, Elements::Part, Elements::Section
79
+ element.info&.title&.content ||
80
+ element.title&.content ||
81
+ "Untitled"
82
+ else
83
+ "Untitled"
84
+ end
85
+ end
86
+
87
+ def get_id(element)
88
+ element.xml_id || generate_id(element)
89
+ end
90
+
91
+ def generate_id(element)
92
+ # Generate a stable ID from class name
93
+ "section-#{element.class.name.split('::').last.downcase}"
94
+ end
95
+
96
+ def element_type(element)
97
+ element.class.name.split('::').last.downcase
98
+ end
99
+
100
+ def get_child_sections(element)
101
+ children = []
102
+
103
+ case element
104
+ when Elements::Book
105
+ # Book has specific child element collections
106
+ children.concat(Array(element.part))
107
+ children.concat(Array(element.chapter))
108
+ children.concat(Array(element.appendix))
109
+ children.concat(Array(element.preface))
110
+ when Elements::Article
111
+ children.concat(Array(element.section))
112
+ children.concat(Array(element.article))
113
+ when Elements::Part
114
+ children.concat(Array(element.chapter))
115
+ children.concat(Array(element.appendix))
116
+ when Elements::Chapter, Elements::Appendix, Elements::Preface
117
+ children.concat(Array(element.section))
118
+ when Elements::Section
119
+ children.concat(Array(element.section))
120
+ when Elements::Reference
121
+ children.concat(Array(element.refentry))
122
+ end
123
+
124
+ children.select { |e| section_like?(e) }
125
+ end
126
+
127
+ def section_like?(element)
128
+ element.is_a?(Elements::Section) ||
129
+ element.is_a?(Elements::Chapter) ||
130
+ element.is_a?(Elements::Appendix) ||
131
+ element.is_a?(Elements::Part) ||
132
+ element.is_a?(Elements::Reference) ||
133
+ element.is_a?(Elements::RefEntry) ||
134
+ element.is_a?(Elements::Preface)
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docbook
4
+ module Services
5
+ # Service classes for data generation
6
+ SERVICES_DIR = "#{__dir__}/services"
7
+
8
+ autoload :TocGenerator, "#{SERVICES_DIR}/toc_generator"
9
+ autoload :IndexGenerator, "#{SERVICES_DIR}/index_generator"
10
+ autoload :NumberingService, "#{SERVICES_DIR}/numbering_service"
11
+ autoload :ElementToHash, "#{SERVICES_DIR}/element_to_hash"
12
+ autoload :DocumentBuilder, "#{SERVICES_DIR}/document_builder"
13
+ end
14
+ end
@@ -0,0 +1,20 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" class="scroll-smooth">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{ docbook_title | escape }}</title>
7
+ {% include 'partials/head' %}
8
+ </head>
9
+ <body class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
10
+ <div id="docbook-app"></div>
11
+ <script>
12
+ window.DOCBOOK_DATA = Object.assign([[DOCBOOK_DATA]], {
13
+ toc: [[DOCBOOK_TOC]],
14
+ content: [[DOCBOOK_CONTENT]],
15
+ index: [[DOCBOOK_INDEX]]
16
+ });
17
+ </script>
18
+ {% include 'partials/scripts' %}
19
+ </body>
20
+ </html>
@@ -0,0 +1,39 @@
1
+ {% comment %}Prism.js for syntax highlighting{% endcomment %}
2
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css">
3
+
4
+ {% comment %}Google Fonts{% endcomment %}
5
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Merriweather:ital,wght@0,400;0,700;1,400&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
6
+
7
+ {% comment %}Tailwind CDN{% endcomment %}
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <script>
10
+ tailwind.config = {
11
+ darkMode: 'class',
12
+ theme: {
13
+ extend: {
14
+ fontFamily: {
15
+ sans: ['Inter', 'system-ui', 'sans-serif'],
16
+ serif: ['Merriweather', 'Georgia', 'serif'],
17
+ mono: ['JetBrains Mono', 'ui-monospace', 'monospace'],
18
+ },
19
+ },
20
+ },
21
+ }
22
+ </script>
23
+
24
+ {% comment %}Custom styles{% endcomment %}
25
+ <style>
26
+ [x-cloak] { display: none !important; }
27
+ .toc-active { color: #2563eb; background: #dbeafe; }
28
+ .dark .toc-active { color: #60a5fa; background: #1e3a5f; }
29
+ .db-bibentry { padding-left: 2em; text-indent: -2em; margin-bottom: 0.5em; }
30
+ .db-appendix { border-left: 4px solid #8b5cf6; padding-left: 1rem; margin-left: -1rem; }
31
+ .dark .db-appendix { border-left-color: #a78bfa; }
32
+ </style>
33
+
34
+ {% comment %}App CSS - inline for single_file, linked for directory{% endcomment %}
35
+ {% if assets_inline %}
36
+ <style>{{ app_css }}</style>
37
+ {% else %}
38
+ <link rel="stylesheet" href="{{ base_url }}/assets/app.css">
39
+ {% endif %}
@@ -0,0 +1,10 @@
1
+ {% comment %}Prism.js core + autoloader for additional languages{% endcomment %}
2
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
3
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
4
+
5
+ {% comment %}App JS - inline for single_file, linked for directory{% endcomment %}
6
+ {% if assets_inline %}
7
+ <script>{{ app_js }}</script>
8
+ {% else %}
9
+ <script src="{{ base_url }}/assets/app.iife.js"></script>
10
+ {% endif %}
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docbook
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+
5
+ module Docbook
6
+ class XIncludeResolver
7
+ XINCLUDE_NS = "http://www.w3.org/2001/XInclude"
8
+
9
+ # Resolve XInclude elements in a Nokogiri document.
10
+ # Handles standard XML includes, text includes, and text+fragid extensions.
11
+ # Processes iteratively to handle nested includes (e.g., file A includes file B
12
+ # which contains text+fragid self-references).
13
+ def self.resolve(doc)
14
+ loop do
15
+ includes = doc.xpath("//xi:include", "xi" => XINCLUDE_NS)
16
+ break if includes.empty?
17
+
18
+ resolved_any = false
19
+ includes.each do |inc|
20
+ href = inc["href"]
21
+ next unless href
22
+
23
+ parse_mode = inc["parse"] || "xml"
24
+ fragid = inc["fragid"]
25
+ base_dir = base_dir_for(inc.document)
26
+ file_path = resolve_path(base_dir, href)
27
+ next unless file_path && File.exist?(file_path)
28
+
29
+ if parse_mode == "text"
30
+ resolve_text_include(doc, inc, file_path, fragid)
31
+ else
32
+ resolve_xml_include(doc, inc, file_path)
33
+ end
34
+ resolved_any = true
35
+ break # re-scan after each resolve (tree changes)
36
+ end
37
+ break unless resolved_any
38
+ end
39
+ doc
40
+ rescue StandardError
41
+ doc
42
+ end
43
+
44
+ # Pre-process an XML string to resolve XIncludes before model parsing.
45
+ def self.resolve_string(xml_string, base_path: nil)
46
+ doc = if base_path
47
+ file_uri = "file://#{File.expand_path(base_path)}"
48
+ Nokogiri::XML(xml_string, file_uri)
49
+ else
50
+ Nokogiri::XML(xml_string)
51
+ end
52
+ resolve(doc)
53
+ end
54
+
55
+ # ── Include Resolution ──────────────────────────────────────────
56
+
57
+ def self.resolve_text_include(doc, inc, file_path, fragid)
58
+ content = File.read(file_path)
59
+ if fragid && !fragid.empty?
60
+ filtered = apply_fragid(content, fragid)
61
+ content = filtered if filtered
62
+ end
63
+ text_node = doc.create_text_node(content)
64
+ inc.replace(text_node)
65
+ end
66
+
67
+ def self.resolve_xml_include(doc, inc, file_path)
68
+ included_xml = File.read(file_path)
69
+ included_doc = Nokogiri::XML(included_xml, "file://#{File.expand_path(file_path)}")
70
+
71
+ root = included_doc.root
72
+ # Ensure namespace is declared in target document
73
+ if root.namespace
74
+ ns = root.namespace
75
+ existing = doc.root.namespace_definitions.find { |n| n.href == ns.href }
76
+ doc.root.add_namespace_definition(ns.prefix, ns.href) unless existing
77
+ end
78
+
79
+ # Replace the xi:include with the root element itself (not its children)
80
+ inc.replace(root.dup)
81
+ end
82
+
83
+ def self.base_dir_for(document)
84
+ url = document.url
85
+ return nil unless url
86
+ File.dirname(url.sub(%r{^file://}, ""))
87
+ end
88
+
89
+ def self.resolve_path(base_dir, href)
90
+ return nil unless base_dir
91
+ File.join(base_dir, href)
92
+ end
93
+
94
+ # ── Fragid Filtering ──────────────────────────────────────────
95
+
96
+ # Apply a fragid filter to text content
97
+ def self.apply_fragid(content, fragid)
98
+ if fragid.start_with?("search=")
99
+ apply_search_fragid(content, fragid[7..])
100
+ elsif fragid.start_with?("line=")
101
+ apply_line_fragid(content, fragid[5..])
102
+ elsif fragid.start_with?("char=")
103
+ apply_char_fragid(content, fragid[5..])
104
+ end
105
+ end
106
+
107
+ # Apply search= fragid scheme (DocBook xslTNG extension)
108
+ # Syntax: search=#PATTERN#[;after|;before],#STOP#
109
+ # or: search=/REGEX/[,/STOP/]
110
+ def self.apply_search_fragid(content, search_spec)
111
+ lines = content.lines
112
+ if search_spec.start_with?("/")
113
+ apply_delimited_search(lines, search_spec, "/")
114
+ elsif search_spec.start_with?("#")
115
+ apply_delimited_search(lines, search_spec, "#")
116
+ end
117
+ end
118
+
119
+ # Parse delimited patterns and apply search
120
+ # Delimiter is # (literal) or / (regex)
121
+ def self.apply_delimited_search(lines, spec, delim)
122
+ # Parse: DELIM pattern DELIM [;modifier] [, DELIM stop DELIM]
123
+ remainder = spec[1..] # skip opening delimiter
124
+ close_idx = remainder.index(delim)
125
+ return nil unless close_idx
126
+
127
+ pattern_str = remainder[0...close_idx]
128
+ remainder = remainder[(close_idx + 1)..]
129
+
130
+ # Check for modifier (;after or ;before)
131
+ modifier = nil
132
+ if remainder&.start_with?(";")
133
+ mod_match = remainder.match(/\A;(after|before)/)
134
+ if mod_match
135
+ modifier = mod_match[1].to_sym
136
+ remainder = remainder[mod_match[0].length..]
137
+ end
138
+ end
139
+
140
+ # Skip comma separator
141
+ remainder = remainder[1..] if remainder&.start_with?(",")
142
+
143
+ # Parse stop pattern if present
144
+ stop_pattern = nil
145
+ if remainder && !remainder.empty? && remainder.start_with?(delim)
146
+ stop_remainder = remainder[1..]
147
+ stop_close = stop_remainder.index(delim)
148
+ if stop_close
149
+ stop_pattern = stop_remainder[0...stop_close]
150
+ end
151
+ end
152
+
153
+ # Apply search
154
+ match_fn = delim == "/" ? ->(line, pat) { line.match?(Regexp.new(pat)) }
155
+ : ->(line, pat) { line.include?(pat) }
156
+
157
+ start_idx = lines.index { |l| match_fn.call(l, pattern_str) }
158
+ return nil unless start_idx
159
+
160
+ # Determine range
161
+ case modifier
162
+ when :after
163
+ range_start = start_idx + 1
164
+ when :before
165
+ range_start = 0
166
+ range_end = start_idx
167
+ else
168
+ range_start = start_idx
169
+ end
170
+
171
+ # Find stop (exclusive)
172
+ if stop_pattern
173
+ stop_slice = lines[range_start..]
174
+ stop_idx = stop_slice&.index { |l| match_fn.call(l, stop_pattern) }
175
+ range_end = stop_idx ? range_start + stop_idx : lines.length
176
+ elsif modifier != :before
177
+ range_end = start_idx + 1
178
+ end
179
+
180
+ range_end ||= lines.length
181
+ selected = lines[range_start...range_end]
182
+ return nil unless selected && !selected.empty?
183
+
184
+ selected.join
185
+ end
186
+
187
+ # Apply line= fragid scheme (RFC 5147)
188
+ # Syntax: line=START,END[;length=N]
189
+ def self.apply_line_fragid(content, line_spec)
190
+ parts = line_spec.split(";")
191
+ range_part = parts[0]
192
+ length_part = parts.find { |p| p.start_with?("length=") }
193
+
194
+ line_start, line_end = range_part.split(",").map(&:to_i)
195
+ return nil unless line_start && line_end
196
+
197
+ lines = content.lines
198
+ selected = lines[(line_start - 1)...(line_end)]
199
+ return nil unless selected
200
+
201
+ result = selected.join
202
+ if length_part
203
+ max_len = length_part.split("=")[1].to_i
204
+ result = result[0...max_len] if max_len > 0
205
+ end
206
+ result
207
+ end
208
+
209
+ # Apply char= fragid scheme (RFC 5147)
210
+ # Syntax: char=START,END
211
+ def self.apply_char_fragid(content, char_spec)
212
+ char_start, char_end = char_spec.split(",").map(&:to_i)
213
+ return nil unless char_start && char_end
214
+ content[char_start...char_end]
215
+ end
216
+ end
217
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docbook
4
+ # Resolves xrefs in a DocBook document by building an O(1) xml:id lookup hash
5
+ # and resolving all xref/linkend references to their target titles.
6
+ class XrefResolver
7
+ def initialize(document)
8
+ @document = document
9
+ @xml_id_map = {}
10
+ end
11
+
12
+ # Build the xml:id -> element hash
13
+ def resolve!
14
+ @xml_id_map = build_xml_id_map(@document)
15
+ self
16
+ end
17
+
18
+ # Get the resolved title text for a linkend ID
19
+ def title_for(linkend)
20
+ target = @xml_id_map[linkend.to_s]
21
+ return nil unless target
22
+ best_title(target)
23
+ end
24
+
25
+ # Get element by xml:id
26
+ def [](xml_id)
27
+ @xml_id_map[xml_id.to_s]
28
+ end
29
+
30
+ private
31
+
32
+ def build_xml_id_map(el, map = {})
33
+ return map unless el.is_a?(Lutaml::Model::Serializable)
34
+
35
+ map[el.xml_id.to_s] = el if el.xml_id
36
+
37
+ # Walk child collections based on element type
38
+ case el
39
+ when Docbook::Elements::Book
40
+ el.part&.each { |p| build_xml_id_map(p, map) }
41
+ el.chapter&.each { |c| build_xml_id_map(c, map) }
42
+ el.appendix&.each { |a| build_xml_id_map(a, map) }
43
+ el.preface&.each { |p| build_xml_id_map(p, map) }
44
+ el.glossary&.each { |g| build_xml_id_map(g, map) }
45
+ el.bibliography&.each { |b| build_xml_id_map(b, map) }
46
+ el.index&.each { |i| build_xml_id_map(i, map) }
47
+ when Docbook::Elements::Article
48
+ el.section&.each { |s| build_xml_id_map(s, map) }
49
+ when Docbook::Elements::Part
50
+ el.chapter&.each { |c| build_xml_id_map(c, map) }
51
+ el.appendix&.each { |a| build_xml_id_map(a, map) }
52
+ el.reference&.each { |r| build_xml_id_map(r, map) }
53
+ el.glossary&.each { |g| build_xml_id_map(g, map) }
54
+ el.bibliography&.each { |b| build_xml_id_map(b, map) }
55
+ el.index&.each { |i| build_xml_id_map(i, map) }
56
+ when Docbook::Elements::Chapter, Docbook::Elements::Appendix,
57
+ Docbook::Elements::Section, Docbook::Elements::Preface
58
+ el.section&.each { |s| build_xml_id_map(s, map) }
59
+ when Docbook::Elements::Reference
60
+ el.refentry&.each { |r| build_xml_id_map(r, map) }
61
+ end
62
+
63
+ map
64
+ end
65
+
66
+ def best_title(el)
67
+ case el
68
+ when Docbook::Elements::Article, Docbook::Elements::Book
69
+ el.info&.title&.content
70
+ when Docbook::Elements::Section, Docbook::Elements::Chapter, Docbook::Elements::Appendix,
71
+ Docbook::Elements::Preface, Docbook::Elements::Part, Docbook::Elements::Reference
72
+ el.title&.content
73
+ else
74
+ el.title&.content rescue nil
75
+ end
76
+ end
77
+ end
78
+ end
data/lib/docbook.rb ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Docbook
6
+ class Error < StandardError; end
7
+
8
+ autoload :Version, "#{__dir__}/docbook/version"
9
+ autoload :Elements, "#{__dir__}/docbook/elements"
10
+ autoload :Models, "#{__dir__}/docbook/models"
11
+ autoload :Services, "#{__dir__}/docbook/services"
12
+ autoload :XIncludeResolver, "#{__dir__}/docbook/xinclude_resolver"
13
+ autoload :XrefResolver, "#{__dir__}/docbook/xref_resolver"
14
+ autoload :Output, "#{__dir__}/docbook/output"
15
+ autoload :CLI, "#{__dir__}/docbook/cli"
16
+ autoload :Document, "#{__dir__}/docbook/document"
17
+ end
data/sig/docbook.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Docbook
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end