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
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: HTML Round-Trip
|
|
4
|
+
parent: Guides
|
|
5
|
+
nav_order: 1
|
|
6
|
+
---
|
|
7
|
+
= HTML Round-Trip
|
|
8
|
+
|
|
9
|
+
This guide shows how to parse HTML into a ProseMirror document model, modify it, and convert back to HTML.
|
|
10
|
+
|
|
11
|
+
== Parse HTML
|
|
12
|
+
|
|
13
|
+
[source,ruby]
|
|
14
|
+
----
|
|
15
|
+
require 'prosereflect'
|
|
16
|
+
|
|
17
|
+
html = '<p>This is <strong>bold</strong> and <em>italic</em> text.</p>'
|
|
18
|
+
doc = Prosereflect::Input::Html.parse(html)
|
|
19
|
+
----
|
|
20
|
+
|
|
21
|
+
== Navigate and Modify
|
|
22
|
+
|
|
23
|
+
[source,ruby]
|
|
24
|
+
----
|
|
25
|
+
# Access the first paragraph
|
|
26
|
+
para = doc.paragraphs.first
|
|
27
|
+
para.text_content # => "This is bold and italic text."
|
|
28
|
+
|
|
29
|
+
# Append text
|
|
30
|
+
para.add_text(" More content.")
|
|
31
|
+
|
|
32
|
+
# Add a new paragraph
|
|
33
|
+
doc.add_paragraph("Second paragraph.")
|
|
34
|
+
----
|
|
35
|
+
|
|
36
|
+
== Convert Back to HTML
|
|
37
|
+
|
|
38
|
+
[source,ruby]
|
|
39
|
+
----
|
|
40
|
+
modified_html = Prosereflect::Output::Html.convert(doc)
|
|
41
|
+
----
|
|
42
|
+
|
|
43
|
+
== Working with Tables
|
|
44
|
+
|
|
45
|
+
[source,ruby]
|
|
46
|
+
----
|
|
47
|
+
html = '<table><tr><th>Header</th></tr><tr><td>Data</td></tr></table>'
|
|
48
|
+
doc = Prosereflect::Input::Html.parse(html)
|
|
49
|
+
|
|
50
|
+
tables = doc.tables
|
|
51
|
+
first_table = tables.first
|
|
52
|
+
first_table.header_row # => row with TableHeader cells
|
|
53
|
+
first_table.data_rows # => rows with TableCell cells
|
|
54
|
+
----
|
|
55
|
+
|
|
56
|
+
== Round-Trip with Marks
|
|
57
|
+
|
|
58
|
+
[source,ruby]
|
|
59
|
+
----
|
|
60
|
+
html = '<p><a href="https://example.com">click here</a></p>'
|
|
61
|
+
doc = Prosereflect::Input::Html.parse(html)
|
|
62
|
+
|
|
63
|
+
# The link mark is preserved
|
|
64
|
+
para = doc.paragraphs.first
|
|
65
|
+
|
|
66
|
+
# Convert back - links are rendered as <a> tags
|
|
67
|
+
output = Prosereflect::Output::Html.convert(doc)
|
|
68
|
+
----
|
|
69
|
+
|
|
70
|
+
== Common Patterns
|
|
71
|
+
|
|
72
|
+
=== Extract Plain Text
|
|
73
|
+
|
|
74
|
+
[source,ruby]
|
|
75
|
+
----
|
|
76
|
+
doc = Prosereflect::Input::Html.parse(html_content)
|
|
77
|
+
plain_text = doc.text_content
|
|
78
|
+
----
|
|
79
|
+
|
|
80
|
+
=== Find and Replace Text
|
|
81
|
+
|
|
82
|
+
[source,ruby]
|
|
83
|
+
----
|
|
84
|
+
doc = Prosereflect::Input::Html.parse(html_content)
|
|
85
|
+
|
|
86
|
+
doc.paragraphs.each do |para|
|
|
87
|
+
# Access text content
|
|
88
|
+
text = para.text_content
|
|
89
|
+
# Modify via transforms
|
|
90
|
+
end
|
|
91
|
+
----
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: Serialization
|
|
4
|
+
parent: Guides
|
|
5
|
+
nav_order: 3
|
|
6
|
+
---
|
|
7
|
+
= Serialization
|
|
8
|
+
|
|
9
|
+
prosereflect supports round-trip serialization through ProseMirror's JSON/YAML format using the `to_h` / `Parser.parse_document` methods.
|
|
10
|
+
|
|
11
|
+
== to_h (Serialize)
|
|
12
|
+
|
|
13
|
+
Every node can be serialized to a Ruby hash matching ProseMirror's JSON format:
|
|
14
|
+
|
|
15
|
+
[source,ruby]
|
|
16
|
+
----
|
|
17
|
+
doc = Prosereflect::Document.create
|
|
18
|
+
para = doc.add_paragraph("Hello")
|
|
19
|
+
para.add_text(" bold", [Prosereflect::Mark::Bold.new])
|
|
20
|
+
|
|
21
|
+
hash = doc.to_h
|
|
22
|
+
# => {
|
|
23
|
+
# "type" => "doc",
|
|
24
|
+
# "content" => [
|
|
25
|
+
# {
|
|
26
|
+
# "type" => "paragraph",
|
|
27
|
+
# "content" => [
|
|
28
|
+
# { "type" => "text", "text" => "Hello" },
|
|
29
|
+
# { "type" => "text", "text" => " bold", "marks" => [{"type" => "bold"}] }
|
|
30
|
+
# ]
|
|
31
|
+
# }
|
|
32
|
+
# ]
|
|
33
|
+
# }
|
|
34
|
+
----
|
|
35
|
+
|
|
36
|
+
== parse_document (Deserialize)
|
|
37
|
+
|
|
38
|
+
Convert a ProseMirror hash back to node objects:
|
|
39
|
+
|
|
40
|
+
[source,ruby]
|
|
41
|
+
----
|
|
42
|
+
doc = Prosereflect::Parser.parse_document(hash)
|
|
43
|
+
doc.paragraphs.first.text_content # => "Hello bold"
|
|
44
|
+
----
|
|
45
|
+
|
|
46
|
+
Works with YAML and JSON strings too:
|
|
47
|
+
|
|
48
|
+
[source,ruby]
|
|
49
|
+
----
|
|
50
|
+
# From YAML
|
|
51
|
+
doc = Prosereflect::Parser.parse_document(YAML.safe_load(yaml_string))
|
|
52
|
+
|
|
53
|
+
# From JSON
|
|
54
|
+
doc = Prosereflect::Parser.parse_document(JSON.parse(json_string))
|
|
55
|
+
----
|
|
56
|
+
|
|
57
|
+
== YAML Round-Trip
|
|
58
|
+
|
|
59
|
+
[source,ruby]
|
|
60
|
+
----
|
|
61
|
+
# Serialize to YAML
|
|
62
|
+
yaml = doc.to_yaml
|
|
63
|
+
|
|
64
|
+
# Deserialize from YAML
|
|
65
|
+
data = YAML.safe_load(yaml)
|
|
66
|
+
doc = Prosereflect::Parser.parse_document(data)
|
|
67
|
+
|
|
68
|
+
# Verify round-trip
|
|
69
|
+
doc.to_h == original_doc.to_h # => true
|
|
70
|
+
----
|
|
71
|
+
|
|
72
|
+
== JSON Round-Trip
|
|
73
|
+
|
|
74
|
+
[source,ruby]
|
|
75
|
+
----
|
|
76
|
+
require 'json'
|
|
77
|
+
|
|
78
|
+
# Serialize to JSON
|
|
79
|
+
json = JSON.pretty_generate(doc.to_h)
|
|
80
|
+
|
|
81
|
+
# Deserialize from JSON
|
|
82
|
+
data = JSON.parse(json)
|
|
83
|
+
doc = Prosereflect::Parser.parse_document(data)
|
|
84
|
+
----
|
|
85
|
+
|
|
86
|
+
== Step Serialization
|
|
87
|
+
|
|
88
|
+
Transform steps can be serialized to JSON:
|
|
89
|
+
|
|
90
|
+
[source,ruby]
|
|
91
|
+
----
|
|
92
|
+
step = Prosereflect::Transform::ReplaceStep.new(2, 5, slice)
|
|
93
|
+
json = step.to_json
|
|
94
|
+
# => {"stepType" => "replace", "from" => 2, "to" => 5, "slice" => [...]}
|
|
95
|
+
|
|
96
|
+
# Deserialize
|
|
97
|
+
step = Prosereflect::Transform::ReplaceStep.from_json(schema, json)
|
|
98
|
+
----
|
|
99
|
+
|
|
100
|
+
== to_h Behavior
|
|
101
|
+
|
|
102
|
+
The `to_h` method includes:
|
|
103
|
+
|
|
104
|
+
* `type` -- always included
|
|
105
|
+
* `attrs` -- only if non-empty
|
|
106
|
+
* `marks` -- only if non-empty
|
|
107
|
+
* `content` -- only if non-empty
|
|
108
|
+
|
|
109
|
+
This matches ProseMirror's JSON serialization format where omitted fields use defaults.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: Getting Started
|
|
4
|
+
nav_order: 2
|
|
5
|
+
---
|
|
6
|
+
= Getting Started
|
|
7
|
+
|
|
8
|
+
== What is prosereflect?
|
|
9
|
+
|
|
10
|
+
`prosereflect` is a Ruby gem for working with the document structure used by the https://prosemirror.net/[ProseMirror rich text editor].
|
|
11
|
+
|
|
12
|
+
It provides a complete Ruby object model for ProseMirror documents, including:
|
|
13
|
+
|
|
14
|
+
* Parsing and serialization (JSON, YAML, HTML)
|
|
15
|
+
* A rich node hierarchy with type-safe accessors
|
|
16
|
+
* A full Transform system for step-based document manipulation
|
|
17
|
+
* Schema definition and content validation
|
|
18
|
+
* Position resolution and mapping
|
|
19
|
+
* HTML input and output with whitespace handling
|
|
20
|
+
* A test builder DSL for constructing test documents
|
|
21
|
+
|
|
22
|
+
== Installation
|
|
23
|
+
|
|
24
|
+
Add to your Gemfile:
|
|
25
|
+
|
|
26
|
+
[source,ruby]
|
|
27
|
+
----
|
|
28
|
+
gem 'prosereflect'
|
|
29
|
+
----
|
|
30
|
+
|
|
31
|
+
Or install directly:
|
|
32
|
+
|
|
33
|
+
[source,sh]
|
|
34
|
+
----
|
|
35
|
+
gem install prosereflect
|
|
36
|
+
----
|
|
37
|
+
|
|
38
|
+
== Quick Example
|
|
39
|
+
|
|
40
|
+
[source,ruby]
|
|
41
|
+
----
|
|
42
|
+
require 'prosereflect'
|
|
43
|
+
|
|
44
|
+
# Create a document
|
|
45
|
+
doc = Prosereflect::Document.create
|
|
46
|
+
doc.add_paragraph("Hello world")
|
|
47
|
+
|
|
48
|
+
# Parse from JSON hash
|
|
49
|
+
doc = Prosereflect::Parser.parse_document({
|
|
50
|
+
"type" => "doc",
|
|
51
|
+
"content" => [
|
|
52
|
+
{ "type" => "paragraph", "content" => [{ "type" => "text", "text" => "Hello" }] }
|
|
53
|
+
]
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
# Parse from HTML
|
|
57
|
+
doc = Prosereflect::Input::Html.parse('<p>Hello <strong>world</strong></p>')
|
|
58
|
+
|
|
59
|
+
# Convert to HTML
|
|
60
|
+
html = Prosereflect::Output::Html.convert(doc)
|
|
61
|
+
----
|
|
62
|
+
|
|
63
|
+
== Next Steps
|
|
64
|
+
|
|
65
|
+
* x:features:node-types[Node Types] -- all available node types
|
|
66
|
+
* x:understanding:document-model[Document Model] -- how the document tree works
|
|
67
|
+
* x:guides:round-trip-html[HTML Round-Trip] -- convert between HTML and ProseMirror
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: Document API
|
|
4
|
+
parent: Reference
|
|
5
|
+
nav_order: 2
|
|
6
|
+
---
|
|
7
|
+
= Document API
|
|
8
|
+
|
|
9
|
+
== Prosereflect::Document
|
|
10
|
+
|
|
11
|
+
Top-level document container. Inherits from Node with PM_TYPE "doc".
|
|
12
|
+
|
|
13
|
+
=== Class Methods
|
|
14
|
+
|
|
15
|
+
[cols="1,3"]
|
|
16
|
+
|===
|
|
17
|
+
| Method | Description
|
|
18
|
+
|
|
19
|
+
| `Document.create(attrs)` | Create an empty document
|
|
20
|
+
|===
|
|
21
|
+
|
|
22
|
+
=== Query Methods
|
|
23
|
+
|
|
24
|
+
[cols="1,3"]
|
|
25
|
+
|===
|
|
26
|
+
| Method | Description
|
|
27
|
+
|
|
28
|
+
| `tables` | Find all direct child Table nodes
|
|
29
|
+
| `paragraphs` | Find all direct child Paragraph nodes
|
|
30
|
+
| `text_content` | Plain text from all nodes, joined by newlines
|
|
31
|
+
|===
|
|
32
|
+
|
|
33
|
+
=== Builder Methods
|
|
34
|
+
|
|
35
|
+
[cols="1,3"]
|
|
36
|
+
|===
|
|
37
|
+
| Method | Description
|
|
38
|
+
|
|
39
|
+
| `add_paragraph(text, attrs)` | Add a paragraph, optionally with text
|
|
40
|
+
| `add_heading(level)` | Add a heading with the given level
|
|
41
|
+
| `add_table(attrs)` | Add a table
|
|
42
|
+
| `add_image(src, alt)` | Add an image
|
|
43
|
+
| `add_bullet_list(attrs)` | Add a bullet list
|
|
44
|
+
| `add_ordered_list(attrs)` | Add an ordered list
|
|
45
|
+
| `add_blockquote(attrs)` | Add a blockquote
|
|
46
|
+
| `add_horizontal_rule(attrs)` | Add a horizontal rule
|
|
47
|
+
| `add_code_block_wrapper(attrs)` | Add a code block wrapper
|
|
48
|
+
| `add_user(id)` | Add a user mention node
|
|
49
|
+
|===
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: Reference
|
|
4
|
+
nav_order: 7
|
|
5
|
+
has_children: true
|
|
6
|
+
---
|
|
7
|
+
= Reference
|
|
8
|
+
|
|
9
|
+
API reference documentation organized by module.
|
|
10
|
+
|
|
11
|
+
* x:node-api[Node API] -- Node base class and Document
|
|
12
|
+
* x:document-api[Document API] -- Document convenience methods
|
|
13
|
+
* x:transform-api[Transform API] -- Transform and Step classes
|
|
14
|
+
* x:schema-api[Schema API] -- Schema, NodeType, MarkType, ContentMatch
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: Node API
|
|
4
|
+
parent: Reference
|
|
5
|
+
nav_order: 1
|
|
6
|
+
---
|
|
7
|
+
= Node API
|
|
8
|
+
|
|
9
|
+
== Prosereflect::Node
|
|
10
|
+
|
|
11
|
+
Base class for all document elements.
|
|
12
|
+
|
|
13
|
+
=== Attributes
|
|
14
|
+
|
|
15
|
+
* `type` (String) -- The node type (e.g. "doc", "paragraph", "text")
|
|
16
|
+
* `attrs` (Hash) -- Node-specific attributes
|
|
17
|
+
* `marks` (Array) -- Formatting marks applied to this node
|
|
18
|
+
* `content` (Array) -- Child nodes
|
|
19
|
+
|
|
20
|
+
=== Class Methods
|
|
21
|
+
|
|
22
|
+
[cols="1,3"]
|
|
23
|
+
|===
|
|
24
|
+
| Method | Description
|
|
25
|
+
|
|
26
|
+
| `Node.create(type, attrs)` | Create a new node with the given type and attrs
|
|
27
|
+
| `Node.from_json(schema, json)` | Deserialize from JSON (requires schema)
|
|
28
|
+
|===
|
|
29
|
+
|
|
30
|
+
=== Instance Methods
|
|
31
|
+
|
|
32
|
+
[cols="1,3"]
|
|
33
|
+
|===
|
|
34
|
+
| Method | Description
|
|
35
|
+
|
|
36
|
+
| `node_size` | Size in document position space
|
|
37
|
+
| `text?` | Whether this is a text node
|
|
38
|
+
| `text_content` | Combined text of all descendant text nodes
|
|
39
|
+
| `cut(from, to)` | Return a copy restricted to position range
|
|
40
|
+
| `copy(content)` | Return a copy with different content
|
|
41
|
+
| `add_child(node)` | Append a child node
|
|
42
|
+
| `find_first(type)` | Find first descendant of type
|
|
43
|
+
| `find_all(type)` | Find all descendants of type
|
|
44
|
+
| `find_children(klass)` | Find direct children of class
|
|
45
|
+
| `resolve(pos)` | Resolve position to ResolvedPos
|
|
46
|
+
| `nodes_between(from, to, &block)` | Iterate nodes in position range
|
|
47
|
+
| `descendants(&block)` | Iterate all descendant nodes
|
|
48
|
+
| `eq?(other)` | Structural equality
|
|
49
|
+
| `to_h` | Serialize to ProseMirror hash
|
|
50
|
+
| `to_yaml` | Serialize to YAML
|
|
51
|
+
|===
|
|
52
|
+
|
|
53
|
+
== Prosereflect::Text
|
|
54
|
+
|
|
55
|
+
Text node (PM_TYPE: "text").
|
|
56
|
+
|
|
57
|
+
[cols="1,3"]
|
|
58
|
+
|===
|
|
59
|
+
| Method | Description
|
|
60
|
+
|
|
61
|
+
| `text` | The text string
|
|
62
|
+
| `node_size` | `text.length + 1`
|
|
63
|
+
| `text?` | Always `true`
|
|
64
|
+
|===
|
|
65
|
+
|
|
66
|
+
== Prosereflect::Mark::Base
|
|
67
|
+
|
|
68
|
+
Base class for all marks.
|
|
69
|
+
|
|
70
|
+
[cols="1,3"]
|
|
71
|
+
|===
|
|
72
|
+
| Method | Description
|
|
73
|
+
|
|
74
|
+
| `type` | The mark type string
|
|
75
|
+
| `attrs` | Mark attributes (e.g. href for links)
|
|
76
|
+
| `to_h` | Serialize to hash
|
|
77
|
+
|===
|
|
78
|
+
|
|
79
|
+
Mark subclasses: `Bold`, `Italic`, `Code`, `Link`, `Strike`, `Subscript`, `Superscript`, `Underline`.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: Schema API
|
|
4
|
+
parent: Reference
|
|
5
|
+
nav_order: 4
|
|
6
|
+
---
|
|
7
|
+
= Schema API
|
|
8
|
+
|
|
9
|
+
== Prosereflect::Schema
|
|
10
|
+
|
|
11
|
+
=== Constructor
|
|
12
|
+
|
|
13
|
+
`Schema.new(nodes_spec:, marks_spec:, top_node: nil)`
|
|
14
|
+
|
|
15
|
+
[source,ruby]
|
|
16
|
+
----
|
|
17
|
+
schema = Prosereflect::Schema.new(
|
|
18
|
+
nodes_spec: {
|
|
19
|
+
"doc" => { content: "block+" },
|
|
20
|
+
"paragraph" => { content: "inline*", group: "block" },
|
|
21
|
+
"text" => { group: "inline" }
|
|
22
|
+
},
|
|
23
|
+
marks_spec: { "bold" => {}, "italic" => {} }
|
|
24
|
+
)
|
|
25
|
+
----
|
|
26
|
+
|
|
27
|
+
=== Methods
|
|
28
|
+
|
|
29
|
+
[cols="1,3"]
|
|
30
|
+
|===
|
|
31
|
+
| Method | Description
|
|
32
|
+
|
|
33
|
+
| `node_type(name)` | Get NodeType by name (raises on unknown)
|
|
34
|
+
| `mark_type(name)` | Get MarkType by name (raises on unknown)
|
|
35
|
+
| `top_node_type` | The top-level NodeType (usually "doc")
|
|
36
|
+
| `node(type, attrs, content, marks)` | Create a validated node
|
|
37
|
+
| `text(text, marks)` | Create a text node
|
|
38
|
+
| `mark(type, attrs)` | Create a mark
|
|
39
|
+
| `node_from_json(json)` | Deserialize node from JSON
|
|
40
|
+
| `mark_from_json(json)` | Deserialize mark from JSON
|
|
41
|
+
|===
|
|
42
|
+
|
|
43
|
+
== Schema::NodeType
|
|
44
|
+
|
|
45
|
+
[cols="1,3"]
|
|
46
|
+
|===
|
|
47
|
+
| Method | Description
|
|
48
|
+
|
|
49
|
+
| `name` | Node type name
|
|
50
|
+
| `attrs` | Hash of Attribute definitions
|
|
51
|
+
| `schema` | Parent Schema
|
|
52
|
+
| `content_match` | ContentMatch for valid children
|
|
53
|
+
| `is_block?` | Whether a block node
|
|
54
|
+
| `is_inline?` | Whether an inline node
|
|
55
|
+
| `is_textblock?` | Whether a block with inline content
|
|
56
|
+
| `is_leaf?` | Whether a leaf node (no children)
|
|
57
|
+
| `is_atom?` | Whether atomic (leaf or atom flag)
|
|
58
|
+
| `text?` | Whether text type
|
|
59
|
+
| `in_group?(name)` | Whether in a named group
|
|
60
|
+
| `create(attrs, content, marks)` | Create node (no validation)
|
|
61
|
+
| `create_checked(attrs, content, marks)` | Create with content validation
|
|
62
|
+
| `create_and_fill(attrs, content, marks)` | Create with default content
|
|
63
|
+
| `valid_content?(fragment)` | Check content validity
|
|
64
|
+
| `allows_mark_type?(mark_type)` | Check if mark type is allowed
|
|
65
|
+
|===
|
|
66
|
+
|
|
67
|
+
== Schema::MarkType
|
|
68
|
+
|
|
69
|
+
[cols="1,3"]
|
|
70
|
+
|===
|
|
71
|
+
| Method | Description
|
|
72
|
+
|
|
73
|
+
| `name` | Mark type name
|
|
74
|
+
| `rank` | Ordering rank
|
|
75
|
+
| `schema` | Parent Schema
|
|
76
|
+
| `create(attrs)` | Create a Schema::Mark instance
|
|
77
|
+
| `excluded` | Set of excluded mark types
|
|
78
|
+
|===
|
|
79
|
+
|
|
80
|
+
== Schema::ContentMatch
|
|
81
|
+
|
|
82
|
+
[cols="1,3"]
|
|
83
|
+
|===
|
|
84
|
+
| Method | Description
|
|
85
|
+
|
|
86
|
+
| `valid_end` | Whether this is a valid terminal state
|
|
87
|
+
| `match_type(node_type)` | Match a node type, returns next state or nil
|
|
88
|
+
| `match_fragment(fragment)` | Match a full fragment
|
|
89
|
+
| `fill_before(after:, to_end:)` | Compute filler content
|
|
90
|
+
| `find_wrapping(target_type)` | Find wrapping for content
|
|
91
|
+
| `inline_content?` | Whether accepts inline content
|
|
92
|
+
| `default_type` | Default NodeType
|
|
93
|
+
| `edge_count` | Number of edges
|
|
94
|
+
| `edge(n)` | Edge at index
|
|
95
|
+
|===
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: Transform API
|
|
4
|
+
parent: Reference
|
|
5
|
+
nav_order: 3
|
|
6
|
+
---
|
|
7
|
+
= Transform API
|
|
8
|
+
|
|
9
|
+
== Prosereflect::Transform::Transform
|
|
10
|
+
|
|
11
|
+
=== Constructor
|
|
12
|
+
|
|
13
|
+
`Transform.new(doc)` -- create a transform chain for the given document.
|
|
14
|
+
|
|
15
|
+
=== Step-adding Methods
|
|
16
|
+
|
|
17
|
+
[cols="1,3"]
|
|
18
|
+
|===
|
|
19
|
+
| Method | Description
|
|
20
|
+
|
|
21
|
+
| `add_mark(from, to, mark)` | Add a mark to range
|
|
22
|
+
| `remove_mark(from, to, mark)` | Remove a mark from range
|
|
23
|
+
| `insert(pos, content)` | Insert content at position
|
|
24
|
+
| `delete(from, to)` | Delete content in range
|
|
25
|
+
| `replace(from, to, slice)` | Replace range with slice
|
|
26
|
+
| `replace_with(from, to, *nodes)` | Replace range with nodes
|
|
27
|
+
| `set_node_attribute(pos, attrs)` | Set attributes on node at pos
|
|
28
|
+
| `set_doc_attribute(attrs)` | Set document-level attributes
|
|
29
|
+
| `split(pos, depth)` | Split at position
|
|
30
|
+
| `join(pos, depth)` | Join nodes at position
|
|
31
|
+
| `lift(range, target)` | Lift content out of wrapper
|
|
32
|
+
| `wrap(range, wrappers)` | Wrap content in new nodes
|
|
33
|
+
|===
|
|
34
|
+
|
|
35
|
+
=== Access Methods
|
|
36
|
+
|
|
37
|
+
[cols="1,3"]
|
|
38
|
+
|===
|
|
39
|
+
| Method | Description
|
|
40
|
+
|
|
41
|
+
| `doc` | Apply steps and return resulting document
|
|
42
|
+
| `steps` | Array of accumulated Step objects
|
|
43
|
+
| `mapping` | The Mapping tracking position changes
|
|
44
|
+
| `maps` | Array of StepMap objects
|
|
45
|
+
| `size` | Number of accumulated steps
|
|
46
|
+
| `empty?` | Whether no steps are accumulated
|
|
47
|
+
|===
|
|
48
|
+
|
|
49
|
+
=== Other Methods
|
|
50
|
+
|
|
51
|
+
[cols="1,3"]
|
|
52
|
+
|===
|
|
53
|
+
| Method | Description
|
|
54
|
+
|
|
55
|
+
| `apply` | Apply all steps (returns self)
|
|
56
|
+
| `rollback` | Undo the last step
|
|
57
|
+
| `clone` | Create a new Transform with same doc
|
|
58
|
+
| `can_apply?` | Check if all steps can be applied
|
|
59
|
+
|===
|
|
60
|
+
|
|
61
|
+
== Step Types
|
|
62
|
+
|
|
63
|
+
All inherit from `Prosereflect::Transform::Step`.
|
|
64
|
+
|
|
65
|
+
* `ReplaceStep` -- replace range with slice
|
|
66
|
+
* `ReplaceAroundStep` -- replace around a gap
|
|
67
|
+
* `AddMarkStep` -- add mark to range
|
|
68
|
+
* `RemoveMarkStep` -- remove mark from range
|
|
69
|
+
* `AttrStep` -- set node attributes
|
|
70
|
+
* `InsertStep` -- insert content at position
|
|
71
|
+
* `DeleteStep` -- delete content in range
|
|
72
|
+
|
|
73
|
+
== Supporting Types
|
|
74
|
+
|
|
75
|
+
* `Slice` -- document slice with open boundaries
|
|
76
|
+
* `StepMap` -- position mapping for a single step
|
|
77
|
+
* `Mapping` -- composed position mapping across multiple steps
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: Document Model
|
|
4
|
+
parent: Understanding
|
|
5
|
+
nav_order: 1
|
|
6
|
+
---
|
|
7
|
+
= Document Model
|
|
8
|
+
|
|
9
|
+
prosereflect models a ProseMirror document as a tree of Node objects. Every node has a `type`, optional `attrs`, optional `marks`, and optional `content` (child nodes).
|
|
10
|
+
|
|
11
|
+
== Node Hierarchy
|
|
12
|
+
|
|
13
|
+
----
|
|
14
|
+
Document
|
|
15
|
+
├── Paragraph
|
|
16
|
+
│ ├── Text (with marks)
|
|
17
|
+
│ └── Text
|
|
18
|
+
├── Heading (level: 1-6)
|
|
19
|
+
│ └── Text
|
|
20
|
+
├── Table
|
|
21
|
+
│ ├── TableRow
|
|
22
|
+
│ │ ├── TableHeader
|
|
23
|
+
│ │ └── TableCell
|
|
24
|
+
│ │ └── Paragraph
|
|
25
|
+
│ └── TableRow
|
|
26
|
+
├── BulletList / OrderedList
|
|
27
|
+
│ └── ListItem
|
|
28
|
+
│ └── Paragraph
|
|
29
|
+
├── Blockquote
|
|
30
|
+
│ └── Paragraph
|
|
31
|
+
├── CodeBlockWrapper
|
|
32
|
+
│ └── CodeBlock
|
|
33
|
+
├── Image
|
|
34
|
+
├── HorizontalRule
|
|
35
|
+
└── ...
|
|
36
|
+
----
|
|
37
|
+
|
|
38
|
+
== Node Size
|
|
39
|
+
|
|
40
|
+
Each node has a `node_size` that represents its footprint in the document position space:
|
|
41
|
+
|
|
42
|
+
* **Non-text nodes**: `1` (opening token) + sum of children's `node_size`
|
|
43
|
+
* **Text nodes**: `text.length + 1`
|
|
44
|
+
|
|
45
|
+
Example: A document with one paragraph containing "hi" has `node_size` of 5:
|
|
46
|
+
|
|
47
|
+
[source,ruby]
|
|
48
|
+
----
|
|
49
|
+
doc.node_size # 1 (doc) + 1 (para open) + 3 ("hi" + 1) = 5
|
|
50
|
+
----
|
|
51
|
+
|
|
52
|
+
== Positions
|
|
53
|
+
|
|
54
|
+
Positions are integer offsets into the document tree. Position 0 is before the first child of the root. Each non-text node contributes its opening token (position +1) and its content. Text nodes contribute `text.length + 1`.
|
|
55
|
+
|
|
56
|
+
[source,ruby]
|
|
57
|
+
----
|
|
58
|
+
# doc(p("abc"))
|
|
59
|
+
# Position 0: before doc content
|
|
60
|
+
# Position 1: before paragraph content
|
|
61
|
+
# Position 2: before "a"
|
|
62
|
+
# Position 3: between "a" and "b"
|
|
63
|
+
# Position 4: between "b" and "c"
|
|
64
|
+
# Position 5: after "c" (end of text)
|
|
65
|
+
----
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: Fragment
|
|
4
|
+
parent: Understanding
|
|
5
|
+
nav_order: 2
|
|
6
|
+
---
|
|
7
|
+
= Fragment
|
|
8
|
+
|
|
9
|
+
`Prosereflect::Fragment` represents a flat sequence of nodes. It is the primary data structure for node content and slice content.
|
|
10
|
+
|
|
11
|
+
== Creating Fragments
|
|
12
|
+
|
|
13
|
+
[source,ruby]
|
|
14
|
+
----
|
|
15
|
+
# From an array of nodes
|
|
16
|
+
frag = Prosereflect::Fragment.new([node1, node2])
|
|
17
|
+
|
|
18
|
+
# Empty fragment
|
|
19
|
+
frag = Prosereflect::Fragment.empty
|
|
20
|
+
|
|
21
|
+
# From arbitrary content
|
|
22
|
+
frag = Prosereflect::Fragment.from(node)
|
|
23
|
+
frag = Prosereflect::Fragment.from([node1, node2])
|
|
24
|
+
frag = Prosereflect::Fragment.from(existing_fragment) # returns as-is
|
|
25
|
+
----
|
|
26
|
+
|
|
27
|
+
== Core Methods
|
|
28
|
+
|
|
29
|
+
* `size` -- total node_size of all contained nodes
|
|
30
|
+
* `empty?` -- whether the fragment has no nodes
|
|
31
|
+
* `length` / `count` -- number of nodes
|
|
32
|
+
* `to_a` -- convert to a plain array
|
|
33
|
+
* `each` -- iterate over nodes
|
|
34
|
+
* `[](index)` -- access node by index
|
|
35
|
+
|
|
36
|
+
== Manipulation
|
|
37
|
+
|
|
38
|
+
* `append(other)` -- concatenate two fragments (returns new Fragment)
|
|
39
|
+
* `cut(from, to)` -- extract a sub-fragment within position range
|
|
40
|
+
* `replace_child(index, replacement)` -- replace a node at index
|
|
41
|
+
|
|
42
|
+
== Traversal
|
|
43
|
+
|
|
44
|
+
* `nodes_between(from, to, &block)` -- iterate nodes within position range, callback receives `(node, position)`
|
|
45
|
+
* `descendants(&block)` -- iterate all descendant nodes
|
|
46
|
+
* `text_between(from, to)` -- extract text between positions
|
|
47
|
+
|
|
48
|
+
== Comparison
|
|
49
|
+
|
|
50
|
+
* `eq?(other)` -- structural equality check
|
|
51
|
+
* `find_diff_start(other)` -- find first position where two fragments differ
|
|
52
|
+
* `find_diff_end(other)` -- find last position where two fragments differ
|