From 7bef827b8e3439dee48572a514b15042d85cc401 Mon Sep 17 00:00:00 2001 From: Julien Calixte Date: Sat, 1 Aug 2026 13:33:44 +0000 Subject: [PATCH] fix: preserve ATProto embed links across markdown processing Audio/at:// embeds (![alt](at://...)) were being stripped or misprocessed in three places: stripMarkdownForText mangled them like regular images, sync's PDS comparison stripped them and could miss changed recordings (skipping republish), and processImages tried to treat them as local files to upload. Now all three preserve at:// embeds verbatim. --- packages/cli/src/lib/markdown.test.ts | 10 ++++++++++ packages/cli/src/lib/markdown.ts | 17 ++++++++++++++++- packages/remanso-cli/src/commands/sync.ts | 13 ++++++++++++- packages/remanso-cli/src/lib/note.ts | 3 +++ 4 files changed, 41 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/lib/markdown.test.ts b/packages/cli/src/lib/markdown.test.ts index 2089a87..da9420d 100644 --- a/packages/cli/src/lib/markdown.test.ts +++ b/packages/cli/src/lib/markdown.test.ts @@ -414,6 +414,16 @@ describe("stripMarkdownForText", () => { ); }); + test("preserves ATProto embeds (e.g. audio) verbatim", () => { + const embed = + "![Accounting for Slavery - audio](at://did:plc:4m3kouplb7s7xozjd3whinvl/space.remanso.recording/3mrzjmp2nit22)"; + expect(stripMarkdownForText(`before\n\n${embed}\n\nafter`)).toBe( + `before\n\n${embed}\n\nafter`, + ); + // Also survives when the embed is at the start of the content (trim edge) + expect(stripMarkdownForText(`${embed} tail`)).toBe(`${embed} tail`); + }); + test("removes code blocks", () => { const input = "Before\n```js\nconst x = 1;\n```\nAfter"; expect(stripMarkdownForText(input)).toContain("Before"); diff --git a/packages/cli/src/lib/markdown.ts b/packages/cli/src/lib/markdown.ts index 5b7d8ba..d2d4f3f 100644 --- a/packages/cli/src/lib/markdown.ts +++ b/packages/cli/src/lib/markdown.ts @@ -372,7 +372,19 @@ export function removeFrontmatterAtUri(rawContent: string): string { } export function stripMarkdownForText(markdown: string): string { - return markdown + // Preserve ATProto embeds (e.g. audio recordings) verbatim so the Remanso + // app can resolve the blob. These use ![alt](at://…) syntax and would + // otherwise be mangled by the link/image passes below. + const embeds: string[] = []; + const withPlaceholders = markdown.replace( + /!\[[^\]]*\]\(at:\/\/[^)]*\)/g, + (match) => { + embeds.push(match); + return `${embeds.length - 1}`; + }, + ); + + const stripped = withPlaceholders .replace(/#{1,6}\s/g, "") // Remove headers .replace(/\*\*([^*]+)\*\*/g, "$1") // Remove bold .replace(/\*([^*]+)\*/g, "$1") // Remove italic @@ -382,6 +394,9 @@ export function stripMarkdownForText(markdown: string): string { .replace(/!\[.*?\]\(.*?\)/g, "") // Remove images .replace(/\n{3,}/g, "\n\n") // Normalize multiple newlines .trim(); + + // Restore preserved embeds + return stripped.replace(/(\d+)/g, (_, i) => embeds[Number(i)]!); } export function getTextContent( diff --git a/packages/remanso-cli/src/commands/sync.ts b/packages/remanso-cli/src/commands/sync.ts index ae2c981..fd6342f 100644 --- a/packages/remanso-cli/src/commands/sync.ts +++ b/packages/remanso-cli/src/commands/sync.ts @@ -28,10 +28,21 @@ import process from "node:process"; // PDS note content has image paths replaced with blob CIDs and internal markdown // links rewritten to AT URIs (see lib/note.ts processNoteContent). Strip both // constructs from each side so the comparison is symmetric. +// +// ATProto embeds (e.g. audio recordings, ![alt](at://…)) are NOT rewritten by +// the publish path — they stay identical on both sides — so preserve them. +// Stripping them would hide a changed/added/removed recording and let sync mark +// the note "unchanged", skipping the republish. function normalizeNoteBodyForCompare(s: string): string { + const embeds: string[] = []; return s + .replace(/!\[[^\]]*\]\(at:\/\/[^)]*\)/g, (match) => { + embeds.push(match); + return `\x00${embeds.length - 1}\x00`; + }) .replace(/!\[[^\]]*\]\([^)]*\)/g, "") - .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1"); + .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") + .replace(/\x00(\d+)\x00/g, (_, i) => embeds[Number(i)]!); } async function matchesPDS( diff --git a/packages/remanso-cli/src/lib/note.ts b/packages/remanso-cli/src/lib/note.ts index e680d93..d3ab80b 100644 --- a/packages/remanso-cli/src/lib/note.ts +++ b/packages/remanso-cli/src/lib/note.ts @@ -111,6 +111,9 @@ async function processImages( const fullMatch = match[0]; const alt = match[1] ?? ""; const src = match[2]!; + // ATProto embeds (e.g. audio recordings, at://…) are not files to upload; + // leave them verbatim so the Remanso app can resolve the blob. + if (src.startsWith("at://")) continue; if (!isLocalPath(src)) continue; let blob = uploadCache.get(src); -- 2.51.2