| | |
| | | import { Root as HTMLRoot } from 'hast' |
| | | import { Root as HTMLRoot } from "hast" |
| | | import { toString } from "hast-util-to-string" |
| | | import { QuartzTransformerPlugin } from "../types" |
| | | import { escapeHTML } from "../../util/escape" |
| | | |
| | | export interface Options { |
| | | descriptionLength: number |
| | | maxDescriptionLength: number |
| | | replaceExternalLinks: boolean |
| | | } |
| | | |
| | | const defaultOptions: Options = { |
| | | descriptionLength: 150 |
| | | descriptionLength: 150, |
| | | maxDescriptionLength: 300, |
| | | replaceExternalLinks: true, |
| | | } |
| | | |
| | | const escapeHTML = (unsafe: string) => { |
| | | return unsafe.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", '''); |
| | | } |
| | | const urlRegex = new RegExp( |
| | | /(https?:\/\/)?(?<domain>([\da-z\.-]+)\.([a-z\.]{2,6})(:\d+)?)(?<path>[\/\w\.-]*)(\?[\/\w\.=&;-]*)?/, |
| | | "g", |
| | | ) |
| | | |
| | | export const Description: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => { |
| | | export const Description: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => { |
| | | const opts = { ...defaultOptions, ...userOpts } |
| | | return { |
| | | name: "Description", |
| | |
| | | return [ |
| | | () => { |
| | | return async (tree: HTMLRoot, file) => { |
| | | const frontMatterDescription = file.data.frontmatter?.description |
| | | const text = escapeHTML(toString(tree)) |
| | | let frontMatterDescription = file.data.frontmatter?.description |
| | | let text = escapeHTML(toString(tree)) |
| | | |
| | | const desc = frontMatterDescription ?? text |
| | | const sentences = desc.replace(/\s+/g, ' ').split('.') |
| | | let finalDesc = "" |
| | | let sentenceIdx = 0 |
| | | const len = opts.descriptionLength |
| | | while (finalDesc.length < len) { |
| | | finalDesc += sentences[sentenceIdx] + '.' |
| | | sentenceIdx++ |
| | | if (opts.replaceExternalLinks) { |
| | | frontMatterDescription = frontMatterDescription?.replace( |
| | | urlRegex, |
| | | "$<domain>" + "$<path>", |
| | | ) |
| | | text = text.replace(urlRegex, "$<domain>" + "$<path>") |
| | | } |
| | | |
| | | file.data.description = finalDesc |
| | | if (frontMatterDescription) { |
| | | file.data.description = frontMatterDescription |
| | | file.data.text = text |
| | | return |
| | | } |
| | | |
| | | // otherwise, use the text content |
| | | const desc = text |
| | | const sentences = desc.replace(/\s+/g, " ").split(/\.\s/) |
| | | let finalDesc = "" |
| | | let sentenceIdx = 0 |
| | | |
| | | // Add full sentences until we exceed the guideline length |
| | | while (sentenceIdx < sentences.length) { |
| | | const sentence = sentences[sentenceIdx] |
| | | if (!sentence) break |
| | | |
| | | const currentSentence = sentence.endsWith(".") ? sentence : sentence + "." |
| | | const nextLength = finalDesc.length + currentSentence.length + (finalDesc ? 1 : 0) |
| | | |
| | | // Add the sentence if we're under the guideline length |
| | | // or if this is the first sentence (always include at least one) |
| | | if (nextLength <= opts.descriptionLength || sentenceIdx === 0) { |
| | | finalDesc += (finalDesc ? " " : "") + currentSentence |
| | | sentenceIdx++ |
| | | } else { |
| | | break |
| | | } |
| | | } |
| | | |
| | | // truncate to max length if necessary |
| | | file.data.description = |
| | | finalDesc.length > opts.maxDescriptionLength |
| | | ? finalDesc.slice(0, opts.maxDescriptionLength) + "..." |
| | | : finalDesc |
| | | file.data.text = text |
| | | } |
| | | } |
| | | }, |
| | | ] |
| | | } |
| | | }, |
| | | } |
| | | } |
| | | |
| | | declare module 'vfile' { |
| | | declare module "vfile" { |
| | | interface DataMap { |
| | | description: string |
| | | text: string |
| | | } |
| | | } |
| | | |