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
data/Gemfile CHANGED
@@ -1,10 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- source 'https://rubygems.org'
3
+ source "https://rubygems.org"
4
4
 
5
5
  # Specify your gem's dependencies in prosereflect.gemspec
6
6
  gemspec
7
7
 
8
- gem 'rake'
9
- gem 'rspec'
10
- gem 'rubocop'
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 'bundler/gem_tasks'
4
- require 'rspec/core/rake_task'
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
5
 
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
 
8
- require 'rubocop/rake_task'
8
+ require "rubocop/rake_task"
9
9
 
10
10
  RuboCop::RakeTask.new
11
11
 
data/docs/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "jekyll", "~> 4.3"
4
+ gem "jekyll-asciidoc"
5
+ gem "just-the-docs"
6
+
7
+ group :jekyll_plugins do
8
+ gem "jekyll-seo-tag"
9
+ gem "jekyll-sitemap"
10
+ end
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
+ ----