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,177 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Html
|
|
5
|
+
# Abstract base class for HTML output converters
|
|
6
|
+
#
|
|
7
|
+
# This class defines the interface that all HTML output converters must implement.
|
|
8
|
+
# It provides common functionality for document validation, configuration building,
|
|
9
|
+
# and HTML output generation.
|
|
10
|
+
#
|
|
11
|
+
# @abstract Subclass and implement {#convert} to create a custom converter
|
|
12
|
+
#
|
|
13
|
+
# @example Creating a custom converter
|
|
14
|
+
# class MyConverter < Coradoc::Html::ConverterBase
|
|
15
|
+
# def convert
|
|
16
|
+
# # Generate HTML from document
|
|
17
|
+
# end
|
|
18
|
+
# end
|
|
19
|
+
class ConverterBase
|
|
20
|
+
# Error class for converter validation errors
|
|
21
|
+
class ValidationError < Coradoc::Error; end
|
|
22
|
+
|
|
23
|
+
# Error class for unsupported document types
|
|
24
|
+
class UnsupportedDocumentError < Coradoc::Error; end
|
|
25
|
+
|
|
26
|
+
attr_reader :document, :config
|
|
27
|
+
|
|
28
|
+
# Initialize a new converter instance
|
|
29
|
+
#
|
|
30
|
+
# @param document [Coradoc::CoreModel::StructuralElement] The document to convert
|
|
31
|
+
# @param config [Hash, Configuration] Converter configuration
|
|
32
|
+
# @raise [UnsupportedDocumentError] if document is not a valid type
|
|
33
|
+
def initialize(document, config = {})
|
|
34
|
+
@document = validate_input(document)
|
|
35
|
+
@config = build_config(config)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Convert the document to HTML
|
|
39
|
+
#
|
|
40
|
+
# @abstract Subclasses must implement this method
|
|
41
|
+
# @return [String] HTML output
|
|
42
|
+
# @raise [NotImplementedError] if not implemented by subclass
|
|
43
|
+
def convert
|
|
44
|
+
raise NotImplementedError,
|
|
45
|
+
"#{self.class.name} must implement #convert method"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Convert and write to file
|
|
49
|
+
#
|
|
50
|
+
# @param output_path [String] Path to write the output file
|
|
51
|
+
# @return [String] The output path
|
|
52
|
+
def to_file(output_path)
|
|
53
|
+
html = convert
|
|
54
|
+
|
|
55
|
+
# Ensure parent directory exists
|
|
56
|
+
output_dir = File.dirname(output_path)
|
|
57
|
+
FileUtils.mkdir_p(output_dir) unless output_dir == '.'
|
|
58
|
+
|
|
59
|
+
File.write(output_path, html)
|
|
60
|
+
|
|
61
|
+
output_path
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Class method to convert a document
|
|
65
|
+
#
|
|
66
|
+
# @param document [Coradoc::CoreModel::StructuralElement] The document to convert
|
|
67
|
+
# @param config [Hash] Converter configuration
|
|
68
|
+
# @return [String] HTML output
|
|
69
|
+
def self.convert(document, config = {})
|
|
70
|
+
new(document, config).convert
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Class method to convert and write to file
|
|
74
|
+
#
|
|
75
|
+
# @param document [Coradoc::CoreModel::StructuralElement] The document to convert
|
|
76
|
+
# @param output_path [String] Path to write the output file
|
|
77
|
+
# @param config [Hash] Converter configuration
|
|
78
|
+
# @return [String] The output path
|
|
79
|
+
def self.to_file(document, output_path, config = {})
|
|
80
|
+
new(document, config).to_file(output_path)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Get the converter name
|
|
84
|
+
#
|
|
85
|
+
# @return [Symbol] Converter name (e.g., :static, :spa)
|
|
86
|
+
def converter_name
|
|
87
|
+
@converter_name ||= self.class.name.split('::').last
|
|
88
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
89
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
90
|
+
.downcase
|
|
91
|
+
.to_sym
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
protected
|
|
95
|
+
|
|
96
|
+
# Validate the input document
|
|
97
|
+
#
|
|
98
|
+
# @param document [Object] The document to validate
|
|
99
|
+
# @return [Coradoc::CoreModel::Base] The validated document
|
|
100
|
+
# @raise [UnsupportedDocumentError] if document is not valid
|
|
101
|
+
def validate_input(document)
|
|
102
|
+
# Handle transformer hash output
|
|
103
|
+
document = Coradoc::Transformer.transform(document) if document.is_a?(Hash) && document.key?(:document)
|
|
104
|
+
|
|
105
|
+
# Validate document type - ONLY accept CoreModel types
|
|
106
|
+
unless document.is_a?(Coradoc::CoreModel::Base)
|
|
107
|
+
raise UnsupportedDocumentError,
|
|
108
|
+
"Expected CoreModel document, got: #{document.class}. " \
|
|
109
|
+
'Transform your document to CoreModel first using the appropriate ' \
|
|
110
|
+
'format transformer (e.g., ToCoreModel for your source format).'
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
document
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Build configuration from options
|
|
117
|
+
#
|
|
118
|
+
# @param config [Hash, Object] Configuration options or object
|
|
119
|
+
# @return [Object] Built configuration object
|
|
120
|
+
def build_config(config)
|
|
121
|
+
if config.public_methods.include?(:validate!)
|
|
122
|
+
config.validate!
|
|
123
|
+
return config
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
config
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def extract_document_title
|
|
130
|
+
if @document.is_a?(Coradoc::CoreModel::StructuralElement) && @document.title
|
|
131
|
+
title = @document.title
|
|
132
|
+
return title if title.is_a?(String)
|
|
133
|
+
return title.text if title.is_a?(Coradoc::CoreModel::Base) && title.text
|
|
134
|
+
|
|
135
|
+
return title.to_s
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
'Untitled Document'
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def extract_text_from_content(content)
|
|
142
|
+
case content
|
|
143
|
+
when Array
|
|
144
|
+
content.map { |item| extract_text_from_content(item) }.join
|
|
145
|
+
when String
|
|
146
|
+
content
|
|
147
|
+
when Coradoc::CoreModel::InlineElement
|
|
148
|
+
content.content.to_s
|
|
149
|
+
when Coradoc::CoreModel::Base
|
|
150
|
+
if content.content
|
|
151
|
+
extract_text_from_content(content.content)
|
|
152
|
+
else
|
|
153
|
+
content.to_s
|
|
154
|
+
end
|
|
155
|
+
else
|
|
156
|
+
content.to_s
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Escape HTML content
|
|
161
|
+
#
|
|
162
|
+
# @param text [String] Text to escape
|
|
163
|
+
# @return [String] Escaped text
|
|
164
|
+
def escape_html(text)
|
|
165
|
+
Coradoc::Html::Base.escape_html(text.to_s)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Escape HTML attribute value
|
|
169
|
+
#
|
|
170
|
+
# @param value [String] Value to escape
|
|
171
|
+
# @return [String] Escaped value
|
|
172
|
+
def escape_attr(value)
|
|
173
|
+
value.to_s.gsub('"', '"').gsub('<', '<').gsub('>', '>')
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Html
|
|
5
|
+
module Converters
|
|
6
|
+
# Converter for CoreModel::AnnotationBlock to HTML admonition block
|
|
7
|
+
class Admonition < Base
|
|
8
|
+
# Convert CoreModel::AnnotationBlock to HTML admonition block
|
|
9
|
+
def self.to_html(admonition, _options = {})
|
|
10
|
+
return '' unless admonition
|
|
11
|
+
|
|
12
|
+
# Get admonition type (NOTE, TIP, IMPORTANT, WARNING, CAUTION)
|
|
13
|
+
type = admonition.annotation_type ? admonition.annotation_type.to_s.upcase : 'NOTE'
|
|
14
|
+
|
|
15
|
+
# Build div attributes
|
|
16
|
+
attrs = build_attributes(admonition, type)
|
|
17
|
+
|
|
18
|
+
# Build title if present
|
|
19
|
+
title_html = build_title(admonition)
|
|
20
|
+
|
|
21
|
+
# Build admonition label
|
|
22
|
+
label = build_label(type)
|
|
23
|
+
|
|
24
|
+
# Process admonition content
|
|
25
|
+
content = process_content(admonition.content)
|
|
26
|
+
|
|
27
|
+
# Combine label, title, and content
|
|
28
|
+
admonition_html = ''
|
|
29
|
+
admonition_html += "#{label}\n"
|
|
30
|
+
admonition_html += "#{title_html}\n" if title_html
|
|
31
|
+
admonition_html += content
|
|
32
|
+
|
|
33
|
+
%(<div#{attrs}>\n#{admonition_html}\n</div>)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Convert HTML admonition div to CoreModel::AnnotationBlock
|
|
37
|
+
def self.to_coradoc(element, _options = {})
|
|
38
|
+
return nil unless element.name == 'div'
|
|
39
|
+
return nil unless element['class']&.include?('admonition')
|
|
40
|
+
|
|
41
|
+
# Extract admonition type from class
|
|
42
|
+
type = extract_type(element)
|
|
43
|
+
return nil unless type
|
|
44
|
+
|
|
45
|
+
# Extract title if present
|
|
46
|
+
title_elem = element.at_css('.admonition-title')
|
|
47
|
+
title = title_elem&.text&.strip
|
|
48
|
+
|
|
49
|
+
# Extract content - all children except label and title
|
|
50
|
+
content_nodes = element.children.reject do |node|
|
|
51
|
+
node == element.at_css('.admonition-label') ||
|
|
52
|
+
node == title_elem ||
|
|
53
|
+
(node.text? && node.text.strip.empty?)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
content = extract_content(content_nodes)
|
|
57
|
+
|
|
58
|
+
# Extract ID if present
|
|
59
|
+
id = element['id']
|
|
60
|
+
|
|
61
|
+
Coradoc::CoreModel::AnnotationBlock.new(
|
|
62
|
+
annotation_type: type.downcase,
|
|
63
|
+
content: content,
|
|
64
|
+
title: title,
|
|
65
|
+
id: id
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.build_attributes(admonition, type)
|
|
70
|
+
attrs = [%( class="admonition admonition-#{type.downcase}")]
|
|
71
|
+
|
|
72
|
+
# Add ID if present
|
|
73
|
+
attrs << %( id="#{escape_attribute(admonition.id)}") if admonition.id
|
|
74
|
+
|
|
75
|
+
attrs.join
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def self.build_label(type)
|
|
79
|
+
%(<div class="admonition-label">#{escape_html(type)}</div>)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def self.build_title(admonition)
|
|
83
|
+
return nil unless admonition.title
|
|
84
|
+
|
|
85
|
+
title_text = admonition.title.to_s
|
|
86
|
+
return nil if title_text.empty?
|
|
87
|
+
|
|
88
|
+
%(<div class="admonition-title">#{escape_html(title_text)}</div>)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def self.process_content(content)
|
|
92
|
+
return '' if content.nil?
|
|
93
|
+
|
|
94
|
+
if content.is_a?(Array)
|
|
95
|
+
# Group consecutive lines into paragraphs, handling inline elements
|
|
96
|
+
result = []
|
|
97
|
+
current_para = []
|
|
98
|
+
|
|
99
|
+
content.each do |item|
|
|
100
|
+
case item
|
|
101
|
+
when String
|
|
102
|
+
current_para << item
|
|
103
|
+
else
|
|
104
|
+
# End current paragraph and start a new one
|
|
105
|
+
if current_para.any?
|
|
106
|
+
result << build_paragraph(current_para)
|
|
107
|
+
current_para = []
|
|
108
|
+
end
|
|
109
|
+
result << convert_item(item)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Don't forget the last paragraph
|
|
114
|
+
result << build_paragraph(current_para) if current_para.any?
|
|
115
|
+
|
|
116
|
+
result.compact.join("\n")
|
|
117
|
+
elsif content.is_a?(String)
|
|
118
|
+
"<p>#{escape_html(content)}</p>"
|
|
119
|
+
else
|
|
120
|
+
convert_item(content)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def self.build_paragraph(items)
|
|
125
|
+
return nil if items.nil? || items.empty?
|
|
126
|
+
|
|
127
|
+
# Convert all items to HTML and join them
|
|
128
|
+
content_html = items.map do |item|
|
|
129
|
+
case item
|
|
130
|
+
when String
|
|
131
|
+
escape_html(item)
|
|
132
|
+
else
|
|
133
|
+
convert_content_to_html(item)
|
|
134
|
+
end
|
|
135
|
+
end.join
|
|
136
|
+
|
|
137
|
+
return nil if content_html.empty?
|
|
138
|
+
|
|
139
|
+
"<p>#{content_html}</p>"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def self.convert_item(item)
|
|
143
|
+
case item
|
|
144
|
+
when String
|
|
145
|
+
"<p>#{escape_html(item)}</p>"
|
|
146
|
+
else
|
|
147
|
+
convert_content_to_html(item)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def self.extract_type(element)
|
|
152
|
+
return nil unless element['class']
|
|
153
|
+
|
|
154
|
+
# Extract type from class like "admonition-note", "admonition-warning", etc.
|
|
155
|
+
classes = element['class'].split
|
|
156
|
+
type_class = classes.find { |c| c.start_with?('admonition-') && c != 'admonition' }
|
|
157
|
+
return nil unless type_class
|
|
158
|
+
|
|
159
|
+
type_class.sub(/^admonition-/, '').upcase
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def self.extract_content(nodes)
|
|
163
|
+
# Extract and convert content nodes
|
|
164
|
+
nodes.map do |node|
|
|
165
|
+
if node.text? && !node.text.strip.empty?
|
|
166
|
+
node.text.strip
|
|
167
|
+
elsif node.element?
|
|
168
|
+
case node.name
|
|
169
|
+
when 'p'
|
|
170
|
+
Paragraph.to_coradoc(node)
|
|
171
|
+
else
|
|
172
|
+
node.text.strip
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end.compact
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'nokogiri'
|
|
4
|
+
|
|
5
|
+
module Coradoc
|
|
6
|
+
module Html
|
|
7
|
+
module Converters
|
|
8
|
+
# Converts document attributes to/from HTML
|
|
9
|
+
#
|
|
10
|
+
# Attributes are document-level directives (e.g., :author:, :toc:).
|
|
11
|
+
# In HTML, we represent them as HTML comments to preserve the attribute
|
|
12
|
+
# information without affecting the rendered output.
|
|
13
|
+
#
|
|
14
|
+
# Examples:
|
|
15
|
+
# :author: John Doe => <!-- :author: John Doe -->
|
|
16
|
+
# :toc: => <!-- :toc: -->
|
|
17
|
+
class Attribute
|
|
18
|
+
# Convert CoreModel::Block (attribute) to HTML comment
|
|
19
|
+
def self.to_html(model, _options = {})
|
|
20
|
+
key = model.metadata&.dig(:key).to_s
|
|
21
|
+
key = escape_html(key)
|
|
22
|
+
|
|
23
|
+
# Handle single value or array of values
|
|
24
|
+
values = Array(model.metadata&.dig(:value)).compact
|
|
25
|
+
|
|
26
|
+
if values.empty?
|
|
27
|
+
# Attribute with no value (e.g., :toc:)
|
|
28
|
+
"<!-- :#{key}: -->"
|
|
29
|
+
else
|
|
30
|
+
# Attribute with value(s)
|
|
31
|
+
value_str = values.map { |v| escape_html(v.to_s) }.join(', ')
|
|
32
|
+
"<!-- :#{key}: #{value_str} -->"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Convert HTML comment to CoreModel::Block (attribute)
|
|
37
|
+
def self.to_coradoc(element, _options = {})
|
|
38
|
+
return nil unless element.is_a?(Nokogiri::XML::Comment)
|
|
39
|
+
|
|
40
|
+
content = element.content.strip
|
|
41
|
+
|
|
42
|
+
# Match attribute pattern: :key: or :key: value
|
|
43
|
+
return nil unless content.match?(/^:([^:]+):(.*)$/)
|
|
44
|
+
|
|
45
|
+
match = content.match(/^:([^:]+):(.*)$/)
|
|
46
|
+
key = match[1].strip
|
|
47
|
+
value_part = match[2].strip
|
|
48
|
+
|
|
49
|
+
# Parse value(s) - could be comma-separated
|
|
50
|
+
values = if value_part.empty?
|
|
51
|
+
[]
|
|
52
|
+
else
|
|
53
|
+
value_part.split(',').map(&:strip)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
Coradoc::CoreModel::Block.new(
|
|
57
|
+
element_type: 'attribute',
|
|
58
|
+
content: key,
|
|
59
|
+
metadata: {
|
|
60
|
+
key: key,
|
|
61
|
+
value: values
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Html
|
|
5
|
+
module Converters
|
|
6
|
+
# Converts CoreModel::InlineElement with format_type "attribute_reference"
|
|
7
|
+
#
|
|
8
|
+
# Attribute references are inline placeholders that reference document attributes
|
|
9
|
+
# (e.g., {author}, {docname}, {revnumber}).
|
|
10
|
+
#
|
|
11
|
+
# In HTML, we render them as-is with the curly braces to preserve the
|
|
12
|
+
# reference syntax. Actual substitution of attribute values would happen
|
|
13
|
+
# at a different processing layer if needed.
|
|
14
|
+
#
|
|
15
|
+
# Examples:
|
|
16
|
+
# {author} => {author}
|
|
17
|
+
# {docname} => {docname}
|
|
18
|
+
class AttributeReference < Base
|
|
19
|
+
# Convert CoreModel::InlineElement (attribute_reference) to HTML
|
|
20
|
+
#
|
|
21
|
+
# @param model [Coradoc::CoreModel::InlineElement] the attribute reference model
|
|
22
|
+
# @param options [Hash] conversion options
|
|
23
|
+
# @option options [Hash] :document_attributes Document attributes for substitution
|
|
24
|
+
# @return [String] HTML representation of the attribute reference
|
|
25
|
+
def self.to_html(model, options = {})
|
|
26
|
+
name = model.target.to_s
|
|
27
|
+
|
|
28
|
+
# Try to substitute with actual attribute value if document_attributes provided
|
|
29
|
+
if options[:document_attributes]
|
|
30
|
+
value = options[:document_attributes][name] || options[:document_attributes][name]
|
|
31
|
+
return escape_html(value.to_s) if value
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Fallback: render as-is with curly braces
|
|
35
|
+
escape_html("{#{name}}")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Convert HTML text to CoreModel::InlineElement (attribute_reference)
|
|
39
|
+
#
|
|
40
|
+
# @param text [String] the HTML text
|
|
41
|
+
# @param _options [Hash] conversion options (unused)
|
|
42
|
+
# @return [Coradoc::CoreModel::InlineElement, nil] the attribute reference model or nil
|
|
43
|
+
def self.to_coradoc(text, _options = {})
|
|
44
|
+
return nil unless text.is_a?(String)
|
|
45
|
+
|
|
46
|
+
# Match attribute reference pattern: {name}
|
|
47
|
+
match = text.match(/^\{([^}]+)\}$/)
|
|
48
|
+
return nil unless match
|
|
49
|
+
|
|
50
|
+
name = match[1]
|
|
51
|
+
|
|
52
|
+
Coradoc::CoreModel::InlineElement.new(
|
|
53
|
+
format_type: 'attribute_reference',
|
|
54
|
+
target: name
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Html
|
|
5
|
+
module Converters
|
|
6
|
+
# Converter for audio elements
|
|
7
|
+
class Audio < Base
|
|
8
|
+
# Convert CoreModel::Block (audio) to HTML <audio>
|
|
9
|
+
def self.to_html(audio, _options = {})
|
|
10
|
+
return '' unless audio
|
|
11
|
+
|
|
12
|
+
# Build audio attributes
|
|
13
|
+
attrs = build_attributes(audio)
|
|
14
|
+
|
|
15
|
+
# Get audio source from metadata or content
|
|
16
|
+
src = audio.metadata&.dig(:src) || audio.content
|
|
17
|
+
|
|
18
|
+
# Build source element
|
|
19
|
+
source_tag = %(<source src="#{escape_attribute(src)}"#{build_type_attr(src)}>)
|
|
20
|
+
|
|
21
|
+
# Build optional caption/title
|
|
22
|
+
caption = audio.title
|
|
23
|
+
|
|
24
|
+
# Determine if we need a wrapper (for block audio with caption)
|
|
25
|
+
if caption
|
|
26
|
+
<<~HTML.strip
|
|
27
|
+
<figure#{build_figure_attrs(audio)}>
|
|
28
|
+
<audio#{attrs}>
|
|
29
|
+
#{source_tag}
|
|
30
|
+
Your browser does not support the audio tag.
|
|
31
|
+
</audio>
|
|
32
|
+
<figcaption>#{escape_html(caption)}</figcaption>
|
|
33
|
+
</figure>
|
|
34
|
+
HTML
|
|
35
|
+
else
|
|
36
|
+
<<~HTML.strip
|
|
37
|
+
<audio#{attrs}>
|
|
38
|
+
#{source_tag}
|
|
39
|
+
Your browser does not support the audio tag.
|
|
40
|
+
</audio>
|
|
41
|
+
HTML
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Convert HTML <audio> to CoreModel::Block (audio)
|
|
46
|
+
def self.to_coradoc(element, _options = {})
|
|
47
|
+
# Handle both <audio> and <figure><audio> structures
|
|
48
|
+
audio_elem = if element.name == 'figure'
|
|
49
|
+
element.at_css('audio')
|
|
50
|
+
elsif element.name == 'audio'
|
|
51
|
+
element
|
|
52
|
+
else
|
|
53
|
+
return nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
return nil unless audio_elem
|
|
57
|
+
|
|
58
|
+
# Extract source from <source> tag or src attribute
|
|
59
|
+
src = extract_audio_src(audio_elem)
|
|
60
|
+
return nil unless src
|
|
61
|
+
|
|
62
|
+
# Extract caption if in figure
|
|
63
|
+
caption = if element.name == 'figure'
|
|
64
|
+
figcaption = element.at_css('figcaption')
|
|
65
|
+
figcaption&.text&.strip
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Extract ID if present
|
|
69
|
+
id = audio_elem['id'] || element['id']
|
|
70
|
+
|
|
71
|
+
# Extract audio attributes
|
|
72
|
+
metadata = extract_audio_metadata(audio_elem)
|
|
73
|
+
metadata[:src] = src
|
|
74
|
+
|
|
75
|
+
Coradoc::CoreModel::Block.new(
|
|
76
|
+
element_type: 'audio',
|
|
77
|
+
content: src,
|
|
78
|
+
title: caption,
|
|
79
|
+
id: id,
|
|
80
|
+
metadata: metadata
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def self.build_attributes(audio)
|
|
85
|
+
attrs = []
|
|
86
|
+
|
|
87
|
+
# Extract options from metadata
|
|
88
|
+
options = audio.metadata&.dig(:options) || []
|
|
89
|
+
|
|
90
|
+
# Add controls by default (unless nocontrols option is set)
|
|
91
|
+
has_controls = !options.include?('nocontrols')
|
|
92
|
+
attrs << ' controls' if has_controls
|
|
93
|
+
|
|
94
|
+
# Add autoplay if specified in options
|
|
95
|
+
attrs << ' autoplay' if options.include?('autoplay')
|
|
96
|
+
|
|
97
|
+
# Add loop if specified in options
|
|
98
|
+
attrs << ' loop' if options.include?('loop')
|
|
99
|
+
|
|
100
|
+
# Add muted if specified in options
|
|
101
|
+
attrs << ' muted' if options.include?('muted')
|
|
102
|
+
|
|
103
|
+
# Add ID if present
|
|
104
|
+
attrs << %( id="#{escape_attribute(audio.id)}") if audio.id
|
|
105
|
+
|
|
106
|
+
attrs.join
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def self.build_type_attr(src)
|
|
110
|
+
# Determine audio MIME type from extension
|
|
111
|
+
ext = File.extname(src).downcase
|
|
112
|
+
type = case ext
|
|
113
|
+
when '.mp3'
|
|
114
|
+
'audio/mpeg'
|
|
115
|
+
when '.ogg', '.oga'
|
|
116
|
+
'audio/ogg'
|
|
117
|
+
when '.wav'
|
|
118
|
+
'audio/wav'
|
|
119
|
+
when '.m4a'
|
|
120
|
+
'audio/mp4'
|
|
121
|
+
when '.aac'
|
|
122
|
+
'audio/aac'
|
|
123
|
+
when '.flac'
|
|
124
|
+
'audio/flac'
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
type ? %( type="#{type}") : ''
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def self.build_figure_attrs(audio)
|
|
131
|
+
attrs = []
|
|
132
|
+
|
|
133
|
+
# Add ID to figure if present
|
|
134
|
+
attrs << %( id="#{escape_attribute(audio.id)}-figure") if audio.id
|
|
135
|
+
|
|
136
|
+
attrs.join
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def self.extract_audio_src(element)
|
|
140
|
+
# Try to get src from <source> tag first
|
|
141
|
+
source = element.at_css('source')
|
|
142
|
+
return source['src'] if source && source['src']
|
|
143
|
+
|
|
144
|
+
# Fall back to src attribute on <audio>
|
|
145
|
+
element['src']
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def self.extract_audio_metadata(element)
|
|
149
|
+
metadata = {}
|
|
150
|
+
options = []
|
|
151
|
+
|
|
152
|
+
# Extract boolean attributes
|
|
153
|
+
options << 'controls' if element.has_attribute?('controls')
|
|
154
|
+
options << 'autoplay' if element.has_attribute?('autoplay')
|
|
155
|
+
options << 'loop' if element.has_attribute?('loop')
|
|
156
|
+
options << 'muted' if element.has_attribute?('muted')
|
|
157
|
+
|
|
158
|
+
metadata[:options] = options unless options.empty?
|
|
159
|
+
|
|
160
|
+
metadata
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|