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,281 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docbook
4
+ module Output
5
+ # Collects all indexterms from a document for index generation
6
+ class IndexCollector
7
+ def initialize(document)
8
+ @document = document
9
+ @index_terms = []
10
+ end
11
+
12
+ # Collect all indexterms from the document
13
+ def collect
14
+ @index_terms = []
15
+ traverse_element(@document)
16
+ @index_terms
17
+ end
18
+
19
+ # Get indexterms grouped by type
20
+ def by_type
21
+ result = {}
22
+ @index_terms.each do |term|
23
+ type = term[:type] || "default"
24
+ result[type] ||= []
25
+ result[type] << term
26
+ end
27
+ result
28
+ end
29
+
30
+ private
31
+
32
+ def traverse_element(element, section_info = {})
33
+ return unless element
34
+
35
+ # Collect section info for linking based on element type
36
+ section_info = update_section_info(element, section_info)
37
+
38
+ # Check for indexterm on this element
39
+ each_attr(element, :indexterm) do |it|
40
+ process_indexterm(it, section_info)
41
+ end
42
+
43
+ # Traverse children based on element type
44
+ case element
45
+ when Elements::Book
46
+ traverse_book(element, section_info)
47
+ when Elements::Part
48
+ traverse_part(element, section_info)
49
+ when Elements::Article
50
+ traverse_article(element, section_info)
51
+ when Elements::Chapter
52
+ traverse_chapter(element, section_info)
53
+ when Elements::Appendix
54
+ traverse_appendix(element, section_info)
55
+ when Elements::Section
56
+ traverse_section(element, section_info)
57
+ when Elements::Simplesect
58
+ traverse_simplesect(element, section_info)
59
+ when Elements::RefEntry
60
+ traverse_refentry(element, section_info)
61
+ when Elements::RefSection
62
+ traverse_refsection(element, section_info)
63
+ when Elements::Para
64
+ # Para children are handled via mixed content traversal
65
+ traverse_mixed_content(element, section_info)
66
+ when Elements::ListItem
67
+ traverse_listitem(element, section_info)
68
+ when Elements::Entry
69
+ traverse_entry(element, section_info)
70
+ end
71
+ end
72
+
73
+ def update_section_info(element, current_info)
74
+ info = current_info.dup
75
+ if element.xml_id
76
+ info[:id] = element.xml_id
77
+ info[:title] = element_title(element)
78
+ elsif element.respond_to?(:title) && element.title
79
+ info[:title] = element.title.content.to_s
80
+ end
81
+ info
82
+ end
83
+
84
+ def traverse_book(book, section_info)
85
+ each_attr(book, :part) { |p| traverse_element(p, section_info) }
86
+ each_attr(book, :chapter) { |c| traverse_element(c, section_info) }
87
+ each_attr(book, :appendix) { |a| traverse_element(a, section_info) }
88
+ each_attr(book, :preface) { |p| traverse_element(p, section_info) }
89
+ each_attr(book, :glossary) { |g| traverse_element(g, section_info) }
90
+ each_attr(book, :bibliography) { |b| traverse_element(b, section_info) }
91
+ each_attr(book, :index) { |i| traverse_element(i, section_info) }
92
+ each_attr(book, :setindex) { |si| traverse_element(si, section_info) }
93
+ end
94
+
95
+ def traverse_part(part, section_info)
96
+ each_attr(part, :part) { |p| traverse_element(p, section_info) }
97
+ each_attr(part, :chapter) { |c| traverse_element(c, section_info) }
98
+ each_attr(part, :appendix) { |a| traverse_element(a, section_info) }
99
+ each_attr(part, :reference) { |r| traverse_element(r, section_info) }
100
+ each_attr(part, :preface) { |p| traverse_element(p, section_info) }
101
+ each_attr(part, :glossary) { |g| traverse_element(g, section_info) }
102
+ each_attr(part, :bibliography) { |b| traverse_element(b, section_info) }
103
+ each_attr(part, :index) { |i| traverse_element(i, section_info) }
104
+ each_attr(part, :setindex) { |si| traverse_element(si, section_info) }
105
+ end
106
+
107
+ def traverse_article(article, section_info)
108
+ each_attr(article, :section) { |s| traverse_element(s, section_info) }
109
+ each_attr(article, :glossary) { |g| traverse_element(g, section_info) }
110
+ each_attr(article, :bibliography) { |b| traverse_element(b, section_info) }
111
+ each_attr(article, :index) { |i| traverse_element(i, section_info) }
112
+ end
113
+
114
+ def traverse_chapter(chapter, section_info)
115
+ each_attr(chapter, :section) { |s| traverse_element(s, section_info) }
116
+ each_attr(chapter, :simplesect) { |ss| traverse_element(ss, section_info) }
117
+ each_attr(chapter, :para) { |p| traverse_element(p, section_info) }
118
+ each_attr(chapter, :indexterm) { |it| process_indexterm(it, section_info) }
119
+ end
120
+
121
+ def traverse_appendix(appendix, section_info)
122
+ each_attr(appendix, :section) { |s| traverse_element(s, section_info) }
123
+ each_attr(appendix, :simplesect) { |ss| traverse_element(ss, section_info) }
124
+ each_attr(appendix, :para) { |p| traverse_element(p, section_info) }
125
+ each_attr(appendix, :indexterm) { |it| process_indexterm(it, section_info) }
126
+ end
127
+
128
+ def traverse_section(section, section_info)
129
+ each_attr(section, :section) { |s| traverse_element(s, section_info) }
130
+ each_attr(section, :simplesect) { |ss| traverse_element(ss, section_info) }
131
+ each_attr(section, :para) { |p| traverse_element(p, section_info) }
132
+ each_attr(section, :indexterm) { |it| process_indexterm(it, section_info) }
133
+ end
134
+
135
+ def traverse_simplesect(simplesect, section_info)
136
+ each_attr(simplesect, :para) { |p| traverse_element(p, section_info) }
137
+ each_attr(simplesect, :indexterm) { |it| process_indexterm(it, section_info) }
138
+ end
139
+
140
+ def traverse_refentry(refentry, section_info)
141
+ each_attr(refentry, :refsection) { |rs| traverse_element(rs, section_info) }
142
+ end
143
+
144
+ def traverse_refsection(refsection, section_info)
145
+ new_info = update_section_info(refsection, section_info)
146
+ each_attr(refsection, :para) { |p| traverse_element(p, new_info) }
147
+ each_attr(refsection, :indexterm) { |it| process_indexterm(it, new_info) }
148
+ end
149
+
150
+ def traverse_listitem(listitem, section_info)
151
+ each_attr(listitem, :para) { |p| traverse_element(p, section_info) }
152
+ each_attr(listitem, :simplesect) { |ss| traverse_element(ss, section_info) }
153
+ each_attr(listitem, :indexterm) { |it| process_indexterm(it, section_info) }
154
+ end
155
+
156
+ def traverse_entry(entry, section_info)
157
+ each_attr(entry, :para) { |p| traverse_element(p, section_info) }
158
+ each_attr(entry, :indexterm) { |it| process_indexterm(it, section_info) }
159
+ end
160
+
161
+ def traverse_mixed_content(element, section_info)
162
+ return unless element.respond_to?(:each_mixed_content)
163
+
164
+ element.each_mixed_content do |node|
165
+ case node
166
+ when Elements::IndexTerm
167
+ process_indexterm(node, section_info)
168
+ when Elements::Para
169
+ traverse_element(node, section_info)
170
+ end
171
+ end
172
+ end
173
+
174
+ def process_indexterm(indexterm, section_info)
175
+ # Skip if no primary (required for meaningful index entry)
176
+ primaries = Array(indexterm.primary).map { |p| p.content.to_s.strip }.compact
177
+ return if primaries.empty?
178
+
179
+ primary_text = primaries.first
180
+ secondaries = Array(indexterm.secondary).map { |s| s.content.to_s.strip }.compact
181
+ tertiaries = Array(indexterm.tertiary).map { |t| t.content.to_s.strip }.compact
182
+
183
+ sees = Array(indexterm.see).map { |s| s.content.to_s.strip }.compact
184
+ see_alsos = Array(indexterm.see_also).map { |sa| sa.content.to_s.strip }.compact
185
+
186
+ term_info = {
187
+ primary: primary_text,
188
+ primary_sort: sort_key(primary_text, indexterm),
189
+ secondary: secondaries.first,
190
+ tertiary: tertiaries.first,
191
+ sees: sees,
192
+ see_alsos: see_alsos,
193
+ type: indexterm.type,
194
+ zone: indexterm.zone,
195
+ xml_id: indexterm.xml_id,
196
+ class_value: indexterm.class_value,
197
+ section_id: section_info[:id],
198
+ section_title: section_info[:title],
199
+ section_info: section_info
200
+ }
201
+
202
+ @index_terms << term_info
203
+ end
204
+
205
+ def element_title(element)
206
+ case element
207
+ when Elements::Book, Elements::Article
208
+ element.info&.title&.content
209
+ when Elements::Chapter, Elements::Appendix, Elements::Section,
210
+ Elements::Part, Elements::Preface
211
+ element.info&.title&.content || element.title&.content
212
+ when Elements::RefEntry
213
+ element.refnamediv&.refname&.first&.content
214
+ else
215
+ element.title&.content
216
+ end
217
+ end
218
+
219
+ # Generate sort key for an index entry
220
+ def sort_key(text, indexterm)
221
+ return "SYMBOLS" if indexterm.class_value == "token" || text.start_with?("@")
222
+
223
+ # Remove leading punctuation for sorting
224
+ text.gsub(/^[^a-zA-Z]+/, "").downcase
225
+ end
226
+
227
+ # Helper to safely call attributes - same pattern as Html class
228
+ def each_attr(obj, name)
229
+ val = obj.send(name)
230
+ return unless val
231
+ Array(val).each { |item| yield item }
232
+ rescue NoMethodError
233
+ nil
234
+ end
235
+ end
236
+
237
+ # Generates index content from collected indexterms
238
+ class IndexGenerator
239
+ def initialize(index_terms, xref_resolver = nil)
240
+ @index_terms = index_terms
241
+ @xref_resolver = xref_resolver
242
+ end
243
+
244
+ # Generate index data structure grouped by letter
245
+ def generate
246
+ by_letter = group_by_letter
247
+ by_letter.sort_by { |letter, _| letter == "SYMBOLS" ? "{" : letter.downcase }
248
+ .to_h
249
+ .map do |letter, terms|
250
+ {
251
+ letter: letter,
252
+ entries: sort_entries(terms)
253
+ }
254
+ end
255
+ end
256
+
257
+ private
258
+
259
+ def group_by_letter
260
+ result = {}
261
+ @index_terms.each do |term|
262
+ letter = letter_for(term[:primary_sort])
263
+ result[letter] ||= []
264
+ result[letter] << term
265
+ end
266
+ result
267
+ end
268
+
269
+ def letter_for(sort_key)
270
+ return "SYMBOLS" if sort_key == "SYMBOLS" || sort_key.start_with?("@")
271
+
272
+ first_char = sort_key.chars.find(&:letter?) || sort_key[0] || ""
273
+ first_char.upcase
274
+ end
275
+
276
+ def sort_entries(terms)
277
+ terms.sort_by { |t| [t[:primary_sort], t[:secondary].to_s] }
278
+ end
279
+ end
280
+ end
281
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docbook
4
+ module Output
5
+ autoload :Html, "#{__dir__}/output/html"
6
+ autoload :Data, "#{__dir__}/output/data"
7
+ end
8
+ end
@@ -0,0 +1,258 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docbook
4
+ module Services
5
+ # Builds the complete DocumentRoot from a parsed Docbook document
6
+ class DocumentBuilder
7
+ def initialize(document, options = {})
8
+ @document = document
9
+ @options = options
10
+ end
11
+
12
+ def build
13
+ doc_root = Models::DocumentRoot.new(
14
+ title: extract_title,
15
+ metadata: build_metadata,
16
+ toc: generate_toc,
17
+ sections: build_sections,
18
+ index: generate_index,
19
+ numbering: generate_numbering,
20
+ generator: "docbook-gem/#{Docbook::VERSION}",
21
+ generated_at: Time.now.utc.iso8601
22
+ )
23
+ end
24
+
25
+ private
26
+
27
+ def extract_title
28
+ @document.respond_to?(:title) && @document.title&.content ||
29
+ @document.respond_to?(:info) && @document.info&.title&.content ||
30
+ "Untitled Document"
31
+ end
32
+
33
+ def build_metadata
34
+ metadata = Models::DocumentMetadata.new
35
+
36
+ if @document.respond_to?(:info)
37
+ info = @document.info
38
+ metadata.author = extract_author(info)
39
+ metadata.title = info.title&.content if info.respond_to?(:title)
40
+ metadata.subtitle = info.subtitle&.content if info.respond_to?(:subtitle)
41
+ metadata.productname = info.productname&.content if info.respond_to?(:productname)
42
+ metadata.pubdate = info.date&.content || info.pubdate&.content if info.respond_to?(:date) || info.respond_to?(:pubdate)
43
+ end
44
+
45
+ metadata
46
+ end
47
+
48
+ def extract_author(info)
49
+ return nil unless info.respond_to?(:author)
50
+
51
+ author = info.author
52
+ return nil unless author
53
+
54
+ if author.respond_to?(:personname)
55
+ author.personname&.content ||
56
+ [author.personname&.firstname&.content, author.personname&.surname&.content].compact.join(" ")
57
+ elsif author.respond_to?(:orgname)
58
+ author.orgname&.content
59
+ else
60
+ author.content
61
+ end
62
+ end
63
+
64
+ def generate_toc
65
+ TocGenerator.new(@document).generate
66
+ end
67
+
68
+ def generate_index
69
+ IndexGenerator.new(@document).generate
70
+ end
71
+
72
+ def generate_numbering
73
+ NumberingService.new(@document).generate
74
+ end
75
+
76
+ def build_sections
77
+ sections = []
78
+
79
+ root_elements = case @document
80
+ when Elements::Book
81
+ roots = []
82
+ roots.concat(Array(@document.part))
83
+ roots.concat(Array(@document.chapter))
84
+ roots.concat(Array(@document.appendix))
85
+ roots.concat(Array(@document.preface))
86
+ roots
87
+ when Elements::Article
88
+ roots = []
89
+ roots.concat(Array(@document.section))
90
+ roots.concat(Array(@document.article))
91
+ roots
92
+ else
93
+ Array(@document.elements)
94
+ end
95
+
96
+ root_elements.each do |element|
97
+ section = build_section(element)
98
+ sections << section if section
99
+ end
100
+
101
+ sections
102
+ end
103
+
104
+ def build_section(element)
105
+ type = element_type(element)
106
+ return nil unless type
107
+
108
+ section = Models::SectionRoot.new(
109
+ id: element.xml_id || generate_id(element),
110
+ type: type,
111
+ title: extract_element_title(element),
112
+ number: get_section_number(element)
113
+ )
114
+
115
+ # Convert content to hash
116
+ section.content = build_section_content(element)
117
+
118
+ # Process children
119
+ children = build_child_sections(element)
120
+ section.children = children if children.any?
121
+
122
+ # For refentry, extract refname
123
+ if element.is_a?(Elements::RefEntry)
124
+ section.refname = element.refnamediv&.refname&.map(&:content)&.join(" ") ||
125
+ element.refmeta&.refentrytitle&.content
126
+ end
127
+
128
+ section
129
+ end
130
+
131
+ def element_type(element)
132
+ case element
133
+ when Elements::Part then "part"
134
+ when Elements::Chapter then "chapter"
135
+ when Elements::Appendix then "appendix"
136
+ when Elements::Section then "section"
137
+ when Elements::Reference then "reference"
138
+ when Elements::RefEntry then "refentry"
139
+ when Elements::Preface then "preface"
140
+ when Elements::Article then "article"
141
+ when Elements::Book then "book"
142
+ else nil
143
+ end
144
+ end
145
+
146
+ def extract_element_title(element)
147
+ case element
148
+ when Elements::RefEntry
149
+ element.refnamediv&.refname&.map(&:content)&.join(" ") ||
150
+ element.refmeta&.refentrytitle&.content ||
151
+ "Untitled"
152
+ when Elements::Reference
153
+ element.info&.title&.content || element.title&.content || "Reference"
154
+ else
155
+ element.info&.title&.content ||
156
+ element.title&.content ||
157
+ "Untitled"
158
+ end
159
+ end
160
+
161
+ def generate_id(element)
162
+ "section-#{element.class.name.split('::').last.downcase}-#{element.object_id}"
163
+ end
164
+
165
+ def get_section_number(element)
166
+ # Will be filled in by numbering service
167
+ nil
168
+ end
169
+
170
+ def build_section_content(element)
171
+ # Use ElementToHash to convert child elements
172
+ blocks = []
173
+
174
+ if element.respond_to?(:elements)
175
+ element.elements.each do |child|
176
+ next if skip_element?(child)
177
+ block = ElementToHash.new(child).to_h
178
+ blocks << block if block
179
+ end
180
+ end
181
+
182
+ # For refentry, also process refsection
183
+ if element.is_a?(Elements::RefEntry)
184
+ element.refsection&.each do |rs|
185
+ rs.elements&.each do |child|
186
+ next if skip_element?(child)
187
+ block = ElementToHash.new(child).to_h
188
+ blocks << block if block
189
+ end
190
+ end
191
+ end
192
+
193
+ blocks
194
+ end
195
+
196
+ def skip_element?(element)
197
+ element.is_a?(Elements::Title) ||
198
+ element.is_a?(Elements::Info) ||
199
+ element.is_a?(Elements::IndexTerm) ||
200
+ element.is_a?(Elements::Simplesect) && element.title.nil?
201
+ end
202
+
203
+ def build_child_sections(element)
204
+ children = []
205
+
206
+ child_elements = get_child_elements(element)
207
+
208
+ child_elements.each do |child|
209
+ next unless section_like?(child)
210
+ section = build_section(child)
211
+ children << section if section
212
+ end
213
+
214
+ children
215
+ end
216
+
217
+ def get_child_elements(element)
218
+ case element
219
+ when Elements::Book
220
+ children = []
221
+ children.concat(Array(element.part))
222
+ children.concat(Array(element.chapter))
223
+ children.concat(Array(element.appendix))
224
+ children.concat(Array(element.preface))
225
+ children
226
+ when Elements::Article
227
+ children = []
228
+ children.concat(Array(element.section))
229
+ children.concat(Array(element.article))
230
+ children
231
+ when Elements::Part
232
+ children = []
233
+ children.concat(Array(element.chapter))
234
+ children.concat(Array(element.appendix))
235
+ children
236
+ when Elements::Chapter, Elements::Appendix, Elements::Preface
237
+ Array(element.section)
238
+ when Elements::Section
239
+ Array(element.section)
240
+ when Elements::Reference
241
+ Array(element.refentry)
242
+ else
243
+ []
244
+ end
245
+ end
246
+
247
+ def section_like?(element)
248
+ element.is_a?(Elements::Section) ||
249
+ element.is_a?(Elements::Chapter) ||
250
+ element.is_a?(Elements::Appendix) ||
251
+ element.is_a?(Elements::Part) ||
252
+ element.is_a?(Elements::Reference) ||
253
+ element.is_a?(Elements::RefEntry) ||
254
+ element.is_a?(Elements::Preface)
255
+ end
256
+ end
257
+ end
258
+ end