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,287 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docbook
4
+ module Services
5
+ # Converts Lutaml elements to Vue-friendly hashes
6
+ # Uses Lutaml's as_json but ensures compatibility with Vue rendering
7
+ class ElementToHash
8
+ def initialize(element)
9
+ @element = element
10
+ end
11
+
12
+ # Convert element to hash suitable for Vue
13
+ def to_h
14
+ case @element
15
+ when Elements::RefEntry
16
+ refentry_to_h
17
+ when Elements::Para
18
+ para_to_h
19
+ when Elements::VariableList
20
+ variablelist_to_h
21
+ when Elements::OrderedList
22
+ orderedlist_to_h
23
+ when Elements::ItemizedList
24
+ itemizedlist_to_h
25
+ when Elements::ProgramListing, Elements::Screen
26
+ codelike_to_h
27
+ when Elements::Example, Elements::InformalExample
28
+ example_to_h
29
+ when Elements::Figure, Elements::InformalFigure
30
+ figure_to_h
31
+ when Elements::Table
32
+ table_to_h
33
+ when Elements::Note, Elements::Warning, Elements::Caution,
34
+ Elements::Important, Elements::Tip, Elements::Danger
35
+ admonition_to_h
36
+ when Elements::BlockQuote
37
+ blockquote_to_h
38
+ when Elements::Title
39
+ { type: "title", text: @element.content }
40
+ else
41
+ generic_to_h
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def refentry_to_h
48
+ content = @element.as_json
49
+
50
+ # Flatten for Vue - extract key fields
51
+ result = {
52
+ type: "refentry",
53
+ id: @element.xml_id
54
+ }
55
+
56
+ # Extract refmeta info
57
+ if @element.refmeta
58
+ result[:refentrytitle] = @element.refmeta.refentrytitle&.content
59
+ result[:refmiscinfo] = @element.refmeta.refmiscinfo&.map(&:content)
60
+ end
61
+
62
+ # Extract refnamediv info
63
+ if @element.refnamediv
64
+ result[:refname] = @element.refnamediv.refname&.map(&:content)&.join(" ")
65
+ result[:refpurpose] = @element.refnamediv.refpurpose&.content
66
+ result[:refclass] = @element.refnamediv.refclass&.content
67
+ end
68
+
69
+ # Convert refsection content
70
+ if @element.refsection&.any?
71
+ result[:sections] = @element.refsection.map do |rs|
72
+ section_content_to_h(rs)
73
+ end
74
+ end
75
+
76
+ result
77
+ end
78
+
79
+ def section_content_to_h(section)
80
+ children = []
81
+
82
+ # Process all child elements
83
+ if section.respond_to?(:elements)
84
+ section.elements.each do |el|
85
+ children << ElementToHash.new(el).to_h
86
+ end
87
+ end
88
+
89
+ { type: "section", children: children }
90
+ end
91
+
92
+ def para_to_h
93
+ content = extract_inline_content(@element)
94
+ {
95
+ type: "para",
96
+ content: content
97
+ }
98
+ end
99
+
100
+ def variablelist_to_h
101
+ {
102
+ type: "variablelist",
103
+ items: @element.varlistentry.map do |ve|
104
+ terms = ve.term&.map { |t| extract_inline_content(t) } || []
105
+ definition = if ve.listitem&.respond_to?(:para)
106
+ ve.listitem.para.map { |p| extract_inline_content(p) }
107
+ elsif ve.listitem&.respond_to?(:elements)
108
+ ve.listitem.elements.map { |e| ElementToHash.new(e).to_h }
109
+ else
110
+ []
111
+ end
112
+ { terms: terms, definition: definition }
113
+ end
114
+ }
115
+ end
116
+
117
+ def orderedlist_to_h
118
+ {
119
+ type: "orderedlist",
120
+ items: @element.listitem.map do |li|
121
+ items = []
122
+ if li.respond_to?(:para)
123
+ items = li.para.map { |p| extract_inline_content(p) }
124
+ elsif li.respond_to?(:elements)
125
+ items = li.elements.map { |e| ElementToHash.new(e).to_h }
126
+ end
127
+ items
128
+ end
129
+ }
130
+ end
131
+
132
+ def itemizedlist_to_h
133
+ {
134
+ type: "itemizedlist",
135
+ items: @element.listitem.map do |li|
136
+ items = []
137
+ if li.respond_to?(:para)
138
+ items = li.para.map { |p| extract_inline_content(p) }
139
+ elsif li.respond_to?(:elements)
140
+ items = li.elements.map { |e| ElementToHash.new(e).to_h }
141
+ end
142
+ items
143
+ end
144
+ }
145
+ end
146
+
147
+ def codelike_to_h
148
+ code_text = build_code_text
149
+ {
150
+ type: "code",
151
+ text: code_text,
152
+ language: @element.language
153
+ }
154
+ end
155
+
156
+ def example_to_h
157
+ {
158
+ type: "example",
159
+ content: @element.elements.map { |e| ElementToHash.new(e).to_h }
160
+ }
161
+ end
162
+
163
+ def figure_to_h
164
+ result = {
165
+ type: "figure",
166
+ id: @element.xml_id
167
+ }
168
+
169
+ if @element.respond_to?(:mediaobject) && @element.mediaobject
170
+ obj = @element.mediaobject
171
+ if obj.respond_to?(:imageobject) && obj.imageobject&.first
172
+ imagedata = obj.imageobject.first.imagedata
173
+ result[:src] = imagedata&.fileref
174
+ result[:alt] = obj.imageobject.first.textobject&.phrase&.content
175
+ end
176
+ end
177
+
178
+ result
179
+ end
180
+
181
+ def table_to_h
182
+ # Simplified table representation
183
+ {
184
+ type: "table",
185
+ id: @element.xml_id,
186
+ rows: [] # Would need complex traversal
187
+ }
188
+ end
189
+
190
+ def admonition_to_h
191
+ title = @element.respond_to?(:title) ? @element.title&.content : nil
192
+ content = @element.elements.map { |e| ElementToHash.new(e).to_h } if @element.elements
193
+
194
+ {
195
+ type: @element.class.name.split('::').last.downcase,
196
+ title: title,
197
+ content: content
198
+ }
199
+ end
200
+
201
+ def blockquote_to_h
202
+ attribution = @element.attribution&.content if @element.attribution
203
+ content = @element.elements.map { |e| ElementToHash.new(e).to_h } if @element.elements
204
+
205
+ {
206
+ type: "blockquote",
207
+ attribution: attribution,
208
+ content: content
209
+ }
210
+ end
211
+
212
+ def generic_to_h
213
+ # Default: try to use as_json
214
+ if @element.respond_to?(:as_json)
215
+ @element.as_json.merge(type: @element.class.name.split('::').last.downcase)
216
+ else
217
+ { type: "unknown", content: @element.to_s }
218
+ end
219
+ end
220
+
221
+ def extract_inline_content(element)
222
+ return [] unless element
223
+
224
+ content = []
225
+ if element.respond_to?(:content)
226
+ c = element.content
227
+ if c.is_a?(Array)
228
+ c.each { |item| content << process_inline_item(item) }
229
+ elsif c.is_a?(String)
230
+ content << { type: "text", value: c }
231
+ end
232
+ elsif element.respond_to?(:elements)
233
+ element.elements.each do |el|
234
+ content << process_inline_item(el)
235
+ end
236
+ end
237
+ content
238
+ end
239
+
240
+ def process_inline_item(item)
241
+ case item
242
+ when String
243
+ { type: "text", value: item }
244
+ when Elements::Emphasis
245
+ {
246
+ type: "emphasis",
247
+ role: item.role,
248
+ content: extract_inline_content(item)
249
+ }
250
+ when Elements::Link
251
+ {
252
+ type: "link",
253
+ href: item.href,
254
+ content: extract_inline_content(item)
255
+ }
256
+ when Elements::Xref
257
+ {
258
+ type: "xref",
259
+ linkend: item.linkend,
260
+ content: item.content || item.linkend
261
+ }
262
+ when Elements::Literal, Elements::Code
263
+ { type: "codetext", value: item.content }
264
+ when Elements::Tag
265
+ { type: "tag", value: "<#{item.content}>" }
266
+ when Elements::Quote
267
+ { type: "quote", value: item.content }
268
+ else
269
+ if item.respond_to?(:content)
270
+ { type: "text", value: item.content.to_s }
271
+ else
272
+ { type: "text", value: item.to_s }
273
+ end
274
+ end
275
+ end
276
+
277
+ def build_code_text
278
+ if @element.respond_to?(:content)
279
+ c = @element.content
280
+ c.is_a?(Array) ? c.join : c.to_s
281
+ else
282
+ ""
283
+ end
284
+ end
285
+ end
286
+ end
287
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docbook
4
+ module Services
5
+ # Generates index entries from IndexTerm elements in the document
6
+ class IndexGenerator
7
+ def initialize(document)
8
+ @document = document
9
+ @entries = []
10
+ end
11
+
12
+ def generate
13
+ @entries = []
14
+ collect_index_terms(@document)
15
+ sort_entries!
16
+ @entries
17
+ end
18
+
19
+ private
20
+
21
+ def collect_index_terms(element, section_info = {})
22
+ return unless element
23
+
24
+ # Extract section info from current element
25
+ section_info = extract_section_info(element) if section_info.empty?
26
+
27
+ # Process index terms at this level
28
+ process_index_terms(element, section_info)
29
+
30
+ # Recurse into children
31
+ if element.respond_to?(:elements)
32
+ Array(element.elements).each do |child|
33
+ collect_index_terms(child, section_info)
34
+ end
35
+ end
36
+
37
+ # Process nested sections
38
+ if element.respond_to?(:section)
39
+ Array(element.section).each do |section|
40
+ collect_index_terms(section, section_info)
41
+ end
42
+ end
43
+ end
44
+
45
+ def extract_section_info(element)
46
+ {
47
+ section_id: element.xml_id || generate_id(element),
48
+ section_title: get_element_title(element)
49
+ }
50
+ end
51
+
52
+ def get_element_title(element)
53
+ case element
54
+ when Elements::RefEntry
55
+ element.refnamediv&.refname&.map(&:content)&.join(" ") ||
56
+ element.refmeta&.refentrytitle&.content
57
+ else
58
+ element.respond_to?(:title) && element.title&.content ||
59
+ element.respond_to?(:info) && element.info&.title&.content
60
+ end
61
+ end
62
+
63
+ def generate_id(element)
64
+ "section-#{element.class.name.split('::').last.downcase}-#{element.object_id}"
65
+ end
66
+
67
+ def process_index_terms(element, section_info)
68
+ # Find indexterm elements
69
+ index_terms = find_index_terms(element)
70
+
71
+ index_terms.each do |term|
72
+ entry = build_index_entry(term, section_info)
73
+ @entries << entry if entry
74
+ end
75
+ end
76
+
77
+ def find_index_terms(element)
78
+ return [] unless element
79
+
80
+ terms = []
81
+ if element.is_a?(Elements::IndexTerm)
82
+ terms << element
83
+ end
84
+
85
+ # Check for nested index terms
86
+ if element.respond_to?(:indexterm)
87
+ terms.concat(Array(element.indexterm))
88
+ end
89
+
90
+ terms
91
+ end
92
+
93
+ def build_index_entry(index_term, section_info)
94
+ primary = extract_primary(index_term)
95
+ return nil unless primary
96
+
97
+ entry = Models::IndexEntry.new(
98
+ primary: primary,
99
+ section_id: section_info[:section_id],
100
+ section_title: section_info[:section_title],
101
+ sort_key: primary.downcase.strip
102
+ )
103
+
104
+ # Extract secondary terms
105
+ if index_term.respond_to?(:secondary)
106
+ entry.secondary = Array(index_term.secondary).map(&:content).compact
107
+ end
108
+
109
+ # Extract see-also
110
+ if index_term.respond_to?(:see_also)
111
+ entry.see_also = Array(index_term.see_also).map(&:content).compact
112
+ end
113
+
114
+ entry
115
+ end
116
+
117
+ def extract_primary(index_term)
118
+ # Primary is typically in the content or primary attribute
119
+ if index_term.respond_to?(:primary) && index_term.primary
120
+ Array(index_term.primary).map(&:content).join(" ")
121
+ elsif index_term.respond_to?(:content)
122
+ content = index_term.content
123
+ content.is_a?(Array) ? content.join(" ") : content.to_s
124
+ elsif index_term.respond_to?(:to_s)
125
+ index_term.to_s
126
+ end
127
+ end
128
+
129
+ def sort_entries!
130
+ @entries.sort_by!(&:sort_key)
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docbook
4
+ module Services
5
+ # Pre-computes section numbering for the document
6
+ # Handles: Roman numerals for Parts, Arabic for Chapters,
7
+ # Hierarchical for Sections, Alpha for Appendices
8
+ class NumberingService
9
+ def initialize(document)
10
+ @document = document
11
+ @numbering = []
12
+ @part_counter = 0
13
+ @chapter_counters = {} # part_index => counter
14
+ @appendix_counter = 0
15
+ @section_counters = {} # scope_id => [level1, level2, ...]
16
+ end
17
+
18
+ def generate
19
+ @numbering = []
20
+ @part_counter = 0
21
+ @chapter_counters = {}
22
+ @appendix_counter = 0
23
+ @section_counters = {}
24
+
25
+ process_document(@document)
26
+ @numbering
27
+ end
28
+
29
+ private
30
+
31
+ def process_document(element, parent_info = {})
32
+ return unless element
33
+
34
+ case element
35
+ when Elements::Part
36
+ process_part(element, parent_info)
37
+ when Elements::Chapter
38
+ process_chapter(element, parent_info)
39
+ when Elements::Appendix
40
+ process_appendix(element, parent_info)
41
+ when Elements::Section
42
+ process_section(element, parent_info)
43
+ when Elements::RefEntry
44
+ process_refentry(element, parent_info)
45
+ else
46
+ # Process children
47
+ process_children(element, parent_info)
48
+ end
49
+ end
50
+
51
+ def process_part(part, parent_info)
52
+ @part_counter += 1
53
+ part_number = roman_numeral(@part_counter)
54
+
55
+ add_numbering(part, part_number, "part")
56
+
57
+ # Track chapters within this part
58
+ @chapter_counters[@part_counter] = 0
59
+
60
+ process_children(part, parent_info.merge(part_counter: @part_counter))
61
+ end
62
+
63
+ def process_chapter(chapter, parent_info)
64
+ part_idx = parent_info[:part_counter] || 0
65
+ @chapter_counters[part_idx] ||= 0
66
+ @chapter_counters[part_idx] += 1
67
+ chapter_number = @chapter_counters[part_idx].to_s
68
+
69
+ add_numbering(chapter, chapter_number, "chapter")
70
+
71
+ # Reset section counters for this chapter
72
+ scope_id = chapter.xml_id || "chapter-#{chapter_number}"
73
+ @section_counters[scope_id] = [0, 0, 0, 0, 0]
74
+
75
+ process_children(chapter, parent_info.merge(chapter_scope: scope_id))
76
+ end
77
+
78
+ def process_appendix(appendix, parent_info)
79
+ @appendix_counter += 1
80
+ appendix_number = alpha_numeral(@appendix_counter)
81
+
82
+ add_numbering(appendix, appendix_number, "appendix")
83
+
84
+ # Reset section counters for this appendix
85
+ scope_id = appendix.xml_id || "appendix-#{appendix_number}"
86
+ @section_counters[scope_id] = [0, 0, 0, 0, 0]
87
+
88
+ process_children(appendix, parent_info.merge(appendix_scope: scope_id))
89
+ end
90
+
91
+ def process_section(section, parent_info)
92
+ # Determine scope
93
+ scope_id = parent_info[:chapter_scope] ||
94
+ parent_info[:appendix_scope] ||
95
+ section.xml_id ||
96
+ "section-#{@numbering.length}"
97
+
98
+ # Initialize counters for this scope
99
+ @section_counters[scope_id] ||= [0, 0, 0, 0, 0]
100
+
101
+ # Count nesting depth
102
+ depth = section_depth(section)
103
+
104
+ # Increment counter at this depth
105
+ @section_counters[scope_id][depth - 1] += 1
106
+
107
+ # Reset counters for deeper levels
108
+ (depth...5).each { |i| @section_counters[scope_id][i] = 0 }
109
+
110
+ # Build number string
111
+ numbers = @section_counters[scope_id].first(depth)
112
+ section_number = numbers.join(".")
113
+
114
+ add_numbering(section, section_number, "section")
115
+
116
+ process_children(section, parent_info)
117
+ end
118
+
119
+ def process_refentry(refentry, parent_info)
120
+ refname = if refentry.refnamediv&.refname
121
+ refentry.refnamediv.refname.map(&:content).join(" ")
122
+ elsif refentry.refmeta&.refentrytitle
123
+ refentry.refmeta.refentrytitle.content
124
+ else
125
+ refentry.xml_id || "refentry"
126
+ end
127
+
128
+ add_numbering(refentry, refname, "refentry")
129
+ end
130
+
131
+ def process_children(element, parent_info)
132
+ return unless element.respond_to?(:elements)
133
+
134
+ Array(element.elements).each do |child|
135
+ process_document(child, parent_info)
136
+ end
137
+ end
138
+
139
+ def section_depth(section)
140
+ # Determine section nesting depth
141
+ depth = 1
142
+ el = section
143
+ while el.respond_to?(:parent) && el.parent
144
+ depth += 1
145
+ el = el.parent
146
+ end
147
+ [depth, 5].min
148
+ end
149
+
150
+ def add_numbering(element, number, type)
151
+ @numbering << Models::SectionNumber.new(
152
+ id: element.xml_id || "element-#{@numbering.length}",
153
+ number: number.to_s,
154
+ type: type
155
+ )
156
+ end
157
+
158
+ def roman_numeral(num)
159
+ result = +""
160
+ roman_map = {
161
+ 1000 => 'M', 900 => 'CM', 500 => 'D', 400 => 'CD',
162
+ 100 => 'C', 90 => 'XC', 50 => 'L', 40 => 'XL',
163
+ 10 => 'X', 9 => 'IX', 5 => 'V', 4 => 'IV', 1 => 'I'
164
+ }
165
+ remaining = num
166
+ roman_map.each do |value, letter|
167
+ while remaining >= value
168
+ result << letter
169
+ remaining -= value
170
+ end
171
+ end
172
+ result
173
+ end
174
+
175
+ def alpha_numeral(num)
176
+ result = ""
177
+ while num > 0
178
+ num -= 1
179
+ result = (('A'.ord + (num % 26)).chr) + result
180
+ num /= 26
181
+ end
182
+ result
183
+ end
184
+ end
185
+ end
186
+ end