authorgravatar for 75276669+Gigioxx@users.noreply.github.comGuillermo Casanova <75276669+Gigioxx@users.noreply.github.com> 2026-09-21 00:01:10-03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-21 00:01:10-03:00
logdca84efb53b071691a88c6712751fb7de9dcbfdb
tree343dff53bd51602ad82fc29c1b80abd655184e32
parentf7efb5354a3e6fb5e6b13a658ba57020e2c554ad
signature Signed by PGP key B5690EEEBB952194

fix(web): paste markdown with inline code inside bold, italic, or strikethrough (#12290)


5 files changed, 25 insertions(+), 1 deletions(-)

apps/web/package.json+1
......@@ -32,6 +32,7 @@
3232 "@tanstack/react-pacer": "^0.19.4",
3333 "@tanstack/react-router": "^1.160.2",
3434 "@tiptap/core": "^3.31.3",
35 "@tiptap/extension-code": "^3.31.3",
3536 "@tiptap/extension-task-item": "^3.31.3",
3637 "@tiptap/extension-task-list": "^3.31.3",
3738 "@tiptap/pm": "^3.31.3",
apps/web/src/components/ComposerPromptEditorTiptap.tsx+4-1
......@@ -46,6 +46,7 @@ import {
4646 buildDocJson,
4747 buildTiptapContent,
4848 collapsedToFlat,
49 ComposerCodeExtension,
4950 ComposerTaskItemExtension,
5051 flatToCollapsed,
5152 flatToMarkdown,
......@@ -754,8 +755,9 @@ function ComposerPromptEditorTiptapInner(props: ComposerPromptEditorProps) {
754755 dropcursor: false,
755756 gapcursor: false,
756757 trailingNode: false,
758 code: false,
757759 // Plain mode has no marks: typed markers stay literal characters.
758 ...(richText ? {} : { bold: false, italic: false, strike: false, code: false }),
760 ...(richText ? {} : { bold: false, italic: false, strike: false }),
759761 }),
760762 ComposerMentionExtension,
761763 ComposerSkillExtension,
......@@ -764,6 +766,7 @@ function ComposerPromptEditorTiptapInner(props: ComposerPromptEditorProps) {
764766 ComposerMarkersExtension,
765767 ...(richText
766768 ? [
769 ComposerCodeExtension,
767770 TaskList,
768771 ComposerTaskItemExtension.extend({
769772 addInputRules() {
apps/web/src/composer-rich-text-doc.test.ts+9
......@@ -7,6 +7,7 @@ import { describe, expect, it } from "vite-plus/test";
77import {
88 buildDocJson,
99 collapsedToFlat,
10 ComposerCodeExtension,
1011 ComposerTaskItemExtension,
1112 flatToCollapsed,
1213 flatToMarkdown,
......@@ -38,7 +39,9 @@ const schema = getSchemaByResolvedExtensions(
3839 dropcursor: false,
3940 gapcursor: false,
4041 trailingNode: false,
42 code: false,
4143 }),
44 ComposerCodeExtension,
4245 stubAtom("composer-mention", { path: { default: "" }, source: { default: "" } }),
4346 stubAtom("composer-skill", {
4447 skillName: { default: "" },
......@@ -64,6 +67,8 @@ const schema = getSchemaByResolvedExtensions(
6467function roundTrip(value: string) {
6568 const json = buildDocJson(value, (name) => ({ label: name, description: null }));
6669 const doc = ProseMirrorNode.fromJSON(schema, json);
70 // `insertContent` validates every node against the schema; `fromJSON` does not.
71 doc.check();
6772 return serializeEditorDoc(doc);
6873}
6974
......@@ -135,6 +140,10 @@ describe("composer rich text document model", () => {
135140 "hello **bold** world",
136141 "a *italic* word and `code` here",
137142 "struck ~~out~~ now",
143 "**`x`**",
144 "*`x`*",
145 "~~`x`~~",
146 "**a `code` c**",
138147 "***bold italic*** keeps nesting",
139148 "line one\nline two",
140149 "trailing newline\n",
apps/web/src/composer-rich-text-doc.ts+8
......@@ -1,4 +1,5 @@
11import type { Node as ProseMirrorNode } from "@tiptap/pm/model";
2import { Code } from "@tiptap/extension-code";
23import { TaskItem } from "@tiptap/extension-task-item";
34
45import { splitPromptIntoComposerSegments } from "~/composer-editor-mentions";
......@@ -41,6 +42,13 @@ const TIPTAP_TO_MARK: Record<string, RichTextMark> = {
4142 code: "code",
4243};
4344
45/**
46 * Tiptap's code mark excludes every other mark, which rejects the `bold+code`
47 * spans markdown like `**\`x\`**` parses into and drops the whole insert.
48 * Code nests inside emphasis here, so it only excludes itself like the rest.
49 */
50export const ComposerCodeExtension = Code.extend({ excludes: "code" });
51
4452/**
4553 * Task list items keep their exact source indent in an attribute so nesting
4654 * round-trips byte-identically. Checkbox case (`[X]`) normalizes to `[x]` —
pnpm-lock.yaml+3
......@@ -637,6 +637,9 @@ importers:
637637 '@tiptap/core':
638638 specifier: ^3.31.3
639639 version: 3.31.3(@tiptap/pm@3.31.3)
640 '@tiptap/extension-code':
641 specifier: ^3.31.3
642 version: 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))
640643 '@tiptap/extension-task-item':
641644 specifier: ^3.31.3
642645 version: 3.31.3(@tiptap/extension-list@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3))