coradoc-html 1.1.7 → 1.1.13

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 (152) hide show
  1. checksums.yaml +4 -4
  2. data/lib/coradoc/html/config.rb +36 -12
  3. data/lib/coradoc/html/converter_base.rb +26 -68
  4. data/lib/coradoc/html/drop/annotation_drop.rb +31 -0
  5. data/lib/coradoc/html/drop/base.rb +77 -0
  6. data/lib/coradoc/html/drop/bibliography_drop.rb +15 -0
  7. data/lib/coradoc/html/drop/bibliography_entry_drop.rb +24 -0
  8. data/lib/coradoc/html/drop/block_drop.rb +69 -0
  9. data/lib/coradoc/html/drop/definition_item_drop.rb +36 -0
  10. data/lib/coradoc/html/drop/definition_list_drop.rb +15 -0
  11. data/lib/coradoc/html/drop/document_drop.rb +52 -0
  12. data/lib/coradoc/html/drop/drop_factory.rb +73 -0
  13. data/lib/coradoc/html/drop/footnote_drop.rb +24 -0
  14. data/lib/coradoc/html/drop/image_drop.rb +35 -0
  15. data/lib/coradoc/html/drop/inline_element_drop.rb +64 -0
  16. data/lib/coradoc/html/drop/list_block_drop.rb +23 -0
  17. data/lib/coradoc/html/drop/list_item_drop.rb +20 -0
  18. data/lib/coradoc/html/drop/table_cell_drop.rb +35 -0
  19. data/lib/coradoc/html/drop/table_drop.rb +15 -0
  20. data/lib/coradoc/html/drop/table_row_drop.rb +23 -0
  21. data/lib/coradoc/html/drop/term_drop.rb +24 -0
  22. data/lib/coradoc/html/drop/text_content_drop.rb +15 -0
  23. data/lib/coradoc/html/drop/toc_drop.rb +15 -0
  24. data/lib/coradoc/html/drop/toc_entry_drop.rb +32 -0
  25. data/lib/coradoc/html/escape.rb +29 -0
  26. data/lib/coradoc/html/input/cleaner.rb +4 -33
  27. data/lib/coradoc/html/input/config.rb +4 -3
  28. data/lib/coradoc/html/input/converters/a.rb +8 -19
  29. data/lib/coradoc/html/input/converters/aside.rb +4 -5
  30. data/lib/coradoc/html/input/converters/audio.rb +8 -35
  31. data/lib/coradoc/html/input/converters/base.rb +29 -27
  32. data/lib/coradoc/html/input/converters/blockquote.rb +4 -2
  33. data/lib/coradoc/html/input/converters/br.rb +4 -4
  34. data/lib/coradoc/html/input/converters/bypass.rb +68 -67
  35. data/lib/coradoc/html/input/converters/code.rb +7 -5
  36. data/lib/coradoc/html/input/converters/div.rb +4 -4
  37. data/lib/coradoc/html/input/converters/dl.rb +3 -25
  38. data/lib/coradoc/html/input/converters/drop.rb +13 -13
  39. data/lib/coradoc/html/input/converters/em.rb +5 -3
  40. data/lib/coradoc/html/input/converters/figure.rb +3 -26
  41. data/lib/coradoc/html/input/converters/h.rb +9 -11
  42. data/lib/coradoc/html/input/converters/head.rb +5 -4
  43. data/lib/coradoc/html/input/converters/hr.rb +4 -5
  44. data/lib/coradoc/html/input/converters/img.rb +4 -9
  45. data/lib/coradoc/html/input/converters/li.rb +3 -1
  46. data/lib/coradoc/html/input/converters/mark.rb +3 -1
  47. data/lib/coradoc/html/input/converters/markup.rb +4 -8
  48. data/lib/coradoc/html/input/converters/math.rb +7 -14
  49. data/lib/coradoc/html/input/converters/media_base.rb +50 -0
  50. data/lib/coradoc/html/input/converters/ol.rb +6 -8
  51. data/lib/coradoc/html/input/converters/p.rb +43 -34
  52. data/lib/coradoc/html/input/converters/pass_through.rb +2 -4
  53. data/lib/coradoc/html/input/converters/positional_formatting.rb +37 -0
  54. data/lib/coradoc/html/input/converters/pre.rb +3 -3
  55. data/lib/coradoc/html/input/converters/q.rb +6 -3
  56. data/lib/coradoc/html/input/converters/strong.rb +4 -2
  57. data/lib/coradoc/html/input/converters/sub.rb +7 -23
  58. data/lib/coradoc/html/input/converters/sup.rb +7 -23
  59. data/lib/coradoc/html/input/converters/table.rb +3 -1
  60. data/lib/coradoc/html/input/converters/td.rb +4 -30
  61. data/lib/coradoc/html/input/converters/text.rb +4 -3
  62. data/lib/coradoc/html/input/converters/tr.rb +3 -2
  63. data/lib/coradoc/html/input/converters/video.rb +14 -36
  64. data/lib/coradoc/html/input/converters.rb +17 -35
  65. data/lib/coradoc/html/input/html_converter.rb +2 -74
  66. data/lib/coradoc/html/input/plugin.rb +8 -50
  67. data/lib/coradoc/html/input/plugins/plateau.rb +4 -19
  68. data/lib/coradoc/html/input/postprocessor.rb +3 -9
  69. data/lib/coradoc/html/input.rb +26 -8
  70. data/lib/coradoc/html/layout_renderer.rb +163 -0
  71. data/lib/coradoc/html/output.rb +6 -12
  72. data/lib/coradoc/html/renderer.rb +84 -350
  73. data/lib/coradoc/html/section_numberable.rb +9 -0
  74. data/lib/coradoc/html/spa.rb +29 -270
  75. data/lib/coradoc/html/static.rb +29 -238
  76. data/lib/coradoc/html/template_caching.rb +31 -0
  77. data/lib/coradoc/html/template_config.rb +11 -70
  78. data/lib/coradoc/html/template_helpers.rb +39 -31
  79. data/lib/coradoc/html/template_locator.rb +17 -11
  80. data/lib/coradoc/html/theme.rb +1 -7
  81. data/lib/coradoc/html/title_text.rb +57 -0
  82. data/lib/coradoc/html/toc_builder.rb +105 -0
  83. data/lib/coradoc/html/toc_serializer.rb +33 -0
  84. data/lib/coradoc/html/transform/from_core_model.rb +13 -12
  85. data/lib/coradoc/html/transform/to_core_model.rb +10 -12
  86. data/lib/coradoc/html/version.rb +1 -1
  87. data/lib/coradoc/html.rb +43 -88
  88. metadata +37 -70
  89. data/lib/coradoc/html/base.rb +0 -157
  90. data/lib/coradoc/html/converters/admonition.rb +0 -180
  91. data/lib/coradoc/html/converters/attribute.rb +0 -68
  92. data/lib/coradoc/html/converters/attribute_reference.rb +0 -60
  93. data/lib/coradoc/html/converters/audio.rb +0 -165
  94. data/lib/coradoc/html/converters/base.rb +0 -615
  95. data/lib/coradoc/html/converters/bibliography.rb +0 -82
  96. data/lib/coradoc/html/converters/bibliography_entry.rb +0 -108
  97. data/lib/coradoc/html/converters/block_image.rb +0 -72
  98. data/lib/coradoc/html/converters/bold.rb +0 -34
  99. data/lib/coradoc/html/converters/break.rb +0 -32
  100. data/lib/coradoc/html/converters/comment_block.rb +0 -42
  101. data/lib/coradoc/html/converters/comment_line.rb +0 -54
  102. data/lib/coradoc/html/converters/cross_reference.rb +0 -59
  103. data/lib/coradoc/html/converters/document.rb +0 -108
  104. data/lib/coradoc/html/converters/example.rb +0 -114
  105. data/lib/coradoc/html/converters/highlight.rb +0 -34
  106. data/lib/coradoc/html/converters/include.rb +0 -68
  107. data/lib/coradoc/html/converters/inline_image.rb +0 -41
  108. data/lib/coradoc/html/converters/italic.rb +0 -34
  109. data/lib/coradoc/html/converters/line_break.rb +0 -31
  110. data/lib/coradoc/html/converters/link.rb +0 -46
  111. data/lib/coradoc/html/converters/list_item.rb +0 -75
  112. data/lib/coradoc/html/converters/listing.rb +0 -99
  113. data/lib/coradoc/html/converters/literal.rb +0 -102
  114. data/lib/coradoc/html/converters/monospace.rb +0 -34
  115. data/lib/coradoc/html/converters/open.rb +0 -78
  116. data/lib/coradoc/html/converters/ordered.rb +0 -53
  117. data/lib/coradoc/html/converters/paragraph.rb +0 -46
  118. data/lib/coradoc/html/converters/quote.rb +0 -113
  119. data/lib/coradoc/html/converters/reviewer_comment.rb +0 -74
  120. data/lib/coradoc/html/converters/reviewer_note.rb +0 -134
  121. data/lib/coradoc/html/converters/section.rb +0 -90
  122. data/lib/coradoc/html/converters/sidebar.rb +0 -113
  123. data/lib/coradoc/html/converters/source.rb +0 -137
  124. data/lib/coradoc/html/converters/source_code.rb +0 -16
  125. data/lib/coradoc/html/converters/span.rb +0 -61
  126. data/lib/coradoc/html/converters/strikethrough.rb +0 -34
  127. data/lib/coradoc/html/converters/subscript.rb +0 -34
  128. data/lib/coradoc/html/converters/superscript.rb +0 -34
  129. data/lib/coradoc/html/converters/table.rb +0 -85
  130. data/lib/coradoc/html/converters/table_cell.rb +0 -203
  131. data/lib/coradoc/html/converters/table_row.rb +0 -45
  132. data/lib/coradoc/html/converters/template_html_converter.rb +0 -105
  133. data/lib/coradoc/html/converters/term.rb +0 -58
  134. data/lib/coradoc/html/converters/text_element.rb +0 -44
  135. data/lib/coradoc/html/converters/underline.rb +0 -34
  136. data/lib/coradoc/html/converters/unordered.rb +0 -47
  137. data/lib/coradoc/html/converters/verse.rb +0 -105
  138. data/lib/coradoc/html/converters/video.rb +0 -179
  139. data/lib/coradoc/html/element_mapping.rb +0 -210
  140. data/lib/coradoc/html/entity.rb +0 -137
  141. data/lib/coradoc/html/input/converters/ignore.rb +0 -22
  142. data/lib/coradoc/html/input/converters/th.rb +0 -20
  143. data/lib/coradoc/html/theme/base.rb +0 -231
  144. data/lib/coradoc/html/theme/classic_renderer.rb +0 -390
  145. data/lib/coradoc/html/theme/modern/components/ui_components.rb +0 -344
  146. data/lib/coradoc/html/theme/modern/css_generator.rb +0 -311
  147. data/lib/coradoc/html/theme/modern/javascript_generator.rb +0 -314
  148. data/lib/coradoc/html/theme/modern/serializers/document_serializer.rb +0 -382
  149. data/lib/coradoc/html/theme/modern/tailwind_config_builder.rb +0 -164
  150. data/lib/coradoc/html/theme/modern/vue_template_generator.rb +0 -374
  151. data/lib/coradoc/html/theme/modern_renderer.rb +0 -250
  152. data/lib/coradoc/html/theme/registry.rb +0 -153
