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,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'coradoc/html/template_renderer'
|
|
4
|
+
|
|
5
|
+
module Coradoc
|
|
6
|
+
module Html
|
|
7
|
+
module Converters
|
|
8
|
+
# HTML converter that uses Liquid templates for rendering
|
|
9
|
+
#
|
|
10
|
+
# This converter provides template-based rendering where:
|
|
11
|
+
# - Users can provide custom template directories
|
|
12
|
+
# - Falls back to default templates
|
|
13
|
+
# - Supports template inheritance
|
|
14
|
+
#
|
|
15
|
+
# @example Basic usage
|
|
16
|
+
# renderer = TemplateHtmlConverter.new(template_paths: ["/my/templates"])
|
|
17
|
+
# html = renderer.render(document)
|
|
18
|
+
#
|
|
19
|
+
class TemplateHtmlConverter < Base
|
|
20
|
+
# Initialize the converter
|
|
21
|
+
#
|
|
22
|
+
# @param template_paths [Array<String>] Custom template directories
|
|
23
|
+
# @param options [Hash] Additional options
|
|
24
|
+
def initialize(template_paths: [], options: {})
|
|
25
|
+
@template_paths = template_paths
|
|
26
|
+
@options = options
|
|
27
|
+
@renderer = nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Get or create the template renderer
|
|
31
|
+
#
|
|
32
|
+
# @return [Coradoc::Html::TemplateRenderer] The renderer
|
|
33
|
+
def renderer
|
|
34
|
+
@renderer ||= Coradoc::Html::TemplateRenderer.new(
|
|
35
|
+
template_paths: @template_paths,
|
|
36
|
+
options: @options
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Render a CoreModel document to HTML
|
|
41
|
+
#
|
|
42
|
+
# @param model [Coradoc::CoreModel::Base] The document to render
|
|
43
|
+
# @param state [Hash] Rendering state
|
|
44
|
+
# @return [String] Rendered HTML
|
|
45
|
+
def self.to_html(model, state = {})
|
|
46
|
+
# Get template_paths from state or use defaults
|
|
47
|
+
template_paths = state[:template_paths] || []
|
|
48
|
+
|
|
49
|
+
# Create converter with template paths
|
|
50
|
+
new(template_paths: template_paths, options: state[:template_options] || {})
|
|
51
|
+
|
|
52
|
+
# Convert content using template renderer
|
|
53
|
+
convert_content_to_html(model, state)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Convert content to HTML using template renderer
|
|
57
|
+
#
|
|
58
|
+
# @param content [Object] Content to convert
|
|
59
|
+
# @param state [Hash] Conversion state
|
|
60
|
+
# @return [String] HTML string
|
|
61
|
+
def self.convert_content_to_html(content, state = {})
|
|
62
|
+
return '' if content.nil?
|
|
63
|
+
|
|
64
|
+
renderer = Coradoc::Html::TemplateRenderer.new(
|
|
65
|
+
template_paths: state[:template_paths] || [],
|
|
66
|
+
options: state[:template_options] || {}
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
renderer.render(content)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Render a CoreModel document to HTML using the template renderer
|
|
73
|
+
#
|
|
74
|
+
# @param model [Coradoc::CoreModel::Base] The document to render
|
|
75
|
+
# @param state [Hash] Rendering state
|
|
76
|
+
# @return [String] Rendered HTML
|
|
77
|
+
def self.render_with_templates(model, state = {})
|
|
78
|
+
renderer = Coradoc::Html::TemplateRenderer.new(
|
|
79
|
+
template_paths: state[:template_paths] || [],
|
|
80
|
+
options: state[:template_options] || {}
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
renderer.render(model)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Helper module for rendering CoreModel elements using templates
|
|
88
|
+
module TemplateHelpers
|
|
89
|
+
# Render a CoreModel element using templates
|
|
90
|
+
#
|
|
91
|
+
# @param element [Coradoc::CoreModel::Base] Element to render
|
|
92
|
+
# @param template_paths [Array<String>] Custom template directories
|
|
93
|
+
# @param options [Hash] Template options
|
|
94
|
+
# @return [String] Rendered HTML
|
|
95
|
+
def render_with_templates(element, template_paths: [], **options)
|
|
96
|
+
renderer = Coradoc::Html::TemplateRenderer.new(
|
|
97
|
+
template_paths: template_paths,
|
|
98
|
+
options: options
|
|
99
|
+
)
|
|
100
|
+
renderer.render(element)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Html
|
|
5
|
+
module Converters
|
|
6
|
+
autoload :Base, "#{__dir__}/base"
|
|
7
|
+
# Converter for CoreModel::InlineElement (term) elements
|
|
8
|
+
#
|
|
9
|
+
# Terms are used in definition lists and can have types like "acronym",
|
|
10
|
+
# "symbol", "preferred", etc.
|
|
11
|
+
class Term < Base
|
|
12
|
+
# Convert HTML to CoreModel::InlineElement (term)
|
|
13
|
+
def self.to_coradoc(node, _state = {})
|
|
14
|
+
attrs = extract_node_attributes(node)
|
|
15
|
+
|
|
16
|
+
term_text = node.text.strip
|
|
17
|
+
term_type = attrs[:'data-term-type'] || attrs[:class]&.split&.find do |c|
|
|
18
|
+
c.start_with?('term-')
|
|
19
|
+
end&.sub('term-', '')
|
|
20
|
+
|
|
21
|
+
Coradoc::CoreModel::InlineElement.new(
|
|
22
|
+
format_type: 'term',
|
|
23
|
+
content: term_text,
|
|
24
|
+
target: term_type || 'term',
|
|
25
|
+
metadata: {
|
|
26
|
+
lang: attrs[:lang] || 'en',
|
|
27
|
+
render_text: attrs[:'data-render-text']
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Convert CoreModel::InlineElement (term) to HTML
|
|
33
|
+
def self.to_html(term, _state = {})
|
|
34
|
+
term_text = term.content || ''
|
|
35
|
+
term_type = term.target || 'term'
|
|
36
|
+
render_text = term.metadata&.dig(:render_text)
|
|
37
|
+
|
|
38
|
+
# Use render_text if available, otherwise use term
|
|
39
|
+
display_text = render_text&.strip&.empty? ? false : render_text
|
|
40
|
+
display_text ||= term_text
|
|
41
|
+
|
|
42
|
+
# Build class attribute
|
|
43
|
+
classes = ['term', "term-#{escape_attribute(term_type)}"]
|
|
44
|
+
class_attr = classes.join(' ')
|
|
45
|
+
|
|
46
|
+
# Build data attributes
|
|
47
|
+
data_attrs = []
|
|
48
|
+
data_attrs << %( data-term-ref="#{escape_attribute(term_text)}")
|
|
49
|
+
lang = term.metadata&.dig(:lang)
|
|
50
|
+
data_attrs << %( lang="#{escape_attribute(lang)}") if lang && lang != 'en'
|
|
51
|
+
|
|
52
|
+
# Render as a styled span with term reference
|
|
53
|
+
%(<span class="#{class_attr}"#{data_attrs.join}>#{escape_html(display_text)}</span>)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Html
|
|
5
|
+
module Converters
|
|
6
|
+
# Converter for TextElement (plain text content)
|
|
7
|
+
# In CoreModel, text is handled as plain strings
|
|
8
|
+
class TextElement < Base
|
|
9
|
+
class << self
|
|
10
|
+
# Convert HTML text node to String
|
|
11
|
+
# @param node [Nokogiri::XML::Node, String] HTML text node or string
|
|
12
|
+
# @param state [Hash] Conversion state
|
|
13
|
+
# @return [String] Plain text
|
|
14
|
+
def to_coradoc(node, state = {})
|
|
15
|
+
text = node.is_a?(String) ? node : node.text
|
|
16
|
+
text = unescape_html(text) unless state[:skip_unescape]
|
|
17
|
+
text
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Convert CoreModel content to HTML
|
|
21
|
+
# @param model [String, CoreModel] Text content
|
|
22
|
+
# @param state [Hash] Conversion state
|
|
23
|
+
# @return [String] Plain text (escaped)
|
|
24
|
+
def to_html(model, state = {})
|
|
25
|
+
# Handle both string and model with content attribute
|
|
26
|
+
content = model.content || model
|
|
27
|
+
|
|
28
|
+
return '' if content.nil?
|
|
29
|
+
|
|
30
|
+
# Process content based on type
|
|
31
|
+
case content
|
|
32
|
+
when String
|
|
33
|
+
escape_html(content)
|
|
34
|
+
when Array
|
|
35
|
+
content.map { |item| convert_content_to_html(item, state) }.join
|
|
36
|
+
else
|
|
37
|
+
escape_html(content.to_s)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Html
|
|
5
|
+
module Converters
|
|
6
|
+
# Converter for Underline inline element
|
|
7
|
+
class Underline < Base
|
|
8
|
+
class << self
|
|
9
|
+
# Convert HTML <u> to CoreModel::InlineElement
|
|
10
|
+
# @param node [Nokogiri::XML::Node] HTML node
|
|
11
|
+
# @param state [Hash] Conversion state
|
|
12
|
+
# @return [Coradoc::CoreModel::InlineElement] Underline inline element
|
|
13
|
+
def to_coradoc(node, state = {})
|
|
14
|
+
content = treat_children(node, state)
|
|
15
|
+
Coradoc::CoreModel::InlineElement.new(
|
|
16
|
+
format_type: 'underline',
|
|
17
|
+
content: content
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Convert CoreModel::InlineElement (underline) to HTML <u>
|
|
22
|
+
# @param model [Coradoc::CoreModel::InlineElement] Underline model
|
|
23
|
+
# @param state [Hash] Conversion state
|
|
24
|
+
# @return [String] HTML string
|
|
25
|
+
def to_html(model, state = {})
|
|
26
|
+
content = convert_content_to_html(model.content, state)
|
|
27
|
+
attributes = extract_model_attributes(model)
|
|
28
|
+
build_element('u', content, attributes)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Html
|
|
5
|
+
module Converters
|
|
6
|
+
# Converter for Unordered (bulleted) lists
|
|
7
|
+
class Unordered < Base
|
|
8
|
+
class << self
|
|
9
|
+
# Convert HTML <ul> to CoreModel::ListBlock
|
|
10
|
+
# @param node [Nokogiri::XML::Node] HTML node
|
|
11
|
+
# @param state [Hash] Conversion state
|
|
12
|
+
# @return [Coradoc::CoreModel::ListBlock] Unordered list model
|
|
13
|
+
def to_coradoc(node, state = {})
|
|
14
|
+
items = node.css('> li').map do |li_node|
|
|
15
|
+
Coradoc::Html::Converters::ListItem.to_coradoc(li_node, state)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
attrs = extract_node_attributes(node)
|
|
19
|
+
|
|
20
|
+
list = Coradoc::CoreModel::ListBlock.new(
|
|
21
|
+
marker_type: 'unordered',
|
|
22
|
+
items: items
|
|
23
|
+
)
|
|
24
|
+
list.id = attrs[:id] if attrs[:id]
|
|
25
|
+
|
|
26
|
+
list
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Convert CoreModel::ListBlock (unordered) to HTML <ul>
|
|
30
|
+
# @param model [Coradoc::CoreModel::ListBlock] Unordered list model
|
|
31
|
+
# @param state [Hash] Conversion state
|
|
32
|
+
# @return [String] HTML string
|
|
33
|
+
def to_html(model, state = {})
|
|
34
|
+
items_html = model.items.map do |item|
|
|
35
|
+
Coradoc::Html::Converters::ListItem.to_html(item, state)
|
|
36
|
+
end.join("\n")
|
|
37
|
+
|
|
38
|
+
attrs = {}
|
|
39
|
+
attrs[:id] = model.id if model.id
|
|
40
|
+
|
|
41
|
+
build_element('ul', "\n#{items_html}\n", attrs)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Html
|
|
5
|
+
module Converters
|
|
6
|
+
# Converter for CoreModel::Block (verse) to HTML <div class="verse">
|
|
7
|
+
class Verse < Base
|
|
8
|
+
# Convert CoreModel::Block (verse) to HTML <div class="verse">
|
|
9
|
+
def self.to_html(verse, _options = {})
|
|
10
|
+
return '' unless verse
|
|
11
|
+
|
|
12
|
+
# Build div attributes
|
|
13
|
+
attrs = build_attributes(verse)
|
|
14
|
+
|
|
15
|
+
# Build title if present
|
|
16
|
+
title_html = build_title(verse)
|
|
17
|
+
|
|
18
|
+
# Build attribution if present
|
|
19
|
+
attribution = build_attribution(verse)
|
|
20
|
+
|
|
21
|
+
# Process verse content - preserve line breaks
|
|
22
|
+
content = process_content(verse.content)
|
|
23
|
+
|
|
24
|
+
# Combine title, content, and attribution
|
|
25
|
+
verse_html = ''
|
|
26
|
+
verse_html += "#{title_html}\n" if title_html
|
|
27
|
+
verse_html += %(<pre class="verse-content">#{content}</pre>)
|
|
28
|
+
verse_html += "\n#{attribution}" if attribution
|
|
29
|
+
|
|
30
|
+
%(<div#{attrs}>\n#{verse_html}\n</div>)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Convert HTML <div class="verse"> to CoreModel::Block (verse)
|
|
34
|
+
def self.to_coradoc(element, _options = {})
|
|
35
|
+
return nil unless element.name == 'div'
|
|
36
|
+
return nil unless element['class']&.include?('verse')
|
|
37
|
+
|
|
38
|
+
# Extract title if present
|
|
39
|
+
title_elem = element.at_css('.verse-title')
|
|
40
|
+
title = title_elem&.text&.strip
|
|
41
|
+
|
|
42
|
+
# Extract content from <pre class="verse-content">
|
|
43
|
+
content_elem = element.at_css('.verse-content, pre')
|
|
44
|
+
content = content_elem&.text || ''
|
|
45
|
+
|
|
46
|
+
# Extract attribution from <cite> or <footer>
|
|
47
|
+
cite_elem = element.at_css('cite, footer')
|
|
48
|
+
attribution = cite_elem&.text&.strip
|
|
49
|
+
|
|
50
|
+
# Extract ID if present
|
|
51
|
+
id = element['id']
|
|
52
|
+
|
|
53
|
+
Coradoc::CoreModel::VerseBlock.new(
|
|
54
|
+
content: content,
|
|
55
|
+
title: title,
|
|
56
|
+
id: id,
|
|
57
|
+
attribution: attribution
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.build_attributes(verse)
|
|
62
|
+
attrs = [%( class="verse")]
|
|
63
|
+
|
|
64
|
+
# Add ID if present
|
|
65
|
+
attrs << %( id="#{escape_attribute(verse.id)}") if verse.id
|
|
66
|
+
|
|
67
|
+
attrs.join
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.build_title(verse)
|
|
71
|
+
return nil unless verse.title
|
|
72
|
+
|
|
73
|
+
title_text = verse.title.to_s
|
|
74
|
+
return nil if title_text.empty?
|
|
75
|
+
|
|
76
|
+
%(<div class="verse-title">#{escape_html(title_text)}</div>)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def self.build_attribution(verse)
|
|
80
|
+
attribution_text = verse.metadata&.dig(:attribution)
|
|
81
|
+
return nil unless attribution_text
|
|
82
|
+
|
|
83
|
+
attribution_text = attribution_text.to_s.strip
|
|
84
|
+
return nil if attribution_text.empty?
|
|
85
|
+
|
|
86
|
+
%(<footer>#{escape_html(attribution_text)}</footer>)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def self.process_content(content)
|
|
90
|
+
return '' if content.nil?
|
|
91
|
+
|
|
92
|
+
# For verse, preserve the content as-is with line breaks
|
|
93
|
+
if content.is_a?(String)
|
|
94
|
+
escape_html(content)
|
|
95
|
+
elsif content.is_a?(Array)
|
|
96
|
+
# Join array items with newlines
|
|
97
|
+
content.map { |line| escape_html(line.to_s) }.join("\n")
|
|
98
|
+
else
|
|
99
|
+
escape_html(content.to_s)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Html
|
|
5
|
+
module Converters
|
|
6
|
+
# Converter for video elements
|
|
7
|
+
class Video < Base
|
|
8
|
+
# Convert CoreModel::Block (video) to HTML <video>
|
|
9
|
+
def self.to_html(video, _options = {})
|
|
10
|
+
return '' unless video
|
|
11
|
+
|
|
12
|
+
# Build video attributes
|
|
13
|
+
attrs = build_attributes(video)
|
|
14
|
+
|
|
15
|
+
# Get video source from metadata or content
|
|
16
|
+
src = video.metadata&.dig(:src) || video.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 = video.title
|
|
23
|
+
|
|
24
|
+
# Determine if we need a wrapper (for block video with caption)
|
|
25
|
+
if caption
|
|
26
|
+
<<~HTML.strip
|
|
27
|
+
<figure#{build_figure_attrs(video)}>
|
|
28
|
+
<video#{attrs}>
|
|
29
|
+
#{source_tag}
|
|
30
|
+
Your browser does not support the video tag.
|
|
31
|
+
</video>
|
|
32
|
+
<figcaption>#{escape_html(caption)}</figcaption>
|
|
33
|
+
</figure>
|
|
34
|
+
HTML
|
|
35
|
+
else
|
|
36
|
+
<<~HTML.strip
|
|
37
|
+
<video#{attrs}>
|
|
38
|
+
#{source_tag}
|
|
39
|
+
Your browser does not support the video tag.
|
|
40
|
+
</video>
|
|
41
|
+
HTML
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Convert HTML <video> to CoreModel::Block (video)
|
|
46
|
+
def self.to_coradoc(element, _options = {})
|
|
47
|
+
# Handle both <video> and <figure><video> structures
|
|
48
|
+
video_elem = if element.name == 'figure'
|
|
49
|
+
element.at_css('video')
|
|
50
|
+
elsif element.name == 'video'
|
|
51
|
+
element
|
|
52
|
+
else
|
|
53
|
+
return nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
return nil unless video_elem
|
|
57
|
+
|
|
58
|
+
# Extract source from <source> tag or src attribute
|
|
59
|
+
src = extract_video_src(video_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 = video_elem['id'] || element['id']
|
|
70
|
+
|
|
71
|
+
# Extract video attributes
|
|
72
|
+
metadata = extract_video_metadata(video_elem)
|
|
73
|
+
metadata[:src] = src
|
|
74
|
+
|
|
75
|
+
Coradoc::CoreModel::Block.new(
|
|
76
|
+
element_type: 'video',
|
|
77
|
+
content: src,
|
|
78
|
+
title: caption,
|
|
79
|
+
id: id,
|
|
80
|
+
metadata: metadata
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def self.build_attributes(video)
|
|
85
|
+
attrs = []
|
|
86
|
+
|
|
87
|
+
# Extract options from metadata
|
|
88
|
+
options = video.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 poster if specified
|
|
104
|
+
poster = video.metadata&.dig(:poster)
|
|
105
|
+
attrs << %( poster="#{escape_attribute(poster)}") if poster
|
|
106
|
+
|
|
107
|
+
# Add width and height if specified
|
|
108
|
+
width = video.metadata&.dig(:width)
|
|
109
|
+
height = video.metadata&.dig(:height)
|
|
110
|
+
|
|
111
|
+
attrs << %( width="#{escape_attribute(width)}") if width
|
|
112
|
+
|
|
113
|
+
attrs << %( height="#{escape_attribute(height)}") if height
|
|
114
|
+
|
|
115
|
+
# Add ID if present
|
|
116
|
+
attrs << %( id="#{escape_attribute(video.id)}") if video.id
|
|
117
|
+
|
|
118
|
+
attrs.join
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def self.build_type_attr(src)
|
|
122
|
+
# Determine video MIME type from extension
|
|
123
|
+
ext = File.extname(src).downcase
|
|
124
|
+
type = case ext
|
|
125
|
+
when '.mp4'
|
|
126
|
+
'video/mp4'
|
|
127
|
+
when '.webm'
|
|
128
|
+
'video/webm'
|
|
129
|
+
when '.ogg', '.ogv'
|
|
130
|
+
'video/ogg'
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
type ? %( type="#{type}") : ''
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def self.build_figure_attrs(video)
|
|
137
|
+
attrs = []
|
|
138
|
+
|
|
139
|
+
# Add ID to figure if present
|
|
140
|
+
attrs << %( id="#{escape_attribute(video.id)}-figure") if video.id
|
|
141
|
+
|
|
142
|
+
attrs.join
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def self.extract_video_src(element)
|
|
146
|
+
# Try to get src from <source> tag first
|
|
147
|
+
source = element.at_css('source')
|
|
148
|
+
return source['src'] if source && source['src']
|
|
149
|
+
|
|
150
|
+
# Fall back to src attribute on <video>
|
|
151
|
+
element['src']
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def self.extract_video_metadata(element)
|
|
155
|
+
metadata = {}
|
|
156
|
+
options = []
|
|
157
|
+
|
|
158
|
+
# Extract boolean attributes
|
|
159
|
+
options << 'controls' if element.has_attribute?('controls')
|
|
160
|
+
options << 'autoplay' if element.has_attribute?('autoplay')
|
|
161
|
+
options << 'loop' if element.has_attribute?('loop')
|
|
162
|
+
options << 'muted' if element.has_attribute?('muted')
|
|
163
|
+
|
|
164
|
+
metadata[:options] = options unless options.empty?
|
|
165
|
+
|
|
166
|
+
# Extract poster
|
|
167
|
+
metadata[:poster] = element['poster'] if element['poster']
|
|
168
|
+
|
|
169
|
+
# Extract dimensions
|
|
170
|
+
metadata[:width] = element['width'] if element['width']
|
|
171
|
+
|
|
172
|
+
metadata[:height] = element['height'] if element['height']
|
|
173
|
+
|
|
174
|
+
metadata
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|