diff --git a/quartz/plugins/transformers/description.test.ts b/quartz/plugins/transformers/description.test.ts new file mode 100644 index 0000000..8f659dc --- /dev/null +++ b/quartz/plugins/transformers/description.test.ts @@ -0,0 +1,109 @@ +import test from "node:test" +import assert from "node:assert/strict" +import { Root } from "hast" +import { extractDescriptionText } from "./description" + +test("extractDescriptionText strips a duplicate top-level h1", () => { + const tree: Root = { + type: "root", + children: [ + { + type: "element", + tagName: "h1", + properties: {}, + children: [{ type: "text", value: "Why Aetheria Went Quiet" }], + }, + { + type: "element", + tagName: "p", + properties: {}, + children: [ + { + type: "text", + value: "Aetheria reached the point every serious ambitious project eventually reaches.", + }, + ], + }, + ], + } + + assert.equal( + extractDescriptionText(tree, "Why Aetheria Went Quiet"), + "Aetheria reached the point every serious ambitious project eventually reaches.", + ) +}) + +test("extractDescriptionText strips a standalone emphasized tagline after the title", () => { + const tree: Root = { + type: "root", + children: [ + { + type: "element", + tagName: "h1", + properties: {}, + children: [{ type: "text", value: "Why Aetheria Went Quiet" }], + }, + { + type: "element", + tagName: "p", + properties: {}, + children: [ + { + type: "element", + tagName: "em", + properties: {}, + children: [ + { type: "text", value: '"The project did not vanish because the idea was small."' }, + ], + }, + ], + }, + { + type: "element", + tagName: "p", + properties: {}, + children: [ + { + type: "text", + value: "Aetheria reached the point every serious ambitious project eventually reaches.", + }, + ], + }, + ], + } + + assert.equal( + extractDescriptionText(tree, "Why Aetheria Went Quiet"), + "Aetheria reached the point every serious ambitious project eventually reaches.", + ) +}) + +test("extractDescriptionText keeps the first paragraph when it is normal content", () => { + const tree: Root = { + type: "root", + children: [ + { + type: "element", + tagName: "h1", + properties: {}, + children: [{ type: "text", value: "Why Aetheria Went Quiet" }], + }, + { + type: "element", + tagName: "p", + properties: {}, + children: [ + { + type: "text", + value: "This first paragraph is the actual summary, not decorative throat-clearing.", + }, + ], + }, + ], + } + + assert.equal( + extractDescriptionText(tree, "Why Aetheria Went Quiet"), + "This first paragraph is the actual summary, not decorative throat-clearing.", + ) +}) diff --git a/quartz/plugins/transformers/description.ts b/quartz/plugins/transformers/description.ts index 3f8519b..c6c1576 100644 --- a/quartz/plugins/transformers/description.ts +++ b/quartz/plugins/transformers/description.ts @@ -1,4 +1,4 @@ -import { Root as HTMLRoot } from "hast" +import { Element, Root as HTMLRoot, RootContent } from "hast" import { toString } from "hast-util-to-string" import { QuartzTransformerPlugin } from "../types" import { escapeHTML } from "../../util/escape" @@ -20,6 +20,83 @@ const urlRegex = new RegExp( "g", ) +function normalizeWhitespace(text: string): string { + return text.replace(/\s+/g, " ").trim() +} + +function isElementNode(node: RootContent): node is Element { + return node.type === "element" +} + +function isMeaningfulNode(node: RootContent): boolean { + return normalizeWhitespace(toString(node)).length > 0 +} + +function isDuplicateTitleHeading(node: RootContent, pageTitle?: string): boolean { + if (!isElementNode(node) || node.tagName !== "h1") { + return false + } + + const headingText = normalizeWhitespace(toString(node)) + const normalizedTitle = normalizeWhitespace(pageTitle ?? "") + + if (headingText.length === 0) { + return false + } + + return normalizedTitle.length > 0 ? headingText === normalizedTitle : true +} + +function isStandalonePreviewTagline(node: RootContent): boolean { + if (!isElementNode(node) || node.tagName !== "p") { + return false + } + + const meaningfulChildren = node.children.filter( + (child) => !(child.type === "text" && normalizeWhitespace(child.value).length === 0), + ) + + if (meaningfulChildren.length !== 1) { + return false + } + + const onlyChild = meaningfulChildren[0] + return isElementNode(onlyChild) && ["em", "strong"].includes(onlyChild.tagName) +} + +export function extractDescriptionText(tree: HTMLRoot, pageTitle?: string): string { + const children = [...tree.children] + let startIdx = 0 + let strippedTitle = false + + while (startIdx < children.length && !isMeaningfulNode(children[startIdx])) { + startIdx++ + } + + if (startIdx < children.length && isDuplicateTitleHeading(children[startIdx], pageTitle)) { + strippedTitle = true + startIdx++ + } + + while (startIdx < children.length && !isMeaningfulNode(children[startIdx])) { + startIdx++ + } + + if ( + startIdx < children.length && + (strippedTitle || pageTitle) && + isStandalonePreviewTagline(children[startIdx]) + ) { + startIdx++ + } + + while (startIdx < children.length && !isMeaningfulNode(children[startIdx])) { + startIdx++ + } + + return escapeHTML(toString({ ...tree, children: children.slice(startIdx) })) +} + export const Description: QuartzTransformerPlugin> = (userOpts) => { const opts = { ...defaultOptions, ...userOpts } return { @@ -29,7 +106,7 @@ export const Description: QuartzTransformerPlugin> = (userOpts) () => { return async (tree: HTMLRoot, file) => { let frontMatterDescription = file.data.frontmatter?.description - let text = escapeHTML(toString(tree)) + let text = extractDescriptionText(tree, file.data.frontmatter?.title) if (opts.replaceExternalLinks) { frontMatterDescription = frontMatterDescription?.replace(