Clean preview description extraction
This commit is contained in:
parent
a7b1be50fc
commit
fdbc4ffb35
2 changed files with 188 additions and 2 deletions
109
quartz/plugins/transformers/description.test.ts
Normal file
109
quartz/plugins/transformers/description.test.ts
Normal file
|
|
@ -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.",
|
||||
)
|
||||
})
|
||||
|
|
@ -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<Partial<Options>> = (userOpts) => {
|
||||
const opts = { ...defaultOptions, ...userOpts }
|
||||
return {
|
||||
|
|
@ -29,7 +106,7 @@ export const Description: QuartzTransformerPlugin<Partial<Options>> = (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(
|
||||
|
|
|
|||
Loading…
Reference in a new issue