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
data/Gemfile
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
source
|
|
3
|
+
source "https://rubygems.org"
|
|
4
4
|
|
|
5
5
|
# Specify your gem's dependencies in prosereflect.gemspec
|
|
6
6
|
gemspec
|
|
7
7
|
|
|
8
|
-
gem
|
|
9
|
-
gem
|
|
10
|
-
gem
|
|
8
|
+
gem "lutaml-model", github: "lutaml/lutaml-model", branch: "main"
|
|
9
|
+
gem "rake"
|
|
10
|
+
gem "rspec"
|
|
11
|
+
gem "rubocop"
|
|
12
|
+
gem "rubocop-performance"
|
|
13
|
+
gem "rubocop-rake"
|
|
14
|
+
gem "rubocop-rspec"
|
data/README.adoc
CHANGED
|
@@ -14,6 +14,8 @@ accessing the hierarchical document tree structure represented in ProseMirror's
|
|
|
14
14
|
JSON/YAML format. This allows for convenient traversal and extraction of content
|
|
15
15
|
from rich text documents.
|
|
16
16
|
|
|
17
|
+
https://metanorma.org/prosereflect/[Full documentation] is available on the docs site.
|
|
18
|
+
|
|
17
19
|
|
|
18
20
|
== Installation
|
|
19
21
|
|
data/Rakefile
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rspec/core/rake_task"
|
|
5
5
|
|
|
6
6
|
RSpec::Core::RakeTask.new(:spec)
|
|
7
7
|
|
|
8
|
-
require
|
|
8
|
+
require "rubocop/rake_task"
|
|
9
9
|
|
|
10
10
|
RuboCop::RakeTask.new
|
|
11
11
|
|
data/docs/Gemfile
ADDED
data/docs/INDEX.adoc
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: Home
|
|
4
|
+
nav_order: 1
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
= Prosereflect Documentation
|
|
8
|
+
|
|
9
|
+
`prosereflect` is a Ruby gem for working with the document structure used by the https://prosemirror.net/[ProseMirror rich text editor].
|
|
10
|
+
|
|
11
|
+
It provides a set of models and utilities for parsing, manipulating, and accessing the hierarchical document tree structure represented in ProseMirror's JSON/YAML format.
|
|
12
|
+
|
|
13
|
+
== Quick Start
|
|
14
|
+
|
|
15
|
+
Install the gem:
|
|
16
|
+
|
|
17
|
+
[source,sh]
|
|
18
|
+
----
|
|
19
|
+
gem install prosereflect
|
|
20
|
+
----
|
|
21
|
+
|
|
22
|
+
Parse a document:
|
|
23
|
+
|
|
24
|
+
[source,ruby]
|
|
25
|
+
----
|
|
26
|
+
require 'prosereflect'
|
|
27
|
+
|
|
28
|
+
doc = Prosereflect::Parser.parse_document({
|
|
29
|
+
"type" => "doc",
|
|
30
|
+
"content" => [
|
|
31
|
+
{ "type" => "paragraph", "content" => [{ "type" => "text", "text" => "Hello world" }] }
|
|
32
|
+
]
|
|
33
|
+
})
|
|
34
|
+
----
|
|
35
|
+
|
|
36
|
+
== Documentation Sections
|
|
37
|
+
|
|
38
|
+
* x:features:node-types[Node Types] -- All available node types and their attributes
|
|
39
|
+
* x:features:marks[Marks] -- Text formatting marks (bold, italic, links, etc.)
|
|
40
|
+
* x:understanding:document-model[Document Model] -- Node hierarchy and content model
|
|
41
|
+
* x:understanding:fragment[Fragment] -- Flat content collections
|
|
42
|
+
* x:understanding:resolved-position[Resolved Positions] -- Position resolution in the document tree
|
|
43
|
+
* x:advanced:transform[Transforms] -- Document transformations and step-based editing
|
|
44
|
+
* x:advanced:schema[Schema] -- Schema definition and validation
|
|
45
|
+
* x:guides:round-trip-html[HTML Round-Trip] -- Convert between HTML and ProseMirror documents
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: Advanced
|
|
4
|
+
nav_order: 5
|
|
5
|
+
has_children: true
|
|
6
|
+
---
|
|
7
|
+
= Advanced
|
|
8
|
+
|
|
9
|
+
This section covers advanced topics for power users.
|
|
10
|
+
|
|
11
|
+
* x:transform[Transforms] -- Step-based document transformations
|
|
12
|
+
* x:steps[Step Types] -- Individual step types and their behavior
|
|
13
|
+
* x:step-map[Step Maps and Mappings] -- Position tracking through transforms
|
|
14
|
+
* x:schema[Schema] -- Schema definition and content validation
|
|
15
|
+
* x:test-builder[Test Builder] -- DSL for building test documents
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: Schema
|
|
4
|
+
parent: Advanced
|
|
5
|
+
nav_order: 4
|
|
6
|
+
---
|
|
7
|
+
= Schema
|
|
8
|
+
|
|
9
|
+
`Prosereflect::Schema` defines the structure of valid documents: which node types exist, what content they can contain, and which marks are allowed.
|
|
10
|
+
|
|
11
|
+
== Creating a Schema
|
|
12
|
+
|
|
13
|
+
[source,ruby]
|
|
14
|
+
----
|
|
15
|
+
schema = Prosereflect::Schema.new(
|
|
16
|
+
nodes_spec: {
|
|
17
|
+
"doc" => { content: "block+" },
|
|
18
|
+
"paragraph" => { content: "inline*", group: "block" },
|
|
19
|
+
"heading" => { content: "inline*", group: "block", attrs: { "level" => { "default" => 1 } } },
|
|
20
|
+
"text" => { group: "inline" }
|
|
21
|
+
},
|
|
22
|
+
marks_spec: {
|
|
23
|
+
"bold" => {},
|
|
24
|
+
"italic" => {},
|
|
25
|
+
"link" => { attrs: { "href" => {} } }
|
|
26
|
+
}
|
|
27
|
+
)
|
|
28
|
+
----
|
|
29
|
+
|
|
30
|
+
== Requirements
|
|
31
|
+
|
|
32
|
+
Every schema must define:
|
|
33
|
+
|
|
34
|
+
* A `"doc"` node type (the top-level container)
|
|
35
|
+
* A `"text"` node type (inline text)
|
|
36
|
+
* At least one block node type in the doc's content expression
|
|
37
|
+
|
|
38
|
+
== Content Expressions
|
|
39
|
+
|
|
40
|
+
Content expressions define what children a node can have. Supported syntax:
|
|
41
|
+
|
|
42
|
+
* `"block+"` -- one or more block nodes
|
|
43
|
+
* `"inline*"` -- zero or more inline nodes
|
|
44
|
+
* `"paragraph heading"` -- a paragraph followed by a heading
|
|
45
|
+
* `"(paragraph | heading)"` -- either paragraph or heading
|
|
46
|
+
* `"block{2,4}"` -- between 2 and 4 block nodes
|
|
47
|
+
* `"block?"` -- optional block node
|
|
48
|
+
|
|
49
|
+
== NodeType
|
|
50
|
+
|
|
51
|
+
Access and use node types from a schema:
|
|
52
|
+
|
|
53
|
+
[source,ruby]
|
|
54
|
+
----
|
|
55
|
+
para_type = schema.node_type("paragraph")
|
|
56
|
+
para_type.name # => "paragraph"
|
|
57
|
+
para_type.is_block? # => true
|
|
58
|
+
para_type.is_inline? # => false
|
|
59
|
+
para_type.is_textblock? # => true
|
|
60
|
+
para_type.is_leaf? # => false
|
|
61
|
+
para_type.is_atom? # => false
|
|
62
|
+
----
|
|
63
|
+
|
|
64
|
+
Creating validated nodes:
|
|
65
|
+
|
|
66
|
+
[source,ruby]
|
|
67
|
+
----
|
|
68
|
+
# Create with validation
|
|
69
|
+
node = para_type.create_checked(nil, [text_node])
|
|
70
|
+
|
|
71
|
+
# Create with defaults
|
|
72
|
+
node = para_type.create_and_fill
|
|
73
|
+
|
|
74
|
+
# Check content validity
|
|
75
|
+
para_type.valid_content?(fragment)
|
|
76
|
+
----
|
|
77
|
+
|
|
78
|
+
== MarkType
|
|
79
|
+
|
|
80
|
+
Access and use mark types:
|
|
81
|
+
|
|
82
|
+
[source,ruby]
|
|
83
|
+
----
|
|
84
|
+
bold_type = schema.mark_type("bold")
|
|
85
|
+
mark = bold_type.create # Schema::Mark instance
|
|
86
|
+
mark.attrs # => {}
|
|
87
|
+
|
|
88
|
+
link_type = schema.mark_type("link")
|
|
89
|
+
link = link_type.create("href" => "https://example.com")
|
|
90
|
+
link.attrs # => {"href" => "https://example.com"}
|
|
91
|
+
----
|
|
92
|
+
|
|
93
|
+
== ContentMatch
|
|
94
|
+
|
|
95
|
+
`ContentMatch` is the engine that validates content expressions. It uses an NFA/DFA approach to parse and match content expressions:
|
|
96
|
+
|
|
97
|
+
* `match_type(node_type)` -- try to match a node type, returns next match state or nil
|
|
98
|
+
* `match_fragment(fragment)` -- match a full fragment
|
|
99
|
+
* `valid_end` -- whether this state is a valid end state
|
|
100
|
+
* `fill_before(after:, to_end:)` -- compute filler content
|
|
101
|
+
* `find_wrapping(target_type)` -- find wrapping nodes for content
|
|
102
|
+
* `inline_content?` -- whether this match accepts inline content
|
|
103
|
+
* `default_type` -- the default node type for this match
|
|
104
|
+
|
|
105
|
+
== Validation Errors
|
|
106
|
+
|
|
107
|
+
The schema raises `Prosereflect::SchemaErrors::ValidationError` for:
|
|
108
|
+
|
|
109
|
+
* Missing required node types (doc, text)
|
|
110
|
+
* Name collisions between nodes and marks
|
|
111
|
+
* Invalid content in created nodes
|
|
112
|
+
* Missing required attributes
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: Step Maps and Mappings
|
|
4
|
+
parent: Advanced
|
|
5
|
+
nav_order: 3
|
|
6
|
+
---
|
|
7
|
+
= Step Maps and Mappings
|
|
8
|
+
|
|
9
|
+
When steps are applied to a document, positions in the original document may no longer correspond to the same locations. Step maps and mappings track these changes.
|
|
10
|
+
|
|
11
|
+
== StepMap
|
|
12
|
+
|
|
13
|
+
`Prosereflect::Transform::StepMap` maps positions through a single step. It stores an array of ranges `[old_start, old_end, new_start, new_end]`.
|
|
14
|
+
|
|
15
|
+
[source,ruby]
|
|
16
|
+
----
|
|
17
|
+
# Create a step map
|
|
18
|
+
map = Prosereflect::Transform::StepMap.new([[0, 5, 0, 8]])
|
|
19
|
+
|
|
20
|
+
# Map a position
|
|
21
|
+
map.map(3) # => new position after the step
|
|
22
|
+
|
|
23
|
+
# Check if a position was deleted
|
|
24
|
+
map.deleted?(3) # => true/false
|
|
25
|
+
|
|
26
|
+
# Map with deletion info
|
|
27
|
+
result = map.map_result(3)
|
|
28
|
+
result.pos # => new position
|
|
29
|
+
result.deleted # => whether the position was deleted
|
|
30
|
+
result.transformed # => whether the position changed
|
|
31
|
+
----
|
|
32
|
+
|
|
33
|
+
=== Factory Methods
|
|
34
|
+
|
|
35
|
+
* `StepMap.empty` -- identity map (no changes)
|
|
36
|
+
* `StepMap.delete(from, to)` -- map for a deletion
|
|
37
|
+
* `StepMap.replace(from, to, target_from, target_to)` -- map for a replacement
|
|
38
|
+
|
|
39
|
+
== Mapping
|
|
40
|
+
|
|
41
|
+
`Prosereflect::Transform::Mapping` composes multiple step maps to track positions through a full transform chain.
|
|
42
|
+
|
|
43
|
+
[source,ruby]
|
|
44
|
+
----
|
|
45
|
+
mapping = Prosereflect::Transform::Mapping.new
|
|
46
|
+
mapping.add_map(step_map_1)
|
|
47
|
+
mapping.add_map(step_map_2)
|
|
48
|
+
|
|
49
|
+
# Map a position through all steps
|
|
50
|
+
mapping.map(5) # => final position
|
|
51
|
+
|
|
52
|
+
# Map with deletion tracking
|
|
53
|
+
result = mapping.map_result(5)
|
|
54
|
+
result[:pos] # => final position
|
|
55
|
+
result[:deleted] # => whether deleted by any step
|
|
56
|
+
|
|
57
|
+
# Map backwards
|
|
58
|
+
mapping.map_reverse(8) # => original position
|
|
59
|
+
----
|
|
60
|
+
|
|
61
|
+
=== Checking Deletions
|
|
62
|
+
|
|
63
|
+
[source,ruby]
|
|
64
|
+
----
|
|
65
|
+
mapping.map_deletes(5) # => whether position 5 was deleted by any step
|
|
66
|
+
----
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: Step Types
|
|
4
|
+
parent: Advanced
|
|
5
|
+
nav_order: 2
|
|
6
|
+
---
|
|
7
|
+
= Step Types
|
|
8
|
+
|
|
9
|
+
Each step is an atomic document change. All steps inherit from `Prosereflect::Transform::Step`.
|
|
10
|
+
|
|
11
|
+
== Step Base Class
|
|
12
|
+
|
|
13
|
+
Every step implements:
|
|
14
|
+
|
|
15
|
+
* `apply(doc)` -- apply to a document, returns `Result` (ok/fail)
|
|
16
|
+
* `get_map` -- returns a `StepMap` for position tracking
|
|
17
|
+
* `invert(doc)` -- returns a step that undoes this one
|
|
18
|
+
* `merge(other)` -- optionally merge with another step (returns nil if not possible)
|
|
19
|
+
* `to_json` -- serialize to JSON
|
|
20
|
+
* `step_type` -- string identifier for the step type
|
|
21
|
+
|
|
22
|
+
== ReplaceStep
|
|
23
|
+
|
|
24
|
+
Replaces a range of content with a slice:
|
|
25
|
+
|
|
26
|
+
[source,ruby]
|
|
27
|
+
----
|
|
28
|
+
step = Prosereflect::Transform::ReplaceStep.new(from, to, slice)
|
|
29
|
+
result = step.apply(doc)
|
|
30
|
+
# step_type: "replace"
|
|
31
|
+
----
|
|
32
|
+
|
|
33
|
+
== ReplaceAroundStep
|
|
34
|
+
|
|
35
|
+
Replaces content while preserving a "gap" -- used by lift and wrap operations:
|
|
36
|
+
|
|
37
|
+
[source,ruby]
|
|
38
|
+
----
|
|
39
|
+
step = Prosereflect::Transform::ReplaceAroundStep.new(
|
|
40
|
+
from, to, gap_from, gap_to, slice, insert, structure: false
|
|
41
|
+
)
|
|
42
|
+
# step_type: "replaceAround"
|
|
43
|
+
----
|
|
44
|
+
|
|
45
|
+
The `structure` flag prevents the step from overwriting content in the non-gap range.
|
|
46
|
+
|
|
47
|
+
== AddMarkStep / RemoveMarkStep
|
|
48
|
+
|
|
49
|
+
Add or remove a mark over a range:
|
|
50
|
+
|
|
51
|
+
[source,ruby]
|
|
52
|
+
----
|
|
53
|
+
add_step = Prosereflect::Transform::AddMarkStep.new(from, to, mark)
|
|
54
|
+
remove_step = Prosereflect::Transform::RemoveMarkStep.new(from, to, mark)
|
|
55
|
+
# step_types: "addMark", "removeMark"
|
|
56
|
+
----
|
|
57
|
+
|
|
58
|
+
== AttrStep
|
|
59
|
+
|
|
60
|
+
Set attributes on a node at a position:
|
|
61
|
+
|
|
62
|
+
[source,ruby]
|
|
63
|
+
----
|
|
64
|
+
step = Prosereflect::Transform::AttrStep.new(pos, { "level" => 3 })
|
|
65
|
+
# step_type: "attr"
|
|
66
|
+
----
|
|
67
|
+
|
|
68
|
+
== InsertStep / DeleteStep
|
|
69
|
+
|
|
70
|
+
Insert or delete content at a position:
|
|
71
|
+
|
|
72
|
+
[source,ruby]
|
|
73
|
+
----
|
|
74
|
+
insert_step = Prosereflect::Transform::InsertStep.new(pos, content)
|
|
75
|
+
delete_step = Prosereflect::Transform::DeleteStep.new(from, to)
|
|
76
|
+
----
|
|
77
|
+
|
|
78
|
+
== Result
|
|
79
|
+
|
|
80
|
+
`Step::Result` indicates success or failure:
|
|
81
|
+
|
|
82
|
+
[source,ruby]
|
|
83
|
+
----
|
|
84
|
+
result = step.apply(doc)
|
|
85
|
+
result.ok? # => true if successful
|
|
86
|
+
result.doc # => the new document (on success)
|
|
87
|
+
result.failed # => error message (on failure)
|
|
88
|
+
----
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: Test Builder
|
|
4
|
+
parent: Advanced
|
|
5
|
+
nav_order: 5
|
|
6
|
+
---
|
|
7
|
+
= Test Builder
|
|
8
|
+
|
|
9
|
+
`TestBuilder` provides a DSL for constructing ProseMirror documents in tests, inspired by ProseMirror's test-builder.
|
|
10
|
+
|
|
11
|
+
== Basic Usage
|
|
12
|
+
|
|
13
|
+
[source,ruby]
|
|
14
|
+
----
|
|
15
|
+
require 'prosereflect/test_builder'
|
|
16
|
+
|
|
17
|
+
# Parse a document from DSL notation
|
|
18
|
+
doc = TestBuilder.parse('doc(p("hello world"))')
|
|
19
|
+
----
|
|
20
|
+
|
|
21
|
+
== DSL Syntax
|
|
22
|
+
|
|
23
|
+
The DSL uses function-call syntax to build document trees:
|
|
24
|
+
|
|
25
|
+
* `doc(...)` -- document node
|
|
26
|
+
* `p(...)` -- paragraph node
|
|
27
|
+
* `h1(...)` through `h6(...)` -- heading nodes
|
|
28
|
+
* `text(...)` -- text node
|
|
29
|
+
* `table(...)` -- table node
|
|
30
|
+
* `ul(...)` / `ol(...)` -- list nodes
|
|
31
|
+
|
|
32
|
+
Strings in quotes become text nodes:
|
|
33
|
+
|
|
34
|
+
[source,ruby]
|
|
35
|
+
----
|
|
36
|
+
doc(p("hello")) # paragraph with text
|
|
37
|
+
doc(p("hello ", bold("world"))) # paragraph with bold text
|
|
38
|
+
----
|
|
39
|
+
|
|
40
|
+
== Position Markers
|
|
41
|
+
|
|
42
|
+
Insert markers to track positions in tests:
|
|
43
|
+
|
|
44
|
+
* `<|>` -- cursor position
|
|
45
|
+
* `<|name>` -- named anchor
|
|
46
|
+
* `<1>` -- numbered position
|
|
47
|
+
|
|
48
|
+
[source,ruby]
|
|
49
|
+
----
|
|
50
|
+
content, positions = TestBuilder.extract_markers('doc(p("hello<|a> world<|b>"))')
|
|
51
|
+
positions["a"] # => position of first marker
|
|
52
|
+
positions["b"] # => position of second marker
|
|
53
|
+
----
|
|
54
|
+
|
|
55
|
+
== Schema Integration
|
|
56
|
+
|
|
57
|
+
[source,ruby]
|
|
58
|
+
----
|
|
59
|
+
builder = TestBuilder.for_schema(schema)
|
|
60
|
+
doc = builder.parse('doc(p("hello"))')
|
|
61
|
+
----
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: Transforms
|
|
4
|
+
parent: Advanced
|
|
5
|
+
nav_order: 1
|
|
6
|
+
---
|
|
7
|
+
= Transforms
|
|
8
|
+
|
|
9
|
+
`Prosereflect::Transform::Transform` provides a chainable API for composing document changes as a sequence of steps. Each transform accumulates steps and their position mappings.
|
|
10
|
+
|
|
11
|
+
== Creating a Transform
|
|
12
|
+
|
|
13
|
+
[source,ruby]
|
|
14
|
+
----
|
|
15
|
+
doc = Prosereflect::Parser.parse_document(data)
|
|
16
|
+
tx = Prosereflect::Transform::Transform.new(doc)
|
|
17
|
+
----
|
|
18
|
+
|
|
19
|
+
== Operations
|
|
20
|
+
|
|
21
|
+
=== Adding and Removing Marks
|
|
22
|
+
|
|
23
|
+
[source,ruby]
|
|
24
|
+
----
|
|
25
|
+
mark = Prosereflect::Mark::Bold.new
|
|
26
|
+
tx.add_mark(1, 4, mark) # Bold text from position 1 to 4
|
|
27
|
+
tx.remove_mark(1, 4, mark) # Remove bold from position 1 to 4
|
|
28
|
+
----
|
|
29
|
+
|
|
30
|
+
=== Inserting and Deleting
|
|
31
|
+
|
|
32
|
+
[source,ruby]
|
|
33
|
+
----
|
|
34
|
+
tx.insert(5, some_content) # Insert content at position 5
|
|
35
|
+
tx.delete(2, 7) # Delete content from position 2 to 7
|
|
36
|
+
----
|
|
37
|
+
|
|
38
|
+
=== Replacing
|
|
39
|
+
|
|
40
|
+
[source,ruby]
|
|
41
|
+
----
|
|
42
|
+
slice = Prosereflect::Transform::Slice.new(fragment)
|
|
43
|
+
tx.replace(2, 7, slice) # Replace positions 2-7 with slice content
|
|
44
|
+
tx.replace_with(2, 7, node) # Replace positions 2-7 with nodes
|
|
45
|
+
----
|
|
46
|
+
|
|
47
|
+
=== Structural Operations
|
|
48
|
+
|
|
49
|
+
[source,ruby]
|
|
50
|
+
----
|
|
51
|
+
tx.split(5) # Split the document at position 5
|
|
52
|
+
tx.join(5) # Join two nodes at position 5
|
|
53
|
+
tx.lift(range, target_depth) # Lift content out of wrapper
|
|
54
|
+
tx.wrap(range, wrappers) # Wrap content in new nodes
|
|
55
|
+
----
|
|
56
|
+
|
|
57
|
+
=== Setting Attributes
|
|
58
|
+
|
|
59
|
+
[source,ruby]
|
|
60
|
+
----
|
|
61
|
+
tx.set_node_attribute(pos, { "level" => 3 })
|
|
62
|
+
tx.set_doc_attribute({ "meta" => "value" })
|
|
63
|
+
----
|
|
64
|
+
|
|
65
|
+
== Applying Transforms
|
|
66
|
+
|
|
67
|
+
[source,ruby]
|
|
68
|
+
----
|
|
69
|
+
# Apply all steps and get the resulting document
|
|
70
|
+
new_doc = tx.doc
|
|
71
|
+
|
|
72
|
+
# Check step count
|
|
73
|
+
tx.size # => number of accumulated steps
|
|
74
|
+
tx.empty? # => whether any steps exist
|
|
75
|
+
----
|
|
76
|
+
|
|
77
|
+
== Mappings
|
|
78
|
+
|
|
79
|
+
Each step records a `StepMap` describing how positions change. The transform's `mapping` tracks all maps:
|
|
80
|
+
|
|
81
|
+
[source,ruby]
|
|
82
|
+
----
|
|
83
|
+
tx.maps # => array of StepMap objects
|
|
84
|
+
tx.mapping.map(pos) # => map a position through all steps
|
|
85
|
+
----
|
|
86
|
+
|
|
87
|
+
== Rollback
|
|
88
|
+
|
|
89
|
+
[source,ruby]
|
|
90
|
+
----
|
|
91
|
+
tx.rollback # Undo the last step
|
|
92
|
+
----
|