1- import type { GitBookSiteContext } from '@/lib/context' ;
1+ import path from 'node:path' ;
2+ import type { GitBookAnyContext , GitBookSiteContext } from '@/lib/context' ;
23import { DataFetcherError , throwIfDataError } from '@/lib/data' ;
34import type { ResolvedPagePath } from '@/lib/pages' ;
45import { getIndexablePages } from '@/lib/sitemap' ;
@@ -10,15 +11,18 @@ import {
1011 RevisionPageType ,
1112 type SiteSpace ,
1213} from '@gitbook/api' ;
13- import type { Root } from 'mdast' ;
14+ import type { Link , Root } from 'mdast' ;
1415import { fromMarkdown } from 'mdast-util-from-markdown' ;
1516import { frontmatterFromMarkdown } from 'mdast-util-frontmatter' ;
1617import { gfmFromMarkdown , gfmToMarkdown } from 'mdast-util-gfm' ;
1718import { toMarkdown } from 'mdast-util-to-markdown' ;
1819import { frontmatter } from 'micromark-extension-frontmatter' ;
1920import { gfm } from 'micromark-extension-gfm' ;
2021import { remove } from 'unist-util-remove' ;
21- import { type GitBookLinker , relativeToAbsoluteLinks } from './links' ;
22+ import { visit } from 'unist-util-visit' ;
23+ import type { GitBookLinker } from './links' ;
24+ import { resolveContentRef , resolveStringContentRef } from './references' ;
25+ import { checkIsAnchor , checkIsExternalURL } from './urls' ;
2226
2327/**
2428 * Generate a markdown version of a page.
@@ -53,8 +57,7 @@ export async function getMarkdownForPage(
5357 throw error ;
5458 }
5559
56- const tree = fromPageMarkdown ( {
57- linker : context . linker ,
60+ const tree = await fromPageMarkdown ( context , {
5861 markdown : rawMarkdown ,
5962 pagePath : page . path ,
6063 } ) ;
@@ -98,8 +101,7 @@ export async function getMarkdownForPageInSpace(
98101 } )
99102 ) ;
100103
101- const tree = fromPageMarkdown ( {
102- linker,
104+ const tree = await fromPageMarkdown ( context , {
103105 markdown : rawMarkdown ,
104106 pagePath : page . path ,
105107 } ) ;
@@ -120,11 +122,13 @@ export async function getMarkdownForPageInSpace(
120122 * Parse markdown from a page, removing frontmatter and rewriting relative links to absolute links.
121123 * Returns the markdown AST that can be further processed or converted back to markdown using `toPageMarkdown`.
122124 */
123- export function fromPageMarkdown ( args : {
124- linker : GitBookLinker ;
125- markdown : string ;
126- pagePath : string ;
127- } ) : Root {
125+ export async function fromPageMarkdown (
126+ context : GitBookAnyContext ,
127+ args : {
128+ markdown : string ;
129+ pagePath : string ;
130+ }
131+ ) : Promise < Root > {
128132 const tree = fromMarkdown ( args . markdown , {
129133 extensions : [ frontmatter ( [ 'yaml' ] ) , gfm ( ) ] ,
130134 mdastExtensions : [ frontmatterFromMarkdown ( [ 'yaml' ] ) , gfmFromMarkdown ( ) ] ,
@@ -133,7 +137,7 @@ export function fromPageMarkdown(args: {
133137 // Remove frontmatter
134138 remove ( tree , 'yaml' ) ;
135139
136- relativeToAbsoluteLinks ( args . linker , tree , args . pagePath ) ;
140+ await rewriteMarkdownLinks ( context , tree , args . pagePath ) ;
137141
138142 return tree ;
139143}
@@ -230,3 +234,55 @@ async function renderGroupPageMarkdown(args: {
230234 bullet : '-' ,
231235 } ) ;
232236}
237+
238+ /**
239+ * Re-writes URLs in a markdown content:
240+ * -
241+ * - the URL of every relative <a> link so it is expressed from the site-root.
242+ */
243+ async function rewriteMarkdownLinks (
244+ context : GitBookAnyContext ,
245+ tree : Root ,
246+ currentPagePath : string
247+ ) : Promise < Root > {
248+ const currentDir = path . posix . dirname ( currentPagePath ) ;
249+
250+ const pending : Array < Promise < void > > = [ ] ;
251+
252+ visit ( tree , 'link' , ( node : Link ) => {
253+ const original = node . url ;
254+
255+ // Skip anchors, mailto:, http(s):, protocol-like
256+ if ( checkIsExternalURL ( original ) || checkIsAnchor ( original ) ) {
257+ return ;
258+ }
259+
260+ const contentRef = resolveStringContentRef ( original ) ;
261+
262+ if ( contentRef ) {
263+ pending . push (
264+ ( async ( ) => {
265+ const resolved = await resolveContentRef ( contentRef , context ) ;
266+ if ( resolved ?. href ) {
267+ node . url = resolved . href ;
268+ }
269+ } ) ( )
270+ ) ;
271+ } else {
272+ // Resolve against the current page’s directory and strip any leading “/” or "../"
273+ // Sometimes the path can be "../" if we are on the default section
274+ // but it means we are just at the root of the site.
275+ const pathInPage = path . posix
276+ . normalize ( path . posix . join ( currentDir , original ) )
277+ . replace ( / ^ [ \/ \. ] + / , '' ) ;
278+
279+ node . url = context . linker . toAbsoluteURL ( context . linker . toPathInSpace ( pathInPage ) ) ;
280+ }
281+ } ) ;
282+
283+ if ( pending . length > 0 ) {
284+ await Promise . all ( pending ) ;
285+ }
286+
287+ return tree ;
288+ }
0 commit comments