prosereflect 0.1.1 → 0.3.0
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 +4 -4
- data/.github/workflows/docs.yml +63 -0
- data/.github/workflows/links.yml +97 -0
- data/.github/workflows/rake.yml +4 -0
- data/.github/workflows/release.yml +5 -0
- data/.gitignore +4 -0
- data/.rubocop.yml +19 -1
- data/.rubocop_todo.yml +119 -183
- data/CLAUDE.md +78 -0
- data/Gemfile +8 -4
- data/README.adoc +2 -0
- data/Rakefile +3 -3
- data/docs/Gemfile +10 -0
- data/docs/INDEX.adoc +45 -0
- data/docs/_advanced/index.adoc +15 -0
- data/docs/_advanced/schema.adoc +112 -0
- data/docs/_advanced/step-map.adoc +66 -0
- data/docs/_advanced/steps.adoc +88 -0
- data/docs/_advanced/test-builder.adoc +61 -0
- data/docs/_advanced/transform.adoc +92 -0
- data/docs/_config.yml +174 -0
- data/docs/_features/html-input.adoc +69 -0
- data/docs/_features/html-output.adoc +45 -0
- data/docs/_features/index.adoc +15 -0
- data/docs/_features/marks.adoc +86 -0
- data/docs/_features/node-types.adoc +124 -0
- data/docs/_features/user-mentions.adoc +47 -0
- data/docs/_guides/custom-nodes.adoc +107 -0
- data/docs/_guides/index.adoc +13 -0
- data/docs/_guides/round-trip-html.adoc +91 -0
- data/docs/_guides/serialization.adoc +109 -0
- data/docs/_pages/index.adoc +67 -0
- data/docs/_reference/document-api.adoc +49 -0
- data/docs/_reference/index.adoc +14 -0
- data/docs/_reference/node-api.adoc +79 -0
- data/docs/_reference/schema-api.adoc +95 -0
- data/docs/_reference/transform-api.adoc +77 -0
- data/docs/_understanding/document-model.adoc +65 -0
- data/docs/_understanding/fragment.adoc +52 -0
- data/docs/_understanding/index.adoc +14 -0
- data/docs/_understanding/resolved-position.adoc +53 -0
- data/docs/_understanding/slice.adoc +54 -0
- data/docs/lychee.toml +63 -0
- data/lib/prosereflect/attribute/base.rb +4 -6
- data/lib/prosereflect/attribute/bold.rb +2 -4
- data/lib/prosereflect/attribute/href.rb +1 -3
- data/lib/prosereflect/attribute/id.rb +7 -7
- data/lib/prosereflect/attribute.rb +4 -7
- data/lib/prosereflect/blockquote.rb +19 -11
- data/lib/prosereflect/bullet_list.rb +36 -29
- data/lib/prosereflect/code_block.rb +23 -27
- data/lib/prosereflect/code_block_wrapper.rb +12 -13
- data/lib/prosereflect/document.rb +14 -22
- data/lib/prosereflect/fragment.rb +249 -0
- data/lib/prosereflect/hard_break.rb +6 -6
- data/lib/prosereflect/heading.rb +14 -15
- data/lib/prosereflect/horizontal_rule.rb +23 -14
- data/lib/prosereflect/image.rb +32 -23
- data/lib/prosereflect/input/html.rb +179 -104
- data/lib/prosereflect/input.rb +7 -0
- data/lib/prosereflect/list_item.rb +11 -12
- data/lib/prosereflect/mark/base.rb +9 -11
- data/lib/prosereflect/mark/bold.rb +1 -3
- data/lib/prosereflect/mark/code.rb +1 -3
- data/lib/prosereflect/mark/italic.rb +1 -3
- data/lib/prosereflect/mark/link.rb +1 -3
- data/lib/prosereflect/mark/strike.rb +1 -3
- data/lib/prosereflect/mark/subscript.rb +1 -3
- data/lib/prosereflect/mark/superscript.rb +1 -3
- data/lib/prosereflect/mark/underline.rb +1 -3
- data/lib/prosereflect/mark.rb +9 -5
- data/lib/prosereflect/node.rb +171 -33
- data/lib/prosereflect/ordered_list.rb +17 -14
- data/lib/prosereflect/output/html.rb +279 -50
- data/lib/prosereflect/output.rb +7 -0
- data/lib/prosereflect/paragraph.rb +11 -13
- data/lib/prosereflect/parser.rb +56 -66
- data/lib/prosereflect/resolved_pos.rb +256 -0
- data/lib/prosereflect/schema/attribute.rb +57 -0
- data/lib/prosereflect/schema/content_match.rb +656 -0
- data/lib/prosereflect/schema/fragment.rb +166 -0
- data/lib/prosereflect/schema/mark.rb +121 -0
- data/lib/prosereflect/schema/mark_type.rb +130 -0
- data/lib/prosereflect/schema/node.rb +236 -0
- data/lib/prosereflect/schema/node_type.rb +274 -0
- data/lib/prosereflect/schema/schema_main.rb +190 -0
- data/lib/prosereflect/schema/spec.rb +92 -0
- data/lib/prosereflect/schema.rb +39 -0
- data/lib/prosereflect/table.rb +12 -13
- data/lib/prosereflect/table_cell.rb +13 -13
- data/lib/prosereflect/table_header.rb +17 -17
- data/lib/prosereflect/table_row.rb +12 -12
- data/lib/prosereflect/text.rb +35 -11
- data/lib/prosereflect/transform/attr_step.rb +157 -0
- data/lib/prosereflect/transform/insert_step.rb +115 -0
- data/lib/prosereflect/transform/mapping.rb +82 -0
- data/lib/prosereflect/transform/mark_step.rb +269 -0
- data/lib/prosereflect/transform/replace_around_step.rb +181 -0
- data/lib/prosereflect/transform/replace_step.rb +157 -0
- data/lib/prosereflect/transform/slice.rb +91 -0
- data/lib/prosereflect/transform/step.rb +89 -0
- data/lib/prosereflect/transform/step_map.rb +126 -0
- data/lib/prosereflect/transform/structure.rb +120 -0
- data/lib/prosereflect/transform/transform.rb +341 -0
- data/lib/prosereflect/transform.rb +26 -0
- data/lib/prosereflect/user.rb +15 -15
- data/lib/prosereflect/version.rb +1 -1
- data/lib/prosereflect.rb +30 -17
- data/prosereflect.gemspec +17 -16
- data/spec/fixtures/documents/formatted_text.yaml +14 -0
- data/spec/fixtures/documents/heading_paragraph.yaml +16 -0
- data/spec/fixtures/documents/lists_doc.yaml +32 -0
- data/spec/fixtures/documents/mixed_content.yaml +40 -0
- data/spec/fixtures/documents/nested_doc.yaml +20 -0
- data/spec/fixtures/documents/simple_doc.yaml +6 -0
- data/spec/fixtures/documents/table_doc.yaml +32 -0
- data/spec/fixtures/documents/transform_test.yaml +14 -0
- data/spec/fixtures/schema/custom_schema.rb +37 -0
- data/spec/fixtures/schema/test_schema.rb +46 -0
- data/spec/fixtures/test_builder/helpers.rb +212 -0
- data/spec/prosereflect/document_spec.rb +332 -330
- data/spec/prosereflect/fragment_spec.rb +273 -0
- data/spec/prosereflect/hard_break_spec.rb +125 -125
- data/spec/prosereflect/input/html_spec.rb +718 -522
- data/spec/prosereflect/node_spec.rb +311 -182
- data/spec/prosereflect/output/html_spec.rb +105 -105
- data/spec/prosereflect/output/whitespace_spec.rb +248 -0
- data/spec/prosereflect/paragraph_spec.rb +275 -274
- data/spec/prosereflect/parser/round_trip_spec.rb +472 -0
- data/spec/prosereflect/parser_spec.rb +185 -180
- data/spec/prosereflect/resolved_pos_spec.rb +74 -0
- data/spec/prosereflect/schema/conftest.rb +68 -0
- data/spec/prosereflect/schema/content_match_spec.rb +237 -0
- data/spec/prosereflect/schema/mark_spec.rb +274 -0
- data/spec/prosereflect/schema/mark_type_spec.rb +86 -0
- data/spec/prosereflect/schema/node_type_spec.rb +142 -0
- data/spec/prosereflect/schema/schema_spec.rb +194 -0
- data/spec/prosereflect/table_cell_spec.rb +183 -183
- data/spec/prosereflect/table_row_spec.rb +149 -149
- data/spec/prosereflect/table_spec.rb +320 -318
- data/spec/prosereflect/test_builder/marks_spec.rb +127 -0
- data/spec/prosereflect/text_spec.rb +133 -132
- data/spec/prosereflect/transform/equivalence_spec.rb +487 -0
- data/spec/prosereflect/transform/mapping_spec.rb +226 -0
- data/spec/prosereflect/transform/replace_spec.rb +832 -0
- data/spec/prosereflect/transform/replace_step_spec.rb +157 -0
- data/spec/prosereflect/transform/slice_spec.rb +48 -0
- data/spec/prosereflect/transform/step_map_spec.rb +70 -0
- data/spec/prosereflect/transform/step_spec.rb +211 -0
- data/spec/prosereflect/transform/structure_spec.rb +98 -0
- data/spec/prosereflect/transform/transform_spec.rb +238 -0
- data/spec/prosereflect/user_spec.rb +31 -28
- data/spec/prosereflect_spec.rb +28 -26
- data/spec/spec_helper.rb +7 -6
- data/spec/support/matchers.rb +6 -6
- data/spec/support/shared_examples.rb +49 -49
- metadata +96 -5
- data/spec/prosereflect/version_spec.rb +0 -11
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require_relative '../document'
|
|
3
|
+
require "nokogiri"
|
|
5
4
|
|
|
6
5
|
module Prosereflect
|
|
7
6
|
module Output
|
|
@@ -16,7 +15,7 @@ module Prosereflect
|
|
|
16
15
|
end
|
|
17
16
|
|
|
18
17
|
doc = Nokogiri::HTML(builder.to_html)
|
|
19
|
-
html = doc.at_css(
|
|
18
|
+
html = doc.at_css("div").children.to_html
|
|
20
19
|
|
|
21
20
|
code_blocks = {}
|
|
22
21
|
html.scan(%r{<code[^>]*>(.*?)</code>}m).each_with_index do |match, i|
|
|
@@ -27,7 +26,7 @@ module Prosereflect
|
|
|
27
26
|
end
|
|
28
27
|
|
|
29
28
|
# Remove newlines and spaces
|
|
30
|
-
html = html.gsub(/\n\s*/,
|
|
29
|
+
html = html.gsub(/\n\s*/, "")
|
|
31
30
|
|
|
32
31
|
code_blocks.each do |placeholder, content|
|
|
33
32
|
html.sub!(placeholder, content)
|
|
@@ -36,6 +35,31 @@ module Prosereflect
|
|
|
36
35
|
html
|
|
37
36
|
end
|
|
38
37
|
|
|
38
|
+
# Render document with options
|
|
39
|
+
def render(document, options = {})
|
|
40
|
+
options = {
|
|
41
|
+
document: true,
|
|
42
|
+
text: ->(text, _marks) { text },
|
|
43
|
+
mark: ->(_mark, content) { content },
|
|
44
|
+
node: ->(_node, content) { content },
|
|
45
|
+
}.merge(options)
|
|
46
|
+
|
|
47
|
+
serializer = DOMSerializer.new(document.schema, options)
|
|
48
|
+
serializer.serialize(document)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Render single node with marks
|
|
52
|
+
def render_node(node, options = {})
|
|
53
|
+
serializer = DOMSerializer.new(nil, options)
|
|
54
|
+
serializer.render_node(node)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Render text with marks applied
|
|
58
|
+
def render_text(text, marks, options = {})
|
|
59
|
+
serializer = DOMSerializer.new(nil, options)
|
|
60
|
+
serializer.render_text(text, marks)
|
|
61
|
+
end
|
|
62
|
+
|
|
39
63
|
private
|
|
40
64
|
|
|
41
65
|
# Process a node and its children
|
|
@@ -43,41 +67,41 @@ module Prosereflect
|
|
|
43
67
|
return unless node
|
|
44
68
|
|
|
45
69
|
case node.type
|
|
46
|
-
when
|
|
70
|
+
when "doc"
|
|
47
71
|
process_document(node, builder)
|
|
48
|
-
when
|
|
72
|
+
when "paragraph"
|
|
49
73
|
process_paragraph(node, builder)
|
|
50
|
-
when
|
|
74
|
+
when "heading"
|
|
51
75
|
process_heading(node, builder)
|
|
52
|
-
when
|
|
76
|
+
when "text"
|
|
53
77
|
process_text(node, builder)
|
|
54
|
-
when
|
|
78
|
+
when "table"
|
|
55
79
|
process_table(node, builder)
|
|
56
|
-
when
|
|
80
|
+
when "table_row"
|
|
57
81
|
process_table_row(node, builder)
|
|
58
|
-
when
|
|
82
|
+
when "table_cell"
|
|
59
83
|
process_table_cell(node, builder)
|
|
60
|
-
when
|
|
84
|
+
when "table_header"
|
|
61
85
|
process_table_header(node, builder)
|
|
62
|
-
when
|
|
86
|
+
when "hard_break"
|
|
63
87
|
builder.br
|
|
64
|
-
when
|
|
88
|
+
when "image"
|
|
65
89
|
process_image(node, builder)
|
|
66
|
-
when
|
|
90
|
+
when "user"
|
|
67
91
|
process_user(node, builder)
|
|
68
|
-
when
|
|
92
|
+
when "bullet_list"
|
|
69
93
|
process_bullet_list(node, builder)
|
|
70
|
-
when
|
|
94
|
+
when "ordered_list"
|
|
71
95
|
process_ordered_list(node, builder)
|
|
72
|
-
when
|
|
96
|
+
when "list_item"
|
|
73
97
|
process_list_item(node, builder)
|
|
74
|
-
when
|
|
98
|
+
when "blockquote"
|
|
75
99
|
process_blockquote(node, builder)
|
|
76
|
-
when
|
|
100
|
+
when "horizontal_rule"
|
|
77
101
|
process_horizontal_rule(node, builder)
|
|
78
|
-
when
|
|
102
|
+
when "code_block_wrapper"
|
|
79
103
|
process_code_block_wrapper(node, builder)
|
|
80
|
-
when
|
|
104
|
+
when "code_block"
|
|
81
105
|
process_code_block(node, builder)
|
|
82
106
|
else
|
|
83
107
|
# Default handling for unknown nodes - treat as a container
|
|
@@ -124,27 +148,27 @@ module Prosereflect
|
|
|
124
148
|
remaining_marks = marks[1..]
|
|
125
149
|
|
|
126
150
|
mark_type = if current_mark.is_a?(Hash)
|
|
127
|
-
current_mark[
|
|
151
|
+
current_mark["type"]
|
|
128
152
|
elsif current_mark.respond_to?(:type)
|
|
129
153
|
current_mark.type
|
|
130
154
|
else
|
|
131
|
-
|
|
155
|
+
"unknown"
|
|
132
156
|
end
|
|
133
157
|
|
|
134
158
|
case mark_type
|
|
135
|
-
when
|
|
159
|
+
when "bold"
|
|
136
160
|
builder.strong do
|
|
137
161
|
apply_marks(text, remaining_marks, builder)
|
|
138
162
|
end
|
|
139
|
-
when
|
|
163
|
+
when "italic"
|
|
140
164
|
builder.em do
|
|
141
165
|
apply_marks(text, remaining_marks, builder)
|
|
142
166
|
end
|
|
143
|
-
when
|
|
167
|
+
when "code"
|
|
144
168
|
builder.code do
|
|
145
169
|
apply_marks(text, remaining_marks, builder)
|
|
146
170
|
end
|
|
147
|
-
when
|
|
171
|
+
when "link"
|
|
148
172
|
href = find_href_attribute(current_mark)
|
|
149
173
|
if href
|
|
150
174
|
builder.a(href: href) do
|
|
@@ -153,19 +177,19 @@ module Prosereflect
|
|
|
153
177
|
else
|
|
154
178
|
apply_marks(text, remaining_marks, builder)
|
|
155
179
|
end
|
|
156
|
-
when
|
|
180
|
+
when "strike"
|
|
157
181
|
builder.del do
|
|
158
182
|
apply_marks(text, remaining_marks, builder)
|
|
159
183
|
end
|
|
160
|
-
when
|
|
184
|
+
when "underline"
|
|
161
185
|
builder.u do
|
|
162
186
|
apply_marks(text, remaining_marks, builder)
|
|
163
187
|
end
|
|
164
|
-
when
|
|
188
|
+
when "subscript"
|
|
165
189
|
builder.sub do
|
|
166
190
|
apply_marks(text, remaining_marks, builder)
|
|
167
191
|
end
|
|
168
|
-
when
|
|
192
|
+
when "superscript"
|
|
169
193
|
builder.sup do
|
|
170
194
|
apply_marks(text, remaining_marks, builder)
|
|
171
195
|
end
|
|
@@ -178,23 +202,23 @@ module Prosereflect
|
|
|
178
202
|
# Find href attribute in a link mark
|
|
179
203
|
def find_href_attribute(mark)
|
|
180
204
|
if mark.is_a?(Hash)
|
|
181
|
-
if mark[
|
|
182
|
-
return mark[
|
|
183
|
-
elsif mark[
|
|
184
|
-
href_attr = mark[
|
|
185
|
-
return href_attr[
|
|
205
|
+
if mark["attrs"].is_a?(Hash)
|
|
206
|
+
return mark["attrs"]["href"]
|
|
207
|
+
elsif mark["attrs"].is_a?(Array)
|
|
208
|
+
href_attr = mark["attrs"].find { |a| a.is_a?(Prosereflect::Attribute::Href) || (a.is_a?(Hash) && a["type"] == "href") }
|
|
209
|
+
return href_attr["href"] if href_attr.is_a?(Hash) && href_attr["href"]
|
|
186
210
|
return href_attr.href if href_attr.respond_to?(:href)
|
|
187
211
|
end
|
|
188
212
|
elsif mark.respond_to?(:attrs)
|
|
189
213
|
attrs = mark.attrs
|
|
190
214
|
if attrs.is_a?(Hash)
|
|
191
|
-
return attrs[
|
|
215
|
+
return attrs["href"]
|
|
192
216
|
elsif attrs.is_a?(Array)
|
|
193
217
|
href_attr = attrs.find { |attr| attr.is_a?(Prosereflect::Attribute::Href) }
|
|
194
218
|
return href_attr&.href if href_attr
|
|
195
219
|
|
|
196
|
-
hash_attr = attrs.find { |attr| attr.is_a?(Hash) && attr[
|
|
197
|
-
return hash_attr[
|
|
220
|
+
hash_attr = attrs.find { |attr| attr.is_a?(Hash) && attr["href"] }
|
|
221
|
+
return hash_attr["href"] if hash_attr
|
|
198
222
|
end
|
|
199
223
|
end
|
|
200
224
|
nil
|
|
@@ -206,7 +230,9 @@ module Prosereflect
|
|
|
206
230
|
rows = node.rows || node.content
|
|
207
231
|
return if rows.empty?
|
|
208
232
|
|
|
209
|
-
has_header = rows.first&.content&.any?
|
|
233
|
+
has_header = rows.first&.content&.any? do |cell|
|
|
234
|
+
cell.type == "table_header"
|
|
235
|
+
end
|
|
210
236
|
|
|
211
237
|
if has_header
|
|
212
238
|
builder.thead do
|
|
@@ -233,7 +259,7 @@ module Prosereflect
|
|
|
233
259
|
# Process a table cell
|
|
234
260
|
def process_table_cell(node, builder)
|
|
235
261
|
builder.td do
|
|
236
|
-
if node.content&.size == 1 && node.content.first.type ==
|
|
262
|
+
if node.content&.size == 1 && node.content.first.type == "paragraph"
|
|
237
263
|
node.content.first.content&.each do |child|
|
|
238
264
|
process_node(child, builder)
|
|
239
265
|
end
|
|
@@ -251,7 +277,7 @@ module Prosereflect
|
|
|
251
277
|
attrs[:colspan] = node.colspan if node.colspan
|
|
252
278
|
|
|
253
279
|
builder.th(attrs) do
|
|
254
|
-
if node.content&.size == 1 && node.content.first.type ==
|
|
280
|
+
if node.content&.size == 1 && node.content.first.type == "paragraph"
|
|
255
281
|
node.content.first.content&.each do |child|
|
|
256
282
|
process_node(child, builder)
|
|
257
283
|
end
|
|
@@ -265,7 +291,7 @@ module Prosereflect
|
|
|
265
291
|
def process_image(node, builder)
|
|
266
292
|
attrs = {
|
|
267
293
|
src: node.src,
|
|
268
|
-
alt: node.alt
|
|
294
|
+
alt: node.alt,
|
|
269
295
|
}
|
|
270
296
|
attrs[:title] = node.title if node.title
|
|
271
297
|
attrs[:width] = node.width if node.width
|
|
@@ -283,7 +309,7 @@ module Prosereflect
|
|
|
283
309
|
def process_bullet_list(node, builder)
|
|
284
310
|
builder.ul do
|
|
285
311
|
node.content&.each do |child|
|
|
286
|
-
if child.type ==
|
|
312
|
+
if child.type == "list_item"
|
|
287
313
|
process_node(child, builder)
|
|
288
314
|
else
|
|
289
315
|
builder.li do
|
|
@@ -330,7 +356,7 @@ module Prosereflect
|
|
|
330
356
|
attrs[:style] << "border-style: #{node.style}" if node.style
|
|
331
357
|
attrs[:style] << "width: #{node.width}" if node.width
|
|
332
358
|
attrs[:style] << "border-width: #{node.thickness}px" if node.thickness
|
|
333
|
-
attrs[:style] = attrs[:style].join(
|
|
359
|
+
attrs[:style] = attrs[:style].join("; ") unless attrs[:style].empty?
|
|
334
360
|
|
|
335
361
|
builder.hr(attrs)
|
|
336
362
|
end
|
|
@@ -339,9 +365,10 @@ module Prosereflect
|
|
|
339
365
|
def process_code_block_wrapper(node, builder)
|
|
340
366
|
attrs = {}
|
|
341
367
|
if node.attrs
|
|
342
|
-
attrs[
|
|
343
|
-
if node.attrs[
|
|
344
|
-
attrs[
|
|
368
|
+
attrs["data-line-numbers"] = "true" if node.attrs["line_numbers"]
|
|
369
|
+
if node.attrs["highlight_lines"].is_a?(Array) && !node.attrs["highlight_lines"].empty? && node.attrs["highlight_lines"] != [0]
|
|
370
|
+
attrs["data-highlight-lines"] =
|
|
371
|
+
node.attrs["highlight_lines"].join(",")
|
|
345
372
|
end
|
|
346
373
|
end
|
|
347
374
|
|
|
@@ -353,7 +380,7 @@ module Prosereflect
|
|
|
353
380
|
# Process a code block node
|
|
354
381
|
def process_code_block(node, builder)
|
|
355
382
|
attrs = {}
|
|
356
|
-
attrs[
|
|
383
|
+
attrs["class"] = "language-#{node.language}" if node.language
|
|
357
384
|
|
|
358
385
|
builder.code(attrs) do
|
|
359
386
|
builder.text node.content
|
|
@@ -370,5 +397,207 @@ module Prosereflect
|
|
|
370
397
|
end
|
|
371
398
|
end
|
|
372
399
|
end
|
|
400
|
+
|
|
401
|
+
# DOMSerializer provides configurable document serialization to HTML
|
|
402
|
+
class DOMSerializer
|
|
403
|
+
attr_reader :schema, :options, :marks
|
|
404
|
+
|
|
405
|
+
def initialize(schema, options = {})
|
|
406
|
+
@schema = schema
|
|
407
|
+
@options = options
|
|
408
|
+
@marks = build_mark_serializers
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def serialize(document)
|
|
412
|
+
render_node(document)
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def serialize_node(node)
|
|
416
|
+
render_node(node)
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def render_node(node)
|
|
420
|
+
return render_text(node.text, node.marks) if node.text?
|
|
421
|
+
|
|
422
|
+
builder = Nokogiri::HTML::Builder.new
|
|
423
|
+
render_node_to_builder(node, builder)
|
|
424
|
+
builder.doc.root.children.to_html
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def render_node_to_builder(node, builder)
|
|
428
|
+
content = render_node_content(node)
|
|
429
|
+
wrap_node(node, content, builder)
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def render_text(text, node_marks = nil)
|
|
433
|
+
marks_to_apply = node_marks || []
|
|
434
|
+
marks_to_apply.each do |mark|
|
|
435
|
+
text = apply_mark(mark, text)
|
|
436
|
+
end
|
|
437
|
+
text
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def apply_mark(mark, content)
|
|
441
|
+
mark_handler = @marks[mark.type]
|
|
442
|
+
return content unless mark_handler
|
|
443
|
+
|
|
444
|
+
case mark.type
|
|
445
|
+
when "bold"
|
|
446
|
+
"<strong>#{content}</strong>"
|
|
447
|
+
when "italic"
|
|
448
|
+
"<em>#{content}</em>"
|
|
449
|
+
when "code"
|
|
450
|
+
"<code>#{content}</code>"
|
|
451
|
+
when "link"
|
|
452
|
+
href = extract_mark_attr(mark, "href")
|
|
453
|
+
"<a href=\"#{href}\">#{content}</a>"
|
|
454
|
+
when "strike"
|
|
455
|
+
"<del>#{content}</del>"
|
|
456
|
+
when "underline"
|
|
457
|
+
"<u>#{content}</u>"
|
|
458
|
+
when "subscript"
|
|
459
|
+
"<sub>#{content}</sub>"
|
|
460
|
+
when "superscript"
|
|
461
|
+
"<sup>#{content}</sup>"
|
|
462
|
+
else
|
|
463
|
+
content
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
private
|
|
468
|
+
|
|
469
|
+
def build_mark_serializers
|
|
470
|
+
return {} unless @schema
|
|
471
|
+
|
|
472
|
+
@schema.marks.transform_values do |_mark_type|
|
|
473
|
+
->(mark, content) { apply_mark(mark, content) }
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def extract_mark_attr(mark, attr_name)
|
|
478
|
+
return nil unless mark.respond_to?(:attrs)
|
|
479
|
+
|
|
480
|
+
attrs = mark.attrs
|
|
481
|
+
return nil unless attrs.is_a?(Hash)
|
|
482
|
+
|
|
483
|
+
attrs[attr_name]
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def render_node_content(node)
|
|
487
|
+
return render_text(node.text, node.marks) if node.text?
|
|
488
|
+
|
|
489
|
+
children = node.content.map { |child| render_node(child) }.join
|
|
490
|
+
apply_node_marks(node, children)
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
def apply_node_marks(node, content)
|
|
494
|
+
return content unless node.marks && !node.marks.empty?
|
|
495
|
+
|
|
496
|
+
node.marks.reverse_each do |mark|
|
|
497
|
+
content = apply_mark(mark, content)
|
|
498
|
+
end
|
|
499
|
+
content
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def wrap_node(node, content, builder)
|
|
503
|
+
tag_name = node_tag_name(node)
|
|
504
|
+
return builder << content unless tag_name
|
|
505
|
+
|
|
506
|
+
builder.tag(tag_name, wrap_attrs(node)) do
|
|
507
|
+
builder << content
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
def node_tag_name(node)
|
|
512
|
+
case node.type
|
|
513
|
+
when "paragraph" then "p"
|
|
514
|
+
when "heading" then "h#{node.attrs[:level] || 1}"
|
|
515
|
+
when "table" then "table"
|
|
516
|
+
when "table_row" then "tr"
|
|
517
|
+
when "table_cell" then "td"
|
|
518
|
+
when "table_header" then "th"
|
|
519
|
+
when "bullet_list" then "ul"
|
|
520
|
+
when "ordered_list" then "ol"
|
|
521
|
+
when "list_item" then "li"
|
|
522
|
+
when "blockquote" then "blockquote"
|
|
523
|
+
when "hard_break" then "br"
|
|
524
|
+
when "horizontal_rule" then "hr"
|
|
525
|
+
when "code_block_wrapper" then "pre"
|
|
526
|
+
when "code_block" then "code"
|
|
527
|
+
when "image" then "img"
|
|
528
|
+
when "doc", "text", "user"
|
|
529
|
+
nil
|
|
530
|
+
end
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def wrap_attrs(node)
|
|
534
|
+
return nil unless node.respond_to?(:attrs) && node.attrs.is_a?(Hash)
|
|
535
|
+
|
|
536
|
+
attrs = {}
|
|
537
|
+
case node.type
|
|
538
|
+
when "image"
|
|
539
|
+
attrs[:src] = node.attrs["src"]
|
|
540
|
+
attrs[:alt] = node.attrs["alt"] if node.attrs["alt"]
|
|
541
|
+
attrs[:title] = node.attrs["title"] if node.attrs["title"]
|
|
542
|
+
when "ordered_list"
|
|
543
|
+
attrs[:start] = node.attrs["start"] if node.attrs["start"]
|
|
544
|
+
end
|
|
545
|
+
attrs.empty? ? nil : attrs
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
# Check if a node should preserve whitespace
|
|
549
|
+
# Nodes like <pre>, <textarea>, or nodes with style="white-space: pre" preserve whitespace
|
|
550
|
+
def preserve_whitespace?(node)
|
|
551
|
+
return false unless node.respond_to?(:type)
|
|
552
|
+
|
|
553
|
+
case node.type
|
|
554
|
+
when "code_block", "code_block_wrapper", "pre"
|
|
555
|
+
return true
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
# Check for white-space style in attrs
|
|
559
|
+
if node.respond_to?(:attrs) && node.attrs.is_a?(Hash)
|
|
560
|
+
style = node.attrs["style"]
|
|
561
|
+
if style.is_a?(String) && style.include?("white-space: pre")
|
|
562
|
+
return true
|
|
563
|
+
end
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
false
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
# Determine how whitespace should be collapsed for a node
|
|
570
|
+
# Returns a symbol: :preserve, :collapse, :normalize
|
|
571
|
+
def whitespace_mode(node)
|
|
572
|
+
if preserve_whitespace?(node)
|
|
573
|
+
:preserve
|
|
574
|
+
else
|
|
575
|
+
:collapse
|
|
576
|
+
end
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
# Collapse multiple spaces into one
|
|
580
|
+
def collapse_whitespace(text)
|
|
581
|
+
text.gsub(/[ \t]+/, " ")
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
# Normalize whitespace (replace tabs/newlines with spaces, collapse multiple spaces)
|
|
585
|
+
def normalize_whitespace(text)
|
|
586
|
+
text.gsub(/[\t \n\r]+/, " ")
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
# Process text content with appropriate whitespace handling
|
|
590
|
+
def process_text_whitespace(text, node)
|
|
591
|
+
mode = whitespace_mode(node)
|
|
592
|
+
case mode
|
|
593
|
+
when :preserve
|
|
594
|
+
text
|
|
595
|
+
when :normalize
|
|
596
|
+
normalize_whitespace(text)
|
|
597
|
+
else
|
|
598
|
+
collapse_whitespace(text)
|
|
599
|
+
end
|
|
600
|
+
end
|
|
601
|
+
end
|
|
373
602
|
end
|
|
374
603
|
end
|
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative 'node'
|
|
4
|
-
require_relative 'text'
|
|
5
|
-
require_relative 'hard_break'
|
|
6
|
-
|
|
7
3
|
module Prosereflect
|
|
8
4
|
class Paragraph < Node
|
|
9
|
-
PM_TYPE =
|
|
5
|
+
PM_TYPE = "paragraph"
|
|
10
6
|
|
|
11
|
-
attribute :type, :string, default: -> {
|
|
7
|
+
attribute :type, :string, default: -> {
|
|
8
|
+
self.class.send(:const_get, "PM_TYPE")
|
|
9
|
+
}
|
|
12
10
|
|
|
13
11
|
key_value do
|
|
14
|
-
map
|
|
15
|
-
map
|
|
16
|
-
map
|
|
17
|
-
map
|
|
12
|
+
map "type", to: :type, render_default: true
|
|
13
|
+
map "content", to: :content
|
|
14
|
+
map "attrs", to: :attrs
|
|
15
|
+
map "marks", to: :marks
|
|
18
16
|
end
|
|
19
17
|
|
|
20
18
|
def initialize(params = {})
|
|
@@ -29,13 +27,13 @@ module Prosereflect
|
|
|
29
27
|
def text_nodes
|
|
30
28
|
return [] unless content
|
|
31
29
|
|
|
32
|
-
content.
|
|
30
|
+
content.grep(Text)
|
|
33
31
|
end
|
|
34
32
|
|
|
35
33
|
def text_content
|
|
36
|
-
return
|
|
34
|
+
return "" unless content
|
|
37
35
|
|
|
38
|
-
result =
|
|
36
|
+
result = ""
|
|
39
37
|
content.each do |node|
|
|
40
38
|
result += node.text_content
|
|
41
39
|
end
|