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.
Files changed (158) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/docs.yml +63 -0
  3. data/.github/workflows/links.yml +97 -0
  4. data/.github/workflows/rake.yml +4 -0
  5. data/.github/workflows/release.yml +5 -0
  6. data/.gitignore +4 -0
  7. data/.rubocop.yml +19 -1
  8. data/.rubocop_todo.yml +119 -183
  9. data/CLAUDE.md +78 -0
  10. data/Gemfile +8 -4
  11. data/README.adoc +2 -0
  12. data/Rakefile +3 -3
  13. data/docs/Gemfile +10 -0
  14. data/docs/INDEX.adoc +45 -0
  15. data/docs/_advanced/index.adoc +15 -0
  16. data/docs/_advanced/schema.adoc +112 -0
  17. data/docs/_advanced/step-map.adoc +66 -0
  18. data/docs/_advanced/steps.adoc +88 -0
  19. data/docs/_advanced/test-builder.adoc +61 -0
  20. data/docs/_advanced/transform.adoc +92 -0
  21. data/docs/_config.yml +174 -0
  22. data/docs/_features/html-input.adoc +69 -0
  23. data/docs/_features/html-output.adoc +45 -0
  24. data/docs/_features/index.adoc +15 -0
  25. data/docs/_features/marks.adoc +86 -0
  26. data/docs/_features/node-types.adoc +124 -0
  27. data/docs/_features/user-mentions.adoc +47 -0
  28. data/docs/_guides/custom-nodes.adoc +107 -0
  29. data/docs/_guides/index.adoc +13 -0
  30. data/docs/_guides/round-trip-html.adoc +91 -0
  31. data/docs/_guides/serialization.adoc +109 -0
  32. data/docs/_pages/index.adoc +67 -0
  33. data/docs/_reference/document-api.adoc +49 -0
  34. data/docs/_reference/index.adoc +14 -0
  35. data/docs/_reference/node-api.adoc +79 -0
  36. data/docs/_reference/schema-api.adoc +95 -0
  37. data/docs/_reference/transform-api.adoc +77 -0
  38. data/docs/_understanding/document-model.adoc +65 -0
  39. data/docs/_understanding/fragment.adoc +52 -0
  40. data/docs/_understanding/index.adoc +14 -0
  41. data/docs/_understanding/resolved-position.adoc +53 -0
  42. data/docs/_understanding/slice.adoc +54 -0
  43. data/docs/lychee.toml +63 -0
  44. data/lib/prosereflect/attribute/base.rb +4 -6
  45. data/lib/prosereflect/attribute/bold.rb +2 -4
  46. data/lib/prosereflect/attribute/href.rb +1 -3
  47. data/lib/prosereflect/attribute/id.rb +7 -7
  48. data/lib/prosereflect/attribute.rb +4 -7
  49. data/lib/prosereflect/blockquote.rb +19 -11
  50. data/lib/prosereflect/bullet_list.rb +36 -29
  51. data/lib/prosereflect/code_block.rb +23 -27
  52. data/lib/prosereflect/code_block_wrapper.rb +12 -13
  53. data/lib/prosereflect/document.rb +14 -22
  54. data/lib/prosereflect/fragment.rb +249 -0
  55. data/lib/prosereflect/hard_break.rb +6 -6
  56. data/lib/prosereflect/heading.rb +14 -15
  57. data/lib/prosereflect/horizontal_rule.rb +23 -14
  58. data/lib/prosereflect/image.rb +32 -23
  59. data/lib/prosereflect/input/html.rb +179 -104
  60. data/lib/prosereflect/input.rb +7 -0
  61. data/lib/prosereflect/list_item.rb +11 -12
  62. data/lib/prosereflect/mark/base.rb +9 -11
  63. data/lib/prosereflect/mark/bold.rb +1 -3
  64. data/lib/prosereflect/mark/code.rb +1 -3
  65. data/lib/prosereflect/mark/italic.rb +1 -3
  66. data/lib/prosereflect/mark/link.rb +1 -3
  67. data/lib/prosereflect/mark/strike.rb +1 -3
  68. data/lib/prosereflect/mark/subscript.rb +1 -3
  69. data/lib/prosereflect/mark/superscript.rb +1 -3
  70. data/lib/prosereflect/mark/underline.rb +1 -3
  71. data/lib/prosereflect/mark.rb +9 -5
  72. data/lib/prosereflect/node.rb +171 -33
  73. data/lib/prosereflect/ordered_list.rb +17 -14
  74. data/lib/prosereflect/output/html.rb +279 -50
  75. data/lib/prosereflect/output.rb +7 -0
  76. data/lib/prosereflect/paragraph.rb +11 -13
  77. data/lib/prosereflect/parser.rb +56 -66
  78. data/lib/prosereflect/resolved_pos.rb +256 -0
  79. data/lib/prosereflect/schema/attribute.rb +57 -0
  80. data/lib/prosereflect/schema/content_match.rb +656 -0
  81. data/lib/prosereflect/schema/fragment.rb +166 -0
  82. data/lib/prosereflect/schema/mark.rb +121 -0
  83. data/lib/prosereflect/schema/mark_type.rb +130 -0
  84. data/lib/prosereflect/schema/node.rb +236 -0
  85. data/lib/prosereflect/schema/node_type.rb +274 -0
  86. data/lib/prosereflect/schema/schema_main.rb +190 -0
  87. data/lib/prosereflect/schema/spec.rb +92 -0
  88. data/lib/prosereflect/schema.rb +39 -0
  89. data/lib/prosereflect/table.rb +12 -13
  90. data/lib/prosereflect/table_cell.rb +13 -13
  91. data/lib/prosereflect/table_header.rb +17 -17
  92. data/lib/prosereflect/table_row.rb +12 -12
  93. data/lib/prosereflect/text.rb +35 -11
  94. data/lib/prosereflect/transform/attr_step.rb +157 -0
  95. data/lib/prosereflect/transform/insert_step.rb +115 -0
  96. data/lib/prosereflect/transform/mapping.rb +82 -0
  97. data/lib/prosereflect/transform/mark_step.rb +269 -0
  98. data/lib/prosereflect/transform/replace_around_step.rb +181 -0
  99. data/lib/prosereflect/transform/replace_step.rb +157 -0
  100. data/lib/prosereflect/transform/slice.rb +91 -0
  101. data/lib/prosereflect/transform/step.rb +89 -0
  102. data/lib/prosereflect/transform/step_map.rb +126 -0
  103. data/lib/prosereflect/transform/structure.rb +120 -0
  104. data/lib/prosereflect/transform/transform.rb +341 -0
  105. data/lib/prosereflect/transform.rb +26 -0
  106. data/lib/prosereflect/user.rb +15 -15
  107. data/lib/prosereflect/version.rb +1 -1
  108. data/lib/prosereflect.rb +30 -17
  109. data/prosereflect.gemspec +17 -16
  110. data/spec/fixtures/documents/formatted_text.yaml +14 -0
  111. data/spec/fixtures/documents/heading_paragraph.yaml +16 -0
  112. data/spec/fixtures/documents/lists_doc.yaml +32 -0
  113. data/spec/fixtures/documents/mixed_content.yaml +40 -0
  114. data/spec/fixtures/documents/nested_doc.yaml +20 -0
  115. data/spec/fixtures/documents/simple_doc.yaml +6 -0
  116. data/spec/fixtures/documents/table_doc.yaml +32 -0
  117. data/spec/fixtures/documents/transform_test.yaml +14 -0
  118. data/spec/fixtures/schema/custom_schema.rb +37 -0
  119. data/spec/fixtures/schema/test_schema.rb +46 -0
  120. data/spec/fixtures/test_builder/helpers.rb +212 -0
  121. data/spec/prosereflect/document_spec.rb +332 -330
  122. data/spec/prosereflect/fragment_spec.rb +273 -0
  123. data/spec/prosereflect/hard_break_spec.rb +125 -125
  124. data/spec/prosereflect/input/html_spec.rb +718 -522
  125. data/spec/prosereflect/node_spec.rb +311 -182
  126. data/spec/prosereflect/output/html_spec.rb +105 -105
  127. data/spec/prosereflect/output/whitespace_spec.rb +248 -0
  128. data/spec/prosereflect/paragraph_spec.rb +275 -274
  129. data/spec/prosereflect/parser/round_trip_spec.rb +472 -0
  130. data/spec/prosereflect/parser_spec.rb +185 -180
  131. data/spec/prosereflect/resolved_pos_spec.rb +74 -0
  132. data/spec/prosereflect/schema/conftest.rb +68 -0
  133. data/spec/prosereflect/schema/content_match_spec.rb +237 -0
  134. data/spec/prosereflect/schema/mark_spec.rb +274 -0
  135. data/spec/prosereflect/schema/mark_type_spec.rb +86 -0
  136. data/spec/prosereflect/schema/node_type_spec.rb +142 -0
  137. data/spec/prosereflect/schema/schema_spec.rb +194 -0
  138. data/spec/prosereflect/table_cell_spec.rb +183 -183
  139. data/spec/prosereflect/table_row_spec.rb +149 -149
  140. data/spec/prosereflect/table_spec.rb +320 -318
  141. data/spec/prosereflect/test_builder/marks_spec.rb +127 -0
  142. data/spec/prosereflect/text_spec.rb +133 -132
  143. data/spec/prosereflect/transform/equivalence_spec.rb +487 -0
  144. data/spec/prosereflect/transform/mapping_spec.rb +226 -0
  145. data/spec/prosereflect/transform/replace_spec.rb +832 -0
  146. data/spec/prosereflect/transform/replace_step_spec.rb +157 -0
  147. data/spec/prosereflect/transform/slice_spec.rb +48 -0
  148. data/spec/prosereflect/transform/step_map_spec.rb +70 -0
  149. data/spec/prosereflect/transform/step_spec.rb +211 -0
  150. data/spec/prosereflect/transform/structure_spec.rb +98 -0
  151. data/spec/prosereflect/transform/transform_spec.rb +238 -0
  152. data/spec/prosereflect/user_spec.rb +31 -28
  153. data/spec/prosereflect_spec.rb +28 -26
  154. data/spec/spec_helper.rb +7 -6
  155. data/spec/support/matchers.rb +6 -6
  156. data/spec/support/shared_examples.rb +49 -49
  157. metadata +96 -5
  158. data/spec/prosereflect/version_spec.rb +0 -11
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'nokogiri'
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('div').children.to_html
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 'doc'
70
+ when "doc"
47
71
  process_document(node, builder)
