diff --git a/bun.lock b/bun.lock index 8a2170d..e9522e5 100644 --- a/bun.lock +++ b/bun.lock @@ -50,6 +50,7 @@ "remark-gfm": "^4.0.1", "smol-toml": "^1.6.0", "strip-markdown": "^6.0.0", + "unist-util-visit": "^5.1.0", }, "devDependencies": { "@biomejs/biome": "2.3.6", diff --git a/packages/cli/package.json b/packages/cli/package.json index 51436a2..bac4504 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -44,6 +44,7 @@ "remark": "^15.0.1", "remark-gfm": "^4.0.1", "smol-toml": "^1.6.0", - "strip-markdown": "^6.0.0" + "strip-markdown": "^6.0.0", + "unist-util-visit": "^5.1.0" } } diff --git a/packages/cli/src/lib/markdown.ts b/packages/cli/src/lib/markdown.ts index 969e415..55660e9 100644 --- a/packages/cli/src/lib/markdown.ts +++ b/packages/cli/src/lib/markdown.ts @@ -8,6 +8,7 @@ import type { BlogPost, FrontmatterMapping, PostFrontmatter } from "./types"; import { resolveImagePath } from "./atproto"; import { remark } from "remark"; import gfm from "remark-gfm"; +import html from "./remark-html"; import strip from "strip-markdown"; export function parseFrontmatter( @@ -391,6 +392,13 @@ export function updateFrontmatterWithAtUri( } export async function stripMarkdownForText(markdown: string): Promise { - const rawContent = await remark().use(gfm).use(strip).process(markdown); + const rawContent = await remark() + // Process GitHub-flavored markdown. + .use(gfm) + // Process remaining HTML into markdown. + .use(html) + // Strip markdown. + .use(strip) + .process(markdown); return String(rawContent).trim(); } diff --git a/packages/cli/src/lib/remark-html.ts b/packages/cli/src/lib/remark-html.ts new file mode 100644 index 0000000..a56ec50 --- /dev/null +++ b/packages/cli/src/lib/remark-html.ts @@ -0,0 +1,67 @@ +import { remark } from "remark"; +import gfm from "remark-gfm"; +import { visit } from "unist-util-visit"; + +interface MarkdownNode { + type: string; + value?: string; + children?: MarkdownNode[]; +} + +type MarkdownParent = MarkdownNode & { children: MarkdownNode[] }; +type MarkdownRoot = MarkdownNode & { type: "root"; children: MarkdownNode[] }; +type HtmlNode = MarkdownNode & { type: "html"; value: string }; + +const markdownProcessor = remark().use(gfm); + +export default function remarkHtml() { + return async function transformer(tree: MarkdownRoot): Promise { + const htmlNodes: Array<{ + parent: MarkdownParent; + index: number; + value: string; + }> = []; + const replacementsByParent = new Map< + MarkdownParent, + Array<{ index: number; nodes: MarkdownNode[] }> + >(); + + visit(tree, "html", (node, index, parent) => { + if ( + typeof index !== "number" || + !parent || + !Array.isArray((parent as MarkdownParent).children) || + typeof (node as HtmlNode).value !== "string" + ) { + return; + } + + htmlNodes.push({ + parent: parent as MarkdownParent, + index, + value: (node as HtmlNode).value, + }); + }); + + for (const htmlNode of htmlNodes) { + const normalizedValue = htmlNode.value + .replace(//g, "") + .replace(/<[^>]+>/g, ""); + const file = await markdownProcessor.process(normalizedValue); + const entries = replacementsByParent.get(htmlNode.parent) ?? []; + entries.push({ + index: htmlNode.index, + nodes: (markdownProcessor.parse(String(file)) as MarkdownRoot).children, + }); + replacementsByParent.set(htmlNode.parent, entries); + } + + // Sort the final output in the order it was read, which may not match the queue. + for (const [parent, entries] of replacementsByParent) { + entries.sort((left, right) => right.index - left.index); + for (const entry of entries) { + parent.children.splice(entry.index, 1, ...entry.nodes); + } + } + }; +} diff --git a/packages/cli/test/markdown.test.ts b/packages/cli/test/markdown.test.ts index f69ff8a..63ca841 100644 --- a/packages/cli/test/markdown.test.ts +++ b/packages/cli/test/markdown.test.ts @@ -521,15 +521,15 @@ describe("stripMarkdownForText", () => { expect(await stripMarkdownForText("\n\nhello\n\n")).toBe("hello"); }); - it("strips HTML figures", async () => { + it("keeps HTML figure captions", async () => { const input = `# Title _Emphasized text._
Figure caption: -- One -- Two +- Foo +- Bar
@@ -541,12 +541,36 @@ List: Emphasized text. +Figure caption: + +Foo + +Bar + List: One Two`); }); + + it("keeps nested HTML DIV content", async () => { + const input = `# Title +_Emphasized text._ + +
+

Text in a DIV.

+
+ +Footer`; + expect(await stripMarkdownForText(input)).toBe(`Title + +Emphasized text. + +Text in a DIV. + +Footer`); + }); }); describe("updateFrontmatterWithAtUri", () => {