import matter from '@gr2m/gray-matter' import { validateJson } from '@/tests/lib/validate-json-schema' interface ReadFrontmatterOptions { schema?: Record // Schema can have arbitrary properties for validation filepath?: string | null } function readFrontmatter(markdown: string, opts: ReadFrontmatterOptions = {}) { const schema = opts.schema || { type: 'object', properties: {} } const filepath = opts.filepath || null let content let data try { ;({ content, data } = matter(markdown)) } catch (e: any) { const defaultReason = 'invalid frontmatter entry' const reason = e.reason ? // make this common error message a little easier to understand e.reason.startsWith('can not read a block mapping entry;') || e.reason === 'bad indentation of a mapping entry' ? defaultReason : e.reason : defaultReason const error: any = { reason, message: 'YML parsing error!', } if (filepath) error.filepath = filepath const errors = [error] console.warn(errors) return { errors } } const validate: any = validateJson(schema, data) // Combine the AJV-supplied `instancePath` and `params` into a more user-friendly frontmatter path. // For example, given: // "instancePath": "/versions", // "params": { "additionalProperty": "ftp" } // return: // property: 'versions.ftp' // // The purpose is to help users understand that the error is on the `fpt` key within the `versions` object. // Note if the error is on a top-level FM property like `title`, the `instancePath` will be empty. const cleanPropertyPath = (params: Record, instancePath: string) => { const mainProps = Object.values(params)[0] if (!instancePath) return mainProps const prefixProps = instancePath.replace('/', '').replace(/\//g, '.') return typeof mainProps !== 'object' ? `${prefixProps}.${mainProps}` : prefixProps } const errors = [] if (!validate.isValid && filepath) { const formattedErrors = validate.errors.map((error: any) => { const userFriendly: any = {} userFriendly.property = cleanPropertyPath(error.params, error.instancePath) userFriendly.message = error.message userFriendly.reason = error.keyword userFriendly.filepath = filepath return userFriendly }) errors.push(...formattedErrors) } else if (!validate.isValid) { errors.push(...validate.errors) } return { content, data, errors } } // Expose gray-matter's underlying stringify method for joining a parsed // frontmatter object and a markdown string back into a unified string // // stringify('some string', {some: 'frontmatter'}) readFrontmatter.stringify = matter.stringify export default readFrontmatter