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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/CLAUDE.md +19 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/README.adoc +335 -0
- data/Rakefile +12 -0
- data/docs/.lycheeignore +33 -0
- data/docs/Gemfile +10 -0
- data/docs/INDEX.adoc +67 -0
- data/docs/_config.yml +186 -0
- data/docs/advanced/element-classes.adoc +185 -0
- data/docs/advanced/frontend-customization.adoc +193 -0
- data/docs/advanced/index.adoc +14 -0
- data/docs/advanced/templates.adoc +123 -0
- data/docs/features/element-coverage.adoc +373 -0
- data/docs/features/html-output/data-model.adoc +285 -0
- data/docs/features/html-output/directory-mode.adoc +180 -0
- data/docs/features/html-output/index.adoc +90 -0
- data/docs/features/html-output/single-file-mode.adoc +125 -0
- data/docs/features/index-generation.adoc +197 -0
- data/docs/features/index.adoc +63 -0
- data/docs/features/numbering.adoc +183 -0
- data/docs/features/toc-generation.adoc +150 -0
- data/docs/features/xinclude/fragid-schemes.adoc +287 -0
- data/docs/features/xinclude/index.adoc +119 -0
- data/docs/features/xinclude/text-includes.adoc +123 -0
- data/docs/features/xinclude/xml-includes.adoc +167 -0
- data/docs/getting-started/index.adoc +50 -0
- data/docs/getting-started/installation.adoc +113 -0
- data/docs/getting-started/quick-start.adoc +161 -0
- data/docs/guides/converting-article.adoc +188 -0
- data/docs/guides/converting-book.adoc +192 -0
- data/docs/guides/index.adoc +12 -0
- data/docs/guides/roundtrip-testing.adoc +129 -0
- data/docs/interfaces/cli/format-command.adoc +109 -0
- data/docs/interfaces/cli/index.adoc +73 -0
- data/docs/interfaces/cli/roundtrip-command.adoc +125 -0
- data/docs/interfaces/cli/to-html-command.adoc +178 -0
- data/docs/interfaces/cli/validate-command.adoc +104 -0
- data/docs/interfaces/index.adoc +101 -0
- data/docs/interfaces/ruby-api/html-output.adoc +186 -0
- data/docs/interfaces/ruby-api/index.adoc +111 -0
- data/docs/interfaces/ruby-api/parsing.adoc +202 -0
- data/docs/interfaces/ruby-api/xinclude.adoc +162 -0
- data/docs/interfaces/ruby-api/xref-resolution.adoc +156 -0
- data/docs/lychee.toml +42 -0
- data/docs/reference/cli-options.adoc +155 -0
- data/docs/reference/content-block-types.adoc +243 -0
- data/docs/reference/glossary.adoc +119 -0
- data/docs/reference/index.adoc +12 -0
- data/docs/reference/supported-elements.adoc +749 -0
- data/docs/understanding/architecture.adoc +145 -0
- data/docs/understanding/content-pipeline.adoc +102 -0
- data/docs/understanding/data-models.adoc +156 -0
- data/docs/understanding/index.adoc +34 -0
- data/exe/docbook +7 -0
- data/frontend/dist/app.css +1 -0
- data/frontend/dist/app.iife.js +24 -0
- data/frontend/package-lock.json +1445 -0
- data/frontend/package.json +22 -0
- data/frontend/src/App.vue +230 -0
- data/frontend/src/app.ts +8 -0
- data/frontend/src/components/AppSidebar.vue +116 -0
- data/frontend/src/components/AppendixSection.vue +39 -0
- data/frontend/src/components/BlockRenderer.vue +358 -0
- data/frontend/src/components/ChapterSection.vue +32 -0
- data/frontend/src/components/ContentRenderer.vue +13 -0
- data/frontend/src/components/EbookContainer.vue +147 -0
- data/frontend/src/components/EbookTopBar.vue +116 -0
- data/frontend/src/components/PartSection.vue +44 -0
- data/frontend/src/components/ReferenceEntry.vue +80 -0
- data/frontend/src/components/SearchModal.vue +286 -0
- data/frontend/src/components/SectionContent.vue +31 -0
- data/frontend/src/components/SettingsPanel.vue +236 -0
- data/frontend/src/components/TocTreeItem.vue +135 -0
- data/frontend/src/composables/useEbookStore.ts +191 -0
- data/frontend/src/composables/useSearch.ts +249 -0
- data/frontend/src/env.d.ts +7 -0
- data/frontend/src/stores/documentStore.ts +221 -0
- data/frontend/src/stores/uiStore.ts +98 -0
- data/frontend/src/styles.css +253 -0
- data/frontend/tsconfig.json +24 -0
- data/frontend/tsconfig.node.json +11 -0
- data/frontend/vite.config.ts +30 -0
- data/lib/docbook/cli.rb +123 -0
- data/lib/docbook/document.rb +67 -0
- data/lib/docbook/elements/abbrev.rb +16 -0
- data/lib/docbook/elements/acknowledgements.rb +22 -0
- data/lib/docbook/elements/address.rb +18 -0
- data/lib/docbook/elements/alt.rb +16 -0
- data/lib/docbook/elements/annotation.rb +18 -0
- data/lib/docbook/elements/appendix.rb +34 -0
- data/lib/docbook/elements/article.rb +31 -0
- data/lib/docbook/elements/att.rb +15 -0
- data/lib/docbook/elements/attribution.rb +20 -0
- data/lib/docbook/elements/audioobject.rb +18 -0
- data/lib/docbook/elements/author.rb +18 -0
- data/lib/docbook/elements/bibliography.rb +24 -0
- data/lib/docbook/elements/bibliomixed.rb +40 -0
- data/lib/docbook/elements/biblioref.rb +20 -0
- data/lib/docbook/elements/blockquote.rb +26 -0
- data/lib/docbook/elements/book.rb +36 -0
- data/lib/docbook/elements/buildtarget.rb +16 -0
- data/lib/docbook/elements/callout.rb +22 -0
- data/lib/docbook/elements/calloutlist.rb +22 -0
- data/lib/docbook/elements/caution.rb +30 -0
- data/lib/docbook/elements/chapter.rb +62 -0
- data/lib/docbook/elements/citation.rb +16 -0
- data/lib/docbook/elements/citerefentry.rb +26 -0
- data/lib/docbook/elements/citetitle.rb +20 -0
- data/lib/docbook/elements/classname.rb +16 -0
- data/lib/docbook/elements/code.rb +16 -0
- data/lib/docbook/elements/colophon.rb +22 -0
- data/lib/docbook/elements/computeroutput.rb +18 -0
- data/lib/docbook/elements/copyright.rb +17 -0
- data/lib/docbook/elements/danger.rb +28 -0
- data/lib/docbook/elements/date.rb +16 -0
- data/lib/docbook/elements/dedication.rb +22 -0
- data/lib/docbook/elements/dir.rb +16 -0
- data/lib/docbook/elements/emphasis.rb +18 -0
- data/lib/docbook/elements/entry.rb +30 -0
- data/lib/docbook/elements/entrytbl.rb +22 -0
- data/lib/docbook/elements/equation.rb +26 -0
- data/lib/docbook/elements/example.rb +30 -0
- data/lib/docbook/elements/fieldsynopsis.rb +21 -0
- data/lib/docbook/elements/figure.rb +28 -0
- data/lib/docbook/elements/filename.rb +16 -0
- data/lib/docbook/elements/firstname.rb +16 -0
- data/lib/docbook/elements/firstterm.rb +18 -0
- data/lib/docbook/elements/footnote.rb +22 -0
- data/lib/docbook/elements/footnoteref.rb +21 -0
- data/lib/docbook/elements/foreignphrase.rb +18 -0
- data/lib/docbook/elements/formalpara.rb +20 -0
- data/lib/docbook/elements/function.rb +16 -0
- data/lib/docbook/elements/glossary.rb +24 -0
- data/lib/docbook/elements/glossdef.rb +18 -0
- data/lib/docbook/elements/glossentry.rb +26 -0
- data/lib/docbook/elements/glosssee.rb +18 -0
- data/lib/docbook/elements/glossseealso.rb +18 -0
- data/lib/docbook/elements/glossterm.rb +18 -0
- data/lib/docbook/elements/holder.rb +16 -0
- data/lib/docbook/elements/honorific.rb +16 -0
- data/lib/docbook/elements/imagedata.rb +29 -0
- data/lib/docbook/elements/imageobject.rb +22 -0
- data/lib/docbook/elements/important.rb +30 -0
- data/lib/docbook/elements/index.rb +26 -0
- data/lib/docbook/elements/indexdiv.rb +22 -0
- data/lib/docbook/elements/indexentry.rb +22 -0
- data/lib/docbook/elements/indexterm.rb +34 -0
- data/lib/docbook/elements/info.rb +25 -0
- data/lib/docbook/elements/informalexample.rb +24 -0
- data/lib/docbook/elements/informalfigure.rb +20 -0
- data/lib/docbook/elements/inlinemediaobject.rb +22 -0
- data/lib/docbook/elements/itemizedlist.rb +21 -0
- data/lib/docbook/elements/legalnotice.rb +22 -0
- data/lib/docbook/elements/link.rb +26 -0
- data/lib/docbook/elements/listitem.rb +32 -0
- data/lib/docbook/elements/literal.rb +16 -0
- data/lib/docbook/elements/literallayout.rb +22 -0
- data/lib/docbook/elements/mediaobject.rb +26 -0
- data/lib/docbook/elements/msg.rb +20 -0
- data/lib/docbook/elements/msgexplan.rb +22 -0
- data/lib/docbook/elements/msgset.rb +22 -0
- data/lib/docbook/elements/note.rb +30 -0
- data/lib/docbook/elements/orderedlist.rb +23 -0
- data/lib/docbook/elements/orgname.rb +16 -0
- data/lib/docbook/elements/para.rb +61 -0
- data/lib/docbook/elements/paragraph_like.rb +18 -0
- data/lib/docbook/elements/parameter.rb +16 -0
- data/lib/docbook/elements/part.rb +38 -0
- data/lib/docbook/elements/personname.rb +22 -0
- data/lib/docbook/elements/phrase.rb +20 -0
- data/lib/docbook/elements/preface.rb +58 -0
- data/lib/docbook/elements/primary.rb +16 -0
- data/lib/docbook/elements/procedure.rb +24 -0
- data/lib/docbook/elements/productname.rb +18 -0
- data/lib/docbook/elements/productnumber.rb +16 -0
- data/lib/docbook/elements/programlisting.rb +28 -0
- data/lib/docbook/elements/property.rb +16 -0
- data/lib/docbook/elements/pubdate.rb +16 -0
- data/lib/docbook/elements/publishername.rb +16 -0
- data/lib/docbook/elements/quotation.rb +24 -0
- data/lib/docbook/elements/quote.rb +18 -0
- data/lib/docbook/elements/refentry.rb +32 -0
- data/lib/docbook/elements/refentrytitle.rb +16 -0
- data/lib/docbook/elements/reference.rb +26 -0
- data/lib/docbook/elements/refmeta.rb +29 -0
- data/lib/docbook/elements/refmiscinfo.rb +16 -0
- data/lib/docbook/elements/refname.rb +16 -0
- data/lib/docbook/elements/refnamediv.rb +22 -0
- data/lib/docbook/elements/refpurpose.rb +16 -0
- data/lib/docbook/elements/refsect1.rb +22 -0
- data/lib/docbook/elements/refsect2.rb +22 -0
- data/lib/docbook/elements/refsect3.rb +22 -0
- data/lib/docbook/elements/refsection.rb +32 -0
- data/lib/docbook/elements/releaseinfo.rb +16 -0
- data/lib/docbook/elements/remark.rb +20 -0
- data/lib/docbook/elements/replaceable.rb +16 -0
- data/lib/docbook/elements/row.rb +15 -0
- data/lib/docbook/elements/screen.rb +28 -0
- data/lib/docbook/elements/secondary.rb +16 -0
- data/lib/docbook/elements/sect1.rb +26 -0
- data/lib/docbook/elements/sect2.rb +24 -0
- data/lib/docbook/elements/sect3.rb +24 -0
- data/lib/docbook/elements/sect4.rb +24 -0
- data/lib/docbook/elements/sect5.rb +22 -0
- data/lib/docbook/elements/section.rb +66 -0
- data/lib/docbook/elements/see.rb +16 -0
- data/lib/docbook/elements/seealso.rb +18 -0
- data/lib/docbook/elements/set.rb +26 -0
- data/lib/docbook/elements/setindex.rb +24 -0
- data/lib/docbook/elements/sidebar.rb +22 -0
- data/lib/docbook/elements/simplesect.rb +32 -0
- data/lib/docbook/elements/step.rb +26 -0
- data/lib/docbook/elements/substeps.rb +18 -0
- data/lib/docbook/elements/subtitle.rb +16 -0
- data/lib/docbook/elements/surname.rb +16 -0
- data/lib/docbook/elements/table.rb +24 -0
- data/lib/docbook/elements/tag.rb +20 -0
- data/lib/docbook/elements/tbody.rb +15 -0
- data/lib/docbook/elements/term.rb +37 -0
- data/lib/docbook/elements/tertiary.rb +16 -0
- data/lib/docbook/elements/textobject.rb +18 -0
- data/lib/docbook/elements/tfoot.rb +15 -0
- data/lib/docbook/elements/tgroup.rb +21 -0
- data/lib/docbook/elements/thead.rb +15 -0
- data/lib/docbook/elements/tip.rb +30 -0
- data/lib/docbook/elements/title.rb +16 -0
- data/lib/docbook/elements/toc.rb +24 -0
- data/lib/docbook/elements/tocdiv.rb +22 -0
- data/lib/docbook/elements/tocentry.rb +20 -0
- data/lib/docbook/elements/topic.rb +26 -0
- data/lib/docbook/elements/type.rb +16 -0
- data/lib/docbook/elements/uri.rb +16 -0
- data/lib/docbook/elements/userinput.rb +18 -0
- data/lib/docbook/elements/variable.rb +16 -0
- data/lib/docbook/elements/variablelist.rb +19 -0
- data/lib/docbook/elements/varlistentry.rb +17 -0
- data/lib/docbook/elements/version.rb +16 -0
- data/lib/docbook/elements/videoobject.rb +18 -0
- data/lib/docbook/elements/warning.rb +30 -0
- data/lib/docbook/elements/xref.rb +18 -0
- data/lib/docbook/elements/year.rb +16 -0
- data/lib/docbook/elements.rb +204 -0
- data/lib/docbook/models/document_metadata.rb +30 -0
- data/lib/docbook/models/document_root.rb +33 -0
- data/lib/docbook/models/index_entry.rb +28 -0
- data/lib/docbook/models/reading_position.rb +22 -0
- data/lib/docbook/models/section_number.rb +18 -0
- data/lib/docbook/models/section_root.rb +29 -0
- data/lib/docbook/models/toc_node.rb +24 -0
- data/lib/docbook/models.rb +16 -0
- data/lib/docbook/output/data.rb +196 -0
- data/lib/docbook/output/html.rb +1450 -0
- data/lib/docbook/output/index_generator.rb +281 -0
- data/lib/docbook/output.rb +8 -0
- data/lib/docbook/services/document_builder.rb +258 -0
- data/lib/docbook/services/element_to_hash.rb +287 -0
- data/lib/docbook/services/index_generator.rb +134 -0
- data/lib/docbook/services/numbering_service.rb +186 -0
- data/lib/docbook/services/toc_generator.rb +138 -0
- data/lib/docbook/services.rb +14 -0
- data/lib/docbook/templates/document.html.liquid +20 -0
- data/lib/docbook/templates/partials/_head.liquid +39 -0
- data/lib/docbook/templates/partials/_scripts.liquid +10 -0
- data/lib/docbook/version.rb +5 -0
- data/lib/docbook/xinclude_resolver.rb +217 -0
- data/lib/docbook/xref_resolver.rb +78 -0
- data/lib/docbook.rb +17 -0
- data/sig/docbook.rbs +4 -0
- metadata +385 -0
|
@@ -0,0 +1,1450 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "erb"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "liquid"
|
|
6
|
+
require "marcel"
|
|
7
|
+
require_relative "data"
|
|
8
|
+
require_relative "index_generator"
|
|
9
|
+
|
|
10
|
+
module Docbook
|
|
11
|
+
module Output
|
|
12
|
+
TEMPLATE_PATH = File.join(__dir__, "..", "templates", "document.html.liquid")
|
|
13
|
+
TEMPLATES_ROOT = File.join(__dir__, "..", "templates")
|
|
14
|
+
FRONTEND_ROOT = File.expand_path("../../../frontend/dist", __dir__)
|
|
15
|
+
|
|
16
|
+
# Configure Liquid template engine with file system for includes
|
|
17
|
+
# Liquid::LocalFileSystem root should be the templates directory
|
|
18
|
+
# When using {% include 'partials/head' %}, it looks for {root}/partials/_head.liquid
|
|
19
|
+
Liquid::Environment.default.file_system = Liquid::LocalFileSystem.new(TEMPLATES_ROOT)
|
|
20
|
+
|
|
21
|
+
class Html
|
|
22
|
+
TEMPLATE_PATH = File.join(__dir__, "..", "templates", "document.html.liquid")
|
|
23
|
+
FRONTEND_ROOT = File.expand_path("../../../frontend/dist", __dir__)
|
|
24
|
+
|
|
25
|
+
def initialize(document, xref_resolver: nil, output_mode: :single_file, base_path: nil, output_path: nil)
|
|
26
|
+
@document = document
|
|
27
|
+
@xref_resolver = xref_resolver
|
|
28
|
+
@output_mode = output_mode
|
|
29
|
+
@base_path = base_path
|
|
30
|
+
@output_path = output_path
|
|
31
|
+
@image_cache = {}
|
|
32
|
+
@index_collector = IndexCollector.new(document)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def to_html
|
|
36
|
+
sections_data = collect_sections
|
|
37
|
+
numbering_hash = build_numbering_map(sections_data)
|
|
38
|
+
all_index_terms = @index_collector.collect
|
|
39
|
+
content_hash = build_content_map(sections_data, all_index_terms)
|
|
40
|
+
|
|
41
|
+
# Build the output model
|
|
42
|
+
output = DocbookOutput.new(
|
|
43
|
+
title: extract_title,
|
|
44
|
+
toc: Toc.new(
|
|
45
|
+
sections: sections_data,
|
|
46
|
+
numbering: numbering_hash.map { |id, value| NumberingEntry.new(id: id, value: value) }
|
|
47
|
+
),
|
|
48
|
+
content: ContentData.new(
|
|
49
|
+
entries: content_hash.map { |key, value| ContentEntry.new(key: key, value: value) }
|
|
50
|
+
),
|
|
51
|
+
index: build_index(all_index_terms)
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
template_content = File.read(TEMPLATE_PATH)
|
|
55
|
+
|
|
56
|
+
if @output_mode == :directory
|
|
57
|
+
# Write full DocbookOutput as JSON for Vue to fetch
|
|
58
|
+
FileUtils.mkdir_p(@output_path) if @output_path
|
|
59
|
+
data_path = @output_path ? File.join(@output_path, "docbook.data.json") : "docbook.data.json"
|
|
60
|
+
File.write(data_path, output.to_json)
|
|
61
|
+
|
|
62
|
+
template = Liquid::Template.parse(template_content)
|
|
63
|
+
rendered = template.render(
|
|
64
|
+
"docbook_title" => output.title || "DocBook Document",
|
|
65
|
+
"base_url" => base_url,
|
|
66
|
+
"assets_inline" => false,
|
|
67
|
+
"app_css" => File.read(File.join(FRONTEND_ROOT, "app.css")),
|
|
68
|
+
"app_js" => File.read(File.join(FRONTEND_ROOT, "app.iife.js")),
|
|
69
|
+
"data_url" => "docbook.data.json"
|
|
70
|
+
)
|
|
71
|
+
rendered = rendered.gsub('[[DOCBOOK_DATA]]', 'null /* loaded from docbook.data.json */')
|
|
72
|
+
.gsub('[[DOCBOOK_TOC]]', 'null')
|
|
73
|
+
.gsub('[[DOCBOOK_CONTENT]]', 'null')
|
|
74
|
+
.gsub('[[DOCBOOK_INDEX]]', 'null')
|
|
75
|
+
else
|
|
76
|
+
# single_file: embed everything inline via gsub placeholders
|
|
77
|
+
docbook_json = DocumentData.new(title: output.title).to_json.gsub('</script>', '<\\/script>')
|
|
78
|
+
toc_json = output.toc.to_json.gsub('</script>', '<\\/script>')
|
|
79
|
+
content_json = output.content.to_json.gsub('</script>', '<\\/script>')
|
|
80
|
+
index_json = output.index&.to_json&.gsub('</script>', '<\\/script>') || 'null'
|
|
81
|
+
|
|
82
|
+
template = Liquid::Template.parse(template_content)
|
|
83
|
+
rendered = template.render(
|
|
84
|
+
"docbook_title" => output.title || "DocBook Document",
|
|
85
|
+
"base_url" => base_url,
|
|
86
|
+
"assets_inline" => true,
|
|
87
|
+
"app_css" => File.read(File.join(FRONTEND_ROOT, "app.css")),
|
|
88
|
+
"app_js" => File.read(File.join(FRONTEND_ROOT, "app.iife.js"))
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
rendered = rendered.gsub('[[DOCBOOK_DATA]]', docbook_json)
|
|
92
|
+
.gsub('[[DOCBOOK_TOC]]', toc_json)
|
|
93
|
+
.gsub('[[DOCBOOK_CONTENT]]', content_json)
|
|
94
|
+
.gsub('[[DOCBOOK_INDEX]]', index_json)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
# ── Output Mode Helpers ──────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
def base_url
|
|
103
|
+
@output_mode == :directory ? "." : ""
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# ── XRef Resolution (using pre-built resolver) ──────────────────
|
|
107
|
+
|
|
108
|
+
def resolve_xref_text(xref)
|
|
109
|
+
linkend = xref.linkend&.to_s
|
|
110
|
+
return "" unless linkend
|
|
111
|
+
@xref_resolver&.title_for(linkend) || linkend
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# ── Section Data Collection ─────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
def collect_sections
|
|
117
|
+
sections = []
|
|
118
|
+
case @document
|
|
119
|
+
when Docbook::Elements::Book
|
|
120
|
+
each_attr(@document, :preface) do |pf|
|
|
121
|
+
sections << SectionData.new(
|
|
122
|
+
id: element_id(pf),
|
|
123
|
+
title: best_title(pf) || "Preface",
|
|
124
|
+
type: 'preface'
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
each_attr(@document, :part) do |part|
|
|
128
|
+
sections << SectionData.new(
|
|
129
|
+
id: element_id(part),
|
|
130
|
+
title: best_title(part) || "Part",
|
|
131
|
+
type: 'part',
|
|
132
|
+
children: collect_part_children(part)
|
|
133
|
+
)
|
|
134
|
+
end
|
|
135
|
+
each_attr(@document, :chapter) do |ch|
|
|
136
|
+
sections << SectionData.new(
|
|
137
|
+
id: element_id(ch),
|
|
138
|
+
title: best_title(ch) || "Chapter",
|
|
139
|
+
type: 'chapter'
|
|
140
|
+
)
|
|
141
|
+
end
|
|
142
|
+
each_attr(@document, :appendix) do |ap|
|
|
143
|
+
sections << SectionData.new(
|
|
144
|
+
id: element_id(ap),
|
|
145
|
+
title: best_title(ap) || "Appendix",
|
|
146
|
+
type: 'appendix',
|
|
147
|
+
children: collect_appendix_children(ap)
|
|
148
|
+
)
|
|
149
|
+
end
|
|
150
|
+
each_attr(@document, :glossary) do |g|
|
|
151
|
+
sections << SectionData.new(
|
|
152
|
+
id: element_id(g),
|
|
153
|
+
title: best_title(g) || "Glossary",
|
|
154
|
+
type: 'glossary'
|
|
155
|
+
)
|
|
156
|
+
end
|
|
157
|
+
each_attr(@document, :bibliography) do |b|
|
|
158
|
+
sections << SectionData.new(
|
|
159
|
+
id: element_id(b),
|
|
160
|
+
title: best_title(b) || "References",
|
|
161
|
+
type: 'bibliography'
|
|
162
|
+
)
|
|
163
|
+
end
|
|
164
|
+
each_attr(@document, :index) do |idx|
|
|
165
|
+
sections << SectionData.new(
|
|
166
|
+
id: element_id(idx),
|
|
167
|
+
title: best_title(idx) || "Index",
|
|
168
|
+
type: 'index'
|
|
169
|
+
)
|
|
170
|
+
end
|
|
171
|
+
when Docbook::Elements::Article
|
|
172
|
+
each_attr(@document, :section) do |s|
|
|
173
|
+
sections << SectionData.new(
|
|
174
|
+
id: element_id(s),
|
|
175
|
+
title: best_title(s) || "Section",
|
|
176
|
+
type: 'section',
|
|
177
|
+
children: collect_section_children(s)
|
|
178
|
+
)
|
|
179
|
+
end
|
|
180
|
+
when Docbook::Elements::Reference
|
|
181
|
+
# Reference is a top-level element containing refentry children
|
|
182
|
+
each_attr(@document, :refentry) do |re|
|
|
183
|
+
sections << SectionData.new(
|
|
184
|
+
id: element_id(re),
|
|
185
|
+
title: best_title(re) || "Reference Entry",
|
|
186
|
+
type: 'reference'
|
|
187
|
+
)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
sections
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def collect_part_children(part)
|
|
194
|
+
children = []
|
|
195
|
+
each_attr(part, :preface) do |pf|
|
|
196
|
+
children << SectionData.new(
|
|
197
|
+
id: element_id(pf),
|
|
198
|
+
title: best_title(pf) || "Preface",
|
|
199
|
+
type: 'preface',
|
|
200
|
+
children: collect_appendix_children(pf)
|
|
201
|
+
)
|
|
202
|
+
end
|
|
203
|
+
each_attr(part, :chapter) do |ch|
|
|
204
|
+
children << SectionData.new(
|
|
205
|
+
id: element_id(ch),
|
|
206
|
+
title: best_title(ch) || "Chapter",
|
|
207
|
+
type: 'chapter',
|
|
208
|
+
children: collect_section_children(ch)
|
|
209
|
+
)
|
|
210
|
+
end
|
|
211
|
+
each_attr(part, :reference) do |ref|
|
|
212
|
+
children << SectionData.new(
|
|
213
|
+
id: element_id(ref),
|
|
214
|
+
title: best_title(ref) || "Reference",
|
|
215
|
+
type: 'chapter'
|
|
216
|
+
)
|
|
217
|
+
end
|
|
218
|
+
each_attr(part, :appendix) do |ap|
|
|
219
|
+
children << SectionData.new(
|
|
220
|
+
id: element_id(ap),
|
|
221
|
+
title: best_title(ap) || "Appendix",
|
|
222
|
+
type: 'appendix',
|
|
223
|
+
children: collect_appendix_children(ap)
|
|
224
|
+
)
|
|
225
|
+
end
|
|
226
|
+
each_attr(part, :glossary) do |g|
|
|
227
|
+
children << SectionData.new(
|
|
228
|
+
id: element_id(g),
|
|
229
|
+
title: best_title(g) || "Glossary",
|
|
230
|
+
type: 'glossary'
|
|
231
|
+
)
|
|
232
|
+
end
|
|
233
|
+
each_attr(part, :bibliography) do |b|
|
|
234
|
+
children << SectionData.new(
|
|
235
|
+
id: element_id(b),
|
|
236
|
+
title: best_title(b) || "References",
|
|
237
|
+
type: 'bibliography'
|
|
238
|
+
)
|
|
239
|
+
end
|
|
240
|
+
each_attr(part, :index) do |idx|
|
|
241
|
+
children << SectionData.new(
|
|
242
|
+
id: element_id(idx),
|
|
243
|
+
title: best_title(idx) || "Index",
|
|
244
|
+
type: 'index'
|
|
245
|
+
)
|
|
246
|
+
end
|
|
247
|
+
children
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def collect_appendix_children(appendix)
|
|
251
|
+
children = []
|
|
252
|
+
each_attr(appendix, :section) do |s|
|
|
253
|
+
children << SectionData.new(
|
|
254
|
+
id: element_id(s),
|
|
255
|
+
title: best_title(s) || "Section",
|
|
256
|
+
type: 'section',
|
|
257
|
+
children: collect_section_children(s)
|
|
258
|
+
)
|
|
259
|
+
end
|
|
260
|
+
children
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def collect_section_children(parent)
|
|
264
|
+
children = []
|
|
265
|
+
each_attr(parent, :section) do |s|
|
|
266
|
+
children << SectionData.new(
|
|
267
|
+
id: element_id(s),
|
|
268
|
+
title: best_title(s) || "Section",
|
|
269
|
+
type: 'section',
|
|
270
|
+
children: collect_section_children(s)
|
|
271
|
+
)
|
|
272
|
+
end
|
|
273
|
+
children
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# ── Numbering ──────────────────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
def build_numbering_map(sections)
|
|
279
|
+
builder = NumberingBuilder.new
|
|
280
|
+
part_index = 0
|
|
281
|
+
|
|
282
|
+
sections.each do |sec|
|
|
283
|
+
case sec.type
|
|
284
|
+
when 'part'
|
|
285
|
+
part_num = builder.next_part
|
|
286
|
+
builder.set_number(sec.id, part_num)
|
|
287
|
+
# Parts have chapters inside them - use same part_index for all children
|
|
288
|
+
number_section_tree(sec.children, builder, part_index: part_index, section_scope: sec.id, parent_number: part_num)
|
|
289
|
+
part_index += 1
|
|
290
|
+
when 'chapter', 'reference'
|
|
291
|
+
chapter_num = builder.next_chapter(part_index)
|
|
292
|
+
builder.set_number(sec.id, chapter_num)
|
|
293
|
+
# Number children sections scoped to this chapter, prefix with chapter number
|
|
294
|
+
number_section_tree(sec.children, builder, chapter_id: sec.id, chapter_num: chapter_num, part_index: part_index, section_scope: sec.id, parent_number: chapter_num)
|
|
295
|
+
when 'appendix'
|
|
296
|
+
appendix_num = builder.next_appendix
|
|
297
|
+
appendix_full = "Appendix #{appendix_num}"
|
|
298
|
+
builder.set_number(sec.id, appendix_full)
|
|
299
|
+
number_section_tree(sec.children, builder, part_index: part_index, section_scope: sec.id, parent_number: appendix_full)
|
|
300
|
+
else
|
|
301
|
+
# preface, glossary, bibliography, index - no numbering typically
|
|
302
|
+
number_section_tree(sec.children, builder, part_index: part_index)
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
builder.numbering
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Recursively number sections within a chapter
|
|
310
|
+
# @param children [Array<SectionData>] child sections to number
|
|
311
|
+
# @param builder [NumberingBuilder] numbering builder
|
|
312
|
+
# @param chapter_id [String, nil] the chapter xml_id for section scope tracking
|
|
313
|
+
# @param chapter_num [String, nil] the chapter number for prefixing sections
|
|
314
|
+
# @param appendix_prefix [String, nil] the appendix prefix for sections (e.g., "Appendix A")
|
|
315
|
+
# @param part_index [Integer] the part index for chapter numbering scope
|
|
316
|
+
# @param section_scope [String] the xml_id of the parent section for numbering scope
|
|
317
|
+
# @param parent_number [String, nil] the full number of the parent section for building hierarchical numbers
|
|
318
|
+
def number_section_tree(children, builder, chapter_id: nil, chapter_num: nil, appendix_prefix: nil, part_index: 0, section_scope: nil, parent_number: nil)
|
|
319
|
+
return if children.nil?
|
|
320
|
+
children.each do |child|
|
|
321
|
+
case child.type
|
|
322
|
+
when 'section'
|
|
323
|
+
# Scope numbering to this section's ID so siblings share a counter
|
|
324
|
+
scope_id = section_scope || chapter_id || "root_#{part_index}"
|
|
325
|
+
section_num = builder.next_section(scope_id)
|
|
326
|
+
# Build full number from parent chain: chapter_num.parent_num.section_num
|
|
327
|
+
# For appendix_prefix, strip "Appendix " to get just the letter for child numbering
|
|
328
|
+
base_prefix = appendix_prefix ? appendix_prefix.sub(/\AAppendix\s*/, '') : nil
|
|
329
|
+
prefix = base_prefix || parent_number || chapter_num
|
|
330
|
+
full_num = prefix ? "#{prefix}.#{section_num}" : section_num
|
|
331
|
+
builder.set_number(child.id, full_num)
|
|
332
|
+
# Recurse for nested sections, passing this section as the new scope
|
|
333
|
+
number_section_tree(child.children, builder,
|
|
334
|
+
chapter_id: chapter_id, chapter_num: chapter_num, appendix_prefix: appendix_prefix,
|
|
335
|
+
part_index: part_index, section_scope: child.id, parent_number: full_num)
|
|
336
|
+
when 'chapter', 'reference'
|
|
337
|
+
new_chapter_num = builder.next_chapter(part_index)
|
|
338
|
+
builder.set_number(child.id, new_chapter_num)
|
|
339
|
+
number_section_tree(child.children, builder, chapter_id: child.id, chapter_num: new_chapter_num, part_index: part_index, section_scope: child.id, parent_number: new_chapter_num)
|
|
340
|
+
when 'appendix'
|
|
341
|
+
appendix_num = builder.next_appendix
|
|
342
|
+
appendix_full = "Appendix #{appendix_num}"
|
|
343
|
+
builder.set_number(child.id, appendix_full)
|
|
344
|
+
# Prefix appendix sections with "Appendix A.", "Appendix B.", etc.
|
|
345
|
+
number_section_tree(child.children, builder, chapter_id: chapter_id, chapter_num: chapter_num, appendix_prefix: appendix_full, part_index: part_index, section_scope: child.id, parent_number: appendix_full)
|
|
346
|
+
else
|
|
347
|
+
# Other types (preface, glossary, etc.) - no numbering, just recurse
|
|
348
|
+
number_section_tree(child.children, builder, chapter_id: chapter_id, chapter_num: chapter_num, appendix_prefix: appendix_prefix, part_index: part_index, section_scope: section_scope, parent_number: parent_number)
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# ── Content Map Building ───────────────────────────────────────
|
|
354
|
+
|
|
355
|
+
def build_content_map(sections, all_index_terms = [])
|
|
356
|
+
content = {}
|
|
357
|
+
|
|
358
|
+
unless sections.empty?
|
|
359
|
+
collect_all_sections(sections).each do |sec|
|
|
360
|
+
content[sec.id] = build_section_content_data(sec)
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
if @document.is_a?(Docbook::Elements::Article) && sections.empty?
|
|
365
|
+
content["article-content"] = build_article_content_data
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Generate index content if there are index elements
|
|
369
|
+
if @document.respond_to?(:index)
|
|
370
|
+
Array(@document.index).each do |idx|
|
|
371
|
+
index_content = build_index_content(idx, all_index_terms)
|
|
372
|
+
content[idx.xml_id || "index"] = index_content if index_content
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Generate setindex content if present
|
|
377
|
+
if @document.respond_to?(:setindex)
|
|
378
|
+
Array(@document.setindex).each do |sidx|
|
|
379
|
+
index_content = build_setindex_content(sidx, all_index_terms)
|
|
380
|
+
content[sidx.xml_id || "setindex"] = index_content if index_content
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
content
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def build_index(all_index_terms)
|
|
388
|
+
return Index.new(groups: []) if all_index_terms.empty?
|
|
389
|
+
|
|
390
|
+
generator = IndexGenerator.new(all_index_terms, @xref_resolver)
|
|
391
|
+
groups = generator.generate
|
|
392
|
+
|
|
393
|
+
index = Index.new(title: "Index")
|
|
394
|
+
groups.each do |group|
|
|
395
|
+
index_group = IndexGroup.new(letter: group[:letter])
|
|
396
|
+
group[:entries].each do |entry|
|
|
397
|
+
index_group.entries << IndexTerm.new(
|
|
398
|
+
primary: entry[:primary],
|
|
399
|
+
secondary: entry[:secondary],
|
|
400
|
+
tertiary: entry[:tertiary],
|
|
401
|
+
section_id: entry[:section_id],
|
|
402
|
+
section_title: entry[:section_title],
|
|
403
|
+
sort_as: entry[:primary_sort],
|
|
404
|
+
sees: entry[:sees],
|
|
405
|
+
see_alsos: entry[:see_alsos]
|
|
406
|
+
)
|
|
407
|
+
end
|
|
408
|
+
index.groups << index_group
|
|
409
|
+
end
|
|
410
|
+
index
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def build_index_content(index_element, all_index_terms)
|
|
414
|
+
return nil if all_index_terms.empty?
|
|
415
|
+
|
|
416
|
+
index_type = index_element.type
|
|
417
|
+
# Filter indexterms by type
|
|
418
|
+
filtered_terms = all_index_terms.select { |t| t[:type] == index_type }
|
|
419
|
+
|
|
420
|
+
return nil if filtered_terms.empty?
|
|
421
|
+
|
|
422
|
+
generator = IndexGenerator.new(filtered_terms, @xref_resolver)
|
|
423
|
+
index_data = generator.generate
|
|
424
|
+
|
|
425
|
+
section_content = SectionContent.new(section_id: index_element.xml_id || "index")
|
|
426
|
+
section_content.add_block(ContentBlock.new(
|
|
427
|
+
type: :index_section,
|
|
428
|
+
text: index_element.title&.content,
|
|
429
|
+
children: index_data.map { |group| index_group_to_block(group) }
|
|
430
|
+
))
|
|
431
|
+
section_content
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def build_setindex_content(setindex_element, all_index_terms)
|
|
435
|
+
generator = IndexGenerator.new(all_index_terms, @xref_resolver)
|
|
436
|
+
index_data = generator.generate
|
|
437
|
+
|
|
438
|
+
section_content = SectionContent.new(section_id: setindex_element.xml_id || "setindex")
|
|
439
|
+
section_content.add_block(ContentBlock.new(
|
|
440
|
+
type: :index_section,
|
|
441
|
+
text: setindex_element.title&.content,
|
|
442
|
+
children: index_data.map { |group| index_group_to_block(group) }
|
|
443
|
+
))
|
|
444
|
+
section_content
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def index_group_to_block(group)
|
|
448
|
+
block = ContentBlock.new(type: :index_letter, text: group[:letter])
|
|
449
|
+
block.children = group[:entries].map { |entry| index_entry_to_block(entry) }
|
|
450
|
+
block
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def index_entry_to_block(entry)
|
|
454
|
+
block = ContentBlock.new(type: :index_entry)
|
|
455
|
+
|
|
456
|
+
# Primary term
|
|
457
|
+
primary = ContentBlock.new(type: :text, text: entry[:primary])
|
|
458
|
+
block.children << primary
|
|
459
|
+
|
|
460
|
+
# Section link
|
|
461
|
+
if entry[:section_id] && entry[:section_title]
|
|
462
|
+
link = ContentBlock.new(
|
|
463
|
+
type: :index_reference,
|
|
464
|
+
text: entry[:section_title],
|
|
465
|
+
src: "##{entry[:section_id]}"
|
|
466
|
+
)
|
|
467
|
+
block.children << link
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
# See entries
|
|
471
|
+
entry[:sees].each do |see|
|
|
472
|
+
block.children << ContentBlock.new(
|
|
473
|
+
type: :index_see,
|
|
474
|
+
text: "see #{see}"
|
|
475
|
+
)
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# See also entries
|
|
479
|
+
entry[:see_alsos].each do |see_also|
|
|
480
|
+
block.children << ContentBlock.new(
|
|
481
|
+
type: :index_see_also,
|
|
482
|
+
text: "see also #{see_also}"
|
|
483
|
+
)
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
block
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def collect_all_sections(sections)
|
|
490
|
+
result = []
|
|
491
|
+
sections.each do |sec|
|
|
492
|
+
result << sec
|
|
493
|
+
result.concat(collect_all_sections(sec.children)) if sec.children && sec.children.any?
|
|
494
|
+
end
|
|
495
|
+
result
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def build_section_content_data(sec)
|
|
499
|
+
element = find_section_element(sec.id)
|
|
500
|
+
return SectionContent.new(section_id: sec.id) unless element
|
|
501
|
+
|
|
502
|
+
section_content = SectionContent.new(section_id: sec.id)
|
|
503
|
+
# RefEntry has its content in refsection children, not from each_mixed_content
|
|
504
|
+
if element.is_a?(Docbook::Elements::RefEntry)
|
|
505
|
+
section_content.add_block(build_refentry_block(element))
|
|
506
|
+
else
|
|
507
|
+
build_element_content(element, section_content)
|
|
508
|
+
end
|
|
509
|
+
section_content
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
def build_article_content_data
|
|
513
|
+
section_content = SectionContent.new(section_id: "article-content")
|
|
514
|
+
build_element_content(@document, section_content)
|
|
515
|
+
section_content
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def build_element_content(element, section_content)
|
|
519
|
+
# Use each_mixed_content to process elements in document order
|
|
520
|
+
return unless element.respond_to?(:each_mixed_content)
|
|
521
|
+
|
|
522
|
+
element.each_mixed_content do |node|
|
|
523
|
+
case node
|
|
524
|
+
when String
|
|
525
|
+
# Skip pure whitespace strings between elements
|
|
526
|
+
next if node =~ /\A\s*\z/
|
|
527
|
+
|
|
528
|
+
when Docbook::Elements::Para
|
|
529
|
+
section_content.add_block(build_para_block(node))
|
|
530
|
+
|
|
531
|
+
when Docbook::Elements::MediaObject
|
|
532
|
+
process_mediaobject(node, section_content)
|
|
533
|
+
|
|
534
|
+
when Docbook::Elements::ProgramListing
|
|
535
|
+
code_text = build_code_content(node)
|
|
536
|
+
next if code_text.to_s.strip.empty?
|
|
537
|
+
section_content.add_block(ContentBlock.new(
|
|
538
|
+
type: :code,
|
|
539
|
+
text: code_text,
|
|
540
|
+
language: node.language
|
|
541
|
+
))
|
|
542
|
+
|
|
543
|
+
when Docbook::Elements::Screen
|
|
544
|
+
code_text = build_code_content(node)
|
|
545
|
+
next if code_text.to_s.strip.empty?
|
|
546
|
+
section_content.add_block(ContentBlock.new(
|
|
547
|
+
type: :code,
|
|
548
|
+
text: code_text,
|
|
549
|
+
language: node.language
|
|
550
|
+
))
|
|
551
|
+
|
|
552
|
+
when Docbook::Elements::BlockQuote
|
|
553
|
+
block = ContentBlock.new(type: :blockquote)
|
|
554
|
+
if node.attribution
|
|
555
|
+
block.text = node.attribution.content
|
|
556
|
+
end
|
|
557
|
+
node.each_mixed_content do |child|
|
|
558
|
+
case child
|
|
559
|
+
when Docbook::Elements::Para
|
|
560
|
+
block.children << build_para_block(child)
|
|
561
|
+
end
|
|
562
|
+
end
|
|
563
|
+
section_content.add_block(block)
|
|
564
|
+
|
|
565
|
+
when Docbook::Elements::OrderedList
|
|
566
|
+
section_content.add_block(build_ordered_list_block(node))
|
|
567
|
+
|
|
568
|
+
when Docbook::Elements::ItemizedList
|
|
569
|
+
section_content.add_block(build_itemized_list_block(node))
|
|
570
|
+
|
|
571
|
+
when Docbook::Elements::VariableList
|
|
572
|
+
section_content.add_block(build_variablelist_block(node))
|
|
573
|
+
|
|
574
|
+
when Docbook::Elements::Note
|
|
575
|
+
section_content.add_block(build_admonition_block(:note, node))
|
|
576
|
+
when Docbook::Elements::Warning
|
|
577
|
+
section_content.add_block(build_admonition_block(:warning, node))
|
|
578
|
+
when Docbook::Elements::Caution
|
|
579
|
+
section_content.add_block(build_admonition_block(:caution, node))
|
|
580
|
+
when Docbook::Elements::Important
|
|
581
|
+
section_content.add_block(build_admonition_block(:important, node))
|
|
582
|
+
when Docbook::Elements::Tip
|
|
583
|
+
section_content.add_block(build_admonition_block(:tip, node))
|
|
584
|
+
when Docbook::Elements::Danger
|
|
585
|
+
section_content.add_block(build_admonition_block(:danger, node))
|
|
586
|
+
|
|
587
|
+
when Docbook::Elements::Figure
|
|
588
|
+
process_figure(node, section_content)
|
|
589
|
+
|
|
590
|
+
when Docbook::Elements::Example
|
|
591
|
+
section_content.add_block(build_example_block(node))
|
|
592
|
+
|
|
593
|
+
when Docbook::Elements::InformalFigure
|
|
594
|
+
process_informalfigure(node, section_content)
|
|
595
|
+
|
|
596
|
+
when Docbook::Elements::GlossEntry
|
|
597
|
+
section_content.add_block(build_glossentry_block(node))
|
|
598
|
+
|
|
599
|
+
when Docbook::Elements::Bibliomixed
|
|
600
|
+
section_content.add_block(build_bibliomixed_block(node))
|
|
601
|
+
|
|
602
|
+
when Docbook::Elements::IndexDiv
|
|
603
|
+
block = ContentBlock.new(type: :index_section, text: node.title&.content)
|
|
604
|
+
section_content.add_block(block)
|
|
605
|
+
|
|
606
|
+
when Docbook::Elements::LiteralLayout
|
|
607
|
+
if node.content
|
|
608
|
+
section_content.add_block(ContentBlock.new(
|
|
609
|
+
type: :code,
|
|
610
|
+
text: node.content.to_s
|
|
611
|
+
))
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
when Docbook::Elements::Simplesect
|
|
615
|
+
block = ContentBlock.new(type: :section)
|
|
616
|
+
block.children ||= []
|
|
617
|
+
if node.title
|
|
618
|
+
block.children << ContentBlock.new(
|
|
619
|
+
type: :heading,
|
|
620
|
+
text: node.title.content.to_s
|
|
621
|
+
)
|
|
622
|
+
end
|
|
623
|
+
ss_blocks = build_block_content_from_element(node)
|
|
624
|
+
block.children.concat(ss_blocks)
|
|
625
|
+
section_content.add_block(block)
|
|
626
|
+
|
|
627
|
+
when Docbook::Elements::Section
|
|
628
|
+
# Nested section - create nested content
|
|
629
|
+
nested = SectionContent.new(section_id: element_id(node))
|
|
630
|
+
build_element_content(node, nested)
|
|
631
|
+
block = ContentBlock.new(type: :section, children: nested.blocks)
|
|
632
|
+
section_content.add_block(block)
|
|
633
|
+
|
|
634
|
+
when Docbook::Elements::RefEntry
|
|
635
|
+
section_content.add_block(build_refentry_block(node))
|
|
636
|
+
|
|
637
|
+
when Docbook::Elements::FormalPara
|
|
638
|
+
# Formal para has title + para content
|
|
639
|
+
if node.title
|
|
640
|
+
block = ContentBlock.new(type: :paragraph)
|
|
641
|
+
block.children ||= []
|
|
642
|
+
block.children << ContentBlock.new(type: :strong, text: node.title.content.to_s)
|
|
643
|
+
block.children.concat(build_inline_content(node))
|
|
644
|
+
section_content.add_block(block)
|
|
645
|
+
elsif node.para
|
|
646
|
+
Array(node.para).each { |p| section_content.add_block(build_para_block(p)) }
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
else
|
|
650
|
+
# Skip unknown node types
|
|
651
|
+
end
|
|
652
|
+
end
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
def process_mediaobject(mo, section_content)
|
|
656
|
+
Array(mo.imageobject).each do |io|
|
|
657
|
+
next unless io.imagedata
|
|
658
|
+
fileref = io.imagedata.fileref
|
|
659
|
+
src = fileref ? process_image(fileref) : ""
|
|
660
|
+
alt = io.alt&.content
|
|
661
|
+
section_content.add_block(ContentBlock.new(
|
|
662
|
+
type: :image,
|
|
663
|
+
src: src,
|
|
664
|
+
alt: alt
|
|
665
|
+
))
|
|
666
|
+
end
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
def process_figure(fig, section_content)
|
|
670
|
+
fig_title = fig.title&.content
|
|
671
|
+
Array(fig.mediaobject).each do |mo|
|
|
672
|
+
mo_alt = mo.alt&.content if mo.respond_to?(:alt) && mo.alt
|
|
673
|
+
Array(mo.imageobject).each do |io|
|
|
674
|
+
next unless io.imagedata
|
|
675
|
+
fileref = io.imagedata.fileref
|
|
676
|
+
src = fileref ? process_image(fileref) : ""
|
|
677
|
+
alt = mo_alt || io.alt&.content || fig_title
|
|
678
|
+
section_content.add_block(ContentBlock.new(
|
|
679
|
+
type: :image,
|
|
680
|
+
src: src,
|
|
681
|
+
alt: alt,
|
|
682
|
+
title: fig_title
|
|
683
|
+
))
|
|
684
|
+
end
|
|
685
|
+
Array(mo.textobject).each do |to|
|
|
686
|
+
# TextObject has content as a string, not nested para
|
|
687
|
+
if to.content && !to.content.to_s.strip.empty?
|
|
688
|
+
section_content.add_block(ContentBlock.new(
|
|
689
|
+
type: :paragraph,
|
|
690
|
+
text: to.content.to_s
|
|
691
|
+
))
|
|
692
|
+
end
|
|
693
|
+
end
|
|
694
|
+
end
|
|
695
|
+
Array(fig.programlisting).each do |pl|
|
|
696
|
+
code_text = build_code_content(pl)
|
|
697
|
+
section_content.add_block(ContentBlock.new(type: :code, text: code_text, language: pl.language)) unless code_text.to_s.strip.empty?
|
|
698
|
+
end
|
|
699
|
+
Array(fig.screen).each do |s|
|
|
700
|
+
code_text = build_code_content(s)
|
|
701
|
+
section_content.add_block(ContentBlock.new(type: :code, text: code_text, language: s.language)) unless code_text.to_s.strip.empty?
|
|
702
|
+
end
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
def process_informalfigure(ifig, section_content)
|
|
706
|
+
Array(ifig.mediaobject).each do |mo|
|
|
707
|
+
Array(mo.imageobject).each do |io|
|
|
708
|
+
next unless io.imagedata
|
|
709
|
+
fileref = io.imagedata.fileref
|
|
710
|
+
src = fileref ? process_image(fileref) : ""
|
|
711
|
+
alt = io.alt&.content
|
|
712
|
+
section_content.add_block(ContentBlock.new(
|
|
713
|
+
type: :image,
|
|
714
|
+
src: src,
|
|
715
|
+
alt: alt
|
|
716
|
+
))
|
|
717
|
+
end
|
|
718
|
+
end
|
|
719
|
+
end
|
|
720
|
+
|
|
721
|
+
def build_ordered_list_block(ol)
|
|
722
|
+
block = ContentBlock.new(type: :ordered_list)
|
|
723
|
+
block.children ||= []
|
|
724
|
+
Array(ol.listitem).each do |li|
|
|
725
|
+
item = ContentBlock.new(type: :list_item)
|
|
726
|
+
item.children ||= []
|
|
727
|
+
li_blocks = build_block_content_from_element(li)
|
|
728
|
+
item.children.concat(li_blocks)
|
|
729
|
+
block.children << item
|
|
730
|
+
end
|
|
731
|
+
block
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
def build_itemized_list_block(ul)
|
|
735
|
+
block = ContentBlock.new(type: :itemized_list)
|
|
736
|
+
block.children ||= []
|
|
737
|
+
Array(ul.listitem).each do |li|
|
|
738
|
+
item = ContentBlock.new(type: :list_item)
|
|
739
|
+
item.children ||= []
|
|
740
|
+
li_blocks = build_block_content_from_element(li)
|
|
741
|
+
item.children.concat(li_blocks)
|
|
742
|
+
block.children << item
|
|
743
|
+
end
|
|
744
|
+
block
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
def build_variablelist_block(vl)
|
|
748
|
+
vl_block = ContentBlock.new(type: :definition_list)
|
|
749
|
+
vl_block.children ||= []
|
|
750
|
+
Array(vl.varlistentry).each do |entry|
|
|
751
|
+
# Term block - rendered in special style
|
|
752
|
+
term_block = ContentBlock.new(type: :definition_term)
|
|
753
|
+
term_block.children ||= []
|
|
754
|
+
Array(entry.term).each do |t|
|
|
755
|
+
term_content = build_inline_content_for_term(t)
|
|
756
|
+
term_block.children << ContentBlock.new(type: :text, text: term_content)
|
|
757
|
+
end
|
|
758
|
+
vl_block.children << term_block
|
|
759
|
+
|
|
760
|
+
# Definition description block - contains the listitem content
|
|
761
|
+
if entry.listitem
|
|
762
|
+
def_block = ContentBlock.new(type: :definition_description)
|
|
763
|
+
def_block.children ||= []
|
|
764
|
+
li_blocks = build_block_content_from_element(entry.listitem)
|
|
765
|
+
def_block.children.concat(li_blocks)
|
|
766
|
+
vl_block.children << def_block
|
|
767
|
+
end
|
|
768
|
+
end
|
|
769
|
+
vl_block
|
|
770
|
+
end
|
|
771
|
+
|
|
772
|
+
def build_example_block(ex)
|
|
773
|
+
block = ContentBlock.new(type: :example)
|
|
774
|
+
block.children ||= []
|
|
775
|
+
block.text = ex.title&.content
|
|
776
|
+
ex.each_mixed_content do |child|
|
|
777
|
+
case child
|
|
778
|
+
when Docbook::Elements::Para
|
|
779
|
+
block.children << build_para_block(child)
|
|
780
|
+
when Docbook::Elements::ProgramListing
|
|
781
|
+
code_text = build_code_content(child)
|
|
782
|
+
block.children << ContentBlock.new(type: :code, text: code_text, language: child.language) unless code_text.to_s.strip.empty?
|
|
783
|
+
when Docbook::Elements::Screen
|
|
784
|
+
code_text = build_code_content(child)
|
|
785
|
+
block.children << ContentBlock.new(type: :code, text: code_text, language: child.language) unless code_text.to_s.strip.empty?
|
|
786
|
+
when Docbook::Elements::LiteralLayout
|
|
787
|
+
block.children << ContentBlock.new(type: :code, text: child.content.to_s) if child.content
|
|
788
|
+
when Docbook::Elements::Figure
|
|
789
|
+
# Process figure within example
|
|
790
|
+
fig_block = ContentBlock.new(type: :image)
|
|
791
|
+
Array(child.mediaobject).each do |mo|
|
|
792
|
+
Array(mo.imageobject).each do |io|
|
|
793
|
+
next unless io.imagedata
|
|
794
|
+
fig_block.src = process_image(io.imagedata.fileref)
|
|
795
|
+
fig_block.alt = io.alt&.content
|
|
796
|
+
end
|
|
797
|
+
end
|
|
798
|
+
block.children << fig_block
|
|
799
|
+
end
|
|
800
|
+
end
|
|
801
|
+
block
|
|
802
|
+
end
|
|
803
|
+
|
|
804
|
+
def build_informalexample_block(element)
|
|
805
|
+
block = ContentBlock.new(type: :example_output)
|
|
806
|
+
block.children ||= []
|
|
807
|
+
element.each_mixed_content do |child|
|
|
808
|
+
case child
|
|
809
|
+
when Docbook::Elements::Para
|
|
810
|
+
block.children << build_para_block(child)
|
|
811
|
+
when Docbook::Elements::ProgramListing
|
|
812
|
+
code_text = build_code_content(child)
|
|
813
|
+
block.children << ContentBlock.new(type: :code, text: code_text, language: child.language) unless code_text.to_s.strip.empty?
|
|
814
|
+
when Docbook::Elements::Screen
|
|
815
|
+
code_text = build_code_content(child)
|
|
816
|
+
block.children << ContentBlock.new(type: :code, text: code_text, language: child.language) unless code_text.to_s.strip.empty?
|
|
817
|
+
when Docbook::Elements::LiteralLayout
|
|
818
|
+
block.children << ContentBlock.new(type: :code, text: child.content.to_s) if child.content
|
|
819
|
+
end
|
|
820
|
+
end
|
|
821
|
+
block
|
|
822
|
+
end
|
|
823
|
+
|
|
824
|
+
def build_glossentry_block(entry)
|
|
825
|
+
block = ContentBlock.new(type: :definition_list)
|
|
826
|
+
block.children ||= []
|
|
827
|
+
# Term
|
|
828
|
+
term_block = ContentBlock.new(type: :definition_term)
|
|
829
|
+
term_block.children ||= []
|
|
830
|
+
if entry.glossterm
|
|
831
|
+
term_text = entry.glossterm.content.to_s
|
|
832
|
+
term_block.children << ContentBlock.new(type: :text, text: term_text)
|
|
833
|
+
end
|
|
834
|
+
block.children << term_block
|
|
835
|
+
# Definition
|
|
836
|
+
if entry.glossdef
|
|
837
|
+
def_block = ContentBlock.new(type: :definition_description)
|
|
838
|
+
def_block.children ||= []
|
|
839
|
+
entry.glossdef.each_mixed_content do |child|
|
|
840
|
+
case child
|
|
841
|
+
when Docbook::Elements::Para
|
|
842
|
+
def_block.children << build_para_block(child)
|
|
843
|
+
end
|
|
844
|
+
end
|
|
845
|
+
block.children << def_block
|
|
846
|
+
end
|
|
847
|
+
block
|
|
848
|
+
end
|
|
849
|
+
|
|
850
|
+
def build_bibliomixed_block(bm)
|
|
851
|
+
block = ContentBlock.new(type: :bibliography_entry)
|
|
852
|
+
block.children ||= []
|
|
853
|
+
|
|
854
|
+
# Abbrev
|
|
855
|
+
if bm.abbrev&.content
|
|
856
|
+
block.children << ContentBlock.new(type: :biblio_abbrev, text: bm.abbrev.content)
|
|
857
|
+
end
|
|
858
|
+
|
|
859
|
+
# Author with personname
|
|
860
|
+
Array(bm.author).each do |author|
|
|
861
|
+
if author.personname
|
|
862
|
+
pn = author.personname
|
|
863
|
+
first = pn.firstname&.content
|
|
864
|
+
sur = pn.surname&.content
|
|
865
|
+
name_text = [first, sur].compact.join(" ")
|
|
866
|
+
if name_text.any?
|
|
867
|
+
block.children << ContentBlock.new(type: :biblio_personname, text: name_text, class_name: "first-last personname")
|
|
868
|
+
end
|
|
869
|
+
end
|
|
870
|
+
end
|
|
871
|
+
|
|
872
|
+
# Personname (inline)
|
|
873
|
+
Array(bm.personname).each do |pn|
|
|
874
|
+
first = pn.firstname&.content
|
|
875
|
+
sur = pn.surname&.content
|
|
876
|
+
name_text = [first, sur].compact.join(" ")
|
|
877
|
+
if name_text.any?
|
|
878
|
+
block.children << ContentBlock.new(type: :biblio_personname, text: name_text, class_name: "first-last personname")
|
|
879
|
+
end
|
|
880
|
+
end
|
|
881
|
+
|
|
882
|
+
# Firstname and surname separately
|
|
883
|
+
if bm.firstname&.content
|
|
884
|
+
block.children << ContentBlock.new(type: :biblio_firstname, text: bm.firstname.content, class_name: "firstname")
|
|
885
|
+
end
|
|
886
|
+
if bm.surname&.content
|
|
887
|
+
block.children << ContentBlock.new(type: :biblio_surname, text: bm.surname.content, class_name: "surname")
|
|
888
|
+
end
|
|
889
|
+
|
|
890
|
+
# Citetitle
|
|
891
|
+
Array(bm.citetitle).each do |ct|
|
|
892
|
+
if ct.href
|
|
893
|
+
block.children << ContentBlock.new(type: :link, text: ct.content, src: ct.href, class_name: "title")
|
|
894
|
+
else
|
|
895
|
+
block.children << ContentBlock.new(type: :biblio_citetitle, text: ct.content, class_name: "title")
|
|
896
|
+
end
|
|
897
|
+
end
|
|
898
|
+
|
|
899
|
+
# Orgname
|
|
900
|
+
Array(bm.orgname).each do |on|
|
|
901
|
+
if on.content
|
|
902
|
+
block.children << ContentBlock.new(type: :biblio_orgname, text: on.content, class_name: "orgname")
|
|
903
|
+
end
|
|
904
|
+
end
|
|
905
|
+
|
|
906
|
+
# Publishername
|
|
907
|
+
Array(bm.publishername).each do |pn|
|
|
908
|
+
if pn.content
|
|
909
|
+
block.children << ContentBlock.new(type: :biblio_publishername, text: pn.content, class_name: "publishername")
|
|
910
|
+
end
|
|
911
|
+
end
|
|
912
|
+
|
|
913
|
+
# Pubdate
|
|
914
|
+
if bm.pubdate
|
|
915
|
+
block.children << ContentBlock.new(type: :biblio_pubdate, text: bm.pubdate.to_s, class_name: "pubdate")
|
|
916
|
+
end
|
|
917
|
+
|
|
918
|
+
# Links
|
|
919
|
+
Array(bm.link).each do |lnk|
|
|
920
|
+
block.children << ContentBlock.new(type: :link, text: lnk.content, src: lnk.href)
|
|
921
|
+
end
|
|
922
|
+
|
|
923
|
+
# Trailing text content
|
|
924
|
+
trailing = bm.content.is_a?(Array) ? bm.content.join : bm.content.to_s
|
|
925
|
+
block.children << ContentBlock.new(type: :text, text: trailing) if trailing
|
|
926
|
+
|
|
927
|
+
block.text = block.children.map { |c| c.text || "" }.join(" ")
|
|
928
|
+
block
|
|
929
|
+
end
|
|
930
|
+
|
|
931
|
+
def build_refentry_block(re)
|
|
932
|
+
block = ContentBlock.new(type: :reference_entry)
|
|
933
|
+
block.children ||= []
|
|
934
|
+
|
|
935
|
+
# Determine the NAME of this entry (priority: refname > refentrytitle > fieldsynopsis.varname)
|
|
936
|
+
entry_name = nil
|
|
937
|
+
if re.refnamediv&.refname && re.refnamediv.refname.any?
|
|
938
|
+
entry_name = re.refnamediv.refname.map { |n| n.content }.join(" ")
|
|
939
|
+
elsif re.refmeta&.refentrytitle
|
|
940
|
+
entry_name = re.refmeta.refentrytitle.content
|
|
941
|
+
elsif re.refmeta&.respond_to?(:fieldsynopsis) && re.refmeta.fieldsynopsis
|
|
942
|
+
# fieldsynopsis is a collection, find first with varname
|
|
943
|
+
re.refmeta.fieldsynopsis.each do |fs|
|
|
944
|
+
# fs.varname might be nil due to namespace issues, so try direct content access
|
|
945
|
+
if fs.respond_to?(:varname) && fs.varname.is_a?(String) && !fs.varname.empty?
|
|
946
|
+
entry_name = fs.varname
|
|
947
|
+
break
|
|
948
|
+
elsif fs.respond_to?(:content)
|
|
949
|
+
# Fallback: extract varname from content using regex
|
|
950
|
+
# Content might be an array or string containing "varname" followed by the actual value
|
|
951
|
+
content_str = fs.content.is_a?(Array) ? fs.content.join(" ") : fs.content.to_s
|
|
952
|
+
# Match "varname" followed by whitespace and the value
|
|
953
|
+
varname_match = content_str.match(/varname\s+([^\n]+)/)
|
|
954
|
+
if varname_match
|
|
955
|
+
entry_name = varname_match[1].strip
|
|
956
|
+
break
|
|
957
|
+
end
|
|
958
|
+
end
|
|
959
|
+
end
|
|
960
|
+
end
|
|
961
|
+
|
|
962
|
+
# Fallback: use refpurpose as name if still no name found
|
|
963
|
+
if entry_name.nil? && re.refnamediv&.refpurpose
|
|
964
|
+
content = re.refnamediv.refpurpose.content
|
|
965
|
+
# Handle both string and array content (mixed content from inline elements)
|
|
966
|
+
purpose_text = if content.is_a?(Array)
|
|
967
|
+
content.join("")
|
|
968
|
+
elsif content.is_a?(String)
|
|
969
|
+
content
|
|
970
|
+
else
|
|
971
|
+
content.to_s
|
|
972
|
+
end
|
|
973
|
+
# Truncate and clean up the purpose to use as a name
|
|
974
|
+
entry_name = purpose_text.length > 50 ? purpose_text[0..47] + "..." : purpose_text
|
|
975
|
+
end
|
|
976
|
+
|
|
977
|
+
# Determine the BADGE (e.g., "pi" for processing instructions, "param" for template params)
|
|
978
|
+
entry_badge = nil
|
|
979
|
+
if re.refnamediv&.refclass
|
|
980
|
+
entry_badge = re.refnamediv.refclass.content.to_s
|
|
981
|
+
elsif entry_name&.start_with?("$")
|
|
982
|
+
entry_badge = "param"
|
|
983
|
+
end
|
|
984
|
+
|
|
985
|
+
# Add badge first (top of entry)
|
|
986
|
+
if entry_badge
|
|
987
|
+
block.children << ContentBlock.new(
|
|
988
|
+
type: :reference_badge,
|
|
989
|
+
text: entry_badge
|
|
990
|
+
)
|
|
991
|
+
end
|
|
992
|
+
|
|
993
|
+
# Add the NAME (headword)
|
|
994
|
+
if entry_name
|
|
995
|
+
block.children << ContentBlock.new(
|
|
996
|
+
type: :reference_name,
|
|
997
|
+
text: entry_name
|
|
998
|
+
)
|
|
999
|
+
end
|
|
1000
|
+
|
|
1001
|
+
# Process refmeta refmiscinfo as metadata (only if not already captured as name)
|
|
1002
|
+
if re.refmeta
|
|
1003
|
+
if re.refmeta.respond_to?(:refmiscinfo) && re.refmeta.refmiscinfo
|
|
1004
|
+
re.refmeta.refmiscinfo.each do |info|
|
|
1005
|
+
block.children << ContentBlock.new(
|
|
1006
|
+
type: :reference_meta,
|
|
1007
|
+
text: info.content
|
|
1008
|
+
) if info && info.content
|
|
1009
|
+
end
|
|
1010
|
+
end
|
|
1011
|
+
if re.refmeta.manvolnum
|
|
1012
|
+
block.children << ContentBlock.new(
|
|
1013
|
+
type: :reference_meta,
|
|
1014
|
+
text: "(#{re.refmeta.manvolnum})"
|
|
1015
|
+
)
|
|
1016
|
+
end
|
|
1017
|
+
end
|
|
1018
|
+
|
|
1019
|
+
# Process refnamediv refpurpose - THE DEFINITION
|
|
1020
|
+
if re.refnamediv&.refpurpose
|
|
1021
|
+
content = re.refnamediv.refpurpose.content
|
|
1022
|
+
# Handle both string and array content (mixed content from inline elements)
|
|
1023
|
+
definition_text = if content.is_a?(Array)
|
|
1024
|
+
content.join("")
|
|
1025
|
+
elsif content.is_a?(String)
|
|
1026
|
+
content
|
|
1027
|
+
else
|
|
1028
|
+
content.to_s
|
|
1029
|
+
end
|
|
1030
|
+
block.children << ContentBlock.new(
|
|
1031
|
+
type: :reference_definition,
|
|
1032
|
+
text: definition_text
|
|
1033
|
+
)
|
|
1034
|
+
end
|
|
1035
|
+
|
|
1036
|
+
# Process refsection sections (CONTENT of this entry)
|
|
1037
|
+
# NOTE: Skip rs.title - the entry_name already serves as the header
|
|
1038
|
+
Array(re.refsection).each do |rs|
|
|
1039
|
+
rs_block = ContentBlock.new(type: :description_section)
|
|
1040
|
+
rs_block.children ||= []
|
|
1041
|
+
# Don't add title - entry_name is the headword
|
|
1042
|
+
rs_block.children.concat(build_block_content_from_element(rs))
|
|
1043
|
+
block.children << rs_block
|
|
1044
|
+
end
|
|
1045
|
+
block
|
|
1046
|
+
end
|
|
1047
|
+
|
|
1048
|
+
def build_admonition_block(type, element)
|
|
1049
|
+
block = ContentBlock.new(type: type, text: title_of(element))
|
|
1050
|
+
block.children ||= []
|
|
1051
|
+
each_attr(element, :para) { |p| block.children << build_para_block(p) }
|
|
1052
|
+
each_attr(element, :programlisting) do |pl|
|
|
1053
|
+
code_text = build_code_content(pl)
|
|
1054
|
+
block.children << ContentBlock.new(type: :code, text: code_text, language: pl.language) unless code_text.to_s.strip.empty?
|
|
1055
|
+
end
|
|
1056
|
+
each_attr(element, :screen) do |s|
|
|
1057
|
+
code_text = build_code_content(s)
|
|
1058
|
+
block.children << ContentBlock.new(type: :code, text: code_text, language: s.language) unless code_text.to_s.strip.empty?
|
|
1059
|
+
end
|
|
1060
|
+
each_attr(element, :literallayout) do |ll|
|
|
1061
|
+
block.children << ContentBlock.new(type: :code, text: ll.content.to_s) if ll.content
|
|
1062
|
+
end
|
|
1063
|
+
block
|
|
1064
|
+
end
|
|
1065
|
+
|
|
1066
|
+
def build_para_block(p)
|
|
1067
|
+
block = ContentBlock.new(type: :paragraph)
|
|
1068
|
+
block.children = build_inline_content(p)
|
|
1069
|
+
block
|
|
1070
|
+
end
|
|
1071
|
+
|
|
1072
|
+
# Build all block-level content from an element (used for listitems, simplesects, etc.)
|
|
1073
|
+
def build_block_content_from_element(element)
|
|
1074
|
+
blocks = []
|
|
1075
|
+
return blocks unless element.respond_to?(:each_mixed_content)
|
|
1076
|
+
|
|
1077
|
+
element.each_mixed_content do |node|
|
|
1078
|
+
case node
|
|
1079
|
+
when String
|
|
1080
|
+
# Skip whitespace strings
|
|
1081
|
+
next if node =~ /\A\s*\z/
|
|
1082
|
+
when Docbook::Elements::Para
|
|
1083
|
+
blocks << build_para_block(node)
|
|
1084
|
+
when Docbook::Elements::LiteralLayout
|
|
1085
|
+
blocks << ContentBlock.new(type: :code, text: node.content.to_s) if node.content
|
|
1086
|
+
when Docbook::Elements::ProgramListing
|
|
1087
|
+
code_text = build_code_content(node)
|
|
1088
|
+
blocks << ContentBlock.new(type: :code, text: code_text, language: node.language) unless code_text.to_s.strip.empty?
|
|
1089
|
+
when Docbook::Elements::Screen
|
|
1090
|
+
code_text = build_code_content(node)
|
|
1091
|
+
blocks << ContentBlock.new(type: :code, text: code_text, language: node.language) unless code_text.to_s.strip.empty?
|
|
1092
|
+
when Docbook::Elements::Simplesect
|
|
1093
|
+
block = ContentBlock.new(type: :section)
|
|
1094
|
+
block.children ||= []
|
|
1095
|
+
if node.title
|
|
1096
|
+
block.children << ContentBlock.new(type: :heading, text: node.title.content.to_s)
|
|
1097
|
+
end
|
|
1098
|
+
ss_blocks = build_block_content_from_element(node)
|
|
1099
|
+
block.children.concat(ss_blocks)
|
|
1100
|
+
blocks << block
|
|
1101
|
+
when Docbook::Elements::VariableList
|
|
1102
|
+
blocks << build_variablelist_block(node)
|
|
1103
|
+
when Docbook::Elements::InformalExample
|
|
1104
|
+
blocks << build_informalexample_block(node)
|
|
1105
|
+
end
|
|
1106
|
+
end
|
|
1107
|
+
blocks
|
|
1108
|
+
end
|
|
1109
|
+
|
|
1110
|
+
# Build inline content (children) from element using each_mixed_content
|
|
1111
|
+
# This yields proper model objects (not XML elements) in document order
|
|
1112
|
+
def build_inline_content(element)
|
|
1113
|
+
children = []
|
|
1114
|
+
element.each_mixed_content do |node|
|
|
1115
|
+
case node
|
|
1116
|
+
when String
|
|
1117
|
+
children << ContentBlock.new(type: :text, text: node) if node&.strip&.then { !_1.empty? }
|
|
1118
|
+
when Docbook::Elements::Emphasis
|
|
1119
|
+
children << build_emphasis_block(node)
|
|
1120
|
+
when Docbook::Elements::Link
|
|
1121
|
+
children << build_link_block(node)
|
|
1122
|
+
when Docbook::Elements::Xref
|
|
1123
|
+
children << build_xref_block(node)
|
|
1124
|
+
when Docbook::Elements::Code, Docbook::Elements::Literal
|
|
1125
|
+
children << ContentBlock.new(type: :codetext, text: extract_text_content(node))
|
|
1126
|
+
when Docbook::Elements::Filename, Docbook::Elements::ProductName,
|
|
1127
|
+
Docbook::Elements::ClassName, Docbook::Elements::Function,
|
|
1128
|
+
Docbook::Elements::Parameter, Docbook::Elements::BuildTarget,
|
|
1129
|
+
Docbook::Elements::Dir, Docbook::Elements::Replaceable
|
|
1130
|
+
children << ContentBlock.new(type: :codetext, text: node.content)
|
|
1131
|
+
when Docbook::Elements::Quote
|
|
1132
|
+
# Handle quote with nested replaceable
|
|
1133
|
+
if Array(node.replaceable).any?
|
|
1134
|
+
text = node.replaceable.map(&:content).join("")
|
|
1135
|
+
children << ContentBlock.new(type: :text, text: "\"#{text}\"")
|
|
1136
|
+
else
|
|
1137
|
+
children << ContentBlock.new(type: :text, text: "\"#{node.content}\"")
|
|
1138
|
+
end
|
|
1139
|
+
when Docbook::Elements::UserInput, Docbook::Elements::Screen
|
|
1140
|
+
children << ContentBlock.new(type: :codetext, text: node.content)
|
|
1141
|
+
when Docbook::Elements::Citetitle
|
|
1142
|
+
if node.href
|
|
1143
|
+
children << ContentBlock.new(type: :citation_link, text: node.content, src: node.href.to_s)
|
|
1144
|
+
else
|
|
1145
|
+
children << ContentBlock.new(type: :citation, text: node.content)
|
|
1146
|
+
end
|
|
1147
|
+
when Docbook::Elements::Biblioref
|
|
1148
|
+
# Use linkend as display text since biblioref is often self-closing
|
|
1149
|
+
text = node.content.to_s.empty? ? node.linkend : node.content
|
|
1150
|
+
children << ContentBlock.new(type: :biblioref, text: text, src: "##{node.linkend}")
|
|
1151
|
+
when Docbook::Elements::FirstTerm, Docbook::Elements::Glossterm
|
|
1152
|
+
children << ContentBlock.new(type: :emphasis, text: node.content)
|
|
1153
|
+
when Docbook::Elements::Tag
|
|
1154
|
+
# Render tag elements like <appendix> as inline code text
|
|
1155
|
+
tag_content = node.content.to_s
|
|
1156
|
+
children << ContentBlock.new(type: :codetext, text: "<#{tag_content}>")
|
|
1157
|
+
when Docbook::Elements::Att
|
|
1158
|
+
children << ContentBlock.new(type: :codetext, text: node.content.to_s)
|
|
1159
|
+
when Docbook::Elements::Inlinemediaobject
|
|
1160
|
+
children << build_inline_image_block(node)
|
|
1161
|
+
else
|
|
1162
|
+
# Generic fallback for any other Serializable
|
|
1163
|
+
children << ContentBlock.new(type: :text, text: node.content.to_s) if node.content
|
|
1164
|
+
end
|
|
1165
|
+
end
|
|
1166
|
+
children
|
|
1167
|
+
end
|
|
1168
|
+
|
|
1169
|
+
def build_emphasis_block(el)
|
|
1170
|
+
type = case el.role
|
|
1171
|
+
when "bold", "strong" then :strong
|
|
1172
|
+
when "italic" then :italic
|
|
1173
|
+
else :emphasis
|
|
1174
|
+
end
|
|
1175
|
+
ContentBlock.new(type: type, text: el.content)
|
|
1176
|
+
end
|
|
1177
|
+
|
|
1178
|
+
def build_link_block(el)
|
|
1179
|
+
href = if el.xlink_href
|
|
1180
|
+
el.xlink_href.to_s
|
|
1181
|
+
elsif el.linkend
|
|
1182
|
+
"##{el.linkend}"
|
|
1183
|
+
else
|
|
1184
|
+
"#"
|
|
1185
|
+
end
|
|
1186
|
+
ContentBlock.new(type: :link, text: el.content, src: href)
|
|
1187
|
+
end
|
|
1188
|
+
|
|
1189
|
+
def build_xref_block(el)
|
|
1190
|
+
href = el.linkend ? "##{el.linkend}" : "#"
|
|
1191
|
+
text = resolve_xref_text(el)
|
|
1192
|
+
text ||= el.content.to_s if el.content
|
|
1193
|
+
ContentBlock.new(type: :xref, text: text || href, src: href)
|
|
1194
|
+
end
|
|
1195
|
+
|
|
1196
|
+
def build_inline_image_block(el)
|
|
1197
|
+
if el.imageobject&.imagedata&.fileref
|
|
1198
|
+
src = process_image(el.imageobject.imagedata.fileref)
|
|
1199
|
+
alt = el.alt&.content
|
|
1200
|
+
ContentBlock.new(type: :inline_image, src: src, alt: alt)
|
|
1201
|
+
else
|
|
1202
|
+
ContentBlock.new(type: :text, text: "[image]")
|
|
1203
|
+
end
|
|
1204
|
+
end
|
|
1205
|
+
|
|
1206
|
+
# Extract text content from an element, using each_mixed_content
|
|
1207
|
+
# to handle CDATA sections that don't get mapped to .content
|
|
1208
|
+
def extract_text_content(el)
|
|
1209
|
+
if el.respond_to?(:each_mixed_content) && (el.content.nil? || el.content.to_s.empty?)
|
|
1210
|
+
text = []
|
|
1211
|
+
el.each_mixed_content { |n| text << n.to_s if n.is_a?(String) }
|
|
1212
|
+
return text.join unless text.empty?
|
|
1213
|
+
end
|
|
1214
|
+
el.content.to_s
|
|
1215
|
+
end
|
|
1216
|
+
|
|
1217
|
+
# Build text content from a screen/programlisting element, handling mixed content
|
|
1218
|
+
# Returns the concatenated text content
|
|
1219
|
+
def build_code_content(el)
|
|
1220
|
+
return el.content.to_s unless el.respond_to?(:each_mixed_content)
|
|
1221
|
+
|
|
1222
|
+
result = []
|
|
1223
|
+
el.each_mixed_content do |node|
|
|
1224
|
+
case node
|
|
1225
|
+
when String
|
|
1226
|
+
result << node
|
|
1227
|
+
when Docbook::Elements::UserInput, Docbook::Elements::ComputerOutput,
|
|
1228
|
+
Docbook::Elements::Code, Docbook::Elements::Literal
|
|
1229
|
+
result << node.content.to_s
|
|
1230
|
+
when Docbook::Elements::Emphasis
|
|
1231
|
+
result << node.content.to_s
|
|
1232
|
+
when Docbook::Elements::Filename, Docbook::Elements::ProductName
|
|
1233
|
+
result << node.content.to_s
|
|
1234
|
+
when Docbook::Elements::Link
|
|
1235
|
+
result << node.content.to_s
|
|
1236
|
+
when Docbook::Elements::Xref
|
|
1237
|
+
result << node.content.to_s
|
|
1238
|
+
when Docbook::Elements::Tag
|
|
1239
|
+
result << "<#{node.content}>"
|
|
1240
|
+
when Docbook::Elements::Att
|
|
1241
|
+
result << node.content.to_s
|
|
1242
|
+
else
|
|
1243
|
+
result << node.content.to_s if node.respond_to?(:content) && node.content
|
|
1244
|
+
end
|
|
1245
|
+
end
|
|
1246
|
+
result.join("")
|
|
1247
|
+
end
|
|
1248
|
+
|
|
1249
|
+
# Build text content from a term element, handling mixed content
|
|
1250
|
+
# Uses each_mixed_content since Term now has typed inline elements
|
|
1251
|
+
def build_inline_content_for_term(term_el)
|
|
1252
|
+
return "" unless term_el.respond_to?(:each_mixed_content)
|
|
1253
|
+
|
|
1254
|
+
result = []
|
|
1255
|
+
term_el.each_mixed_content do |node|
|
|
1256
|
+
case node
|
|
1257
|
+
when String
|
|
1258
|
+
result << node if node&.strip&.then { !_1.empty? }
|
|
1259
|
+
when Docbook::Elements::Code, Docbook::Elements::Literal
|
|
1260
|
+
result << node.content.to_s
|
|
1261
|
+
when Docbook::Elements::Emphasis
|
|
1262
|
+
result << node.content.to_s
|
|
1263
|
+
when Docbook::Elements::Filename, Docbook::Elements::ProductName
|
|
1264
|
+
result << node.content.to_s
|
|
1265
|
+
when Docbook::Elements::Link
|
|
1266
|
+
result << node.content.to_s
|
|
1267
|
+
when Docbook::Elements::Xref
|
|
1268
|
+
result << node.content.to_s
|
|
1269
|
+
when Docbook::Elements::Tag
|
|
1270
|
+
result << "<#{node.content}>"
|
|
1271
|
+
when Docbook::Elements::Att
|
|
1272
|
+
result << node.content.to_s
|
|
1273
|
+
when Docbook::Elements::Property
|
|
1274
|
+
result << node.content.to_s
|
|
1275
|
+
end
|
|
1276
|
+
end
|
|
1277
|
+
result.join("")
|
|
1278
|
+
end
|
|
1279
|
+
|
|
1280
|
+
def find_section_element(id)
|
|
1281
|
+
return find_in_document_fallback(id) unless @xref_resolver
|
|
1282
|
+
@xref_resolver[id] || find_in_document_fallback(id)
|
|
1283
|
+
end
|
|
1284
|
+
|
|
1285
|
+
def find_in_document_fallback(id)
|
|
1286
|
+
find_in_document(@document, id)
|
|
1287
|
+
end
|
|
1288
|
+
|
|
1289
|
+
# Recursively search for element with matching id using typed collections
|
|
1290
|
+
def find_in_document(el, id)
|
|
1291
|
+
return el if element_id(el) == id
|
|
1292
|
+
|
|
1293
|
+
case el
|
|
1294
|
+
when Docbook::Elements::Book
|
|
1295
|
+
[
|
|
1296
|
+
el.part, el.chapter, el.appendix, el.preface,
|
|
1297
|
+
el.glossary, el.bibliography, el.index
|
|
1298
|
+
].each do |children|
|
|
1299
|
+
Array(children).each do |child|
|
|
1300
|
+
result = find_in_document(child, id)
|
|
1301
|
+
return result if result
|
|
1302
|
+
end
|
|
1303
|
+
end
|
|
1304
|
+
when Docbook::Elements::Part
|
|
1305
|
+
[
|
|
1306
|
+
el.chapter, el.appendix, el.reference, el.preface,
|
|
1307
|
+
el.glossary, el.bibliography, el.index
|
|
1308
|
+
].each do |children|
|
|
1309
|
+
Array(children).each do |child|
|
|
1310
|
+
result = find_in_document(child, id)
|
|
1311
|
+
return result if result
|
|
1312
|
+
end
|
|
1313
|
+
end
|
|
1314
|
+
when Docbook::Elements::Article
|
|
1315
|
+
Array(el.section).each do |child|
|
|
1316
|
+
result = find_in_document(child, id)
|
|
1317
|
+
return result if result
|
|
1318
|
+
end
|
|
1319
|
+
when Docbook::Elements::Chapter, Docbook::Elements::Appendix,
|
|
1320
|
+
Docbook::Elements::Section, Docbook::Elements::Preface
|
|
1321
|
+
Array(el.section).each do |child|
|
|
1322
|
+
result = find_in_document(child, id)
|
|
1323
|
+
return result if result
|
|
1324
|
+
end
|
|
1325
|
+
when Docbook::Elements::Reference
|
|
1326
|
+
Array(el.refentry).each do |child|
|
|
1327
|
+
result = find_in_document(child, id)
|
|
1328
|
+
return result if result
|
|
1329
|
+
end
|
|
1330
|
+
end
|
|
1331
|
+
|
|
1332
|
+
nil
|
|
1333
|
+
end
|
|
1334
|
+
|
|
1335
|
+
# ── Image Processing ────────────────────────────────────────────
|
|
1336
|
+
|
|
1337
|
+
def process_image(fileref)
|
|
1338
|
+
return fileref unless @base_path
|
|
1339
|
+
|
|
1340
|
+
if @output_mode == :single_file
|
|
1341
|
+
embed_image_base64(fileref)
|
|
1342
|
+
else
|
|
1343
|
+
find_file_path(fileref) || fileref
|
|
1344
|
+
end
|
|
1345
|
+
end
|
|
1346
|
+
|
|
1347
|
+
# Find the actual file path by searching in base_path and parent directories
|
|
1348
|
+
# This handles cases where media/ is at a different level than xml/
|
|
1349
|
+
# Also tries prepending common prefixes like 'resources/' when direct path not found
|
|
1350
|
+
def find_file_path(fileref)
|
|
1351
|
+
# Try variations of the path with common prefixes
|
|
1352
|
+
path_prefixes = ["", "resources/", "../resources/"]
|
|
1353
|
+
search_paths = []
|
|
1354
|
+
|
|
1355
|
+
# Build search paths: base_path and all parents
|
|
1356
|
+
path = @base_path
|
|
1357
|
+
loop do
|
|
1358
|
+
search_paths << path
|
|
1359
|
+
parent = File.dirname(path)
|
|
1360
|
+
break if parent == path
|
|
1361
|
+
path = parent
|
|
1362
|
+
end
|
|
1363
|
+
|
|
1364
|
+
# For each search path, try with and without resource prefixes
|
|
1365
|
+
search_paths.each do |base|
|
|
1366
|
+
path_prefixes.each do |prefix|
|
|
1367
|
+
full_path = File.join(base, prefix, fileref)
|
|
1368
|
+
return full_path if File.exist?(full_path)
|
|
1369
|
+
end
|
|
1370
|
+
end
|
|
1371
|
+
nil
|
|
1372
|
+
end
|
|
1373
|
+
|
|
1374
|
+
def embed_image_base64(fileref)
|
|
1375
|
+
return @image_cache[fileref] if @image_cache[fileref]
|
|
1376
|
+
|
|
1377
|
+
full_path = find_file_path(fileref)
|
|
1378
|
+
return fileref unless full_path && File.exist?(full_path)
|
|
1379
|
+
|
|
1380
|
+
data = File.binread(full_path)
|
|
1381
|
+
mime = Marcel::MimeType.for(data)
|
|
1382
|
+
encoded = Base64.strict_encode64(data)
|
|
1383
|
+
@image_cache[fileref] = "data:#{mime};base64,#{encoded}"
|
|
1384
|
+
end
|
|
1385
|
+
|
|
1386
|
+
# ── Utility ─────────────────────────────────────────────────────
|
|
1387
|
+
|
|
1388
|
+
def escape(text)
|
|
1389
|
+
return "" if text.nil?
|
|
1390
|
+
ERB::Util.html_escape(text.to_s)
|
|
1391
|
+
end
|
|
1392
|
+
|
|
1393
|
+
def each_attr(obj, name)
|
|
1394
|
+
val = obj.send(name)
|
|
1395
|
+
return unless val
|
|
1396
|
+
Array(val).each { |item| yield item }
|
|
1397
|
+
rescue NoMethodError
|
|
1398
|
+
nil
|
|
1399
|
+
end
|
|
1400
|
+
|
|
1401
|
+
# Get title content from element - elements with title attribute
|
|
1402
|
+
def title_of(el)
|
|
1403
|
+
case el
|
|
1404
|
+
when Docbook::Elements::Article, Docbook::Elements::Book
|
|
1405
|
+
el.info&.title&.content
|
|
1406
|
+
when Docbook::Elements::Section, Docbook::Elements::Chapter, Docbook::Elements::Appendix,
|
|
1407
|
+
Docbook::Elements::Preface, Docbook::Elements::Part, Docbook::Elements::Reference
|
|
1408
|
+
# These elements may have title inside info or as direct child
|
|
1409
|
+
el.info&.title&.content || el.title&.content
|
|
1410
|
+
when Docbook::Elements::RefEntry
|
|
1411
|
+
# RefEntry title is in refnamediv.refname, not in title element
|
|
1412
|
+
el.refnamediv&.refname&.first&.content
|
|
1413
|
+
when Docbook::Elements::Note, Docbook::Elements::Warning, Docbook::Elements::Caution,
|
|
1414
|
+
Docbook::Elements::Important, Docbook::Elements::Tip, Docbook::Elements::Danger
|
|
1415
|
+
el.title&.content
|
|
1416
|
+
when Docbook::Elements::BlockQuote, Docbook::Elements::Figure, Docbook::Elements::Example,
|
|
1417
|
+
Docbook::Elements::Equation
|
|
1418
|
+
el.title&.content
|
|
1419
|
+
else
|
|
1420
|
+
el.title&.content rescue nil
|
|
1421
|
+
end
|
|
1422
|
+
end
|
|
1423
|
+
|
|
1424
|
+
def best_title(el)
|
|
1425
|
+
title_of(el)
|
|
1426
|
+
end
|
|
1427
|
+
|
|
1428
|
+
def element_id(el)
|
|
1429
|
+
el.xml_id || begin
|
|
1430
|
+
t = best_title(el)
|
|
1431
|
+
t&.downcase&.gsub(/[^a-z0-9]+/, "-")&.gsub(/^-|-$/, "") || "el-#{el.object_id}"
|
|
1432
|
+
end
|
|
1433
|
+
end
|
|
1434
|
+
|
|
1435
|
+
# ── Title Extraction ─────────────────────────────────────────────
|
|
1436
|
+
|
|
1437
|
+
def extract_title
|
|
1438
|
+
case @document
|
|
1439
|
+
when Docbook::Elements::Book, Docbook::Elements::Article
|
|
1440
|
+
best_title(@document) || "DocBook"
|
|
1441
|
+
when Docbook::Elements::Section, Docbook::Elements::Preface,
|
|
1442
|
+
Docbook::Elements::Chapter, Docbook::Elements::Appendix
|
|
1443
|
+
best_title(@document) || "DocBook"
|
|
1444
|
+
else
|
|
1445
|
+
"DocBook"
|
|
1446
|
+
end
|
|
1447
|
+
end
|
|
1448
|
+
end
|
|
1449
|
+
end
|
|
1450
|
+
end
|