1+ import { existsSync } from 'node:fs'
12import { readFile , writeFile } from 'node:fs/promises'
23import { dirname , join , resolve } from 'node:path'
34import { fileURLToPath } from 'node:url'
@@ -6,10 +7,50 @@ import { versions } from '../vocs/versions'
67const root = resolve ( dirname ( fileURLToPath ( import . meta. url ) ) , '..' )
78const publicDir = join ( root , 'vocs/dist/public' )
89const pagesDir = join ( root , 'vocs/docs/pages' )
10+ const examplesDir = join ( root , 'lib/examples' )
911const baseUrl = 'https://alloy.rs'
1012
13+ function proseLines ( markdown : string , transform : ( line : string ) => string ) : string {
14+ let inFence = false
15+
16+ return markdown
17+ . split ( '\n' )
18+ . map ( ( line ) => {
19+ if ( / ^ \s * ( ` ` ` | ~ ~ ~ ) / . test ( line ) ) {
20+ inFence = ! inFence
21+ return line
22+ }
23+ return inFence ? line : transform ( line )
24+ } )
25+ . join ( '\n' )
26+ }
27+
1128function absoluteLinks ( markdown : string ) : string {
12- return markdown . replace ( / ] \( \/ (? ! \/ ) / g, `](${ baseUrl } /` )
29+ return proseLines ( markdown , ( line ) =>
30+ line
31+ . replace ( / ( ! ? \[ [ ^ \] ] * ] \( ) \/ (? ! \/ ) / g, `$1${ baseUrl } /` )
32+ . replace ( / ( \b (?: h r e f | s r c ) \s * = \s * [ " ' ] ) \/ (? ! \/ ) / g, `$1${ baseUrl } /` ) ,
33+ )
34+ }
35+
36+ function unresolvedLinks ( markdown : string ) : string [ ] {
37+ const unresolved : string [ ] = [ ]
38+ proseLines ( markdown , ( line ) => {
39+ const targets = [
40+ ...[ ...line . matchAll ( / ! ? \[ [ ^ \] ] * ] \( ( < ? [ ^ ) \s > ] + > ? ) / g) ] . map ( ( match ) => match [ 1 ] ) ,
41+ ...[ ...line . matchAll ( / \b (?: h r e f | s r c ) \s * = \s * [ " ' ] ( [ ^ " ' ] + ) [ " ' ] / g) ] . map (
42+ ( match ) => match [ 1 ] ,
43+ ) ,
44+ ]
45+
46+ for ( const rawTarget of targets ) {
47+ const target = rawTarget . replace ( / ^ < | > $ / g, '' )
48+ if ( / ^ [ a - z ] [ a - z + . - ] * : / i. test ( target ) || target . startsWith ( '//' ) ) continue
49+ unresolved . push ( target )
50+ }
51+ return line
52+ } )
53+ return unresolved
1354}
1455
1556const llmsPath = join ( publicDir , 'llms.txt' )
@@ -20,13 +61,35 @@ const llmsFull = absoluteLinks(await readFile(llmsFullPath, 'utf8'))
2061await writeFile ( llmsPath , llms )
2162await writeFile ( llmsFullPath , llmsFull )
2263
23- if ( / ] \( \/ (? ! \/ ) / . test ( llms ) || / ] \( \/ (? ! \/ ) / . test ( llmsFull ) ) {
24- throw new Error ( 'Agent text still contains root-relative Markdown links' )
64+ const unresolved = [ ...unresolvedLinks ( llms ) , ...unresolvedLinks ( llmsFull ) ]
65+ if ( unresolved . length ) {
66+ throw new Error ( `Agent text still contains unresolved links: ${ [ ...new Set ( unresolved ) ] . join ( ', ' ) } ` )
2567}
2668
2769const sourceByRoute = new Map < string , string > ( )
70+ const exampleSourceByRoute = new Map < string , string > ( )
2871for ( const file of new Bun . Glob ( '**/*.{md,mdx}' ) . scanSync ( { cwd : pagesDir , onlyFiles : true } ) ) {
29- sourceByRoute . set ( `/${ file . replace ( / \\ / g, '/' ) . replace ( / \. ( m d | m d x ) $ / , '' ) } ` , file )
72+ const pathname = `/${ file . replace ( / \\ / g, '/' ) . replace ( / \. ( m d | m d x ) $ / , '' ) } `
73+ sourceByRoute . set ( pathname , file )
74+
75+ if ( ! pathname . startsWith ( '/examples/' ) || pathname . endsWith ( '/README' ) ) continue
76+
77+ const text = await readFile ( join ( pagesDir , file ) , 'utf8' )
78+ const match = / h t t p s : \/ \/ g i t h u b \. c o m \/ a l l o y - r s \/ e x a m p l e s \/ (?: b l o b | t r e e ) \/ [ ^ / ) \s ] + \/ ( e x a m p l e s \/ [ ^ ) \s ] + \. r s ) / . exec ( text )
79+ if ( ! match ) throw new Error ( `Generated example page ${ pathname } has no source metadata` )
80+ if ( ! existsSync ( join ( examplesDir , match [ 1 ] ) ) ) {
81+ throw new Error ( `Generated example page ${ pathname } references missing source ${ match [ 1 ] } ` )
82+ }
83+
84+ exampleSourceByRoute . set ( pathname , match [ 0 ] . replace ( '/tree/' , '/blob/' ) )
85+ }
86+
87+ function exampleSourceFor ( pathname : string ) : string | null {
88+ if ( ! pathname . startsWith ( '/examples/' ) || pathname . endsWith ( '/README' ) ) return null
89+
90+ const source = exampleSourceByRoute . get ( pathname )
91+ if ( ! source ) throw new Error ( `Example page ${ pathname } has no verified source URL` )
92+ return source
3093}
3194
3295function kindFor ( pathname : string ) : string {
@@ -43,8 +106,6 @@ const pages = [...llms.matchAll(/^- \[([^\]]+)]\((https:\/\/alloy\.rs\/[^)]+)\)(
43106 const pathname = new URL ( url ) . pathname
44107 const source = sourceByRoute . get ( pathname )
45108 const parts = pathname . split ( '/' ) . filter ( Boolean )
46- const category = parts [ 1 ]
47- const name = parts [ 2 ]
48109
49110 return {
50111 id : pathname ,
@@ -56,10 +117,7 @@ const pages = [...llms.matchAll(/^- \[([^\]]+)]\((https:\/\/alloy\.rs\/[^)]+)\)(
56117 source_url : source
57118 ? `https://github.com/alloy-rs/docs/blob/main/vocs/docs/pages/${ source } `
58119 : null ,
59- example_source_url :
60- parts [ 0 ] === 'examples' && category && name && name !== 'README'
61- ? `https://github.com/alloy-rs/examples/blob/main/examples/${ category } /examples/${ name } .rs`
62- : null ,
120+ example_source_url : exampleSourceFor ( pathname ) ,
63121 }
64122 } )
65123 . filter ( ( page , index , all ) => all . findIndex ( ( candidate ) => candidate . url === page . url ) === index )
@@ -89,10 +147,7 @@ for (const [pathname, source] of sourceByRoute) {
89147 section : parts [ 0 ] ?? 'home' ,
90148 url : `${ baseUrl } ${ pathname } ` ,
91149 source_url : `https://github.com/alloy-rs/docs/blob/main/vocs/docs/pages/${ source } ` ,
92- example_source_url :
93- parts [ 0 ] === 'examples' && category && name && name !== 'README'
94- ? `https://github.com/alloy-rs/examples/blob/main/examples/${ category } /examples/${ name } .rs`
95- : null ,
150+ example_source_url : exampleSourceFor ( pathname ) ,
96151 } )
97152 indexedPaths . add ( pathname )
98153}
0 commit comments