@@ -1,409 +1,143 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'liquid'
4
- require_relative 'template_locator'
5
- require_relative 'template_helpers'
4
+ require 'nokogiri'
6
5
 
7
6
  module Coradoc
8
7
  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
8
  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
9
+ DEFAULT_TEMPLATE_DIR = TemplateLocator::DEFAULT_TEMPLATE_DIR
73
10
 
74
- # Default template directory (built-in templates)
75
- DEFAULT_TEMPLATE_DIR = Pathname.new(File.join(File.dirname(__FILE__), 'templates', 'core_model'))
11
+ include TemplateCaching
76
12
 
77
- attr_reader :template_dirs, :include_default_templates, :options
13
+ attr_reader :template_dirs, :options
78
14
 
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)
15
+ def initialize(template_dirs: nil, **options)
16
+ @template_dirs = normalize_dirs(template_dirs)
17
+ @options = options
93
18
  @template_cache = {}
94
- ensure_core_model_drops
19
+ @locator = TemplateLocator.new(
20
+ user_dirs: @template_dirs,
21
+ default_dir: DEFAULT_TEMPLATE_DIR
22
+ )
23
+ @section_numbers = {}
24
+ @layout_renderer = LayoutRenderer.new
25
+ end
26
+
27
+ def render(element)
28
+ result = Drop::DropFactory.create(element)
29
+ case result
30
+ when Drop::Base then render_drop(result)
31
+ when Array then result.map { |r| r.is_a?(Drop::Base) ? render_drop(r) : r }.join("\n")
32
+ when nil then ''
33
+ else result
34
+ end
95
35
  end
