11import { existsSync , readFileSync , statSync } from 'node:fs' ;
22import path from 'node:path' ;
33import MarkdownIt from 'markdown-it' ;
4- import type StateCore from 'markdown-it/lib/rules_core/state_core' ;
5- import type Token from 'markdown-it/lib/token' ;
64import { slugify } from '@mdit-vue/shared' ;
5+ import type { VitePressMarkdownIt } from './types' ;
76
87interface HeadingAnchor {
98 alignedId : string ;
@@ -21,10 +20,19 @@ interface CachedHeadingLookup {
2120 mtimeMs : number ;
2221}
2322
23+ interface MarkdownToken {
24+ children : MarkdownToken [ ] | null ;
25+ content : string ;
26+ tag : string ;
27+ type : string ;
28+ attrGet : ( name : string ) => string | null ;
29+ attrSet : ( name : string , value : string ) => void ;
30+ }
31+
2432interface HeadingToken {
25- inlineToken : Token ;
33+ inlineToken : MarkdownToken ;
2634 pathKey : string ;
27- token : Token ;
35+ token : MarkdownToken ;
2836}
2937
3038interface LinkResolveContext {
@@ -52,9 +60,9 @@ const createHeadingPathTracker = (): ((level: number) => string) => {
5260 } ;
5361} ;
5462
55- const getHeadingLevel = ( token : Token ) : number => Number . parseInt ( token . tag . slice ( 1 ) , 10 ) ;
63+ const getHeadingLevel = ( token : MarkdownToken ) : number => Number . parseInt ( token . tag . slice ( 1 ) , 10 ) ;
5664
57- const getHeadingText = ( inlineToken : Token ) : string =>
65+ const getHeadingText = ( inlineToken : MarkdownToken ) : string =>
5866 ( inlineToken . children ?? [ ] )
5967 . filter ( ( child ) => child . type === 'text' || child . type === 'code_inline' )
6068 . map ( ( child ) => child . content )
@@ -72,7 +80,7 @@ const createUniqueSlug = (candidate: string, usedSlugs: Set<string>): string =>
7280 return resolvedSlug ;
7381} ;
7482
75- const collectHeadings = ( tokens : Token [ ] ) : HeadingAnchor [ ] => {
83+ const collectHeadings = ( tokens : MarkdownToken [ ] ) : HeadingAnchor [ ] => {
7684 const nextPathKey = createHeadingPathTracker ( ) ;
7785 const usedSlugs = new Set < string > ( ) ;
7886 const headings : HeadingAnchor [ ] = [ ] ;
@@ -165,7 +173,7 @@ const resolveHeadingLookup = (filePathRelative: string | null | undefined): Head
165173 return lookup ;
166174} ;
167175
168- const collectHeadingTokens = ( tokens : Token [ ] ) : HeadingToken [ ] => {
176+ const collectHeadingTokens = ( tokens : MarkdownToken [ ] ) : HeadingToken [ ] => {
169177 const nextPathKey = createHeadingPathTracker ( ) ;
170178 const headings : HeadingToken [ ] = [ ] ;
171179 for ( let index = 0 ; index < tokens . length ; index += 1 ) {
@@ -186,7 +194,7 @@ const collectHeadingTokens = (tokens: Token[]): HeadingToken[] => {
186194 return headings ;
187195} ;
188196
189- const syncPermalinkHref = ( inlineToken : Token , id : string ) : void => {
197+ const syncPermalinkHref = ( inlineToken : MarkdownToken , id : string ) : void => {
190198 for ( const child of inlineToken . children ?? [ ] ) {
191199 if ( child . type !== 'link_open' ) {
192200 continue ;
@@ -218,22 +226,6 @@ const hasUriScheme = (value: string): boolean => /^[a-z][a-z\d+.-]*:/i.test(valu
218226const normalizeFilePathRelative = ( filePathRelative : string ) : string =>
219227 filePathRelative . replace ( / \\ / g, '/' ) ;
220228
221- const normalizeBase = ( base = '/' ) : string => {
222- const trimmed = base . trim ( ) ;
223- if ( trimmed === '' || trimmed === '/' ) {
224- return '/' ;
225- }
226- return `/${ trimmed . replace ( / ^ \/ + | \/ + $ / g, '' ) } /` ;
227- } ;
228-
229- const toSiteRoutePath = ( filePathRelative : string , base = '/' ) : string => {
230- const normalizedFilePath = normalizeFilePathRelative ( filePathRelative ) ;
231- const routePath = normalizedFilePath . endsWith ( '/index.md' )
232- ? normalizedFilePath . slice ( 0 , - 'index.md' . length )
233- : normalizedFilePath . replace ( / \. m d $ / , '.html' ) ;
234- return `${ normalizeBase ( base ) } ${ routePath } ` . replace ( / \/ { 2 , } / g, '/' ) ;
235- } ;
236-
237229const resolveTargetFilePathRelative = ( currentFilePathRelative : string , rawPath : string , base = '/' ) : string | null => {
238230 const normalizedCurrentPath = normalizeFilePathRelative ( currentFilePathRelative ) ;
239231 if ( rawPath . length === 0 ) {
@@ -264,9 +256,10 @@ const resolveTargetFilePathRelative = (currentFilePathRelative: string, rawPath:
264256 return `${ path . posix . join ( path . posix . dirname ( normalizedCurrentPath ) , rawPath ) } .md` ;
265257} ;
266258
267- export const alignI18nAnchors = ( md : MarkdownIt ) : void => {
268- md . core . ruler . after ( 'anchor' , 'align-i18n-anchors' , ( state : StateCore ) => {
269- const lookup = resolveHeadingLookup ( state . env . filePathRelative ) ;
259+ /** Aligns localized heading IDs with the matching English document structure. */
260+ export const alignI18nAnchors = ( md : VitePressMarkdownIt ) : void => {
261+ md . core . ruler . after ( 'anchor' , 'align-i18n-anchors' , ( state ) => {
262+ const lookup = resolveHeadingLookup ( state . env . relativePath ) ;
270263 if ( ! lookup ) {
271264 return ;
272265 }
@@ -285,6 +278,7 @@ export const alignI18nAnchors = (md: MarkdownIt): void => {
285278 } ) ;
286279} ;
287280
281+ /** Rewrites localized heading fragments while preserving VitePress source-relative paths. */
288282export const resolveI18nLink = ( context : LinkResolveContext , rawHref : string ) : string => {
289283 if ( ! context . filePathRelative || ! rawHref . includes ( '#' ) ) {
290284 return rawHref ;
@@ -308,5 +302,5 @@ export const resolveI18nLink = (context: LinkResolveContext, rawHref: string): s
308302 if ( normalizeFilePathRelative ( targetFilePathRelative ) === currentFilePathRelative ) {
309303 return `#${ resolvedHash } ` ;
310304 }
311- return `${ toSiteRoutePath ( targetFilePathRelative , context . base ) } #${ resolvedHash } ` ;
305+ return `${ rawPath } #${ resolvedHash } ` ;
312306} ;
0 commit comments