Skip to content
Open
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
27 changes: 27 additions & 0 deletions desktop/src/features/messages/lib/composerTextNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Node } from "@tiptap/core";

/**
* Composer text node with Markdown serialization that leaves angle brackets
* untouched. tiptap-markdown's stock text serializer HTML-escapes them,
* which makes literal inline-code Markdown render `<`/`>` visibly.
*/
export const ComposerText = Node.create({
name: "text",
group: "inline",

addStorage() {
return {
markdown: {
serialize(
state: { text: (value: string) => void },
node: { text: string },
) {
state.text(node.text);
},
parse: {
// handled by markdown-it
},
},
};
},
});
103 changes: 103 additions & 0 deletions desktop/src/features/messages/lib/markdownSerialization.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import assert from "node:assert/strict";
import test from "node:test";

import { Editor } from "@tiptap/core";
import StarterKit from "@tiptap/starter-kit";
import { Markdown as TiptapMarkdown } from "tiptap-markdown";

import { ComposerText } from "./composerTextNode.ts";

function createEditor(content) {
return new Editor({
element: null,
extensions: [
StarterKit.configure({
heading: false,
link: false,
text: false,
trailingNode: false,
}),
ComposerText,
TiptapMarkdown.configure({
html: false,
breaks: true,
}),
],
content,
});
}

function serializeComposerMarkdown(editor) {
// Mirror getMarkdownFromEditor: the composer intentionally removes
// prosemirror-markdown's backslash escapes for user-authored Markdown.
let markdown = editor.storage.markdown.getMarkdown();
markdown = markdown.replace(/\\\n/g, "\n");
return markdown.replace(/\\([`*\\~[\]_])/g, "$1");
}

test("inline-code Markdown preserves angle brackets in Markdown serialization", () => {
const editor = createEditor({
type: "doc",
content: [
{
type: "paragraph",
content: [
{
type: "text",
text: "`<repo root>/website`",
},
],
},
],
});

try {
assert.equal(serializeComposerMarkdown(editor), "`<repo root>/website`");
} finally {
editor.destroy();
}
});

test("typed HTML entities remain literal text", () => {
const editor = createEditor({
type: "doc",
content: [
{
type: "paragraph",
content: [{ type: "text", text: "literal &lt;repo&gt;" }],
},
],
});

try {
assert.equal(serializeComposerMarkdown(editor), "literal &lt;repo&gt;");
} finally {
editor.destroy();
}
});

test("Markdown punctuation and inline-code delimiters remain unchanged", () => {
const editor = createEditor({
type: "doc",
content: [
{
type: "paragraph",
content: [
{
type: "text",
text: "**bold** and `inline` [brackets] ~tilde~",
},
],
},
],
});

try {
assert.equal(
serializeComposerMarkdown(editor),
"**bold** and `inline` [brackets] ~tilde~",
);
} finally {
editor.destroy();
}
});
3 changes: 3 additions & 0 deletions desktop/src/features/messages/lib/useRichTextEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { SpoilerMark } from "./spoilerMark";
import { createComposerLinkPasteHandler } from "./composerMessageLinkNode";
import type { ComposerMessageLinkChannel } from "./useComposerMessageLinks";
import { useComposerMessageLinks } from "./useComposerMessageLinks";
import { ComposerText } from "./composerTextNode";

function hardBreakLineBounds($from: ResolvedPos) {
const parentStart = $from.start();
Expand Down Expand Up @@ -244,6 +245,7 @@ export function useRichTextEditor({
{
extensions: [
StarterKit.configure({
text: false,
// Use hard breaks (Shift+Enter) — Enter submits the message.
hardBreak: {
keepMarks: true,
Expand All @@ -270,6 +272,7 @@ export function useRichTextEditor({
// below with custom options (autolink, openOnClick, etc.).
link: false,
}),
ComposerText,
// macOS text fields traditionally support a small set of Emacs-style
// Control shortcuts. Keep movement and kill-line scoped to the current
// hard-break-delimited line rather than the whole ProseMirror block.
Expand Down
Loading