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);