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,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Html
5
+ module Converters
6
+ # Converter for CoreModel::Block (open) to HTML
7
+ class Open < Base
8
+ def self.to_html(block, _options = {})
9
+ return '' unless block
10
+
11
+ # Build content
12
+ content = process_content(block.content)
13
+
14
+ # Build attributes
15
+ attrs = build_attributes(block)
16
+
17
+ # Wrap in div with openblock class
18
+ "<div#{attrs}>\n#{content}\n</div>"
19
+ end
20
+
21
+ # Convert HTML div to CoreModel::Block (open)
22
+ def self.to_coradoc(element, _options = {})
23
+ return nil unless element.name == 'div'
24
+
25
+ # Extract content
26
+ content = element.children.map do |node|
27
+ if node.text? && !node.text.strip.empty?
28
+ node.text.strip
29
+ elsif node.element?
30
+ case node.name
31
+ when 'p'
32
+ Paragraph.to_coradoc(node)
33
+ else
34
+ node.text.strip
35
+ end
36
+ end
37
+ end.compact
38
+
39
+ # Extract ID if present
40
+ id = element['id']
41
+
42
+ Coradoc::CoreModel::OpenBlock.new(
43
+ content: content,
44
+ id: id
45
+ )
46
+ end
47
+
48
+ def self.build_attributes(block)
49
+ attrs = []
50
+ attrs << %( class="openblock")
51
+
52
+ attrs << %( id="#{escape_attribute(block.id)}") if block.id
53
+
54
+ attrs.join
55
+ end
56
+
57
+ def self.process_content(content)
58
+ return '' if content.nil?
59
+
60
+ if content.is_a?(Array)
61
+ content.map { |item| convert_item(item) }.join("\n")
62
+ else
63
+ convert_item(content)
64
+ end
65
+ end
66
+
67
+ def self.convert_item(item)
68
+ case item
69
+ when String
70
+ "<p>#{escape_html(item)}</p>"
71
+ else
72
+ convert_content_to_html(item)
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Html
5
+ module Converters
6
+ # Converter for Ordered (numbered) lists
7
+ class Ordered < Base
8
+ class << self
9
+ # Convert HTML <ol> to CoreModel::ListBlock
10
+ # @param node [Nokogiri::XML::Node] HTML node
11
+ # @param state [Hash] Conversion state
12
+ # @return [Coradoc::CoreModel::ListBlock] Ordered 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: 'ordered',
22
+ items: items
23
+ )
24
+ list.id = attrs[:id] if attrs[:id]
25
+
26
+ # Extract start value if present
27
+ list.start = attrs[:start].to_i if attrs[:start]
28
+
29
+ list
30
+ end
31
+
32
+ # Convert CoreModel::ListBlock (ordered) to HTML <ol>
33
+ # @param model [Coradoc::CoreModel::ListBlock] Ordered list model
34
+ # @param state [Hash] Conversion state
35
+ # @return [String] HTML string
36
+ def to_html(model, state = {})
37
+ items_html = model.items.map do |item|
38
+ Coradoc::Html::Converters::ListItem.to_html(item, state)
39
+ end.join("\n")
40
+
41
+ attrs = {}
42
+ attrs[:id] = model.id if model.id
43
+
44
+ # Add start attribute if not starting from 1
45
+ attrs[:start] = model.start if model.start && model.start != 1
46
+
47
+ build_element('ol', "\n#{items_html}\n", attrs)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Html
5
+ module Converters
6
+ # Converter for Paragraph block element
7
+ class Paragraph < Base
8
+ class << self
9
+ # Convert HTML <p> to CoreModel Block
10
+ # @param node [Nokogiri::XML::Node] HTML node
11
+ # @param state [Hash] Conversion state
12
+ # @return [Coradoc::CoreModel::Block] Block model with element_type: paragraph
13
+ def to_coradoc(node, state = {})
14
+ content = treat_children(node, state)
15
+ attrs = extract_node_attributes(node)
16
+
17
+ # Create paragraph block with content
18
+ paragraph = Coradoc::CoreModel::Block.new(
19
+ element_type: 'paragraph',
20
+ children: [content]
21
+ )
22
+
23
+ # Set ID if present
24
+ paragraph.id = attrs[:id] if attrs[:id]
25
+
26
+ paragraph
27
+ end
28
+
29
+ # Convert CoreModel::Block (element_type: paragraph) to HTML <p>
30
+ # @param model [Coradoc::CoreModel::Block] Paragraph block model
31
+ # @param state [Hash] Conversion state
32
+ # @return [String] HTML string
33
+ def to_html(model, state = {})
34
+ # Use renderable_content to handle both content and children
35
+ content_to_render = model.renderable_content
36
+ content = convert_content_to_html(content_to_render, state)
37
+ # Strip trailing whitespace from content to avoid issues with line breaks
38
+ content = content.rstrip
39
+ attributes = extract_model_attributes(model)
40
+ build_element('p', content, attributes)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Html
5
+ module Converters
6
+ # Converter for CoreModel::Block (quote) to HTML <blockquote>
7
+ class Quote < Base
8
+ # Convert CoreModel::Block (quote) to HTML <blockquote>
9
+ def self.to_html(quote, _options = {})
10
+ return '' unless quote
11
+
12
+ # Build blockquote attributes
13
+ attrs = build_attributes(quote)
14
+
15
+ # Process quote content
16
+ content = process_content(quote.content)
17
+
18
+ # Build attribution if present
19
+ attribution = build_attribution(quote)
20
+
21
+ # Combine content and attribution
22
+ quote_html = content
23
+ quote_html += "\n#{attribution}" if attribution
24
+
25
+ "<blockquote#{attrs}>\n#{quote_html}\n</blockquote>"
26
+ end
27
+
28
+ # Convert HTML <blockquote> to CoreModel::Block (quote)
29
+ def self.to_coradoc(element, _options = {})
30
+ return nil unless element.name == 'blockquote'
31
+
32
+ # Extract content - all children except cite/footer
33
+ content_nodes = element.children.reject do |node|
34
+ %w[cite footer].include?(node.name)
35
+ end
36
+
37
+ content = extract_content(content_nodes)
38
+
39
+ # Extract attribution from <cite> or <footer>
40
+ cite_elem = element.at_css('cite, footer')
41
+ attribution = cite_elem&.text&.strip
42
+
43
+ # Extract ID if present
44
+ id = element['id']
45
+
46
+ Coradoc::CoreModel::QuoteBlock.new(
47
+ content: content,
48
+ id: id,
49
+ attribution: attribution
50
+ )
51
+ end
52
+
53
+ def self.build_attributes(quote)
54
+ attrs = []
55
+
56
+ # Add ID if present
57
+ attrs << %( id="#{escape_attribute(quote.id)}") if quote.id
58
+
59
+ attrs.join
60
+ end
61
+
62
+ def self.process_content(content)
63
+ return '' if content.nil?
64
+
65
+ if content.is_a?(Array)
66
+ content.map { |item| convert_item(item) }.join("\n")
67
+ elsif content.is_a?(String)
68
+ "<p>#{escape_html(content)}</p>"
69
+ else
70
+ convert_item(content)
71
+ end
72
+ end
73
+
74
+ def self.convert_item(item)
75
+ case item
76
+ when String
77
+ "<p>#{escape_html(item)}</p>"
78
+ else
79
+ # Use centralized content conversion
80
+ convert_content_to_html(item)
81
+ end
82
+ end
83
+
84
+ def self.build_attribution(quote)
85
+ # Check metadata for attribution
86
+ attribution_text = quote.metadata&.dig(:attribution)
87
+ return nil unless attribution_text
88
+
89
+ attribution_text = attribution_text.to_s.strip
90
+ return nil if attribution_text.empty?
91
+
92
+ %(<footer>#{escape_html(attribution_text)}</footer>)
93
+ end
94
+
95
+ def self.extract_content(nodes)
96
+ # Extract and convert content nodes
97
+ nodes.map do |node|
98
+ if node.text? && !node.text.strip.empty?
99
+ node.text.strip
100
+ elsif node.element?
101
+ case node.name
102
+ when 'p'
103
+ Paragraph.to_coradoc(node)
104
+ else
105
+ node.text.strip
106
+ end
107
+ end
108
+ end.compact
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Html
5
+ module Converters
6
+ # Converter for CoreModel::AnnotationBlock (reviewer comment) to HTML
7
+ class ReviewerComment < Base
8
+ # Convert CoreModel::AnnotationBlock (reviewer comment) to HTML
9
+ def self.to_html(comment, _options = {})
10
+ return '' unless comment
11
+
12
+ # Build attributes
13
+ attrs = build_attributes(comment)
14
+
15
+ # Process content
16
+ content = process_content(comment.content)
17
+
18
+ # Parse reviewer info from metadata
19
+ reviewer_info = extract_reviewer_info(comment.metadata)
20
+
21
+ %(<div#{attrs}>
22
+ <span class="reviewer-note-label">Reviewer Note</span>
23
+ #{reviewer_info}
24
+ <div class="reviewer-note-content">
25
+ #{content}
26
+ </div>
27
+ </div>)
28
+ end
29
+
30
+ def self.build_attributes(comment)
31
+ attrs = [%( class="reviewer-note")]
32
+
33
+ # Add ID if present
34
+ attrs << %( id="#{escape_attribute(comment.id)}") if comment.id
35
+
36
+ attrs.join
37
+ end
38
+
39
+ def self.process_content(content)
40
+ return '' if content.nil?
41
+
42
+ if content.is_a?(String)
43
+ escape_html(content)
44
+ elsif content.is_a?(Array)
45
+ content.map { |item| convert_item(item) }.join("\n")
46
+ else
47
+ convert_item(content)
48
+ end
49
+ end
50
+
51
+ def self.convert_item(item)
52
+ case item
53
+ when String
54
+ escape_html(item)
55
+ else
56
+ convert_content_to_html(item)
57
+ end
58
+ end
59
+
60
+ def self.extract_reviewer_info(metadata)
61
+ return '' if metadata.nil?
62
+
63
+ # Extract reviewer info from metadata
64
+ reviewer = metadata[:reviewer]
65
+ return '' unless reviewer
66
+
67
+ %(<div class="reviewer-note-metadata">
68
+ <span class="metadata-item">reviewer=#{escape_html(reviewer)}</span>
69
+ </div>)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Html
5
+ module Converters
6
+ # Converter for CoreModel::AnnotationBlock (reviewer note) to HTML <aside>
7
+ class ReviewerNote < Base
8
+ # Convert CoreModel::AnnotationBlock (reviewer note) to HTML <aside>
9
+ def self.to_html(reviewer_note, options = {})
10
+ return '' unless reviewer_note
11
+
12
+ # Build aside attributes with reviewer metadata
13
+ attrs = build_attributes(reviewer_note)
14
+
15
+ # Build header with label and visible metadata
16
+ header = build_header(reviewer_note)
17
+
18
+ # Process reviewer note content
19
+ content = process_content(reviewer_note.content, options)
20
+
21
+ %(<aside#{attrs}>\n#{header}#{content}\n</div>\n</aside>)
22
+ end
23
+
24
+ # Convert HTML <aside> with reviewer data to CoreModel::AnnotationBlock
25
+ def self.to_coradoc(element, _options = {})
26
+ return nil unless element.name == 'aside'
27
+ return nil unless element['data-reviewer'] # Must have reviewer attribute
28
+
29
+ # Extract reviewer metadata
30
+ reviewer = element['data-reviewer']
31
+ date = element['data-date']
32
+ from = element['data-from']
33
+ to = element['data-to']
34
+
35
+ # Extract content
36
+ content = extract_content(element.children)
37
+
38
+ Coradoc::CoreModel::AnnotationBlock.new(
39
+ annotation_type: 'reviewer_note',
40
+ content: content,
41
+ metadata: {
42
+ reviewer: reviewer,
43
+ date: date,
44
+ from: from,
45
+ to: to
46
+ }
47
+ )
48
+ end
49
+
50
+ def self.build_header(reviewer_note)
51
+ # Build header with label and visible metadata
52
+ header_parts = []
53
+ header_parts << %(<div class="reviewer-note-header">)
54
+ header_parts << %( <span class="reviewer-note-label">Reviewer's note</span>)
55
+
56
+ # Build metadata display if any metadata exists
57
+ metadata_items = []
58
+ metadata = reviewer_note.metadata || {}
59
+ metadata_items << %(reviewer=#{escape_html(metadata[:reviewer])}) if metadata[:reviewer]
60
+ metadata_items << %(date=#{escape_html(metadata[:date])}) if metadata[:date]
61
+ metadata_items << %(from=#{escape_html(metadata[:from])}) if metadata[:from]
62
+ metadata_items << %(to=#{escape_html(metadata[:to])}) if metadata[:to]
63
+
64
+ unless metadata_items.empty?
65
+ header_parts << %( <div class="reviewer-note-metadata">)
66
+ metadata_items.each do |item|
67
+ header_parts << %( <span class="metadata-item">#{item}</span>)
68
+ end
69
+ header_parts << %( </div>)
70
+ end
71
+
72
+ header_parts << %(</div>)
73
+ header_parts << %(<div class="reviewer-note-content">)
74
+
75
+ header_parts.join("\n")
76
+ end
77
+
78
+ def self.build_attributes(reviewer_note)
79
+ attrs = [%( class="reviewer-note")]
80
+
81
+ # Add reviewer metadata as data attributes
82
+ metadata = reviewer_note.metadata || {}
83
+
84
+ attrs << %( data-reviewer="#{escape_html(metadata[:reviewer])}") if metadata[:reviewer]
85
+
86
+ attrs << %( data-date="#{escape_html(metadata[:date])}") if metadata[:date]
87
+
88
+ attrs << %( data-from="#{escape_html(metadata[:from])}") if metadata[:from]
89
+
90
+ attrs << %( data-to="#{escape_html(metadata[:to])}") if metadata[:to]
91
+
92
+ attrs.join
93
+ end
94
+
95
+ def self.process_content(content, options = {})
96
+ return '' if content.nil?
97
+
98
+ if content.is_a?(Array)
99
+ content.map { |item| convert_item(item, options) }.join("\n")
100
+ elsif content.is_a?(String)
101
+ "<p>#{escape_html(content)}</p>"
102
+ else
103
+ convert_item(content, options)
104
+ end
105
+ end
106
+
107
+ def self.convert_item(item, _options = {})
108
+ case item
109
+ when String
110
+ "<p>#{escape_html(item)}</p>"
111
+ else
112
+ convert_content_to_html(item)
113
+ end
114
+ end
115
+
116
+ def self.extract_content(nodes)
117
+ # Extract and convert content nodes
118
+ nodes.map do |node|
119
+ if node.text? && !node.text.strip.empty?
120
+ node.text.strip
121
+ elsif node.element?
122
+ case node.name
123
+ when 'p'
124
+ Paragraph.to_coradoc(node)
125
+ else
126
+ node.text.strip
127
+ end
128
+ end
129
+ end.compact
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Html
5
+ module Converters
6
+ # Converter for Section structural element
7
+ class Section < Base
8
+ class << self
9
+ # Convert HTML <section> to CoreModel::StructuralElement
10
+ # @param node [Nokogiri::XML::Node] HTML section node
11
+ # @param state [Hash] Conversion state
12
+ # @return [Coradoc::CoreModel::StructuralElement] Section model
13
+ def to_coradoc(node, state = {})
14
+ # Extract section title from heading
15
+ title_node = node.at('h1, h2, h3, h4, h5, h6')
16
+ title = title_node&.text&.strip
17
+ level = title_node ? title_node.name[1].to_i : 1
18
+
19
+ # Extract attributes
20
+ attrs = extract_node_attributes(node)
21
+
22
+ # Process children (skip the heading as we already extracted it)
23
+ child_nodes = node.children.reject { |child| child.name =~ /^h[1-6]$/ }
24
+ children = child_nodes.flat_map do |child|
25
+ convert_node_to_core(child, state)
26
+ end.compact
27
+
28
+ # Create CoreModel section
29
+ section = Coradoc::CoreModel::StructuralElement.new(
30
+ element_type: 'section',
31
+ level: level,
32
+ title: title,
33
+ children: children
34
+ )
35
+
36
+ # Set ID if present
37
+ section.id = attrs[:id] if attrs[:id]
38
+
39
+ section
40
+ end
41
+
42
+ # Convert CoreModel::StructuralElement to HTML <section>
43
+ # @param model [Coradoc::CoreModel::StructuralElement] Section model
44
+ # @param state [Hash] Conversion state
45
+ # @return [String] HTML string
46
+ def to_html(model, state = {})
47
+ parts = []
48
+
49
+ # Add title as heading
50
+ if model.title
51
+ # Calculate heading level (level 0 -> h1, level 1 -> h2, etc.)
52
+ level = model.level || 1
53
+ heading_level = [[level + 1, 1].max, 6].min # Clamp between h1-h6
54
+ heading_tag = "h#{heading_level}"
55
+
56
+ title_text = escape_html(model.title)
57
+
58
+ title_attrs = {}
59
+ title_attrs[:id] = model.id if model.id
60
+
61
+ parts << build_element(heading_tag, title_text, title_attrs)
62
+ end
63
+
64
+ # Add section children (paragraphs, lists, nested sections, etc.)
65
+ model.children&.each do |child|
66
+ parts << convert_content_to_html(child, state)
67
+ end
68
+
69
+ # Wrap in section tag
70
+ content = parts.join("\n")
71
+ attributes = extract_section_attributes(model)
72
+ build_element('section', content, attributes)
73
+ end
74
+
75
+ private
76
+
77
+ # Extract section-level attributes
78
+ def extract_section_attributes(model)
79
+ attrs = {}
80
+
81
+ # Don't duplicate ID if already on heading
82
+ attrs[:id] = model.id if !(model.title && model.id) && model.id
83
+
84
+ attrs
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Html
5
+ module Converters
6
+ # Converter for CoreModel::Block (sidebar) to HTML <aside>
7
+ class Sidebar < Base
8
+ # Convert CoreModel::Block (sidebar) to HTML <aside>
9
+ def self.to_html(sidebar, _options = {})
10
+ return '' unless sidebar
11
+
12
+ # Build aside attributes
13
+ attrs = build_attributes(sidebar)
14
+
15
+ # Build title if present
16
+ title_html = build_title(sidebar)
17
+
18
+ # Process sidebar content
19
+ content = process_content(sidebar.content)
20
+
21
+ # Combine title and content
22
+ sidebar_html = ''
23
+ sidebar_html += "#{title_html}\n" if title_html
24
+ sidebar_html += content
25
+
26
+ %(<aside#{attrs}>\n#{sidebar_html}\n</aside>)
27
+ end
28
+
29
+ # Convert HTML <aside> to CoreModel::Block (sidebar)
30
+ def self.to_coradoc(element, _options = {})
31
+ return nil unless element.name == 'aside'
32
+
33
+ # Extract title if present
34
+ title_elem = element.at_css('.sidebar-title, h1, h2, h3, h4, h5, h6')
35
+ title = title_elem&.text&.strip
36
+
37
+ # Extract content - all children except title
38
+ content_nodes = if title_elem
39
+ element.children.reject { |node| node == title_elem }
40
+ else
41
+ element.children
42
+ end
43
+
44
+ content = extract_content(content_nodes)
45
+
46
+ # Extract ID if present
47
+ id = element['id']
48
+
49
+ Coradoc::CoreModel::SidebarBlock.new(
50
+ content: content,
51
+ title: title,
52
+ id: id
53
+ )
54
+ end
55
+
56
+ def self.build_attributes(sidebar)
57
+ attrs = [%( class="sidebar")]
58
+
59
+ # Add ID if present
60
+ attrs << %( id="#{escape_attribute(sidebar.id)}") if sidebar.id
61
+
62
+ attrs.join
63
+ end
64
+
65
+ def self.build_title(sidebar)
66
+ return nil unless sidebar.title
67
+
68
+ title_text = sidebar.title.to_s
69
+ return nil if title_text.empty?
70
+
71
+ %(<div class="sidebar-title">#{escape_html(title_text)}</div>)
72
+ end
73
+
74
+ def self.process_content(content)
75
+ return '' if content.nil?
76
+
77
+ if content.is_a?(Array)
78
+ content.map { |item| convert_item(item) }.join("\n")
79
+ elsif content.is_a?(String)
80
+ "<p>#{escape_html(content)}</p>"
81
+ else
82
+ convert_item(content)
83
+ end
84
+ end
85
+
86
+ def self.convert_item(item)
87
+ case item
88
+ when String
89
+ "<p>#{escape_html(item)}</p>"
90
+ else
91
+ convert_content_to_html(item)
92
+ end
93
+ end
94
+
95
+ def self.extract_content(nodes)
96
+ # Extract and convert content nodes
97
+ nodes.map do |node|
98
+ if node.text? && !node.text.strip.empty?
99
+ node.text.strip
100
+ elsif node.element?
101
+ case node.name
102
+ when 'p'
103
+ Paragraph.to_coradoc(node)
104
+ else
105
+ node.text.strip
106
+ end
107
+ end
108
+ end.compact
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end