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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ef32c1399675f7e55b4ab77c56dd992b246c0f5fc0d42f027a4792c35b67ed05
4
+ data.tar.gz: be81a3f8b9baa8dd31515bfeb32b134761271e4066d53cbeb57856a6735a4c77
5
+ SHA512:
6
+ metadata.gz: 7e2696c2a2e80fb3059881b2ef7a4dbd124f8ca252b1b2fed68590324ae7d875c0d178349647be091547cf7d9143b64b75286abd55a5059dd8d91e15aff55715
7
+ data.tar.gz: 5ccdc1ff38c8c851f0923deebddbe933488820738a6fb2faa1ed02d3262ca559e41d0c0d9b85e4fb5954a7809cf18ff7b1158dc960f5bfa973404c3749edbdda
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 Abu Nashir
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Html
5
+ # Base module for HTML processing utilities
6
+ module Base
7
+ class << self
8
+ # Convert content to HTML, handling various input types
9
+ def convert_content(content, state = {})
10
+ return '' if content.nil?
11
+
12
+ case content
13
+ when String
14
+ return '' if content.empty?
15
+
16
+ escape_html(content)
17
+ when Array
18
+ return '' if content.empty?
19
+
20
+ content.map { |item| convert_content(item, state) }.join
21
+ else
22
+ # If content responds to a converter, use it
23
+ converter = find_converter(content.class)
24
+ if converter
25
+ converter.to_html(content, state)
26
+ else
27
+ content.to_s
28
+ end
29
+ end
30
+ end
31
+
32
+ # Find the appropriate converter for a model class
33
+ def find_converter(model_class)
34
+ return nil unless defined?(Coradoc::Html::Converters)
35
+
36
+ converter_name = model_class.name.split('::').last
37
+
38
+ # Use const_get on the Converters module to trigger autoload
39
+ begin
40
+ klass = Coradoc::Html::Converters.const_get(converter_name, false)
41
+ # Return nil if this is the Base class itself (not a real converter)
42
+ # or if it doesn't inherit from Converters::Base
43
+ return nil if klass == Coradoc::Html::Converters::Base
44
+ return nil unless klass <= Coradoc::Html::Converters::Base
45
+
46
+ klass
47
+ rescue NameError
48
+ nil
49
+ end
50
+ end
51
+
52
+ # Escape HTML special characters
53
+ def escape_html(text)
54
+ return '' if text.nil?
55
+ return text unless text.is_a?(String)
56
+
57
+ text
58
+ .gsub('&', '&amp;')
59
+ .gsub('<', '&lt;')
60
+ .gsub('>', '&gt;')
61
+ .gsub('"', '&quot;')
62
+ .gsub("'", '&#39;')
63
+ end
64
+
65
+ # Unescape HTML entities
66
+ def unescape_html(text)
67
+ return '' if text.nil?
68
+ return text unless text.is_a?(String)
69
+
70
+ text
71
+ .gsub('&amp;', '&')
72
+ .gsub('&lt;', '<')
73
+ .gsub('&gt;', '>')
74
+ .gsub('&quot;', '"')
75
+ .gsub('&#39;', "'")
76
+ .gsub('&#x27;', "'")
77
+ end
78
+
79
+ # Build HTML element with attributes
80
+ def build_element(tag, content = nil, attributes = {})
81
+ attrs = build_attributes(attributes)
82
+ attr_string = attrs.empty? ? '' : " #{attrs}"
83
+
84
+ # Handle empty content (String, Array, or nil)
85
+ content_empty = case content
86
+ when nil, String, Array
87
+ content.nil? || content.empty?
88
+ else
89
+ false
90
+ end
91
+
92
+ if content_empty
93
+ # Self-closing for void elements
94
+ if void_element?(tag)
95
+ "<#{tag}#{attr_string}>"
96
+ else
97
+ "<#{tag}#{attr_string}></#{tag}>"
98
+ end
99
+ else
100
+ "<#{tag}#{attr_string}>#{content}</#{tag}>"
101
+ end
102
+ end
103
+
104
+ # Build HTML attributes string
105
+ def build_attributes(attributes)
106
+ return '' if attributes.nil? || attributes.empty?
107
+
108
+ attributes.map do |key, value|
109
+ next if value.nil?
110
+
111
+ escaped_value = escape_html(value.to_s)
112
+ %(#{key}="#{escaped_value}")
113
+ end.compact.join(' ')
114
+ end
115
+
116
+ # Check if element is a void element (self-closing)
117
+ def void_element?(tag)
118
+ %w[area base br col embed hr img input link meta param source track wbr].include?(tag.to_s)
119
+ end
120
+
121
+ # Extract attributes from a CoreModel
122
+ #
123
+ # @param model [Coradoc::CoreModel::Base] Model to extract attributes from
124
+ # @return [Hash] Attributes hash
125
+ def extract_attributes(model)
126
+ attrs = {}
127
+
128
+ attrs[:id] = model.id if model.id
129
+ attrs[:title] = model.title if model.title
130
+
131
+ if model.is_a?(Coradoc::CoreModel::StructuralElement) && model.metadata
132
+ attrs[:class] = model.metadata[:class] || model.metadata[:role]
133
+ attrs.merge!(model.metadata.except(:class, :role))
134
+ end
135
+
136
+ attrs
137
+ end
138
+
139
+ # Wrap content with line breaks if needed
140
+ def wrap_lines(content)
141
+ return content unless content.is_a?(String)
142
+
143
+ content.split("\n").join("<br>\n")
144
+ end
145
+
146
+ # Process children of a node (common operation)
147
+ def treat_children(children, state = {})
148
+ return [] if children.nil? || children.empty?
149
+
150
+ Array(children).map do |child|
151
+ convert_content(child, state)
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,467 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Html
5
+ # HTML configuration and options
6
+ module Config
7
+ # Default HTML output options
8
+ DEFAULT_OPTIONS = {
9
+ # Theme system options
10
+ theme: :classic,
11
+ modern: {
12
+ # Appearance
13
+ color_scheme: :glass,
14
+ primary_color: '#6366f1',
15
+ accent_color: '#8b5cf6',
16
+
17
+ # Layout
18
+ max_width: '1200px',
19
+ content_width: '65ch',
20
+ sidebar_width: '280px',
21
+
22
+ # Features
23
+ theme_toggle: true,
24
+ reading_progress: true,
25
+ back_to_top: true,
26
+ toc_sticky: true,
27
+ copy_code_buttons: true,
28
+
29
+ # Animation
30
+ enable_animations: true,
31
+ animation_duration: '300ms',
32
+
33
+ # Performance
34
+ lazy_load_images: true
35
+ }.freeze,
36
+
37
+ # HTML version
38
+ html_version: :html5,
39
+
40
+ # Formatting options
41
+ pretty_print: false,
42
+ indent: ' ',
43
+ line_wrap: 0,
44
+
45
+ # Content options
46
+ escape_content: true,
47
+ preserve_whitespace: false,
48
+ convert_line_breaks: true,
49
+ preserve_comments: false,
50
+
51
+ # Element options
52
+ use_semantic_elements: true,
53
+ add_css_classes: true,
54
+ add_data_attributes: false,
55
+
56
+ # Link options
57
+ external_link_target: nil,
58
+ link_rel: nil,
59
+
60
+ # Image options
61
+ image_loading: nil,
62
+ image_decoding: nil,
63
+
64
+ # Code block options
65
+ syntax_highlighter: nil,
66
+ syntax_highlighter_opts: {},
67
+
68
+ # Table options
69
+ table_border: false,
70
+ table_stripes: false,
71
+
72
+ # Attribute options
73
+ preserve_custom_attributes: true,
74
+ attribute_prefix: 'data-',
75
+
76
+ # CSS & Styling options
77
+ stylesheet: 'coradoc.css',
78
+ stylesdir: './css',
79
+ linkcss: false,
80
+ copycss: true,
81
+ css_theme: 'professional',
82
+ custom_css: nil,
83
+
84
+ # JavaScript options
85
+ javascript: 'coradoc.js',
86
+ jsdir: './js',
87
+ linkjs: false,
88
+ theme_toggle: true,
89
+ toc_interactive: nil,
90
+
91
+ # Document metadata options
92
+ author: nil,
93
+ description: nil,
94
+ keywords: nil,
95
+ lang: 'en',
96
+ embedded: false,
97
+ meta_tags: {},
98
+
99
+ # Table of contents options
100
+ toc: false,
101
+ toclevels: 2,
102
+ toc_title: 'Table of Contents',
103
+ toc_placement: :auto,
104
+
105
+ # Section numbering options
106
+ sectnums: false,
107
+ sectnumlevels: 3,
108
+
109
+ # Syntax highlighting options
110
+ source_highlighter: nil,
111
+ highlightjs_theme: 'github',
112
+ pygments_style: 'default',
113
+ rouge_style: 'github'
114
+ }.freeze
115
+
116
+ class << self
117
+ # Get default options
118
+ def default_options
119
+ DEFAULT_OPTIONS.dup
120
+ end
121
+
122
+ # Merge user options with defaults
123
+ def merge_options(user_options = {})
124
+ default_options.merge(user_options)
125
+ end
126
+
127
+ # Validate options
128
+ def validate_options(options)
129
+ valid_keys = DEFAULT_OPTIONS.keys
130
+ invalid_keys = options.keys - valid_keys
131
+
132
+ raise ArgumentError, "Invalid options: #{invalid_keys.join(', ')}" unless invalid_keys.empty?
133
+
134
+ options
135
+ end
136
+
137
+ # Get CSS class for element type
138
+ def css_class_for(element_type, role = nil)
139
+ classes = [element_type.to_s.tr('_', '-')]
140
+ classes << role if role
141
+ classes.join(' ')
142
+ end
143
+
144
+ # Get data attribute name
145
+ def data_attribute_name(name, prefix: 'data-')
146
+ "#{prefix}#{name.to_s.tr('_', '-')}"
147
+ end
148
+
149
+ # Build element configuration
150
+ def element_config(element_type, options = {})
151
+ {
152
+ tag: html_tag_for(element_type),
153
+ css_class: css_class_for(element_type, options[:role]),
154
+ attributes: options[:attributes] || {}
155
+ }
156
+ end
157
+
158
+ # Map element type to HTML tag
159
+ def html_tag_for(element_type)
160
+ TAG_MAPPING[element_type] || 'div'
161
+ end
162
+
163
+ # Get stylesheet path
164
+ def stylesheet_path(options = {})
165
+ # When linking, use css_theme-based filename, not the stylesheet option
166
+ css_theme = options[:css_theme] || DEFAULT_OPTIONS[:css_theme]
167
+ stylesheet = "#{css_theme}.css"
168
+ stylesdir = options[:stylesdir] || DEFAULT_OPTIONS[:stylesdir]
169
+
170
+ if stylesdir && stylesdir != '.'
171
+ File.join(stylesdir, stylesheet)
172
+ else
173
+ stylesheet
174
+ end
175
+ end
176
+
177
+ # Get embedded stylesheet content
178
+ def embedded_stylesheet(options = {})
179
+ css_theme = options[:css_theme] || DEFAULT_OPTIONS[:css_theme]
180
+ stylesheet_name = "#{css_theme}.css"
181
+
182
+ # Try themes directory first
183
+ themes_path = File.join(__dir__, 'assets', 'themes', stylesheet_name)
184
+ asset_path = if File.exist?(themes_path)
185
+ themes_path
186
+ else
187
+ # Fall back to assets directory for backward compatibility
188
+ File.join(__dir__, 'assets', stylesheet_name)
189
+ end
190
+
191
+ css_content = if File.exist?(asset_path)
192
+ File.read(asset_path)
193
+ else
194
+ # Fallback to default coradoc.css
195
+ default_path = File.join(__dir__, 'assets', 'coradoc.css')
196
+ File.exist?(default_path) ? File.read(default_path) : ''
197
+ end
198
+
199
+ # Resolve @import statements for embedded CSS
200
+ # @import doesn't work in inline <style> tags
201
+ resolve_css_imports(css_content, File.dirname(asset_path))
202
+ end
203
+
204
+ # Resolve @import statements in CSS content
205
+ # @param css_content [String] CSS content with potential @import statements
206
+ # @param base_dir [String] Base directory for resolving relative imports
207
+ # @return [String] CSS content with imports resolved
208
+ def resolve_css_imports(css_content, base_dir)
209
+ # Match @import url('...') or @import url("...") or @import '...' or @import "..."
210
+ css_content.gsub(/@import\s+(?:url\()?['"]([^'"]+)['"]\)?;?/) do
211
+ import_path = ::Regexp.last_match(1)
212
+ full_path = File.join(base_dir, import_path)
213
+
214
+ if File.exist?(full_path)
215
+ # Read the imported file and recursively resolve its imports
216
+ imported_content = File.read(full_path)
217
+ resolve_css_imports(imported_content, File.dirname(full_path))
218
+ else
219
+ # Keep the original import if file not found
220
+ ::Regexp.last_match(0)
221
+ end
222
+ end
223
+ end
224
+
225
+ # Build CSS link tag
226
+ def css_link_tag(options = {})
227
+ href = stylesheet_path(options)
228
+ %(<link rel="stylesheet" href="#{href}">)
229
+ end
230
+
231
+ # Build CSS style tag with embedded content
232
+ def css_style_tag(options = {})
233
+ css_content = embedded_stylesheet(options)
234
+ custom_css = options[:custom_css]
235
+
236
+ content = css_content
237
+ content += "\n\n#{custom_css}" if custom_css && !custom_css.empty?
238
+
239
+ %(<style>\n#{content}\n</style>)
240
+ end
241
+
242
+ # Build custom CSS style tag
243
+ def custom_css_tag(custom_css)
244
+ return '' unless custom_css && !custom_css.empty?
245
+
246
+ %(<style>\n#{custom_css}\n</style>)
247
+ end
248
+
249
+ # Determine whether to embed or link CSS
250
+ def embed_css?(options = {})
251
+ # Embed if linkcss is false or embedded mode is true
252
+ !options.fetch(:linkcss, DEFAULT_OPTIONS[:linkcss]) ||
253
+ options.fetch(:embedded, DEFAULT_OPTIONS[:embedded])
254
+ end
255
+
256
+ # Build complete CSS tags (link or embedded, plus custom)
257
+ def css_tags(options = {})
258
+ tags = []
259
+
260
+ if embed_css?(options)
261
+ # Embedded mode: include full stylesheet in style tag
262
+ tags << css_style_tag(options)
263
+ else
264
+ # Linked mode: link to external stylesheet
265
+ tags << css_link_tag(options)
266
+ # Add custom CSS separately if provided
267
+ tags << custom_css_tag(options[:custom_css]) if options[:custom_css]
268
+ end
269
+
270
+ tags.join("\n")
271
+ end
272
+
273
+ # Determine whether to embed or link JavaScript
274
+ def embed_js?(options = {})
275
+ # Embed if linkjs is false, embedded mode is true, or linkcss is false (to match CSS behavior)
276
+ !options.fetch(:linkjs, DEFAULT_OPTIONS[:linkjs]) ||
277
+ options.fetch(:embedded, DEFAULT_OPTIONS[:embedded]) ||
278
+ !options.fetch(:linkcss, DEFAULT_OPTIONS[:linkcss])
279
+ end
280
+
281
+ # Get JavaScript file path
282
+ def javascript_path(options = {})
283
+ javascript = options[:javascript] || DEFAULT_OPTIONS[:javascript]
284
+ jsdir = options[:jsdir] || DEFAULT_OPTIONS[:jsdir]
285
+
286
+ if jsdir && jsdir != '.'
287
+ File.join(jsdir, javascript)
288
+ else
289
+ javascript
290
+ end
291
+ end
292
+
293
+ # Get embedded JavaScript content
294
+ def embedded_javascript(options = {})
295
+ javascript_name = options[:javascript] || DEFAULT_OPTIONS[:javascript]
296
+ asset_path = File.join(__dir__, 'assets', 'js', javascript_name)
297
+
298
+ if File.exist?(asset_path)
299
+ File.read(asset_path)
300
+ else
301
+ ''
302
+ end
303
+ end
304
+
305
+ # Build JavaScript link tag
306
+ def js_link_tag(options = {})
307
+ src = javascript_path(options)
308
+ %(<script src="#{src}" defer></script>)
309
+ end
310
+
311
+ # Build JavaScript script tag with embedded content
312
+ def js_script_tag(options = {})
313
+ js_content = embedded_javascript(options)
314
+ return '' if js_content.empty?
315
+
316
+ %(<script>\n#{js_content}\n</script>)
317
+ end
318
+
319
+ # Build complete JavaScript tags (link or embedded)
320
+ def js_tags(options = {})
321
+ return '' if options[:javascript] == false
322
+
323
+ tags = []
324
+
325
+ tags << if embed_js?(options)
326
+ # Embedded mode: include full JavaScript in script tag
327
+ js_script_tag(options)
328
+ else
329
+ # Linked mode: link to external JavaScript file
330
+ js_link_tag(options)
331
+ end
332
+
333
+ tags.join("\n")
334
+ end
335
+
336
+ # Check if theme toggle should be enabled
337
+ def theme_toggle?(options = {})
338
+ options.fetch(:theme_toggle, DEFAULT_OPTIONS[:theme_toggle])
339
+ end
340
+
341
+ # Check if interactive TOC should be enabled
342
+ def toc_interactive?(options = {})
343
+ # Default to true if TOC is enabled and toc_interactive is not explicitly set to false
344
+ toc_enabled = options.fetch(:toc, DEFAULT_OPTIONS[:toc])
345
+ toc_interactive = options[:toc_interactive]
346
+
347
+ # If toc_interactive is nil, default to true when TOC is enabled
348
+ if toc_interactive.nil?
349
+ toc_enabled
350
+ else
351
+ toc_interactive
352
+ end
353
+ end
354
+
355
+ # Build syntax highlighter tags (CSS and JS)
356
+ # @param options [Hash] Configuration options
357
+ # @return [String] HTML tags for syntax highlighting
358
+ def syntax_highlighter_tags(options = {})
359
+ highlighter = options[:source_highlighter]
360
+ return '' unless highlighter
361
+
362
+ case highlighter.to_sym
363
+ when :highlightjs, :highlight_js, :'highlight.js'
364
+ highlightjs_tags(options)
365
+ when :pygments
366
+ # Pygments requires server-side processing, not implemented for client-side HTML
367
+ ''
368
+ when :rouge
369
+ # Rouge requires server-side processing, not implemented for client-side HTML
370
+ ''
371
+ else
372
+ ''
373
+ end
374
+ end
375
+
376
+ # Build Highlight.js tags
377
+ # @param options [Hash] Configuration options
378
+ # @return [String] HTML tags for Highlight.js
379
+ def highlightjs_tags(options = {})
380
+ theme = options[:highlightjs_theme] || DEFAULT_OPTIONS[:highlightjs_theme]
381
+ tags = []
382
+
383
+ # Add Highlight.js library
384
+ tags << %(<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/#{theme}.min.css">)
385
+ tags << %(<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>)
386
+ tags << %(<script>hljs.highlightAll();</script>)
387
+
388
+ tags.join("\n")
389
+ end
390
+
391
+ # Get data attributes for code block
392
+ # @param language [String] Programming language
393
+ # @param options [Hash] Code block options
394
+ # @return [Hash] Data attributes
395
+ def code_block_attributes(language, options = {})
396
+ attrs = {}
397
+
398
+ attrs[:class] = "language-#{language}" if language && !language.empty?
399
+
400
+ if options[:linenums] || options[:line_numbers]
401
+ attrs[:class] =
402
+ [attrs[:class], 'line-numbers'].compact.join(' ')
403
+ end
404
+
405
+ attrs
406
+ end
407
+
408
+ # Mapping of Coradoc elements to HTML tags
409
+ TAG_MAPPING = {
410
+ # Sections
411
+ section: 'section',
412
+ header: 'header',
413
+
414
+ # Blocks
415
+ paragraph: 'p',
416
+ example: 'div',
417
+ sidebar: 'aside',
418
+ quote: 'blockquote',
419
+ verse: 'div',
420
+ listing: 'pre',
421
+ literal: 'pre',
422
+ source: 'pre',
423
+ open: 'div',
424
+
425
+ # Lists
426
+ ordered_list: 'ol',
427
+ unordered_list: 'ul',
428
+ list_item: 'li',
429
+ description_list: 'dl',
430
+ description_term: 'dt',
431
+ description_detail: 'dd',
432
+
433
+ # Tables
434
+ table: 'table',
435
+ table_row: 'tr',
436
+ table_cell: 'td',
437
+ table_header: 'th',
438
+
439
+ # Inline
440
+ bold: 'strong',
441
+ italic: 'em',
442
+ monospace: 'code',
443
+ highlight: 'mark',
444
+ superscript: 'sup',
445
+ subscript: 'sub',
446
+ underline: 'u',
447
+ strikethrough: 'del',
448
+ small_caps: 'span',
449
+
450
+ # Links
451
+ anchor: 'a',
452
+ cross_reference: 'a',
453
+
454
+ # Media
455
+ image: 'img',
456
+ video: 'video',
457
+ audio: 'audio',
458
+
459
+ # Other
460
+ break: 'hr',
461
+ line_break: 'br',
462
+ admonition: 'div'
463
+ }.freeze
464
+ end
465
+ end
466
+ end
467
+ end