| author | |
| committer | |
| log | dca84efb53b071691a88c6712751fb7de9dcbfdb |
| tree | 343dff53bd51602ad82fc29c1b80abd655184e32 |
| parent | f7efb5354a3e6fb5e6b13a658ba57020e2c554ad |
| signature | Signed by PGP key B5690EEEBB952194 |
5 files changed, 25 insertions(+), 1 deletions(-)
apps/web/package.json+1| ... | ... | @@ -32,6 +32,7 @@ |
| 32 | 32 | "@tanstack/react-pacer": "^0.19.4", |
| 33 | 33 | "@tanstack/react-router": "^1.160.2", |
| 34 | 34 | "@tiptap/core": "^3.31.3", |
| 35 | "@tiptap/extension-code": "^3.31.3", | |
| 35 | 36 | "@tiptap/extension-task-item": "^3.31.3", |
| 36 | 37 | "@tiptap/extension-task-list": "^3.31.3", |
| 37 | 38 | "@tiptap/pm": "^3.31.3", |
apps/web/src/components/ComposerPromptEditorTiptap.tsx+4-1| ... | ... | @@ -46,6 +46,7 @@ import { |
| 46 | 46 | buildDocJson, |
| 47 | 47 | buildTiptapContent, |
| 48 | 48 | collapsedToFlat, |
| 49 | ComposerCodeExtension, | |
| 49 | 50 | ComposerTaskItemExtension, |
| 50 | 51 | flatToCollapsed, |
| 51 | 52 | flatToMarkdown, |
| ... | ... | @@ -754,8 +755,9 @@ function ComposerPromptEditorTiptapInner(props: ComposerPromptEditorProps) { |
| 754 | 755 | dropcursor: false, |
| 755 | 756 | gapcursor: false, |
| 756 | 757 | trailingNode: false, |
| 758 | code: false, | |
| 757 | 759 | // 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 }), | |
| 759 | 761 | }), |
| 760 | 762 | ComposerMentionExtension, |
| 761 | 763 | ComposerSkillExtension, |
| ... | ... | @@ -764,6 +766,7 @@ function ComposerPromptEditorTiptapInner(props: ComposerPromptEditorProps) { |
| 764 | 766 | ComposerMarkersExtension, |
| 765 | 767 | ...(richText |
| 766 | 768 | ? [ |
| 769 | ComposerCodeExtension, | |
| 767 | 770 | TaskList, |
| 768 | 771 | ComposerTaskItemExtension.extend({ |
| 769 | 772 | addInputRules() { |
apps/web/src/composer-rich-text-doc.test.ts+9| ... | ... | @@ -7,6 +7,7 @@ import { describe, expect, it } from "vite-plus/test"; |
| 7 | 7 | import { |
| 8 | 8 | buildDocJson, |
| 9 | 9 | collapsedToFlat, |
| 10 | ComposerCodeExtension, | |
| 10 | 11 | ComposerTaskItemExtension, |
| 11 | 12 | flatToCollapsed, |
| 12 | 13 | flatToMarkdown, |
| ... | ... | @@ -38,7 +39,9 @@ const schema = getSchemaByResolvedExtensions( |
| 38 | 39 | dropcursor: false, |
| 39 | 40 | gapcursor: false, |
| 40 | 41 | trailingNode: false, |
| 42 | code: false, | |
| 41 | 43 | }), |
| 44 | ComposerCodeExtension, | |
| 42 | 45 | stubAtom("composer-mention", { path: { default: "" }, source: { default: "" } }), |
| 43 | 46 | stubAtom("composer-skill", { |
| 44 | 47 | skillName: { default: "" }, |
| ... | ... | @@ -64,6 +67,8 @@ const schema = getSchemaByResolvedExtensions( |
| 64 | 67 | function roundTrip(value: string) { |
| 65 | 68 | const json = buildDocJson(value, (name) => ({ label: name, description: null })); |
| 66 | 69 | const doc = ProseMirrorNode.fromJSON(schema, json); |
| 70 | // `insertContent` validates every node against the schema; `fromJSON` does not. | |
| 71 | doc.check(); | |
| 67 | 72 | return serializeEditorDoc(doc); |
| 68 | 73 | } |
| 69 | 74 | |
| ... | ... | @@ -135,6 +140,10 @@ describe("composer rich text document model", () => { |
| 135 | 140 | "hello **bold** world", |
| 136 | 141 | "a *italic* word and `code` here", |
| 137 | 142 | "struck ~~out~~ now", |
| 143 | "**`x`**", | |
| 144 | "*`x`*", | |
| 145 | "~~`x`~~", | |
| 146 | "**a `code` c**", | |
| 138 | 147 | "***bold italic*** keeps nesting", |
| 139 | 148 | "line one\nline two", |
| 140 | 149 | "trailing newline\n", |
apps/web/src/composer-rich-text-doc.ts+8| ... | ... | @@ -1,4 +1,5 @@ |
| 1 | 1 | import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; |
| 2 | import { Code } from "@tiptap/extension-code"; | |
| 2 | 3 | import { TaskItem } from "@tiptap/extension-task-item"; |
| 3 | 4 | |
| 4 | 5 | import { splitPromptIntoComposerSegments } from "~/composer-editor-mentions"; |
| ... | ... | @@ -41,6 +42,13 @@ const TIPTAP_TO_MARK: Record<string, RichTextMark> = { |
| 41 | 42 | code: "code", |
| 42 | 43 | }; |
| 43 | 44 | |
| 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 | */ | |
| 50 | export const ComposerCodeExtension = Code.extend({ excludes: "code" }); | |
| 51 | ||
| 44 | 52 | /** |
| 45 | 53 | * Task list items keep their exact source indent in an attribute so nesting |
| 46 | 54 | * round-trips byte-identically. Checkbox case (`[X]`) normalizes to `[x]` — |
pnpm-lock.yaml+3| ... | ... | @@ -637,6 +637,9 @@ importers: |
| 637 | 637 | '@tiptap/core': |
| 638 | 638 | specifier: ^3.31.3 |
| 639 | 639 | 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)) | |
| 640 | 643 | '@tiptap/extension-task-item': |
| 641 | 644 | specifier: ^3.31.3 |
| 642 | 645 | 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)) |