96
36
 
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)
37
+ def render_drop(drop)
38
+ return '' if drop.nil?
39
+ return drop.to_s unless drop.is_a?(Drop::Base)
107
40
 
108
- # Handle arrays
109
- return element.map { |e| render(e, context) }.join("\n") if element.is_a?(Array)
41
+ annotate_section_number(drop)
110
42
 
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
43
+ template_type = drop.template_type
44
+ template = find_and_load_template(template_type)
45
+ return render_fallback_drop(drop) unless template
120
46
 
121
- # Get template type name for this element
122
- type_name = template_type_for(element)
123
- return render_fallback(element, context) unless type_name
47
+ assigns = { 'element' => drop }
48
+ template.render(assigns, registers: { renderer: self, section_numbers: @section_numbers }).strip
49
+ end
124
50
 
125
- # Find the template file
126
- template_path = find_template(type_name)
127
- return render_fallback(element, context) if template_path.nil?
51
+ def render_html5(document, **options)
52
+ @section_numbers = compute_section_numbers(document, options)
53
+ body_html = render(document)
128
54
 
129
- # Load and render the template
130
- template = load_template(template_path)
131
- if template
132
- render_with_template(template, element, context)
55
+ if options[:layout] == :spa
56
+ render_spa_layout(document, body_html, options)
133
57
  else
