chore: Replace custom toPlainText serialization with leafText (#10039)

This commit is contained in:
Hemachandar
2025-08-30 20:26:07 +05:30
committed by GitHub
parent 27d116c8e2
commit 7f818c7329
18 changed files with 25 additions and 71 deletions

View File

@@ -44,9 +44,7 @@ export default class ClipboardTextSerializer extends Extension {
softBreak: true,
})
: slice.content.content
.map((node) =>
ProsemirrorHelper.toPlainText(node, this.editor.schema)
)
.map((node) => ProsemirrorHelper.toPlainText(node))
.join("");
},
},

View File

@@ -35,7 +35,6 @@ import Extension, {
import ExtensionManager from "@shared/editor/lib/ExtensionManager";
import { MarkdownSerializer } from "@shared/editor/lib/markdown/serializer";
import textBetween from "@shared/editor/lib/textBetween";
import { getTextSerializers } from "@shared/editor/lib/textSerializers";
import Mark from "@shared/editor/marks/Mark";
import { basicExtensions as extensions } from "@shared/editor/nodes";
import Node from "@shared/editor/nodes/Node";
@@ -627,8 +626,7 @@ export class Editor extends React.PureComponent<
*
* @returns A list of headings in the document
*/
public getHeadings = () =>
ProsemirrorHelper.getHeadings(this.view.state.doc, this.schema);
public getHeadings = () => ProsemirrorHelper.getHeadings(this.view.state.doc);
/**
* Return the images in the current editor.
@@ -721,9 +719,8 @@ export class Editor extends React.PureComponent<
*/
public getPlainText = () => {
const { doc } = this.view.state;
const textSerializers = getTextSerializers(this.schema);
return textBetween(doc, 0, doc.content.size, textSerializers);
return textBetween(doc, 0, doc.content.size);
};
private dispatchThemeChanged = (event: CustomEvent) => {

View File

@@ -723,8 +723,7 @@ export default class Document extends ArchivableModel implements Searchable {
marks: extensionManager.marks,
});
const text = ProsemirrorHelper.toPlainText(
Node.fromJSON(schema, this.data),
schema
Node.fromJSON(schema, this.data)
);
return text;
};

View File

@@ -136,7 +136,7 @@ class Comment extends ParanoidModel<
*/
public toPlainText() {
const node = Node.fromJSON(schema, this.data);
return ProsemirrorHelper.toPlainText(node, schema);
return ProsemirrorHelper.toPlainText(node);
}
// hooks

View File

@@ -4,7 +4,6 @@ import ukkonen from "ukkonen";
import { updateYFragment, yDocToProsemirrorJSON } from "y-prosemirror";
import * as Y from "yjs";
import textBetween from "@shared/editor/lib/textBetween";
import { getTextSerializers } from "@shared/editor/lib/textSerializers";
import { EditorStyleHelper } from "@shared/editor/styles/EditorStyleHelper";
import { IconType, ProsemirrorData } from "@shared/types";
import { determineIconType } from "@shared/utils/icon";
@@ -141,8 +140,7 @@ export class DocumentHelper {
*/
static toPlainText(document: Document | Revision | ProsemirrorData) {
const node = DocumentHelper.toProsemirror(document);
return textBetween(node, 0, node.content.size, this.textSerializers);
return textBetween(node, 0, node.content.size);
}
/**
@@ -523,6 +521,4 @@ export class DocumentHelper {
const distance = ukkonen(first, second, threshold + 1);
return distance > threshold;
}
private static textSerializers = getTextSerializers(schema);
}

View File

@@ -25,10 +25,7 @@ export default function TextLength({
let text;
try {
text = ProsemirrorHelper.toPlainText(
Node.fromJSON(schema, value),
schema
);
text = ProsemirrorHelper.toPlainText(Node.fromJSON(schema, value));
} catch (_err) {
throw new Error("Invalid data");
}

View File

@@ -143,7 +143,7 @@ export function exportTable({
.map((row) =>
row
.map((cell) => {
let value = ProsemirrorHelper.toPlainText(cell, state.schema);
let value = ProsemirrorHelper.toPlainText(cell);
// Escape double quotes by doubling them
if (value.includes('"')) {

View File

@@ -1,5 +1,4 @@
import { Node as ProseMirrorNode } from "prosemirror-model";
import { PlainTextSerializer } from "../types";
/**
* Returns the text content between two positions.
@@ -7,25 +6,22 @@ import { PlainTextSerializer } from "../types";
* @param doc The Prosemirror document to use
* @param from A start point
* @param to An end point
* @param plainTextSerializers A map of node names to PlainTextSerializers which convert a node to plain text
* @returns A string of plain text
*/
export default function textBetween(
doc: ProseMirrorNode,
from: number,
to: number,
plainTextSerializers: Record<string, PlainTextSerializer | undefined>
to: number
): string {
let text = "";
let first = true;
const blockSeparator = "\n";
doc.nodesBetween(from, to, (node, pos) => {
const toPlainText = plainTextSerializers[node.type.name];
let nodeText = "";
if (toPlainText) {
nodeText += toPlainText(node);
if (node.type.spec.leafText) {
nodeText += node.type.spec.leafText(node);
} else if (node.isText) {
nodeText += node.textBetween(
Math.max(from, pos) - pos,

View File

@@ -1,14 +0,0 @@
import { Schema } from "prosemirror-model";
/**
* Generate a map of text serializers for a given schema
* @param schema
* @returns Text serializers
*/
export function getTextSerializers(schema: Schema) {
return Object.fromEntries(
Object.entries(schema.nodes)
.filter(([, node]) => node.spec.toPlainText)
.map(([name, node]) => [name, node.spec.toPlainText])
);
}

View File

@@ -65,7 +65,7 @@ export default class Attachment extends Node {
},
String(node.attrs.title),
],
toPlainText: (node) => node.attrs.title,
leafText: (node) => node.attrs.title,
};
}

View File

@@ -87,7 +87,7 @@ export default class Embed extends Node {
];
}
},
toPlainText: (node) => node.attrs.href,
leafText: (node) => node.attrs.href,
};
}

