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,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pathname'
|
|
4
|
+
|
|
5
|
+
module Coradoc
|
|
6
|
+
module Html
|
|
7
|
+
# Configuration for the Liquid template system
|
|
8
|
+
#
|
|
9
|
+
# This class manages template directories and provides utilities
|
|
10
|
+
# for template discovery and customization.
|
|
11
|
+
#
|
|
12
|
+
# @example Global configuration
|
|
13
|
+
# Coradoc::Html.configure do |config|
|
|
14
|
+
# config.template_dirs = ["/path/to/custom/templates"]
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# @example Per-render configuration
|
|
18
|
+
# Coradoc::Html.serialize(document, template_dirs: ["/custom/templates"])
|
|
19
|
+
#
|
|
20
|
+
class TemplateConfig
|
|
21
|
+
# Default template directory within the gem
|
|
22
|
+
DEFAULT_TEMPLATE_DIR = Pathname.new(File.join(
|
|
23
|
+
File.dirname(__FILE__), 'templates', 'core_model'
|
|
24
|
+
)).freeze
|
|
25
|
+
|
|
26
|
+
# @return [Array<Pathname>] List of user-provided template directories
|
|
27
|
+
attr_accessor :template_dirs
|
|
28
|
+
|
|
29
|
+
# Initialize a new configuration
|
|
30
|
+
#
|
|
31
|
+
# @param template_dirs [Array<String, Pathname>] Custom template directories
|
|
32
|
+
def initialize(template_dirs: [])
|
|
33
|
+
@template_dirs = Array(template_dirs).map { |dir| Pathname.new(dir) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Get all template directories (user + default)
|
|
37
|
+
#
|
|
38
|
+
# @return [Array<Pathname>] All template directories in search order
|
|
39
|
+
def all_template_dirs
|
|
40
|
+
@template_dirs + [DEFAULT_TEMPLATE_DIR]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# List all available default templates
|
|
44
|
+
#
|
|
45
|
+
# @return [Array<Symbol>] List of template names (without .liquid extension)
|
|
46
|
+
def self.available_templates
|
|
47
|
+
@available_templates ||= begin
|
|
48
|
+
return [] unless DEFAULT_TEMPLATE_DIR.exist?
|
|
49
|
+
|
|
50
|
+
DEFAULT_TEMPLATE_DIR
|
|
51
|
+
.glob('*.liquid')
|
|
52
|
+
.map { |f| f.basename('.liquid').to_s.to_sym }
|
|
53
|
+
.sort
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Get the path to a specific default template
|
|
58
|
+
#
|
|
59
|
+
# @param name [Symbol, String] Template name (e.g., :bibliography)
|
|
60
|
+
# @return [Pathname, nil] Path to the template file, or nil if not found
|
|
61
|
+
def self.template_path_for(name)
|
|
62
|
+
path = DEFAULT_TEMPLATE_DIR.join("#{name}.liquid")
|
|
63
|
+
path.exist? ? path : nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Check if a template exists
|
|
67
|
+
#
|
|
68
|
+
# @param name [Symbol, String] Template name
|
|
69
|
+
# @return [Boolean] True if template exists in any directory
|
|
70
|
+
def template_exists?(name)
|
|
71
|
+
all_template_dirs.any? do |dir|
|
|
72
|
+
dir.join("#{name}.liquid").exist?
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Find a template by name
|
|
77
|
+
#
|
|
78
|
+
# @param name [Symbol, String] Template name
|
|
79
|
+
# @return [Pathname, nil] Path to the first matching template
|
|
80
|
+
def find_template(name)
|
|
81
|
+
all_template_dirs.each do |dir|
|
|
82
|
+
path = dir.join("#{name}.liquid")
|
|
83
|
+
return path if path.exist?
|
|
84
|
+
end
|
|
85
|
+
nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Reset configuration to defaults
|
|
89
|
+
#
|
|
90
|
+
# @return [void]
|
|
91
|
+
def reset!
|
|
92
|
+
@template_dirs = []
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Create a copy of this configuration with additional directories
|
|
96
|
+
#
|
|
97
|
+
# @param additional_dirs [Array<String, Pathname>] Extra directories
|
|
98
|
+
# @return [TemplateConfig] New configuration with merged directories
|
|
99
|
+
def with_dirs(additional_dirs)
|
|
100
|
+
self.class.new(
|
|
101
|
+
template_dirs: @template_dirs + Array(additional_dirs).map { |d| Pathname.new(d) }
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Module-level configuration storage
|
|
107
|
+
class << self
|
|
108
|
+
# Get the global configuration
|
|
109
|
+
#
|
|
110
|
+
# @return [TemplateConfig] The global configuration
|
|
111
|
+
def configuration
|
|
112
|
+
@configuration ||= TemplateConfig.new
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Configure the template system
|
|
116
|
+
#
|
|
117
|
+
# @yield [TemplateConfig] Yields the configuration object
|
|
118
|
+
# @return [void]
|
|
119
|
+
#
|
|
120
|
+
# @example
|
|
121
|
+
# Coradoc::Html.configure do |config|
|
|
122
|
+
# config.template_dirs = ["/path/to/templates"]
|
|
123
|
+
# end
|
|
124
|
+
def configure
|
|
125
|
+
yield(configuration) if block_given?
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Reset configuration to defaults
|
|
129
|
+
#
|
|
130
|
+
# @return [void]
|
|
131
|
+
def reset_configuration!
|
|
132
|
+
@configuration = nil
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# List all available default templates
|
|
136
|
+
#
|
|
137
|
+
# @return [Array<Symbol>] List of template names
|
|
138
|
+
def available_templates
|
|
139
|
+
TemplateConfig.available_templates
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Get the path to a default template
|
|
143
|
+
#
|
|
144
|
+
# @param name [Symbol, String] Template name
|
|
145
|
+
# @return [Pathname, nil] Path to template or nil
|
|
146
|
+
def template_path_for(name)
|
|
147
|
+
TemplateConfig.template_path_for(name)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'liquid'
|
|
4
|
+
|
|
5
|
+
module Coradoc
|
|
6
|
+
module Html
|
|
7
|
+
# Liquid filters for template rendering
|
|
8
|
+
module TemplateFilters
|
|
9
|
+
# Render a CoreModel element by looking up its template
|
|
10
|
+
#
|
|
11
|
+
# Usage in templates:
|
|
12
|
+
# {{ child | render_element }}
|
|
13
|
+
# {% for item in children %}{{ item | render_element }}{% endfor %}
|
|
14
|
+
#
|
|
15
|
+
def render_element(input, renderer = nil)
|
|
16
|
+
return '' if input.nil?
|
|
17
|
+
return input.map { |i| render_element(i, renderer) }.join("\n") if input.is_a?(Array)
|
|
18
|
+
|
|
19
|
+
# Get the renderer from context registers
|
|
20
|
+
renderer ||= @context.registers[:renderer]
|
|
21
|
+
|
|
22
|
+
# If input is a Liquid::Drop, extract the original model object.
|
|
23
|
+
# Liquid::Drop stores the wrapped object as @object with no public
|
|
24
|
+
# reader — this is the only way to unwrap it.
|
|
25
|
+
original = if input.is_a?(::Liquid::Drop)
|
|
26
|
+
input.instance_variable_get(:@object)
|
|
27
|
+
else
|
|
28
|
+
input
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Renderer is always a Coradoc::Html::Renderer which has a render method
|
|
32
|
+
renderer.render(original)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Escape HTML entities
|
|
36
|
+
def escape_html(input)
|
|
37
|
+
input.to_s
|
|
38
|
+
.gsub(/&/, '&')
|
|
39
|
+
.gsub(/</, '<')
|
|
40
|
+
.gsub(/>/, '>')
|
|
41
|
+
.gsub(/"/, '"')
|
|
42
|
+
.gsub(/'/, ''')
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Escape HTML attribute values
|
|
46
|
+
def escape_attr(input)
|
|
47
|
+
input.to_s
|
|
48
|
+
.gsub(/&/, '&')
|
|
49
|
+
.gsub(/"/, '"')
|
|
50
|
+
.gsub(/</, '<')
|
|
51
|
+
.gsub(/>/, '>')
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Register filters with Liquid (using the non-deprecated API)
|
|
58
|
+
Liquid::Environment.default.register_filter(Coradoc::Html::TemplateFilters)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'liquid'
|
|
4
|
+
require 'pathname'
|
|
5
|
+
|
|
6
|
+
module Coradoc
|
|
7
|
+
module Html
|
|
8
|
+
# Locates Liquid templates from filesystem with fallback support
|
|
9
|
+
#
|
|
10
|
+
# This class implements a template lookup algorithm:
|
|
11
|
+
# 1. Check user's template directories in order (first match wins)
|
|
12
|
+
# 2. Fall back to default templates if not found
|
|
13
|
+
# 3. Return nil if no template found (caller decides fallback)
|
|
14
|
+
#
|
|
15
|
+
# @example Basic usage
|
|
16
|
+
# locator = TemplateLocator.new(
|
|
17
|
+
# user_dirs: ["/path/to/templates"],
|
|
18
|
+
# default_dir: "/default/templates"
|
|
19
|
+
# )
|
|
20
|
+
# locator.find("bibliography") # => "/path/to/templates/bibliography.liquid"
|
|
21
|
+
#
|
|
22
|
+
class TemplateLocator
|
|
23
|
+
# Default template subdirectory within each template root
|
|
24
|
+
CORE_MODEL_DIR = 'core_model'
|
|
25
|
+
|
|
26
|
+
attr_reader :user_dirs, :default_dir
|
|
27
|
+
|
|
28
|
+
# Initialize the locator
|
|
29
|
+
#
|
|
30
|
+
# @param user_dirs [Array<String>] User template directories (checked first)
|
|
31
|
+
# @param default_dir [String] Default templates directory (fallback)
|
|
32
|
+
def initialize(user_dirs: [], default_dir: nil)
|
|
33
|
+
@user_dirs = Array(user_dirs).map { |d| Pathname.new(d) }
|
|
34
|
+
@default_dir = default_dir ? Pathname.new(default_dir) : default_template_dir
|
|
35
|
+
@cache = {}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Find a template by type name
|
|
39
|
+
#
|
|
40
|
+
# @param type_name [String] The template type (e.g., "bibliography", "section")
|
|
41
|
+
# @return [Pathname, nil] Path to template file or nil if not found
|
|
42
|
+
def find(type_name)
|
|
43
|
+
return @cache[type_name] if @cache.key?(type_name)
|
|
44
|
+
|
|
45
|
+
# First check user directories
|
|
46
|
+
@user_dirs.each do |dir|
|
|
47
|
+
template_path = dir / CORE_MODEL_DIR / "#{type_name}.liquid"
|
|
48
|
+
if template_path.exist?
|
|
49
|
+
@cache[type_name] = template_path
|
|
50
|
+
return template_path
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Then check default directory
|
|
55
|
+
if @default_dir
|
|
56
|
+
template_path = @default_dir / "#{type_name}.liquid"
|
|
57
|
+
if template_path.exist?
|
|
58
|
+
@cache[type_name] = template_path
|
|
59
|
+
return template_path
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
@cache[type_name] = nil
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Check if a template exists (without loading it)
|
|
68
|
+
#
|
|
69
|
+
# @param type_name [String] The template type
|
|
70
|
+
# @return [Boolean] True if template exists
|
|
71
|
+
def exists?(type_name)
|
|
72
|
+
!find(type_name).nil?
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Get all available template types
|
|
76
|
+
#
|
|
77
|
+
# @return [Array<String>] List of available template names
|
|
78
|
+
def available_templates
|
|
79
|
+
types = Set.new
|
|
80
|
+
|
|
81
|
+
# Collect from user directories
|
|
82
|
+
@user_dirs.each do |dir|
|
|
83
|
+
core_model_path = dir / CORE_MODEL_DIR
|
|
84
|
+
next unless core_model_path.exist? && core_model_path.directory?
|
|
85
|
+
|
|
86
|
+
core_model_path.glob('*.liquid') do |f|
|
|
87
|
+
types.add(f.basename('.liquid').to_s)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Collect from default directory
|
|
92
|
+
if @default_dir&.exist? && @default_dir.directory?
|
|
93
|
+
@default_dir.glob('*.liquid') do |f|
|
|
94
|
+
types.add(f.basename('.liquid').to_s)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
types.to_a.sort
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Get the default template directory path
|
|
102
|
+
#
|
|
103
|
+
# @return [Pathname] Path to default templates
|
|
104
|
+
def default_template_dir
|
|
105
|
+
Pathname.new(File.join(File.dirname(__FILE__), 'templates', CORE_MODEL_DIR))
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Clear the template cache (useful when template directories change)
|
|
109
|
+
def clear_cache
|
|
110
|
+
@cache.clear
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'coradoc/html/base'
|
|
4
|
+
|
|
5
|
+
module Coradoc
|
|
6
|
+
module Html
|
|
7
|
+
module Theme
|
|
8
|
+
# Abstract base class for all HTML themes
|
|
9
|
+
#
|
|
10
|
+
# This class defines the interface that all theme renderers must implement.
|
|
11
|
+
# Themes are responsible for converting Coradoc document models to HTML output.
|
|
12
|
+
#
|
|
13
|
+
# @abstract Subclass and implement {#render} to create a custom theme
|
|
14
|
+
class Base
|
|
15
|
+
attr_reader :document, :options
|
|
16
|
+
|
|
17
|
+
# Initialize a new theme instance
|
|
18
|
+
#
|
|
19
|
+
# @param document [Coradoc::CoreModel::StructuralElement] The document to render
|
|
20
|
+
# @param options [Hash] Rendering options
|
|
21
|
+
def initialize(document, options = {})
|
|
22
|
+
@document = document
|
|
23
|
+
@options = options
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Render the document to HTML
|
|
27
|
+
#
|
|
28
|
+
# This method must be implemented by subclasses.
|
|
29
|
+
#
|
|
30
|
+
# @abstract
|
|
31
|
+
# @return [String] The rendered HTML content
|
|
32
|
+
def render
|
|
33
|
+
raise NotImplementedError,
|
|
34
|
+
"#{self.class.name} must implement #render method"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Render the complete HTML5 document
|
|
38
|
+
#
|
|
39
|
+
# @return [String] Complete HTML5 document
|
|
40
|
+
def render_html5
|
|
41
|
+
html_body = render
|
|
42
|
+
build_html5_document(html_body)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Get the theme name
|
|
46
|
+
#
|
|
47
|
+
# @return [Symbol] Theme name (e.g., :classic, :modern)
|
|
48
|
+
def theme_name
|
|
49
|
+
@theme_name ||= self.class.name.split('::').last
|
|
50
|
+
.gsub(/Renderer$/, '')
|
|
51
|
+
.downcase
|
|
52
|
+
.to_sym
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Check if this theme supports a specific feature
|
|
56
|
+
#
|
|
57
|
+
# @param feature [Symbol] Feature to check (e.g., :dark_mode, :interactive_toc)
|
|
58
|
+
# @return [Boolean] true if the feature is supported
|
|
59
|
+
def supports?(feature)
|
|
60
|
+
supported_features.include?(feature)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# List of features supported by this theme
|
|
64
|
+
#
|
|
65
|
+
# Subclasses can override to declare supported features.
|
|
66
|
+
#
|
|
67
|
+
# @return [Array<Symbol>] List of supported features
|
|
68
|
+
def supported_features
|
|
69
|
+
[]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
protected
|
|
73
|
+
|
|
74
|
+
# Build complete HTML5 document
|
|
75
|
+
#
|
|
76
|
+
# @param body_html [String] HTML body content
|
|
77
|
+
# @return [String] Complete HTML5 document
|
|
78
|
+
def build_html5_document(body_html)
|
|
79
|
+
lang = @options[:lang] || 'en'
|
|
80
|
+
body_classes = build_body_classes
|
|
81
|
+
|
|
82
|
+
<<~HTML
|
|
83
|
+
<!DOCTYPE html>
|
|
84
|
+
<html lang="#{lang}">
|
|
85
|
+
<head>
|
|
86
|
+
#{build_head_content}
|
|
87
|
+
</head>
|
|
88
|
+
<body#{body_classes}>
|
|
89
|
+
#{body_html}
|
|
90
|
+
</body>
|
|
91
|
+
</html>
|
|
92
|
+
HTML
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Build HTML head content
|
|
96
|
+
#
|
|
97
|
+
# @return [String] Head section HTML
|
|
98
|
+
def build_head_content
|
|
99
|
+
parts = []
|
|
100
|
+
parts << build_meta_tags
|
|
101
|
+
parts << build_title_tag
|
|
102
|
+
parts << build_css_tags
|
|
103
|
+
parts << build_script_tags
|
|
104
|
+
parts.compact.reject(&:empty?).join("\n")
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Build meta tags
|
|
108
|
+
#
|
|
109
|
+
# @return [String] Meta tags HTML
|
|
110
|
+
def build_meta_tags
|
|
111
|
+
meta = []
|
|
112
|
+
meta << ' <meta charset="UTF-8">'
|
|
113
|
+
meta << ' <meta name="viewport" content="width=device-width, initial-scale=1.0">'
|
|
114
|
+
|
|
115
|
+
# Author meta tag
|
|
116
|
+
if @options[:author]
|
|
117
|
+
author = escape_attr(@options[:author])
|
|
118
|
+
meta << %( <meta name="author" content="#{author}">)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Description meta tag
|
|
122
|
+
if @options[:description]
|
|
123
|
+
description = escape_attr(@options[:description])
|
|
124
|
+
meta << %( <meta name="description" content="#{description}">)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Keywords meta tag
|
|
128
|
+
if @options[:keywords]
|
|
129
|
+
keywords = escape_attr(@options[:keywords])
|
|
130
|
+
meta << %( <meta name="keywords" content="#{keywords}">)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Generator meta tag with version
|
|
134
|
+
meta << %( <meta name="generator" content="Coradoc #{Coradoc::VERSION}">)
|
|
135
|
+
|
|
136
|
+
# Generation timestamp
|
|
137
|
+
meta << %( <meta name="generated" content="#{Time.now.utc.iso8601}">)
|
|
138
|
+
|
|
139
|
+
# Custom meta tags
|
|
140
|
+
if @options[:meta_tags].is_a?(Hash)
|
|
141
|
+
@options[:meta_tags].each do |name, content|
|
|
142
|
+
meta << %( <meta name="#{escape_attr(name)}" content="#{escape_attr(content)}">)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
meta.join("\n")
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Build title tag
|
|
150
|
+
#
|
|
151
|
+
# @return [String] Title tag HTML
|
|
152
|
+
def build_title_tag
|
|
153
|
+
title = extract_document_title
|
|
154
|
+
" <title>#{escape_html(title)}</title>"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Extract document title
|
|
158
|
+
#
|
|
159
|
+
# @return [String] Document title
|
|
160
|
+
def extract_document_title
|
|
161
|
+
if @document.is_a?(Coradoc::CoreModel::StructuralElement) && @document.title
|
|
162
|
+
title = @document.title
|
|
163
|
+
return title if title.is_a?(String)
|
|
164
|
+
return title.text if title.is_a?(Coradoc::CoreModel::Base) && title.text
|
|
165
|
+
|
|
166
|
+
return title.to_s
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
'Untitled Document'
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Extract text from content (array of inline elements)
|
|
173
|
+
#
|
|
174
|
+
# @param content [Array] Content elements
|
|
175
|
+
# @return [String] Extracted text
|
|
176
|
+
def extract_text_from_content(content)
|
|
177
|
+
case content
|
|
178
|
+
when Array
|
|
179
|
+
content.map { |item| extract_text_from_content(item) }.join
|
|
180
|
+
when String
|
|
181
|
+
content
|
|
182
|
+
when Coradoc::CoreModel::InlineElement
|
|
183
|
+
content.text
|
|
184
|
+
else
|
|
185
|
+
content.to_s
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Build body classes
|
|
190
|
+
#
|
|
191
|
+
# @return [String] Body class attribute
|
|
192
|
+
def build_body_classes
|
|
193
|
+
classes = []
|
|
194
|
+
classes << "theme-#{theme_name}"
|
|
195
|
+
|
|
196
|
+
classes.empty? ? '' : %( class="#{classes.join(' ')}")
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Build CSS tags
|
|
200
|
+
#
|
|
201
|
+
# @return [String] CSS link or style tags
|
|
202
|
+
def build_css_tags
|
|
203
|
+
''
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Build script tags
|
|
207
|
+
#
|
|
208
|
+
# @return [String] Script tags
|
|
209
|
+
def build_script_tags
|
|
210
|
+
''
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Escape HTML content
|
|
214
|
+
#
|
|
215
|
+
# @param text [String] Text to escape
|
|
216
|
+
# @return [String] Escaped text
|
|
217
|
+
def escape_html(text)
|
|
218
|
+
Coradoc::Html::Base.escape_html(text.to_s)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Escape HTML attribute value
|
|
222
|
+
#
|
|
223
|
+
# @param value [String] Value to escape
|
|
224
|
+
# @return [String] Escaped value
|
|
225
|
+
def escape_attr(value)
|
|
226
|
+
value.to_s.gsub('"', '"').gsub('<', '<').gsub('>', '>')
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|