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,409 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'liquid'
|
|
4
|
+
require_relative 'template_locator'
|
|
5
|
+
require_relative 'template_helpers'
|
|
6
|
+
|
|
7
|
+
module Coradoc
|
|
8
|
+
module Html
|
|
9
|
+
# Unified template renderer using Liquid templates for CoreModel types
|
|
10
|
+
#
|
|
11
|
+
# This class provides a template-based rendering system where:
|
|
12
|
+
# - Users provide template directories (checked in order, first wins)
|
|
13
|
+
# - System falls back to default templates if enabled
|
|
14
|
+
# - CoreModel objects are automatically converted to Liquid drops via to_liquid
|
|
15
|
+
#
|
|
16
|
+
# @example Basic usage with default templates only
|
|
17
|
+
# renderer = Coradoc::Html::Renderer.new
|
|
18
|
+
# html = renderer.render(bibliography_element)
|
|
19
|
+
#
|
|
20
|
+
# @example With custom template directory (falls back to defaults)
|
|
21
|
+
# renderer = Coradoc::Html::Renderer.new(
|
|
22
|
+
# template_dirs: ["./my_templates"]
|
|
23
|
+
# )
|
|
24
|
+
# html = renderer.render(bibliography_element)
|
|
25
|
+
#
|
|
26
|
+
# @example Multiple template dirs with priority (first wins)
|
|
27
|
+
# renderer = Coradoc::Html::Renderer.new(
|
|
28
|
+
# template_dirs: ["./project_templates", "./shared_templates"],
|
|
29
|
+
# include_default_templates: true
|
|
30
|
+
# )
|
|
31
|
+
#
|
|
32
|
+
# @example Custom templates only, no defaults
|
|
33
|
+
# renderer = Coradoc::Html::Renderer.new(
|
|
34
|
+
# template_dirs: ["./my_templates"],
|
|
35
|
+
# include_default_templates: false
|
|
36
|
+
# )
|
|
37
|
+
#
|
|
38
|
+
class Renderer
|
|
39
|
+
# Mapping of CoreModel class names to template type names
|
|
40
|
+
TEMPLATE_TYPE_MAP = {
|
|
41
|
+
'Coradoc::CoreModel::Bibliography' => 'bibliography',
|
|
42
|
+
'Coradoc::CoreModel::BibliographyEntry' => 'bibliography_entry',
|
|
43
|
+
'Coradoc::CoreModel::StructuralElement' => 'structural_element',
|
|
44
|
+
'Coradoc::CoreModel::Block' => 'block',
|
|
45
|
+
'Coradoc::CoreModel::SourceBlock' => 'source_block',
|
|
46
|
+
'Coradoc::CoreModel::ExampleBlock' => 'example_block',
|
|
47
|
+
'Coradoc::CoreModel::QuoteBlock' => 'quote_block',
|
|
48
|
+
'Coradoc::CoreModel::SidebarBlock' => 'sidebar_block',
|
|
49
|
+
'Coradoc::CoreModel::LiteralBlock' => 'literal_block',
|
|
50
|
+
'Coradoc::CoreModel::PassBlock' => 'pass_block',
|
|
51
|
+
'Coradoc::CoreModel::ListingBlock' => 'listing_block',
|
|
52
|
+
'Coradoc::CoreModel::OpenBlock' => 'open_block',
|
|
53
|
+
'Coradoc::CoreModel::VerseBlock' => 'verse_block',
|
|
54
|
+
'Coradoc::CoreModel::ReviewerBlock' => 'reviewer_block',
|
|
55
|
+
'Coradoc::CoreModel::AnnotationBlock' => 'annotation_block',
|
|
56
|
+
'Coradoc::CoreModel::ListBlock' => 'list_block',
|
|
57
|
+
'Coradoc::CoreModel::ListItem' => 'list_item',
|
|
58
|
+
'Coradoc::CoreModel::Table' => 'table',
|
|
59
|
+
'Coradoc::CoreModel::TableRow' => 'table_row',
|
|
60
|
+
'Coradoc::CoreModel::TableCell' => 'table_cell',
|
|
61
|
+
'Coradoc::CoreModel::Image' => 'image',
|
|
62
|
+
'Coradoc::CoreModel::InlineElement' => 'inline_element',
|
|
63
|
+
'Coradoc::CoreModel::Paragraph' => 'paragraph',
|
|
64
|
+
'Coradoc::CoreModel::Term' => 'term',
|
|
65
|
+
'Coradoc::CoreModel::Footnote' => 'footnote',
|
|
66
|
+
'Coradoc::CoreModel::FootnoteReference' => 'footnote_reference',
|
|
67
|
+
'Coradoc::CoreModel::Toc' => 'toc',
|
|
68
|
+
'Coradoc::CoreModel::TocEntry' => 'toc_entry',
|
|
69
|
+
'Coradoc::CoreModel::DefinitionList' => 'definition_list',
|
|
70
|
+
'Coradoc::CoreModel::DefinitionItem' => 'definition_item',
|
|
71
|
+
'Coradoc::CoreModel::Abbreviation' => 'abbreviation'
|
|
72
|
+
}.freeze
|
|
73
|
+
|
|
74
|
+
# Default template directory (built-in templates)
|
|
75
|
+
DEFAULT_TEMPLATE_DIR = Pathname.new(File.join(File.dirname(__FILE__), 'templates', 'core_model'))
|
|
76
|
+
|
|
77
|
+
attr_reader :template_dirs, :include_default_templates, :options
|
|
78
|
+
|
|
79
|
+
# Initialize the renderer
|
|
80
|
+
#
|
|
81
|
+
# @param template_dirs [Array<String>, String, nil] Custom template directories
|
|
82
|
+
# Searched in order (first match wins). Can be a single path or array.
|
|
83
|
+
# @param include_default_templates [Boolean] Whether to fall back to built-in
|
|
84
|
+
# templates when not found in template_dirs. Default: true
|
|
85
|
+
# @param options [Hash] Additional options
|
|
86
|
+
# @option options [Boolean] :strict Raise error if template not found (default: false)
|
|
87
|
+
# @option options [Boolean] :cache_templates Cache parsed templates (default: true)
|
|
88
|
+
#
|
|
89
|
+
def initialize(template_dirs: nil, include_default_templates: true, options: {})
|
|
90
|
+
@template_dirs = normalize_template_dirs(template_dirs)
|
|
91
|
+
@include_default_templates = include_default_templates
|
|
92
|
+
@options = { cache_templates: true, strict: false }.merge(options)
|
|
93
|
+
@template_cache = {}
|
|
94
|
+
ensure_core_model_drops
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Render a CoreModel element to HTML
|
|
98
|
+
#
|
|
99
|
+
# @param element [Coradoc::CoreModel::Base] The element to render
|
|
100
|
+
# @param context [Hash] Additional context for the template
|
|
101
|
+
# @return [String] Rendered HTML
|
|
102
|
+
def render(element, context = {})
|
|
103
|
+
return '' if element.nil?
|
|
104
|
+
|
|
105
|
+
# Ensure liquid drop class exists for lutaml-model elements
|
|
106
|
+
ensure_drop_class(element)
|
|
107
|
+
|
|
108
|
+
# Handle arrays
|
|
109
|
+
return element.map { |e| render(e, context) }.join("\n") if element.is_a?(Array)
|
|
110
|
+
|
|
111
|
+
# Handle primitives
|
|
112
|
+
case element
|
|
113
|
+
when String
|
|
114
|
+
return escape_html(element)
|
|
115
|
+
when Numeric, TrueClass, FalseClass
|
|
116
|
+
return element.to_s
|
|
117
|
+
when NilClass
|
|
118
|
+
return ''
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Get template type name for this element
|
|
122
|
+
type_name = template_type_for(element)
|
|
123
|
+
return render_fallback(element, context) unless type_name
|
|
124
|
+
|
|
125
|
+
# Find the template file
|
|
126
|
+
template_path = find_template(type_name)
|
|
127
|
+
return render_fallback(element, context) if template_path.nil?
|
|
128
|
+
|
|
129
|
+
# Load and render the template
|
|
130
|
+
template = load_template(template_path)
|
|
131
|
+
if template
|
|
132
|
+
render_with_template(template, element, context)
|
|
133
|
+
else
|
|
134
|
+
render_fallback(element, context)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Render a CoreModel element as a complete HTML5 document
|
|
139
|
+
#
|
|
140
|
+
# Wraps the fragment output of #render in a proper HTML5 document
|
|
141
|
+
# with DOCTYPE, charset, and viewport meta tags.
|
|
142
|
+
#
|
|
143
|
+
# @param element [Coradoc::CoreModel::Base] The element to render
|
|
144
|
+
# @param options [Hash] Document-level options
|
|
145
|
+
# @option options [String] :lang Document language (default: "en")
|
|
146
|
+
# @option options [String] :title Document title (default: extracted from element)
|
|
147
|
+
# @return [String] Complete HTML5 document
|
|
148
|
+
def render_html5(element, options = {})
|
|
149
|
+
body_html = render(element)
|
|
150
|
+
|
|
151
|
+
lang = options[:lang] || 'en'
|
|
152
|
+
title = options[:title] || extract_title(element) || 'Untitled Document'
|
|
153
|
+
|
|
154
|
+
<<~HTML
|
|
155
|
+
<!DOCTYPE html>
|
|
156
|
+
<html lang="#{lang}">
|
|
157
|
+
<head>
|
|
158
|
+
<meta charset="UTF-8">
|
|
159
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
160
|
+
<title>#{escape_html(title)}</title>
|
|
161
|
+
</head>
|
|
162
|
+
<body>
|
|
163
|
+
#{body_html}
|
|
164
|
+
</body>
|
|
165
|
+
</html>
|
|
166
|
+
HTML
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Get list of all available template names
|
|
170
|
+
#
|
|
171
|
+
# @return [Array<String>] List of template names (without .liquid extension)
|
|
172
|
+
def available_templates
|
|
173
|
+
templates = Set.new
|
|
174
|
+
|
|
175
|
+
# Scan user template directories
|
|
176
|
+
@template_dirs.each do |dir|
|
|
177
|
+
core_model_dir = File.join(dir, 'core_model')
|
|
178
|
+
next unless File.directory?(core_model_dir)
|
|
179
|
+
|
|
180
|
+
Dir.glob(File.join(core_model_dir, '*.liquid')).each do |file|
|
|
181
|
+
templates << File.basename(file, '.liquid')
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Scan default templates if included
|
|
186
|
+
if @include_default_templates && File.directory?(DEFAULT_TEMPLATE_DIR)
|
|
187
|
+
Dir.glob(File.join(DEFAULT_TEMPLATE_DIR, '*.liquid')).each do |file|
|
|
188
|
+
templates << File.basename(file, '.liquid')
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
templates.to_a.sort
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Check if a template exists for a given type
|
|
196
|
+
#
|
|
197
|
+
# @param type_name [String] Template type name (e.g., "bibliography")
|
|
198
|
+
# @return [Boolean] True if template exists
|
|
199
|
+
def template_exists?(type_name)
|
|
200
|
+
!find_template(type_name).nil?
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Register a custom template type mapping
|
|
204
|
+
#
|
|
205
|
+
# @param class_name [String] Full class name (e.g., "Coradoc::CoreModel::Bibliography")
|
|
206
|
+
# @param type_name [String] Template type name (e.g., "bibliography")
|
|
207
|
+
def self.register_type(class_name, type_name)
|
|
208
|
+
@custom_type_map ||= {}
|
|
209
|
+
@custom_type_map[class_name] = type_name
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Get custom type mappings
|
|
213
|
+
def self.custom_type_map
|
|
214
|
+
@custom_type_map ||= {}
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
private
|
|
218
|
+
|
|
219
|
+
# Ensure liquid drop class exists for lutaml-model elements
|
|
220
|
+
# (lutaml-model may not create drops if Liquid was loaded after the model class)
|
|
221
|
+
def ensure_drop_class(element)
|
|
222
|
+
klass = element.class
|
|
223
|
+
if klass.public_methods.include?(:register_class_if_liquid_defined) &&
|
|
224
|
+
klass.public_methods.include?(:base_drop_class) && !klass.base_drop_class
|
|
225
|
+
klass.register_class_if_liquid_defined
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def ensure_core_model_drops
|
|
230
|
+
return unless defined?(Coradoc::CoreModel)
|
|
231
|
+
|
|
232
|
+
CoreModel.constants(false).each do |const_name|
|
|
233
|
+
klass = CoreModel.const_get(const_name)
|
|
234
|
+
next unless klass.is_a?(Class)
|
|
235
|
+
next unless klass.public_methods.include?(:register_class_if_liquid_defined)
|
|
236
|
+
next if klass.public_methods.include?(:base_drop_class) && klass.base_drop_class
|
|
237
|
+
|
|
238
|
+
klass.register_class_if_liquid_defined
|
|
239
|
+
rescue StandardError
|
|
240
|
+
nil
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Normalize template_dirs to an array of absolute paths
|
|
245
|
+
def normalize_template_dirs(dirs)
|
|
246
|
+
return [] if dirs.nil?
|
|
247
|
+
|
|
248
|
+
Array(dirs).map do |dir|
|
|
249
|
+
path = Pathname.new(dir)
|
|
250
|
+
path.absolute? ? path.to_s : File.expand_path(dir)
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Find template file for a type name
|
|
255
|
+
# Searches template_dirs in order, then defaults if enabled
|
|
256
|
+
#
|
|
257
|
+
# @param type_name [String] Template type name
|
|
258
|
+
# @return [String, nil] Path to template file or nil
|
|
259
|
+
def find_template(type_name)
|
|
260
|
+
template_file = "#{type_name}.liquid"
|
|
261
|
+
|
|
262
|
+
# Search user template directories in order
|
|
263
|
+
@template_dirs.each do |dir|
|
|
264
|
+
# Check core_model subdirectory first
|
|
265
|
+
core_model_dir = File.join(dir, 'core_model')
|
|
266
|
+
path = File.join(core_model_dir, template_file)
|
|
267
|
+
return path if File.file?(path)
|
|
268
|
+
|
|
269
|
+
# Also check the directory itself
|
|
270
|
+
path = File.join(dir, template_file)
|
|
271
|
+
return path if File.file?(path)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Fall back to default templates if enabled
|
|
275
|
+
if @include_default_templates
|
|
276
|
+
path = File.join(DEFAULT_TEMPLATE_DIR, template_file)
|
|
277
|
+
return path if File.file?(path)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
nil
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Get template type name for an element
|
|
284
|
+
#
|
|
285
|
+
# @param element [Object] The element
|
|
286
|
+
# @return [String, nil] Template type name or nil
|
|
287
|
+
def template_type_for(element)
|
|
288
|
+
class_name = element.class.name
|
|
289
|
+
|
|
290
|
+
# Check custom registrations first
|
|
291
|
+
self.class.custom_type_map[class_name] ||
|
|
292
|
+
TEMPLATE_TYPE_MAP[class_name] ||
|
|
293
|
+
derive_type_name(class_name)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Derive template type name from class name
|
|
297
|
+
#
|
|
298
|
+
# @param class_name [String] Full class name
|
|
299
|
+
# @return [String] Derived type name
|
|
300
|
+
def derive_type_name(class_name)
|
|
301
|
+
parts = class_name.split('::')
|
|
302
|
+
return nil unless parts.length >= 2
|
|
303
|
+
|
|
304
|
+
# Just use the class name, underscored
|
|
305
|
+
parts.last
|
|
306
|
+
.gsub(/([A-Z])/, '_\1')
|
|
307
|
+
.downcase
|
|
308
|
+
.sub(/^_/, '')
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Load a template from file
|
|
312
|
+
#
|
|
313
|
+
# @param path [String] Path to template file
|
|
314
|
+
# @return [Liquid::Template, nil] Parsed template or nil
|
|
315
|
+
def load_template(path)
|
|
316
|
+
cache_key = path.to_s
|
|
317
|
+
return @template_cache[cache_key] if @template_cache.key?(cache_key)
|
|
318
|
+
|
|
319
|
+
template_content = File.read(path)
|
|
320
|
+
template = Liquid::Template.parse(template_content)
|
|
321
|
+
@template_cache[cache_key] = template if @options[:cache_templates]
|
|
322
|
+
template
|
|
323
|
+
rescue Liquid::SyntaxError => e
|
|
324
|
+
warn "Template syntax error in #{path}: #{e.message}"
|
|
325
|
+
nil
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Render element with template
|
|
329
|
+
#
|
|
330
|
+
# @param template [Liquid::Template] The template
|
|
331
|
+
# @param element [Object] The element to render
|
|
332
|
+
# @param context [Hash] Additional context
|
|
333
|
+
# @return [String] Rendered HTML
|
|
334
|
+
def render_with_template(template, element, context)
|
|
335
|
+
# Convert element to Liquid Drop
|
|
336
|
+
liquid_drop = element.to_liquid
|
|
337
|
+
|
|
338
|
+
# If no extra context, render with drop directly
|
|
339
|
+
if context.empty?
|
|
340
|
+
return template.render(liquid_drop, {
|
|
341
|
+
registers: { renderer: self }
|
|
342
|
+
}).strip
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# Build hash from drop's known attributes
|
|
346
|
+
# The Drop exposes attributes via method calls
|
|
347
|
+
assigns = build_assigns_from_drop(liquid_drop).merge(context)
|
|
348
|
+
|
|
349
|
+
# Render with registers containing renderer for recursive calls
|
|
350
|
+
template.render(assigns, {
|
|
351
|
+
registers: { renderer: self }
|
|
352
|
+
}).strip
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Build a hash from a Liquid Drop's accessible attributes
|
|
356
|
+
def build_assigns_from_drop(drop)
|
|
357
|
+
# Get all attribute names from the drop's class
|
|
358
|
+
# These are defined by the Lutaml::Model attributes
|
|
359
|
+
assigns = {}
|
|
360
|
+
|
|
361
|
+
# Common attributes that most CoreModel types have
|
|
362
|
+
%w[id title content children element_type block_semantic_type language lines
|
|
363
|
+
delimiter_type delimiter_length metadata_entries element_attributes
|
|
364
|
+
text href alt src level entries items rows cells
|
|
365
|
+
anchor term definition abbreviations].each do |key|
|
|
366
|
+
assigns[key] = drop[key] if drop.key?(key)
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
assigns
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# Fallback rendering when no template found
|
|
373
|
+
#
|
|
374
|
+
# @param element [Object] The element
|
|
375
|
+
# @param context [Hash] Context
|
|
376
|
+
# @return [String] Rendered HTML
|
|
377
|
+
def render_fallback(element, _context)
|
|
378
|
+
raise "No template found for #{element.class.name}" if @options[:strict]
|
|
379
|
+
|
|
380
|
+
# Simple fallback - convert to string
|
|
381
|
+
class_name = element.class.name
|
|
382
|
+
simple_name = class_name.split('::').last
|
|
383
|
+
underscored = simple_name&.gsub(/([A-Z])/, '_\1')&.downcase&.sub(/^_/, '') || 'unknown'
|
|
384
|
+
|
|
385
|
+
"<div class=\"element element-#{underscored}\">#{escape_html(element.to_s)}</div>"
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# Escape HTML entities
|
|
389
|
+
def escape_html(text)
|
|
390
|
+
text.to_s
|
|
391
|
+
.gsub(/&/, '&')
|
|
392
|
+
.gsub(/</, '<')
|
|
393
|
+
.gsub(/>/, '>')
|
|
394
|
+
.gsub(/"/, '"')
|
|
395
|
+
.gsub(/'/, ''')
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def extract_title(element)
|
|
399
|
+
return nil unless element
|
|
400
|
+
return element.title if element.is_a?(Coradoc::CoreModel::StructuralElement) && element.title
|
|
401
|
+
|
|
402
|
+
nil
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# Backwards compatibility alias
|
|
407
|
+
TemplateRenderer = Renderer
|
|
408
|
+
end
|
|
409
|
+
end
|