View File

@@ -58,7 +58,7 @@ export default class Emoji extends Extension {
getEmojiFromName(name),
];
},
toPlainText: (node) => getEmojiFromName(node.attrs["data-name"]),
leafText: (node) => getEmojiFromName(node.attrs["data-name"]),
};
}

View File

@@ -19,7 +19,7 @@ export default class HardBreak extends Node {
selectable: false,
parseDOM: [{ tag: "br" }],
toDOM: () => ["br"],
toPlainText: () => "\n",
leafText: () => "\n",
};
}

View File

@@ -204,7 +204,7 @@ export default class Image extends SimpleImage {
...children,
];
},
toPlainText: (node) =>
leafText: (node) =>
node.attrs.alt ? `(image: ${node.attrs.alt})` : "(image)",
};
}

View File

@@ -119,7 +119,6 @@ export default class Mention extends Node {
},
toPlainText(node),
],
toPlainText,
leafText: toPlainText,
};
}

View File

@@ -77,7 +77,7 @@ export default class Video extends Node {
String(node.attrs.title),
],
],
toPlainText: (node) => node.attrs.title,
leafText: (node) => node.attrs.title,
};
}

View File

@@ -8,11 +8,4 @@ declare module "prosemirror-model" {
// https://github.com/ProseMirror/prosemirror-model/blob/bd13a2329fda39f1c4d09abd8f0db2032bdc8014/src/replace.js#L51
removeBetween(from: number, to: number): Slice;
}
interface NodeSpec {
/**
* Defines the text representation of the node when copying to clipboard.
*/
toPlainText?: PlainTextSerializer;
}
}

View File

@@ -1,7 +1,6 @@
import { Node, Schema } from "prosemirror-model";
import headingToSlug from "../editor/lib/headingToSlug";
import textBetween from "../editor/lib/textBetween";
import { getTextSerializers } from "../editor/lib/textSerializers";
import { ProsemirrorData } from "../types";
import { TextHelper } from "./TextHelper";
import env from "../env";
@@ -91,9 +90,8 @@ export class ProsemirrorHelper {
* @param schema The schema to use.
* @returns The document content as plain text without formatting.
*/
static toPlainText(root: Node, schema: Schema) {
const textSerializers = getTextSerializers(schema);
return textBetween(root, 0, root.content.size, textSerializers);
static toPlainText(root: Node) {
return textBetween(root, 0, root.content.size);
}
/**
@@ -102,7 +100,6 @@ export class ProsemirrorHelper {
* @returns True if the editor is empty
*/
static trim(doc: Node) {
const { schema } = doc.type;
let index = 0,
start = 0,
end = doc.nodeSize - 2,
@@ -118,7 +115,7 @@ export class ProsemirrorHelper {
if (!node) {
break;
}
isEmpty = ProsemirrorHelper.toPlainText(node, schema).trim() === "";
isEmpty = ProsemirrorHelper.toPlainText(node).trim() === "";
if (isEmpty) {
start += node.nodeSize;
}
@@ -131,7 +128,7 @@ export class ProsemirrorHelper {
if (!node) {
break;
}
isEmpty = ProsemirrorHelper.toPlainText(node, schema).trim() === "";
isEmpty = ProsemirrorHelper.toPlainText(node).trim() === "";
if (isEmpty) {
end -= node.nodeSize;
}
@@ -150,8 +147,6 @@ export class ProsemirrorHelper {
return !doc || doc.textContent.trim() === "";
}
const textSerializers = getTextSerializers(schema);
let empty = true;
doc.descendants((child: Node) => {
// If we've already found non-empty data, we can stop descending further
@@ -159,9 +154,8 @@ export class ProsemirrorHelper {
return false;
}
const toPlainText = textSerializers[child.type.name];
if (toPlainText) {
empty = !toPlainText(child).trim();
if (child.type.spec.leafText) {
empty = !child.type.spec.leafText(child).trim();
} else if (child.isText) {
empty = !child.text?.trim();
}
@@ -331,10 +325,9 @@ export class ProsemirrorHelper {
* Iterates through the document to find all of the headings and their level.
*
* @param doc Prosemirror document node
* @param schema Prosemirror schema
* @returns Array<Heading>
*/
static getHeadings(doc: Node, schema: Schema) {
static getHeadings(doc: Node) {
const headings: Heading[] = [];
const previouslySeen: Record<string, number> = {};
@@ -356,7 +349,7 @@ export class ProsemirrorHelper {
previouslySeen[id] !== undefined ? previouslySeen[id] + 1 : 1;
headings.push({
title: ProsemirrorHelper.toPlainText(node, schema),
title: ProsemirrorHelper.toPlainText(node),
level: node.attrs.level,
id: name,
});