1
1
import MagicString from 'magic-string' ;
2
2
import { createHash } from 'node:crypto' ;
3
- import { mkdir , readFile , readdir , stat , writeFile } from 'node:fs/promises ' ;
3
+ import fs from 'node:fs' ;
4
4
import path from 'node:path' ;
5
5
import ts from 'typescript' ;
6
6
import * as prettier from 'prettier' ;
@@ -31,8 +31,6 @@ interface RenderContentOptions {
31
31
const METADATA_REGEX =
32
32
/ (?: < ! - - - \s * | \/ \/ \/ \s * | # # # \s * ) (?< key > f i l e | l i n k | c o p y ) : \s * (?< value > .* ?) (?: \s * - - - > | $ ) \n / gm;
33
33
34
- const CACHE_MAP = new Map < string , string > ( ) ;
35
-
36
34
let twoslash_module : typeof import ( 'shiki-twoslash' ) ;
37
35
38
36
/**
@@ -133,130 +131,121 @@ export async function render_content_markdown(
133
131
const highlighter = await twoslash_module . createShikiHighlighter ( { theme : 'css-variables' } ) ;
134
132
135
133
const { type_links, type_regex } = create_type_links ( modules , resolveTypeLinks ) ;
136
- const SNIPPET_CACHE = await create_snippet_cache ( cacheCodeSnippets ) ;
134
+ const snippets = await create_snippet_cache ( cacheCodeSnippets ) ;
137
135
138
136
body = await replace_export_type_placeholders ( body , modules ) ;
139
137
140
- const conversions = new Map < string , string > ( ) ;
141
-
142
- for ( const [ _ , language , code ] of body . matchAll ( / ` ` ` ( j s | s v e l t e ) \n ( [ \s \S ] + ?) \n ` ` ` / g) ) {
143
- let { source, options } = parse_options ( code , language ) ;
144
-
145
- const converted = await generate_ts_from_js ( source , language as 'js' | 'svelte' , options ) ;
146
- if ( converted ) {
147
- conversions . set ( source , converted ) ;
148
- }
149
- }
150
-
151
138
const headings : string [ ] = [ ] ;
152
139
153
140
// this is a bit hacky, but it allows us to prevent type declarations
154
141
// from linking to themselves
155
142
let current = '' ;
156
143
157
144
return await transform ( body , {
158
- text ( token ) {
159
- // @ts -expect-error I think this is a bug in marked — some text tokens have children,
160
- // but that's not reflected in the types. In these cases we can't just use `token.tokens`
161
- // because that will result in e.g. `<code>` elements not being generated
162
- if ( token . tokens ) {
163
- // @ts -expect-error
164
- return this . parser ! . parseInline ( token . tokens ) ;
145
+ async walkTokens ( token ) {
146
+ if ( token . type === 'heading' ) {
147
+ current = token . text ;
165
148
}
166
149
167
- return smart_quotes ( token . text , true ) ;
168
- } ,
169
- heading ( { tokens, depth, raw } ) {
170
- const text = this . parser ! . parseInline ( tokens ) ;
171
-
172
- const title = text
173
- . replace ( / < \/ ? c o d e > / g, '' )
174
- . replace ( / & q u o t ; / g, '"' )
175
- . replace ( / & l t ; / g, '<' )
176
- . replace ( / & g t ; / g, '>' ) ;
177
- current = title ;
178
- const normalized = normalizeSlugify ( raw ) ;
179
- headings [ depth - 1 ] = normalized ;
180
- headings . length = depth ;
181
- const slug = headings . filter ( Boolean ) . join ( '-' ) ;
182
- return `<h${ depth } id="${ slug } ">${ text . replace (
183
- / < \/ ? c o d e > / g,
184
- ''
185
- ) } <a href="#${ slug } " class="permalink"><span class="visually-hidden">permalink</span></a></h${ depth } >`;
186
- } ,
187
- code ( { text, lang = 'js' } ) {
188
- const cached_snippet = SNIPPET_CACHE . get ( text + lang + current ) ;
189
- if ( cached_snippet . code ) return cached_snippet . code ;
190
-
191
- let { source, options } = parse_options ( text , lang ) ;
192
- source = adjust_tab_indentation ( source , lang ) ;
150
+ if ( token . type === 'code' ) {
151
+ if ( snippets . get ( token . text ) ) return ;
193
152
194
- const converted = conversions . get ( source ) ;
153
+ let { source, options } = parse_options ( token . text , token . lang ) ;
154
+ source = adjust_tab_indentation ( source , token . lang ) ;
195
155
196
- let html = '<div class="code-block"><div class="controls">' ;
156
+ const converted =
157
+ token . lang === 'js' || token . lang === 'svelte'
158
+ ? await generate_ts_from_js ( source , token . lang , options )
159
+ : undefined ;
197
160
198
- if ( options . file ) {
199
- const ext = options . file . slice ( options . file . lastIndexOf ( '.' ) ) ;
200
- if ( ! ext ) throw new Error ( `Missing file extension: ${ options . file } ` ) ;
161
+ let html = '<div class="code-block"><div class="controls">' ;
201
162
202
- html += `<span class="filename" data-ext="${ ext } ">${ options . file . slice ( 0 , - ext . length ) } </span>` ;
203
- }
163
+ if ( options . file ) {
164
+ const ext = options . file . slice ( options . file . lastIndexOf ( '.' ) ) ;
165
+ if ( ! ext ) throw new Error ( `Missing file extension: ${ options . file } ` ) ;
204
166
205
- if ( converted ) {
206
- html += `<input class="ts-toggle raised" checked title="Toggle language" type="checkbox" aria-label="Toggle JS/TS">` ;
207
- }
167
+ html += `<span class="filename" data-ext="${ ext } ">${ options . file . slice ( 0 , - ext . length ) } </span>` ;
168
+ }
208
169
209
- if ( options . copy ) {
210
- html += `<button class="copy-to-clipboard raised" title="Copy to clipboard" aria-label="Copy to clipboard"></button >` ;
211
- }
170
+ if ( converted ) {
171
+ html += `<input class="ts-toggle raised" checked title="Toggle language" type="checkbox" aria-label="Toggle JS/TS" >` ;
172
+ }
212
173
213
- html += '</div>' ;
174
+ if ( options . copy ) {
175
+ html += `<button class="copy-to-clipboard raised" title="Copy to clipboard" aria-label="Copy to clipboard"></button>` ;
176
+ }
214
177
215
- html += syntax_highlight ( {
216
- filename,
217
- highlighter,
218
- language : lang ,
219
- source,
220
- twoslashBanner,
221
- options
222
- } ) ;
178
+ html += '</div>' ;
223
179
224
- if ( converted ) {
225
180
html += syntax_highlight ( {
226
181
filename,
227
182
highlighter,
228
- language : lang === 'js' ? 'ts' : lang ,
229
- source : converted ,
183
+ language : token . lang ,
184
+ source,
230
185
twoslashBanner,
231
186
options
232
187
} ) ;
233
- }
234
188
235
- html += '</div>' ;
189
+ if ( converted ) {
190
+ html += syntax_highlight ( {
191
+ filename,
192
+ highlighter,
193
+ language : token . lang === 'js' ? 'ts' : token . lang ,
194
+ source : converted ,
195
+ twoslashBanner,
196
+ options
197
+ } ) ;
198
+ }
236
199
237
- // TODO this is currently disabled, we don't have access to `modules`
238
- if ( type_regex ) {
239
- type_regex . lastIndex = 0 ;
200
+ html += '</div>' ;
240
201
241
- html = html . replace ( type_regex , ( match , prefix , name , pos , str ) => {
242
- const char_after = str . slice ( pos + match . length , pos + match . length + 1 ) ;
202
+ // TODO this is currently disabled, we don't have access to `modules`
203
+ if ( type_regex ) {
204
+ type_regex . lastIndex = 0 ;
243
205
244
- if ( ! options . link || name === current || / ( \$ | \d | \w ) / . test ( char_after ) ) {
245
- // we don't want e.g. RequestHandler to link to RequestHandler
246
- return match ;
247
- }
206
+ html = html . replace ( type_regex , ( match , prefix , name , pos , str ) => {
207
+ const char_after = str . slice ( pos + match . length , pos + match . length + 1 ) ;
248
208
249
- const link = type_links ?. get ( name )
250
- ? `<a href="${ type_links . get ( name ) ?. relativeURL } ">${ name } </a>`
251
- : '' ;
252
- return `${ prefix || '' } ${ link } ` ;
253
- } ) ;
209
+ if ( ! options . link || name === current || / ( \$ | \d | \w ) / . test ( char_after ) ) {
210
+ // we don't want e.g. RequestHandler to link to RequestHandler
211
+ return match ;
212
+ }
213
+
214
+ const link = type_links ?. get ( name )
215
+ ? `<a href="${ type_links . get ( name ) ?. relativeURL } ">${ name } </a>`
216
+ : '' ;
217
+ return `${ prefix || '' } ${ link } ` ;
218
+ } ) ;
219
+ }
220
+
221
+ // Save everything locally now
222
+ snippets . save ( token . text , html ) ;
223
+ }
224
+ } ,
225
+ text ( token ) {
226
+ // @ts -expect-error I think this is a bug in marked — some text tokens have children,
227
+ // but that's not reflected in the types. In these cases we can't just use `token.tokens`
228
+ // because that will result in e.g. `<code>` elements not being generated
229
+ if ( token . tokens ) {
230
+ // @ts -expect-error
231
+ return this . parser ! . parseInline ( token . tokens ) ;
254
232
}
255
233
256
- // Save everything locally now
257
- SNIPPET_CACHE . save ( cached_snippet ?. uid , html ) ;
234
+ return smart_quotes ( token . text , true ) ;
235
+ } ,
236
+ heading ( { tokens, depth, raw } ) {
237
+ const text = this . parser ! . parseInline ( tokens ) ;
258
238
259
- return html ;
239
+ headings [ depth - 1 ] = normalizeSlugify ( raw ) ;
240
+ headings . length = depth ;
241
+ const slug = headings . filter ( Boolean ) . join ( '-' ) ;
242
+ return `<h${ depth } id="${ slug } ">${ text . replace (
243
+ / < \/ ? c o d e > / g,
244
+ ''
245
+ ) } <a href="#${ slug } " class="permalink"><span class="visually-hidden">permalink</span></a></h${ depth } >`;
246
+ } ,
247
+ code ( { text } ) {
248
+ return snippets . get ( text ) ;
260
249
} ,
261
250
codespan ( { text } ) {
262
251
return (
@@ -799,17 +788,12 @@ function stringify(member: TypeElement, lang: keyof typeof SHIKI_LANGUAGE_MAP =
799
788
) ;
800
789
}
801
790
802
- async function find_nearest_node_modules ( start_path : string ) : Promise < string | null > {
803
- try {
804
- if ( await stat ( path . join ( start_path , 'node_modules' ) ) ) {
805
- return path . resolve ( start_path , 'node_modules' ) ;
806
- }
807
- } catch {
808
- const parentDir = path . dirname ( start_path ) ;
791
+ function find_nearest_node_modules ( file : string ) : string | null {
792
+ let current = file ;
809
793
810
- if ( start_path === parentDir ) return null ;
811
-
812
- return find_nearest_node_modules ( parentDir ) ;
794
+ while ( current !== ( current = path . dirname ( current ) ) ) {
795
+ const resolved = path . join ( current , 'node_modules' ) ;
796
+ if ( fs . existsSync ( resolved ) ) return resolved ;
813
797
}
814
798
815
799
return null ;
@@ -830,57 +814,44 @@ async function find_nearest_node_modules(start_path: string): Promise<string | n
830
814
* ```
831
815
*/
832
816
async function create_snippet_cache ( should : boolean ) {
833
- const snippet_cache = ( await find_nearest_node_modules ( import . meta. url ) ) + '/.snippets' ;
817
+ const cache = new Map ( ) ;
818
+ const directory = find_nearest_node_modules ( import . meta. url ) + '/.snippets' ;
834
819
835
- // No local cache exists yet
836
- if ( ! CACHE_MAP . size && should ) {
837
- try {
838
- await mkdir ( snippet_cache , { recursive : true } ) ;
839
- } catch { }
840
-
841
- // Read all the cache files and populate the CACHE_MAP
842
- try {
843
- const files = await readdir ( snippet_cache ) ;
844
-
845
- const file_contents = await Promise . all (
846
- files . map ( async ( file ) => ( {
847
- file,
848
- content : await readFile ( `${ snippet_cache } /${ file } ` , 'utf-8' )
849
- } ) )
850
- ) ;
820
+ function get_file ( source : string ) {
821
+ const hash = createHash ( 'sha256' ) ;
822
+ hash . update ( source ) ;
823
+ const digest = hash . digest ( ) . toString ( 'base64' ) . replace ( / \/ / g, '-' ) ;
851
824
852
- for ( const { file, content } of file_contents ) {
853
- const uid = file . replace ( / \. h t m l $ / , '' ) ;
854
- CACHE_MAP . set ( uid , content ) ;
855
- }
856
- } catch { }
825
+ return `${ directory } /${ digest } .html` ;
857
826
}
858
827
859
- function get ( source : string ) {
860
- if ( ! should ) return { uid : null , code : null } ;
828
+ return {
829
+ get ( source : string ) {
830
+ if ( ! should ) return ;
861
831
862
- const hash = createHash ( 'sha256' ) ;
863
- hash . update ( source ) ;
864
- const digest = hash . digest ( ) . toString ( 'base64' ) . replace ( / \/ / g, '-' ) ;
832
+ let snippet = cache . get ( source ) ;
865
833
866
- try {
867
- return {
868
- uid : digest ,
869
- code : CACHE_MAP . get ( digest )
870
- } ;
871
- } catch { }
834
+ if ( snippet === undefined ) {
835
+ const file = get_file ( source ) ;
872
836
873
- return { uid : digest , code : null } ;
874
- }
837
+ if ( fs . existsSync ( file ) ) {
838
+ snippet = fs . readFileSync ( file , 'utf-8' ) ;
839
+ cache . set ( source , snippet ) ;
840
+ }
841
+ }
875
842
876
- function save ( uid : string | null , content : string ) {
877
- if ( ! should || ! uid ) return ;
843
+ return snippet ;
844
+ } ,
845
+ save ( source : string , html : string ) {
846
+ cache . set ( source , html ) ;
878
847
879
- CACHE_MAP . set ( uid , content ) ;
880
- writeFile ( ` ${ snippet_cache } / ${ uid } .html` , content ) ;
881
- }
848
+ try {
849
+ fs . mkdirSync ( directory ) ;
850
+ } catch { }
882
851
883
- return { get, save } ;
852
+ fs . writeFileSync ( get_file ( source ) , html ) ;
853
+ }
854
+ } ;
884
855
}
885
856
886
857
function create_type_links (
0 commit comments