Skip to content

Commit cc26683

Browse files
committed
fix(rich-markdown-editor): keep bare URLs and autolinks bare on serialize
The normalizing serializer rewrote a bare URL or <url>/<email> autolink to [url](url) / [a@b.com](mailto:a@b.com) on every save, churning every README's links. postProcessSerializedMarkdown now collapses a link back to its bare form when the visible text already equals the destination (a plain http(s) URL, or an email behind mailto:) — GFM re-autolinks it, so the round-trip is identical with a far quieter diff. Titled links, explicit links, and any link inside a fenced/inline code region are left untouched. Idempotent.
1 parent 7bcacc7 commit cc26683

2 files changed

Lines changed: 65 additions & 1 deletion

File tree

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

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,33 @@ const BOM = '\uFEFF'
88
const FRONTMATTER_REGEX = /^---\r?\n(?:[\s\S]*?\r?\n)?---[ \t]*(?:\r?\n)*/
99
const ESCAPED_CALLOUT_REGEX = /^(\s*>(?:\s*>)*\s*)\\\[!([A-Za-z]+)\\\]/gm
1010

11+
/**
12+
* Alternates a code region (fenced block or inline span \u2014 never rewritten) with an inline link whose
13+
* destination has no title and isn't angle-bracketed. The code branch is listed first so a link inside
14+
* code is consumed as code and left untouched. The destination stops at `)` / whitespace, so a link
15+
* carrying a title (`[x](url "t")`) never matches and is preserved verbatim.
16+
*/
17+
const CODE_OR_PLAIN_LINK_REGEX =
18+
/(```[\s\S]*?```|~~~[\s\S]*?~~~|`[^`\n]+`)|\[([^\]]+)]\(([^)\s<>]+)\)/g
19+
const HTTP_URL_REGEX = /^https?:\/\/\S+$/i
20+
21+
/**
22+
* Collapses an autolinked destination back to its bare form: our normalizing serializer rewrites a bare
23+
* URL or `<url>` autolink to `[url](url)` and a bare email to `[a@b.com](mailto:a@b.com)`, which churns
24+
* every README's links into explicit-link syntax on the first save. When the visible text already equals
25+
* the destination (a plain `http(s)` URL, or an email behind `mailto:`), GFM re-autolinks the bare form,
26+
* so emitting it round-trips identically with a far quieter diff. Links inside code and titled links are
27+
* left untouched (see {@link CODE_OR_PLAIN_LINK_REGEX}).
28+
*/
29+
function collapseAutolinkedUrls(markdown: string): string {
30+
return markdown.replace(CODE_OR_PLAIN_LINK_REGEX, (match, code, text, href) => {
31+
if (code) return code
32+
if (text === href && HTTP_URL_REGEX.test(href)) return href
33+
if (href === `mailto:${text}`) return text
34+
return match
35+
})
36+
}
37+
1138
export interface SplitMarkdown {
1239
/** Out-of-band leading prefix (a BOM and/or the frontmatter block), byte-exact, or `''`. */
1340
frontmatter: string
@@ -87,5 +114,8 @@ export function normalizeLinkHref(href: string): string {
87114
* begins with whitespace.
88115
*/
89116
export function postProcessSerializedMarkdown(markdown: string): string {
90-
return markdown.replace(ESCAPED_CALLOUT_REGEX, '$1[!$2]').replace(/\n+$/, '\n')
117+
return collapseAutolinkedUrls(markdown.replace(ESCAPED_CALLOUT_REGEX, '$1[!$2]')).replace(
118+
/\n+$/,
119+
'\n'
120+
)
91121
}

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,3 +501,37 @@ describe('highlight ==mark==', () => {
501501
expect(markPresent('a == b == c')).toBe(false)
502502
})
503503
})
504+
505+
describe('autolink / bare-URL preservation', () => {
506+
it('keeps a bare URL bare instead of rewriting it to [url](url)', () => {
507+
expect(roundTrip('visit https://sim.ai today').trim()).toBe('visit https://sim.ai today')
508+
expect(roundTrip('both https://a.com and https://b.com').trim()).toBe(
509+
'both https://a.com and https://b.com'
510+
)
511+
})
512+
513+
it('collapses an angle autolink and a bare email to their bare form', () => {
514+
expect(roundTrip('see <https://sim.ai> here').trim()).toBe('see https://sim.ai here')
515+
expect(roundTrip('mail <a@b.com> now').trim()).toBe('mail a@b.com now')
516+
})
517+
518+
it('preserves explicit and titled links (only bare autolinks collapse)', () => {
519+
expect(roundTrip('[Sim](https://sim.ai)').trim()).toBe('[Sim](https://sim.ai)')
520+
expect(roundTrip('[https://a.com](https://b.com)').trim()).toBe(
521+
'[https://a.com](https://b.com)'
522+
)
523+
expect(roundTrip('[text](https://x.com "t")').trim()).toBe('[text](https://x.com "t")')
524+
})
525+
526+
it('never collapses a URL that only appears inside code', () => {
527+
expect(roundTrip('```\nvisit https://sim.ai\n```').trim()).toBe(
528+
'```\nvisit https://sim.ai\n```'
529+
)
530+
expect(roundTrip('inline `https://sim.ai` code').trim()).toBe('inline `https://sim.ai` code')
531+
})
532+
533+
it('round-trips a bare URL idempotently', () => {
534+
const once = roundTrip('go to https://sim.ai now')
535+
expect(roundTrip(once)).toBe(once)
536+
})
537+
})

0 commit comments

Comments
 (0)