134
- render_fallback(element, context)
58
+ render_static_layout(document, body_html, options)
135
59
  end
136
60
  end
137
61
 
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
62
  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
63
+ @locator.available_templates
193
64
  end
194
65
 
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
66
  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
67
+ @locator.exists?(type_name)
210
68
  end
211
69
 
212
- # Get custom type mappings
213
- def self.custom_type_map
214
- @custom_type_map ||= {}
70
+ def find_template(type_name)
71
+ find_and_load_template(type_name)
215
72
  end
216
73
 
217
74
  private
218
75
 
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
76
+ def build_toc(_document, options)
77
+ TocBuilder.from_options(options)
227
78
  end
228
79
 
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
80
+ def compute_section_numbers(document, options)
81
+ if options[:section_numbers] && document.is_a?(CoreModel::StructuralElement)
82
+ build_toc(document, options).section_number_map(document)
83
+ else
84
+ {}
241
85
  end
242
86
  end
243
87
 
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
88
+ def render_toc_for(document, options)
89
+ toc = build_toc(document, options).build(document)
90
+ render(toc)
252
91
  end
253
92
 
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)
93
+ def render_static_layout(document, body_html, options)
94
+ if options[:toc] && document.is_a?(CoreModel::StructuralElement)
95
+ toc_html = render_toc_for(document, options)
96
+ body_html = "#{toc_html}\n#{body_html}" unless toc_html.empty?
272
97
  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
98
+ @layout_renderer.render_static(document, body_html, options)
281
99
  end
282
100
 
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)
101
+ def render_spa_layout(document, body_html, options)
102
+ toc_data = TocSerializer.new.build_json(document, options)
103
+ @layout_renderer.render_spa(document, options, body_html, toc_data)
294
104
  end
295
105
 
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(/^_/, '')
106
+ def find_and_load_template(type_name)
107
+ cache_key = type_name.to_s
108
+ path = @locator.find(type_name)
109
+ load_template(cache: @template_cache, cache_key: cache_key, path: path)
309
110
  end
310
111
 
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)
112
+ def annotate_section_number(drop)
113
+ return unless drop.is_a?(SectionNumberable)
114
+ return if @section_numbers.empty?
318
115
 
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
116
+ id = drop.id
117
+ number = @section_numbers[id]
118
+ drop.section_number = number if number
326
119
  end
327
120
 
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
121
+ def render_fallback_drop(drop)
122
+ type = drop.template_type
123
+ resolved = TitleText.resolve(drop.model)
124
+ text = resolved ? Escape.escape_html(resolved) : ''
337
125
 
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
126
+ doc = Nokogiri::HTML::Document.new
127
+ fragment = Nokogiri::HTML::Builder.with(doc) do |builder|
128
+ builder.div(class: "element element-#{type}") { builder.text text }
343
129
  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
130
+ fragment.at_css('div').to_html
353
131
  end
354
132
 
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 = {}
133
+ def normalize_dirs(dirs)
134
+ return [] if dirs.nil?
360
135
 
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)
136
+ Array(dirs).map do |dir|
137
+ path = Pathname.new(dir)
138
+ path.absolute? ? path.to_s : File.expand_path(dir)
367
139
  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
140
  end
404
141
  end
405
-
406
- # Backwards compatibility alias
407
- TemplateRenderer = Renderer
408
142
  end
409
143
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Html
5
+ module SectionNumberable
6
+ attr_accessor :section_number
7
+ end
8
+ end
9
+ end