@igor.dvlpr/astro-post-excerpt
Version:
⭐ An Astro component that renders post excerpts for your Astro blog - directly from your Markdown and MDX files. Astro v2+ collections are supported as well! 💎
151 lines (119 loc) • 4.16 kB
text/typescript
import { stripString } from '@igor.dvlpr/strip-yaml-front-matter'
import type { Root, RootContent } from 'mdast'
import { fromMarkdown } from 'mdast-util-from-markdown'
import { mdxFromMarkdown } from 'mdast-util-mdx'
import { mdxjs } from 'micromark-extension-mdxjs'
import { Props } from './Props'
export function isPost(post: any): boolean {
return typeof post['body'] === 'string'
}
function restoreWhitespace(current: string, previous: string): string {
if (previous === '') {
return current
}
if (!current.endsWith(' ') && !previous.startsWith(' ')) {
return `${previous} ${current}`
}
return `${previous}${current}`
}
function walk(node: RootContent | any): string | undefined {
const allowedNodes: string[] = ['emphasis', 'paragraph', 'strong', 'text']
if (!allowedNodes.includes(node.type)) {
return
}
let result: string = ''
if (node.value?.length > 0) {
result += node.value
} else if (node.children?.length > 0) {
node.children.forEach((child: RootContent) => {
if (allowedNodes.includes(child.type)) {
const value: string | undefined = walk(child)
if (value) {
result += value
}
}
})
}
return result
}
export function getPlainText(markdown: string): string {
let result: string = ''
const tree: Root = fromMarkdown(markdown, {
extensions: [mdxjs()],
mdastExtensions: [mdxFromMarkdown()],
})
tree.children.forEach((node: RootContent): void => {
const value: string | undefined = walk(node)
if (value) {
result = restoreWhitespace(value, result)
}
})
return result
}
export function generateExcerpt(props: Props): string {
const APE_ELLIPSIS: string = '…'
const APE_DEF_WORDS: number = 40
props = {
post: props.post,
words: props.words ?? 40,
maxLength: props.maxLength ?? 0,
addEllipsis: props.addEllipsis !== undefined ? props.addEllipsis : true,
smartEllipsis:
props.smartEllipsis !== undefined ? props.smartEllipsis : true,
ellipsis: props.ellipsis !== undefined ? props.ellipsis : '…',
}
if (!props.post || !isPost(props.post)) {
throw new TypeError('The required prop post is not valid, aborting now.')
}
if (typeof props.words !== 'number' || props.words < 0) {
props.words = APE_DEF_WORDS
console.warn(
`The optional prop words is not valid, defaulting to ${APE_DEF_WORDS}.`,
)
}
if (typeof props.maxLength !== 'number' || props.maxLength < 0) {
props.maxLength = 0
console.warn('The optional prop maxLength is not valid, defaulting to 0.')
}
if (typeof props.addEllipsis !== 'boolean') {
props.addEllipsis = true
console.warn(
'The optional prop addEllipsis is not valid, defaulting to true.',
)
}
if (typeof props.smartEllipsis !== 'boolean') {
props.smartEllipsis = true
console.warn(
'The optional prop smartEllipsis is not valid, defaulting to true.',
)
}
if (typeof props.ellipsis !== 'string' || props.ellipsis.length < 1) {
props.ellipsis = APE_ELLIPSIS
console.warn('The optional prop ellipsis is not valid, defaulting to "…".')
}
const punctuationSymbols: string[] = ['.', ',', '?', '!', ';', APE_ELLIPSIS]
let postExcerpt: string = ''
postExcerpt = stripString(props.post['body'])
postExcerpt = getPlainText(postExcerpt)
postExcerpt = postExcerpt.trim()
if (props.words > 0) {
postExcerpt = postExcerpt.split(' ').slice(0, props.words).join(' ')
}
if (props.maxLength > 0) {
postExcerpt = postExcerpt.substring(0, props.maxLength)
}
if (props.addEllipsis) {
const postLength: number = postExcerpt.length
if (postLength > 0) {
if (props.smartEllipsis) {
const lastChar: string | undefined = postExcerpt.at(-1)
if (lastChar && !punctuationSymbols.includes(lastChar)) {
postExcerpt += props.ellipsis
}
} else {
postExcerpt += props.ellipsis
}
}
}
return postExcerpt
}