48
- when 'paragraph'
72
+ when "paragraph"
49
73
  process_paragraph(node, builder)
50
- when 'heading'
74
+ when "heading"
51
75
  process_heading(node, builder)
52
- when 'text'
76
+ when "text"
53
77
  process_text(node, builder)
54
- when 'table'
78
+ when "table"
55
79
  process_table(node, builder)
56
- when 'table_row'
80
+ when "table_row"
57
81
  process_table_row(node, builder)
58
- when 'table_cell'
82
+ when "table_cell"
59
83
  process_table_cell(node, builder)
60
- when 'table_header'
84
+ when "table_header"
61
85
  process_table_header(node, builder)
62
- when 'hard_break'
86
+ when "hard_break"
63
87
  builder.br
64
- when 'image'
88
+ when "image"
65
89
  process_image(node, builder)
66
- when 'user'
90
+ when "user"
67
91
  process_user(node, builder)
68
- when 'bullet_list'
92
+ when "bullet_list"
69
93
  process_bullet_list(node, builder)
70
- when 'ordered_list'
94
+ when "ordered_list"
71
95
  process_ordered_list(node, builder)
72
- when 'list_item'
96
+ when "list_item"
73
97
  process_list_item(node, builder)
74
- when 'blockquote'
98
+ when "blockquote"
75
99
  process_blockquote(node, builder)
