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,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