prosereflect 0.2.0 → 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 (110) 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/.gitignore +4 -0
  5. data/.rubocop_todo.yml +61 -75
  6. data/README.adoc +2 -0
  7. data/docs/Gemfile +10 -0
  8. data/docs/INDEX.adoc +45 -0
  9. data/docs/_advanced/index.adoc +15 -0
  10. data/docs/_advanced/schema.adoc +112 -0
  11. data/docs/_advanced/step-map.adoc +66 -0
  12. data/docs/_advanced/steps.adoc +88 -0
  13. data/docs/_advanced/test-builder.adoc +61 -0
  14. data/docs/_advanced/transform.adoc +92 -0
  15. data/docs/_config.yml +174 -0
  16. data/docs/_features/html-input.adoc +69 -0
  17. data/docs/_features/html-output.adoc +45 -0
  18. data/docs/_features/index.adoc +15 -0
  19. data/docs/_features/marks.adoc +86 -0
  20. data/docs/_features/node-types.adoc +124 -0
  21. data/docs/_features/user-mentions.adoc +47 -0
  22. data/docs/_guides/custom-nodes.adoc +107 -0
  23. data/docs/_guides/index.adoc +13 -0
  24. data/docs/_guides/round-trip-html.adoc +91 -0
  25. data/docs/_guides/serialization.adoc +109 -0
  26. data/docs/_pages/index.adoc +67 -0
  27. data/docs/_reference/document-api.adoc +49 -0
  28. data/docs/_reference/index.adoc +14 -0
  29. data/docs/_reference/node-api.adoc +79 -0
  30. data/docs/_reference/schema-api.adoc +95 -0
  31. data/docs/_reference/transform-api.adoc +77 -0
  32. data/docs/_understanding/document-model.adoc +65 -0
  33. data/docs/_understanding/fragment.adoc +52 -0
  34. data/docs/_understanding/index.adoc +14 -0
  35. data/docs/_understanding/resolved-position.adoc +53 -0
  36. data/docs/_understanding/slice.adoc +54 -0
  37. data/docs/lychee.toml +63 -0
  38. data/lib/prosereflect/blockquote.rb +9 -0
  39. data/lib/prosereflect/bullet_list.rb +25 -19
  40. data/lib/prosereflect/code_block.rb +1 -5
  41. data/lib/prosereflect/fragment.rb +249 -0
  42. data/lib/prosereflect/horizontal_rule.rb +9 -0
  43. data/lib/prosereflect/image.rb +9 -0
  44. data/lib/prosereflect/input/html.rb +96 -0
  45. data/lib/prosereflect/node.rb +141 -3
  46. data/lib/prosereflect/ordered_list.rb +2 -0
  47. data/lib/prosereflect/output/html.rb +227 -0
  48. data/lib/prosereflect/parser.rb +9 -0
  49. data/lib/prosereflect/resolved_pos.rb +256 -0
  50. data/lib/prosereflect/schema/attribute.rb +57 -0
  51. data/lib/prosereflect/schema/content_match.rb +656 -0
  52. data/lib/prosereflect/schema/fragment.rb +166 -0
  53. data/lib/prosereflect/schema/mark.rb +121 -0
  54. data/lib/prosereflect/schema/mark_type.rb +130 -0
  55. data/lib/prosereflect/schema/node.rb +236 -0
  56. data/lib/prosereflect/schema/node_type.rb +274 -0
  57. data/lib/prosereflect/schema/schema_main.rb +190 -0
  58. data/lib/prosereflect/schema/spec.rb +92 -0
  59. data/lib/prosereflect/schema.rb +39 -0
  60. data/lib/prosereflect/text.rb +24 -0
  61. data/lib/prosereflect/transform/attr_step.rb +157 -0
  62. data/lib/prosereflect/transform/insert_step.rb +115 -0
  63. data/lib/prosereflect/transform/mapping.rb +82 -0
  64. data/lib/prosereflect/transform/mark_step.rb +269 -0
  65. data/lib/prosereflect/transform/replace_around_step.rb +181 -0
  66. data/lib/prosereflect/transform/replace_step.rb +157 -0
  67. data/lib/prosereflect/transform/slice.rb +91 -0
  68. data/lib/prosereflect/transform/step.rb +89 -0
  69. data/lib/prosereflect/transform/step_map.rb +126 -0
  70. data/lib/prosereflect/transform/structure.rb +120 -0
  71. data/lib/prosereflect/transform/transform.rb +341 -0
  72. data/lib/prosereflect/transform.rb +26 -0
  73. data/lib/prosereflect/version.rb +1 -1
  74. data/lib/prosereflect.rb +3 -0
  75. data/spec/fixtures/documents/formatted_text.yaml +14 -0
  76. data/spec/fixtures/documents/heading_paragraph.yaml +16 -0
  77. data/spec/fixtures/documents/lists_doc.yaml +32 -0
  78. data/spec/fixtures/documents/mixed_content.yaml +40 -0
  79. data/spec/fixtures/documents/nested_doc.yaml +20 -0
  80. data/spec/fixtures/documents/simple_doc.yaml +6 -0
  81. data/spec/fixtures/documents/table_doc.yaml +32 -0
  82. data/spec/fixtures/documents/transform_test.yaml +14 -0
  83. data/spec/fixtures/schema/custom_schema.rb +37 -0
  84. data/spec/fixtures/schema/test_schema.rb +46 -0
  85. data/spec/fixtures/test_builder/helpers.rb +212 -0
  86. data/spec/prosereflect/document_spec.rb +1 -1
  87. data/spec/prosereflect/fragment_spec.rb +273 -0
  88. data/spec/prosereflect/input/html_spec.rb +197 -1
  89. data/spec/prosereflect/node_spec.rb +128 -0
  90. data/spec/prosereflect/output/whitespace_spec.rb +248 -0
  91. data/spec/prosereflect/parser/round_trip_spec.rb +472 -0
  92. data/spec/prosereflect/resolved_pos_spec.rb +74 -0
  93. data/spec/prosereflect/schema/conftest.rb +68 -0
  94. data/spec/prosereflect/schema/content_match_spec.rb +237 -0
  95. data/spec/prosereflect/schema/mark_spec.rb +274 -0
  96. data/spec/prosereflect/schema/mark_type_spec.rb +86 -0
  97. data/spec/prosereflect/schema/node_type_spec.rb +142 -0
  98. data/spec/prosereflect/schema/schema_spec.rb +194 -0
  99. data/spec/prosereflect/test_builder/marks_spec.rb +127 -0
  100. data/spec/prosereflect/transform/equivalence_spec.rb +487 -0
  101. data/spec/prosereflect/transform/mapping_spec.rb +226 -0
  102. data/spec/prosereflect/transform/replace_spec.rb +832 -0
  103. data/spec/prosereflect/transform/replace_step_spec.rb +157 -0
  104. data/spec/prosereflect/transform/slice_spec.rb +48 -0
  105. data/spec/prosereflect/transform/step_map_spec.rb +70 -0
  106. data/spec/prosereflect/transform/step_spec.rb +211 -0
  107. data/spec/prosereflect/transform/structure_spec.rb +98 -0
  108. data/spec/prosereflect/transform/transform_spec.rb +238 -0
  109. data/spec/spec_helper.rb +1 -0
  110. metadata +90 -2
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require_relative "conftest"
5
+
6
+ RSpec.describe Prosereflect::Schema::NodeType do
7
+ include_context "test_schema"
8
+
9
+ describe "is_block?" do
10
+ it "returns true for block nodes" do
11
+ expect(schema.node_type("paragraph").is_block?).to be true
12
+ expect(schema.node_type("heading").is_block?).to be true
13
+ expect(schema.node_type("blockquote").is_block?).to be true
14
+ end
15
+
16
+ it "returns false for inline nodes" do
17
+ expect(schema.node_type("text").is_block?).to be false
18
+ end
19
+ end
20
+
21
+ describe "is_inline?" do
22
+ it "returns true for inline nodes" do
23
+ expect(schema.node_type("text").is_inline?).to be true
24
+ end
25
+
26
+ it "returns false for block nodes" do
27
+ expect(schema.node_type("paragraph").is_inline?).to be false
28
+ end
29
+ end
30
+
31
+ describe "text?" do
32
+ it "returns true only for text type" do
33
+ expect(schema.node_type("text").text?).to be true
34
+ expect(schema.node_type("paragraph").text?).to be false
35
+ end
36
+ end
37
+
38
+ describe "is_leaf?" do
39
+ it "returns true for nodes with empty content expression" do
40
+ expect(schema.node_type("text").is_leaf?).to be true
41
+ expect(schema.node_type("hard_break").is_leaf?).to be true
42
+ expect(schema.node_type("image").is_leaf?).to be true
43
+ end
44
+
45
+ it "returns false for nodes with content" do
46
+ expect(schema.node_type("paragraph").is_leaf?).to be false
47
+ end
48
+ end
49
+
50
+ describe "has_required_attrs?" do
51
+ it "returns true when node has required attributes" do
52
+ expect(schema.node_type("image").has_required_attrs?).to be true
53
+ end
54
+
55
+ it "returns false when all attrs have defaults" do
56
+ expect(schema.node_type("heading").has_required_attrs?).to be false
57
+ end
58
+ end
59
+
60
+ describe "default_attrs" do
61
+ it "returns defaults for all attrs" do
62
+ defaults = schema.node_type("heading").default_attrs
63
+ expect(defaults).to eq({ "level" => 1 })
64
+ end
65
+
66
+ it "returns nil if any required attr has no default" do
67
+ defaults = schema.node_type("image").default_attrs
68
+ expect(defaults).to be_nil
69
+ end
70
+ end
71
+
72
+ describe "compute_attrs" do
73
+ it "uses defaults when attrs not provided" do
74
+ attrs = schema.node_type("heading").compute_attrs(nil)
75
+ expect(attrs).to eq({ "level" => 1 })
76
+ end
77
+
78
+ it "overrides defaults with provided values" do
79
+ attrs = schema.node_type("heading").compute_attrs({ "level" => 3 })
80
+ expect(attrs).to eq({ "level" => 3 })
81
+ end
82
+
83
+ it "raises error for missing required attrs" do
84
+ expect do
85
+ schema.node_type("image").compute_attrs({})
86
+ end.to raise_error(Prosereflect::Schema::ValidationError)
87
+ end
88
+ end
89
+
90
+ describe "create" do
91
+ it "creates a node with computed attrs" do
92
+ node = schema.node_type("heading").create({ "level" => 2 }, nil, [])
93
+ expect(node.attrs["level"]).to eq(2)
94
+ end
95
+
96
+ it "raises error for text node" do
97
+ expect do
98
+ schema.node_type("text").create({}, nil, [])
99
+ end.to raise_error(Prosereflect::Schema::Error)
100
+ end
101
+ end
102
+
103
+ describe "valid_content?" do
104
+ it "returns true for valid content" do
105
+ frag = Prosereflect::Schema::Fragment.new([
106
+ schema.node_type("paragraph").create(
107
+ nil, [], []
108
+ ),
109
+ ])
110
+ expect(schema.node_type("doc").valid_content?(frag)).to be true
111
+ end
112
+
113
+ it "returns false for invalid content" do
114
+ frag = Prosereflect::Schema::Fragment.new([
115
+ schema.text(""),
116
+ ])
117
+ expect(schema.node_type("doc").valid_content?(frag)).to be false
118
+ end
119
+ end
120
+
121
+ describe "check_content" do
122
+ it "raises error for invalid content" do
123
+ frag = Prosereflect::Schema::Fragment.new([
124
+ schema.text(""),
125
+ ])
126
+ expect do
127
+ schema.node_type("doc").check_content(frag)
128
+ end.to raise_error(Prosereflect::Schema::ValidationError)
129
+ end
130
+ end
131
+
132
+ describe "allows_mark_type?" do
133
+ it "returns true when mark is in mark_set" do
134
+ expect(schema.node_type("paragraph").allows_mark_type?(schema.mark_type("em"))).to be true
135
+ expect(schema.node_type("paragraph").allows_mark_type?(schema.mark_type("strong"))).to be true
136
+ end
137
+
138
+ it "returns false when mark is not in mark_set" do
139
+ expect(schema.node_type("code_block").allows_mark_type?(schema.mark_type("em"))).to be false
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require_relative "conftest"
5
+
6
+ RSpec.describe Prosereflect::Schema do
7
+ include_context "test_schema"
8
+
9
+ describe "initialization" do
10
+ it "creates a schema with nodes and marks" do
11
+ expect(schema.nodes).to be_a(Hash)
12
+ expect(schema.marks).to be_a(Hash)
13
+ expect(schema.nodes.keys).to include("doc", "paragraph", "text")
14
+ expect(schema.marks.keys).to include("em", "strong", "link")
15
+ end
16
+
17
+ it "raises error for missing top node" do
18
+ expect do
19
+ described_class.new(
20
+ nodes_spec: { "text" => {} },
21
+ marks_spec: {},
22
+ )
23
+ end.to raise_error(Prosereflect::Schema::ValidationError,
24
+ /missing its top node/)
25
+ end
26
+
27
+ it "raises error for missing text type" do
28
+ expect do
29
+ described_class.new(
30
+ nodes_spec: { "doc" => { content: "block+" } },
31
+ marks_spec: {},
32
+ )
33
+ end.to raise_error(Prosereflect::Schema::ValidationError,
34
+ /every schema needs a 'text' type/)
35
+ end
36
+
37
+ it "raises error if text has attrs" do
38
+ expect do
39
+ described_class.new(
40
+ nodes_spec: {
41
+ "doc" => { content: "block+" },
42
+ "text" => { attrs: { "something" => {} } },
43
+ },
44
+ marks_spec: {},
45
+ )
46
+ end.to raise_error(Prosereflect::Schema::ValidationError,
47
+ /text node type should not have attributes/)
48
+ end
49
+
50
+ it "raises error if node and mark share same name" do
51
+ expect do
52
+ described_class.new(
53
+ nodes_spec: {
54
+ "doc" => { content: "block+" },
55
+ "text" => {},
56
+ "bold" => {},
57
+ },
58
+ marks_spec: {
59
+ "bold" => {},
60
+ },
61
+ )
62
+ end.to raise_error(Prosereflect::Schema::ValidationError,
63
+ /can not be both a node and a mark/)
64
+ end
65
+ end
66
+
67
+ describe "node_type" do
68
+ it "returns node type by name" do
69
+ node_type = schema.node_type("paragraph")
70
+ expect(node_type.name).to eq("paragraph")
71
+ end
72
+
73
+ it "raises error for unknown node type" do
74
+ expect do
75
+ schema.node_type("unknown")
76
+ end.to raise_error(Prosereflect::Schema::ValidationError,
77
+ /Unknown node type/)
78
+ end
79
+ end
80
+
81
+ describe "mark_type" do
82
+ it "returns mark type by name" do
83
+ mark_type = schema.mark_type("em")
84
+ expect(mark_type.name).to eq("em")
85
+ end
86
+
87
+ it "raises error for unknown mark type" do
88
+ expect do
89
+ schema.mark_type("unknown")
90
+ end.to raise_error(Prosereflect::Schema::ValidationError,
91
+ /Unknown mark type/)
92
+ end
93
+ end
94
+
95
+ describe "node" do
96
+ it "creates a node by type name" do
97
+ node = schema.node("paragraph")
98
+ expect(node.type.name).to eq("paragraph")
99
+ end
100
+
101
+ it "creates a node with attrs" do
102
+ node = schema.node("heading", { "level" => 2 })
103
+ expect(node.attrs["level"]).to eq(2)
104
+ end
105
+
106
+ it "creates a node with content" do
107
+ text = schema.text("Hello")
108
+ para = schema.node("paragraph", nil, [text])
109
+ expect(para.content.content.first.text).to eq("Hello")
110
+ end
111
+
112
+ it "validates content" do
113
+ text = schema.text("Hello")
114
+ expect do
115
+ # doc requires block+, not inline content directly
116
+ schema.node("doc", nil, [text])
117
+ end.to raise_error(Prosereflect::Schema::ValidationError)
118
+ end
119
+ end
120
+
121
+ describe "text" do
122
+ it "creates a text node" do
123
+ text_node = schema.text("Hello")
124
+ expect(text_node.text).to eq("Hello")
125
+ expect(text_node.type.name).to eq("text")
126
+ end
127
+
128
+ it "creates text with marks" do
129
+ em = schema.mark("em")
130
+ text_node = schema.text("Hello", [em])
131
+ expect(text_node.marks.length).to eq(1)
132
+ end
133
+ end
134
+
135
+ describe "mark" do
136
+ it "creates a mark by name" do
137
+ mark = schema.mark("em")
138
+ expect(mark.type.name).to eq("em")
139
+ end
140
+
141
+ it "creates a mark with attrs" do
142
+ mark = schema.mark("link", { "href" => "http://example.com" })
143
+ expect(mark.attrs["href"]).to eq("http://example.com")
144
+ end
145
+ end
146
+
147
+ describe "node_from_json" do
148
+ it "deserializes a node from JSON" do
149
+ json = {
150
+ "type" => "paragraph",
151
+ "content" => [
152
+ { "type" => "text", "text" => "Hello" },
153
+ ],
154
+ }
155
+
156
+ node = schema.node_from_json(json)
157
+ expect(node.type.name).to eq("paragraph")
158
+ expect(node.content.content.first.text).to eq("Hello")
159
+ end
160
+
161
+ it "deserializes marks" do
162
+ json = {
163
+ "type" => "paragraph",
164
+ "content" => [
165
+ { "type" => "text", "text" => "Hello",
166
+ "marks" => [{ "type" => "em" }] },
167
+ ],
168
+ }
169
+
170
+ node = schema.node_from_json(json)
171
+ expect(node.content.content.first.marks.length).to eq(1)
172
+ end
173
+ end
174
+
175
+ describe "mark_from_json" do
176
+ it "deserializes a mark from JSON" do
177
+ json = { "type" => "em" }
178
+ mark = schema.mark_from_json(json)
179
+ expect(mark.type.name).to eq("em")
180
+ end
181
+
182
+ it "deserializes mark with attrs" do
183
+ json = { "type" => "link", "attrs" => { "href" => "http://example.com" } }
184
+ mark = schema.mark_from_json(json)
185
+ expect(mark.attrs["href"]).to eq("http://example.com")
186
+ end
187
+ end
188
+
189
+ describe "top_node_type" do
190
+ it "returns the top node type" do
191
+ expect(schema.top_node_type.name).to eq("doc")
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require_relative "../../fixtures/test_builder/helpers"
5
+
6
+ RSpec.describe TestBuilder do # rubocop:disable RSpec/SpecFilePathFormat
7
+ describe ".extract_markers" do
8
+ it "extracts an anchor marker and its position" do
9
+ _content, positions = described_class.extract_markers('doc(p("hello<|a> world"))')
10
+ expect(positions).to include("a")
11
+ expect(positions["a"]).to eq(12)
12
+ end
13
+
14
+ it "extracts a cursor position marker" do
15
+ _content, positions = described_class.extract_markers('doc(p("hello<|> world"))')
16
+ expect(positions).to include(:cursor)
17
+ expect(positions[:cursor]).to eq(12)
18
+ end
19
+
20
+ it "extracts a numbered position marker" do
21
+ _content, positions = described_class.extract_markers('doc(p("hello<1> world"))')
22
+ expect(positions).to include(1)
23
+ expect(positions[1]).to eq(12)
24
+ end
25
+
26
+ it "extracts multiple markers from a single string" do
27
+ _content, positions = described_class.extract_markers('doc(p("hello<|a> world<|b>"))')
28
+ expect(positions).to include("a", "b")
29
+ expect(positions["a"]).to be < positions["b"]
30
+ end
31
+
32
+ it "returns an empty positions hash when no markers are present" do
33
+ _content, positions = described_class.extract_markers('doc(p("hello world"))')
34
+ expect(positions).to be_empty
35
+ end
36
+
37
+ it "returns the content string with all markers removed" do
38
+ content, _positions = described_class.extract_markers('doc(p("hello<|a> world<1>"))')
39
+ expect(content).to eq('doc(p("hello world"))')
40
+ end
41
+
42
+ it "removes a cursor marker from the content string" do
43
+ content, _positions = described_class.extract_markers('doc(p("hello<|> world"))')
44
+ expect(content).to eq('doc(p("hello world"))')
45
+ end
46
+
47
+ it "returns a two-element array of [cleaned_string, positions_hash]" do
48
+ result = described_class.extract_markers('doc(p("hello<|> world"))')
49
+ expect(result).to be_a(Array)
50
+ expect(result.size).to eq(2)
51
+ expect(result[0]).to be_a(String)
52
+ expect(result[1]).to be_a(Hash)
53
+ end
54
+ end
55
+
56
+ describe ".parse" do
57
+ it "delegates to Builder#parse" do
58
+ builder = described_class.for_schema(nil)
59
+ expect(builder).to be_a(TestBuilder::Builder)
60
+ end
61
+
62
+ it "returns nil when extract_content returns nil" do
63
+ # For strings where extract_content cannot find matching outer parens,
64
+ # parse returns nil.
65
+ doc = described_class.parse("hello")
66
+ expect(doc).to be_nil
67
+ end
68
+ end
69
+
70
+ describe ".for_schema" do
71
+ it "returns a Builder instance" do
72
+ builder = described_class.for_schema(nil)
73
+ expect(builder).to be_a(TestBuilder::Builder)
74
+ end
75
+
76
+ it "assigns the schema to the builder" do
77
+ schema = Prosereflect::Schema.new(
78
+ nodes_spec: {
79
+ "doc" => { content: "block+" },
80
+ "paragraph" => { content: "inline*", group: "block" },
81
+ "text" => { group: "inline" },
82
+ },
83
+ marks_spec: {
84
+ "em" => { group: "mark" },
85
+ "strong" => { group: "mark" },
86
+ },
87
+ )
88
+ builder = described_class.for_schema(schema)
89
+ expect(builder.schema).to eq(schema)
90
+ end
91
+
92
+ it "creates a builder with nil schema by default" do
93
+ builder = described_class.for_schema(nil)
94
+ expect(builder.schema).to be_nil
95
+ end
96
+ end
97
+
98
+ describe TestBuilder::Builder do
99
+ describe "#parse_content" do
100
+ it "parses a bare string literal into a string" do
101
+ builder = described_class.new
102
+ result = builder.parse_content('"hello"')
103
+ expect(result).to eq("hello")
104
+ end
105
+ end
106
+
107
+ describe "#schema" do
108
+ it "is nil by default" do
109
+ builder = described_class.new
110
+ expect(builder.schema).to be_nil
111
+ end
112
+
113
+ it "stores the provided schema" do
114
+ schema = Prosereflect::Schema.new(
115
+ nodes_spec: {
116
+ "doc" => { content: "block+" },
117
+ "paragraph" => { content: "inline*", group: "block" },
118
+ "text" => { group: "inline" },
119
+ },
120
+ marks_spec: {},
121
+ )
122
+ builder = described_class.new(schema: schema)
123
+ expect(builder.schema).to eq(schema)
124
+ end
125
+ end
126
+ end
127
+ end