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.
Files changed (124) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/lib/coradoc/html/base.rb +157 -0
  4. data/lib/coradoc/html/config.rb +467 -0
  5. data/lib/coradoc/html/converter_base.rb +177 -0
  6. data/lib/coradoc/html/converters/admonition.rb +180 -0
  7. data/lib/coradoc/html/converters/attribute.rb +68 -0
  8. data/lib/coradoc/html/converters/attribute_reference.rb +60 -0
  9. data/lib/coradoc/html/converters/audio.rb +165 -0
  10. data/lib/coradoc/html/converters/base.rb +615 -0
  11. data/lib/coradoc/html/converters/bibliography.rb +82 -0
  12. data/lib/coradoc/html/converters/bibliography_entry.rb +108 -0
  13. data/lib/coradoc/html/converters/block_image.rb +72 -0
  14. data/lib/coradoc/html/converters/bold.rb +34 -0
  15. data/lib/coradoc/html/converters/break.rb +32 -0
  16. data/lib/coradoc/html/converters/comment_block.rb +42 -0
  17. data/lib/coradoc/html/converters/comment_line.rb +54 -0
  18. data/lib/coradoc/html/converters/cross_reference.rb +59 -0
  19. data/lib/coradoc/html/converters/document.rb +108 -0
  20. data/lib/coradoc/html/converters/example.rb +114 -0
  21. data/lib/coradoc/html/converters/highlight.rb +34 -0
  22. data/lib/coradoc/html/converters/include.rb +68 -0
  23. data/lib/coradoc/html/converters/inline_image.rb +41 -0
  24. data/lib/coradoc/html/converters/italic.rb +34 -0
  25. data/lib/coradoc/html/converters/line_break.rb +31 -0
  26. data/lib/coradoc/html/converters/link.rb +46 -0
  27. data/lib/coradoc/html/converters/list_item.rb +75 -0
  28. data/lib/coradoc/html/converters/listing.rb +99 -0
  29. data/lib/coradoc/html/converters/literal.rb +102 -0
  30. data/lib/coradoc/html/converters/monospace.rb +34 -0
  31. data/lib/coradoc/html/converters/open.rb +78 -0
  32. data/lib/coradoc/html/converters/ordered.rb +53 -0
  33. data/lib/coradoc/html/converters/paragraph.rb +46 -0
  34. data/lib/coradoc/html/converters/quote.rb +113 -0
  35. data/lib/coradoc/html/converters/reviewer_comment.rb +74 -0
  36. data/lib/coradoc/html/converters/reviewer_note.rb +134 -0
  37. data/lib/coradoc/html/converters/section.rb +90 -0
  38. data/lib/coradoc/html/converters/sidebar.rb +113 -0
  39. data/lib/coradoc/html/converters/source.rb +137 -0
  40. data/lib/coradoc/html/converters/source_code.rb +16 -0
  41. data/lib/coradoc/html/converters/span.rb +61 -0
  42. data/lib/coradoc/html/converters/strikethrough.rb +34 -0
  43. data/lib/coradoc/html/converters/subscript.rb +34 -0
  44. data/lib/coradoc/html/converters/superscript.rb +34 -0
  45. data/lib/coradoc/html/converters/table.rb +85 -0
  46. data/lib/coradoc/html/converters/table_cell.rb +203 -0
  47. data/lib/coradoc/html/converters/table_row.rb +45 -0
  48. data/lib/coradoc/html/converters/template_html_converter.rb +105 -0
  49. data/lib/coradoc/html/converters/term.rb +58 -0
  50. data/lib/coradoc/html/converters/text_element.rb +44 -0
  51. data/lib/coradoc/html/converters/underline.rb +34 -0
  52. data/lib/coradoc/html/converters/unordered.rb +47 -0
  53. data/lib/coradoc/html/converters/verse.rb +105 -0
  54. data/lib/coradoc/html/converters/video.rb +179 -0
  55. data/lib/coradoc/html/element_mapping.rb +210 -0
  56. data/lib/coradoc/html/entity.rb +137 -0
  57. data/lib/coradoc/html/input/cleaner.rb +163 -0
  58. data/lib/coradoc/html/input/config.rb +79 -0
  59. data/lib/coradoc/html/input/converters/a.rb +90 -0
  60. data/lib/coradoc/html/input/converters/aside.rb +23 -0
  61. data/lib/coradoc/html/input/converters/audio.rb +50 -0
  62. data/lib/coradoc/html/input/converters/base.rb +116 -0
  63. data/lib/coradoc/html/input/converters/blockquote.rb +25 -0
  64. data/lib/coradoc/html/input/converters/br.rb +19 -0
  65. data/lib/coradoc/html/input/converters/bypass.rb +83 -0
  66. data/lib/coradoc/html/input/converters/code.rb +25 -0
  67. data/lib/coradoc/html/input/converters/div.rb +25 -0
  68. data/lib/coradoc/html/input/converters/dl.rb +106 -0
  69. data/lib/coradoc/html/input/converters/drop.rb +28 -0
  70. data/lib/coradoc/html/input/converters/em.rb +23 -0
  71. data/lib/coradoc/html/input/converters/figure.rb +58 -0
  72. data/lib/coradoc/html/input/converters/h.rb +76 -0
  73. data/lib/coradoc/html/input/converters/head.rb +30 -0
  74. data/lib/coradoc/html/input/converters/hr.rb +20 -0
  75. data/lib/coradoc/html/input/converters/ignore.rb +22 -0
  76. data/lib/coradoc/html/input/converters/img.rb +110 -0
  77. data/lib/coradoc/html/input/converters/li.rb +35 -0
  78. data/lib/coradoc/html/input/converters/mark.rb +21 -0
  79. data/lib/coradoc/html/input/converters/markup.rb +107 -0
  80. data/lib/coradoc/html/input/converters/math.rb +46 -0
  81. data/lib/coradoc/html/input/converters/ol.rb +46 -0
  82. data/lib/coradoc/html/input/converters/p.rb +81 -0
  83. data/lib/coradoc/html/input/converters/pass_through.rb +19 -0
  84. data/lib/coradoc/html/input/converters/pre.rb +59 -0
  85. data/lib/coradoc/html/input/converters/q.rb +24 -0
  86. data/lib/coradoc/html/input/converters/strong.rb +22 -0
  87. data/lib/coradoc/html/input/converters/sub.rb +40 -0
  88. data/lib/coradoc/html/input/converters/sup.rb +40 -0
  89. data/lib/coradoc/html/input/converters/table.rb +64 -0
  90. data/lib/coradoc/html/input/converters/td.rb +70 -0
  91. data/lib/coradoc/html/input/converters/text.rb +67 -0
  92. data/lib/coradoc/html/input/converters/th.rb +20 -0
  93. data/lib/coradoc/html/input/converters/tr.rb +28 -0
  94. data/lib/coradoc/html/input/converters/video.rb +53 -0
  95. data/lib/coradoc/html/input/converters.rb +122 -0
  96. data/lib/coradoc/html/input/errors.rb +22 -0
  97. data/lib/coradoc/html/input/html_converter.rb +170 -0
  98. data/lib/coradoc/html/input/plugin.rb +169 -0
  99. data/lib/coradoc/html/input/plugins/plateau.rb +229 -0
  100. data/lib/coradoc/html/input/postprocessor.rb +31 -0
  101. data/lib/coradoc/html/input.rb +68 -0
  102. data/lib/coradoc/html/output.rb +95 -0
  103. data/lib/coradoc/html/renderer.rb +409 -0
  104. data/lib/coradoc/html/spa.rb +309 -0
  105. data/lib/coradoc/html/static.rb +293 -0
  106. data/lib/coradoc/html/template_config.rb +151 -0
  107. data/lib/coradoc/html/template_helpers.rb +58 -0
  108. data/lib/coradoc/html/template_locator.rb +114 -0
  109. data/lib/coradoc/html/theme/base.rb +231 -0
  110. data/lib/coradoc/html/theme/classic_renderer.rb +390 -0
  111. data/lib/coradoc/html/theme/modern/components/ui_components.rb +344 -0
  112. data/lib/coradoc/html/theme/modern/css_generator.rb +311 -0
  113. data/lib/coradoc/html/theme/modern/javascript_generator.rb +314 -0
  114. data/lib/coradoc/html/theme/modern/serializers/document_serializer.rb +382 -0
  115. data/lib/coradoc/html/theme/modern/tailwind_config_builder.rb +164 -0
  116. data/lib/coradoc/html/theme/modern/vue_template_generator.rb +374 -0
  117. data/lib/coradoc/html/theme/modern_renderer.rb +250 -0
  118. data/lib/coradoc/html/theme/registry.rb +153 -0
  119. data/lib/coradoc/html/theme.rb +13 -0
  120. data/lib/coradoc/html/transform/from_core_model.rb +32 -0
  121. data/lib/coradoc/html/transform/to_core_model.rb +39 -0
  122. data/lib/coradoc/html/version.rb +7 -0
  123. data/lib/coradoc/html.rb +255 -0
  124. 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(/&/, '&amp;')
392
+ .gsub(/</, '&lt;')
393
+ .gsub(/>/, '&gt;')
394
+ .gsub(/"/, '&quot;')
395
+ .gsub(/'/, '&#39;')
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