pax_global_header00006660000000000000000000000064137144666310014525gustar00rootroot0000000000000052 comment=0ed09abe98ccacf9459bd6d7813219f54e799e09 prosemirror-transform-1.2.8/000077500000000000000000000000001371446663100161315ustar00rootroot00000000000000prosemirror-transform-1.2.8/.gitignore000066400000000000000000000000731371446663100201210ustar00rootroot00000000000000/node_modules .tern-port /dist /notes.txt webpack.config.jsprosemirror-transform-1.2.8/.npmignore000066400000000000000000000000371371446663100201300ustar00rootroot00000000000000/node_modules .tern-port /test prosemirror-transform-1.2.8/.tern-project000066400000000000000000000001571371446663100205510ustar00rootroot00000000000000{ "libs": ["browser"], "plugins": { "node": {}, "complete_strings": {}, "es_modules": {} } } prosemirror-transform-1.2.8/CHANGELOG.md000066400000000000000000000272761371446663100177600ustar00rootroot00000000000000## 1.2.8 (2020-08-11) ### Bug fixes Fix an issue where fitting a slice at the top level of the document would, in some circumstances, crash. ## 1.2.7 (2020-07-09) ### Bug fixes Fix an issue where in some cases replace fitting would insert an additional bogus node when fitting content into nodes with strict content restrictions. ## 1.2.6 (2020-06-10) ### Bug fixes Fix an issue where creating a replace step would sometimes fail due to unmatchable close tokens after the replaced range. ## 1.2.5 (2020-04-15) ### Bug fixes Rewrite the slice-fitting code used by `replaceStep` to handle a few more corner cases. ## 1.2.4 (2020-03-10) ### Bug fixes Fix `joinPoint` to return check whether the parent node allows a given join. ## 1.2.3 (2019-12-03) ### Bug fixes Fix a crash in `deleteRange` that occurred when deleting a region that spans to the ends of two nodes at different depths. ## 1.2.2 (2019-11-20) ### Bug fixes Rename ES module files to use a .js extension, since Webpack gets confused by .mjs ## 1.2.1 (2019-11-19) ### Bug fixes The file referred to in the package's `module` field now is compiled down to ES5. ## 1.2.0 (2019-11-08) ### New features Add a `module` field to package json file. ## 1.1.6 (2019-11-01) ### Bug fixes Fixes an issue where deleting a range from the start of block A to the end of block B would leave you with an empty block of type B. ## 1.1.5 (2019-10-02) ### Bug fixes Fix crash in building replace steps for open-ended slices with complicated node content expressions. ## 1.1.4 (2019-08-26) ### Bug fixes [`Mapping.invert`](https://prosemirror.net/docs/ref/#transform.Mapping.invert) is now part of the documented API (it was intented to be public from the start, but a typo prevented it from showing up in the docs). Fix an issue where a replace could needlessly drop content when the first node of the slice didn't fit the target context. `replaceRange` now more aggressively expands the replaced region if `replace` fails to place the slice. ## 1.1.3 (2018-07-03) ### Bug fixes Replacing from a code block into a paragraph that has marks, or similar scenarios that would join content with the wrong marks into a node, no longer crashes. ## 1.1.2 (2018-06-29) ### Bug fixes Fix issue where [`replaceRange`](https://prosemirror.net/docs/ref/#transform.Transform.replaceRange) might create invalid nodes. ## 1.1.1 (2018-06-26) ### Bug fixes Fix issues in the new [`dropPoint`](https://prosemirror.net/docs/ref/#transform.dropPoint) function. ## 1.1.0 (2018-06-20) ### New features [`Transform.getMirror`](https://prosemirror.net/docs/ref/#transform.Transform.getMirror), usable in obscure circumstances for inspecting the mirroring structure or a transform, is now a public method. New utility function [`dropPoint`](https://prosemirror.net/docs/ref/#transform.dropPoint), which tries to find a valid position for dropping a slice near a given document position. ## 1.0.10 (2018-04-15) ### Bug fixes [`Transform.setBlockType`](https://prosemirror.net/docs/ref/#transform.Transform.setBlockType) no longer drops marks on the nodes it updates. ## 1.0.9 (2018-04-05) ### Bug fixes Fix a bug that made [`replaceStep`](https://prosemirror.net/docs/ref/#transform.replaceStep) unable to generate wrapper nodes in some circumstances. ## 1.0.8 (2018-04-04) ### Bug fixes Fixes an issue where [`replaceStep`](https://prosemirror.net/docs/ref/#transform.replaceStep) could generate slices that internally violated the schema. ## 1.0.7 (2018-03-21) ### Bug fixes [`Transform.deleteRange`](https://prosemirror.net/docs/ref/#transform.Transform.deleteRange) will cover unmatched opening at the start of the deleted range. ## 1.0.6 (2018-03-15) ### Bug fixes Throw errors, rather than constructing invalid objects, when deserializing from invalid JSON data. ## 1.0.5 (2018-03-14) ### Bug fixes [`replaceStep`](https://prosemirror.net/docs/ref/#transform.replaceStep) will now return null rather than an empty step when it fails to place the slice. Avoid duplicating defining parent nodes in [`replaceRange`](https://prosemirror.net/docs/ref/#tranform.Transform.replaceRange). ## 1.0.4 (2018-02-23) ### Bug fixes Fix overeager closing of destination nodes when fitting a slice during replacing. ## 1.0.3 (2018-02-23) ### Bug fixes Fix a problem where slice-placing didn't handle content matches correctly and might generate invalid steps or fail to generate steps though a valid one exists. Allows adjacent nodes from an inserted slice to be placed in different parent nodes, allowing `Transform.replace` to create fits that weren't previously found. ## 1.0.2 (2018-01-24) ### Bug fixes Fixes a crash in [`replace`](https://prosemirror.net/docs/ref/#transform.Transform.replace). ## 1.0.1 (2017-11-10) ### Bug fixes The errors raised by [`Transform.step`](https://prosemirror.net/docs/ref/#transform.Transform.step) now properly inherit from Error. ## 1.0.0 (2017-10-13) ### Bug fixes When [`setBlockType`](https://prosemirror.net/docs/ref/#transform.Transform.setBlockType) comes across a textblock that can't be changed due to schema constraints, it skips it instead of failing. [`canSplit`](https://prosemirror.net/docs/ref/#transform.canSplit) now returns false when the requested split goes through isolating nodes. ## 0.24.0 (2017-09-25) ### Breaking changes The `setNodeType` method on transforms is now more descriptively called [`setNodeMarkup`](https://prosemirror.net/docs/ref/version/0.24.0.html#transform.Transform.setNodeMarkup). The old name will continue to work with a warning until the next release. ## 0.23.0 (2017-09-13) ### Breaking changes [`Step.toJSON`](https://prosemirror.net/docs/ref/version/0.23.0.html#transform.Step.toJSON) no longer has a default implementation. Steps no longer have an `offset` method. Map them through a map created with [`StepMap.offset`](https://prosemirror.net/docs/ref/version/0.23.0.html#transform.StepMap^offset) instead. The `clearMarkup` method on [`Transform`](https://prosemirror.net/docs/ref/version/0.23.0.html#transform.Transform) is no longer supported (you probably needed [`clearIncompatible`](https://prosemirror.net/docs/ref/version/0.23.0.html#transform.Transform.clearIncompatible) anyway). ### Bug fixes Pasting a list item at the start of a non-empty textblock now wraps the textblock in a list. Marks on open nodes at the left of a slice are no longer dropped by [`Transform.replace`](https://prosemirror.net/docs/ref/version/0.23.0.html#transform.Transform.replace). ### New features `StepMap` now has a static method [`offset`](https://prosemirror.net/docs/ref/version/0.23.0.html#transform.StepMap^offset), which can be used to create a map that offsets all positions by a given distance. Transform objects now have a [`clearIncompatible`](https://prosemirror.net/docs/ref/version/0.23.0.html#transform.Transform.clearIncompatible) method that can help make sure a node's content matches another node type. ## 0.22.2 (2017-07-06) ### Bug fixes Fix another bug in the way `canSplit` interpreted its `typesAfter` argument. ## 0.22.1 (2017-07-03) ### Bug fixes Fix crash in [`canSplit`](https://prosemirror.net/docs/ref/version/0.22.0.html#transform.canSplit) when an array containing null fields is passed as fourth argument. ## 0.22.0 (2017-06-29) ### Bug fixes [`canSplit`](https://prosemirror.net/docs/ref/version/0.22.0.html#transform.canSplit) now returns false when given custom after-split node types that don't match the content at that point. Fixes [`canLift`](https://prosemirror.net/docs/ref/version/0.22.0.html#transform.canLift) incorrectly returning null when lifting into an isolating node. ## 0.21.1 (2017-05-16) ### Bug fixes [`addMark`](https://prosemirror.net/docs/ref/version/0.21.0.html#transform.Transform.addMark) no longer assumes marks always [exclude](https://prosemirror.net/docs/ref/version/0.21.0.html#model.MarkSpec.excludes) only themselves. `replaceRange`](https://prosemirror.net/docs/ref/version/0.21.0.html#transform.Transform.replaceRange) and [`deleteRange`](https://prosemirror.net/docs/ref/version/0.21.0.html#transform.Transform.deleteRange) will no longer expand the range across isolating node boundaries. ## 0.20.0 (2017-04-03) ### Bug fixes Fixes issue where replacing would sometimes unexpectedly split nodes. ## 0.18.0 (2017-02-24) ### New features [`Transform.setNodeType`](https://prosemirror.net/docs/ref/version/0.18.0.html#transform.Transform.setNodeType) now takes an optional argument to set the new node's attributes. Steps now provide an [`offset`](https://prosemirror.net/docs/ref/version/0.18.0.html#transform.Step.offset) method, which makes it possible to create a copy the step with its position offset by a given amount. [`docChanged`](https://prosemirror.net/docs/ref/version/0.18.0.html#transform.Transform.docChanged) is now a property on the `Transform` class, rather than its `Transaction` subclass. `Mapping` instances now have [`invert`](https://prosemirror.net/docs/ref/version/0.18.0.html#transform.Mapping.invert) and [`appendMappingInverted`](https://prosemirror.net/docs/ref/version/0.18.0.html#transform.Mapping.appendMappingInverted) methods to make mapping through them in reverse easier. ## 0.15.0 (2016-12-10) ### Bug fixes Fix bug where pasted/inserted content would sometimes get incorrectly closed at the right side. ## 0.13.0 (2016-11-11) ### Bug fixes Fix issue where [`Transform.replace`](https://prosemirror.net/docs/ref/version/0.13.0.html#transform.Transform.replace) would, in specific circumstances, unneccessarily drop content. ### New features The new [`Transform`](https://prosemirror.net/docs/ref/version/0.13.0.html#transform.Transform) method [`replaceRange`](https://prosemirror.net/docs/ref/version/0.13.0.html#transform.Transform.replaceRange), [`replaceRangeWith`](https://prosemirror.net/docs/ref/version/0.13.0.html#transform.Transform.replaceRangeWith), and [`deleteRange`](https://prosemirror.net/docs/ref/version/0.13.0.html#transform.Transform.deleteRange) provide a way to replace and delete content in a 'do what I mean' way, automatically expanding the replaced region over empty parent nodes and including the parent nodes in the inserted content when appropriate. ## 0.12.1 (2016-11-01) ### Bug fixes Fix bug in `Transform.setBlockType` when used in a transform that already has steps. ## 0.12.0 (2016-10-21) ### Breaking changes Mapped positions now count as deleted when the token to the side specified by the `assoc` parameter is deleted, rather than when both tokens around them are deleted. (This is usually what you already wanted anyway.) ## 0.11.0 (2016-09-21) ### Breaking changes Moved into a separate module. The `Remapping` class was renamed to `Mapping` and works differently (simpler, grows in only one direction, and has provision for mapping through only a part of it). [`Transform`](https://prosemirror.net/docs/ref/version/0.11.0.html#transform.Transform) objects now build up a `Mapping` instead of an array of maps. `PosMap` was renamed to [`StepMap`](https://prosemirror.net/docs/ref/version/0.11.0.html#transform.StepMap) to make it clearer that this applies only to a single step (as opposed to [`Mapping`](https://prosemirror.net/docs/ref/version/0.11.0.html#transform.Mapping). The arguments to [`canSplit`](https://prosemirror.net/docs/ref/version/0.11.0.html#transform.canSplit) and [`split`](https://prosemirror.net/docs/ref/version/0.11.0.html#transform.Transform.split) were changed to make it possible to specify multiple split-off node types for splits with a depth greater than 1. Rename `joinable` to [`canJoin`](https://prosemirror.net/docs/ref/version/0.11.0.html#transform.canJoin). ### New features Steps can now be [merged](https://prosemirror.net/docs/ref/version/0.11.0.html#transform.Step.merge) in some circumstances, which can be useful when storing a lot of them. prosemirror-transform-1.2.8/CONTRIBUTING.md000066400000000000000000000075161371446663100203730ustar00rootroot00000000000000# 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. prosemirror-transform-1.2.8/LICENSE000066400000000000000000000021131371446663100171330ustar00rootroot00000000000000Copyright (C) 2015-2017 by Marijn Haverbeke 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. prosemirror-transform-1.2.8/README.md000066400000000000000000000030241371446663100174070ustar00rootroot00000000000000# prosemirror-transform [ [**WEBSITE**](https://prosemirror.net) | [**ISSUES**](https://github.com/prosemirror/prosemirror/issues) | [**FORUM**](https://discuss.prosemirror.net) | [**GITTER**](https://gitter.im/ProseMirror/prosemirror) | [**CHANGELOG**](https://github.com/ProseMirror/prosemirror-transform/blob/master/CHANGELOG.md) ] This is a [core module](https://prosemirror.net/docs/ref/#transform) of [ProseMirror](https://prosemirror.net). ProseMirror is a well-behaved rich semantic content editor based on contentEditable, with support for collaborative editing and custom document schemas. This [module](https://prosemirror.net/docs/ref/#transform) implements document [transforms](https://prosemirror.net/docs/guide/#transform), which are used by the editor to treat changes as first-class values, which can be saved, shared, and reasoned about. The [project page](https://prosemirror.net) has more information, a number of [examples](https://prosemirror.net/examples/) and the [documentation](https://prosemirror.net/docs/). This code is released under an [MIT license](https://github.com/prosemirror/prosemirror/tree/master/LICENSE). There's a [forum](http://discuss.prosemirror.net) for general discussion and support requests, and the [Github bug tracker](https://github.com/prosemirror/prosemirror/issues) is the place to report issues. We aim to be an inclusive, welcoming community. To make that explicit, we have a [code of conduct](http://contributor-covenant.org/version/1/1/0/) that applies to communication around the project. prosemirror-transform-1.2.8/package.json000066400000000000000000000014771371446663100204300ustar00rootroot00000000000000{ "name": "prosemirror-transform", "version": "1.2.8", "description": "ProseMirror document transformations", "main": "dist/index.js", "module": "dist/index.es.js", "license": "MIT", "maintainers": [ { "name": "Marijn Haverbeke", "email": "marijnh@gmail.com", "web": "http://marijnhaverbeke.nl" } ], "repository": { "type": "git", "url": "git://github.com/prosemirror/prosemirror-transform.git" }, "dependencies": { "prosemirror-model": "^1.0.0" }, "devDependencies": { "mocha": "^3.0.2", "ist": "^1.0.0", "prosemirror-test-builder": "^1.0.0", "rollup": "^1.26.3", "@rollup/plugin-buble": "^0.20.0" }, "scripts": { "test": "mocha test/test-*.js", "build": "rollup -c", "watch": "rollup -c -w", "prepare": "npm run build" } } prosemirror-transform-1.2.8/rollup.config.js000066400000000000000000000004561371446663100212550ustar00rootroot00000000000000module.exports = { input: './src/index.js', output: [{ file: 'dist/index.js', format: 'cjs', sourcemap: true }, { file: 'dist/index.es.js', format: 'es', sourcemap: true }], plugins: [require('@rollup/plugin-buble')()], external(id) { return !/^[\.\/]/.test(id) } } prosemirror-transform-1.2.8/src/000077500000000000000000000000001371446663100167205ustar00rootroot00000000000000prosemirror-transform-1.2.8/src/README.md000066400000000000000000000027771371446663100202140ustar00rootroot00000000000000This 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 ### 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 prosemirror-transform-1.2.8/src/index.js000066400000000000000000000006471371446663100203740ustar00rootroot00000000000000export {Transform, TransformError} from "./transform" export {Step, StepResult} from "./step" export {joinPoint, canJoin, canSplit, insertPoint, dropPoint, liftTarget, findWrapping} from "./structure" export {StepMap, MapResult, Mapping} from "./map" export {AddMarkStep, RemoveMarkStep} from "./mark_step" export {ReplaceStep, ReplaceAroundStep} from "./replace_step" import "./mark" export {replaceStep} from "./replace" prosemirror-transform-1.2.8/src/map.js000066400000000000000000000230101371446663100200270ustar00rootroot00000000000000// Mappable:: interface // There are several things that positions can be mapped through. // Such objects conform to this interface. // // map:: (pos: number, assoc: ?number) → number // 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. // // mapResult:: (pos: number, assoc: ?number) → MapResult // 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. // 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, offset) { return index + offset * factor16 } function recoverIndex(value) { return value & lower16 } function recoverOffset(value) { return (value - (value & lower16)) / factor16 } // ::- An object representing a mapped position with extra // information. export class MapResult { constructor(pos, deleted = false, recover = null) { // :: number The mapped version of the position. this.pos = pos // :: bool Tells you whether the position was deleted, that is, // whether the step removed its surroundings from the document. this.deleted = deleted this.recover = recover } } // :: class extends Mappable // 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 { // :: ([number]) // 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(ranges, inverted = false) { this.ranges = ranges this.inverted = inverted } recover(value) { 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) } // : (number, ?number) → MapResult mapResult(pos, assoc = 1) { return this._map(pos, assoc, false) } // : (number, ?number) → number map(pos, assoc = 1) { return this._map(pos, assoc, true) } _map(pos, assoc, simple) { 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) return new MapResult(result, assoc < 0 ? pos != start : pos != end, recover) } diff += newSize - oldSize } return simple ? pos + diff : new MapResult(pos + diff) } touches(pos, recover) { 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 } // :: ((oldStart: number, oldEnd: number, newStart: number, newEnd: number)) // Calls the given function on each of the changed ranges included in // this map. forEach(f) { 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 } } // :: () → StepMap // 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) } toString() { return (this.inverted ? "-" : "") + JSON.stringify(this.ranges) } // :: (n: number) → 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) { return n == 0 ? StepMap.empty : new StepMap(n < 0 ? [0, -n, 0] : [0, 0, n]) } } StepMap.empty = new StepMap([]) // :: class extends Mappable // 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 { // :: (?[StepMap]) // Create a new mapping with the given position maps. constructor(maps, mirror, from, to) { // :: [StepMap] // The step maps in this mapping. this.maps = maps || [] // :: number // The starting position in the `maps` array, used when `map` or // `mapResult` is called. this.from = from || 0 // :: number // The end position in the `maps` array. this.to = to == null ? this.maps.length : to this.mirror = mirror } // :: (?number, ?number) → Mapping // 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) } copy() { return new Mapping(this.maps.slice(), this.mirror && this.mirror.slice(), this.from, this.to) } // :: (StepMap, ?number) // 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, mirrors) { this.to = this.maps.push(map) if (mirrors != null) this.setMirror(this.maps.length - 1, mirrors) } // :: (Mapping) // Add all the step maps in a given mapping to this one (preserving // mirroring information). appendMapping(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 : null) } } // :: (number) → ?number // 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) { 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)] } setMirror(n, m) { if (!this.mirror) this.mirror = [] this.mirror.push(n, m) } // :: (Mapping) // Append the inverse of the given mapping to this one. appendMappingInverted(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 : null) } } // :: () → Mapping // Create an inverted version of this mapping. invert() { let inverse = new Mapping inverse.appendMappingInverted(this) return inverse } // : (number, ?number) → number // Map a position through this mapping. map(pos, assoc = 1) { if (this.mirror) return this._map(pos, assoc, true) for (let i = this.from; i < this.to; i++) pos = this.maps[i].map(pos, assoc) return pos } // : (number, ?number) → MapResult // Map a position through this mapping, returning a mapping // result. mapResult(pos, assoc = 1) { return this._map(pos, assoc, false) } _map(pos, assoc, simple) { let deleted = false 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 } } if (result.deleted) deleted = true pos = result.pos } return simple ? pos : new MapResult(pos, deleted) } } prosemirror-transform-1.2.8/src/mark.js000066400000000000000000000072731371446663100202210ustar00rootroot00000000000000import {MarkType, Slice, Fragment} from "prosemirror-model" import {Transform} from "./transform" import {AddMarkStep, RemoveMarkStep} from "./mark_step" import {ReplaceStep} from "./replace_step" // :: (number, number, Mark) → this // Add the given mark to the inline content between `from` and `to`. Transform.prototype.addMark = function(from, to, mark) { let removed = [], added = [], removing = null, adding = null this.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.to = end else removed.push(removing = new RemoveMarkStep(start, end, marks[i])) } } if (adding && adding.to == start) adding.to = end else added.push(adding = new AddMarkStep(start, end, mark)) } }) removed.forEach(s => this.step(s)) added.forEach(s => this.step(s)) return this } // :: (number, number, ?union) → 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. Transform.prototype.removeMark = function(from, to, mark = null) { let matched = [], step = 0 this.doc.nodesBetween(from, to, (node, pos) => { if (!node.isInline) return step++ let toRemove = null if (mark instanceof MarkType) { let found = mark.isInSet(node.marks) if (found) toRemove = [found] } 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 => this.step(new RemoveMarkStep(m.from, m.to, m.style))) return this } // :: (number, NodeType, ?ContentMatch) → 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. Transform.prototype.clearIncompatible = function(pos, parentType, match = parentType.contentMatch) { let node = this.doc.nodeAt(pos) let delSteps = [], 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, child.attrs) if (!allowed) { delSteps.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)) this.step(new RemoveMarkStep(cur, end, child.marks[j])) } cur = end } if (!match.validEnd) { let fill = match.fillBefore(Fragment.empty, true) this.replace(cur, cur, new Slice(fill, 0, 0)) } for (let i = delSteps.length - 1; i >= 0; i--) this.step(delSteps[i]) return this } prosemirror-transform-1.2.8/src/mark_step.js000066400000000000000000000072531371446663100212520ustar00rootroot00000000000000import {Fragment, Slice} from "prosemirror-model" import {Step, StepResult} from "./step" function mapFragment(fragment, f, parent) { 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 { // :: (number, number, Mark) constructor(from, to, mark) { super() this.from = from this.to = to this.mark = mark } apply(doc) { 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 (!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() { return new RemoveMarkStep(this.from, this.to, this.mark) } map(mapping) { 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) { 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) } toJSON() { return {stepType: "addMark", mark: this.mark.toJSON(), from: this.from, to: this.to} } static fromJSON(schema, json) { 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 { // :: (number, number, Mark) constructor(from, to, mark) { super() this.from = from this.to = to this.mark = mark } apply(doc) { let oldSlice = doc.slice(this.from, this.to) let slice = new Slice(mapFragment(oldSlice.content, node => { return node.mark(this.mark.removeFromSet(node.marks)) }), oldSlice.openStart, oldSlice.openEnd) return StepResult.fromReplace(doc, this.from, this.to, slice) } invert() { return new AddMarkStep(this.from, this.to, this.mark) } map(mapping) { 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) { 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) } toJSON() { return {stepType: "removeMark", mark: this.mark.toJSON(), from: this.from, to: this.to} } static fromJSON(schema, json) { 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) prosemirror-transform-1.2.8/src/replace.js000066400000000000000000000521311371446663100206730ustar00rootroot00000000000000import {Fragment, Slice} from "prosemirror-model" import {ReplaceStep, ReplaceAroundStep} from "./replace_step" import {Transform} from "./transform" import {insertPoint} from "./structure" // :: (Node, number, ?number, ?Slice) → ?Step // ‘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, from, to = from, slice = Slice.empty) { 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() } // :: (number, ?number, ?Slice) → this // Replace the part of the document between `from` and `to` with the // given `slice`. Transform.prototype.replace = function(from, to = from, slice = Slice.empty) { let step = replaceStep(this.doc, from, to, slice) if (step) this.step(step) return this } // :: (number, number, union) → this // Replace the given range with the given content, which may be a // fragment, node, or array of nodes. Transform.prototype.replaceWith = function(from, to, content) { return this.replace(from, to, new Slice(Fragment.from(content), 0, 0)) } // :: (number, number) → this // Delete the content between the given positions. Transform.prototype.delete = function(from, to) { return this.replace(from, to, Slice.empty) } // :: (number, union) → this // Insert the given content at the given position. Transform.prototype.insert = function(pos, content) { return this.replaceWith(pos, pos, content) } function fitsTrivially($from, $to, slice) { return !slice.openStart && !slice.openEnd && $from.start() == $to.start() && $from.parent.canReplace($from.index(), $to.index(), slice.content) } // 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 , 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 { constructor($from, $to, slice) { this.$to = $to this.$from = $from this.unplaced = slice this.frontier = [] 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)) }) } this.placed = Fragment.empty 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) } // 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() { // Only try wrapping nodes (pass 2) after finding a place without // wrapping failed. for (let pass = 1; pass <= 2; pass++) { for (let sliceDepth = this.unplaced.openStart; sliceDepth >= 0; sliceDepth--) { let fragment, parent 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 // 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)) : 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) } } // : ({sliceDepth: number, frontierDepth: number, parent: ?Node, wrap: ?[NodeType], inject: ?Fragment}) // 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}) { 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 || this.$to.end() == this.$to.pos) 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) { 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) { 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, attrs, content) { 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, depth, count) { if (depth == 0) return fragment.cutByIndex(count) return fragment.replaceChild(0, fragment.firstChild.copy(dropFromFragment(fragment.firstChild.content, depth - 1, count))) } function addToFragment(fragment, depth, content) { 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, depth) { for (let i = 0; i < depth; i++) fragment = fragment.firstChild.content return fragment } function closeNodeStart(node, openStart, openEnd) { 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, depth, type, match, open) { 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, fragment, start) { for (let i = start; i < fragment.childCount; i++) if (!type.allowsMarks(fragment.child(i).marks)) return true return false } // :: (number, number, Slice) → this // 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](#model.NodeSpec.defining), or // including an open parent node from the slice that _is_ marked as // [defining](#model.NodeSpec.defining). // // 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. Transform.prototype.replaceRange = function(from, to, slice) { if (!slice.size) return this.deleteRange(from, to) let $from = this.doc.resolve(from), $to = this.doc.resolve(to) if (fitsTrivially($from, $to, slice)) return this.step(new ReplaceStep(from, to, slice)) let targetDepths = coveredDepths($from, this.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.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 = [], 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 if the node directly above openStart, or the node above // that separated only by a non-defining textblock node, is defining. if (preferredDepth > 0 && leftNodes[preferredDepth - 1].type.spec.defining && $from.node(preferredTargetIndex).type != leftNodes[preferredDepth - 1].type) preferredDepth -= 1 else if (preferredDepth >= 2 && leftNodes[preferredDepth - 1].isTextblock && leftNodes[preferredDepth - 2].type.spec.defining && $from.node(preferredTargetIndex).type != leftNodes[preferredDepth - 2].type) preferredDepth -= 2 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 this.replace($from.before(targetDepth), expand ? $to.after(targetDepth) : to, new Slice(closeFragment(slice.content, 0, slice.openStart, openDepth), openDepth, slice.openEnd)) } } let startSteps = this.steps.length for (let i = targetDepths.length - 1; i >= 0; i--) { this.replace(from, to, slice) if (this.steps.length > startSteps) break let depth = targetDepths[i] if (i < 0) continue from = $from.before(depth); to = $to.after(depth) } return this } function closeFragment(fragment, depth, oldOpen, newOpen, parent) { 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 } // :: (number, number, Node) → 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. Transform.prototype.replaceRangeWith = function(from, to, node) { if (!node.isInline && from == to && this.doc.resolve(from).parent.content.size) { let point = insertPoint(this.doc, from, node.type) if (point != null) from = to = point } return this.replaceRange(from, to, new Slice(Fragment.from(node), 0, 0)) } // :: (number, number) → this // Delete the given range, expanding it to cover fully covered // parent nodes until a valid replace is found. Transform.prototype.deleteRange = function(from, to) { let $from = this.doc.resolve(from), $to = this.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 this.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 this.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 this.delete($from.before(d), to) } return this.delete(from, to) } // : (ResolvedPos, ResolvedPos) → [number] // Returns an array of all depths for which $from - $to spans the // whole content of the nodes at that depth. function coveredDepths($from, $to) { let result = [], 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)) result.push(d) } return result } prosemirror-transform-1.2.8/src/replace_step.js000066400000000000000000000146571371446663100217410ustar00rootroot00000000000000import {Slice} from "prosemirror-model" import {Step, StepResult} from "./step" import {StepMap} from "./map" // ::- Replace a part of the document with a slice of new content. export class ReplaceStep extends Step { // :: (number, number, Slice, ?bool) // 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(from, to, slice, structure) { super() this.from = from this.to = to this.slice = slice this.structure = !!structure } apply(doc) { 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) { return new ReplaceStep(this.from, this.from + this.slice.size, doc.slice(this.from, this.to)) } map(mapping) { let from = mapping.mapResult(this.from, 1), to = mapping.mapResult(this.to, -1) if (from.deleted && to.deleted) return null return new ReplaceStep(from.pos, Math.max(from.pos, to.pos), this.slice) } merge(other) { 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() { let json = {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 } static fromJSON(schema, json) { 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 { // :: (number, number, number, number, Slice, number, ?bool) // 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(from, to, gapFrom, gapTo, slice, insert, structure) { super() this.from = from this.to = to this.gapFrom = gapFrom this.gapTo = gapTo this.slice = slice this.insert = insert this.structure = !!structure } apply(doc) { 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) { 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) { let from = mapping.mapResult(this.from, 1), to = mapping.mapResult(this.to, -1) let gapFrom = mapping.map(this.gapFrom, -1), gapTo = mapping.map(this.gapTo, 1) if ((from.deleted && to.deleted) || gapFrom < from.pos || gapTo > to.pos) return null return new ReplaceAroundStep(from.pos, to.pos, gapFrom, gapTo, this.slice, this.insert, this.structure) } toJSON() { let json = {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 } static fromJSON(schema, json) { 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, from, to) { 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 } prosemirror-transform-1.2.8/src/step.js000066400000000000000000000101201371446663100202230ustar00rootroot00000000000000import {ReplaceError} from "prosemirror-model" import {StepMap} from "./map" function mustOverride() { throw new Error("Override me") } const stepsByID = 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 class Step { // :: (doc: Node) → StepResult // 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. apply(_doc) { return mustOverride() } // :: () → StepMap // 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() { return StepMap.empty } // :: (doc: Node) → Step // Create an inverted version of this step. Needs the document as it // was before the step as argument. invert(_doc) { return mustOverride() } // :: (mapping: Mappable) → ?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. map(_mapping) { return mustOverride() } // :: (other: Step) → ?Step // 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) { return null } // :: () → Object // 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. toJSON() { return mustOverride() } // :: (Schema, Object) → Step // Deserialize a step from its JSON representation. Will call // through to the step class' own implementation of this method. static fromJSON(schema, json) { 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) } // :: (string, constructor) // 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, stepClass) { if (id in stepsByID) throw new RangeError("Duplicate use of step JSON ID " + id) stepsByID[id] = stepClass stepClass.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 { // : (?Node, ?string) constructor(doc, failed) { // :: ?Node The transformed document. this.doc = doc // :: ?string Text providing information about a failed step. this.failed = failed } // :: (Node) → StepResult // Create a successful step result. static ok(doc) { return new StepResult(doc, null) } // :: (string) → StepResult // Create a failed step result. static fail(message) { return new StepResult(null, message) } // :: (Node, number, number, Slice) → StepResult // 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, from, to, slice) { try { return StepResult.ok(doc.replace(from, to, slice)) } catch (e) { if (e instanceof ReplaceError) return StepResult.fail(e.message) throw e } } } prosemirror-transform-1.2.8/src/structure.js000066400000000000000000000304061371446663100213210ustar00rootroot00000000000000import {Slice, Fragment} from "prosemirror-model" import {Transform} from "./transform" import {ReplaceStep, ReplaceAroundStep} from "./replace_step" function canCut(node, start, end) { return (start == 0 || node.canReplace(start, node.childCount)) && (end == node.childCount || node.canReplace(0, end)) } // :: (NodeRange) → ?number // 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) { 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 } } // :: (NodeRange, 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`](#transform.liftTarget) to compute `target`, to make // sure the lift is valid. Transform.prototype.lift = function(range, target) { 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++ } return this.step(new ReplaceAroundStep(start, end, gapStart, gapEnd, new Slice(before.append(after), openStart, openEnd), before.size - openStart, true)) } // :: (NodeRange, NodeType, ?Object, ?NodeRange) → ?[{type: NodeType, attrs: ?Object}] // 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, nodeType, attrs, innerRange = range) { let around = findWrappingOutside(range, nodeType) let inner = around && findWrappingInside(innerRange, nodeType) if (!inner) return null return around.map(withAttrs).concat({type: nodeType, attrs}).concat(inner.map(withAttrs)) } function withAttrs(type) { return {type, attrs: null} } function findWrappingOutside(range, type) { 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, type) { 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 = 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 } // :: (NodeRange, [{type: NodeType, attrs: ?Object}]) → 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). Transform.prototype.wrap = function(range, wrappers) { let content = Fragment.empty for (let i = wrappers.length - 1; i >= 0; i--) content = Fragment.from(wrappers[i].type.create(wrappers[i].attrs, content)) let start = range.start, end = range.end return this.step(new ReplaceAroundStep(start, end, start, end, new Slice(content, 0, 0), wrappers.length, true)) } // :: (number, ?number, NodeType, ?Object) → this // Set the type of all textblocks (partly) between `from` and `to` to // the given node type with the given attributes. Transform.prototype.setBlockType = function(from, to = from, type, attrs) { if (!type.isTextblock) throw new RangeError("Type given to setBlockType should be a textblock") let mapFrom = this.steps.length this.doc.nodesBetween(from, to, (node, pos) => { if (node.isTextblock && !node.hasMarkup(type, attrs) && canChangeType(this.doc, this.mapping.slice(mapFrom).map(pos), type)) { // Ensure all markup that isn't allowed in the new node type is cleared this.clearIncompatible(this.mapping.slice(mapFrom).map(pos, 1), type) let mapping = this.mapping.slice(mapFrom) let startM = mapping.map(pos, 1), endM = mapping.map(pos + node.nodeSize, 1) this.step(new ReplaceAroundStep(startM, endM, startM + 1, endM - 1, new Slice(Fragment.from(type.create(attrs, null, node.marks)), 0, 0), 1, true)) return false } }) return this } function canChangeType(doc, pos, type) { let $pos = doc.resolve(pos), index = $pos.index() return $pos.parent.canReplaceWith(index, index + 1, type) } // :: (number, ?NodeType, ?Object, ?[Mark]) → this // Change the type, attributes, and/or marks of the node at `pos`. // When `type` isn't given, the existing node type is preserved, Transform.prototype.setNodeMarkup = function(pos, type, attrs, marks) { let node = this.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 this.replaceWith(pos, pos + node.nodeSize, newNode) if (!type.validContent(node.content)) throw new RangeError("Invalid content for node type " + type.name) return this.step(new ReplaceAroundStep(pos, pos + node.nodeSize, pos + 1, pos + node.nodeSize - 1, new Slice(Fragment.from(newNode), 0, 0), 1, true)) } // :: (Node, number, number, ?[?{type: NodeType, attrs: ?Object}]) → bool // Check whether splitting at the given position is allowed. export function canSplit(doc, pos, depth = 1, typesAfter) { 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 after = (typesAfter && typesAfter[i]) || node if (after != node) rest = rest.replaceChild(0, after.type.create(after.attrs)) 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) } // :: (number, ?number, ?[?{type: NodeType, attrs: ?Object}]) → 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. Transform.prototype.split = function(pos, depth = 1, typesAfter) { let $pos = this.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)) } return this.step(new ReplaceStep(pos, pos, new Slice(before.append(after), depth, depth), true)) } // :: (Node, number) → bool // Test whether the blocks before and after a given position can be // joined. export function canJoin(doc, pos) { let $pos = doc.resolve(pos), index = $pos.index() return joinable($pos.nodeBefore, $pos.nodeAfter) && $pos.parent.canReplace(index, index + 1) } function joinable(a, b) { return a && b && !a.isLeaf && a.canAppend(b) } // :: (Node, number, ?number) → ?number // 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, pos, 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) } } // :: (number, ?number) → this // Join the blocks around the given position. If depth is 2, their // last and first siblings are also joined, and so on. Transform.prototype.join = function(pos, depth = 1) { let step = new ReplaceStep(pos - depth, pos + depth, Slice.empty, true) return this.step(step) } // :: (Node, number, NodeType) → ?number // 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, pos, nodeType) { 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 } } // :: (Node, number, Slice) → ?number // 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, pos, slice) { 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) if (pass == 1 ? $pos.node(d).canReplace(insertPos, insertPos, content) : $pos.node(d).contentMatchAt(insertPos).findWrapping(content.firstChild.type)) return bias == 0 ? $pos.pos : bias < 0 ? $pos.before(d + 1) : $pos.after(d + 1) } } return null } prosemirror-transform-1.2.8/src/transform.js000066400000000000000000000037511371446663100212770ustar00rootroot00000000000000import {Mapping} from "./map" export function TransformError(message) { let err = Error.call(this, message) err.__proto__ = TransformError.prototype return err } 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 { // :: (Node) // Create a transform that starts with the given document. constructor(doc) { // :: Node // The current document (the result of applying the steps in the // transform). this.doc = doc // :: [Step] // The steps in this transform. this.steps = [] // :: [Node] // The documents before each of the steps. this.docs = [] // :: Mapping // A mapping with the maps for each of the steps in this transform. this.mapping = new Mapping } // :: Node The starting document. get before() { return this.docs.length ? this.docs[0] : this.doc } // :: (step: Step) → this // Apply a new step in this transform, saving the result. Throws an // error when the step fails. step(object) { let result = this.maybeStep(object) if (result.failed) throw new TransformError(result.failed) return this } // :: (Step) → StepResult // Try to apply a step in this transformation, ignoring it if it // fails. Returns the step result. maybeStep(step) { let result = step.apply(this.doc) if (!result.failed) this.addStep(step, result.doc) return result } // :: bool // True when the document has been changed (when there are any // steps). get docChanged() { return this.steps.length > 0 } addStep(step, doc) { this.docs.push(this.doc) this.steps.push(step) this.mapping.appendMap(step.getMap()) this.doc = doc } } prosemirror-transform-1.2.8/test/000077500000000000000000000000001371446663100171105ustar00rootroot00000000000000prosemirror-transform-1.2.8/test/test-mapping.js000066400000000000000000000027441371446663100220650ustar00rootroot00000000000000const ist = require("ist") const {Mapping, StepMap} = require("..") function testMapping(mapping, ...cases) { let inverted = mapping.invert() for (let i = 0; i < cases.length; i++) { let [from, to, bias = 1, lossy] = cases[i] ist(mapping.map(from, bias), to) if (!lossy) ist(inverted.map(to, bias), from) } } function mk(...args) { let mapping = new Mapping args.forEach(arg => { if (Array.isArray(arg)) mapping.appendMap(new StepMap(arg)) else for (let from in arg) mapping.setMirror(from, arg[from]) }) return mapping } describe("Mapping", () => { it("can map through a single insertion", () => { testMapping(mk([2, 0, 4]), [0, 0], [2, 6], [2, 2, -1], [3, 7]) }) it("can map through a single deletion", () => { testMapping(mk([2, 4, 0]), [0, 0], [2, 2, -1], [3, 2, 1, true], [6, 2, 1], [6, 2, -1, true], [7, 3]) }) it("can map through a single replace", () => { testMapping(mk([2, 4, 4]), [0, 0], [2, 2, 1], [4, 6, 1, true], [4, 2, -1, true], [6, 6, -1], [8, 8]) }) it("can map through a mirrorred delete-insert", () => { testMapping(mk([2, 4, 0], [2, 0, 4], {0: 1}), [0, 0], [2, 2], [4, 4], [6, 6], [7, 7]) }) it("cap map through a mirrorred insert-delete", () => { testMapping(mk([2, 0, 4], [2, 4, 0], {0: 1}), [0, 0], [2, 2], [3, 3]) }) it("can map through an delete-insert with an insert in between", () => { testMapping(mk([2, 4, 0], [1, 0, 1], [3, 0, 4], {0: 2}), [0, 0], [1, 2], [4, 5], [6, 7], [7, 8]) }) }) prosemirror-transform-1.2.8/test/test-step.js000066400000000000000000000046541371446663100214070ustar00rootroot00000000000000const {Slice, Fragment} = require("prosemirror-model") const {ReplaceStep, AddMarkStep, RemoveMarkStep} = require("..") const ist = require("ist") const {eq, schema, doc, p} = require("prosemirror-test-builder") const testDoc = doc(p("foobar")) function mkStep(from, to, val) { if (val == "+em") return new AddMarkStep(from, to, schema.marks.em.create()) else if (val == "-em") return new RemoveMarkStep(from, to, schema.marks.em.create()) else return new ReplaceStep(from, to, val == null ? Slice.empty : new Slice(Fragment.from(schema.text(val)), 0, 0)) } describe("Step", () => { describe("merge", () => { function yes(from1, to1, val1, from2, to2, val2) { return () => { let step1 = mkStep(from1, to1, val1), step2 = mkStep(from2, to2, val2) let merged = step1.merge(step2) ist(merged) ist(merged.apply(testDoc).doc, step2.apply(step1.apply(testDoc).doc).doc, eq) } } function no(from1, to1, val1, from2, to2, val2) { return () => { let step1 = mkStep(from1, to1, val1), step2 = mkStep(from2, to2, val2) ist(!step1.merge(step2)) } } it("merges typing changes", yes(2, 2, "a", 3, 3, "b")) it("merges inverse typing", yes(2, 2, "a", 2, 2, "b")) it("doesn't merge separated typing", no(2, 2, "a", 4, 4, "b")) it("doesn't merge inverted separated typing", no(3, 3, "a", 2, 2, "b")) it("merges adjacent backspaces", yes(3, 4, null, 2, 3, null)) it("merges adjacent deletes", yes(2, 3, null, 2, 3, null)) it("doesn't merge separate backspaces", no(1, 2, null, 2, 3, null)) it("merges backspace and type", yes(2, 3, null, 2, 2, "x")) it("merges longer adjacent inserts", yes(2, 2, "quux", 6, 6, "baz")) it("merges inverted longer inserts", yes(2, 2, "quux", 2, 2, "baz")) it("merges longer deletes", yes(2, 5, null, 2, 4, null)) it("merges inverted longer deletes", yes(4, 6, null, 2, 4, null)) it("merges overwrites", yes(3, 4, "x", 4, 5, "y")) it("merges adding adjacent styles", yes(1, 2, "+em", 2, 4, "+em")) it("merges adding overlapping styles", yes(1, 3, "+em", 2, 4, "+em")) it("doesn't merge separate styles", no(1, 2, "+em", 3, 4, "+em")) it("merges removing adjacent styles", yes(1, 2, "-em", 2, 4, "-em")) it("merges removing overlapping styles", yes(1, 3, "-em", 2, 4, "-em")) it("doesn't merge removing separate styles", no(1, 2, "-em", 3, 4, "-em")) }) }) prosemirror-transform-1.2.8/test/test-structure.js000066400000000000000000000147531371446663100224750ustar00rootroot00000000000000const {Schema, Slice} = require("prosemirror-model") const {canSplit, liftTarget, findWrapping, Transform} = require("..") const {eq, schema: baseSchema} = require("prosemirror-test-builder") const ist = require("ist") const schema = new Schema({ nodes: { doc: {content: "head? block* sect* closing?"}, para: {content: "text*", group: "block"}, head: {content: "text*", marks: ""}, figure: {content: "caption figureimage", group: "block"}, quote: {content: "block+", group: "block"}, figureimage: {}, caption: {content: "text*", marks: ""}, sect: {content: "head block* sect*"}, closing: {content: "text*"}, text: baseSchema.spec.nodes.get("text"), fixed: {content: "head para closing", group: "block"} }, marks: { em: {} } }) function n(name, ...content) { return schema.nodes[name].create(null, content) } function t(str, em) { return schema.text(str, em ? [schema.mark("em")] : null) } const doc = n("doc", // 0 n("head", t("Head")), // 6 n("para", t("Intro")), // 13 n("sect", // 14 n("head", t("Section head")), // 28 n("sect", // 29 n("head", t("Subsection head")), // 46 n("para", t("Subtext")), // 55 n("figure", // 56 n("caption", t("Figure caption")), // 72 n("figureimage")), // 74 n("quote", n("para", t("!"))))), // 81 n("sect", // 82 n("head", t("S2")), // 86 n("para", t("Yes"))), // 92 n("closing", t("fin"))) // 97 function range(pos, end) { return doc.resolve(pos).blockRange(end == null ? undefined : doc.resolve(end)) } describe("canSplit", () => { function yes(pos, depth, after) { return () => ist(canSplit(doc, pos, depth, after && [{type: schema.nodes[after]}])) } function no(pos, depth, after) { return () => ist(!canSplit(doc, pos, depth, after && [{type: schema.nodes[after]}])) } it("can't at start", no(0)) it("can't in head", no(3)) it("can by making head a para", yes(3, 1, "para")) it("can't on top level", no(6)) it("can in regular para", yes(8)) it("can't at start of section", no(14)) it("can't in section head", no(17)) it("can if also splitting the section", yes(17, 2)) it("can if making the remaining head a para", yes(18, 1, "para")) it("can't after the section head", no(46)) it("can in the first section para", yes(48)) it("can't in the figure caption", no(60)) it("can't if it also splits the figure", no(62, 2)) it("can't after the figure caption", no(72)) it("can in the first para in a quote", yes(76)) it("can if it also splits the quote", yes(77, 2)) it("can't at the end of the document", no(97)) it("doesn't return true when the split-off content doesn't fit in the given node type", () => { let s = new Schema({nodes: schema.spec.nodes.addBefore("heading", "title", {content: "text*"}) .addToEnd("chapter", {content: "title scene+"}) .addToEnd("scene", {content: "para+"}) .update("doc", {content: "chapter+"})}) ist(!canSplit(s.node("doc", null, s.node("chapter", null, [ s.node("title", null, s.text("title")), s.node("scene", null, s.node("para", null, s.text("scene"))) ])), 4, 1, [{type: s.nodes.scene}])) }) }) describe("liftTarget", () => { function yes(pos) { return () => { let r = range(pos); ist(r && liftTarget(r)) } } function no(pos) { return () => { let r = range(pos); ist(!(r && liftTarget(r))) } } it("can't at the start of the doc", no(0)) it("can't in the heading", no(3)) it("can't in a subsection para", no(52)) it("can't in a figure caption", no(70)) it("can from a quote", yes(76)) it("can't in a section head", no(86)) }) describe("findWrapping", () => { function yes(pos, end, type) { return () => { let r = range(pos, end); ist(findWrapping(r, schema.nodes[type])) } } function no(pos, end, type) { return () => { let r = range(pos, end); ist(!findWrapping(r, schema.nodes[type])) } } it("can wrap the whole doc in a section", yes(0, 92, "sect")) it("can't wrap a head before a para in a section", no(4, 4, "sect")) it("can wrap a top paragraph in a quote", yes(8, 8, "quote")) it("can't wrap a section head in a quote", no(18, 18, "quote")) it("can wrap a figure in a quote", yes(55, 74, "quote")) it("can't wrap a head in a figure", no(90, 90, "figure")) }) describe("Transform", () => { describe("replace", () => { function repl(doc, from, to, content, openStart, openEnd, result) { return () => { let slice = content ? new Slice(content.content, openStart, openEnd) : Slice.empty let tr = new Transform(doc).replace(from, to, slice) ist(tr.doc, result, eq) } } it("automatically adds a heading to a section", repl(n("doc", n("sect", n("head", t("foo")), n("para", t("bar")))), 6, 6, n("doc", n("sect"), n("sect")), 1, 1, n("doc", n("sect", n("head", t("foo"))), n("sect", n("head"), n("para", t("bar")))))) it("suppresses impossible inputs", repl(n("doc", n("para", t("a")), n("para", t("b"))), 3, 3, n("doc", n("closing", t("."))), 0, 0, n("doc", n("para", t("a")), n("para", t("b"))))) it("adds necessary nodes to the left", repl(n("doc", n("sect", n("head", t("foo")), n("para", t("bar")))), 1, 3, n("doc", n("sect"), n("sect", n("head", t("hi")))), 1, 2, n("doc", n("sect", n("head")), n("sect", n("head", t("hioo")), n("para", t("bar")))))) it("adds a caption to a figure", repl(n("doc"), 0, 0, n("doc", n("figure", n("figureimage"))), 1, 0, n("doc", n("figure", n("caption"), n("figureimage"))))) it("adds an image to a figure", repl(n("doc"), 0, 0, n("doc", n("figure", n("caption"))), 0, 1, n("doc", n("figure", n("caption"), n("figureimage"))))) it("can join figures", repl(n("doc", n("figure", n("caption"), n("figureimage")), n("figure", n("caption"), n("figureimage"))), 3, 8, null, 0, 0, n("doc", n("figure", n("caption"), n("figureimage"))))) it("adds necessary nodes to a parent node", repl(n("doc", n("sect", n("head"), n("figure", n("caption"), n("figureimage")))), 7, 9, n("doc", n("para", t("hi"))), 0, 0, n("doc", n("sect", n("head"), n("figure", n("caption"), n("figureimage")), n("para", t("hi")))))) }) }) prosemirror-transform-1.2.8/test/test-trans.js000066400000000000000000001002501371446663100215500ustar00rootroot00000000000000const {schema, doc, blockquote, pre, h1, h2, p, li, ol, ul, em, strong, code, a, img, br, hr, eq, builders} = require("prosemirror-test-builder") const {testTransform} = require("./trans") const {Transform, liftTarget, findWrapping} = require("..") const {Slice, Fragment, Schema} = require("prosemirror-model") const ist = require("ist") describe("Transform", () => { describe("addMark", () => { function add(doc, mark, expect) { testTransform(new Transform(doc).addMark(doc.tag.a, doc.tag.b, mark), expect) } it("should add a mark", () => add(doc(p("hello there!")), schema.mark("strong"), doc(p("hello ", strong("there"), "!")))) it("should only add a mark once", () => add(doc(p("hello ", strong("there"), "!")), schema.mark("strong"), doc(p("hello ", strong("there!"))))) it("should join overlapping marks", () => add(doc(p("one two ", em("three four"))), schema.mark("strong"), doc(p("one ", strong("two ", em("three")), em(" four"))))) it("should overwrite marks with different attributes", () => add(doc(p("this is a ", a("link"))), schema.mark("link", {href: "bar"}), doc(p("this is a ", a({href: "bar"}, "link"))))) it("can add a mark in a nested node", () => add(doc(p("before"), blockquote(p("the variable is called i")), p("after")), schema.mark("code"), doc(p("before"), blockquote(p("the variable is called ", code("i"))), p("after")))) it("can add a mark across blocks", () => add(doc(p("hi this"), blockquote(p("is")), p("a document"), p("!")), schema.mark("em"), doc(p("hi ", em("this")), blockquote(p(em("is"))), p(em("a docu"), "ment"), p("!")))) it("does not remove non-excluded marks of the same type", () => { let schema = new Schema({ nodes: {doc: {content: "text*"}, text: {}}, marks: {comment: {excludes: "", attrs: {id: {}}}} }) let tr = new Transform(schema.node("doc", null, schema.text("hi", [schema.mark("comment", {id: 10})]))) tr.addMark(0, 2, schema.mark("comment", {id: 20})) ist(tr.doc.firstChild.marks.length, 2) }) it("can remove multiple excluded marks", () => { let schema = new Schema({ nodes: {doc: {content: "text*"}, text: {}}, marks: {big: {excludes: "small1 small2"}, small1: {}, small2: {}} }) let tr = new Transform(schema.node("doc", null, schema.text("hi", [schema.mark("small1"), schema.mark("small2")]))) ist(tr.doc.firstChild.marks.length, 2) tr.addMark(0, 2, schema.mark("big")) ist(tr.doc.firstChild.marks.length, 1) ist(tr.doc.firstChild.marks[0].type.name, "big") }) }) describe("removeMark", () => { function rem(doc, mark, expect) { testTransform(new Transform(doc).removeMark(doc.tag.a, doc.tag.b, mark), expect) } it("can cut a gap", () => rem(doc(p(em("hello world!"))), schema.mark("em"), doc(p(em("hello "), "world", em("!"))))) it("doesn't do anything when there's no mark", () => rem(doc(p(em("hello"), " world!")), schema.mark("em"), doc(p(em("hello"), " world!")))) it("can remove marks from nested nodes", () => rem(doc(p(em("one ", strong("two"), " three"))), schema.mark("strong"), doc(p(em("one two three"))))) it("can remove a link", () => rem(doc(p("hello ", a("link"))), schema.mark("link", {href: "foo"}), doc(p("hello link")))) it("doesn't remove a non-matching link", () => rem(doc(p("hello ", a("link"))), schema.mark("link", {href: "bar"}), doc(p("hello ", a("link"))))) it("can remove across blocks", () => rem(doc(blockquote(p(em("much em")), p(em("here too"))), p("between", em("...")), p(em("end"))), schema.mark("em"), doc(blockquote(p(em("much "), "em"), p("here too")), p("between..."), p("end")))) it("can remove everything", () => rem(doc(p("hello, ", em("this is ", strong("much"), " ", a("markup")))), null, doc(p("hello, this is much markup")))) }) describe("insert", () => { function ins(doc, nodes, expect) { testTransform(new Transform(doc).insert(doc.tag.a, nodes), expect) } it("can insert a break", () => ins(doc(p("hellothere")), schema.node("hard_break"), doc(p("hello", br, "there")))) it("can insert an empty paragraph at the top", () => ins(doc(p("one"), "", p("two<2>")), schema.node("paragraph"), doc(p("one"), p(), "", p("two<2>")))) it("can insert two block nodes", () => ins(doc(p("one"), "", p("two<2>")), [schema.node("paragraph", null, [schema.text("hi")]), schema.node("horizontal_rule")], doc(p("one"), p("hi"), hr, "", p("two<2>")))) it("can insert at the end of a blockquote", () => ins(doc(blockquote(p("hey"), ""), p("after")), schema.node("paragraph"), doc(blockquote(p("hey"), p()), p("after")))) it("can insert at the start of a blockquote", () => ins(doc(blockquote("", p("he<1>y")), p("after<2>")), schema.node("paragraph"), doc(blockquote(p(), "", p("he<1>y")), p("after<2>")))) it("will wrap a node with the suitable parent", () => ins(doc(p("foobar")), schema.nodes.list_item.createAndFill(), doc(p("foo"), ol(li(p())), p("bar")))) }) describe("delete", () => { function del(doc, expect) { testTransform(new Transform(doc).delete(doc.tag.a, doc.tag.b), expect) } it("can delete a word", () => del(doc(p("<1>one"), "", p("tw<2>o"), "", p("<3>three")), doc(p("<1>one"), "<2>", p("<3>three")))) it("preserves content constraints", () => del(doc(blockquote("", p("hi"), ""), p("x")), doc(blockquote(p()), p("x")))) it("preserves positions after the range", () => del(doc(blockquote(p("a"), "", p("b"), ""), p("c<1>")), doc(blockquote(p("a")), p("c<1>")))) it("doesn't join incompatible nodes", () => del(doc(pre("foo"), p("bar", img)), doc(pre("fo"), p("ar", img)))) it("doesn't join when marks are incompatible", () => del(doc(pre("foo"), p(em("bar"))), doc(pre("fo"), p(em("ar"))))) }) describe("join", () => { function join(doc, expect) { testTransform(new Transform(doc).join(doc.tag.a), expect) } it("can join blocks", () => join(doc(blockquote(p("a")), "", blockquote(p("b")), p("after")), doc(blockquote(p("a"), "", p("b")), p("after")))) it("can join compatible blocks", () => join(doc(h1("foo"), "", p("bar")), doc(h1("foobar")))) it("can join nested blocks", () => join(doc(blockquote(blockquote(p("a"), p("b")), "", blockquote(p("c"), p("d")))), doc(blockquote(blockquote(p("a"), p("b"), "", p("c"), p("d")))))) it("can join lists", () => join(doc(ol(li(p("one")), li(p("two"))), "", ol(li(p("three")))), doc(ol(li(p("one")), li(p("two")), "", li(p("three")))))) it("can join list items", () => join(doc(ol(li(p("one")), li(p("two")), "", li(p("three")))), doc(ol(li(p("one")), li(p("two"), "", p("three")))))) it("can join textblocks", () => join(doc(p("foo"), "", p("bar")), doc(p("foobar")))) }) describe("split", () => { function split(doc, expect, ...args) { if (expect == "fail") ist.throws(() => new Transform(doc).split(doc.tag.a, ...args)) else testTransform(new Transform(doc).split(doc.tag.a, ...args), expect) } it("can split a textblock", () => split(doc(p("foobar")), doc(p("foo"), p("bar")))) it("correctly maps positions", () => split(doc(p("<1>a"), p("<2>foobar<3>"), p("<4>b")), doc(p("<1>a"), p("<2>foo"), p("bar<3>"), p("<4>b")))) it("can split two deep", () => split(doc(blockquote(blockquote(p("foobar"))), p("after<1>")), doc(blockquote(blockquote(p("foo")), blockquote(p("bar"))), p("after<1>")), 2)) it("can split three deep", () => split(doc(blockquote(blockquote(p("foobar"))), p("after<1>")), doc(blockquote(blockquote(p("foo"))), blockquote(blockquote(p("bar"))), p("after<1>")), 3)) it("can split at end", () => split(doc(blockquote(p("hi"))), doc(blockquote(p("hi"), p(""))))) it("can split at start", () => split(doc(blockquote(p("hi"))), doc(blockquote(p(), p("hi"))))) it("can split inside a list item", () => split(doc(ol(li(p("one<1>")), li(p("twothree")), li(p("four<2>")))), doc(ol(li(p("one<1>")), li(p("two"), p("three")), li(p("four<2>")))))) it("can split a list item", () => split(doc(ol(li(p("one<1>")), li(p("twothree")), li(p("four<2>")))), doc(ol(li(p("one<1>")), li(p("two")), li(p("three")), li(p("four<2>")))), 2)) it("respects the type param", () => split(doc(h1("hello!")), doc(h1("hell"), p("o!")), undefined, [{type: schema.nodes.paragraph}])) it("preserves content constraints before", () => split(doc(blockquote("", p("x"))), "fail")) it("preserves content constraints after", () => split(doc(blockquote(p("x"), "")), "fail")) }) describe("lift", () => { function lift(doc, expect) { let range = doc.resolve(doc.tag.a).blockRange(doc.resolve(doc.tag.b || doc.tag.a)) testTransform(new Transform(doc).lift(range, liftTarget(range)), expect) } it("can lift a block out of the middle of its parent", () => lift(doc(blockquote(p("one"), p("two"), p("three"))), doc(blockquote(p("one")), p("two"), blockquote(p("three"))))) it("can lift a block from the start of its parent", () => lift(doc(blockquote(p("two"), p("three"))), doc(p("two"), blockquote(p("three"))))) it("can lift a block from the end of its parent", () => lift(doc(blockquote(p("one"), p("two"))), doc(blockquote(p("one")), p("two")))) it("can lift a single child", () => lift(doc(blockquote(p("two"))), doc(p("two")))) it("can lift multiple blocks", () => lift(doc(blockquote(blockquote(p("one"), p("two")), p("three"))), doc(blockquote(p("one"), p("two"), p("three"))))) it("finds a valid range from a lopsided selection", () => lift(doc(p("start"), blockquote(blockquote(p("a"), p("b")), p("c"))), doc(p("start"), blockquote(p("a"), p("b")), p("c")))) it("can lift from a nested node", () => lift(doc(blockquote(blockquote(p("<1>one"), p("two"), p("<3>three"), p("four"), p("<5>five")))), doc(blockquote(blockquote(p("<1>one")), p("two"), p("<3>three"), p("four"), blockquote(p("<5>five")))))) it("can lift from a list", () => lift(doc(ul(li(p("one")), li(p("two")), li(p("three")))), doc(ul(li(p("one"))), p("two"), ul(li(p("three")))))) it("can lift from the end of a list", () => lift(doc(ul(li(p("a")), li(p("b")), "<1>")), doc(ul(li(p("a"))), p("b"), "<1>"))) }) describe("wrap", () => { function wrap(doc, expect, type, attrs) { let range = doc.resolve(doc.tag.a).blockRange(doc.resolve(doc.tag.b || doc.tag.a)) testTransform(new Transform(doc).wrap(range, findWrapping(range, schema.nodes[type], attrs)), expect) } it("can wrap in a blockquote", () => wrap(doc(p("one"), p("two"), p("three")), doc(p("one"), blockquote(p("two")), p("three")), "blockquote")) it("can wrap two paragraphs", () => wrap(doc(p("one<1>"), p("two"), p("three"), p("four<4>")), doc(p("one<1>"), blockquote(p("two"), p("three")), p("four<4>")), "blockquote")) it("can wrap in a list", () => wrap(doc(p("one"), p("two")), doc(ol(li(p("one"), p("two")))), "ordered_list")) it("can wrap in a nested list", () => wrap(doc(ol(li(p("<1>one")), li(p("..."), p("two"), p("three")), li(p("<4>four")))), doc(ol(li(p("<1>one")), li(p("..."), ol(li(p("two"), p("three")))), li(p("<4>four")))), "ordered_list")) it("includes half-covered parent nodes", () => wrap(doc(blockquote(p("<1>one"), p("two")), p("three")), doc(blockquote(blockquote(p("<1>one"), p("two")), p("three"))), "blockquote")) }) describe("setBlockType", () => { function type(doc, expect, nodeType, attrs) { testTransform(new Transform(doc).setBlockType(doc.tag.a, doc.tag.b || doc.tag.a, schema.nodes[nodeType], attrs), expect) } it("can change a single textblock", () => type(doc(p("am i")), doc(h2("am i")), "heading", {level: 2})) it("can change multiple blocks", () => type(doc(h1("hello"), p("there"), p("you"), p("end")), doc(pre("hello"), pre("there"), pre("you"), p("end")), "code_block")) it("can change a wrapped block", () => type(doc(blockquote(p("one"), p("two"))), doc(blockquote(h1("one"), h1("two"))), "heading", {level: 1})) it("clears markup when necessary", () => type(doc(p("hello ", em("world"))), doc(pre("hello world")), "code_block")) it("only clears markup when needed", () => type(doc(p("hello ", em("world"))), doc(h1("hello ", em("world"))), "heading", {level: 1})) it("works after another step", () => { let d = doc(p("foobar"), p("baz")) let tr = new Transform(d).delete(d.tag.x, d.tag.y), pos = tr.mapping.map(d.tag.a) tr.setBlockType(pos, pos, schema.nodes.heading, {level: 1}) testTransform(tr, doc(p("far"), h1("baz"))) }) it("skips nodes that can't be changed due to constraints", () => type(doc(p("hello", img), p("okay"), ul(li(p("foo")))), doc(pre("hello"), pre("okay"), ul(li(p("foo")))), "code_block")) }) describe("setNodeMarkup", () => { function markup(doc, expect, type, attrs) { testTransform(new Transform(doc).setNodeMarkup(doc.tag.a, schema.nodes[type], attrs), expect) } it("can change a textblock", () => markup(doc("", p("foo")), doc(h1("foo")), "heading", {level: 1})) it("can change an inline node", () => markup(doc(p("foo", img, "bar")), doc(p("foo", img({src: "bar", alt: "y"}), "bar")), "image", {src: "bar", alt: "y"})) }) describe("replace", () => { function repl(doc, source, expect) { let slice = !source ? Slice.empty : source instanceof Slice ? source : source.slice(source.tag.a, source.tag.b) testTransform(new Transform(doc).replace(doc.tag.a, doc.tag.b || doc.tag.a, slice), expect) } it("can delete text", () => repl(doc(p("hello you")), null, doc(p("hellou")))) it("can join blocks", () => repl(doc(p("hello"), p("you")), null, doc(p("hellou")))) it("can delete right-leaning lopsided regions", () => repl(doc(blockquote(p("abc")), "", p("def")), null, doc(blockquote(p("ab")), "", p("def")))) it("can delete left-leaning lopsided regions", () => repl(doc(p("abc"), "", blockquote(p("def"))), null, doc(p("abc"), "", blockquote(p("ef"))))) it("can overwrite text", () => repl(doc(p("hello you")), doc(p("i k")), doc(p("helli kou")))) it("can insert text", () => repl(doc(p("hello")), doc(p("i k")), doc(p("helli ko")))) it("can add a textblock", () => repl(doc(p("helloyou")), doc("", p("there"), ""), doc(p("hello"), p("there"), p("you")))) it("can insert while joining textblocks", () => repl(doc(h1("hello"), p("arg!")), doc(p("123")), doc(h1("he2!")))) it("will match open list items", () => repl(doc(ol(li(p("one")), li(p("three")))), doc(ol(li(p("half")), li(p("two")), "")), doc(ol(li(p("onehalf")), li(p("two")), li(p("three")))))) it("merges blocks across deleted content", () => repl(doc(p("a"), p("b"), p("c")), null, doc(p("ac")))) it("can merge text down from nested nodes", () => repl(doc(h1("woah"), blockquote(p("ahha"))), null, doc(h1("woha")))) it("can merge text up into nested nodes", () => repl(doc(blockquote(p("foobar")), p("middle"), h1("quuxbaz")), null, doc(blockquote(p("foobaz"))))) it("will join multiple levels when possible", () => repl(doc(blockquote(ul(li(p("a")), li(p("b")), li(p("c")), li(p("d")), li(p("e"))))), null, doc(blockquote(ul(li(p("a")), li(p("bd")), li(p("e"))))))) it("can replace a piece of text", () => repl(doc(p("hello world")), doc(p(" big")), doc(p("hello big world")))) it("respects open empty nodes at the edges", () => repl(doc(p("onetwo")), doc(p("a"), p("hello"), p("b")), doc(p("one"), p("hello"), p("two")))) it("can completely overwrite a paragraph", () => repl(doc(p("one"), p("two"), p("three")), doc(p("a"), p("TWO"), p("b")), doc(p("one"), p("TWO"), p("three")))) it("joins marks", () => repl(doc(p("foo ", em("barbaz"), " quux")), doc(p("foo ", em("xyzzy"), " foo")), doc(p("foo ", em("barzzy"), " foo quux")))) it("can replace text with a break", () => repl(doc(p("foobbbar")), doc(p("", br, "")), doc(p("foo", br, "bar")))) it("can join different blocks", () => repl(doc(h1("hello"), p("bye")), null, doc(h1("helle")))) it("can restore a list parent", () => repl(doc(h1("hello"), ""), doc(ol(li(p("one")), li(p("two")))), doc(h1("helle"), ol(li(p("tw")))))) it("can restore a list parent and join text after it", () => repl(doc(h1("hello"), p("you")), doc(ol(li(p("one")), li(p("two")))), doc(h1("helle"), ol(li(p("twu")))))) it("can insert into an empty block", () => repl(doc(p("a"), p(""), p("b")), doc(p("xyz")), doc(p("a"), p("y"), p("b")))) it("doesn't change the nesting of blocks after the selection", () => repl(doc(p("one"), p("two"), p("three")), doc(p("outside"), blockquote(p("inside"))), doc(p("one"), blockquote(p("inside")), p("two"), p("three")))) it("can close a parent node", () => repl(doc(blockquote(p("bc"), p("de"), p("f"))), doc(blockquote(p("xy")), p("after"), ""), doc(blockquote(p("by")), p("after"), blockquote(p("e"), p("f"))))) it("accepts lopsided regions", () => repl(doc(blockquote(p("bc"), p("de"), p("f"))), doc(blockquote(p("xy")), p("z")), doc(blockquote(p("by")), p("ze"), blockquote(p("f"))))) it("can close nested parent nodes", () => repl(doc(blockquote(blockquote(p("one"), p("two"), p("three<3>"), p("four<4>")))), doc(ol(li(p("helloworld")), li(p("bye"))), p("next")), doc(blockquote(blockquote(p("one"), p("twworld"), ol(li(p("bye"))), p("nehree<3>"), p("four<4>")))))) it("will close open nodes to the right", () => repl(doc(p("x"), ""), doc("", ul(li(p("a")), li("", p("b")))), doc(p("x"), ul(li(p("a")), li(p())), ""))) it("can delete the whole document", () => repl(doc("", h1("hi"), p("you"), ""), null, doc(p()))) it("preserves an empty parent to the left", () => repl(doc(blockquote("", p("hi")), p("bx")), doc(p("hi")), doc(blockquote(p("hix"))))) it("drops an empty parent to the right", () => repl(doc(p("xhi"), blockquote(p("yy"), ""), p("c")), doc(p("hi")), doc(p("xhi"), p("c")))) it("drops an empty node at the start of the slice", () => repl(doc(p("x")), doc(blockquote(p("hi"), ""), p("b")), doc(p(), p("bx")))) it("drops an empty node at the end of the slice", () => repl(doc(p("x")), doc(p("b"), blockquote("", p("hi"))), doc(p(), blockquote(p()), p("x")))) it("does nothing when given an unfittable slice", () => repl(p("x"), new Slice(Fragment.from([blockquote(), hr]), 0, 0), p("x"))) it("doesn't drop content when things only fit at the top level", () => repl(doc(p("foo"), "", p("bar")), ol(li(p("a")), li(p("b"))), doc(p("foo"), p("a"), ol(li(p("b")))))) it("preserves openEnd when top isn't placed", () => repl(doc(ul(li(p("abcd")), li(p("efgh")))), doc(ul(li(p("ABCD")), li(p("EFGH")))).slice(5, 13, true), doc(ul(li(p("abCD")), li(p("EFgh")))))) it("will auto-close a list item when it fits in a list", () => repl(doc(ul(li(p("foo")), "", li(p("bar")))), ul(li(p("abc")), li(p("def"))), doc(ul(li(p("foo")), li(p("bc")), li(p("de")), li(p("bar")))))) it("finds the proper openEnd value when unwrapping a deep slice", () => repl(doc("", p(), ""), doc(blockquote(blockquote(blockquote(p("hi"))))).slice(3, 6, true), doc(p("hi")))) // A schema that allows marks on top-level block nodes let ms = new Schema({ nodes: schema.spec.nodes.update("doc", Object.assign({}, schema.spec.nodes.get("doc"), {marks: "_"})), marks: schema.spec.marks }) it("preserves marks on block nodes", () => { let tr = new Transform(ms.node("doc", null, [ ms.node("paragraph", null, [ms.text("hey")], [ms.mark("em")]), ms.node("paragraph", null, [ms.text("ok")], [ms.mark("strong")]) ])) tr.replace(2, 7, tr.doc.slice(2, 7)) ist(tr.doc, tr.before, eq) }) it("preserves marks on open slice block nodes", () => { let tr = new Transform(ms.node("doc", null, [ms.node("paragraph", null, [ms.text("a")])])) tr.replace(3, 3, ms.node("doc", null, [ ms.node("paragraph", null, [ms.text("b")], [ms.mark("em")]) ]).slice(1, 3)) ist(tr.doc.childCount, 2) ist(tr.doc.lastChild.marks.length, 1) }) // A schema that enforces a heading and a body at the top level let hbSchema = new Schema({ nodes: schema.spec.nodes.append({ doc: Object.assign({}, schema.spec.nodes.get("doc"), {content: "heading body"}), body: {content: "block+"} }) }) let hb = builders(hbSchema, { p: {nodeType: "paragraph"}, b: {nodeType: "body"}, h: {nodeType: "heading", level: 1}, }) it("can unwrap a paragraph when replacing into a strict schema", () => { let tr = new Transform(hb.doc(hb.h("Head"), hb.b(hb.p("Content")))) tr.replace(0, tr.doc.content.size, tr.doc.slice(7, 16)) ist(tr.doc, hb.doc(hb.h("Content"), hb.b(hb.p())), eq) }) it("can unwrap a body after a placed node", () => { let tr = new Transform(hb.doc(hb.h("Head"), hb.b(hb.p("Content")))) tr.replace(7, 7, tr.doc.slice(0, tr.doc.content.size)) ist(tr.doc, hb.doc(hb.h("Head"), hb.b(hb.h("Head"), hb.p("Content"), hb.p("Content"))), eq) }) it("can wrap a paragraph in a body, even when it's not the first node", () => { let tr = new Transform(hb.doc(hb.h("Head"), hb.b(hb.p("One"), hb.p("Two")))) tr.replace(0, tr.doc.content.size, tr.doc.slice(8, 16)) ist(tr.doc, hb.doc(hb.h("One"), hb.b(hb.p("Two"))), eq) }) it("can split a fragment and place its children in different parents", () => { let tr = new Transform(hb.doc(hb.h("Head"), hb.b(hb.h("One"), hb.p("Two")))) tr.replace(0, tr.doc.content.size, tr.doc.slice(7, 17)) ist(tr.doc, hb.doc(hb.h("One"), hb.b(hb.p("Two"))), eq) }) it("will insert filler nodes before a node when necessary", () => { let tr = new Transform(hb.doc(hb.h("Head"), hb.b(hb.p("One")))) tr.replace(0, tr.doc.content.size, tr.doc.slice(6, tr.doc.content.size)) ist(tr.doc, hb.doc(hb.h(), hb.b(hb.p("One"))), eq) }) it("doesn't fail when moving text would solve an unsatisfied content constraint", () => { let s = new Schema({ nodes: schema.spec.nodes.append({ title: {content: "text*"}, doc: {content: "title? block*"} }) }) let tr = new Transform(s.node("doc", null, s.node("title", null, s.text("hi")))) tr.replace(1, 1, s.node("bullet_list", null, [ s.node("list_item", null, s.node("paragraph", null, s.text("one"))), s.node("list_item", null, s.node("paragraph", null, s.text("two"))) ]).slice(2, 12)) ist(tr.steps.length, 0, ">") }) it("doesn't fail when pasting a half-open slice with a title and a code block into an empty title", () => { let s = new Schema({ nodes: schema.spec.nodes.append({ title: {content: "text*"}, doc: {content: "title? block*"} }) }) let tr = new Transform(s.node("doc", null, [s.node("title", null, [])])) tr.replace(1, 1, s.node("doc", null, [ s.node("title", null, s.text("title")), s.node("code_block", null, s.text("two")), ]).slice(1)) ist(tr.steps.length, 0, ">") }) it("doesn't fail when pasting a half-open slice with a heading and a code block into an empty title", () => { let s = new Schema({ nodes: schema.spec.nodes.append({ title: {content: "text*"}, doc: {content: "title? block*"} }) }) let tr = new Transform(s.node("doc", null, [s.node("title")])) tr.replace(1, 1, s.node("doc", null, [ s.node("heading", {level: 1}, [s.text("heading")]), s.node("code_block", null, [s.text("code")]), ]).slice(1)) ist(tr.steps.length, 0, ">") }) it("can handle replacing in nodes with fixed content", () => { let s = new Schema({ nodes: { doc: {content: "block+"}, a: {content: "inline*"}, b: {content: "inline*"}, block: {content: "a b"}, text: {group: "inline"} } }) let doc = s.node("doc", null, [ s.node("block", null, [s.node("a", null, [s.text("aa")]), s.node("b", null, [s.text("bb")])]) ]) let from = 3, to = doc.content.size ist(new Transform(doc).replace(from, to, doc.slice(from, to)).doc, doc, eq) }) }) describe("replaceRange", () => { function repl(doc, source, expect) { let slice = !source ? Slice.empty : source instanceof Slice ? source : source.slice(source.tag.a, source.tag.b, true) testTransform(new Transform(doc).replaceRange(doc.tag.a, doc.tag.b || doc.tag.a, slice), expect) } it("replaces inline content", () => repl(doc(p("foobar")), p("xx"), doc(p("fooxxar")))) it("replaces an empty paragraph with a heading", () => repl(doc(p("")), doc(h1("text")), doc(h1("text")))) it("replaces a fully selected paragraph with a heading", () => repl(doc(p("abc")), doc(h1("text")), doc(h1("text")))) it("recreates a list when overwriting a paragraph", () => repl(doc(p("")), doc(ul(li(p("foobar")))), doc(ul(li(p("foobar")))))) it("drops context when it doesn't fit", () => repl(doc(ul(li(p("")), li(p("b")))), doc(h1("h")), doc(ul(li(p("h")), li(p("b")))))) it("can replace a node when endpoints are in different children", () => repl(doc(p("a"), ul(li(p("b")), li(p("c"), blockquote(p("d")))), p("e")), doc(h1("x")), doc(p("a"), h1("x"), p("e")))) it("keeps defining context when inserting at the start of a textblock", () => repl(doc(p("foo")), doc(ul(li(p("one")), li(p("two")))), doc(ul(li(p("one")), li(p("twofoo")))))) it("drops defining context when it matches the parent structure", () => repl(doc(blockquote(p(""))), doc(blockquote(p("one"))), doc(blockquote(p("one"))))) it("closes open nodes at the start", () => repl(doc("", p("abc"), ""), doc(ul(li("")), p("def"), ""), doc(ul(li(p())), p("def")))) }) describe("replaceRangeWith", () => { function repl(doc, node, expect) { testTransform(new Transform(doc).replaceRangeWith(doc.tag.a, doc.tag.b || doc.tag.a, node), expect) } it("can insert an inline node", () => repl(doc(p("foo")), img(), doc(p("fo", img, "o")))) it("can replace content with an inline node", () => repl(doc(p("foo")), img(), doc(p("", img, "o")))) it("can replace a block node with an inline node", () => repl(doc("", blockquote(p("a")), ""), img(), doc(p(img)))) it("can replace a block node with a block node", () => repl(doc("", blockquote(p("a")), ""), hr(), doc(hr))) it("can insert a block quote in the middle of text", () => repl(doc(p("foobar")), hr(), doc(p("foo"), hr, p("bar")))) it("can replace empty parents with a block node", () => repl(doc(blockquote(p(""))), hr(), doc(blockquote(hr)))) it("can move an inserted block forward out of parent nodes", () => repl(doc(h1("foo")), hr(), doc(h1("foo"), hr))) it("can move an inserted block backward out of parent nodes", () => repl(doc(p("a"), blockquote(p("b"))), hr(), doc(p("a"), blockquote(hr, p("b"))))) }) describe("deleteRange", () => { function del(doc, expect) { testTransform(new Transform(doc).deleteRange(doc.tag.a, doc.tag.b || doc.tag.a), expect) } it("deletes the given range", () => del(doc(p("foo"), p("bar")), doc(p("foar")))) it("deletes empty parent nodes", () => del(doc(blockquote(ul(li("", p("foo"), "")), p("x"))), doc(blockquote("", p("x"))))) it("doesn't delete parent nodes that can be empty", () => del(doc(p("foo")), doc(p("")))) it("is okay with deleting empty ranges", () => del(doc(p("")), doc(p("")))) it("will delete a whole covered node even if selection ends are in different nodes", () => del(doc(ul(li(p("foo")), li(p("bar"))), p("hi")), doc(p("hi")))) it("leaves wrapping textblock when deleting all text in it", () => del(doc(p("a"), p("b")), doc(p("a"), p()))) it("expands to cover the whole parent node", () => del(doc(p("a"), blockquote(blockquote(p("foo")), p("bar")), p("b")), doc(p("a"), p("b")))) it("expands to cover the whole document", () => del(doc(h1("foo"), p("bar"), p("baz")), doc(p()))) it("deletes the open token when deleting from start to past end of block", () => del(doc(h1("foo"), p("bar")), doc(p("ar")))) it("doesn't delete the open token when the range end is at end of its own block", () => del(doc(p("one"), h1("two"), blockquote(p("three")), p("four")), doc(p("one"), h1(), p("four")))) }) }) prosemirror-transform-1.2.8/test/trans.js000066400000000000000000000021141371446663100205730ustar00rootroot00000000000000const {Transform, Step, Mapping} = require("..") const {eq} = require("prosemirror-test-builder") const ist = require("ist") function invert(transform) { let out = new Transform(transform.doc) for (let i = transform.steps.length - 1; i >= 0; i--) out.step(transform.steps[i].invert(transform.docs[i])) return out } function testMapping(mapping, pos, newPos) { let mapped = mapping.map(pos, 1) ist(mapped, newPos) let remap = new Mapping(mapping.maps.map(m => m.invert())) for (let i = mapping.maps.length - 1, mapFrom = mapping.maps.length; i >= 0; i--) remap.appendMap(mapping.maps[i], --mapFrom) ist(remap.map(pos, 1), pos) } function testStepJSON(tr) { let newTR = new Transform(tr.before) tr.steps.forEach(step => newTR.step(Step.fromJSON(tr.doc.type.schema, step.toJSON()))) ist(tr.doc, newTR.doc, eq) } function testTransform(tr, expect) { ist(tr.doc, expect, eq) ist(invert(tr).doc, tr.before, eq) testStepJSON(tr) for (let tag in expect.tag) testMapping(tr.mapping, tr.before.tag[tag], expect.tag[tag]) } exports.testTransform = testTransform