From 0a41ecbe1781699e06837598a238dc9fbad48cf4 Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Sun, 30 Aug 2026 23:37:04 +0530 Subject: [PATCH] refactor(markdown): keep token maps in full-source coordinates Vendor @mdit-vue/plugin-frontmatter and pad the stripped frontmatter with blank lines before parsing, so every token.map stays in the coordinates of the file instead of shifting up by the frontmatter height. This deletes the contentLineOffset / src.endsWith(content) reconstruction in markdownToVue. Offering the padding upstream as a preserveLines option is a follow-up. Co-Authored-By: Claude Fable 5 --- src/node/markdown/markdown.ts | 6 ++-- src/node/markdown/plugins/frontmatter.ts | 39 ++++++++++++++++++++++++ src/node/markdownToVue.ts | 14 +-------- 3 files changed, 42 insertions(+), 17 deletions(-) create mode 100644 src/node/markdown/plugins/frontmatter.ts 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