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,309 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Html
5
+ autoload :ConverterBase, "#{__dir__}/converter_base"
6
+
7
+ # SPA (Single Page Application) HTML converter
8
+ #
9
+ # Converts Coradoc::CoreModel::StructuralElement to a modern Vue.js + Tailwind CSS
10
+ # single-page application with glass morphism aesthetics.
11
+ #
12
+ # Features:
13
+ # - Vue.js 3 reactive components
14
+ # - Tailwind CSS styling
15
+ # - Glass morphism design
16
+ # - Dark/light theme toggle
17
+ # - Reading progress indicator
18
+ # - Sticky TOC sidebar
19
+ # - Copy code buttons
20
+ # - Smooth animations
21
+ #
22
+ # @example Basic usage
23
+ # doc = Coradoc.parse_file('document.adoc')
24
+ # html = Coradoc::Html::Spa.convert(doc)
25
+ #
26
+ # @example With configuration
27
+ # config = Coradoc::Html::Spa::Configuration.new(
28
+ # theme_variant: :glass,
29
+ # primary_color: '#6366f1',
30
+ # theme_toggle: true,
31
+ # reading_progress: true
32
+ # )
33
+ # html = Coradoc::Html::Spa.convert(doc, config)
34
+ #
35
+ # @example Write to file
36
+ # Coradoc::Html::Spa.to_file(doc, 'output.html', config)
37
+ class Spa < ConverterBase
38
+ # Configuration for SPA HTML output
39
+ #
40
+ # Plain Ruby configuration class with accessors and defaults.
41
+ class Configuration
42
+ # How to deliver assets (:embedded always for SPA)
43
+ attr_accessor :asset_delivery
44
+
45
+ # Theme appearance variant (:glass, :minimal, :vibrant)
46
+ attr_accessor :theme_variant
47
+
48
+ # Primary color (hex string, e.g., '#6366f1')
49
+ attr_accessor :primary_color
50
+
51
+ # Accent color (hex string, e.g., '#8b5cf6')
52
+ attr_accessor :accent_color
53
+
54
+ # Whether to enable theme toggle (dark/light mode)
55
+ attr_accessor :theme_toggle
56
+
57
+ # Whether to show reading progress bar
58
+ attr_accessor :reading_progress
59
+
60
+ # Whether to show back to top button
61
+ attr_accessor :back_to_top
62
+
63
+ # Whether TOC should be sticky
64
+ attr_accessor :toc_sticky
65
+
66
+ # Whether to add copy buttons to code blocks
67
+ attr_accessor :copy_code_buttons
68
+
69
+ # TOC levels to include (1-5)
70
+ attr_accessor :toc_levels
71
+
72
+ # TOC title text
73
+ attr_accessor :toc_title
74
+
75
+ # Whether to enable animations
76
+ attr_accessor :enable_animations
77
+
78
+ # Animation duration (CSS value, e.g., '300ms')
79
+ attr_accessor :animation_duration
80
+
81
+ # Whether to lazy load images
82
+ attr_accessor :lazy_load_images
83
+
84
+ # Maximum width of the container (CSS value)
85
+ attr_accessor :max_width
86
+
87
+ # Content width (CSS value)
88
+ attr_accessor :content_width
89
+
90
+ # Sidebar width for TOC (CSS value)
91
+ attr_accessor :sidebar_width
92
+
93
+ # Language attribute for HTML
94
+ attr_accessor :lang
95
+
96
+ # Custom meta description
97
+ attr_accessor :meta_description
98
+
99
+ # Custom meta keywords
100
+ attr_accessor :meta_keywords
101
+
102
+ # Enable Open Graph meta tags
103
+ attr_accessor :open_graph
104
+
105
+ # Valid theme variants
106
+ VALID_THEME_VARIANTS = %i[glass minimal vibrant].freeze
107
+
108
+ # Initialize configuration with options
109
+ #
110
+ # @param options [Hash] Configuration options
111
+ def initialize(**options)
112
+ @asset_delivery = options[:asset_delivery] || :embedded
113
+ @theme_variant = options[:theme_variant] || :glass
114
+ @primary_color = options[:primary_color] || '#6366f1'
115
+ @accent_color = options[:accent_color] || '#8b5cf6'
116
+ @theme_toggle = options.fetch(:theme_toggle, true)
117
+ @reading_progress = options.fetch(:reading_progress, true)
118
+ @back_to_top = options.fetch(:back_to_top, true)
119
+ @toc_sticky = options.fetch(:toc_sticky, true)
120
+ @copy_code_buttons = options.fetch(:copy_code_buttons, true)
121
+ @toc_levels = options[:toc_levels] || 2
122
+ @toc_title = options[:toc_title] || 'Table of Contents'
123
+ @enable_animations = options.fetch(:enable_animations, true)
124
+ @animation_duration = options[:animation_duration] || '300ms'
125
+ @lazy_load_images = options.fetch(:lazy_load_images, true)
126
+ @max_width = options[:max_width] || '1200px'
127
+ @content_width = options[:content_width] || '65ch'
128
+ @sidebar_width = options[:sidebar_width] || '280px'
129
+ @lang = options[:lang] || 'en'
130
+ @meta_description = options[:meta_description]
131
+ @meta_keywords = options[:meta_keywords]
132
+ @open_graph = options.fetch(:open_graph, false)
133
+ end
134
+
135
+ # Default configuration
136
+ #
137
+ # @return [Configuration] Default configuration instance
138
+ def self.defaults
139
+ new
140
+ end
141
+
142
+ # Merge with another configuration or hash
143
+ #
144
+ # @param other [Hash, Configuration] Configuration to merge
145
+ # @return [Configuration] New merged configuration
146
+ def merge(other)
147
+ other_hash = other.is_a?(Configuration) ? other.to_h : other.to_h.transform_keys(&:to_sym)
148
+ self.class.new(**to_h.merge(other_hash))
149
+ end
150
+
151
+ # Convert to hash
152
+ #
153
+ # @return [Hash] Configuration as hash
154
+ def to_h
155
+ {
156
+ asset_delivery: @asset_delivery,
157
+ theme_variant: @theme_variant,
158
+ primary_color: @primary_color,
159
+ accent_color: @accent_color,
160
+ theme_toggle: @theme_toggle,
161
+ reading_progress: @reading_progress,
162
+ back_to_top: @back_to_top,
163
+ toc_sticky: @toc_sticky,
164
+ copy_code_buttons: @copy_code_buttons,
165
+ toc_levels: @toc_levels,
166
+ toc_title: @toc_title,
167
+ enable_animations: @enable_animations,
168
+ animation_duration: @animation_duration,
169
+ lazy_load_images: @lazy_load_images,
170
+ max_width: @max_width,
171
+ content_width: @content_width,
172
+ sidebar_width: @sidebar_width,
173
+ lang: @lang,
174
+ meta_description: @meta_description,
175
+ meta_keywords: @meta_keywords,
176
+ open_graph: @open_graph
177
+ }
178
+ end
179
+
180
+ # Validate configuration
181
+ #
182
+ # @raise [ConverterBase::ValidationError] if configuration is invalid
183
+ def validate!
184
+ validate_hex_color(@primary_color, 'primary_color')
185
+ validate_hex_color(@accent_color, 'accent_color')
186
+ validate_css_value(@max_width, 'max_width')
187
+ validate_css_value(@content_width, 'content_width')
188
+ validate_css_value(@sidebar_width, 'sidebar_width')
189
+ validate_css_value(@animation_duration, 'animation_duration')
190
+
191
+ unless VALID_THEME_VARIANTS.include?(@theme_variant.to_sym)
192
+ raise ConverterBase::ValidationError,
193
+ "Invalid theme variant: #{@theme_variant}. " \
194
+ "Valid variants: #{VALID_THEME_VARIANTS.join(', ')}"
195
+ end
196
+
197
+ return if @toc_levels.is_a?(Integer) && @toc_levels.between?(1, 5)
198
+
199
+ raise ConverterBase::ValidationError,
200
+ 'TOC levels must be an integer between 1 and 5'
201
+ end
202
+
203
+ # Convert to options hash for ModernRenderer
204
+ #
205
+ # @return [Hash] Options hash for the modern renderer
206
+ def to_renderer_options
207
+ {
208
+ modern: to_h,
209
+ lang: @lang,
210
+ toc: @toc_sticky,
211
+ toclevels: @toc_levels,
212
+ toc_title: @toc_title
213
+ }
214
+ end
215
+
216
+ private
217
+
218
+ # Validate hex color format
219
+ #
220
+ # @param color [String] Color string to validate
221
+ # @param field_name [String] Field name for error message
222
+ # @raise [ConverterBase::ValidationError] if invalid
223
+ def validate_hex_color(color, field_name)
224
+ return unless color
225
+
226
+ return if color.match?(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/)
227
+
228
+ raise ConverterBase::ValidationError,
229
+ "Invalid hex color for #{field_name}: #{color}. " \
230
+ 'Expected format: #RRGGBB or #RGB'
231
+ end
232
+
233
+ # Validate CSS dimension value
234
+ #
235
+ # @param value [String] CSS value to validate
236
+ # @param field_name [String] Field name for error message
237
+ # @raise [ConverterBase::ValidationError] if invalid
238
+ def validate_css_value(value, field_name)
239
+ return unless value
240
+
241
+ # Allow common CSS units
242
+ return if value.match?(/^\d+(\.\d+)?(px|%|ch|em|rem|vw|vh|ms|s)$/)
243
+
244
+ raise ConverterBase::ValidationError,
245
+ "Invalid CSS value for #{field_name}: #{value}. " \
246
+ 'Expected format: number with unit (px, %, ch, em, rem, vw, vh, ms, s)'
247
+ end
248
+ end
249
+
250
+ # Convert document to SPA HTML
251
+ #
252
+ # @return [String] Complete HTML5 document with Vue.js application
253
+ def convert
254
+ # Build options hash for ModernRenderer
255
+ options = @config.to_renderer_options
256
+
257
+ # Use ModernRenderer to generate HTML
258
+ renderer = Html::Theme::ModernRenderer.new(@document, options)
259
+ renderer.render_html5
260
+ end
261
+
262
+ private
263
+
264
+ # Build configuration from options
265
+ #
266
+ # @param config [Hash, Configuration] Configuration options
267
+ # @return [Configuration] Configuration object
268
+ def build_config(config)
269
+ case config
270
+ when Configuration
271
+ config.validate!
272
+ config
273
+ when Hash
274
+ Configuration.new(**config)
275
+ else
276
+ Configuration.defaults
277
+ end
278
+ end
279
+
280
+ # Output processor interface: unique identifier
281
+ #
282
+ # @return [Symbol] Processor identifier
283
+ def self.processor_id
284
+ :html_spa
285
+ end
286
+
287
+ # Output processor interface: check if this processor handles the file
288
+ #
289
+ # @param filename [String] Output filename
290
+ # @return [Boolean] true if this processor can handle the file
291
+ def self.processor_match?(filename)
292
+ filename.downcase.end_with?('.html', '.htm')
293
+ end
294
+
295
+ # Output processor interface: execute the conversion
296
+ #
297
+ # @param input [Hash] Input from the converter (contains document)
298
+ # @param options [Hash] Output options
299
+ # @return [Hash] Hash with nil => HTML output
300
+ def self.processor_execute(input, options = {})
301
+ # Handle hash input from converter pipeline
302
+ document = input.is_a?(Hash) ? (input[:document] || input.values.first) : input
303
+ html = convert(document, options)
304
+ # Return in format expected by converter (hash with filename => content)
305
+ { nil => html }
306
+ end
307
+ end
308
+ end
309
+ end
@@ -0,0 +1,293 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Html
5
+ autoload :ConverterBase, "#{__dir__}/converter_base"
6
+
7
+ # Static HTML converter
8
+ #
9
+ # Converts CoreModel documents to static HTML5 output.
10
+ # This converter produces traditional HTML with external or embedded CSS/JS.
11
+ #
12
+ # @example Basic usage
13
+ # doc = Coradoc.parse_file('document.adoc')
14
+ # html = Coradoc::Html::Static.convert(doc)
15
+ #
16
+ # @example With configuration
17
+ # config = Coradoc::Html::Static::Configuration.new(
18
+ # css_theme: :professional,
19
+ # include_toc: true,
20
+ # theme_toggle: true
21
+ # )
22
+ # html = Coradoc::Html::Static.convert(doc, config)
23
+ #
24
+ # @example Write to file
25
+ # Coradoc::Html::Static.to_file(doc, 'output.html', config)
26
+ class Static < ConverterBase
27
+ # Configuration for Static HTML output
28
+ #
29
+ # Plain Ruby configuration class with accessors and defaults.
30
+ class Configuration
31
+ # CSS theme to use (:professional, :academic, :tech)
32
+ attr_accessor :css_theme
33
+
34
+ # How to deliver assets (:embedded, :external)
35
+ attr_accessor :asset_delivery
36
+
37
+ # Whether to include table of contents
38
+ attr_accessor :include_toc
39
+
40
+ # TOC levels to include (1-5)
41
+ attr_accessor :toc_levels
42
+
43
+ # TOC title text
44
+ attr_accessor :toc_title
45
+
46
+ # TOC placement (:auto, :left, :right, :preamble)
47
+ attr_accessor :toc_placement
48
+
49
+ # Whether to enable theme toggle (dark/light mode)
50
+ attr_accessor :theme_toggle
51
+
52
+ # Whether to preserve comments in output
53
+ attr_accessor :preserve_comments
54
+
55
+ # Whether to apply section numbering
56
+ attr_accessor :section_numbering
57
+
58
+ # Maximum section level for numbering
59
+ attr_accessor :section_numbering_levels
60
+
61
+ # Language attribute for HTML
62
+ attr_accessor :lang
63
+
64
+ # Custom meta tags
65
+ attr_accessor :meta_tags
66
+
67
+ # Custom CSS to append
68
+ attr_accessor :custom_css
69
+
70
+ # Whether to embed output (no full HTML document)
71
+ attr_accessor :embedded
72
+
73
+ # Valid CSS themes
74
+ VALID_CSS_THEMES = %i[professional academic tech].freeze
75
+
76
+ # Valid asset delivery methods
77
+ VALID_ASSET_DELIVERIES = %i[embedded external].freeze
78
+
79
+ # Valid TOC placements
80
+ VALID_TOC_PLACEMENTS = %i[auto left right preamble].freeze
81
+
82
+ # Initialize configuration with options
83
+ #
84
+ # @param options [Hash] Configuration options
85
+ def initialize(**options)
86
+ @css_theme = options[:css_theme] || :professional
87
+ @asset_delivery = options[:asset_delivery] || :embedded
88
+ @include_toc = options.fetch(:include_toc, false)
89
+ @toc_levels = options[:toc_levels] || 2
90
+ @toc_title = options[:toc_title] || 'Table of Contents'
91
+ @toc_placement = options[:toc_placement] || :auto
92
+ @theme_toggle = options.fetch(:theme_toggle, true)
93
+ @preserve_comments = options.fetch(:preserve_comments, false)
94
+ @section_numbering = options.fetch(:section_numbering, false)
95
+ @section_numbering_levels = options[:section_numbering_levels] || 3
96
+ @lang = options[:lang] || 'en'
97
+ @meta_tags = options[:meta_tags] || {}
98
+ @custom_css = options[:custom_css]
99
+ @embedded = options.fetch(:embedded, false)
100
+ end
101
+
102
+ # Default configuration
103
+ #
104
+ # @return [Configuration] Default configuration instance
105
+ def self.defaults
106
+ new
107
+ end
108
+
109
+ # Merge with another configuration or hash
110
+ #
111
+ # @param other [Hash, Configuration] Configuration to merge
112
+ # @return [Configuration] New merged configuration
113
+ def merge(other)
114
+ other_hash = other.is_a?(Configuration) ? other.to_h : other.to_h.transform_keys(&:to_sym)
115
+ self.class.new(**to_h.merge(other_hash))
116
+ end
117
+
118
+ # Convert to hash
119
+ #
120
+ # @return [Hash] Configuration as hash
121
+ def to_h
122
+ {
123
+ css_theme: @css_theme,
124
+ asset_delivery: @asset_delivery,
125
+ include_toc: @include_toc,
126
+ toc_levels: @toc_levels,
127
+ toc_title: @toc_title,
128
+ toc_placement: @toc_placement,
129
+ theme_toggle: @theme_toggle,
130
+ preserve_comments: @preserve_comments,
131
+ section_numbering: @section_numbering,
132
+ section_numbering_levels: @section_numbering_levels,
133
+ lang: @lang,
134
+ meta_tags: @meta_tags,
135
+ custom_css: @custom_css,
136
+ embedded: @embedded
137
+ }
138
+ end
139
+
140
+ # Validate configuration
141
+ #
142
+ # @raise [ConverterBase::ValidationError] if configuration is invalid
143
+ def validate!
144
+ unless VALID_CSS_THEMES.include?(@css_theme.to_sym)
145
+ raise ConverterBase::ValidationError,
146
+ "Invalid CSS theme: #{@css_theme}. " \
147
+ "Valid themes: #{VALID_CSS_THEMES.join(', ')}"
148
+ end
149
+
150
+ unless VALID_ASSET_DELIVERIES.include?(@asset_delivery.to_sym)
151
+ raise ConverterBase::ValidationError,
152
+ "Invalid asset delivery: #{@asset_delivery}. " \
153
+ "Valid options: #{VALID_ASSET_DELIVERIES.join(', ')}"
154
+ end
155
+
156
+ if @include_toc && !VALID_TOC_PLACEMENTS.include?(@toc_placement.to_sym)
157
+ raise ConverterBase::ValidationError,
158
+ "Invalid TOC placement: #{@toc_placement}. " \
159
+ "Valid options: #{VALID_TOC_PLACEMENTS.join(', ')}"
160
+ end
161
+
162
+ unless @toc_levels.is_a?(Integer) && @toc_levels.between?(1, 5)
163
+ raise ConverterBase::ValidationError,
164
+ 'TOC levels must be an integer between 1 and 5'
165
+ end
166
+
167
+ unless @section_numbering_levels.is_a?(Integer) &&
168
+ @section_numbering_levels.between?(1, 6)
169
+ raise ConverterBase::ValidationError,
170
+ 'Section numbering levels must be an integer between 1 and 6'
171
+ end
172
+ end
173
+
174
+ # Check if assets should be embedded
175
+ #
176
+ # @return [Boolean] true if assets should be embedded
177
+ def embed_assets?
178
+ @asset_delivery == :embedded || @embedded
179
+ end
180
+
181
+ # Check if external assets should be linked
182
+ #
183
+ # @return [Boolean] true if assets should be linked
184
+ def link_assets?
185
+ !embed_assets?
186
+ end
187
+ end
188
+
189
+ # Convert document to static HTML
190
+ #
191
+ # @return [String] Complete HTML5 document or fragment (if embedded mode)
192
+ def convert
193
+ # Build options hash for ClassicRenderer
194
+ options = build_renderer_options
195
+
196
+ # Use ClassicRenderer to generate HTML
197
+ renderer = Html::Theme::ClassicRenderer.new(@document, options)
198
+
199
+ if @config.embedded
200
+ renderer.render
201
+ else
202
+ renderer.render_html5
203
+ end
204
+ end
205
+
206
+ private
207
+
208
+ # Build options hash for ClassicRenderer
209
+ #
210
+ # @return [Hash] Options for the classic renderer
211
+ def build_renderer_options
212
+ # When TOC is enabled with auto placement, default to left sidebar
213
+ effective_toc_placement = if @config.include_toc && @config.toc_placement == :auto
214
+ :left
215
+ else
216
+ @config.toc_placement
217
+ end
218
+
219
+ options = {
220
+ theme: :classic,
221
+ css_theme: @config.css_theme.to_s,
222
+ linkcss: @config.link_assets?,
223
+ copycss: true,
224
+ toc: @config.include_toc,
225
+ toclevels: @config.toc_levels,
226
+ toc_title: @config.toc_title,
227
+ toc_placement: effective_toc_placement,
228
+ theme_toggle: @config.theme_toggle,
229
+ preserve_comments: @config.preserve_comments,
230
+ sectnums: @config.section_numbering,
231
+ sectnumlevels: @config.section_numbering_levels,
232
+ lang: @config.lang,
233
+ meta_tags: @config.meta_tags,
234
+ custom_css: @config.custom_css,
235
+ embedded: @config.embedded
236
+ }
237
+
238
+ # Handle JavaScript based on asset delivery
239
+ options[:linkjs] = if @config.embed_assets?
240
+ false
241
+ else
242
+ true
243
+ end
244
+
245
+ options
246
+ end
247
+
248
+ # Build configuration from options
249
+ #
250
+ # @param config [Hash, Configuration] Configuration options
251
+ # @return [Configuration] Configuration object
252
+ def build_config(config)
253
+ case config
254
+ when Configuration
255
+ config.validate!
256
+ config
257
+ when Hash
258
+ Configuration.new(**config)
259
+ else
260
+ Configuration.defaults
261
+ end
262
+ end
263
+
264
+ # Output processor interface: unique identifier
265
+ #
266
+ # @return [Symbol] Processor identifier
267
+ def self.processor_id
268
+ :html_static
269
+ end
270
+
271
+ # Output processor interface: check if this processor handles the file
272
+ #
273
+ # @param filename [String] Output filename
274
+ # @return [Boolean] true if this processor can handle the file
275
+ def self.processor_match?(filename)
276
+ filename.downcase.end_with?('.html', '.htm')
277
+ end
278
+
279
+ # Output processor interface: execute the conversion
280
+ #
281
+ # @param input [Hash] Input from the converter (contains document)
282
+ # @param options [Hash] Output options
283
+ # @return [Hash] Hash with nil => HTML output
284
+ def self.processor_execute(input, options = {})
285
+ # Handle hash input from converter pipeline
286
+ document = input.is_a?(Hash) ? (input[:document] || input.values.first) : input
287
+ html = convert(document, options)
288
+ # Return in format expected by converter (hash with filename => content)
289
+ { nil => html }
290
+ end
291
+ end
292
+ end
293
+ end