This commit is contained in:
2025-01-04 00:34:03 +01:00
parent 41829408dc
commit 0ca14bbc19
18111 changed files with 1871397 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
{
"libs": ["browser"],
"plugins": {
"node": {},
"complete_strings": {},
"es_modules": {}
}
}

View File

@@ -0,0 +1,104 @@
# How to contribute
- [Getting help](#getting-help)
- [Submitting bug reports](#submitting-bug-reports)
- [Contributing code](#contributing-code)
## Getting help
Community discussion, questions, and informal bug reporting is done on the
[discuss.ProseMirror forum](http://discuss.prosemirror.net).
## Submitting bug reports
Report bugs on the
[GitHub issue tracker](http://github.com/prosemirror/prosemirror/issues).
Before reporting a bug, please read these pointers.
- The issue tracker is for *bugs*, not requests for help. Questions
should be asked on the [forum](http://discuss.prosemirror.net).
- Include information about the version of the code that exhibits the
problem. For browser-related issues, include the browser and browser
version on which the problem occurred.
- Mention very precisely what went wrong. "X is broken" is not a good
bug report. What did you expect to happen? What happened instead?
Describe the exact steps a maintainer has to take to make the
problem occur. A screencast can be useful, but is no substitute for
a textual description.
- A great way to make it easy to reproduce your problem, if it can not
be trivially reproduced on the website demos, is to submit a script
that triggers the issue.
## Contributing code
If you want to make a change that involves a significant overhaul of
the code or introduces a user-visible new feature, create an
[RFC](https://github.com/ProseMirror/rfcs/) first with your proposal.
- Make sure you have a [GitHub Account](https://github.com/signup/free)
- Fork the relevant repository
([how to fork a repo](https://help.github.com/articles/fork-a-repo))
- Create a local checkout of the code. You can use the
[main repository](https://github.com/prosemirror/prosemirror) to
easily check out all core modules.
- Make your changes, and commit them
- Follow the code style of the rest of the project (see below). Run
`npm run lint` (in the main repository checkout) to make sure that
the linter is happy.
- If your changes are easy to test or likely to regress, add tests in
the relevant `test/` directory. Either put them in an existing
`test-*.js` file, if they fit there, or add a new file.
- Make sure all tests pass. Run `npm run test` to verify tests pass
(you will need Node.js v6+).
- Submit a pull request ([how to create a pull request](https://help.github.com/articles/fork-a-repo)).
Don't put more than one feature/fix in a single pull request.
By contributing code to ProseMirror you
- Agree to license the contributed code under the project's [MIT
license](https://github.com/ProseMirror/prosemirror/blob/master/LICENSE).
- Confirm that you have the right to contribute and license the code
in question. (Either you hold all rights on the code, or the rights
holder has explicitly granted the right to use it like this,
through a compatible open source license or through a direct
agreement with you.)
### Coding standards
- ES6 syntax, targeting an ES5 runtime (i.e. don't use library
elements added by ES6, don't use ES7/ES.next syntax).
- 2 spaces per indentation level, no tabs.
- No semicolons except when necessary.
- Follow the surrounding code when it comes to spacing, brace
placement, etc.
- Brace-less single-statement bodies are encouraged (whenever they
don't impact readability).
- [getdocs](https://github.com/marijnh/getdocs)-style doc comments
above items that are part of the public API.
- When documenting non-public items, you can put the type after a
single colon, so that getdocs doesn't pick it up and add it to the
API reference.
- The linter (`npm run lint`) complains about unused variables and
functions. Prefix their names with an underscore to muffle it.
- ProseMirror does *not* follow JSHint or JSLint prescribed style.
Patches that try to 'fix' code to pass one of these linters will not
be accepted.

View File

@@ -0,0 +1,19 @@
Copyright (C) 2015-2017 by Marijn Haverbeke <marijn@haverbeke.berlin> and others
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,839 @@
import { Node, Schema, Slice, Fragment, NodeRange, NodeType, Attrs, Mark, MarkType, ContentMatch } from 'prosemirror-model';
/**
There are several things that positions can be mapped through.
Such objects conform to this interface.
*/
interface Mappable {
/**
Map a position through this object. When given, `assoc` (should
be -1 or 1, defaults to 1) determines with which side the
position is associated, which determines in which direction to
move when a chunk of content is inserted at the mapped position.
*/
map: (pos: number, assoc?: number) => number;
/**
Map a position, and return an object containing additional
information about the mapping. The result's `deleted` field tells
you whether the position was deleted (completely enclosed in a
replaced range) during the mapping. When content on only one side
is deleted, the position itself is only considered deleted when
`assoc` points in the direction of the deleted content.
*/
mapResult: (pos: number, assoc?: number) => MapResult;
}
/**
An object representing a mapped position with extra
information.
*/
declare class MapResult {
/**
The mapped version of the position.
*/
readonly pos: number;
/**
Tells you whether the position was deleted, that is, whether the
step removed the token on the side queried (via the `assoc`)
argument from the document.
*/
get deleted(): boolean;
/**
Tells you whether the token before the mapped position was deleted.
*/
get deletedBefore(): boolean;
/**
True when the token after the mapped position was deleted.
*/
get deletedAfter(): boolean;
/**
Tells whether any of the steps mapped through deletes across the
position (including both the token before and after the
position).
*/
get deletedAcross(): boolean;
}
/**
A map describing the deletions and insertions made by a step, which
can be used to find the correspondence between positions in the
pre-step version of a document and the same position in the
post-step version.
*/
declare class StepMap implements Mappable {
/**
Create a position map. The modifications to the document are
represented as an array of numbers, in which each group of three
represents a modified chunk as `[start, oldSize, newSize]`.
*/
constructor(
/**
@internal
*/
ranges: readonly number[],
/**
@internal
*/
inverted?: boolean);
mapResult(pos: number, assoc?: number): MapResult;
map(pos: number, assoc?: number): number;
/**
Calls the given function on each of the changed ranges included in
this map.
*/
forEach(f: (oldStart: number, oldEnd: number, newStart: number, newEnd: number) => void): void;
/**
Create an inverted version of this map. The result can be used to
map positions in the post-step document to the pre-step document.
*/
invert(): StepMap;
/**
Create a map that moves all positions by offset `n` (which may be
negative). This can be useful when applying steps meant for a
sub-document to a larger document, or vice-versa.
*/
static offset(n: number): StepMap;
/**
A StepMap that contains no changed ranges.
*/
static empty: StepMap;
}
/**
A mapping represents a pipeline of zero or more [step
maps](https://prosemirror.net/docs/ref/#transform.StepMap). It has special provisions for losslessly
handling mapping positions through a series of steps in which some
steps are inverted versions of earlier steps. (This comes up when
[rebasing](/docs/guide/#transform.rebasing) steps for
collaboration or history management.)
*/
declare class Mapping implements Mappable {
/**
The step maps in this mapping.
*/
readonly maps: StepMap[];
/**
The starting position in the `maps` array, used when `map` or
`mapResult` is called.
*/
from: number;
/**
The end position in the `maps` array.
*/
to: number;
/**
Create a new mapping with the given position maps.
*/
constructor(
/**
The step maps in this mapping.
*/
maps?: StepMap[],
/**
@internal
*/
mirror?: number[] | undefined,
/**
The starting position in the `maps` array, used when `map` or
`mapResult` is called.
*/
from?: number,
/**
The end position in the `maps` array.
*/
to?: number);
/**
Create a mapping that maps only through a part of this one.
*/
slice(from?: number, to?: number): Mapping;
/**
Add a step map to the end of this mapping. If `mirrors` is
given, it should be the index of the step map that is the mirror
image of this one.
*/
appendMap(map: StepMap, mirrors?: number): void;
/**
Add all the step maps in a given mapping to this one (preserving
mirroring information).
*/
appendMapping(mapping: Mapping): void;
/**
Finds the offset of the step map that mirrors the map at the
given offset, in this mapping (as per the second argument to
`appendMap`).
*/
getMirror(n: number): number | undefined;
/**
Append the inverse of the given mapping to this one.
*/
appendMappingInverted(mapping: Mapping): void;
/**
Create an inverted version of this mapping.
*/
invert(): Mapping;
/**
Map a position through this mapping.
*/
map(pos: number, assoc?: number): number;
/**
Map a position through this mapping, returning a mapping
result.
*/
mapResult(pos: number, assoc?: number): MapResult;
}
/**
A step object represents an atomic change. It generally applies
only to the document it was created for, since the positions
stored in it will only make sense for that document.
New steps are defined by creating classes that extend `Step`,
overriding the `apply`, `invert`, `map`, `getMap` and `fromJSON`
methods, and registering your class with a unique
JSON-serialization identifier using
[`Step.jsonID`](https://prosemirror.net/docs/ref/#transform.Step^jsonID).
*/
declare abstract class Step {
/**
Applies this step to the given document, returning a result
object that either indicates failure, if the step can not be
applied to this document, or indicates success by containing a
transformed document.
*/
abstract apply(doc: Node): StepResult;
/**
Get the step map that represents the changes made by this step,
and which can be used to transform between positions in the old
and the new document.
*/
getMap(): StepMap;
/**
Create an inverted version of this step. Needs the document as it
was before the step as argument.
*/
abstract invert(doc: Node): Step;
/**
Map this step through a mappable thing, returning either a
version of that step with its positions adjusted, or `null` if
the step was entirely deleted by the mapping.
*/
abstract map(mapping: Mappable): Step | null;
/**
Try to merge this step with another one, to be applied directly
after it. Returns the merged step when possible, null if the
steps can't be merged.
*/
merge(other: Step): Step | null;
/**
Create a JSON-serializeable representation of this step. When
defining this for a custom subclass, make sure the result object
includes the step type's [JSON id](https://prosemirror.net/docs/ref/#transform.Step^jsonID) under
the `stepType` property.
*/
abstract toJSON(): any;
/**
Deserialize a step from its JSON representation. Will call
through to the step class' own implementation of this method.
*/
static fromJSON(schema: Schema, json: any): Step;
/**
To be able to serialize steps to JSON, each step needs a string
ID to attach to its JSON representation. Use this method to
register an ID for your step classes. Try to pick something
that's unlikely to clash with steps from other modules.
*/
static jsonID(id: string, stepClass: {
fromJSON(schema: Schema, json: any): Step;
}): {
fromJSON(schema: Schema, json: any): Step;
};
}
/**
The result of [applying](https://prosemirror.net/docs/ref/#transform.Step.apply) a step. Contains either a
new document or a failure value.
*/
declare class StepResult {
/**
The transformed document, if successful.
*/
readonly doc: Node | null;
/**
The failure message, if unsuccessful.
*/
readonly failed: string | null;
/**
Create a successful step result.
*/
static ok(doc: Node): StepResult;
/**
Create a failed step result.
*/
static fail(message: string): StepResult;
/**
Call [`Node.replace`](https://prosemirror.net/docs/ref/#model.Node.replace) with the given
arguments. Create a successful result if it succeeds, and a
failed one if it throws a `ReplaceError`.
*/
static fromReplace(doc: Node, from: number, to: number, slice: Slice): StepResult;
}
/**
Abstraction to build up and track an array of
[steps](https://prosemirror.net/docs/ref/#transform.Step) representing a document transformation.
Most transforming methods return the `Transform` object itself, so
that they can be chained.
*/
declare class Transform {
/**
The current document (the result of applying the steps in the
transform).
*/
doc: Node;
/**
The steps in this transform.
*/
readonly steps: Step[];
/**
The documents before each of the steps.
*/
readonly docs: Node[];
/**
A mapping with the maps for each of the steps in this transform.
*/
readonly mapping: Mapping;
/**
Create a transform that starts with the given document.
*/
constructor(
/**
The current document (the result of applying the steps in the
transform).
*/
doc: Node);
/**
The starting document.
*/
get before(): Node;
/**
Apply a new step in this transform, saving the result. Throws an
error when the step fails.
*/
step(step: Step): this;
/**
Try to apply a step in this transformation, ignoring it if it
fails. Returns the step result.
*/
maybeStep(step: Step): StepResult;
/**
True when the document has been changed (when there are any
steps).
*/
get docChanged(): boolean;
/**
Replace the part of the document between `from` and `to` with the
given `slice`.
*/
replace(from: number, to?: number, slice?: Slice): this;
/**
Replace the given range with the given content, which may be a
fragment, node, or array of nodes.
*/
replaceWith(from: number, to: number, content: Fragment | Node | readonly Node[]): this;
/**
Delete the content between the given positions.
*/
delete(from: number, to: number): this;
/**
Insert the given content at the given position.
*/
insert(pos: number, content: Fragment | Node | readonly Node[]): this;
/**
Replace a range of the document with a given slice, using
`from`, `to`, and the slice's
[`openStart`](https://prosemirror.net/docs/ref/#model.Slice.openStart) property as hints, rather
than fixed start and end points. This method may grow the
replaced area or close open nodes in the slice in order to get a
fit that is more in line with WYSIWYG expectations, by dropping
fully covered parent nodes of the replaced region when they are
marked [non-defining as
context](https://prosemirror.net/docs/ref/#model.NodeSpec.definingAsContext), or including an
open parent node from the slice that _is_ marked as [defining
its content](https://prosemirror.net/docs/ref/#model.NodeSpec.definingForContent).
This is the method, for example, to handle paste. The similar
[`replace`](https://prosemirror.net/docs/ref/#transform.Transform.replace) method is a more
primitive tool which will _not_ move the start and end of its given
range, and is useful in situations where you need more precise
control over what happens.
*/
replaceRange(from: number, to: number, slice: Slice): this;
/**
Replace the given range with a node, but use `from` and `to` as
hints, rather than precise positions. When from and to are the same
and are at the start or end of a parent node in which the given
node doesn't fit, this method may _move_ them out towards a parent
that does allow the given node to be placed. When the given range
completely covers a parent node, this method may completely replace
that parent node.
*/
replaceRangeWith(from: number, to: number, node: Node): this;
/**
Delete the given range, expanding it to cover fully covered
parent nodes until a valid replace is found.
*/
deleteRange(from: number, to: number): this;
/**
Split the content in the given range off from its parent, if there
is sibling content before or after it, and move it up the tree to
the depth specified by `target`. You'll probably want to use
[`liftTarget`](https://prosemirror.net/docs/ref/#transform.liftTarget) to compute `target`, to make
sure the lift is valid.
*/
lift(range: NodeRange, target: number): this;
/**
Join the blocks around the given position. If depth is 2, their
last and first siblings are also joined, and so on.
*/
join(pos: number, depth?: number): this;
/**
Wrap the given [range](https://prosemirror.net/docs/ref/#model.NodeRange) in the given set of wrappers.
The wrappers are assumed to be valid in this position, and should
probably be computed with [`findWrapping`](https://prosemirror.net/docs/ref/#transform.findWrapping).
*/
wrap(range: NodeRange, wrappers: readonly {
type: NodeType;
attrs?: Attrs | null;
}[]): this;
/**
Set the type of all textblocks (partly) between `from` and `to` to
the given node type with the given attributes.
*/
setBlockType(from: number, to: number | undefined, type: NodeType, attrs?: Attrs | null): this;
/**
Change the type, attributes, and/or marks of the node at `pos`.
When `type` isn't given, the existing node type is preserved,
*/
setNodeMarkup(pos: number, type?: NodeType | null, attrs?: Attrs | null, marks?: readonly Mark[]): this;
/**
Set a single attribute on a given node to a new value.
The `pos` addresses the document content. Use `setDocAttribute`
to set attributes on the document itself.
*/
setNodeAttribute(pos: number, attr: string, value: any): this;
/**
Set a single attribute on the document to a new value.
*/
setDocAttribute(attr: string, value: any): this;
/**
Add a mark to the node at position `pos`.
*/
addNodeMark(pos: number, mark: Mark): this;
/**
Remove a mark (or a mark of the given type) from the node at
position `pos`.
*/
removeNodeMark(pos: number, mark: Mark | MarkType): this;
/**
Split the node at the given position, and optionally, if `depth` is
greater than one, any number of nodes above that. By default, the
parts split off will inherit the node type of the original node.
This can be changed by passing an array of types and attributes to
use after the split.
*/
split(pos: number, depth?: number, typesAfter?: (null | {
type: NodeType;
attrs?: Attrs | null;
})[]): this;
/**
Add the given mark to the inline content between `from` and `to`.
*/
addMark(from: number, to: number, mark: Mark): this;
/**
Remove marks from inline nodes between `from` and `to`. When
`mark` is a single mark, remove precisely that mark. When it is
a mark type, remove all marks of that type. When it is null,
remove all marks of any type.
*/
removeMark(from: number, to: number, mark?: Mark | MarkType | null): this;
/**
Removes all marks and nodes from the content of the node at
`pos` that don't match the given new parent node type. Accepts
an optional starting [content match](https://prosemirror.net/docs/ref/#model.ContentMatch) as
third argument.
*/
clearIncompatible(pos: number, parentType: NodeType, match?: ContentMatch): this;
}
/**
Try to find a target depth to which the content in the given range
can be lifted. Will not go across
[isolating](https://prosemirror.net/docs/ref/#model.NodeSpec.isolating) parent nodes.
*/
declare function liftTarget(range: NodeRange): number | null;
/**
Try to find a valid way to wrap the content in the given range in a
node of the given type. May introduce extra nodes around and inside
the wrapper node, if necessary. Returns null if no valid wrapping
could be found. When `innerRange` is given, that range's content is
used as the content to fit into the wrapping, instead of the
content of `range`.
*/
declare function findWrapping(range: NodeRange, nodeType: NodeType, attrs?: Attrs | null, innerRange?: NodeRange): {
type: NodeType;
attrs: Attrs | null;
}[] | null;
/**
Check whether splitting at the given position is allowed.
*/
declare function canSplit(doc: Node, pos: number, depth?: number, typesAfter?: (null | {
type: NodeType;
attrs?: Attrs | null;
})[]): boolean;
/**
Test whether the blocks before and after a given position can be
joined.
*/
declare function canJoin(doc: Node, pos: number): boolean;
/**
Find an ancestor of the given position that can be joined to the
block before (or after if `dir` is positive). Returns the joinable
point, if any.
*/
declare function joinPoint(doc: Node, pos: number, dir?: number): number | undefined;
/**
Try to find a point where a node of the given type can be inserted
near `pos`, by searching up the node hierarchy when `pos` itself
isn't a valid place but is at the start or end of a node. Return
null if no position was found.
*/
declare function insertPoint(doc: Node, pos: number, nodeType: NodeType): number | null;
/**
Finds a position at or around the given position where the given
slice can be inserted. Will look at parent nodes' nearest boundary
and try there, even if the original position wasn't directly at the
start or end of that node. Returns null when no position was found.
*/
declare function dropPoint(doc: Node, pos: number, slice: Slice): number | null;
/**
Add a mark to all inline content between two positions.
*/
declare class AddMarkStep extends Step {
/**
The start of the marked range.
*/
readonly from: number;
/**
The end of the marked range.
*/
readonly to: number;
/**
The mark to add.
*/
readonly mark: Mark;
/**
Create a mark step.
*/
constructor(
/**
The start of the marked range.
*/
from: number,
/**
The end of the marked range.
*/
to: number,
/**
The mark to add.
*/
mark: Mark);
apply(doc: Node): StepResult;
invert(): Step;
map(mapping: Mappable): Step | null;
merge(other: Step): Step | null;
toJSON(): any;
}
/**
Remove a mark from all inline content between two positions.
*/
declare class RemoveMarkStep extends Step {
/**
The start of the unmarked range.
*/
readonly from: number;
/**
The end of the unmarked range.
*/
readonly to: number;
/**
The mark to remove.
*/
readonly mark: Mark;
/**
Create a mark-removing step.
*/
constructor(
/**
The start of the unmarked range.
*/
from: number,
/**
The end of the unmarked range.
*/
to: number,
/**
The mark to remove.
*/
mark: Mark);
apply(doc: Node): StepResult;
invert(): Step;
map(mapping: Mappable): Step | null;
merge(other: Step): Step | null;
toJSON(): any;
}
/**
Add a mark to a specific node.
*/
declare class AddNodeMarkStep extends Step {
/**
The position of the target node.
*/
readonly pos: number;
/**
The mark to add.
*/
readonly mark: Mark;
/**
Create a node mark step.
*/
constructor(
/**
The position of the target node.
*/
pos: number,
/**
The mark to add.
*/
mark: Mark);
apply(doc: Node): StepResult;
invert(doc: Node): Step;
map(mapping: Mappable): Step | null;
toJSON(): any;
}
/**
Remove a mark from a specific node.
*/
declare class RemoveNodeMarkStep extends Step {
/**
The position of the target node.
*/
readonly pos: number;
/**
The mark to remove.
*/
readonly mark: Mark;
/**
Create a mark-removing step.
*/
constructor(
/**
The position of the target node.
*/
pos: number,
/**
The mark to remove.
*/
mark: Mark);
apply(doc: Node): StepResult;
invert(doc: Node): Step;
map(mapping: Mappable): Step | null;
toJSON(): any;
}
/**
Replace a part of the document with a slice of new content.
*/
declare class ReplaceStep extends Step {
/**
The start position of the replaced range.
*/
readonly from: number;
/**
The end position of the replaced range.
*/
readonly to: number;
/**
The slice to insert.
*/
readonly slice: Slice;
/**
The given `slice` should fit the 'gap' between `from` and
`to`—the depths must line up, and the surrounding nodes must be
able to be joined with the open sides of the slice. When
`structure` is true, the step will fail if the content between
from and to is not just a sequence of closing and then opening
tokens (this is to guard against rebased replace steps
overwriting something they weren't supposed to).
*/
constructor(
/**
The start position of the replaced range.
*/
from: number,
/**
The end position of the replaced range.
*/
to: number,
/**
The slice to insert.
*/
slice: Slice,
/**
@internal
*/
structure?: boolean);
apply(doc: Node): StepResult;
getMap(): StepMap;
invert(doc: Node): ReplaceStep;
map(mapping: Mappable): ReplaceStep | null;
merge(other: Step): ReplaceStep | null;
toJSON(): any;
}
/**
Replace a part of the document with a slice of content, but
preserve a range of the replaced content by moving it into the
slice.
*/
declare class ReplaceAroundStep extends Step {
/**
The start position of the replaced range.
*/
readonly from: number;
/**
The end position of the replaced range.
*/
readonly to: number;
/**
The start of preserved range.
*/
readonly gapFrom: number;
/**
The end of preserved range.
*/
readonly gapTo: number;
/**
The slice to insert.
*/
readonly slice: Slice;
/**
The position in the slice where the preserved range should be
inserted.
*/
readonly insert: number;
/**
Create a replace-around step with the given range and gap.
`insert` should be the point in the slice into which the content
of the gap should be moved. `structure` has the same meaning as
it has in the [`ReplaceStep`](https://prosemirror.net/docs/ref/#transform.ReplaceStep) class.
*/
constructor(
/**
The start position of the replaced range.
*/
from: number,
/**
The end position of the replaced range.
*/
to: number,
/**
The start of preserved range.
*/
gapFrom: number,
/**
The end of preserved range.
*/
gapTo: number,
/**
The slice to insert.
*/
slice: Slice,
/**
The position in the slice where the preserved range should be
inserted.
*/
insert: number,
/**
@internal
*/
structure?: boolean);
apply(doc: Node): StepResult;
getMap(): StepMap;
invert(doc: Node): ReplaceAroundStep;
map(mapping: Mappable): ReplaceAroundStep | null;
toJSON(): any;
}
/**
Update an attribute in a specific node.
*/
declare class AttrStep extends Step {
/**
The position of the target node.
*/
readonly pos: number;
/**
The attribute to set.
*/
readonly attr: string;
readonly value: any;
/**
Construct an attribute step.
*/
constructor(
/**
The position of the target node.
*/
pos: number,
/**
The attribute to set.
*/
attr: string, value: any);
apply(doc: Node): StepResult;
getMap(): StepMap;
invert(doc: Node): AttrStep;
map(mapping: Mappable): AttrStep | null;
toJSON(): any;
static fromJSON(schema: Schema, json: any): AttrStep;
}
/**
Update an attribute in the doc node.
*/
declare class DocAttrStep extends Step {
/**
The attribute to set.
*/
readonly attr: string;
readonly value: any;
/**
Construct an attribute step.
*/
constructor(
/**
The attribute to set.
*/
attr: string, value: any);
apply(doc: Node): StepResult;
getMap(): StepMap;
invert(doc: Node): DocAttrStep;
map(mapping: Mappable): this;
toJSON(): any;
static fromJSON(schema: Schema, json: any): DocAttrStep;
}
/**
Fit a slice into a given position in the document, producing a
[step](https://prosemirror.net/docs/ref/#transform.Step) that inserts it. Will return null if
there's no meaningful way to insert the slice here, or inserting it
would be a no-op (an empty slice over an empty range).
*/
declare function replaceStep(doc: Node, from: number, to?: number, slice?: Slice): Step | null;
export { AddMarkStep, AddNodeMarkStep, AttrStep, DocAttrStep, MapResult, type Mappable, Mapping, RemoveMarkStep, RemoveNodeMarkStep, ReplaceAroundStep, ReplaceStep, Step, StepMap, StepResult, Transform, canJoin, canSplit, dropPoint, findWrapping, insertPoint, joinPoint, liftTarget, replaceStep };

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
{
"name": "prosemirror-transform",
"version": "1.9.0",
"description": "ProseMirror document transformations",
"type": "module",
"main": "dist/index.cjs",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"sideEffects": false,
"license": "MIT",
"maintainers": [
{
"name": "Marijn Haverbeke",
"email": "marijn@haverbeke.berlin",
"web": "http://marijnhaverbeke.nl"
}
],
"repository": {
"type": "git",
"url": "git://github.com/prosemirror/prosemirror-transform.git"
},
"dependencies": {
"prosemirror-model": "^1.21.0"
},
"devDependencies": {
"@prosemirror/buildhelper": "^0.1.5",
"prosemirror-test-builder": "^1.0.0"
}
}

View File

@@ -0,0 +1,59 @@
This module defines a way of modifying documents that allows changes
to be recorded, replayed, and reordered. You can read more about
transformations in [the guide](/docs/guide/#transform).
### Steps
Transforming happens in `Step`s, which are atomic, well-defined
modifications to a document. [Applying](#transform.Step.apply) a step
produces a new document.
Each step provides a [change map](#transform.StepMap) that maps
positions in the old document to position in the transformed document.
Steps can be [inverted](#transform.Step.invert) to create a step that
undoes their effect, and chained together in a convenience object
called a [`Transform`](#transform.Transform).
@Step
@StepResult
@ReplaceStep
@ReplaceAroundStep
@AddMarkStep
@RemoveMarkStep
@AddNodeMarkStep
@RemoveNodeMarkStep
@AttrStep
@DocAttrStep
### Position Mapping
Mapping positions from one document to another by running through the
[step maps](#transform.StepMap) produced by steps is an important
operation in ProseMirror. It is used, for example, for updating the
selection when the document changes.
@Mappable
@MapResult
@StepMap
@Mapping
### Document transforms
Because you often need to collect a number of steps together to effect
a composite change, ProseMirror provides an abstraction to make this
easy. [State transactions](#state.Transaction) are a subclass of
transforms.
@Transform
The following helper functions can be useful when creating
transformations or determining whether they are even possible.
@replaceStep
@liftTarget
@findWrapping
@canSplit
@canJoin
@joinPoint
@insertPoint
@dropPoint

View File

@@ -0,0 +1,98 @@
import {Fragment, Slice, Node, Schema} from "prosemirror-model"
import {Step, StepResult} from "./step"
import {StepMap, Mappable} from "./map"
/// Update an attribute in a specific node.
export class AttrStep extends Step {
/// Construct an attribute step.
constructor(
/// The position of the target node.
readonly pos: number,
/// The attribute to set.
readonly attr: string,
// The attribute's new value.
readonly value: any
) {
super()
}
apply(doc: Node) {
let node = doc.nodeAt(this.pos)
if (!node) return StepResult.fail("No node at attribute step's position")
let attrs = Object.create(null)
for (let name in node.attrs) attrs[name] = node.attrs[name]
attrs[this.attr] = this.value
let updated = node.type.create(attrs, null, node.marks)
return StepResult.fromReplace(doc, this.pos, this.pos + 1, new Slice(Fragment.from(updated), 0, node.isLeaf ? 0 : 1))
}
getMap() {
return StepMap.empty
}
invert(doc: Node) {
return new AttrStep(this.pos, this.attr, doc.nodeAt(this.pos)!.attrs[this.attr])
}
map(mapping: Mappable) {
let pos = mapping.mapResult(this.pos, 1)
return pos.deletedAfter ? null : new AttrStep(pos.pos, this.attr, this.value)
}
toJSON(): any {
return {stepType: "attr", pos: this.pos, attr: this.attr, value: this.value}
}
static fromJSON(schema: Schema, json: any) {
if (typeof json.pos != "number" || typeof json.attr != "string")
throw new RangeError("Invalid input for AttrStep.fromJSON")
return new AttrStep(json.pos, json.attr, json.value)
}
}
Step.jsonID("attr", AttrStep)
/// Update an attribute in the doc node.
export class DocAttrStep extends Step {
/// Construct an attribute step.
constructor(
/// The attribute to set.
readonly attr: string,
// The attribute's new value.
readonly value: any
) {
super()
}
apply(doc: Node) {
let attrs = Object.create(null)
for (let name in doc.attrs) attrs[name] = doc.attrs[name]
attrs[this.attr] = this.value
let updated = doc.type.create(attrs, doc.content, doc.marks)
return StepResult.ok(updated)
}
getMap() {
return StepMap.empty
}
invert(doc: Node) {
return new DocAttrStep(this.attr, doc.attrs[this.attr])
}
map(mapping: Mappable) {
return this
}
toJSON(): any {
return {stepType: "docAttr", attr: this.attr, value: this.value}
}
static fromJSON(schema: Schema, json: any) {
if (typeof json.attr != "string")
throw new RangeError("Invalid input for DocAttrStep.fromJSON")
return new DocAttrStep(json.attr, json.value)
}
}
Step.jsonID("docAttr", DocAttrStep)

View File

@@ -0,0 +1,11 @@
export {Transform} from "./transform"
/// @internal
export {TransformError} from "./transform"
export {Step, StepResult} from "./step"
export {joinPoint, canJoin, canSplit, insertPoint, dropPoint, liftTarget, findWrapping} from "./structure"
export {StepMap, MapResult, Mapping, Mappable} from "./map"
export {AddMarkStep, RemoveMarkStep, AddNodeMarkStep, RemoveNodeMarkStep} from "./mark_step"
export {ReplaceStep, ReplaceAroundStep} from "./replace_step"
export {AttrStep, DocAttrStep} from "./attr_step"
import "./mark"
export {replaceStep} from "./replace"

View File

@@ -0,0 +1,275 @@
/// There are several things that positions can be mapped through.
/// Such objects conform to this interface.
export interface Mappable {
/// Map a position through this object. When given, `assoc` (should
/// be -1 or 1, defaults to 1) determines with which side the
/// position is associated, which determines in which direction to
/// move when a chunk of content is inserted at the mapped position.
map: (pos: number, assoc?: number) => number
/// Map a position, and return an object containing additional
/// information about the mapping. The result's `deleted` field tells
/// you whether the position was deleted (completely enclosed in a
/// replaced range) during the mapping. When content on only one side
/// is deleted, the position itself is only considered deleted when
/// `assoc` points in the direction of the deleted content.
mapResult: (pos: number, assoc?: number) => MapResult
}
// Recovery values encode a range index and an offset. They are
// represented as numbers, because tons of them will be created when
// mapping, for example, a large number of decorations. The number's
// lower 16 bits provide the index, the remaining bits the offset.
//
// Note: We intentionally don't use bit shift operators to en- and
// decode these, since those clip to 32 bits, which we might in rare
// cases want to overflow. A 64-bit float can represent 48-bit
// integers precisely.
const lower16 = 0xffff
const factor16 = Math.pow(2, 16)
function makeRecover(index: number, offset: number) { return index + offset * factor16 }
function recoverIndex(value: number) { return value & lower16 }
function recoverOffset(value: number) { return (value - (value & lower16)) / factor16 }
const DEL_BEFORE = 1, DEL_AFTER = 2, DEL_ACROSS = 4, DEL_SIDE = 8
/// An object representing a mapped position with extra
/// information.
export class MapResult {
/// @internal
constructor(
/// The mapped version of the position.
readonly pos: number,
/// @internal
readonly delInfo: number,
/// @internal
readonly recover: number | null
) {}
/// Tells you whether the position was deleted, that is, whether the
/// step removed the token on the side queried (via the `assoc`)
/// argument from the document.
get deleted() { return (this.delInfo & DEL_SIDE) > 0 }
/// Tells you whether the token before the mapped position was deleted.
get deletedBefore() { return (this.delInfo & (DEL_BEFORE | DEL_ACROSS)) > 0 }
/// True when the token after the mapped position was deleted.
get deletedAfter() { return (this.delInfo & (DEL_AFTER | DEL_ACROSS)) > 0 }
/// Tells whether any of the steps mapped through deletes across the
/// position (including both the token before and after the
/// position).
get deletedAcross() { return (this.delInfo & DEL_ACROSS) > 0 }
}
/// A map describing the deletions and insertions made by a step, which
/// can be used to find the correspondence between positions in the
/// pre-step version of a document and the same position in the
/// post-step version.
export class StepMap implements Mappable {
/// Create a position map. The modifications to the document are
/// represented as an array of numbers, in which each group of three
/// represents a modified chunk as `[start, oldSize, newSize]`.
constructor(
/// @internal
readonly ranges: readonly number[],
/// @internal
readonly inverted = false
) {
if (!ranges.length && StepMap.empty) return StepMap.empty
}
/// @internal
recover(value: number) {
let diff = 0, index = recoverIndex(value)
if (!this.inverted) for (let i = 0; i < index; i++)
diff += this.ranges[i * 3 + 2] - this.ranges[i * 3 + 1]
return this.ranges[index * 3] + diff + recoverOffset(value)
}
mapResult(pos: number, assoc = 1): MapResult { return this._map(pos, assoc, false) as MapResult }
map(pos: number, assoc = 1): number { return this._map(pos, assoc, true) as number }
/// @internal
_map(pos: number, assoc: number, simple: boolean) {
let diff = 0, oldIndex = this.inverted ? 2 : 1, newIndex = this.inverted ? 1 : 2
for (let i = 0; i < this.ranges.length; i += 3) {
let start = this.ranges[i] - (this.inverted ? diff : 0)
if (start > pos) break
let oldSize = this.ranges[i + oldIndex], newSize = this.ranges[i + newIndex], end = start + oldSize
if (pos <= end) {
let side = !oldSize ? assoc : pos == start ? -1 : pos == end ? 1 : assoc
let result = start + diff + (side < 0 ? 0 : newSize)
if (simple) return result
let recover = pos == (assoc < 0 ? start : end) ? null : makeRecover(i / 3, pos - start)
let del = pos == start ? DEL_AFTER : pos == end ? DEL_BEFORE : DEL_ACROSS
if (assoc < 0 ? pos != start : pos != end) del |= DEL_SIDE
return new MapResult(result, del, recover)
}
diff += newSize - oldSize
}
return simple ? pos + diff : new MapResult(pos + diff, 0, null)
}
/// @internal
touches(pos: number, recover: number) {
let diff = 0, index = recoverIndex(recover)
let oldIndex = this.inverted ? 2 : 1, newIndex = this.inverted ? 1 : 2
for (let i = 0; i < this.ranges.length; i += 3) {
let start = this.ranges[i] - (this.inverted ? diff : 0)
if (start > pos) break
let oldSize = this.ranges[i + oldIndex], end = start + oldSize
if (pos <= end && i == index * 3) return true
diff += this.ranges[i + newIndex] - oldSize
}
return false
}
/// Calls the given function on each of the changed ranges included in
/// this map.
forEach(f: (oldStart: number, oldEnd: number, newStart: number, newEnd: number) => void) {
let oldIndex = this.inverted ? 2 : 1, newIndex = this.inverted ? 1 : 2
for (let i = 0, diff = 0; i < this.ranges.length; i += 3) {
let start = this.ranges[i], oldStart = start - (this.inverted ? diff : 0), newStart = start + (this.inverted ? 0 : diff)
let oldSize = this.ranges[i + oldIndex], newSize = this.ranges[i + newIndex]
f(oldStart, oldStart + oldSize, newStart, newStart + newSize)
diff += newSize - oldSize
}
}
/// Create an inverted version of this map. The result can be used to
/// map positions in the post-step document to the pre-step document.
invert() {
return new StepMap(this.ranges, !this.inverted)
}
/// @internal
toString() {
return (this.inverted ? "-" : "") + JSON.stringify(this.ranges)
}
/// Create a map that moves all positions by offset `n` (which may be
/// negative). This can be useful when applying steps meant for a
/// sub-document to a larger document, or vice-versa.
static offset(n: number) {
return n == 0 ? StepMap.empty : new StepMap(n < 0 ? [0, -n, 0] : [0, 0, n])
}
/// A StepMap that contains no changed ranges.
static empty = new StepMap([])
}
/// A mapping represents a pipeline of zero or more [step
/// maps](#transform.StepMap). It has special provisions for losslessly
/// handling mapping positions through a series of steps in which some
/// steps are inverted versions of earlier steps. (This comes up when
/// [rebasing](/docs/guide/#transform.rebasing) steps for
/// collaboration or history management.)
export class Mapping implements Mappable {
/// Create a new mapping with the given position maps.
constructor(
/// The step maps in this mapping.
readonly maps: StepMap[] = [],
/// @internal
public mirror?: number[],
/// The starting position in the `maps` array, used when `map` or
/// `mapResult` is called.
public from = 0,
/// The end position in the `maps` array.
public to = maps.length
) {}
/// Create a mapping that maps only through a part of this one.
slice(from = 0, to = this.maps.length) {
return new Mapping(this.maps, this.mirror, from, to)
}
/// @internal
copy() {
return new Mapping(this.maps.slice(), this.mirror && this.mirror.slice(), this.from, this.to)
}
/// Add a step map to the end of this mapping. If `mirrors` is
/// given, it should be the index of the step map that is the mirror
/// image of this one.
appendMap(map: StepMap, mirrors?: number) {
this.to = this.maps.push(map)
if (mirrors != null) this.setMirror(this.maps.length - 1, mirrors)
}
/// Add all the step maps in a given mapping to this one (preserving
/// mirroring information).
appendMapping(mapping: Mapping) {
for (let i = 0, startSize = this.maps.length; i < mapping.maps.length; i++) {
let mirr = mapping.getMirror(i)
this.appendMap(mapping.maps[i], mirr != null && mirr < i ? startSize + mirr : undefined)
}
}
/// Finds the offset of the step map that mirrors the map at the
/// given offset, in this mapping (as per the second argument to
/// `appendMap`).
getMirror(n: number): number | undefined {
if (this.mirror) for (let i = 0; i < this.mirror.length; i++)
if (this.mirror[i] == n) return this.mirror[i + (i % 2 ? -1 : 1)]
}
/// @internal
setMirror(n: number, m: number) {
if (!this.mirror) this.mirror = []
this.mirror.push(n, m)
}
/// Append the inverse of the given mapping to this one.
appendMappingInverted(mapping: Mapping) {
for (let i = mapping.maps.length - 1, totalSize = this.maps.length + mapping.maps.length; i >= 0; i--) {
let mirr = mapping.getMirror(i)
this.appendMap(mapping.maps[i].invert(), mirr != null && mirr > i ? totalSize - mirr - 1 : undefined)
}
}
/// Create an inverted version of this mapping.
invert() {
let inverse = new Mapping
inverse.appendMappingInverted(this)
return inverse
}
/// Map a position through this mapping.
map(pos: number, assoc = 1) {
if (this.mirror) return this._map(pos, assoc, true) as number
for (let i = this.from; i < this.to; i++)
pos = this.maps[i].map(pos, assoc)
return pos
}
/// Map a position through this mapping, returning a mapping
/// result.
mapResult(pos: number, assoc = 1) { return this._map(pos, assoc, false) as MapResult }
/// @internal
_map(pos: number, assoc: number, simple: boolean) {
let delInfo = 0
for (let i = this.from; i < this.to; i++) {
let map = this.maps[i], result = map.mapResult(pos, assoc)
if (result.recover != null) {
let corr = this.getMirror(i)
if (corr != null && corr > i && corr < this.to) {
i = corr
pos = this.maps[corr].recover(result.recover)
continue
}
}
delInfo |= result.delInfo
pos = result.pos
}
return simple ? pos : new MapResult(pos, delInfo, null)
}
}

View File

@@ -0,0 +1,106 @@
import {Mark, MarkType, Slice, Fragment, NodeType} from "prosemirror-model"
import {Step} from "./step"
import {Transform} from "./transform"
import {AddMarkStep, RemoveMarkStep} from "./mark_step"
import {ReplaceStep} from "./replace_step"
export function addMark(tr: Transform, from: number, to: number, mark: Mark) {
let removed: Step[] = [], added: Step[] = []
let removing: RemoveMarkStep | undefined, adding: AddMarkStep | undefined
tr.doc.nodesBetween(from, to, (node, pos, parent) => {
if (!node.isInline) return
let marks = node.marks
if (!mark.isInSet(marks) && parent!.type.allowsMarkType(mark.type)) {
let start = Math.max(pos, from), end = Math.min(pos + node.nodeSize, to)
let newSet = mark.addToSet(marks)
for (let i = 0; i < marks.length; i++) {
if (!marks[i].isInSet(newSet)) {
if (removing && removing.to == start && removing.mark.eq(marks[i]))
(removing as any).to = end
else
removed.push(removing = new RemoveMarkStep(start, end, marks[i]))
}
}
if (adding && adding.to == start)
(adding as any).to = end
else
added.push(adding = new AddMarkStep(start, end, mark))
}
})
removed.forEach(s => tr.step(s))
added.forEach(s => tr.step(s))
}
export function removeMark(tr: Transform, from: number, to: number, mark?: Mark | MarkType | null) {
let matched: {style: Mark, from: number, to: number, step: number}[] = [], step = 0
tr.doc.nodesBetween(from, to, (node, pos) => {
if (!node.isInline) return
step++
let toRemove = null
if (mark instanceof MarkType) {
let set = node.marks, found
while (found = mark.isInSet(set)) {
;(toRemove || (toRemove = [])).push(found)
set = found.removeFromSet(set)
}
} else if (mark) {
if (mark.isInSet(node.marks)) toRemove = [mark]
} else {
toRemove = node.marks
}
if (toRemove && toRemove.length) {
let end = Math.min(pos + node.nodeSize, to)
for (let i = 0; i < toRemove.length; i++) {
let style = toRemove[i], found
for (let j = 0; j < matched.length; j++) {
let m = matched[j]
if (m.step == step - 1 && style.eq(matched[j].style)) found = m
}
if (found) {
found.to = end
found.step = step
} else {
matched.push({style, from: Math.max(pos, from), to: end, step})
}
}
}
})
matched.forEach(m => tr.step(new RemoveMarkStep(m.from, m.to, m.style)))
}
export function clearIncompatible(tr: Transform, pos: number, parentType: NodeType,
match = parentType.contentMatch,
clearNewlines = true) {
let node = tr.doc.nodeAt(pos)!
let replSteps: Step[] = [], cur = pos + 1
for (let i = 0; i < node.childCount; i++) {
let child = node.child(i), end = cur + child.nodeSize
let allowed = match.matchType(child.type)
if (!allowed) {
replSteps.push(new ReplaceStep(cur, end, Slice.empty))
} else {
match = allowed
for (let j = 0; j < child.marks.length; j++) if (!parentType.allowsMarkType(child.marks[j].type))
tr.step(new RemoveMarkStep(cur, end, child.marks[j]))
if (clearNewlines && child.isText && parentType.whitespace != "pre") {
let m, newline = /\r?\n|\r/g, slice
while (m = newline.exec(child.text!)) {
if (!slice) slice = new Slice(Fragment.from(parentType.schema.text(" ", parentType.allowedMarks(child.marks))),
0, 0)
replSteps.push(new ReplaceStep(cur + m.index, cur + m.index + m[0].length, slice))
}
}
}
cur = end
}
if (!match.validEnd) {
let fill = match.fillBefore(Fragment.empty, true)
tr.replace(cur, cur, new Slice(fill!, 0, 0))
}
for (let i = replSteps.length - 1; i >= 0; i--) tr.step(replSteps[i])
}

View File

@@ -0,0 +1,224 @@
import {Fragment, Slice, Node, Mark, Schema} from "prosemirror-model"
import {Step, StepResult} from "./step"
import {Mappable} from "./map"
function mapFragment(fragment: Fragment, f: (child: Node, parent: Node, i: number) => Node, parent: Node): Fragment {
let mapped = []
for (let i = 0; i < fragment.childCount; i++) {
let child = fragment.child(i)
if (child.content.size) child = child.copy(mapFragment(child.content, f, child))
if (child.isInline) child = f(child, parent, i)
mapped.push(child)
}
return Fragment.fromArray(mapped)
}
/// Add a mark to all inline content between two positions.
export class AddMarkStep extends Step {
/// Create a mark step.
constructor(
/// The start of the marked range.
readonly from: number,
/// The end of the marked range.
readonly to: number,
/// The mark to add.
readonly mark: Mark
) {
super()
}
apply(doc: Node) {
let oldSlice = doc.slice(this.from, this.to), $from = doc.resolve(this.from)
let parent = $from.node($from.sharedDepth(this.to))
let slice = new Slice(mapFragment(oldSlice.content, (node, parent) => {
if (!node.isAtom || !parent.type.allowsMarkType(this.mark.type)) return node
return node.mark(this.mark.addToSet(node.marks))
}, parent), oldSlice.openStart, oldSlice.openEnd)
return StepResult.fromReplace(doc, this.from, this.to, slice)
}
invert(): Step {
return new RemoveMarkStep(this.from, this.to, this.mark)
}
map(mapping: Mappable): Step | null {
let from = mapping.mapResult(this.from, 1), to = mapping.mapResult(this.to, -1)
if (from.deleted && to.deleted || from.pos >= to.pos) return null
return new AddMarkStep(from.pos, to.pos, this.mark)
}
merge(other: Step): Step | null {
if (other instanceof AddMarkStep &&
other.mark.eq(this.mark) &&
this.from <= other.to && this.to >= other.from)
return new AddMarkStep(Math.min(this.from, other.from),
Math.max(this.to, other.to), this.mark)
return null
}
toJSON(): any {
return {stepType: "addMark", mark: this.mark.toJSON(),
from: this.from, to: this.to}
}
/// @internal
static fromJSON(schema: Schema, json: any) {
if (typeof json.from != "number" || typeof json.to != "number")
throw new RangeError("Invalid input for AddMarkStep.fromJSON")
return new AddMarkStep(json.from, json.to, schema.markFromJSON(json.mark))
}
}
Step.jsonID("addMark", AddMarkStep)
/// Remove a mark from all inline content between two positions.
export class RemoveMarkStep extends Step {
/// Create a mark-removing step.
constructor(
/// The start of the unmarked range.
readonly from: number,
/// The end of the unmarked range.
readonly to: number,
/// The mark to remove.
readonly mark: Mark
) {
super()
}
apply(doc: Node) {
let oldSlice = doc.slice(this.from, this.to)
let slice = new Slice(mapFragment(oldSlice.content, node => {
return node.mark(this.mark.removeFromSet(node.marks))
}, doc), oldSlice.openStart, oldSlice.openEnd)
return StepResult.fromReplace(doc, this.from, this.to, slice)
}
invert(): Step {
return new AddMarkStep(this.from, this.to, this.mark)
}
map(mapping: Mappable): Step | null {
let from = mapping.mapResult(this.from, 1), to = mapping.mapResult(this.to, -1)
if (from.deleted && to.deleted || from.pos >= to.pos) return null
return new RemoveMarkStep(from.pos, to.pos, this.mark)
}
merge(other: Step): Step | null {
if (other instanceof RemoveMarkStep &&
other.mark.eq(this.mark) &&
this.from <= other.to && this.to >= other.from)
return new RemoveMarkStep(Math.min(this.from, other.from),
Math.max(this.to, other.to), this.mark)
return null
}
toJSON(): any {
return {stepType: "removeMark", mark: this.mark.toJSON(),
from: this.from, to: this.to}
}
/// @internal
static fromJSON(schema: Schema, json: any) {
if (typeof json.from != "number" || typeof json.to != "number")
throw new RangeError("Invalid input for RemoveMarkStep.fromJSON")
return new RemoveMarkStep(json.from, json.to, schema.markFromJSON(json.mark))
}
}
Step.jsonID("removeMark", RemoveMarkStep)
/// Add a mark to a specific node.
export class AddNodeMarkStep extends Step {
/// Create a node mark step.
constructor(
/// The position of the target node.
readonly pos: number,
/// The mark to add.
readonly mark: Mark
) {
super()
}
apply(doc: Node) {
let node = doc.nodeAt(this.pos)
if (!node) return StepResult.fail("No node at mark step's position")
let updated = node.type.create(node.attrs, null, this.mark.addToSet(node.marks))
return StepResult.fromReplace(doc, this.pos, this.pos + 1, new Slice(Fragment.from(updated), 0, node.isLeaf ? 0 : 1))
}
invert(doc: Node): Step {
let node = doc.nodeAt(this.pos)
if (node) {
let newSet = this.mark.addToSet(node.marks)
if (newSet.length == node.marks.length) {
for (let i = 0; i < node.marks.length; i++)
if (!node.marks[i].isInSet(newSet))
return new AddNodeMarkStep(this.pos, node.marks[i])
return new AddNodeMarkStep(this.pos, this.mark)
}
}
return new RemoveNodeMarkStep(this.pos, this.mark)
}
map(mapping: Mappable): Step | null {
let pos = mapping.mapResult(this.pos, 1)
return pos.deletedAfter ? null : new AddNodeMarkStep(pos.pos, this.mark)
}
toJSON(): any {
return {stepType: "addNodeMark", pos: this.pos, mark: this.mark.toJSON()}
}
/// @internal
static fromJSON(schema: Schema, json: any) {
if (typeof json.pos != "number")
throw new RangeError("Invalid input for AddNodeMarkStep.fromJSON")
return new AddNodeMarkStep(json.pos, schema.markFromJSON(json.mark))
}
}
Step.jsonID("addNodeMark", AddNodeMarkStep)
/// Remove a mark from a specific node.
export class RemoveNodeMarkStep extends Step {
/// Create a mark-removing step.
constructor(
/// The position of the target node.
readonly pos: number,
/// The mark to remove.
readonly mark: Mark
) {
super()
}
apply(doc: Node) {
let node = doc.nodeAt(this.pos)
if (!node) return StepResult.fail("No node at mark step's position")
let updated = node.type.create(node.attrs, null, this.mark.removeFromSet(node.marks))
return StepResult.fromReplace(doc, this.pos, this.pos + 1, new Slice(Fragment.from(updated), 0, node.isLeaf ? 0 : 1))
}
invert(doc: Node): Step {
let node = doc.nodeAt(this.pos)
if (!node || !this.mark.isInSet(node.marks)) return this
return new AddNodeMarkStep(this.pos, this.mark)
}
map(mapping: Mappable): Step | null {
let pos = mapping.mapResult(this.pos, 1)
return pos.deletedAfter ? null : new RemoveNodeMarkStep(pos.pos, this.mark)
}
toJSON(): any {
return {stepType: "removeNodeMark", pos: this.pos, mark: this.mark.toJSON()}
}
/// @internal
static fromJSON(schema: Schema, json: any) {
if (typeof json.pos != "number")
throw new RangeError("Invalid input for RemoveNodeMarkStep.fromJSON")
return new RemoveNodeMarkStep(json.pos, schema.markFromJSON(json.mark))
}
}
Step.jsonID("removeNodeMark", RemoveNodeMarkStep)

View File

@@ -0,0 +1,459 @@
import {Fragment, Slice, Node, ResolvedPos, NodeType, ContentMatch, Attrs} from "prosemirror-model"
import {Step} from "./step"
import {ReplaceStep, ReplaceAroundStep} from "./replace_step"
import {Transform} from "./transform"
import {insertPoint} from "./structure"
/// Fit a slice into a given position in the document, producing a
/// [step](#transform.Step) that inserts it. Will return null if
/// there's no meaningful way to insert the slice here, or inserting it
/// would be a no-op (an empty slice over an empty range).
export function replaceStep(doc: Node, from: number, to = from, slice = Slice.empty): Step | null {
if (from == to && !slice.size) return null
let $from = doc.resolve(from), $to = doc.resolve(to)
// Optimization -- avoid work if it's obvious that it's not needed.
if (fitsTrivially($from, $to, slice)) return new ReplaceStep(from, to, slice)
return new Fitter($from, $to, slice).fit()
}
function fitsTrivially($from: ResolvedPos, $to: ResolvedPos, slice: Slice) {
return !slice.openStart && !slice.openEnd && $from.start() == $to.start() &&
$from.parent.canReplace($from.index(), $to.index(), slice.content)
}
interface Fittable {
sliceDepth: number
frontierDepth: number
parent: Node | null
inject?: Fragment | null
wrap?: readonly NodeType[]
}
// Algorithm for 'placing' the elements of a slice into a gap:
//
// We consider the content of each node that is open to the left to be
// independently placeable. I.e. in <p("foo"), p("bar")>, when the
// paragraph on the left is open, "foo" can be placed (somewhere on
// the left side of the replacement gap) independently from p("bar").
//
// This class tracks the state of the placement progress in the
// following properties:
//
// - `frontier` holds a stack of `{type, match}` objects that
// represent the open side of the replacement. It starts at
// `$from`, then moves forward as content is placed, and is finally
// reconciled with `$to`.
//
// - `unplaced` is a slice that represents the content that hasn't
// been placed yet.
//
// - `placed` is a fragment of placed content. Its open-start value
// is implicit in `$from`, and its open-end value in `frontier`.
class Fitter {
frontier: {type: NodeType, match: ContentMatch}[] = []
placed: Fragment = Fragment.empty
constructor(
readonly $from: ResolvedPos,
readonly $to: ResolvedPos,
public unplaced: Slice
) {
for (let i = 0; i <= $from.depth; i++) {
let node = $from.node(i)
this.frontier.push({
type: node.type,
match: node.contentMatchAt($from.indexAfter(i))
})
}
for (let i = $from.depth; i > 0; i--)
this.placed = Fragment.from($from.node(i).copy(this.placed))
}
get depth() { return this.frontier.length - 1 }
fit() {
// As long as there's unplaced content, try to place some of it.
// If that fails, either increase the open score of the unplaced
// slice, or drop nodes from it, and then try again.
while (this.unplaced.size) {
let fit = this.findFittable()
if (fit) this.placeNodes(fit)
else this.openMore() || this.dropNode()
}
// When there's inline content directly after the frontier _and_
// directly after `this.$to`, we must generate a `ReplaceAround`
// step that pulls that content into the node after the frontier.
// That means the fitting must be done to the end of the textblock
// node after `this.$to`, not `this.$to` itself.
let moveInline = this.mustMoveInline(), placedSize = this.placed.size - this.depth - this.$from.depth
let $from = this.$from, $to = this.close(moveInline < 0 ? this.$to : $from.doc.resolve(moveInline))
if (!$to) return null
// If closing to `$to` succeeded, create a step
let content = this.placed, openStart = $from.depth, openEnd = $to.depth
while (openStart && openEnd && content.childCount == 1) { // Normalize by dropping open parent nodes
content = content.firstChild!.content
openStart--; openEnd--
}
let slice = new Slice(content, openStart, openEnd)
if (moveInline > -1)
return new ReplaceAroundStep($from.pos, moveInline, this.$to.pos, this.$to.end(), slice, placedSize)
if (slice.size || $from.pos != this.$to.pos) // Don't generate no-op steps
return new ReplaceStep($from.pos, $to.pos, slice)
return null
}
// Find a position on the start spine of `this.unplaced` that has
// content that can be moved somewhere on the frontier. Returns two
// depths, one for the slice and one for the frontier.
findFittable(): Fittable | undefined {
let startDepth = this.unplaced.openStart
for (let cur = this.unplaced.content, d = 0, openEnd = this.unplaced.openEnd; d < startDepth; d++) {
let node = cur.firstChild!
if (cur.childCount > 1) openEnd = 0
if (node.type.spec.isolating && openEnd <= d) {
startDepth = d
break
}
cur = node.content
}
// Only try wrapping nodes (pass 2) after finding a place without
// wrapping failed.
for (let pass = 1; pass <= 2; pass++) {
for (let sliceDepth = pass == 1 ? startDepth : this.unplaced.openStart; sliceDepth >= 0; sliceDepth--) {
let fragment, parent = null
if (sliceDepth) {
parent = contentAt(this.unplaced.content, sliceDepth - 1).firstChild
fragment = parent!.content
} else {
fragment = this.unplaced.content
}
let first = fragment.firstChild
for (let frontierDepth = this.depth; frontierDepth >= 0; frontierDepth--) {
let {type, match} = this.frontier[frontierDepth], wrap, inject: Fragment | null = null
// In pass 1, if the next node matches, or there is no next
// node but the parents look compatible, we've found a
// place.
if (pass == 1 && (first ? match.matchType(first.type) || (inject = match.fillBefore(Fragment.from(first), false))
: parent && type.compatibleContent(parent.type)))
return {sliceDepth, frontierDepth, parent, inject}
// In pass 2, look for a set of wrapping nodes that make
// `first` fit here.
else if (pass == 2 && first && (wrap = match.findWrapping(first.type)))
return {sliceDepth, frontierDepth, parent, wrap}
// Don't continue looking further up if the parent node
// would fit here.
if (parent && match.matchType(parent.type)) break
}
}
}
}
openMore() {
let {content, openStart, openEnd} = this.unplaced
let inner = contentAt(content, openStart)
if (!inner.childCount || inner.firstChild!.isLeaf) return false
this.unplaced = new Slice(content, openStart + 1,
Math.max(openEnd, inner.size + openStart >= content.size - openEnd ? openStart + 1 : 0))
return true
}
dropNode() {
let {content, openStart, openEnd} = this.unplaced
let inner = contentAt(content, openStart)
if (inner.childCount <= 1 && openStart > 0) {
let openAtEnd = content.size - openStart <= openStart + inner.size
this.unplaced = new Slice(dropFromFragment(content, openStart - 1, 1), openStart - 1,
openAtEnd ? openStart - 1 : openEnd)
} else {
this.unplaced = new Slice(dropFromFragment(content, openStart, 1), openStart, openEnd)
}
}
// Move content from the unplaced slice at `sliceDepth` to the
// frontier node at `frontierDepth`. Close that frontier node when
// applicable.
placeNodes({sliceDepth, frontierDepth, parent, inject, wrap}: Fittable) {
while (this.depth > frontierDepth) this.closeFrontierNode()
if (wrap) for (let i = 0; i < wrap.length; i++) this.openFrontierNode(wrap[i])
let slice = this.unplaced, fragment = parent ? parent.content : slice.content
let openStart = slice.openStart - sliceDepth
let taken = 0, add = []
let {match, type} = this.frontier[frontierDepth]
if (inject) {
for (let i = 0; i < inject.childCount; i++) add.push(inject.child(i))
match = match.matchFragment(inject)!
}
// Computes the amount of (end) open nodes at the end of the
// fragment. When 0, the parent is open, but no more. When
// negative, nothing is open.
let openEndCount = (fragment.size + sliceDepth) - (slice.content.size - slice.openEnd)
// Scan over the fragment, fitting as many child nodes as
// possible.
while (taken < fragment.childCount) {
let next = fragment.child(taken), matches = match.matchType(next.type)
if (!matches) break
taken++
if (taken > 1 || openStart == 0 || next.content.size) { // Drop empty open nodes
match = matches
add.push(closeNodeStart(next.mark(type.allowedMarks(next.marks)), taken == 1 ? openStart : 0,
taken == fragment.childCount ? openEndCount : -1))
}
}
let toEnd = taken == fragment.childCount
if (!toEnd) openEndCount = -1
this.placed = addToFragment(this.placed, frontierDepth, Fragment.from(add))
this.frontier[frontierDepth].match = match
// If the parent types match, and the entire node was moved, and
// it's not open, close this frontier node right away.
if (toEnd && openEndCount < 0 && parent && parent.type == this.frontier[this.depth].type && this.frontier.length > 1)
this.closeFrontierNode()
// Add new frontier nodes for any open nodes at the end.
for (let i = 0, cur = fragment; i < openEndCount; i++) {
let node = cur.lastChild!
this.frontier.push({type: node.type, match: node.contentMatchAt(node.childCount)})
cur = node.content
}
// Update `this.unplaced`. Drop the entire node from which we
// placed it we got to its end, otherwise just drop the placed
// nodes.
this.unplaced = !toEnd ? new Slice(dropFromFragment(slice.content, sliceDepth, taken), slice.openStart, slice.openEnd)
: sliceDepth == 0 ? Slice.empty
: new Slice(dropFromFragment(slice.content, sliceDepth - 1, 1),
sliceDepth - 1, openEndCount < 0 ? slice.openEnd : sliceDepth - 1)
}
mustMoveInline() {
if (!this.$to.parent.isTextblock) return -1
let top = this.frontier[this.depth], level
if (!top.type.isTextblock || !contentAfterFits(this.$to, this.$to.depth, top.type, top.match, false) ||
(this.$to.depth == this.depth && (level = this.findCloseLevel(this.$to)) && level.depth == this.depth)) return -1
let {depth} = this.$to, after = this.$to.after(depth)
while (depth > 1 && after == this.$to.end(--depth)) ++after
return after
}
findCloseLevel($to: ResolvedPos) {
scan: for (let i = Math.min(this.depth, $to.depth); i >= 0; i--) {
let {match, type} = this.frontier[i]
let dropInner = i < $to.depth && $to.end(i + 1) == $to.pos + ($to.depth - (i + 1))
let fit = contentAfterFits($to, i, type, match, dropInner)
if (!fit) continue
for (let d = i - 1; d >= 0; d--) {
let {match, type} = this.frontier[d]
let matches = contentAfterFits($to, d, type, match, true)
if (!matches || matches.childCount) continue scan
}
return {depth: i, fit, move: dropInner ? $to.doc.resolve($to.after(i + 1)) : $to}
}
}
close($to: ResolvedPos) {
let close = this.findCloseLevel($to)
if (!close) return null
while (this.depth > close.depth) this.closeFrontierNode()
if (close.fit.childCount) this.placed = addToFragment(this.placed, close.depth, close.fit)
$to = close.move
for (let d = close.depth + 1; d <= $to.depth; d++) {
let node = $to.node(d), add = node.type.contentMatch.fillBefore(node.content, true, $to.index(d))!
this.openFrontierNode(node.type, node.attrs, add)
}
return $to
}
openFrontierNode(type: NodeType, attrs: Attrs | null = null, content?: Fragment) {
let top = this.frontier[this.depth]
top.match = top.match.matchType(type)!
this.placed = addToFragment(this.placed, this.depth, Fragment.from(type.create(attrs, content)))
this.frontier.push({type, match: type.contentMatch})
}
closeFrontierNode() {
let open = this.frontier.pop()!
let add = open.match.fillBefore(Fragment.empty, true)!
if (add.childCount) this.placed = addToFragment(this.placed, this.frontier.length, add)
}
}
function dropFromFragment(fragment: Fragment, depth: number, count: number): Fragment {
if (depth == 0) return fragment.cutByIndex(count, fragment.childCount)
return fragment.replaceChild(0, fragment.firstChild!.copy(dropFromFragment(fragment.firstChild!.content, depth - 1, count)))
}
function addToFragment(fragment: Fragment, depth: number, content: Fragment): Fragment {
if (depth == 0) return fragment.append(content)
return fragment.replaceChild(fragment.childCount - 1,
fragment.lastChild!.copy(addToFragment(fragment.lastChild!.content, depth - 1, content)))
}
function contentAt(fragment: Fragment, depth: number) {
for (let i = 0; i < depth; i++) fragment = fragment.firstChild!.content
return fragment
}
function closeNodeStart(node: Node, openStart: number, openEnd: number) {
if (openStart <= 0) return node
let frag = node.content
if (openStart > 1)
frag = frag.replaceChild(0, closeNodeStart(frag.firstChild!, openStart - 1, frag.childCount == 1 ? openEnd - 1 : 0))
if (openStart > 0) {
frag = node.type.contentMatch.fillBefore(frag)!.append(frag)
if (openEnd <= 0) frag = frag.append(node.type.contentMatch.matchFragment(frag)!.fillBefore(Fragment.empty, true)!)
}
return node.copy(frag)
}
function contentAfterFits($to: ResolvedPos, depth: number, type: NodeType, match: ContentMatch, open: boolean) {
let node = $to.node(depth), index = open ? $to.indexAfter(depth) : $to.index(depth)
if (index == node.childCount && !type.compatibleContent(node.type)) return null
let fit = match.fillBefore(node.content, true, index)
return fit && !invalidMarks(type, node.content, index) ? fit : null
}
function invalidMarks(type: NodeType, fragment: Fragment, start: number) {
for (let i = start; i < fragment.childCount; i++)
if (!type.allowsMarks(fragment.child(i).marks)) return true
return false
}
function definesContent(type: NodeType) {
return type.spec.defining || type.spec.definingForContent
}
export function replaceRange(tr: Transform, from: number, to: number, slice: Slice) {
if (!slice.size) return tr.deleteRange(from, to)
let $from = tr.doc.resolve(from), $to = tr.doc.resolve(to)
if (fitsTrivially($from, $to, slice))
return tr.step(new ReplaceStep(from, to, slice))
let targetDepths = coveredDepths($from, tr.doc.resolve(to))
// Can't replace the whole document, so remove 0 if it's present
if (targetDepths[targetDepths.length - 1] == 0) targetDepths.pop()
// Negative numbers represent not expansion over the whole node at
// that depth, but replacing from $from.before(-D) to $to.pos.
let preferredTarget = -($from.depth + 1)
targetDepths.unshift(preferredTarget)
// This loop picks a preferred target depth, if one of the covering
// depths is not outside of a defining node, and adds negative
// depths for any depth that has $from at its start and does not
// cross a defining node.
for (let d = $from.depth, pos = $from.pos - 1; d > 0; d--, pos--) {
let spec = $from.node(d).type.spec
if (spec.defining || spec.definingAsContext || spec.isolating) break
if (targetDepths.indexOf(d) > -1) preferredTarget = d
else if ($from.before(d) == pos) targetDepths.splice(1, 0, -d)
}
// Try to fit each possible depth of the slice into each possible
// target depth, starting with the preferred depths.
let preferredTargetIndex = targetDepths.indexOf(preferredTarget)
let leftNodes: Node[] = [], preferredDepth = slice.openStart
for (let content = slice.content, i = 0;; i++) {
let node = content.firstChild!
leftNodes.push(node)
if (i == slice.openStart) break
content = node.content
}
// Back up preferredDepth to cover defining textblocks directly
// above it, possibly skipping a non-defining textblock.
for (let d = preferredDepth - 1; d >= 0; d--) {
let leftNode = leftNodes[d], def = definesContent(leftNode.type)
if (def && !leftNode.sameMarkup($from.node(Math.abs(preferredTarget) - 1))) preferredDepth = d
else if (def || !leftNode.type.isTextblock) break
}
for (let j = slice.openStart; j >= 0; j--) {
let openDepth = (j + preferredDepth + 1) % (slice.openStart + 1)
let insert = leftNodes[openDepth]
if (!insert) continue
for (let i = 0; i < targetDepths.length; i++) {
// Loop over possible expansion levels, starting with the
// preferred one
let targetDepth = targetDepths[(i + preferredTargetIndex) % targetDepths.length], expand = true
if (targetDepth < 0) { expand = false; targetDepth = -targetDepth }
let parent = $from.node(targetDepth - 1), index = $from.index(targetDepth - 1)
if (parent.canReplaceWith(index, index, insert.type, insert.marks))
return tr.replace($from.before(targetDepth), expand ? $to.after(targetDepth) : to,
new Slice(closeFragment(slice.content, 0, slice.openStart, openDepth),
openDepth, slice.openEnd))
}
}
let startSteps = tr.steps.length
for (let i = targetDepths.length - 1; i >= 0; i--) {
tr.replace(from, to, slice)
if (tr.steps.length > startSteps) break
let depth = targetDepths[i]
if (depth < 0) continue
from = $from.before(depth); to = $to.after(depth)
}
}
function closeFragment(fragment: Fragment, depth: number, oldOpen: number, newOpen: number, parent?: Node) {
if (depth < oldOpen) {
let first = fragment.firstChild!
fragment = fragment.replaceChild(0, first.copy(closeFragment(first.content, depth + 1, oldOpen, newOpen, first)))
}
if (depth > newOpen) {
let match = parent!.contentMatchAt(0)!
let start = match.fillBefore(fragment)!.append(fragment)
fragment = start.append(match.matchFragment(start)!.fillBefore(Fragment.empty, true)!)
}
return fragment
}
export function replaceRangeWith(tr: Transform, from: number, to: number, node: Node) {
if (!node.isInline && from == to && tr.doc.resolve(from).parent.content.size) {
let point = insertPoint(tr.doc, from, node.type)
if (point != null) from = to = point
}
tr.replaceRange(from, to, new Slice(Fragment.from(node), 0, 0))
}
export function deleteRange(tr: Transform, from: number, to: number) {
let $from = tr.doc.resolve(from), $to = tr.doc.resolve(to)
let covered = coveredDepths($from, $to)
for (let i = 0; i < covered.length; i++) {
let depth = covered[i], last = i == covered.length - 1
if ((last && depth == 0) || $from.node(depth).type.contentMatch.validEnd)
return tr.delete($from.start(depth), $to.end(depth))
if (depth > 0 && (last || $from.node(depth - 1).canReplace($from.index(depth - 1), $to.indexAfter(depth - 1))))
return tr.delete($from.before(depth), $to.after(depth))
}
for (let d = 1; d <= $from.depth && d <= $to.depth; d++) {
if (from - $from.start(d) == $from.depth - d && to > $from.end(d) && $to.end(d) - to != $to.depth - d)
return tr.delete($from.before(d), to)
}
tr.delete(from, to)
}
// Returns an array of all depths for which $from - $to spans the
// whole content of the nodes at that depth.
function coveredDepths($from: ResolvedPos, $to: ResolvedPos) {
let result: number[] = [], minDepth = Math.min($from.depth, $to.depth)
for (let d = minDepth; d >= 0; d--) {
let start = $from.start(d)
if (start < $from.pos - ($from.depth - d) ||
$to.end(d) > $to.pos + ($to.depth - d) ||
$from.node(d).type.spec.isolating ||
$to.node(d).type.spec.isolating) break
if (start == $to.start(d) ||
(d == $from.depth && d == $to.depth && $from.parent.inlineContent && $to.parent.inlineContent &&
d && $to.start(d - 1) == start - 1))
result.push(d)
}
return result
}

View File

@@ -0,0 +1,178 @@
import {Slice, Node, Schema} from "prosemirror-model"
import {Step, StepResult} from "./step"
import {StepMap, Mappable} from "./map"
/// Replace a part of the document with a slice of new content.
export class ReplaceStep extends Step {
/// The given `slice` should fit the 'gap' between `from` and
/// `to`—the depths must line up, and the surrounding nodes must be
/// able to be joined with the open sides of the slice. When
/// `structure` is true, the step will fail if the content between
/// from and to is not just a sequence of closing and then opening
/// tokens (this is to guard against rebased replace steps
/// overwriting something they weren't supposed to).
constructor(
/// The start position of the replaced range.
readonly from: number,
/// The end position of the replaced range.
readonly to: number,
/// The slice to insert.
readonly slice: Slice,
/// @internal
readonly structure = false
) {
super()
}
apply(doc: Node) {
if (this.structure && contentBetween(doc, this.from, this.to))
return StepResult.fail("Structure replace would overwrite content")
return StepResult.fromReplace(doc, this.from, this.to, this.slice)
}
getMap() {
return new StepMap([this.from, this.to - this.from, this.slice.size])
}
invert(doc: Node) {
return new ReplaceStep(this.from, this.from + this.slice.size, doc.slice(this.from, this.to))
}
map(mapping: Mappable) {
let from = mapping.mapResult(this.from, 1), to = mapping.mapResult(this.to, -1)
if (from.deletedAcross && to.deletedAcross) return null
return new ReplaceStep(from.pos, Math.max(from.pos, to.pos), this.slice)
}
merge(other: Step) {
if (!(other instanceof ReplaceStep) || other.structure || this.structure) return null
if (this.from + this.slice.size == other.from && !this.slice.openEnd && !other.slice.openStart) {
let slice = this.slice.size + other.slice.size == 0 ? Slice.empty
: new Slice(this.slice.content.append(other.slice.content), this.slice.openStart, other.slice.openEnd)
return new ReplaceStep(this.from, this.to + (other.to - other.from), slice, this.structure)
} else if (other.to == this.from && !this.slice.openStart && !other.slice.openEnd) {
let slice = this.slice.size + other.slice.size == 0 ? Slice.empty
: new Slice(other.slice.content.append(this.slice.content), other.slice.openStart, this.slice.openEnd)
return new ReplaceStep(other.from, this.to, slice, this.structure)
} else {
return null
}
}
toJSON(): any {
let json: any = {stepType: "replace", from: this.from, to: this.to}
if (this.slice.size) json.slice = this.slice.toJSON()
if (this.structure) json.structure = true
return json
}
/// @internal
static fromJSON(schema: Schema, json: any) {
if (typeof json.from != "number" || typeof json.to != "number")
throw new RangeError("Invalid input for ReplaceStep.fromJSON")
return new ReplaceStep(json.from, json.to, Slice.fromJSON(schema, json.slice), !!json.structure)
}
}
Step.jsonID("replace", ReplaceStep)
/// Replace a part of the document with a slice of content, but
/// preserve a range of the replaced content by moving it into the
/// slice.
export class ReplaceAroundStep extends Step {
/// Create a replace-around step with the given range and gap.
/// `insert` should be the point in the slice into which the content
/// of the gap should be moved. `structure` has the same meaning as
/// it has in the [`ReplaceStep`](#transform.ReplaceStep) class.
constructor(
/// The start position of the replaced range.
readonly from: number,
/// The end position of the replaced range.
readonly to: number,
/// The start of preserved range.
readonly gapFrom: number,
/// The end of preserved range.
readonly gapTo: number,
/// The slice to insert.
readonly slice: Slice,
/// The position in the slice where the preserved range should be
/// inserted.
readonly insert: number,
/// @internal
readonly structure = false
) {
super()
}
apply(doc: Node) {
if (this.structure && (contentBetween(doc, this.from, this.gapFrom) ||
contentBetween(doc, this.gapTo, this.to)))
return StepResult.fail("Structure gap-replace would overwrite content")
let gap = doc.slice(this.gapFrom, this.gapTo)
if (gap.openStart || gap.openEnd)
return StepResult.fail("Gap is not a flat range")
let inserted = this.slice.insertAt(this.insert, gap.content)
if (!inserted) return StepResult.fail("Content does not fit in gap")
return StepResult.fromReplace(doc, this.from, this.to, inserted)
}
getMap() {
return new StepMap([this.from, this.gapFrom - this.from, this.insert,
this.gapTo, this.to - this.gapTo, this.slice.size - this.insert])
}
invert(doc: Node) {
let gap = this.gapTo - this.gapFrom
return new ReplaceAroundStep(this.from, this.from + this.slice.size + gap,
this.from + this.insert, this.from + this.insert + gap,
doc.slice(this.from, this.to).removeBetween(this.gapFrom - this.from, this.gapTo - this.from),
this.gapFrom - this.from, this.structure)
}
map(mapping: Mappable) {
let from = mapping.mapResult(this.from, 1), to = mapping.mapResult(this.to, -1)
let gapFrom = this.from == this.gapFrom ? from.pos : mapping.map(this.gapFrom, -1)
let gapTo = this.to == this.gapTo ? to.pos : mapping.map(this.gapTo, 1)
if ((from.deletedAcross && to.deletedAcross) || gapFrom < from.pos || gapTo > to.pos) return null
return new ReplaceAroundStep(from.pos, to.pos, gapFrom, gapTo, this.slice, this.insert, this.structure)
}
toJSON(): any {
let json: any = {stepType: "replaceAround", from: this.from, to: this.to,
gapFrom: this.gapFrom, gapTo: this.gapTo, insert: this.insert}
if (this.slice.size) json.slice = this.slice.toJSON()
if (this.structure) json.structure = true
return json
}
/// @internal
static fromJSON(schema: Schema, json: any) {
if (typeof json.from != "number" || typeof json.to != "number" ||
typeof json.gapFrom != "number" || typeof json.gapTo != "number" || typeof json.insert != "number")
throw new RangeError("Invalid input for ReplaceAroundStep.fromJSON")
return new ReplaceAroundStep(json.from, json.to, json.gapFrom, json.gapTo,
Slice.fromJSON(schema, json.slice), json.insert, !!json.structure)
}
}
Step.jsonID("replaceAround", ReplaceAroundStep)
function contentBetween(doc: Node, from: number, to: number) {
let $from = doc.resolve(from), dist = to - from, depth = $from.depth
while (dist > 0 && depth > 0 && $from.indexAfter(depth) == $from.node(depth).childCount) {
depth--
dist--
}
if (dist > 0) {
let next = $from.node(depth).maybeChild($from.indexAfter(depth))
while (dist > 0) {
if (!next || next.isLeaf) return true
next = next.firstChild
dist--
}
}
return false
}

View File

@@ -0,0 +1,97 @@
import {ReplaceError, Schema, Slice, Node} from "prosemirror-model"
import {StepMap, Mappable} from "./map"
const stepsByID: {[id: string]: {fromJSON(schema: Schema, json: any): Step}} = Object.create(null)
/// A step object represents an atomic change. It generally applies
/// only to the document it was created for, since the positions
/// stored in it will only make sense for that document.
///
/// New steps are defined by creating classes that extend `Step`,
/// overriding the `apply`, `invert`, `map`, `getMap` and `fromJSON`
/// methods, and registering your class with a unique
/// JSON-serialization identifier using
/// [`Step.jsonID`](#transform.Step^jsonID).
export abstract class Step {
/// Applies this step to the given document, returning a result
/// object that either indicates failure, if the step can not be
/// applied to this document, or indicates success by containing a
/// transformed document.
abstract apply(doc: Node): StepResult
/// Get the step map that represents the changes made by this step,
/// and which can be used to transform between positions in the old
/// and the new document.
getMap(): StepMap { return StepMap.empty }
/// Create an inverted version of this step. Needs the document as it
/// was before the step as argument.
abstract invert(doc: Node): Step
/// Map this step through a mappable thing, returning either a
/// version of that step with its positions adjusted, or `null` if
/// the step was entirely deleted by the mapping.
abstract map(mapping: Mappable): Step | null
/// Try to merge this step with another one, to be applied directly
/// after it. Returns the merged step when possible, null if the
/// steps can't be merged.
merge(other: Step): Step | null { return null }
/// Create a JSON-serializeable representation of this step. When
/// defining this for a custom subclass, make sure the result object
/// includes the step type's [JSON id](#transform.Step^jsonID) under
/// the `stepType` property.
abstract toJSON(): any
/// Deserialize a step from its JSON representation. Will call
/// through to the step class' own implementation of this method.
static fromJSON(schema: Schema, json: any): Step {
if (!json || !json.stepType) throw new RangeError("Invalid input for Step.fromJSON")
let type = stepsByID[json.stepType]
if (!type) throw new RangeError(`No step type ${json.stepType} defined`)
return type.fromJSON(schema, json)
}
/// To be able to serialize steps to JSON, each step needs a string
/// ID to attach to its JSON representation. Use this method to
/// register an ID for your step classes. Try to pick something
/// that's unlikely to clash with steps from other modules.
static jsonID(id: string, stepClass: {fromJSON(schema: Schema, json: any): Step}) {
if (id in stepsByID) throw new RangeError("Duplicate use of step JSON ID " + id)
stepsByID[id] = stepClass
;(stepClass as any).prototype.jsonID = id
return stepClass
}
}
/// The result of [applying](#transform.Step.apply) a step. Contains either a
/// new document or a failure value.
export class StepResult {
/// @internal
constructor(
/// The transformed document, if successful.
readonly doc: Node | null,
/// The failure message, if unsuccessful.
readonly failed: string | null
) {}
/// Create a successful step result.
static ok(doc: Node) { return new StepResult(doc, null) }
/// Create a failed step result.
static fail(message: string) { return new StepResult(null, message) }
/// Call [`Node.replace`](#model.Node.replace) with the given
/// arguments. Create a successful result if it succeeds, and a
/// failed one if it throws a `ReplaceError`.
static fromReplace(doc: Node, from: number, to: number, slice: Slice) {
try {
return StepResult.ok(doc.replace(from, to, slice))
} catch (e) {
if (e instanceof ReplaceError) return StepResult.fail(e.message)
throw e
}
}
}

View File

@@ -0,0 +1,308 @@
import {Slice, Fragment, NodeRange, NodeType, Node, Mark, Attrs, ContentMatch} from "prosemirror-model"
import {Transform} from "./transform"
import {ReplaceStep, ReplaceAroundStep} from "./replace_step"
import {clearIncompatible} from "./mark"
function canCut(node: Node, start: number, end: number) {
return (start == 0 || node.canReplace(start, node.childCount)) &&
(end == node.childCount || node.canReplace(0, end))
}
/// Try to find a target depth to which the content in the given range
/// can be lifted. Will not go across
/// [isolating](#model.NodeSpec.isolating) parent nodes.
export function liftTarget(range: NodeRange): number | null {
let parent = range.parent
let content = parent.content.cutByIndex(range.startIndex, range.endIndex)
for (let depth = range.depth;; --depth) {
let node = range.$from.node(depth)
let index = range.$from.index(depth), endIndex = range.$to.indexAfter(depth)
if (depth < range.depth && node.canReplace(index, endIndex, content))
return depth
if (depth == 0 || node.type.spec.isolating || !canCut(node, index, endIndex)) break
}
return null
}
export function lift(tr: Transform, range: NodeRange, target: number) {
let {$from, $to, depth} = range
let gapStart = $from.before(depth + 1), gapEnd = $to.after(depth + 1)
let start = gapStart, end = gapEnd
let before = Fragment.empty, openStart = 0
for (let d = depth, splitting = false; d > target; d--)
if (splitting || $from.index(d) > 0) {
splitting = true
before = Fragment.from($from.node(d).copy(before))
openStart++
} else {
start--
}
let after = Fragment.empty, openEnd = 0
for (let d = depth, splitting = false; d > target; d--)
if (splitting || $to.after(d + 1) < $to.end(d)) {
splitting = true
after = Fragment.from($to.node(d).copy(after))
openEnd++
} else {
end++
}
tr.step(new ReplaceAroundStep(start, end, gapStart, gapEnd,
new Slice(before.append(after), openStart, openEnd),
before.size - openStart, true))
}
/// Try to find a valid way to wrap the content in the given range in a
/// node of the given type. May introduce extra nodes around and inside
/// the wrapper node, if necessary. Returns null if no valid wrapping
/// could be found. When `innerRange` is given, that range's content is
/// used as the content to fit into the wrapping, instead of the
/// content of `range`.
export function findWrapping(
range: NodeRange,
nodeType: NodeType,
attrs: Attrs | null = null,
innerRange = range
): {type: NodeType, attrs: Attrs | null}[] | null {
let around = findWrappingOutside(range, nodeType)
let inner = around && findWrappingInside(innerRange, nodeType)
if (!inner) return null
return (around!.map(withAttrs) as {type: NodeType, attrs: Attrs | null}[])
.concat({type: nodeType, attrs}).concat(inner.map(withAttrs))
}
function withAttrs(type: NodeType) { return {type, attrs: null} }
function findWrappingOutside(range: NodeRange, type: NodeType) {
let {parent, startIndex, endIndex} = range
let around = parent.contentMatchAt(startIndex).findWrapping(type)
if (!around) return null
let outer = around.length ? around[0] : type
return parent.canReplaceWith(startIndex, endIndex, outer) ? around : null
}
function findWrappingInside(range: NodeRange, type: NodeType) {
let {parent, startIndex, endIndex} = range
let inner = parent.child(startIndex)
let inside = type.contentMatch.findWrapping(inner.type)
if (!inside) return null
let lastType = inside.length ? inside[inside.length - 1] : type
let innerMatch: ContentMatch | null = lastType.contentMatch
for (let i = startIndex; innerMatch && i < endIndex; i++)
innerMatch = innerMatch.matchType(parent.child(i).type)
if (!innerMatch || !innerMatch.validEnd) return null
return inside
}
export function wrap(tr: Transform, range: NodeRange, wrappers: readonly {type: NodeType, attrs?: Attrs | null}[]) {
let content = Fragment.empty
for (let i = wrappers.length - 1; i >= 0; i--) {
if (content.size) {
let match = wrappers[i].type.contentMatch.matchFragment(content)
if (!match || !match.validEnd)
throw new RangeError("Wrapper type given to Transform.wrap does not form valid content of its parent wrapper")
}
content = Fragment.from(wrappers[i].type.create(wrappers[i].attrs, content))
}
let start = range.start, end = range.end
tr.step(new ReplaceAroundStep(start, end, start, end, new Slice(content, 0, 0), wrappers.length, true))
}
export function setBlockType(tr: Transform, from: number, to: number, type: NodeType, attrs: Attrs | null) {
if (!type.isTextblock) throw new RangeError("Type given to setBlockType should be a textblock")
let mapFrom = tr.steps.length
tr.doc.nodesBetween(from, to, (node, pos) => {
if (node.isTextblock && !node.hasMarkup(type, attrs) && canChangeType(tr.doc, tr.mapping.slice(mapFrom).map(pos), type)) {
let convertNewlines = null
if (type.schema.linebreakReplacement) {
let pre = type.whitespace == "pre", supportLinebreak = !!type.contentMatch.matchType(type.schema.linebreakReplacement)
if (pre && !supportLinebreak) convertNewlines = false
else if (!pre && supportLinebreak) convertNewlines = true
}
// Ensure all markup that isn't allowed in the new node type is cleared
if (convertNewlines === false) replaceLinebreaks(tr, node, pos, mapFrom)
clearIncompatible(tr, tr.mapping.slice(mapFrom).map(pos, 1), type, undefined, convertNewlines === null)
let mapping = tr.mapping.slice(mapFrom)
let startM = mapping.map(pos, 1), endM = mapping.map(pos + node.nodeSize, 1)
tr.step(new ReplaceAroundStep(startM, endM, startM + 1, endM - 1,
new Slice(Fragment.from(type.create(attrs, null, node.marks)), 0, 0), 1, true))
if (convertNewlines === true) replaceNewlines(tr, node, pos, mapFrom)
return false
}
})
}
function replaceNewlines(tr: Transform, node: Node, pos: number, mapFrom: number) {
node.forEach((child, offset) => {
if (child.isText) {
let m, newline = /\r?\n|\r/g
while (m = newline.exec(child.text!)) {
let start = tr.mapping.slice(mapFrom).map(pos + 1 + offset + m.index)
tr.replaceWith(start, start + 1, node.type.schema.linebreakReplacement!.create())
}
}
})
}
function replaceLinebreaks(tr: Transform, node: Node, pos: number, mapFrom: number) {
node.forEach((child, offset) => {
if (child.type == child.type.schema.linebreakReplacement) {
let start = tr.mapping.slice(mapFrom).map(pos + 1 + offset)
tr.replaceWith(start, start + 1, node.type.schema.text("\n"))
}
})
}
function canChangeType(doc: Node, pos: number, type: NodeType) {
let $pos = doc.resolve(pos), index = $pos.index()
return $pos.parent.canReplaceWith(index, index + 1, type)
}
/// Change the type, attributes, and/or marks of the node at `pos`.
/// When `type` isn't given, the existing node type is preserved,
export function setNodeMarkup(tr: Transform, pos: number, type: NodeType | undefined | null,
attrs: Attrs | null, marks: readonly Mark[] | undefined) {
let node = tr.doc.nodeAt(pos)
if (!node) throw new RangeError("No node at given position")
if (!type) type = node.type
let newNode = type.create(attrs, null, marks || node.marks)
if (node.isLeaf)
return tr.replaceWith(pos, pos + node.nodeSize, newNode)
if (!type.validContent(node.content))
throw new RangeError("Invalid content for node type " + type.name)
tr.step(new ReplaceAroundStep(pos, pos + node.nodeSize, pos + 1, pos + node.nodeSize - 1,
new Slice(Fragment.from(newNode), 0, 0), 1, true))
}
/// Check whether splitting at the given position is allowed.
export function canSplit(doc: Node, pos: number, depth = 1,
typesAfter?: (null | {type: NodeType, attrs?: Attrs | null})[]): boolean {
let $pos = doc.resolve(pos), base = $pos.depth - depth
let innerType = (typesAfter && typesAfter[typesAfter.length - 1]) || $pos.parent
if (base < 0 || $pos.parent.type.spec.isolating ||
!$pos.parent.canReplace($pos.index(), $pos.parent.childCount) ||
!innerType.type.validContent($pos.parent.content.cutByIndex($pos.index(), $pos.parent.childCount)))
return false
for (let d = $pos.depth - 1, i = depth - 2; d > base; d--, i--) {
let node = $pos.node(d), index = $pos.index(d)
if (node.type.spec.isolating) return false
let rest = node.content.cutByIndex(index, node.childCount)
let overrideChild = typesAfter && typesAfter[i + 1]
if (overrideChild)
rest = rest.replaceChild(0, overrideChild.type.create(overrideChild.attrs))
let after = (typesAfter && typesAfter[i]) || node
if (!node.canReplace(index + 1, node.childCount) || !after.type.validContent(rest))
return false
}
let index = $pos.indexAfter(base)
let baseType = typesAfter && typesAfter[0]
return $pos.node(base).canReplaceWith(index, index, baseType ? baseType.type : $pos.node(base + 1).type)
}
export function split(tr: Transform, pos: number, depth = 1, typesAfter?: (null | {type: NodeType, attrs?: Attrs | null})[]) {
let $pos = tr.doc.resolve(pos), before = Fragment.empty, after = Fragment.empty
for (let d = $pos.depth, e = $pos.depth - depth, i = depth - 1; d > e; d--, i--) {
before = Fragment.from($pos.node(d).copy(before))
let typeAfter = typesAfter && typesAfter[i]
after = Fragment.from(typeAfter ? typeAfter.type.create(typeAfter.attrs, after) : $pos.node(d).copy(after))
}
tr.step(new ReplaceStep(pos, pos, new Slice(before.append(after), depth, depth), true))
}
/// Test whether the blocks before and after a given position can be
/// joined.
export function canJoin(doc: Node, pos: number): boolean {
let $pos = doc.resolve(pos), index = $pos.index()
return joinable($pos.nodeBefore, $pos.nodeAfter) &&
$pos.parent.canReplace(index, index + 1)
}
function joinable(a: Node | null, b: Node | null) {
return !!(a && b && !a.isLeaf && a.canAppend(b))
}
/// Find an ancestor of the given position that can be joined to the
/// block before (or after if `dir` is positive). Returns the joinable
/// point, if any.
export function joinPoint(doc: Node, pos: number, dir = -1) {
let $pos = doc.resolve(pos)
for (let d = $pos.depth;; d--) {
let before, after, index = $pos.index(d)
if (d == $pos.depth) {
before = $pos.nodeBefore
after = $pos.nodeAfter
} else if (dir > 0) {
before = $pos.node(d + 1)
index++
after = $pos.node(d).maybeChild(index)
} else {
before = $pos.node(d).maybeChild(index - 1)
after = $pos.node(d + 1)
}
if (before && !before.isTextblock && joinable(before, after) &&
$pos.node(d).canReplace(index, index + 1)) return pos
if (d == 0) break
pos = dir < 0 ? $pos.before(d) : $pos.after(d)
}
}
export function join(tr: Transform, pos: number, depth: number) {
let step = new ReplaceStep(pos - depth, pos + depth, Slice.empty, true)
tr.step(step)
}
/// Try to find a point where a node of the given type can be inserted
/// near `pos`, by searching up the node hierarchy when `pos` itself
/// isn't a valid place but is at the start or end of a node. Return
/// null if no position was found.
export function insertPoint(doc: Node, pos: number, nodeType: NodeType): number | null {
let $pos = doc.resolve(pos)
if ($pos.parent.canReplaceWith($pos.index(), $pos.index(), nodeType)) return pos
if ($pos.parentOffset == 0)
for (let d = $pos.depth - 1; d >= 0; d--) {
let index = $pos.index(d)
if ($pos.node(d).canReplaceWith(index, index, nodeType)) return $pos.before(d + 1)
if (index > 0) return null
}
if ($pos.parentOffset == $pos.parent.content.size)
for (let d = $pos.depth - 1; d >= 0; d--) {
let index = $pos.indexAfter(d)
if ($pos.node(d).canReplaceWith(index, index, nodeType)) return $pos.after(d + 1)
if (index < $pos.node(d).childCount) return null
}
return null
}
/// Finds a position at or around the given position where the given
/// slice can be inserted. Will look at parent nodes' nearest boundary
/// and try there, even if the original position wasn't directly at the
/// start or end of that node. Returns null when no position was found.
export function dropPoint(doc: Node, pos: number, slice: Slice): number | null {
let $pos = doc.resolve(pos)
if (!slice.content.size) return pos
let content = slice.content
for (let i = 0; i < slice.openStart; i++) content = content.firstChild!.content
for (let pass = 1; pass <= (slice.openStart == 0 && slice.size ? 2 : 1); pass++) {
for (let d = $pos.depth; d >= 0; d--) {
let bias = d == $pos.depth ? 0 : $pos.pos <= ($pos.start(d + 1) + $pos.end(d + 1)) / 2 ? -1 : 1
let insertPos = $pos.index(d) + (bias > 0 ? 1 : 0)
let parent = $pos.node(d), fits: boolean | null = false
if (pass == 1) {
fits = parent.canReplace(insertPos, insertPos, content)
} else {
let wrapping = parent.contentMatchAt(insertPos).findWrapping(content.firstChild!.type)
fits = wrapping && parent.canReplaceWith(insertPos, insertPos, wrapping[0])
}
if (fits)
return bias == 0 ? $pos.pos : bias < 0 ? $pos.before(d + 1) : $pos.after(d + 1)
}
}
return null
}

View File

@@ -0,0 +1,247 @@
import {Node, NodeType, Mark, MarkType, ContentMatch, Slice, Fragment, NodeRange, Attrs} from "prosemirror-model"
import {Mapping} from "./map"
import {Step} from "./step"
import {addMark, removeMark, clearIncompatible} from "./mark"
import {replaceStep, replaceRange, replaceRangeWith, deleteRange} from "./replace"
import {lift, wrap, setBlockType, setNodeMarkup, split, join} from "./structure"
import {AttrStep, DocAttrStep} from "./attr_step"
import {AddNodeMarkStep, RemoveNodeMarkStep} from "./mark_step"
/// @internal
export let TransformError = class extends Error {}
TransformError = function TransformError(this: any, message: string) {
let err = Error.call(this, message)
;(err as any).__proto__ = TransformError.prototype
return err
} as any
TransformError.prototype = Object.create(Error.prototype)
TransformError.prototype.constructor = TransformError
TransformError.prototype.name = "TransformError"
/// Abstraction to build up and track an array of
/// [steps](#transform.Step) representing a document transformation.
///
/// Most transforming methods return the `Transform` object itself, so
/// that they can be chained.
export class Transform {
/// The steps in this transform.
readonly steps: Step[] = []
/// The documents before each of the steps.
readonly docs: Node[] = []
/// A mapping with the maps for each of the steps in this transform.
readonly mapping: Mapping = new Mapping
/// Create a transform that starts with the given document.
constructor(
/// The current document (the result of applying the steps in the
/// transform).
public doc: Node
) {}
/// The starting document.
get before() { return this.docs.length ? this.docs[0] : this.doc }
/// Apply a new step in this transform, saving the result. Throws an
/// error when the step fails.
step(step: Step) {
let result = this.maybeStep(step)
if (result.failed) throw new TransformError(result.failed)
return this
}
/// Try to apply a step in this transformation, ignoring it if it
/// fails. Returns the step result.
maybeStep(step: Step) {
let result = step.apply(this.doc)
if (!result.failed) this.addStep(step, result.doc!)
return result
}
/// True when the document has been changed (when there are any
/// steps).
get docChanged() {
return this.steps.length > 0
}
/// @internal
addStep(step: Step, doc: Node) {
this.docs.push(this.doc)
this.steps.push(step)
this.mapping.appendMap(step.getMap())
this.doc = doc
}
/// Replace the part of the document between `from` and `to` with the
/// given `slice`.
replace(from: number, to = from, slice = Slice.empty): this {
let step = replaceStep(this.doc, from, to, slice)
if (step) this.step(step)
return this
}
/// Replace the given range with the given content, which may be a
/// fragment, node, or array of nodes.
replaceWith(from: number, to: number, content: Fragment | Node | readonly Node[]): this {
return this.replace(from, to, new Slice(Fragment.from(content), 0, 0))
}
/// Delete the content between the given positions.
delete(from: number, to: number): this {
return this.replace(from, to, Slice.empty)
}
/// Insert the given content at the given position.
insert(pos: number, content: Fragment | Node | readonly Node[]): this {
return this.replaceWith(pos, pos, content)
}
/// Replace a range of the document with a given slice, using
/// `from`, `to`, and the slice's
/// [`openStart`](#model.Slice.openStart) property as hints, rather
/// than fixed start and end points. This method may grow the
/// replaced area or close open nodes in the slice in order to get a
/// fit that is more in line with WYSIWYG expectations, by dropping
/// fully covered parent nodes of the replaced region when they are
/// marked [non-defining as
/// context](#model.NodeSpec.definingAsContext), or including an
/// open parent node from the slice that _is_ marked as [defining
/// its content](#model.NodeSpec.definingForContent).
///
/// This is the method, for example, to handle paste. The similar
/// [`replace`](#transform.Transform.replace) method is a more
/// primitive tool which will _not_ move the start and end of its given
/// range, and is useful in situations where you need more precise
/// control over what happens.
replaceRange(from: number, to: number, slice: Slice): this {
replaceRange(this, from, to, slice)
return this
}
/// Replace the given range with a node, but use `from` and `to` as
/// hints, rather than precise positions. When from and to are the same
/// and are at the start or end of a parent node in which the given
/// node doesn't fit, this method may _move_ them out towards a parent
/// that does allow the given node to be placed. When the given range
/// completely covers a parent node, this method may completely replace
/// that parent node.
replaceRangeWith(from: number, to: number, node: Node): this {
replaceRangeWith(this, from, to, node)
return this
}
/// Delete the given range, expanding it to cover fully covered
/// parent nodes until a valid replace is found.
deleteRange(from: number, to: number): this {
deleteRange(this, from, to)
return this
}
/// Split the content in the given range off from its parent, if there
/// is sibling content before or after it, and move it up the tree to
/// the depth specified by `target`. You'll probably want to use
/// [`liftTarget`](#transform.liftTarget) to compute `target`, to make
/// sure the lift is valid.
lift(range: NodeRange, target: number): this {
lift(this, range, target)
return this
}
/// Join the blocks around the given position. If depth is 2, their
/// last and first siblings are also joined, and so on.
join(pos: number, depth: number = 1): this {
join(this, pos, depth)
return this
}
/// Wrap the given [range](#model.NodeRange) in the given set of wrappers.
/// The wrappers are assumed to be valid in this position, and should
/// probably be computed with [`findWrapping`](#transform.findWrapping).
wrap(range: NodeRange, wrappers: readonly {type: NodeType, attrs?: Attrs | null}[]): this {
wrap(this, range, wrappers)
return this
}
/// Set the type of all textblocks (partly) between `from` and `to` to
/// the given node type with the given attributes.
setBlockType(from: number, to = from, type: NodeType, attrs: Attrs | null = null): this {
setBlockType(this, from, to, type, attrs)
return this
}
/// Change the type, attributes, and/or marks of the node at `pos`.
/// When `type` isn't given, the existing node type is preserved,
setNodeMarkup(pos: number, type?: NodeType | null, attrs: Attrs | null = null, marks?: readonly Mark[]): this {
setNodeMarkup(this, pos, type, attrs, marks)
return this
}
/// Set a single attribute on a given node to a new value.
/// The `pos` addresses the document content. Use `setDocAttribute`
/// to set attributes on the document itself.
setNodeAttribute(pos: number, attr: string, value: any): this {
this.step(new AttrStep(pos, attr, value))
return this
}
/// Set a single attribute on the document to a new value.
setDocAttribute(attr: string, value: any): this {
this.step(new DocAttrStep(attr, value))
return this
}
/// Add a mark to the node at position `pos`.
addNodeMark(pos: number, mark: Mark): this {
this.step(new AddNodeMarkStep(pos, mark))
return this
}
/// Remove a mark (or a mark of the given type) from the node at
/// position `pos`.
removeNodeMark(pos: number, mark: Mark | MarkType): this {
if (!(mark instanceof Mark)) {
let node = this.doc.nodeAt(pos)
if (!node) throw new RangeError("No node at position " + pos)
mark = mark.isInSet(node.marks)!
if (!mark) return this
}
this.step(new RemoveNodeMarkStep(pos, mark))
return this
}
/// Split the node at the given position, and optionally, if `depth` is
/// greater than one, any number of nodes above that. By default, the
/// parts split off will inherit the node type of the original node.
/// This can be changed by passing an array of types and attributes to
/// use after the split.
split(pos: number, depth = 1, typesAfter?: (null | {type: NodeType, attrs?: Attrs | null})[]) {
split(this, pos, depth, typesAfter)
return this
}
/// Add the given mark to the inline content between `from` and `to`.
addMark(from: number, to: number, mark: Mark): this {
addMark(this, from, to, mark)
return this
}
/// Remove marks from inline nodes between `from` and `to`. When
/// `mark` is a single mark, remove precisely that mark. When it is
/// a mark type, remove all marks of that type. When it is null,
/// remove all marks of any type.
removeMark(from: number, to: number, mark?: Mark | MarkType | null) {
removeMark(this, from, to, mark)
return this
}
/// Removes all marks and nodes from the content of the node at
/// `pos` that don't match the given new parent node type. Accepts
/// an optional starting [content match](#model.ContentMatch) as
/// third argument.
clearIncompatible(pos: number, parentType: NodeType, match?: ContentMatch) {
clearIncompatible(this, pos, parentType, match)
return this
}
}