Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions packages/core/src/blocks/defaultBlocks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Editor, Extension, getSchema, Mark, Node } from "@tiptap/core";
import { describe, expect, it } from "vite-plus/test";
import { CommentMark } from "../comments/mark.js";
import { NON_FORMATTING_MARK_GROUP } from "../schema/markGroups.js";
import { defaultStyleSpecs } from "./defaultBlocks.js";

const extensions = [
Node.create({ name: "doc", topNode: true, content: "text*" }),
Node.create({ name: "text", group: "inline" }),
...Object.values(defaultStyleSpecs).map((style) => style.implementation.mark),
];

describe("inline code mark exclusions", () => {
it.each([false, true])(
"builds exclusions from enabled marks (comments enabled: %s)",
(withComments) => {
const editor = new Editor({
element: null,
extensions: [
...extensions,
Extension.create({
name: "customMarks",
addExtensions() {
return [
Mark.create({ name: "customStyle" }),
...(withComments
? [
CommentMark,
Mark.create({
name: "customAnnotation",
group: NON_FORMATTING_MARK_GROUP,
}),
]
: []),
];
},
}),
],
});

try {
const { marks } = editor.schema;
const code = marks.code;
expect(code.spec.excludes).not.toBe("_");
for (const mark of Object.values(marks)) {
expect(code.excludes(mark)).toBe(
!mark.spec.group?.split(" ").includes(NON_FORMATTING_MARK_GROUP),
);
}

if (withComments) {
const codeMark = code.create();
const commentMark = marks.comment.create({ threadId: "thread" });
expect(codeMark.addToSet([commentMark])).toHaveLength(2);
expect(commentMark.addToSet([codeMark])).toHaveLength(2);
}
} finally {
editor.destroy();
}
},
);

it("supports schema creation without an editor", () => {
const { marks } = getSchema(extensions);
expect(marks.code.excludes(marks.bold)).toBe(true);
});
});
4 changes: 4 additions & 0 deletions packages/core/src/blocks/defaultBlocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
getInlineContentSchemaFromSpecs,
getStyleSchemaFromSpecs,
} from "../schema/index.js";
import { marksExcludingNonFormattingMarks } from "../schema/markGroups.js";
import {
createAudioBlockSpec,
createBulletListItemBlockSpec,
Expand Down Expand Up @@ -139,6 +140,9 @@ export const defaultStyleSpecs = {
strike: createStyleSpecFromTipTapMark(Strike, "boolean"),
code: createStyleSpecFromTipTapMark(
Code.extend({
excludes() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we have NON_FORMATTING_MARK_GROUP for this right? cc @nperez0111 ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yea, true. we would need a function similar to nonFormattingMarks which sort of does the same thing based on group membership

return marksExcludingNonFormattingMarks(this.editor);
},
addInputRules() {
return [
// Matches any string that starts with a backtick, ends with a
Expand Down
36 changes: 34 additions & 2 deletions packages/core/src/schema/markGroups.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Editor, getExtensionField } from "@tiptap/core";
import { Editor, flattenExtensions, getExtensionField } from "@tiptap/core";

/**
* ProseMirror mark group for "non-formatting" marks: comments and the
Expand Down Expand Up @@ -36,7 +36,9 @@ export function nonFormattingMarks(editor: Editor | undefined): string {
if (!editor) {
return "";
}
const hasNonFormattingMark = editor.options.extensions.some((extension) => {
const hasNonFormattingMark = flattenExtensions(
editor.options.extensions,
).some((extension) => {
if (extension.type !== "mark") {
return false;
}
Expand All @@ -48,3 +50,33 @@ export function nonFormattingMarks(editor: Editor | undefined): string {
});
return hasNonFormattingMark ? NON_FORMATTING_MARK_GROUP : "";
}

/**
* The `excludes` field value for marks which may coexist with annotation marks
* but should exclude all other marks.
*
* Returns `"_"` when no editor is available, preserving ProseMirror's default
* behavior for schema creation outside an editor.
*/
export function marksExcludingNonFormattingMarks(
editor: Editor | undefined,
): string {
if (!editor) {
return "_";
}

return flattenExtensions(editor.options.extensions)
.filter((extension) => {
if (extension.type !== "mark") {
return false;
}

const group = getExtensionField(extension, "group") as string | undefined;
return (
typeof group !== "string" ||
!group.split(" ").includes(NON_FORMATTING_MARK_GROUP)
);
})
.map((extension) => extension.name)
.join(" ");
}
32 changes: 31 additions & 1 deletion tests/src/end-to-end/comments/comments.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import App from "@examples/07-collaboration/09-comments-testing/src/App";
import { beforeEach, describe, expect, test, vi } from "vite-plus/test";
import { render } from "vitest-browser-react";
import { browserName, page, userEvent } from "../../utils/context.js";
import { browserName, MOD, page, userEvent } from "../../utils/context.js";
import { EDITOR_SELECTOR, LINK_BUTTON_SELECTOR } from "../../utils/const.js";
import {
expectElement,
Expand Down Expand Up @@ -190,6 +190,36 @@ describe("Check Comments functionality", () => {
).toBeVisible();
});

test("Should preserve existing comments when adding a code mark", async () => {
await focusOnEditor();

await userEvent.keyboard("hello");
await doubleClickElement(page.getByText("hello").element());

await userEvent.click(await waitForSelector('[data-test="addcomment"]'));
await waitForSelector(".bn-thread");

await userEvent.keyboard("test comment");
await userEvent.click(await waitForSelector('button[data-test="save"]'));

// Re-select the commented text and toggle inline code on it (Cmd/Ctrl+E).
await doubleClickElement(
document.querySelectorAll("span.bn-thread-mark")[0] as HTMLElement,
);
await userEvent.keyboard(`{${MOD}>}e{/${MOD}}`);

// The comment must be preserved, and the text must now also be inline code,
// i.e. the comment and code marks coexist on the same text.
await expectElement(
await waitForSelector("span.bn-thread-mark"),
).toBeVisible();
await expectElement(
await waitForSelector(
"span.bn-thread-mark code, code span.bn-thread-mark",
),
).toBeVisible();
});

test.skipIf(browserName === "webkit")(
"Should select thread on first click and open link on second click",
async () => {
Expand Down
Loading