Vendorize prosemirror-recreate-transform (#5861)
This commit is contained in:
@@ -9,13 +9,10 @@
|
||||
"^@server/(.*)$": "<rootDir>/server/$1",
|
||||
"^@shared/(.*)$": "<rootDir>/shared/$1"
|
||||
},
|
||||
"setupFiles": [
|
||||
"<rootDir>/__mocks__/console.js",
|
||||
"<rootDir>/server/test/env.ts"
|
||||
],
|
||||
"setupFiles": ["<rootDir>/__mocks__/console.js", "<rootDir>/server/test/env.ts"],
|
||||
"setupFilesAfterEnv": ["<rootDir>/server/test/setup.ts"],
|
||||
"globalSetup": "<rootDir>/server/test/globalSetup.ts",
|
||||
"globalTeardown": "<rootDir>/server/test/globalTeardown.ts",
|
||||
"globalSetup": "<rootDir>/server/test/globalSetup.js",
|
||||
"globalTeardown": "<rootDir>/server/test/globalTeardown.js",
|
||||
"testEnvironment": "node"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"datadog-metrics": "^0.11.0",
|
||||
"date-fns": "^2.30.0",
|
||||
"dd-trace": "^3.33.0",
|
||||
"diff": "^5.1.0",
|
||||
"dotenv": "^4.0.0",
|
||||
"email-providers": "^1.14.0",
|
||||
"emoji-mart": "^5.5.2",
|
||||
@@ -197,6 +198,7 @@
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"refractor": "^3.6.0",
|
||||
"request-filtering-agent": "^1.1.2",
|
||||
"rfc6902": "^5.0.1",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"semver": "^7.5.2",
|
||||
"sequelize": "^6.32.1",
|
||||
@@ -240,6 +242,7 @@
|
||||
"@types/addressparser": "^1.0.1",
|
||||
"@types/body-scroll-lock": "^3.1.0",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/diff": "^5.0.4",
|
||||
"@types/emoji-regex": "^9.2.0",
|
||||
"@types/enzyme": "^3.10.13",
|
||||
"@types/enzyme-adapter-react-16": "^1.0.6",
|
||||
|
||||
@@ -5,7 +5,7 @@ module.exports = async function () {
|
||||
const sql = sequelize.getQueryInterface();
|
||||
const tables = Object.keys(sequelize.models).map((model) => {
|
||||
const n = sequelize.models[model].getTableName();
|
||||
return (sql.queryGenerator as any).quoteTable(
|
||||
return sql.queryGenerator.quoteTable(
|
||||
typeof n === "string" ? n : n.tableName
|
||||
);
|
||||
});
|
||||
201
shared/editor/lib/prosemirror-recreate-transform/LICENSE
Normal file
201
shared/editor/lib/prosemirror-recreate-transform/LICENSE
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
2
shared/editor/lib/prosemirror-recreate-transform/NOTICE
Normal file
2
shared/editor/lib/prosemirror-recreate-transform/NOTICE
Normal file
@@ -0,0 +1,2 @@
|
||||
prosemirror-recreate-steps
|
||||
Copyright 2018 Atypon Systems, LLC.
|
||||
3
shared/editor/lib/prosemirror-recreate-transform/copy.ts
Normal file
3
shared/editor/lib/prosemirror-recreate-transform/copy.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function copy<T>(value: T): T {
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { JSONValue } from "./types";
|
||||
|
||||
/**
|
||||
* get target value from json-pointer (e.g. /content/0/content)
|
||||
* @param {AnyObject} obj object to resolve path into
|
||||
* @param {string} path json-pointer
|
||||
* @return {any} target value
|
||||
*/
|
||||
export function getFromPath(obj: JSONValue, path: string): JSONValue {
|
||||
const pathParts = path.split("/");
|
||||
pathParts.shift(); // remove root-entry
|
||||
while (pathParts.length) {
|
||||
if (typeof obj !== "object") {
|
||||
throw new Error();
|
||||
}
|
||||
const property = pathParts.shift() as string;
|
||||
obj = obj[property];
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Node } from "prosemirror-model";
|
||||
import { ReplaceStep } from "prosemirror-transform";
|
||||
|
||||
export function getReplaceStep(fromDoc: Node, toDoc: Node) {
|
||||
let start = toDoc.content.findDiffStart(fromDoc.content);
|
||||
if (start === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let { a: endA, b: endB } = toDoc.content.findDiffEnd(fromDoc.content) as {
|
||||
a: number;
|
||||
b: number;
|
||||
};
|
||||
const overlap = start - Math.min(endA, endB);
|
||||
if (overlap > 0) {
|
||||
// If there is an overlap, there is some freedom of choice in how to calculate the
|
||||
// start/end boundary. for an inserted/removed slice. We choose the extreme with
|
||||
// the lowest depth value.
|
||||
if (
|
||||
fromDoc.resolve(start - overlap).depth <
|
||||
toDoc.resolve(endA + overlap).depth
|
||||
) {
|
||||
start -= overlap;
|
||||
} else {
|
||||
endA += overlap;
|
||||
endB += overlap;
|
||||
}
|
||||
}
|
||||
|
||||
return new ReplaceStep(start, endB, toDoc.slice(start, endA));
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { recreateTransform, RecreateTransform } from "./recreateTransform";
|
||||
|
||||
export type { Options } from "./recreateTransform";
|
||||
@@ -0,0 +1,284 @@
|
||||
import { diffWordsWithSpace, diffChars, Change } from "diff";
|
||||
import { Node, Schema } from "prosemirror-model";
|
||||
import { Transform } from "prosemirror-transform";
|
||||
import { applyPatch, createPatch, Operation } from "rfc6902";
|
||||
import { ReplaceOperation } from "rfc6902/diff";
|
||||
import { copy } from "./copy";
|
||||
import { getFromPath } from "./getFromPath";
|
||||
import { getReplaceStep } from "./getReplaceStep";
|
||||
import { removeMarks } from "./removeMarks";
|
||||
import { simplifyTransform } from "./simplifyTransform";
|
||||
import { JSONObject } from "./types";
|
||||
|
||||
export interface Options {
|
||||
complexSteps?: boolean;
|
||||
wordDiffs?: boolean;
|
||||
simplifyDiff?: boolean;
|
||||
}
|
||||
|
||||
export class RecreateTransform {
|
||||
fromDoc: Node;
|
||||
toDoc: Node;
|
||||
complexSteps: boolean;
|
||||
wordDiffs: boolean;
|
||||
simplifyDiff: boolean;
|
||||
schema: Schema;
|
||||
tr: Transform;
|
||||
/* current working document data, may get updated while recalculating node steps */
|
||||
currentJSON: JSONObject;
|
||||
/* final document as json data */
|
||||
finalJSON: JSONObject;
|
||||
ops: Array<Operation>;
|
||||
|
||||
constructor(fromDoc: Node, toDoc: Node, options: Options = {}) {
|
||||
const o = {
|
||||
complexSteps: true,
|
||||
wordDiffs: false,
|
||||
simplifyDiff: true,
|
||||
...options,
|
||||
};
|
||||
|
||||
this.fromDoc = fromDoc;
|
||||
this.toDoc = toDoc;
|
||||
this.complexSteps = o.complexSteps; // Whether to return steps other than ReplaceSteps
|
||||
this.wordDiffs = o.wordDiffs; // Whether to make text diffs cover entire words
|
||||
this.simplifyDiff = o.simplifyDiff;
|
||||
this.schema = fromDoc.type.schema;
|
||||
this.tr = new Transform(fromDoc);
|
||||
this.currentJSON = {};
|
||||
this.finalJSON = {};
|
||||
this.ops = [];
|
||||
}
|
||||
|
||||
init() {
|
||||
if (this.complexSteps) {
|
||||
// For First steps: we create versions of the documents without marks as
|
||||
// these will only confuse the diffing mechanism and marks won't cause
|
||||
// any mapping changes anyway.
|
||||
this.currentJSON = removeMarks(this.fromDoc).toJSON();
|
||||
this.finalJSON = removeMarks(this.toDoc).toJSON();
|
||||
this.ops = createPatch(this.currentJSON, this.finalJSON);
|
||||
this.recreateChangeContentSteps();
|
||||
this.recreateChangeMarkSteps();
|
||||
} else {
|
||||
// We don't differentiate between mark changes and other changes.
|
||||
this.currentJSON = this.fromDoc.toJSON();
|
||||
this.finalJSON = this.toDoc.toJSON();
|
||||
this.ops = createPatch(this.currentJSON, this.finalJSON);
|
||||
this.recreateChangeContentSteps();
|
||||
}
|
||||
|
||||
if (this.simplifyDiff) {
|
||||
this.tr = simplifyTransform(this.tr) || this.tr;
|
||||
}
|
||||
|
||||
return this.tr;
|
||||
}
|
||||
|
||||
/** convert json-diff to prosemirror steps */
|
||||
recreateChangeContentSteps() {
|
||||
// First step: find content changing steps.
|
||||
let ops = [];
|
||||
while (this.ops.length) {
|
||||
// get next
|
||||
let op = this.ops.shift() as Operation;
|
||||
ops.push(op);
|
||||
|
||||
let toDoc;
|
||||
const afterStepJSON = copy(this.currentJSON); // working document receiving patches
|
||||
const pathParts = op.path.split("/");
|
||||
|
||||
// collect operations until we receive a valid document:
|
||||
// apply ops-patches until a valid prosemirror document is retrieved,
|
||||
// then try to create a transformation step or retry with next operation
|
||||
while (toDoc === null) {
|
||||
applyPatch(afterStepJSON, [op]);
|
||||
|
||||
try {
|
||||
toDoc = this.schema.nodeFromJSON(afterStepJSON);
|
||||
toDoc.check();
|
||||
} catch (error) {
|
||||
toDoc = null;
|
||||
if (this.ops.length > 0) {
|
||||
op = this.ops.shift() as Operation;
|
||||
ops.push(op);
|
||||
} else {
|
||||
throw new Error(`No valid diff possible applying ${op.path}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// apply operation (ignoring afterStepJSON)
|
||||
if (
|
||||
this.complexSteps &&
|
||||
ops.length === 1 &&
|
||||
(pathParts.includes("attrs") || pathParts.includes("type"))
|
||||
) {
|
||||
// Node markup is changing
|
||||
this.addSetNodeMarkup(); // a lost update is ignored
|
||||
ops = [];
|
||||
} else if (
|
||||
ops.length === 1 &&
|
||||
op.op === "replace" &&
|
||||
pathParts[pathParts.length - 1] === "text"
|
||||
) {
|
||||
// Text is being replaced, we apply text diffing to find the smallest possible diffs.
|
||||
this.addReplaceTextSteps(op, afterStepJSON);
|
||||
ops = [];
|
||||
} else if (toDoc && this.addReplaceStep(toDoc, afterStepJSON)) {
|
||||
// operations have been applied
|
||||
ops = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** update node with attrs and marks, may also change type */
|
||||
addSetNodeMarkup() {
|
||||
// first diff in document is supposed to be a node-change (in type and/or attributes)
|
||||
// thus simply find the first change and apply a node change step, then recalculate the diff
|
||||
// after updating the document
|
||||
const fromDoc = this.schema.nodeFromJSON(this.currentJSON);
|
||||
const toDoc = this.schema.nodeFromJSON(this.finalJSON);
|
||||
const start = toDoc.content.findDiffStart(fromDoc.content) as number;
|
||||
// @note start is the same (first) position for current and target document
|
||||
const fromNode = fromDoc.nodeAt(start) as Node;
|
||||
const toNode = toDoc.nodeAt(start) as Node;
|
||||
|
||||
if (start !== null) {
|
||||
// @note this completly updates all attributes in one step, by completely replacing node
|
||||
const nodeType = fromNode.type === toNode.type ? null : toNode.type;
|
||||
try {
|
||||
this.tr.setNodeMarkup(start, nodeType, toNode.attrs, toNode.marks);
|
||||
} catch (e) {
|
||||
// if nodetypes differ, the updated node-type and contents might not be compatible
|
||||
// with schema and requires a replace
|
||||
if (nodeType && (e as Error).message.includes("Invalid content")) {
|
||||
// @todo add test-case for this scenario
|
||||
this.tr.replaceWith(start, start + fromNode.nodeSize, toNode);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
this.currentJSON = removeMarks(this.tr.doc).toJSON();
|
||||
// setting the node markup may have invalidated the following ops, so we calculate them again.
|
||||
this.ops = createPatch(this.currentJSON, this.finalJSON);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
recreateChangeMarkSteps() {
|
||||
// Now the documents should be the same, except their marks, so everything should map 1:1.
|
||||
// Second step: Iterate through the toDoc and make sure all marks are the same in tr.doc
|
||||
this.toDoc.descendants((tNode, tPos) => {
|
||||
if (!tNode.isInline) {
|
||||
return true;
|
||||
}
|
||||
|
||||
this.tr.doc.nodesBetween(tPos, tPos + tNode.nodeSize, (fNode, fPos) => {
|
||||
if (!fNode.isInline) {
|
||||
return true;
|
||||
}
|
||||
const from = Math.max(tPos, fPos);
|
||||
const to = Math.min(tPos + tNode.nodeSize, fPos + fNode.nodeSize);
|
||||
fNode.marks.forEach((nodeMark) => {
|
||||
if (!nodeMark.isInSet(tNode.marks)) {
|
||||
this.tr.removeMark(from, to, nodeMark);
|
||||
}
|
||||
});
|
||||
tNode.marks.forEach((nodeMark) => {
|
||||
if (!nodeMark.isInSet(fNode.marks)) {
|
||||
this.tr.addMark(from, to, nodeMark);
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
});
|
||||
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* retrieve and possibly apply replace-step based from doc changes
|
||||
* From http://prosemirror.net/examples/footnote/
|
||||
*/
|
||||
addReplaceStep(toDoc: Node, afterStepJSON: JSONObject) {
|
||||
const fromDoc = this.schema.nodeFromJSON(this.currentJSON);
|
||||
const step = getReplaceStep(fromDoc, toDoc);
|
||||
|
||||
if (!step) {
|
||||
return false;
|
||||
} else if (!this.tr.maybeStep(step).failed) {
|
||||
this.currentJSON = afterStepJSON;
|
||||
return true; // @change previously null
|
||||
}
|
||||
|
||||
throw new Error("No valid step found.");
|
||||
}
|
||||
|
||||
/** retrieve and possibly apply text replace-steps based from doc changes */
|
||||
addReplaceTextSteps(op: ReplaceOperation, afterStepJSON: JSONObject) {
|
||||
// We find the position number of the first character in the string
|
||||
const op1 = { ...op, value: "xx" };
|
||||
const op2 = { ...op, value: "yy" };
|
||||
const afterOP1JSON = copy(this.currentJSON);
|
||||
const afterOP2JSON = copy(this.currentJSON);
|
||||
applyPatch(afterOP1JSON, [op1]);
|
||||
applyPatch(afterOP2JSON, [op2]);
|
||||
const op1Doc = this.schema.nodeFromJSON(afterOP1JSON);
|
||||
const op2Doc = this.schema.nodeFromJSON(afterOP2JSON);
|
||||
|
||||
// get text diffs
|
||||
const finalText = op.value;
|
||||
const currentText = getFromPath(this.currentJSON, op.path) as string;
|
||||
const textDiffs = this.wordDiffs
|
||||
? diffWordsWithSpace(currentText, finalText)
|
||||
: diffChars(currentText, finalText);
|
||||
|
||||
let offset = op1Doc.content.findDiffStart(op2Doc.content) as number;
|
||||
const marks = op1Doc.resolve(offset + 1).marks();
|
||||
|
||||
while (textDiffs.length) {
|
||||
const diff = textDiffs.shift() as Change;
|
||||
|
||||
if (diff.added) {
|
||||
const textNode = this.schema
|
||||
.nodeFromJSON({ type: "text", text: diff.value })
|
||||
.mark(marks);
|
||||
|
||||
if (textDiffs.length && textDiffs[0].removed) {
|
||||
const nextDiff = textDiffs.shift() as Change;
|
||||
this.tr.replaceWith(offset, offset + nextDiff.value.length, textNode);
|
||||
} else {
|
||||
this.tr.insert(offset, textNode);
|
||||
}
|
||||
offset += diff.value.length;
|
||||
} else if (diff.removed) {
|
||||
if (textDiffs.length && textDiffs[0].added) {
|
||||
const nextDiff = textDiffs.shift() as Change;
|
||||
const textNode = this.schema
|
||||
.nodeFromJSON({ type: "text", text: nextDiff.value })
|
||||
.mark(marks);
|
||||
this.tr.replaceWith(offset, offset + diff.value.length, textNode);
|
||||
offset += nextDiff.value.length;
|
||||
} else {
|
||||
this.tr.delete(offset, offset + diff.value.length);
|
||||
}
|
||||
} else {
|
||||
offset += diff.value.length;
|
||||
}
|
||||
}
|
||||
|
||||
this.currentJSON = afterStepJSON;
|
||||
}
|
||||
}
|
||||
|
||||
export function recreateTransform(
|
||||
fromDoc: Node,
|
||||
toDoc: Node,
|
||||
options: Options = {}
|
||||
): Transform {
|
||||
const recreator = new RecreateTransform(fromDoc, toDoc, options);
|
||||
return recreator.init();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Node } from "prosemirror-model";
|
||||
import { Transform } from "prosemirror-transform";
|
||||
|
||||
export function removeMarks(doc: Node) {
|
||||
const tr = new Transform(doc);
|
||||
tr.removeMark(0, doc.nodeSize - 2);
|
||||
return tr.doc;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Node } from "prosemirror-model";
|
||||
import { Transform, ReplaceStep, Step } from "prosemirror-transform";
|
||||
import { getReplaceStep } from "./getReplaceStep";
|
||||
|
||||
// join adjacent ReplaceSteps
|
||||
export function simplifyTransform(tr: Transform) {
|
||||
if (!tr.steps.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const newTr = new Transform(tr.docs[0]);
|
||||
const oldSteps = tr.steps.slice();
|
||||
|
||||
while (oldSteps.length) {
|
||||
let step = oldSteps.shift() as Step;
|
||||
while (oldSteps.length && step.merge(oldSteps[0])) {
|
||||
const addedStep = oldSteps.shift() as Step;
|
||||
if (step instanceof ReplaceStep && addedStep instanceof ReplaceStep) {
|
||||
step = getReplaceStep(
|
||||
newTr.doc,
|
||||
addedStep.apply(step.apply(newTr.doc).doc as Node).doc as Node
|
||||
) as Step;
|
||||
} else {
|
||||
step = step.merge(addedStep) as Step;
|
||||
}
|
||||
}
|
||||
newTr.step(step);
|
||||
}
|
||||
return newTr;
|
||||
}
|
||||
10
shared/editor/lib/prosemirror-recreate-transform/types.ts
Normal file
10
shared/editor/lib/prosemirror-recreate-transform/types.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface JSONObject {
|
||||
[p: string]: JSONValue;
|
||||
}
|
||||
|
||||
export type JSONValue =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| JSONObject
|
||||
| Array<JSONValue>;
|
||||
@@ -1,9 +1,9 @@
|
||||
import { recreateTransform } from "@fellow/prosemirror-recreate-transform";
|
||||
import { EditorState, Plugin } from "prosemirror-state";
|
||||
import { Decoration, DecorationSet } from "prosemirror-view";
|
||||
import * as React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import FileExtension from "../components/FileExtension";
|
||||
import { recreateTransform } from "../lib/prosemirror-recreate-transform";
|
||||
|
||||
// based on the example at: https://prosemirror.net/examples/upload/
|
||||
const uploadPlaceholder = new Plugin({
|
||||
@@ -19,7 +19,7 @@ const uploadPlaceholder = new Plugin({
|
||||
wordDiffs: false,
|
||||
simplifyDiff: true,
|
||||
}).mapping;
|
||||
return set.map(mapping, tr.doc);
|
||||
set = set.map(mapping, tr.doc);
|
||||
} else {
|
||||
set = set.map(tr.mapping, tr.doc);
|
||||
}
|
||||
|
||||
@@ -2852,6 +2852,11 @@
|
||||
dependencies:
|
||||
"@types/ms" "*"
|
||||
|
||||
"@types/diff@^5.0.4":
|
||||
version "5.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/diff/-/diff-5.0.4.tgz#ba774c225ee68ce13a090fec16cf34b97a78537b"
|
||||
integrity sha512-d7489/WO4B65k0SIqxXtviR9+MrPDipWQF6w+5D7YPrqgu6Qb87JsTdWQaNZo7itcdbViQSev3Jaz7dtKO0+Dg==
|
||||
|
||||
"@types/emoji-regex@^9.2.0":
|
||||
version "9.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/emoji-regex/-/emoji-regex-9.2.0.tgz#2e117de04f5fa561c5dcbe43a860ecd856517525"
|
||||
|
||||
Reference in New Issue
Block a user