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