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