Skip to content

Commit 69c271f

Browse files
committed
fix(rich-editor): escape bracketed mention labels + disable images in field editors
- Escape/unescape `[`/`]` in mention labels so an entity named e.g. `data[1].csv` round-trips into a chip instead of degrading to a plain link - Hide the `/Image` command where image upload isn't wired (the skill + version description field editors), so images can't be inserted there; the file viewer keeps image support
1 parent b36d031 commit 69c271f

5 files changed

Lines changed: 56 additions & 10 deletions

File tree

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention-node.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ describe('mention node round-trip', () => {
3636
}
3737
})
3838

39+
it('round-trips a label containing brackets (e.g. a bracketed file name) as a chip', () => {
40+
const input = '[data\\[1\\].csv](sim:file/abc)'
41+
const doc = parseMarkdownToDoc(input)
42+
const mention = findMention(doc)
43+
expect(mention?.attrs).toEqual({ kind: 'file', id: 'abc', label: 'data[1].csv' })
44+
expect(serializeMarkdownBody(input).trim()).toBe(input)
45+
})
46+
3947
it('leaves a normal http link as a link, not a mention', () => {
4048
const doc = parseMarkdownToDoc('[Sim](https://sim.ai)')
4149
expect(findMention(doc)).toBeNull()

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention-node.tsx

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,22 @@ interface MentionAttrs {
1515
label: string
1616
}
1717

18-
/** The markdown form of a mention — the chat's portable `[label](sim:<kind>/<id>)` link. */
19-
const MENTION_MD_RE = /^\[([^\]]+)\]\(sim:([a-z_]+)\/([^)\s]+)\)/
18+
/**
19+
* The markdown form of a mention — the chat's portable `[label](sim:<kind>/<id>)` link. The label
20+
* group accepts backslash-escaped characters so a label containing `[`/`]` (e.g. a file named
21+
* `data[1].csv`) still round-trips into a chip instead of degrading to a plain link.
22+
*/
23+
const MENTION_MD_RE = /^\[((?:\\.|[^\]\\])+)\]\(sim:([a-z_]+)\/([^)\s]+)\)/
24+
25+
/** Escape `\`, `[`, `]` in a mention label so brackets in entity names can't break the link syntax. */
26+
function escapeLabel(label: string): string {
27+
return label.replace(/[\\[\]]/g, '\\$&')
28+
}
29+
30+
/** Inverse of {@link escapeLabel}, applied when parsing a mention back from markdown. */
31+
function unescapeLabel(label: string): string {
32+
return label.replace(/\\([\\[\]])/g, '$1')
33+
}
2034

2135
/** Custom fields the mention tokenizer hangs on the marked token (all optional, like the image token). */
2236
interface MentionTokenFields {
@@ -78,11 +92,14 @@ export const MarkdownMention = Node.create({
7892
},
7993
parseMarkdown: (token: MarkdownToken): JSONContent => {
8094
const { kind, id, label } = token as MentionTokenFields
81-
return { type: 'mention', attrs: { kind: kind ?? '', id: id ?? '', label: label ?? '' } }
95+
return {
96+
type: 'mention',
97+
attrs: { kind: kind ?? '', id: id ?? '', label: unescapeLabel(label ?? '') },
98+
}
8299
},
83100
renderMarkdown: (node: JSONContent): string => {
84101
const { kind, id, label } = (node.attrs ?? {}) as MentionAttrs
85-
return `[${label}](${toSimHref(kind, id)})`
102+
return `[${escapeLabel(label)}](${toSimHref(kind, id)})`
86103
},
87104
})
88105

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/slash-command/commands.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@ describe('filterSlashCommands', () => {
3232
it('returns empty for no match', () => {
3333
expect(filterSlashCommands('zzz')).toEqual([])
3434
})
35+
36+
it('drops the Image command when image insertion is disallowed', () => {
37+
expect(filterSlashCommands('', { allowImages: false }).map((c) => c.title)).not.toContain(
38+
'Image'
39+
)
40+
expect(filterSlashCommands('image', { allowImages: false })).toEqual([])
41+
expect(filterSlashCommands('image', { allowImages: true }).map((c) => c.title)).toContain(
42+
'Image'
43+
)
44+
})
3545
})
3646

3747
describe('SLASH_COMMANDS registry', () => {

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/slash-command/commands.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -156,13 +156,19 @@ export const SLASH_COMMANDS: readonly SlashCommandItem[] = [
156156
]
157157

158158
/**
159-
* Filters commands by a case-insensitive match against title or aliases. Order is
160-
* preserved so the menu stays stable as the query narrows.
159+
* Filters commands by a case-insensitive match against title or aliases. Order is preserved so the
160+
* menu stays stable as the query narrows. The Image command is dropped when image insertion isn't
161+
* available (`allowImages: false`) — e.g. the modal field editors, which have no upload affordance.
161162
*/
162-
export function filterSlashCommands(query: string): SlashCommandItem[] {
163+
export function filterSlashCommands(
164+
query: string,
165+
options?: { allowImages?: boolean }
166+
): SlashCommandItem[] {
167+
const allowImages = options?.allowImages ?? true
168+
const available = allowImages ? SLASH_COMMANDS : SLASH_COMMANDS.filter((c) => c.title !== 'Image')
163169
const q = query.trim().toLowerCase()
164-
if (!q) return [...SLASH_COMMANDS]
165-
return SLASH_COMMANDS.filter(
170+
if (!q) return [...available]
171+
return available.filter(
166172
(command) =>
167173
command.title.toLowerCase().includes(q) || command.aliases.some((alias) => alias.includes(q))
168174
)

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,12 @@ export const SlashCommand = Extension.create<Record<string, never>, SlashCommand
4646
if ($from.parentOffset === 0) return true
4747
return /\s/.test($from.parent.textBetween($from.parentOffset - 1, $from.parentOffset))
4848
},
49-
items: ({ query }) => filterSlashCommands(query),
49+
// The Image command is offered only where image upload is wired (the file viewer); the modal
50+
// field editors never set `insertImage`, so `@`-style image insertion is hidden there.
51+
items: ({ editor, query }) =>
52+
filterSlashCommands(query, {
53+
allowImages: editor.storage.slashCommand.insertImage != null,
54+
}),
5055
command: ({ editor, range, props }) => {
5156
const ctx: SlashCommandContext = { editor, range }
5257
props.run(ctx)

0 commit comments

Comments
 (0)