diff --git a/src/node/markdown/markdown.ts b/src/node/markdown/markdown.ts index eeb90ced..f13e7949 100644 --- a/src/node/markdown/markdown.ts +++ b/src/node/markdown/markdown.ts @@ -4,10 +4,7 @@ import { componentPlugin, type ComponentPluginOptions } from '@mdit-vue/plugin-component' -import { - frontmatterPlugin, - type FrontmatterPluginOptions -} from '@mdit-vue/plugin-frontmatter' +import type { FrontmatterPluginOptions } from '@mdit-vue/plugin-frontmatter' import { headersPlugin, type HeadersPluginOptions @@ -52,6 +49,7 @@ import { type ContainerOptions } from './plugins/containers' import { eagerFrontmatterInterpolationPlugin } from './plugins/eagerFrontmatterInterpolation' +import { frontmatterPlugin } from './plugins/frontmatter' import { highlight as createHighlighter } from './plugins/highlight' import { imagePlugin, type Options as ImageOptions } from './plugins/image' import { diff --git a/src/node/markdown/plugins/frontmatter.ts b/src/node/markdown/plugins/frontmatter.ts new file mode 100644 index 00000000..6f122882 --- /dev/null +++ b/src/node/markdown/plugins/frontmatter.ts @@ -0,0 +1,39 @@ +import type { FrontmatterPluginOptions } from '@mdit-vue/plugin-frontmatter' +import matter from 'gray-matter' +import type { MarkdownItAsync } from 'markdown-it-async' + +/** + * Vendored from `@mdit-vue/plugin-frontmatter` (extracts `env.frontmatter`, + * `env.content` and `env.excerpt` before parsing), with one behavioral + * change: the stripped frontmatter block is replaced with blank lines, so + * every `token.map` stays in the coordinates of the full source instead of + * shifting up by the frontmatter height. Blank lines produce no tokens, so + * the rendered output is unchanged. Offering this upstream as a + * `preserveLines` option is tracked as a follow-up. + */ +export function frontmatterPlugin( + md: MarkdownItAsync, + { grayMatterOptions, renderExcerpt = true }: FrontmatterPluginOptions = {} +): void { + const parse = md.parse.bind(md) + md.parse = (src, env: Record = {}) => { + const { data, content, excerpt = '' } = matter(src, grayMatterOptions) + + env.content = content + env.frontmatter = { ...(env.frontmatter as object), ...data } + env.excerpt = + renderExcerpt && excerpt ? md.render(excerpt, { ...env }) : excerpt + + // gray-matter only ever removes lines from the top of the file, so the + // difference in line-break counts is exactly the removed line count + const removed = + src.length === content.length + ? 0 + : countLineBreaks(src) - countLineBreaks(content) + return parse(removed > 0 ? '\n'.repeat(removed) + content : content, env) + } +} + +function countLineBreaks(str: string): number { + return str.match(/\r\n|[\r\n]/g)?.length ?? 0 +} diff --git a/src/node/markdownToVue.ts b/src/node/markdownToVue.ts index 661af795..ceb67235 100644 --- a/src/node/markdownToVue.ts +++ b/src/node/markdownToVue.ts @@ -174,7 +174,6 @@ export async function createMarkdownToVueRenderFn( throw e } const { - content, frontmatter = {}, headers = [], includes = [], @@ -183,10 +182,6 @@ export async function createMarkdownToVueRenderFn( sfcBlocks, title = '' } = env - src = env.src ?? src - const contentLineOffset = countLineBreaks( - content && src.endsWith(content) ? src.slice(0, -content.length) : '' - ) // validate data.links const deadLinks: MarkdownCompileResult['deadLinks'] = [] @@ -219,10 +214,7 @@ export async function createMarkdownToVueRenderFn( const dir = path.dirname(file) for (const [index, rawUrl] of links.entries()) { let url = rawUrl - const line = - linkLines[index] == null - ? undefined - : linkLines[index] + contentLineOffset + const line = linkLines[index] == null ? undefined : linkLines[index] const { pathname } = new URL(url, 'http://a.com') if (!treatAsHtml(pathname)) continue @@ -390,10 +382,6 @@ const inferDescription = (frontmatter: Record) => { return (head && getHeadMetaContent(head, 'description')) || '' } -function countLineBreaks(str: string) { - return str.match(/\r?\n/g)?.length ?? 0 -} - const getHeadMetaContent = (head: HeadConfig[], name: string) => { if (!head || !head.length) { return undefined