Skip to content

Commit 6a9b3f8

Browse files
committed
fix(editor): upgrade tiptap to 3.31.4 with a single prosemirror copy to pick up the paste XSS fix
1 parent 0f79f27 commit 6a9b3f8

12 files changed

Lines changed: 130 additions & 143 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/extensions.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,8 @@ function guardParagraphLeading(text: string): string {
168168
* paragraph renders as just its inline children; this override wraps that with the leading guard.
169169
*/
170170
const BlockSafeParagraph = Paragraph.extend({
171-
parseMarkdown: (token, helpers) => {
172-
const parsed = Paragraph.config.parseMarkdown?.(token, helpers) ?? []
171+
parseMarkdown(token, helpers) {
172+
const parsed = this.parent?.(token, helpers) ?? []
173173
return Array.isArray(parsed) ? parsed : splitBlockImageParagraph(parsed)
174174
},
175175
renderMarkdown: (node: JSONContent, h, context) => {
@@ -271,8 +271,8 @@ export function createMarkdownContentExtensions(
271271
nodeViews.mention ?? MarkdownMention,
272272
TaskList,
273273
TaskItem.extend({
274-
parseMarkdown: (token, helpers) => {
275-
const parsed = TaskItem.config.parseMarkdown?.(token, helpers) ?? []
274+
parseMarkdown(token, helpers) {
275+
const parsed = this.parent?.(token, helpers) ?? []
276276
if (Array.isArray(parsed)) return parsed
277277
const content = (parsed.content ?? []).flatMap(splitBlockImageParagraph)
278278
if (content[0]?.type !== 'paragraph') content.unshift({ type: 'paragraph' })

‎apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/field-upload.test.tsx‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ async function render(
5757
)
5858
await act(async () => vi.advanceTimersByTimeAsync(10))
5959
}
60+
61+
/** ProseMirror buffers inline atoms at a block start with an empty separator `<img>`. */
62+
const RENDERED_IMAGE = 'img:not(.ProseMirror-separator)'
63+
6064
function editor() {
6165
return host.querySelector<HTMLElement & { editor: Editor }>('.tiptap')!.editor
6266
}
@@ -116,9 +120,9 @@ describe('field upload completion boundary', () => {
116120
expect(host.querySelector('strong')?.textContent).toBe('Tail')
117121
expect(host.querySelector('a')?.getAttribute('href')).toBe('/destination')
118122
expect(
119-
Array.from(host.querySelectorAll('img')).map((image) => image.getAttribute('src'))
123+
Array.from(host.querySelectorAll(RENDERED_IMAGE)).map((image) => image.getAttribute('src'))
120124
).toEqual(['/api/files/view/uploaded', '/other.png'])
121-
expect(host.querySelector('img')?.getAttribute('alt')).toBe('Original alt')
125+
expect(host.querySelector(RENDERED_IMAGE)?.getAttribute('alt')).toBe('Original alt')
122126
expect(owner.getMarkdown()).toContain('width="123"')
123127
expect(owner.getMarkdown()).not.toContain('/inline?')
124128
expect(() => owner.state.doc.check()).not.toThrow()
@@ -151,7 +155,7 @@ describe('field upload completion boundary', () => {
151155
expect(upload).not.toHaveBeenCalled()
152156
expect(host.querySelector('h2')?.textContent).toBe('Caption')
153157
expect(owner.state.doc.textContent).toContain('Tail')
154-
expect(Array.from(host.querySelectorAll('img')).map((image) => image.alt)).toEqual([
158+
expect(Array.from(host.querySelectorAll<HTMLImageElement>(RENDERED_IMAGE)).map((image) => image.alt)).toEqual([
155159
'One',
156160
'Two',
157161
])

‎apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/image-input-rule.test.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ describe.each([
217217
expect(editor.getText()).toBe(`Prefix ${source}\n\nAfter`)
218218
})
219219

220-
it('undoes a collaborative conversion without removing peer text', () => {
220+
it('undoes a collaborative conversion to its source, then the input, without removing peer text', () => {
221221
const seed = markdownToYDoc('TARGET\n\nAfter')
222222
const a = createPeer(seed)
223223
const b = createPeer(seed)
@@ -232,9 +232,14 @@ describe.each([
232232
Y.applyUpdate(a.doc, Y.encodeStateAsUpdate(b.doc))
233233
expect(a.editor.commands.undo()).toBe(true)
234234
Y.applyUpdate(b.doc, Y.encodeStateAsUpdate(a.doc))
235+
expect(a.editor.getText()).toBe(`Prefix ${source}\n\nPeer text`)
236+
expect(b.editor.getJSON()).toEqual(a.editor.getJSON())
237+
expect(a.editor.commands.undo()).toBe(true)
238+
Y.applyUpdate(b.doc, Y.encodeStateAsUpdate(a.doc))
235239
expect(a.editor.getText()).toBe('TARGET\n\nPeer text')
236240
expect(b.editor.getJSON()).toEqual(a.editor.getJSON())
237241
expect(a.editor.commands.redo()).toBe(true)
242+
expect(a.editor.commands.redo()).toBe(true)
238243
Y.applyUpdate(b.doc, Y.encodeStateAsUpdate(a.doc))
239244
assertConverted(a.editor)
240245
expect(a.editor.getText()).toContain('Peer text')

‎apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/image-schema.ts‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { Plugin } from '@tiptap/pm/state'
55
import { Lexer, Tokenizer } from 'marked'
66
import { imageTypeAt } from '@/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/image-node'
77
import { normalizeLinkHref } from '@/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/markdown-fidelity'
8-
import { createTextInputRulePlugins } from '@/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/text-input-rule'
98

109
/**
1110
* React-free schema half of the image node. Lives apart from {@link ./image} (its React resize node
@@ -187,10 +186,8 @@ const ImageSchema = Image.extend({
187186
})
188187

189188
export const MarkdownImage = ImageSchema.extend({
190-
addProseMirrorPlugins() {
191-
return createTextInputRulePlugins(
192-
this.editor,
193-
this.name,
189+
addInputRules() {
190+
return [
194191
new InputRule({
195192
find: new RegExp(`${inputRegex.source}(?![\\s\\S])`),
196193
handler: ({ state, range, match }) => {
@@ -201,8 +198,8 @@ export const MarkdownImage = ImageSchema.extend({
201198
.replaceRangeWith(from, range.to, type.create({ alt, src, title: title ?? null }))
202199
.scrollIntoView()
203200
},
204-
})
205-
)
201+
}),
202+
]
206203
},
207204
addCommands() {
208205
return {

‎apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/link-input-rule.ts‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Extension, InputRule } from '@tiptap/core'
22
import { normalizeLinkHref } from '@/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/markdown-fidelity'
3-
import { createTextInputRulePlugins } from '@/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/text-input-rule'
43

54
/**
65
* Typed markdown link: `[text](url)` or `[text](url "title")`, completed by the closing `)`. The URL
@@ -20,10 +19,8 @@ const LINK_INPUT_RULE = /(?<!!)\[([^\]]+)]\(([^)\s]+)(?:\s+"([^"]*)")?\)(?![\s\S
2019
export const MarkdownLinkInputRule = Extension.create({
2120
name: 'markdownLinkInputRule',
2221

23-
addProseMirrorPlugins() {
24-
return createTextInputRulePlugins(
25-
this.editor,
26-
this.name,
22+
addInputRules() {
23+
return [
2724
new InputRule({
2825
find: LINK_INPUT_RULE,
2926
handler: ({ state, range, match }) => {
@@ -41,7 +38,7 @@ export const MarkdownLinkInputRule = Extension.create({
4138
tr.addMark(range.from, markEnd, linkType.create({ href, title: title || null }))
4239
tr.removeStoredMark(linkType)
4340
},
44-
})
45-
)
41+
}),
42+
]
4643
},
4744
})

‎apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/list-item.ts‎

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,32 @@
1-
import { ListItem } from '@tiptap/extension-list'
1+
import { type RenderContext, renderNestedMarkdownContent } from '@tiptap/core'
2+
import { getListMarker, ListItem } from '@tiptap/extension-list'
23
import { splitBlockImageParagraph } from '@/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/block-image-paragraph'
34

5+
function listItemMarker(context: RenderContext): string {
6+
if (context.parentType !== 'orderedList') return '- '
7+
const { start, type } = context.meta?.parentAttrs ?? {}
8+
const first = (typeof start === 'number' && start) || 1
9+
return getListMarker(typeof type === 'string' ? type : null, first - 1 + context.index, '. ')
10+
}
11+
412
/**
513
* Markdown items may start with a nested block, but the list-item schema requires a paragraph
614
* first. Supply that empty paragraph before Yjs hydration can discard the invalid subtree.
15+
*
16+
* Nested blocks keep the two-space indent under every marker. Upstream aligns ordered-item
17+
* children to the marker width, but its ordered-list tokenizer strips one column less, so a
18+
* fenced code block inside a numbered item would gain a leading space on every save.
719
*/
820
export const MarkdownListItem = ListItem.extend({
9-
parseMarkdown: (token, helpers) => {
10-
const parsed = ListItem.config.parseMarkdown?.(token, helpers) ?? []
21+
parseMarkdown(token, helpers) {
22+
const parsed = this.parent?.(token, helpers) ?? []
1123
if (Array.isArray(parsed)) return parsed
1224
const content = (parsed.content ?? []).flatMap(splitBlockImageParagraph)
1325
if (content[0]?.type !== 'paragraph') content.unshift({ type: 'paragraph' })
1426
return { ...parsed, content }
1527
},
1628
renderMarkdown: (node, helpers, context) => {
17-
const rendered = ListItem.config.renderMarkdown?.(node, helpers, context) ?? ''
29+
const rendered = renderNestedMarkdownContent(node, helpers, listItemMarker(context))
1830
/** Marked misreads an opening empty `- ` line as prose when it has a trailing space. */
1931
return rendered.replace(/^-[ \t]+(?=\n|$)/, '-')
2032
},

‎apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/ordered-list.ts‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,15 @@ import { excludeTableBlockInputRules } from '@/app/workspace/[workspaceId]/files
1414
*/
1515
export function createJoiningOrderedList() {
1616
const delimiters = new WeakMap<JSONContent, '.' | ')'>()
17-
const parse = OrderedList.config.parseMarkdown
1817

1918
return OrderedList.extend({
2019
addInputRules() {
2120
return excludeTableBlockInputRules(
2221
joinListInputRules(this.parent?.() ?? [], this.type, { compatible: orderedListContinues })
2322
)
2423
},
25-
parseMarkdown: (token, helpers) => {
26-
if (!token.ordered || !token.items?.length) return parse?.(token, helpers) ?? []
24+
parseMarkdown(token, helpers) {
25+
if (!token.ordered || !token.items?.length) return this.parent?.(token, helpers) ?? []
2726

2827
/**
2928
* The upstream tokenizer retains each item's raw marker but ignores delimiter changes.
@@ -61,7 +60,7 @@ export function createJoiningOrderedList() {
6160
),
6261
})),
6362
}
64-
const parsedList = parse?.(resolved, helpers) ?? []
63+
const parsedList = this.parent?.(resolved, helpers) ?? []
6564
if (Array.isArray(parsedList)) return parsedList
6665
/** Use the registered item parser so empty items retain their schema-required paragraph. */
6766
const parsed = { ...parsedList, content: helpers.parseChildren(resolved.items) }

‎apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/round-trip-safety.test.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,13 @@ describe('isRoundTripSafe', () => {
167167
})
168168

169169
it('rejects non-idempotent churn', () => {
170-
expect(isRoundTripSafe('render `` a`b `` inline')).toBe(false)
170+
expect(isRoundTripSafe('10. ten\n ```\n code\n ```')).toBe(false)
171+
})
172+
173+
it('keeps fenced code inside a numbered item editable and byte-stable', () => {
174+
const source = '1. step\n ```\n code\n ```\n2. next\n'
175+
expect(serializeMarkdownDocument(source)).toBe(source)
176+
expect(isRoundTripSafe(source)).toBe(true)
171177
})
172178

173179
it('keeps HTML images with unsupported attributes in source mode', () => {

‎apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/text-input-rule.ts‎

Lines changed: 0 additions & 41 deletions
This file was deleted.

‎apps/sim/package.json‎

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -142,23 +142,23 @@
142142
"@t3-oss/env-nextjs": "0.13.11",
143143
"@tanstack/react-query": "5.90.8",
144144
"@tanstack/react-virtual": "3.13.24",
145-
"@tiptap/core": "3.30.5",
146-
"@tiptap/extension-code": "3.30.5",
147-
"@tiptap/extension-code-block": "3.30.5",
148-
"@tiptap/extension-collaboration": "3.30.5",
149-
"@tiptap/extension-collaboration-caret": "3.30.5",
150-
"@tiptap/extension-document": "3.30.5",
151-
"@tiptap/extension-hard-break": "3.30.5",
152-
"@tiptap/extension-image": "3.30.5",
153-
"@tiptap/extension-list": "3.30.5",
154-
"@tiptap/extension-paragraph": "3.30.5",
155-
"@tiptap/extension-placeholder": "3.30.5",
156-
"@tiptap/extension-table": "3.30.5",
157-
"@tiptap/markdown": "3.30.5",
158-
"@tiptap/pm": "3.30.5",
159-
"@tiptap/react": "3.30.5",
160-
"@tiptap/starter-kit": "3.30.5",
161-
"@tiptap/suggestion": "3.30.5",
145+
"@tiptap/core": "3.31.4",
146+
"@tiptap/extension-code": "3.31.4",
147+
"@tiptap/extension-code-block": "3.31.4",
148+
"@tiptap/extension-collaboration": "3.31.4",
149+
"@tiptap/extension-collaboration-caret": "3.31.4",
150+
"@tiptap/extension-document": "3.31.4",
151+
"@tiptap/extension-hard-break": "3.31.4",
152+
"@tiptap/extension-image": "3.31.4",
153+
"@tiptap/extension-list": "3.31.4",
154+
"@tiptap/extension-paragraph": "3.31.4",
155+
"@tiptap/extension-placeholder": "3.31.4",
156+
"@tiptap/extension-table": "3.31.4",
157+
"@tiptap/markdown": "3.31.4",
158+
"@tiptap/pm": "3.31.4",
159+
"@tiptap/react": "3.31.4",
160+
"@tiptap/starter-kit": "3.31.4",
161+
"@tiptap/suggestion": "3.31.4",
162162
"@tiptap/y-tiptap": "3.0.7",
163163
"@trigger.dev/core": "4.5.16",
164164
"@trigger.dev/sdk": "4.5.16",

0 commit comments

Comments
 (0)