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 <noreply@anthropic.com>
feat/md-sourcemaps
Divyansh Singh 2 weeks ago
parent 09f9672ee1
commit 0a41ecbe17

@ -4,10 +4,7 @@ import {
componentPlugin, componentPlugin,
type ComponentPluginOptions type ComponentPluginOptions
} from '@mdit-vue/plugin-component' } from '@mdit-vue/plugin-component'
import { import type { FrontmatterPluginOptions } from '@mdit-vue/plugin-frontmatter'
frontmatterPlugin,
type FrontmatterPluginOptions
} from '@mdit-vue/plugin-frontmatter'
import { import {
headersPlugin, headersPlugin,
type HeadersPluginOptions type HeadersPluginOptions
@ -52,6 +49,7 @@ import {
type ContainerOptions type ContainerOptions
} from './plugins/containers' } from './plugins/containers'
import { eagerFrontmatterInterpolationPlugin } from './plugins/eagerFrontmatterInterpolation' import { eagerFrontmatterInterpolationPlugin } from './plugins/eagerFrontmatterInterpolation'
import { frontmatterPlugin } from './plugins/frontmatter'
import { highlight as createHighlighter } from './plugins/highlight' import { highlight as createHighlighter } from './plugins/highlight'
import { imagePlugin, type Options as ImageOptions } from './plugins/image' import { imagePlugin, type Options as ImageOptions } from './plugins/image'
import { import {

@ -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<string, unknown> = {}) => {
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
}

@ -174,7 +174,6 @@ export async function createMarkdownToVueRenderFn(
throw e throw e
} }
const { const {
content,
frontmatter = {}, frontmatter = {},
headers = [], headers = [],
includes = [], includes = [],
@ -183,10 +182,6 @@ export async function createMarkdownToVueRenderFn(
sfcBlocks, sfcBlocks,
title = '' title = ''
} = env } = env
src = env.src ?? src
const contentLineOffset = countLineBreaks(
content && src.endsWith(content) ? src.slice(0, -content.length) : ''
)
// validate data.links // validate data.links
const deadLinks: MarkdownCompileResult['deadLinks'] = [] const deadLinks: MarkdownCompileResult['deadLinks'] = []
@ -219,10 +214,7 @@ export async function createMarkdownToVueRenderFn(
const dir = path.dirname(file) const dir = path.dirname(file)
for (const [index, rawUrl] of links.entries()) { for (const [index, rawUrl] of links.entries()) {
let url = rawUrl let url = rawUrl
const line = const line = linkLines[index] == null ? undefined : linkLines[index]
linkLines[index] == null
? undefined
: linkLines[index] + contentLineOffset
const { pathname } = new URL(url, 'http://a.com') const { pathname } = new URL(url, 'http://a.com')
if (!treatAsHtml(pathname)) continue if (!treatAsHtml(pathname)) continue
@ -390,10 +382,6 @@ const inferDescription = (frontmatter: Record<string, any>) => {
return (head && getHeadMetaContent(head, 'description')) || '' return (head && getHeadMetaContent(head, 'description')) || ''
} }
function countLineBreaks(str: string) {
return str.match(/\r?\n/g)?.length ?? 0
}
const getHeadMetaContent = (head: HeadConfig[], name: string) => { const getHeadMetaContent = (head: HeadConfig[], name: string) => {
if (!head || !head.length) { if (!head || !head.length) {
return undefined return undefined

Loading…
Cancel
Save