coradoc-html 1.1.7
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/LICENSE.txt +21 -0
- data/lib/coradoc/html/base.rb +157 -0
- data/lib/coradoc/html/config.rb +467 -0
- data/lib/coradoc/html/converter_base.rb +177 -0
- data/lib/coradoc/html/converters/admonition.rb +180 -0
- data/lib/coradoc/html/converters/attribute.rb +68 -0
- data/lib/coradoc/html/converters/attribute_reference.rb +60 -0
- data/lib/coradoc/html/converters/audio.rb +165 -0
- data/lib/coradoc/html/converters/base.rb +615 -0
- data/lib/coradoc/html/converters/bibliography.rb +82 -0
- data/lib/coradoc/html/converters/bibliography_entry.rb +108 -0
- data/lib/coradoc/html/converters/block_image.rb +72 -0
- data/lib/coradoc/html/converters/bold.rb +34 -0
- data/lib/coradoc/html/converters/break.rb +32 -0
- data/lib/coradoc/html/converters/comment_block.rb +42 -0
- data/lib/coradoc/html/converters/comment_line.rb +54 -0
- data/lib/coradoc/html/converters/cross_reference.rb +59 -0
- data/lib/coradoc/html/converters/document.rb +108 -0
- data/lib/coradoc/html/converters/example.rb +114 -0
- data/lib/coradoc/html/converters/highlight.rb +34 -0
- data/lib/coradoc/html/converters/include.rb +68 -0
- data/lib/coradoc/html/converters/inline_image.rb +41 -0
- data/lib/coradoc/html/converters/italic.rb +34 -0
- data/lib/coradoc/html/converters/line_break.rb +31 -0
- data/lib/coradoc/html/converters/link.rb +46 -0
- data/lib/coradoc/html/converters/list_item.rb +75 -0
- data/lib/coradoc/html/converters/listing.rb +99 -0
- data/lib/coradoc/html/converters/literal.rb +102 -0
- data/lib/coradoc/html/converters/monospace.rb +34 -0
- data/lib/coradoc/html/converters/open.rb +78 -0
- data/lib/coradoc/html/converters/ordered.rb +53 -0
- data/lib/coradoc/html/converters/paragraph.rb +46 -0
- data/lib/coradoc/html/converters/quote.rb +113 -0
- data/lib/coradoc/html/converters/reviewer_comment.rb +74 -0
- data/lib/coradoc/html/converters/reviewer_note.rb +134 -0
- data/lib/coradoc/html/converters/section.rb +90 -0
- data/lib/coradoc/html/converters/sidebar.rb +113 -0
- data/lib/coradoc/html/converters/source.rb +137 -0
- data/lib/coradoc/html/converters/source_code.rb +16 -0
- data/lib/coradoc/html/converters/span.rb +61 -0
- data/lib/coradoc/html/converters/strikethrough.rb +34 -0
- data/lib/coradoc/html/converters/subscript.rb +34 -0
- data/lib/coradoc/html/converters/superscript.rb +34 -0
- data/lib/coradoc/html/converters/table.rb +85 -0
- data/lib/coradoc/html/converters/table_cell.rb +203 -0
- data/lib/coradoc/html/converters/table_row.rb +45 -0
- data/lib/coradoc/html/converters/template_html_converter.rb +105 -0
- data/lib/coradoc/html/converters/term.rb +58 -0
- data/lib/coradoc/html/converters/text_element.rb +44 -0
- data/lib/coradoc/html/converters/underline.rb +34 -0
- data/lib/coradoc/html/converters/unordered.rb +47 -0
- data/lib/coradoc/html/converters/verse.rb +105 -0
- data/lib/coradoc/html/converters/video.rb +179 -0
- data/lib/coradoc/html/element_mapping.rb +210 -0
- data/lib/coradoc/html/entity.rb +137 -0
- data/lib/coradoc/html/input/cleaner.rb +163 -0
- data/lib/coradoc/html/input/config.rb +79 -0
- data/lib/coradoc/html/input/converters/a.rb +90 -0
- data/lib/coradoc/html/input/converters/aside.rb +23 -0
- data/lib/coradoc/html/input/converters/audio.rb +50 -0
- data/lib/coradoc/html/input/converters/base.rb +116 -0
- data/lib/coradoc/html/input/converters/blockquote.rb +25 -0
- data/lib/coradoc/html/input/converters/br.rb +19 -0
- data/lib/coradoc/html/input/converters/bypass.rb +83 -0
- data/lib/coradoc/html/input/converters/code.rb +25 -0
- data/lib/coradoc/html/input/converters/div.rb +25 -0
- data/lib/coradoc/html/input/converters/dl.rb +106 -0
- data/lib/coradoc/html/input/converters/drop.rb +28 -0
- data/lib/coradoc/html/input/converters/em.rb +23 -0
- data/lib/coradoc/html/input/converters/figure.rb +58 -0
- data/lib/coradoc/html/input/converters/h.rb +76 -0
- data/lib/coradoc/html/input/converters/head.rb +30 -0
- data/lib/coradoc/html/input/converters/hr.rb +20 -0
- data/lib/coradoc/html/input/converters/ignore.rb +22 -0
- data/lib/coradoc/html/input/converters/img.rb +110 -0
- data/lib/coradoc/html/input/converters/li.rb +35 -0
- data/lib/coradoc/html/input/converters/mark.rb +21 -0
- data/lib/coradoc/html/input/converters/markup.rb +107 -0
- data/lib/coradoc/html/input/converters/math.rb +46 -0
- data/lib/coradoc/html/input/converters/ol.rb +46 -0
- data/lib/coradoc/html/input/converters/p.rb +81 -0
- data/lib/coradoc/html/input/converters/pass_through.rb +19 -0
- data/lib/coradoc/html/input/converters/pre.rb +59 -0
- data/lib/coradoc/html/input/converters/q.rb +24 -0
- data/lib/coradoc/html/input/converters/strong.rb +22 -0
- data/lib/coradoc/html/input/converters/sub.rb +40 -0
- data/lib/coradoc/html/input/converters/sup.rb +40 -0
- data/lib/coradoc/html/input/converters/table.rb +64 -0
- data/lib/coradoc/html/input/converters/td.rb +70 -0
- data/lib/coradoc/html/input/converters/text.rb +67 -0
- data/lib/coradoc/html/input/converters/th.rb +20 -0
- data/lib/coradoc/html/input/converters/tr.rb +28 -0
- data/lib/coradoc/html/input/converters/video.rb +53 -0
- data/lib/coradoc/html/input/converters.rb +122 -0
- data/lib/coradoc/html/input/errors.rb +22 -0
- data/lib/coradoc/html/input/html_converter.rb +170 -0
- data/lib/coradoc/html/input/plugin.rb +169 -0
- data/lib/coradoc/html/input/plugins/plateau.rb +229 -0
- data/lib/coradoc/html/input/postprocessor.rb +31 -0
- data/lib/coradoc/html/input.rb +68 -0
- data/lib/coradoc/html/output.rb +95 -0
- data/lib/coradoc/html/renderer.rb +409 -0
- data/lib/coradoc/html/spa.rb +309 -0
- data/lib/coradoc/html/static.rb +293 -0
- data/lib/coradoc/html/template_config.rb +151 -0
- data/lib/coradoc/html/template_helpers.rb +58 -0
- data/lib/coradoc/html/template_locator.rb +114 -0
- data/lib/coradoc/html/theme/base.rb +231 -0
- data/lib/coradoc/html/theme/classic_renderer.rb +390 -0
- data/lib/coradoc/html/theme/modern/components/ui_components.rb +344 -0
- data/lib/coradoc/html/theme/modern/css_generator.rb +311 -0
- data/lib/coradoc/html/theme/modern/javascript_generator.rb +314 -0
- data/lib/coradoc/html/theme/modern/serializers/document_serializer.rb +382 -0
- data/lib/coradoc/html/theme/modern/tailwind_config_builder.rb +164 -0
- data/lib/coradoc/html/theme/modern/vue_template_generator.rb +374 -0
- data/lib/coradoc/html/theme/modern_renderer.rb +250 -0
- data/lib/coradoc/html/theme/registry.rb +153 -0
- data/lib/coradoc/html/theme.rb +13 -0
- data/lib/coradoc/html/transform/from_core_model.rb +32 -0
- data/lib/coradoc/html/transform/to_core_model.rb +39 -0
- data/lib/coradoc/html/version.rb +7 -0
- data/lib/coradoc/html.rb +255 -0
- metadata +264 -0
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Html
|
|
5
|
+
module Converters
|
|
6
|
+
# Base class for HTML output converters
|
|
7
|
+
#
|
|
8
|
+
# This class handles ONLY CoreModel types for HTML output.
|
|
9
|
+
# Source models should be transformed to CoreModel before HTML conversion:
|
|
10
|
+
#
|
|
11
|
+
# core_model = Coradoc::Transform::SourceToCoreModel.transform(source_model)
|
|
12
|
+
# html = Coradoc::Html::Static.convert(core_model)
|
|
13
|
+
#
|
|
14
|
+
class Base
|
|
15
|
+
class << self
|
|
16
|
+
# Convert CoreModel to HTML
|
|
17
|
+
# @param model [Coradoc::CoreModel::Base] CoreModel to convert
|
|
18
|
+
# @param state [Hash] Conversion state
|
|
19
|
+
# @return [String] HTML string
|
|
20
|
+
def to_html(model, state = {})
|
|
21
|
+
raise NotImplementedError, "#{self}.to_html must be implemented"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Convert content to HTML (CoreModel → HTML)
|
|
25
|
+
# @param content [various] Content to convert
|
|
26
|
+
# @param state [Hash] Conversion state
|
|
27
|
+
# @return [String] HTML string
|
|
28
|
+
def convert_content_to_html(content, state = {})
|
|
29
|
+
return '' if content.nil?
|
|
30
|
+
|
|
31
|
+
# Handle primitives first
|
|
32
|
+
case content
|
|
33
|
+
when String
|
|
34
|
+
return escape_html(content)
|
|
35
|
+
when Array
|
|
36
|
+
return content.map { |item| convert_content_to_html(item, state) }.join
|
|
37
|
+
when Numeric
|
|
38
|
+
return escape_html(content.to_s)
|
|
39
|
+
when TrueClass, FalseClass
|
|
40
|
+
return escape_html(content.to_s)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Handle CoreModel types
|
|
44
|
+
# NOTE: AnnotationBlock must be checked before Block since AnnotationBlock < Block.
|
|
45
|
+
# We use is_a? directly instead of defined?() because CoreModel uses autoload.
|
|
46
|
+
# The defined?() check doesn't trigger autoload, so it returns nil even when
|
|
47
|
+
# the class is available via autoload. Using is_a? triggers the autoload.
|
|
48
|
+
return render_core_inline_element(content, state) if content.is_a?(Coradoc::CoreModel::InlineElement)
|
|
49
|
+
|
|
50
|
+
return render_core_annotation_block(content, state) if content.is_a?(Coradoc::CoreModel::AnnotationBlock)
|
|
51
|
+
|
|
52
|
+
return render_core_block(content, state) if content.is_a?(Coradoc::CoreModel::Block)
|
|
53
|
+
|
|
54
|
+
if content.is_a?(Coradoc::CoreModel::StructuralElement)
|
|
55
|
+
# Use Section converter for sections
|
|
56
|
+
return Coradoc::Html::Converters::Section.to_html(content, state) if content.section?
|
|
57
|
+
|
|
58
|
+
return render_core_structural_element(content, state)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
return render_core_list_block(content, state) if content.is_a?(Coradoc::CoreModel::ListBlock)
|
|
62
|
+
|
|
63
|
+
return render_core_list_item(content, state) if content.is_a?(Coradoc::CoreModel::ListItem)
|
|
64
|
+
|
|
65
|
+
return Coradoc::Html::Converters::Table.to_html(content, state) if content.is_a?(Coradoc::CoreModel::Table)
|
|
66
|
+
|
|
67
|
+
return render_core_table_row(content, state) if content.is_a?(Coradoc::CoreModel::TableRow)
|
|
68
|
+
|
|
69
|
+
return render_core_table_cell(content, state) if content.is_a?(Coradoc::CoreModel::TableCell)
|
|
70
|
+
|
|
71
|
+
return render_core_term(content, state) if content.is_a?(Coradoc::CoreModel::Term)
|
|
72
|
+
|
|
73
|
+
if content.is_a?(Coradoc::CoreModel::Image)
|
|
74
|
+
return render_core_inline_image(content, state) if content.inline
|
|
75
|
+
|
|
76
|
+
return render_core_block_image(content, state)
|
|
77
|
+
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
return render_core_footnote(content, state) if content.is_a?(Coradoc::CoreModel::Footnote)
|
|
81
|
+
|
|
82
|
+
if content.is_a?(Coradoc::CoreModel::FootnoteReference)
|
|
83
|
+
return render_core_footnote_reference(content,
|
|
84
|
+
state)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
return render_core_abbreviation(content, state) if content.is_a?(Coradoc::CoreModel::Abbreviation)
|
|
88
|
+
|
|
89
|
+
return render_core_definition_list(content, state) if content.is_a?(Coradoc::CoreModel::DefinitionList)
|
|
90
|
+
|
|
91
|
+
return render_core_definition_item(content, state) if content.is_a?(Coradoc::CoreModel::DefinitionItem)
|
|
92
|
+
|
|
93
|
+
return render_core_toc(content, state) if content.is_a?(Coradoc::CoreModel::Toc)
|
|
94
|
+
|
|
95
|
+
return render_core_toc_entry(content, state) if content.is_a?(Coradoc::CoreModel::TocEntry)
|
|
96
|
+
|
|
97
|
+
return render_core_bibliography(content, state) if content.is_a?(Coradoc::CoreModel::Bibliography)
|
|
98
|
+
|
|
99
|
+
if content.is_a?(Coradoc::CoreModel::BibliographyEntry)
|
|
100
|
+
return render_core_bibliography_entry(content,
|
|
101
|
+
state)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Handle unknown types gracefully
|
|
105
|
+
handle_unknown_content(content, state)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# === CoreModel rendering methods ===
|
|
109
|
+
|
|
110
|
+
# Render CoreModel inline element
|
|
111
|
+
def render_core_inline_element(element, state = {})
|
|
112
|
+
case element.format_type
|
|
113
|
+
when 'bold'
|
|
114
|
+
"<strong>#{convert_content_to_html(element.content, state)}</strong>"
|
|
115
|
+
when 'italic'
|
|
116
|
+
"<em>#{convert_content_to_html(element.content, state)}</em>"
|
|
117
|
+
when 'monospace'
|
|
118
|
+
"<code>#{convert_content_to_html(element.content, state)}</code>"
|
|
119
|
+
when 'superscript'
|
|
120
|
+
"<sup>#{convert_content_to_html(element.content, state)}</sup>"
|
|
121
|
+
when 'subscript'
|
|
122
|
+
"<sub>#{convert_content_to_html(element.content, state)}</sub>"
|
|
123
|
+
when 'underline'
|
|
124
|
+
"<u>#{convert_content_to_html(element.content, state)}</u>"
|
|
125
|
+
when 'strikethrough'
|
|
126
|
+
"<del>#{convert_content_to_html(element.content, state)}</del>"
|
|
127
|
+
when 'highlight'
|
|
128
|
+
"<mark>#{convert_content_to_html(element.content, state)}</mark>"
|
|
129
|
+
when 'link'
|
|
130
|
+
href = element.target || element.metadata&.dig(:href) || '#'
|
|
131
|
+
"<a href=\"#{escape_attribute(href)}\">#{convert_content_to_html(element.content, state)}</a>"
|
|
132
|
+
when 'xref'
|
|
133
|
+
href = element.target || element.metadata&.dig(:href) || '#'
|
|
134
|
+
"<a href=\"##{escape_attribute(href)}\">#{convert_content_to_html(element.content, state)}</a>"
|
|
135
|
+
when 'footnote'
|
|
136
|
+
footnote_id = element.target || element.metadata&.dig(:id) || ''
|
|
137
|
+
"<sup class=\"footnote\" id=\"fn-#{escape_attribute(footnote_id)}\">#{convert_content_to_html(
|
|
138
|
+
element.content, state
|
|
139
|
+
)}</sup>"
|
|
140
|
+
when 'stem'
|
|
141
|
+
"<code class=\"stem\">#{escape_html(element.content)}</code>"
|
|
142
|
+
when 'term'
|
|
143
|
+
# Term reference: term:[text] or term:[text,display]
|
|
144
|
+
%(<span class="term" data-term-ref="#{escape_attribute(element.content)}">#{escape_html(element.content)}</span>)
|
|
145
|
+
when 'break'
|
|
146
|
+
break_type = element.metadata&.dig(:break_type) || 'thematic'
|
|
147
|
+
break_type == 'thematic' ? '<hr>' : '<br>'
|
|
148
|
+
when 'quotation'
|
|
149
|
+
"<q>#{convert_content_to_html(element.content, state)}</q>"
|
|
150
|
+
when 'small'
|
|
151
|
+
"<small>#{convert_content_to_html(element.content, state)}</small>"
|
|
152
|
+
when 'span'
|
|
153
|
+
render_core_span(element, state)
|
|
154
|
+
else
|
|
155
|
+
convert_content_to_html(element.content, state)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Render CoreModel span
|
|
160
|
+
def render_core_span(element, state = {})
|
|
161
|
+
attrs = build_class_attribute(element.metadata&.dig(:class))
|
|
162
|
+
"<span#{attrs}>#{convert_content_to_html(element.content, state)}</span>"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Render CoreModel block
|
|
166
|
+
def render_core_block(block, state = {})
|
|
167
|
+
attrs = build_html_attributes(block.id, block.title)
|
|
168
|
+
|
|
169
|
+
# Get renderable content (children if present, otherwise content)
|
|
170
|
+
renderable = block.renderable_content
|
|
171
|
+
|
|
172
|
+
semantic = resolve_block_semantic_type(block)
|
|
173
|
+
|
|
174
|
+
case semantic
|
|
175
|
+
when :paragraph
|
|
176
|
+
content = convert_content_to_html(renderable, state)
|
|
177
|
+
return "<p#{attrs}>#{content}</p>" if content && !content.empty?
|
|
178
|
+
|
|
179
|
+
''
|
|
180
|
+
when :source_code
|
|
181
|
+
lang = block.language || block.metadata&.dig(:language)
|
|
182
|
+
lang_attr = lang ? " data-lang=\"#{escape_attribute(lang)}\"" : ''
|
|
183
|
+
"<pre#{attrs}><code#{lang_attr}>#{escape_html(block.flat_text)}</code></pre>"
|
|
184
|
+
when :quote, :verse
|
|
185
|
+
"<blockquote#{attrs}>#{convert_content_to_html(renderable, state)}</blockquote>"
|
|
186
|
+
when :example
|
|
187
|
+
"<div class=\"example\"#{attrs}>#{convert_content_to_html(renderable, state)}</div>"
|
|
188
|
+
when :sidebar
|
|
189
|
+
"<aside class=\"sidebar\"#{attrs}>#{convert_content_to_html(renderable, state)}</aside>"
|
|
190
|
+
when :literal
|
|
191
|
+
"<pre class=\"literal\"#{attrs}>#{escape_html(block.flat_text)}</pre>"
|
|
192
|
+
when :pass
|
|
193
|
+
block.flat_text
|
|
194
|
+
when :listing
|
|
195
|
+
"<pre#{attrs}>#{escape_html(block.flat_text)}</pre>"
|
|
196
|
+
when :open
|
|
197
|
+
"<div#{attrs}>#{convert_content_to_html(renderable, state)}</div>"
|
|
198
|
+
when :verse
|
|
199
|
+
"<blockquote#{attrs}>#{convert_content_to_html(renderable, state)}</blockquote>"
|
|
200
|
+
when :comment, :reviewer
|
|
201
|
+
''
|
|
202
|
+
when :horizontal_rule
|
|
203
|
+
"<hr#{attrs}>"
|
|
204
|
+
else
|
|
205
|
+
"<div#{attrs}>#{convert_content_to_html(renderable, state)}</div>"
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Resolve the semantic type from a block via polymorphic dispatch.
|
|
210
|
+
# Block#resolve_semantic_type handles class-level semantic_type →
|
|
211
|
+
# block_semantic_type attribute → element_type → delimiter fallback.
|
|
212
|
+
def resolve_block_semantic_type(block)
|
|
213
|
+
block.resolve_semantic_type ||
|
|
214
|
+
resolve_format_specific_semantic(block)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Format-specific semantic mappings not covered by the core model
|
|
218
|
+
def resolve_format_specific_semantic(block)
|
|
219
|
+
delim = block.delimiter_type
|
|
220
|
+
return nil unless delim && !delim.empty?
|
|
221
|
+
|
|
222
|
+
case delim
|
|
223
|
+
when "'''", '---', '___', '***' then :horizontal_rule
|
|
224
|
+
when '[verse]' then :verse
|
|
225
|
+
when 'comment' then :comment
|
|
226
|
+
when 'paragraph' then :paragraph
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Render CoreModel structural element
|
|
231
|
+
def render_core_structural_element(element, state = {})
|
|
232
|
+
attrs = build_html_attributes(element.id, nil)
|
|
233
|
+
|
|
234
|
+
children_html = (element.children || []).map { |c| convert_content_to_html(c, state) }.join
|
|
235
|
+
case element.element_type
|
|
236
|
+
when 'document'
|
|
237
|
+
"<article#{attrs}>#{children_html}</article>"
|
|
238
|
+
when 'header'
|
|
239
|
+
"<header#{attrs}>#{children_html}</header>"
|
|
240
|
+
when 'section'
|
|
241
|
+
level = element.heading_level
|
|
242
|
+
level = [level, 6].min
|
|
243
|
+
title_html = element.title ? "<h#{level}>#{escape_html(element.title)}</h#{level}>" : ''
|
|
244
|
+
"<section#{attrs}>#{title_html}#{children_html}</section>"
|
|
245
|
+
else
|
|
246
|
+
"<div#{attrs}>#{children_html}</div>"
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Render CoreModel list block
|
|
251
|
+
def render_core_list_block(list, state = {})
|
|
252
|
+
attrs = build_html_attributes(list.id, list.title)
|
|
253
|
+
|
|
254
|
+
items_html = (list.items || []).map { |i| convert_content_to_html(i, state) }.join
|
|
255
|
+
case list.marker_type
|
|
256
|
+
when 'unordered'
|
|
257
|
+
"<ul#{attrs}>#{items_html}</ul>"
|
|
258
|
+
when 'ordered'
|
|
259
|
+
"<ol#{attrs}>#{items_html}</ol>"
|
|
260
|
+
when 'definition'
|
|
261
|
+
"<dl#{attrs}>#{items_html}</dl>"
|
|
262
|
+
else
|
|
263
|
+
"<ul#{attrs}>#{items_html}</ul>"
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Render CoreModel list item
|
|
268
|
+
def render_core_list_item(item, state = {})
|
|
269
|
+
# Use renderable_content to get children if present
|
|
270
|
+
renderable = item.renderable_content
|
|
271
|
+
content = convert_content_to_html(renderable, state)
|
|
272
|
+
|
|
273
|
+
# Handle nested list
|
|
274
|
+
content += convert_content_to_html(item.nested_list, state) if item.nested_list
|
|
275
|
+
|
|
276
|
+
"<li>#{content}</li>"
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Render CoreModel annotation block (admonition)
|
|
280
|
+
def render_core_annotation_block(block, state = {})
|
|
281
|
+
attrs = build_html_attributes(block.id, block.title)
|
|
282
|
+
type_class = block.annotation_type ? " #{escape_html(block.annotation_type)}" : ''
|
|
283
|
+
label = block.annotation_label || block.annotation_type&.upcase
|
|
284
|
+
|
|
285
|
+
html = "<div class=\"admonition#{type_class}\"#{attrs}>"
|
|
286
|
+
html += "<div class=\"admonition-label\">#{escape_html(label)}</div>" if label
|
|
287
|
+
renderable = block.renderable_content
|
|
288
|
+
html += convert_content_to_html(renderable, state)
|
|
289
|
+
html += '</div>'
|
|
290
|
+
html
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Render CoreModel table row
|
|
294
|
+
def render_core_table_row(row, state = {})
|
|
295
|
+
cells = row.cells || row.columns || []
|
|
296
|
+
cells_html = cells.map { |c| convert_content_to_html(c, state) }.join
|
|
297
|
+
tag = row.header ? 'thead' : 'tr'
|
|
298
|
+
"<#{tag}>#{cells_html}</#{tag}>"
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Render CoreModel table cell
|
|
302
|
+
def render_core_table_cell(cell, state = {})
|
|
303
|
+
tag = cell.header ? 'th' : 'td'
|
|
304
|
+
attrs = ''
|
|
305
|
+
attrs += " colspan=\"#{cell.colspan}\"" if cell.colspan
|
|
306
|
+
attrs += " rowspan=\"#{cell.rowspan}\"" if cell.rowspan
|
|
307
|
+
attrs += " style=\"text-align: #{escape_html(cell.alignment)}\"" if cell.alignment
|
|
308
|
+
|
|
309
|
+
# Use renderable_content to get children if present, otherwise content
|
|
310
|
+
renderable = cell.renderable_content
|
|
311
|
+
content = convert_content_to_html(renderable, state)
|
|
312
|
+
"<#{tag}#{attrs}>#{content}</#{tag}>"
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Render CoreModel term
|
|
316
|
+
def render_core_term(term, _state = {})
|
|
317
|
+
term_text = term.text || ''
|
|
318
|
+
term_type = term.term_type || term.type || 'term'
|
|
319
|
+
display_text = term.render_text&.strip&.empty? ? false : term.render_text
|
|
320
|
+
display_text ||= term_text
|
|
321
|
+
|
|
322
|
+
%(<span class="term term-#{escape_attribute(term_type)}" data-term-ref="#{escape_attribute(term_text)}">#{escape_html(display_text)}</span>)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Render CoreModel inline image
|
|
326
|
+
def render_core_inline_image(image, _state = {})
|
|
327
|
+
attrs = "src=\"#{escape_attribute(image.src)}\""
|
|
328
|
+
attrs += " alt=\"#{escape_attribute(image.alt)}\"" if image.alt
|
|
329
|
+
attrs += " width=\"#{escape_attribute(image.width)}\"" if image.width
|
|
330
|
+
attrs += " height=\"#{escape_attribute(image.height)}\"" if image.height
|
|
331
|
+
|
|
332
|
+
%(<img #{attrs}>)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Render CoreModel block image
|
|
336
|
+
def render_core_block_image(image, _state = {})
|
|
337
|
+
attrs = build_html_attributes(image.id, nil)
|
|
338
|
+
img_attrs = "src=\"#{escape_attribute(image.src)}\""
|
|
339
|
+
img_attrs += " alt=\"#{escape_attribute(image.alt)}\"" if image.alt
|
|
340
|
+
img_attrs += " width=\"#{escape_attribute(image.width)}\"" if image.width
|
|
341
|
+
img_attrs += " height=\"#{escape_attribute(image.height)}\"" if image.height
|
|
342
|
+
|
|
343
|
+
html = "<figure#{attrs}>"
|
|
344
|
+
html += %(<img #{img_attrs}>)
|
|
345
|
+
html += "<figcaption>#{escape_html(image.caption)}</figcaption>" if image.caption
|
|
346
|
+
html += '</figure>'
|
|
347
|
+
html
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# Render CoreModel footnote
|
|
351
|
+
def render_core_footnote(footnote, state = {})
|
|
352
|
+
footnote_id = footnote.id || ''
|
|
353
|
+
content = footnote.content || footnote.inline_content
|
|
354
|
+
|
|
355
|
+
if footnote_id.empty?
|
|
356
|
+
# Anonymous footnote
|
|
357
|
+
text = content.is_a?(Array) ? content.join : content.to_s
|
|
358
|
+
title_text = text[0..50]
|
|
359
|
+
%(<sup class="footnote" title="#{escape_attribute(title_text)}">#{convert_content_to_html(content,
|
|
360
|
+
state)}</sup>)
|
|
361
|
+
else
|
|
362
|
+
# Named footnote reference
|
|
363
|
+
%(<sup class="footnote"><a href="#fn-#{escape_attribute(footnote_id)}" id="fnref-#{escape_attribute(footnote_id)}">#{escape_html(footnote_id)}</a></sup>)
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Render CoreModel footnote reference
|
|
368
|
+
def render_core_footnote_reference(ref, _state = {})
|
|
369
|
+
footnote_id = ref.id || ''
|
|
370
|
+
%(<sup class="footnote"><a href="#fn-#{escape_attribute(footnote_id)}">[#{escape_html(footnote_id)}]</a></sup>)
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# Render CoreModel abbreviation
|
|
374
|
+
def render_core_abbreviation(abbr, _state = {})
|
|
375
|
+
term = abbr.term || ''
|
|
376
|
+
definition = abbr.definition || ''
|
|
377
|
+
%(<abbr title="#{escape_attribute(definition)}">#{escape_html(term)}</abbr>)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Render CoreModel definition list
|
|
381
|
+
def render_core_definition_list(dl, state = {})
|
|
382
|
+
attrs = build_html_attributes(dl.id, dl.title)
|
|
383
|
+
items_html = (dl.items || []).map { |i| convert_content_to_html(i, state) }.join
|
|
384
|
+
"<dl#{attrs}>#{items_html}</dl>"
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# Render CoreModel definition item
|
|
388
|
+
def render_core_definition_item(item, state = {})
|
|
389
|
+
term_html = convert_content_to_html(item.term, state)
|
|
390
|
+
definitions_html = (item.definitions || []).map { |d| "<dd>#{convert_content_to_html(d, state)}</dd>" }.join
|
|
391
|
+
"<dt>#{term_html}</dt>#{definitions_html}"
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# Render CoreModel TOC
|
|
395
|
+
def render_core_toc(toc, state = {})
|
|
396
|
+
attrs = build_html_attributes(nil, nil)
|
|
397
|
+
attrs += ' class="toc"'
|
|
398
|
+
entries_html = (toc.entries || []).map { |e| convert_content_to_html(e, state) }.join
|
|
399
|
+
"<nav#{attrs}><h2>Table of Contents</h2><ul>#{entries_html}</ul></nav>"
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
# Render CoreModel TOC entry
|
|
403
|
+
def render_core_toc_entry(entry, state = {})
|
|
404
|
+
id = entry.id || ''
|
|
405
|
+
title = entry.title || ''
|
|
406
|
+
entry.level || 1
|
|
407
|
+
number = entry.number
|
|
408
|
+
display_title = number ? "#{number}. #{title}" : title
|
|
409
|
+
|
|
410
|
+
item_html = if id.empty?
|
|
411
|
+
escape_html(display_title)
|
|
412
|
+
else
|
|
413
|
+
%(<a href="##{escape_attribute(id)}">#{escape_html(display_title)}</a>)
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
children_html = (entry.children || []).map { |c| convert_content_to_html(c, state) }.join
|
|
417
|
+
children_html = "<ul>#{children_html}</ul>" unless children_html.empty?
|
|
418
|
+
|
|
419
|
+
"<li>#{item_html}#{children_html}</li>"
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def render_core_bibliography(bib, state = {})
|
|
423
|
+
attrs = %( class="bibliography")
|
|
424
|
+
attrs += %( id="#{escape_attribute(bib.id)}") if bib.id
|
|
425
|
+
|
|
426
|
+
title_html = (%(<h2 class="bibliography-title">#{escape_html(bib.title)}</h2>) if bib.title && !bib.title.to_s.empty?)
|
|
427
|
+
|
|
428
|
+
entries_html = Array(bib.entries).map { |e| convert_content_to_html(e, state) }.join("\n")
|
|
429
|
+
|
|
430
|
+
inner = ''
|
|
431
|
+
inner += "#{title_html}\n" if title_html
|
|
432
|
+
inner += "<div class=\"bibliography-entries\">\n#{entries_html}\n</div>" unless entries_html.empty?
|
|
433
|
+
|
|
434
|
+
"<section#{attrs}>\n#{inner}\n</section>"
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def render_core_bibliography_entry(entry, _state = {})
|
|
438
|
+
entry_id = entry.anchor_name || entry.document_id
|
|
439
|
+
anchor_html = entry_id ? %(<a id="#{escape_attribute(entry_id)}" class="bibliography-anchor"></a>) : ''
|
|
440
|
+
label = entry.document_id || ''
|
|
441
|
+
ref_text = entry.ref_text || ''
|
|
442
|
+
label_html = label.empty? ? '' : %(<span class="bibliography-label">#{escape_html(label)}</span> )
|
|
443
|
+
|
|
444
|
+
"<div class=\"bibliography-entry\">#{anchor_html}#{label_html}#{escape_html(ref_text)}</div>"
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
# Handle unknown content types
|
|
448
|
+
def handle_unknown_content(content, _state = {})
|
|
449
|
+
if content.is_a?(Coradoc::CoreModel::Base)
|
|
450
|
+
raise ArgumentError,
|
|
451
|
+
"Unknown CoreModel type for HTML conversion: #{content.class}. " \
|
|
452
|
+
'Expected a recognized CoreModel type.'
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# Handle non-CoreModel types (strings from mixed content, etc.)
|
|
456
|
+
escape_html(content.to_s)
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
# Extract text from unknown model types as a fallback
|
|
460
|
+
def extract_text_fallback(content)
|
|
461
|
+
if content.is_a?(Coradoc::CoreModel::Base)
|
|
462
|
+
if content.class.attributes.key?(:text) && content.text
|
|
463
|
+
text_val = content.text
|
|
464
|
+
return text_val if text_val.is_a?(String)
|
|
465
|
+
|
|
466
|
+
return text_val.to_s
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
if content.class.attributes.key?(:content) && content.content
|
|
470
|
+
content_val = content.content
|
|
471
|
+
if content_val.is_a?(String)
|
|
472
|
+
return content_val
|
|
473
|
+
elsif content_val.is_a?(Array)
|
|
474
|
+
return content_val.map { |item| convert_content_to_html(item, {}) }.join
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
return content.href.to_s if content.class.attributes.key?(:href) && content.href
|
|
479
|
+
return content.term.to_s if content.class.attributes.key?(:term) && content.term
|
|
480
|
+
return content.id.to_s if content.class.attributes.key?(:id) && content.id
|
|
481
|
+
return content.name.to_s if content.class.attributes.key?(:name) && content.name
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
''
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
# === Helper methods ===
|
|
488
|
+
|
|
489
|
+
# Build HTML attributes string
|
|
490
|
+
def build_html_attributes(id, title)
|
|
491
|
+
attrs = ''
|
|
492
|
+
attrs += " id=\"#{escape_attribute(id)}\"" if id && !id.to_s.empty?
|
|
493
|
+
attrs += " title=\"#{escape_attribute(title)}\"" if title && !title.to_s.empty?
|
|
494
|
+
attrs
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
# Build class attribute
|
|
498
|
+
def build_class_attribute(class_name)
|
|
499
|
+
class_name ? " class=\"#{escape_attribute(class_name)}\"" : ''
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
# Find converter for model class
|
|
503
|
+
def find_converter_for_model(model_class)
|
|
504
|
+
Coradoc::Html::Base.find_converter(model_class)
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
# Type-safe lookup of converter class by name
|
|
508
|
+
def find_converter_class_by_name(converter_name)
|
|
509
|
+
klass = Coradoc::Html::Converters.const_get(converter_name, false)
|
|
510
|
+
return klass if klass <= Coradoc::Html::Converters::Base
|
|
511
|
+
|
|
512
|
+
nil
|
|
513
|
+
rescue NameError
|
|
514
|
+
nil
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
# Escape HTML entities
|
|
518
|
+
def escape_html(text)
|
|
519
|
+
Coradoc::Html::Base.escape_html(text)
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# Escape HTML attribute values
|
|
523
|
+
def escape_attribute(value)
|
|
524
|
+
return '' if value.nil?
|
|
525
|
+
|
|
526
|
+
value.to_s.gsub(/&/, '&').gsub(/"/, '"').gsub(/</, '<').gsub(/>/, '>')
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
# Build HTML element
|
|
530
|
+
def build_element(tag, content = nil, attributes = {})
|
|
531
|
+
Coradoc::Html::Base.build_element(tag, content, attributes)
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
# Extract attributes from a CoreModel
|
|
535
|
+
# @param model [Coradoc::CoreModel::Base] Model to extract attributes from
|
|
536
|
+
# @return [Hash] Attributes hash
|
|
537
|
+
def extract_model_attributes(model)
|
|
538
|
+
Coradoc::Html::Base.extract_attributes(model)
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
# === HTML Input Direction (HTML → CoreModel) ===
|
|
542
|
+
|
|
543
|
+
# Process children of an HTML node
|
|
544
|
+
# @param node [Nokogiri::XML::Node] Parent node
|
|
545
|
+
# @param state [Hash] Conversion state
|
|
546
|
+
# @return [Array] Array of converted content
|
|
547
|
+
def treat_children(node, state = {})
|
|
548
|
+
return [] unless node&.children
|
|
549
|
+
|
|
550
|
+
node.children.flat_map do |child|
|
|
551
|
+
convert_node_to_core(child, state)
|
|
552
|
+
end.compact
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
# Convert HTML node to CoreModel
|
|
556
|
+
# @param node [Nokogiri::XML::Node] Node to convert
|
|
557
|
+
# @param state [Hash] Conversion state
|
|
558
|
+
# @return [Coradoc::CoreModel::Base, String, nil]
|
|
559
|
+
def convert_node_to_core(node, state = {})
|
|
560
|
+
case node.type
|
|
561
|
+
when Nokogiri::XML::Node::TEXT_NODE
|
|
562
|
+
text = node.text
|
|
563
|
+
return nil if text.strip.empty? && !state[:preserve_whitespace]
|
|
564
|
+
|
|
565
|
+
text
|
|
566
|
+
when Nokogiri::XML::Node::ELEMENT_NODE
|
|
567
|
+
convert_element_to_core(node, state)
|
|
568
|
+
when Nokogiri::XML::Node::COMMENT_NODE
|
|
569
|
+
nil # Skip comments
|
|
570
|
+
end
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
# Convert HTML element to CoreModel
|
|
574
|
+
# @param node [Nokogiri::XML::Node] Element node
|
|
575
|
+
# @param state [Hash] Conversion state
|
|
576
|
+
# @return [Coradoc::CoreModel::Base, Array, nil]
|
|
577
|
+
def convert_element_to_core(node, state = {})
|
|
578
|
+
# Delegate to Html::Input::Converters for HTML input
|
|
579
|
+
# This maintains separation between input and output converters
|
|
580
|
+
if defined?(Coradoc::Html::Input::Converters)
|
|
581
|
+
converter = Coradoc::Html::Input::Converters.lookup(node.name)
|
|
582
|
+
if converter
|
|
583
|
+
result = converter.to_coradoc(node, state)
|
|
584
|
+
# Transform to CoreModel if needed
|
|
585
|
+
return transform_to_coremodel(result) if result
|
|
586
|
+
end
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
# Fallback: treat children
|
|
590
|
+
treat_children(node, state)
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
# Transform model to CoreModel
|
|
594
|
+
# @param model [Object] Model to transform
|
|
595
|
+
# @return [Coradoc::CoreModel::Base, Object]
|
|
596
|
+
def transform_to_coremodel(model)
|
|
597
|
+
# Already a CoreModel type - return as-is
|
|
598
|
+
model
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
# Extract attributes from HTML node
|
|
602
|
+
# @param node [Nokogiri::XML::Node] HTML node
|
|
603
|
+
# @return [Hash] Attributes hash
|
|
604
|
+
def extract_node_attributes(node)
|
|
605
|
+
return {} unless node.is_a?(Nokogiri::XML::Node)
|
|
606
|
+
|
|
607
|
+
node.attributes.each_with_object({}) do |(name, attr), hash|
|
|
608
|
+
hash[name.to_sym] = attr.value
|
|
609
|
+
end
|
|
610
|
+
end
|
|
611
|
+
end
|
|
612
|
+
end
|
|
613
|
+
end
|
|
614
|
+
end
|
|
615
|
+
end
|