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