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
@@ -0,0 +1,472 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Prosereflect::Parser, ".round_trip" do
6
+ # Helper to create a document from YAML
7
+ def parse_doc(yaml_string)
8
+ Prosereflect::Parser.parse_document(YAML.safe_load(yaml_string))
9
+ end
10
+
11
+ describe "Document serialization round-trip" do
12
+ it "round-trips simple paragraph" do
13
+ doc = parse_doc({
14
+ "type" => "doc",
15
+ "content" => [
16
+ {
17
+ "type" => "paragraph",
18
+ "content" => [
19
+ { "type" => "text", "text" => "Hello World" },
20
+ ],
21
+ },
22
+ ],
23
+ }.to_yaml)
24
+
25
+ ruby_json = doc.to_h
26
+
27
+ # Parse again and verify same structure
28
+ doc2 = described_class.parse_document(ruby_json)
29
+ expect(doc2.to_h).to eq(ruby_json)
30
+ end
31
+
32
+ it "round-trips document with formatted text" do
33
+ doc = parse_doc({
34
+ "type" => "doc",
35
+ "content" => [
36
+ {
37
+ "type" => "paragraph",
38
+ "content" => [
39
+ {
40
+ "type" => "text",
41
+ "text" => "Hello",
42
+ "marks" => [{ "type" => "bold" }],
43
+ },
44
+ {
45
+ "type" => "text",
46
+ "text" => " World",
47
+ },
48
+ ],
49
+ },
50
+ ],
51
+ }.to_yaml)
52
+
53
+ ruby_json = doc.to_h
54
+
55
+ doc2 = described_class.parse_document(ruby_json)
56
+ expect(doc2.to_h).to eq(ruby_json)
57
+ end
58
+
59
+ it "round-trips heading" do
60
+ doc = parse_doc({
61
+ "type" => "doc",
62
+ "content" => [
63
+ {
64
+ "type" => "heading",
65
+ "attrs" => { "level" => 1 },
66
+ "content" => [
67
+ { "type" => "text", "text" => "Title" },
68
+ ],
69
+ },
70
+ ],
71
+ }.to_yaml)
72
+
73
+ ruby_json = doc.to_h
74
+
75
+ doc2 = described_class.parse_document(ruby_json)
76
+ expect(doc2.to_h).to eq(ruby_json)
77
+ end
78
+
79
+ it "round-trips blockquote" do
80
+ doc = parse_doc({
81
+ "type" => "doc",
82
+ "content" => [
83
+ {
84
+ "type" => "blockquote",
85
+ "content" => [
86
+ {
87
+ "type" => "paragraph",
88
+ "content" => [
89
+ { "type" => "text", "text" => "Quote text" },
90
+ ],
91
+ },
92
+ ],
93
+ },
94
+ ],
95
+ }.to_yaml)
96
+
97
+ ruby_json = doc.to_h
98
+
99
+ doc2 = described_class.parse_document(ruby_json)
100
+ expect(doc2.to_h).to eq(ruby_json)
101
+ end
102
+
103
+ it "round-trips bullet list" do
104
+ doc = parse_doc({
105
+ "type" => "doc",
106
+ "content" => [
107
+ {
108
+ "type" => "bullet_list",
109
+ "content" => [
110
+ {
111
+ "type" => "list_item",
112
+ "content" => [
113
+ {
114
+ "type" => "paragraph",
115
+ "content" => [
116
+ { "type" => "text", "text" => "Item 1" },
117
+ ],
118
+ },
119
+ ],
120
+ },
121
+ ],
122
+ },
123
+ ],
124
+ }.to_yaml)
125
+
126
+ ruby_json = doc.to_h
127
+
128
+ doc2 = described_class.parse_document(ruby_json)
129
+ expect(doc2.to_h).to eq(ruby_json)
130
+ end
131
+
132
+ it "round-trips ordered list" do
133
+ doc = parse_doc({
134
+ "type" => "doc",
135
+ "content" => [
136
+ {
137
+ "type" => "ordered_list",
138
+ "attrs" => { "order" => 1 },
139
+ "content" => [
140
+ {
141
+ "type" => "list_item",
142
+ "content" => [
143
+ {
144
+ "type" => "paragraph",
145
+ "content" => [
146
+ { "type" => "text", "text" => "Item 1" },
147
+ ],
148
+ },
149
+ ],
150
+ },
151
+ ],
152
+ },
153
+ ],
154
+ }.to_yaml)
155
+
156
+ ruby_json = doc.to_h
157
+
158
+ doc2 = described_class.parse_document(ruby_json)
159
+ expect(doc2.to_h).to eq(ruby_json)
160
+ end
161
+
162
+ it "round-trips table" do
163
+ doc = parse_doc({
164
+ "type" => "doc",
165
+ "content" => [
166
+ {
167
+ "type" => "table",
168
+ "content" => [
169
+ {
170
+ "type" => "table_row",
171
+ "content" => [
172
+ {
173
+ "type" => "table_cell",
174
+ "content" => [
175
+ {
176
+ "type" => "paragraph",
177
+ "content" => [
178
+ { "type" => "text", "text" => "Cell 1" },
179
+ ],
180
+ },
181
+ ],
182
+ },
183
+ ],
184
+ },
185
+ ],
186
+ },
187
+ ],
188
+ }.to_yaml)
189
+
190
+ ruby_json = doc.to_h
191
+
192
+ doc2 = described_class.parse_document(ruby_json)
193
+ expect(doc2.to_h).to eq(ruby_json)
194
+ end
195
+
196
+ it "round-trips hard_break" do
197
+ doc = parse_doc({
198
+ "type" => "doc",
199
+ "content" => [
200
+ {
201
+ "type" => "paragraph",
202
+ "content" => [
203
+ { "type" => "text", "text" => "Line 1" },
204
+ { "type" => "hard_break" },
205
+ { "type" => "text", "text" => "Line 2" },
206
+ ],
207
+ },
208
+ ],
209
+ }.to_yaml)
210
+
211
+ ruby_json = doc.to_h
212
+
213
+ doc2 = described_class.parse_document(ruby_json)
214
+ expect(doc2.to_h).to eq(ruby_json)
215
+ end
216
+
217
+ it "round-trips horizontal rule" do
218
+ doc = parse_doc({
219
+ "type" => "doc",
220
+ "content" => [
221
+ { "type" => "horizontal_rule" },
222
+ ],
223
+ }.to_yaml)
224
+
225
+ ruby_json = doc.to_h
226
+
227
+ doc2 = described_class.parse_document(ruby_json)
228
+ expect(doc2.to_h).to eq(ruby_json)
229
+ end
230
+
231
+ it "round-trips code block" do
232
+ doc = parse_doc({
233
+ "type" => "doc",
234
+ "content" => [
235
+ {
236
+ "type" => "code_block",
237
+ "attrs" => { "language" => "ruby" },
238
+ "content" => [
239
+ { "type" => "text", "text" => "puts 'hello'" },
240
+ ],
241
+ },
242
+ ],
243
+ }.to_yaml)
244
+
245
+ ruby_json = doc.to_h
246
+
247
+ doc2 = described_class.parse_document(ruby_json)
248
+ # Verify structure is preserved - code block type and language
249
+ code_block = doc2.find_first("code_block")
250
+ expect(code_block).to be_a(Prosereflect::CodeBlock)
251
+ expect(code_block.language).to eq("ruby")
252
+ end
253
+ end
254
+
255
+ describe "Node equality" do
256
+ it "same nodes are equal" do
257
+ doc1 = parse_doc({
258
+ "type" => "doc",
259
+ "content" => [
260
+ {
261
+ "type" => "paragraph",
262
+ "content" => [
263
+ { "type" => "text", "text" => "Hello" },
264
+ ],
265
+ },
266
+ ],
267
+ }.to_yaml)
268
+
269
+ doc2 = parse_doc({
270
+ "type" => "doc",
271
+ "content" => [
272
+ {
273
+ "type" => "paragraph",
274
+ "content" => [
275
+ { "type" => "text", "text" => "Hello" },
276
+ ],
277
+ },
278
+ ],
279
+ }.to_yaml)
280
+
281
+ # Both should have same structure
282
+ expect(doc1.to_h).to eq(doc2.to_h)
283
+ end
284
+
285
+ it "different nodes are not equal" do
286
+ doc1 = parse_doc({
287
+ "type" => "doc",
288
+ "content" => [
289
+ {
290
+ "type" => "paragraph",
291
+ "content" => [
292
+ { "type" => "text", "text" => "Hello" },
293
+ ],
294
+ },
295
+ ],
296
+ }.to_yaml)
297
+
298
+ doc2 = parse_doc({
299
+ "type" => "doc",
300
+ "content" => [
301
+ {
302
+ "type" => "paragraph",
303
+ "content" => [
304
+ { "type" => "text", "text" => "World" },
305
+ ],
306
+ },
307
+ ],
308
+ }.to_yaml)
309
+
310
+ expect(doc1.to_h).not_to eq(doc2.to_h)
311
+ end
312
+ end
313
+
314
+ describe "Text content extraction" do
315
+ it "extracts text from simple document" do
316
+ doc = parse_doc({
317
+ "type" => "doc",
318
+ "content" => [
319
+ {
320
+ "type" => "paragraph",
321
+ "content" => [
322
+ { "type" => "text", "text" => "Hello World" },
323
+ ],
324
+ },
325
+ ],
326
+ }.to_yaml)
327
+
328
+ expect(doc.text_content).to eq("Hello World")
329
+ end
330
+
331
+ it "extracts text from nested structure" do
332
+ doc = parse_doc({
333
+ "type" => "doc",
334
+ "content" => [
335
+ {
336
+ "type" => "blockquote",
337
+ "content" => [
338
+ {
339
+ "type" => "paragraph",
340
+ "content" => [
341
+ { "type" => "text", "text" => "Quote text" },
342
+ ],
343
+ },
344
+ ],
345
+ },
346
+ ],
347
+ }.to_yaml)
348
+
349
+ expect(doc.text_content).to eq("Quote text")
350
+ end
351
+
352
+ it "extracts concatenated text from multiple paragraphs" do
353
+ doc = parse_doc({
354
+ "type" => "doc",
355
+ "content" => [
356
+ {
357
+ "type" => "paragraph",
358
+ "content" => [
359
+ { "type" => "text", "text" => "First" },
360
+ ],
361
+ },
362
+ {
363
+ "type" => "paragraph",
364
+ "content" => [
365
+ { "type" => "text", "text" => "Second" },
366
+ ],
367
+ },
368
+ ],
369
+ }.to_yaml)
370
+
371
+ # text_content returns text with newlines between block elements
372
+ expect(doc.text_content).to include("First")
373
+ expect(doc.text_content).to include("Second")
374
+ end
375
+ end
376
+
377
+ describe "Mark preservation" do
378
+ it "preserves bold mark" do
379
+ doc = parse_doc({
380
+ "type" => "doc",
381
+ "content" => [
382
+ {
383
+ "type" => "paragraph",
384
+ "content" => [
385
+ {
386
+ "type" => "text",
387
+ "text" => "bold text",
388
+ "marks" => [{ "type" => "bold" }],
389
+ },
390
+ ],
391
+ },
392
+ ],
393
+ }.to_yaml)
394
+
395
+ para = doc.find_first("paragraph")
396
+ text = para.content.first
397
+ expect(text.marks).to include("type" => "bold")
398
+ end
399
+
400
+ it "preserves italic mark" do
401
+ doc = parse_doc({
402
+ "type" => "doc",
403
+ "content" => [
404
+ {
405
+ "type" => "paragraph",
406
+ "content" => [
407
+ {
408
+ "type" => "text",
409
+ "text" => "italic text",
410
+ "marks" => [{ "type" => "italic" }],
411
+ },
412
+ ],
413
+ },
414
+ ],
415
+ }.to_yaml)
416
+
417
+ para = doc.find_first("paragraph")
418
+ text = para.content.first
419
+ expect(text.marks).to include("type" => "italic")
420
+ end
421
+
422
+ it "preserves multiple marks" do
423
+ doc = parse_doc({
424
+ "type" => "doc",
425
+ "content" => [
426
+ {
427
+ "type" => "paragraph",
428
+ "content" => [
429
+ {
430
+ "type" => "text",
431
+ "text" => "bold and italic",
432
+ "marks" => [
433
+ { "type" => "bold" },
434
+ { "type" => "italic" },
435
+ ],
436
+ },
437
+ ],
438
+ },
439
+ ],
440
+ }.to_yaml)
441
+
442
+ para = doc.find_first("paragraph")
443
+ text = para.content.first
444
+ expect(text.marks).to include("type" => "bold")
445
+ expect(text.marks).to include("type" => "italic")
446
+ end
447
+
448
+ it "preserves link mark with attrs" do
449
+ doc = parse_doc({
450
+ "type" => "doc",
451
+ "content" => [
452
+ {
453
+ "type" => "paragraph",
454
+ "content" => [
455
+ {
456
+ "type" => "text",
457
+ "text" => "link text",
458
+ "marks" => [
459
+ { "type" => "link", "attrs" => { "href" => "https://example.com" } },
460
+ ],
461
+ },
462
+ ],
463
+ },
464
+ ],
465
+ }.to_yaml)
466
+
467
+ para = doc.find_first("paragraph")
468
+ text = para.content.first
469
+ expect(text.marks).to include("type" => "link", "attrs" => { "href" => "https://example.com" })
470
+ end
471
+ end
472
+ end