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,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
|