Preserve inline-code link labels in rich markdown

This commit is contained in:
brennanb2025
2026-06-23 19:39:27 -07:00
parent e03a1cf769
commit 71c0ea3149
2 changed files with 75 additions and 1 deletions
@@ -20,6 +20,35 @@ function roundTripMarkdown(content: string): string {
}
}
function markdownAfterTextReplace(content: string, search: string, replacement: string): string {
const editor = new Editor({
element: null,
extensions: createRichMarkdownExtensions(),
content: encodeRawMarkdownHtmlForRichEditor(content),
contentType: 'markdown'
})
try {
let from: number | null = null
editor.state.doc.descendants((node, pos) => {
if (from !== null || !node.isText || !node.text) {
return
}
const index = node.text.indexOf(search)
if (index !== -1) {
from = pos + index
}
})
if (from === null) {
throw new Error(`Missing text: ${search}`)
}
editor.view.dispatch(editor.state.tr.insertText(replacement, from, from + search.length))
return editor.getMarkdown().trimEnd()
} finally {
editor.destroy()
}
}
function slashCommandMarkdown(commandId: SlashCommandId): string {
const editor = new Editor({
element: null,
@@ -158,6 +187,36 @@ describe('rich markdown round trip', () => {
)
})
it('preserves links whose label is inline code', () => {
expect(roundTripMarkdown('Link to [`foo.md`](./foo.md) here\n')).toBe(
'Link to [`foo.md`](./foo.md) here'
)
})
it('preserves links whose label is inline code after an editor transaction', () => {
expect(
markdownAfterTextReplace('Link to [`foo.md`](./foo.md) here\n', 'here', 'here saved')
).toBe('Link to [`foo.md`](./foo.md) here saved')
})
it('preserves links when editing inside an inline-code label', () => {
expect(markdownAfterTextReplace('Link to [`foo.md`](./foo.md) here\n', 'foo', 'bar')).toBe(
'Link to [`bar.md`](./foo.md) here'
)
})
it('preserves titled links whose label is inline code', () => {
expect(roundTripMarkdown('Link to [`foo.md`](./foo.md "Foo") here\n')).toBe(
'Link to [`foo.md`](./foo.md "Foo") here'
)
})
it('preserves bold link labels as formatted link text', () => {
expect(roundTripMarkdown('Link to [**bold**](./foo.md) here\n')).toBe(
'Link to [**bold**](./foo.md) here'
)
})
it('does not surface Linear issue reference definitions as description text', () => {
const input = [
'- [x] [H-279]',
@@ -1,6 +1,7 @@
import type { AnyExtension } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import Link from '@tiptap/extension-link'
import { Code } from '@tiptap/extension-code'
import Image from '@tiptap/extension-image'
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
import Placeholder from '@tiptap/extension-placeholder'
@@ -28,6 +29,18 @@ import { createRichMarkdownAnnotationHighlightExtension } from './rich-markdown-
const lowlight = createLowlight(common)
const RichMarkdownLink = Link.extend({
// Why: link's priority must stay below code's default 100 so Markdown
// serializes code-styled labels as [`label`](href).
priority: 90
})
const RichMarkdownCode = Code.extend({
// Why: Markdown supports linked code labels, so code cannot exclude the link
// mark even though it should still stay exclusive with emphasis marks.
excludes: 'code bold italic strike underline'
})
export function createRichMarkdownExtensions({
includePlaceholder = false
}: {
@@ -39,8 +52,10 @@ export function createRichMarkdownExtensions({
// preview and then still lose syntax on save.
StarterKit.configure({
link: false,
code: false,
codeBlock: false
}),
RichMarkdownCode,
CodeBlockLowlight.extend({
addNodeView() {
return safeReactNodeViewRenderer(RichMarkdownCodeBlock)
@@ -49,7 +64,7 @@ export function createRichMarkdownExtensions({
lowlight,
defaultLanguage: null
}),
Link.configure({
RichMarkdownLink.configure({
openOnClick: false,
autolink: true,
linkOnPaste: true