76
- when 'horizontal_rule'
100
+ when "horizontal_rule"
77
101
  process_horizontal_rule(node, builder)
78
- when 'code_block_wrapper'
102
+ when "code_block_wrapper"
79
103
  process_code_block_wrapper(node, builder)
80
- when 'code_block'
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['type']
151
+ current_mark["type"]
128
152
  elsif current_mark.respond_to?(:type)
129
153
  current_mark.type
130
154
  else
131
- 'unknown'
155
+ "unknown"
132
156
  end
133
157
 
134
158
  case mark_type
135
- when 'bold'
159
+ when "bold"
136
160
  builder.strong do
137
161
  apply_marks(text, remaining_marks, builder)
138
162
  end
139
- when 'italic'
163
+ when "italic"
140
164
  builder.em do
141
165
  apply_marks(text, remaining_marks, builder)
142
166
  end
143
- when 'code'
167
+ when "code"
144
168
  builder.code do
145
169
  apply_marks(text, remaining_marks, builder)
146
170
  end
147
- when 'link'
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 'strike'
180
+ when "strike"
157
181
  builder.del do
158
182
  apply_marks(text, remaining_marks, builder)
159
183
  end
160
- when 'underline'
184
+ when "underline"
161
185
  builder.u do
162
186
  apply_marks(text, remaining_marks, builder)
163
187
  end
164
- when 'subscript'
188
+ when "subscript"
165
189
  builder.sub do
166
190
  apply_marks(text, remaining_marks, builder)
167
191
  end
168
- when 'superscript'
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['attrs'].is_a?(Hash)
182
- return mark['attrs']['href']
183
- elsif mark['attrs'].is_a?(Array)
184
- href_attr = mark['attrs'].find { |a| a.is_a?(Prosereflect::Attribute::Href) || (a.is_a?(Hash) && a['type'] == 'href') }
185
- return href_attr['href'] if href_attr.is_a?(Hash) && href_attr['href']
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['href']
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['href'] }
197
- return hash_attr['href'] if 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? { |cell| cell.type == 'table_header' }
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 == 'paragraph'
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 == 'paragraph'
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 == 'list_item'
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('; ') unless attrs[:style].empty?
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['data-line-numbers'] = 'true' if node.attrs['line_numbers']
343
- if node.attrs['highlight_lines'].is_a?(Array) && !node.attrs['highlight_lines'].empty? && node.attrs['highlight_lines'] != [0]
344
- attrs['data-highlight-lines'] = node.attrs['highlight_lines'].join(',')
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['class'] = "language-#{node.language}" if node.language
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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosereflect
4
+ module Output
5
+ autoload :Html, "#{__dir__}/output/html"
6
+ end
7
+ 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 = 'paragraph'
5
+ PM_TYPE = "paragraph"
10
6
 
11
- attribute :type, :string, default: -> { send('const_get', 'PM_TYPE') }
7
+ attribute :type, :string, default: -> {
8
+ self.class.send(:const_get, "PM_TYPE")
9
+ }
12
10
 
13
11
  key_value do
14
- map 'type', to: :type, render_default: true
15
- map 'content', to: :content
16
- map 'attrs', to: :attrs
17
- map 'marks', to: :marks
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.select { |node| node.is_a?(Text) }
30
+ content.grep(Text)
33
31
  end
34
32
 
35
33
  def text_content
36
- return '' unless content
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