1+ import { CodeClient } from './CodeClient' ;
12import { CodeFold } from './CodeFold' ;
23import { CodeLink } from './Link' ;
34import { CodeProps } from './VisualExampleClient' ;
45import { HastNode , HastTextNode , highlightHast , Language } from 'tree-sitter-highlight' ;
5- import React , { ReactNode } from 'react' ;
6+ import React , { cache } from 'react' ;
67import { style , StyleString } from '@react-spectrum/s2/style' with { type : 'macro' } ;
78import { TabLink } from './FileTabs' ;
9+ import { Token , TokenType } from './CodeToken' ;
810
911const property = style ( { color : 'indigo-1000' } ) ;
1012const fn = style ( { color : 'red-1000' } ) ;
@@ -36,14 +38,18 @@ const mark = style({
3638 color : 'inherit'
3739} ) ;
3840
39- function Highlight ( { children} ) {
40- return < mark className = { mark } > { children } </ mark > ;
41+ function Highlight ( { tokens} ) {
42+ return < mark className = { mark } > < CodeClient tokens = { tokens } /> </ mark > ;
43+ }
44+
45+ function Focus ( { tokens} ) {
46+ return < span > < CodeClient tokens = { tokens } /> </ span > ;
4147}
4248
4349const groupings = {
4450 highlight : Highlight ,
4551 collapse : CodeFold ,
46- focus : 'span'
52+ focus : Focus
4753} ;
4854
4955type Links = { [ name : string ] : string } ;
@@ -57,59 +63,11 @@ export interface ICodeProps {
5763
5864export function Code ( { children, lang, hideImports = true , links, styles} : ICodeProps ) {
5965 if ( lang ) {
60- // @ts -ignore
61- let highlighted = highlightHast ( children , Language [ lang === 'json' ? 'JS' : lang . toUpperCase ( ) ] ) ;
62- let lineNodes = lines ( highlighted ) ;
63- let idx = lineNodes . findIndex ( line => ! / ^ ( [ " ' ] u s e c l i e n t [ " ' ] | ( \s * $ ) ) / . test ( text ( line ) ) ) ;
64- if ( idx > 0 ) {
65- lineNodes = lineNodes . slice ( idx ) ;
66- }
67-
68- if ( hideImports ) {
69- // Group into hidden and visible nodes.
70- // Hidden nodes will include all import statements. If a highlighted block is seen,
71- // then we'll hide all the lines up until 2 lines before this.
72- let hidden : HastNode [ ] = [ ] ;
73- let visible : HastNode [ ] = [ ] ;
74- let seenNonImportLine = false ;
75- let hasHighlight = false ;
76- for ( let line of lineNodes ) {
77- if ( ! seenNonImportLine && / ^ ( [ " ' ] u s e c l i e n t [ " ' ] | @ ? i m p o r t | ( \s * $ ) ) / . test ( text ( line ) ) ) {
78- hidden . push ( line ) ;
79- } else {
80- seenNonImportLine = true ;
81- visible . push ( line ) ;
82- }
83-
84- if ( ( line . tagName === 'highlight' || line . tagName === 'focus' ) && ! hasHighlight ) {
85- hasHighlight = true ;
86- // Center highlighted lines within collapsed window (~8 lines).
87- let highlightedLines = line . children . length ;
88- let contextLines = highlightedLines < 6
89- ? Math . floor ( ( 8 - highlightedLines ) / 2 )
90- : 2 ;
91- contextLines ++ ;
92- hidden . push ( ...visible . slice ( 0 , - contextLines ) ) ;
93- visible = visible . slice ( - contextLines ) ;
94- }
95- }
96-
97- if ( hidden . length && visible . length ) {
98- lineNodes = [
99- {
100- type : 'element' ,
101- tagName : 'span' ,
102- children : hidden ,
103- properties : {
104- className : 'import'
105- }
106- } ,
107- ...visible
108- ] ;
109- }
110- }
111-
112- return < code className = { styles } style = { { fontFamily : 'inherit' , WebkitTextSizeAdjust : 'none' } } > { renderChildren ( lineNodes , '0' , links ) } </ code > ;
66+ return (
67+ < code className = { styles } style = { { fontFamily : 'inherit' , WebkitTextSizeAdjust : 'none' } } >
68+ < CodeClient tokens = { highlightCode ( children , lang , hideImports , links ) } />
69+ </ code >
70+ ) ;
11371 }
11472
11573 return (
@@ -129,6 +87,62 @@ export function Code({children, lang, hideImports = true, links, styles}: ICodeP
12987 ) ;
13088}
13189
90+ const highlightCode = cache ( ( children : string , lang : string , hideImports = true , links ?: Links ) : Token [ ] => {
91+ // @ts -ignore
92+ let highlighted = highlightHast ( children , Language [ lang === 'json' ? 'JS' : lang . toUpperCase ( ) ] ) ;
93+ let lineNodes = lines ( highlighted ) ;
94+ let idx = lineNodes . findIndex ( line => ! / ^ ( [ " ' ] u s e c l i e n t [ " ' ] | ( \s * $ ) ) / . test ( text ( line ) ) ) ;
95+ if ( idx > 0 ) {
96+ lineNodes = lineNodes . slice ( idx ) ;
97+ }
98+
99+ if ( hideImports ) {
100+ // Group into hidden and visible nodes.
101+ // Hidden nodes will include all import statements. If a highlighted block is seen,
102+ // then we'll hide all the lines up until 2 lines before this.
103+ let hidden : HastNode [ ] = [ ] ;
104+ let visible : HastNode [ ] = [ ] ;
105+ let seenNonImportLine = false ;
106+ let hasHighlight = false ;
107+ for ( let line of lineNodes ) {
108+ if ( ! seenNonImportLine && / ^ ( [ " ' ] u s e c l i e n t [ " ' ] | @ ? i m p o r t | ( \s * $ ) ) / . test ( text ( line ) ) ) {
109+ hidden . push ( line ) ;
110+ } else {
111+ seenNonImportLine = true ;
112+ visible . push ( line ) ;
113+ }
114+
115+ if ( ( line . tagName === 'highlight' || line . tagName === 'focus' ) && ! hasHighlight ) {
116+ hasHighlight = true ;
117+ // Center highlighted lines within collapsed window (~8 lines).
118+ let highlightedLines = line . children . length ;
119+ let contextLines = highlightedLines < 6
120+ ? Math . floor ( ( 8 - highlightedLines ) / 2 )
121+ : 2 ;
122+ contextLines ++ ;
123+ hidden . push ( ...visible . slice ( 0 , - contextLines ) ) ;
124+ visible = visible . slice ( - contextLines ) ;
125+ }
126+ }
127+
128+ if ( hidden . length && visible . length ) {
129+ lineNodes = [
130+ {
131+ type : 'element' ,
132+ tagName : 'span' ,
133+ children : hidden ,
134+ properties : {
135+ className : 'import'
136+ }
137+ } ,
138+ ...visible
139+ ] ;
140+ }
141+ }
142+
143+ return renderChildren ( lineNodes , '0' , links ) ;
144+ } ) ;
145+
132146function lines ( node : HastNode ) {
133147 let resultLines : HastNode [ ] = [ ] ;
134148 let currentLine : ( HastNode | HastTextNode ) [ ] = [ ] ;
@@ -210,47 +224,57 @@ function lines(node: HastNode) {
210224 return resultLines ;
211225}
212226
213- function renderHast ( node : HastNode | HastTextNode , key : string , links ?: Links , indent = '' ) : ReactNode {
227+ // Renders a Hast Node to a list of tokens. A token is either a string, a React element, or a token type (number) + string.
228+ // These are flattened into an array that gets sent to the client. This format significantly reduces the payload size vs JSX.
229+ function renderHast ( node : HastNode | HastTextNode , key : string , links ?: Links , indent = '' ) : Token | Token [ ] {
214230 if ( node . type === 'element' && 'children' in node ) {
215- let childArray : ReactNode [ ] = renderChildren ( node . children , key , links ) ;
231+ let childArray : Token [ ] = renderChildren ( node . children , key , links ) ;
216232 if ( node . tagName === 'div' ) {
217- if ( typeof childArray . at ( - 1 ) === 'string' ) {
233+ if ( typeof childArray . at ( - 1 ) === 'string' && typeof childArray . at ( - 2 ) !== 'number' ) {
218234 childArray [ childArray . length - 1 ] += '\n' ;
219235 } else {
220236 childArray . push ( '\n' ) ;
221237 }
222238 }
223239
224- let children = childArray . length === 1 ? childArray [ 0 ] : childArray ;
225- let className = node . properties ?. className . split ( ' ' ) . map ( c => styles [ c ] ) . filter ( Boolean ) . join ( ' ' ) || undefined ;
240+ let tokenType = node . properties ?. className . split ( ' ' ) . map ( c => TokenType [ c ] ) . filter ( v => v != null ) || [ ] ;
226241 if ( node . properties ?. className === 'comment' && text ( node ) === '/* PROPS */' ) {
227242 return < CodeProps key = { key } indent = { indent } /> ;
228243 }
229244
230245 // CodeProps includes the indent and newlines in case there are no props to show.
231246 if ( node . tagName === 'div' && typeof childArray [ 0 ] === 'string' && / ^ \s + $ / . test ( childArray [ 0 ] ) && React . isValidElement ( childArray [ 1 ] ) && childArray [ 1 ] . type === CodeProps ) {
232- children = childArray . slice ( 1 ) ;
247+ childArray = childArray . slice ( 1 ) ;
233248 }
234249
250+ let children = childArray . length === 1 ? childArray [ 0 ] : childArray ;
235251 let tagName : any = node . tagName ;
236252 let properties : any = node . properties ;
237253 if ( links && typeof children === 'string' && links [ children ] ) {
238254 let link = links [ children ] ;
239- tagName = CodeLink ;
240- properties = { ...properties , href : link } ;
255+ return (
256+ < CodeLink
257+ key = { key }
258+ className = { styles [ properties ?. className ] }
259+ href = { link } >
260+ { children }
261+ </ CodeLink >
262+ ) ;
241263 }
242264
243265 // Link to imported files.
244266 if ( properties ?. className === 'string' && typeof children === 'string' && / ^ [ ' " ] \. \/ / . test ( children ) ) {
245- tagName = TabLink ;
246- properties = { ...properties , name : children . slice ( 3 , - 1 ) } ;
267+ return (
268+ < TabLink
269+ key = { key }
270+ className = { styles . string }
271+ name = { children . slice ( 3 , - 1 ) } >
272+ < CodeClient tokens = { childArray } />
273+ </ TabLink >
274+ ) ;
247275 }
248276
249- if ( tagName === 'span' && ! className ) {
250- return children ;
251- }
252-
253- if ( tagName === 'div' && ! className ) {
277+ if ( ( tagName === 'div' || tagName === 'span' ) && tokenType . length === 0 ) {
254278 return children ;
255279 }
256280
@@ -260,24 +284,56 @@ function renderHast(node: HastNode | HastTextNode, key: string, links?: Links, i
260284 type = 'span' ;
261285 }
262286
263- return React . createElement ( type , { ...properties , className, key} , children ) ;
287+ if ( type === 'span' ) {
288+ return [ tokenType [ 0 ] , children ] ;
289+ }
290+
291+ let className = node . properties ?. className . split ( ' ' ) . map ( c => styles [ c ] ) . filter ( Boolean ) . join ( ' ' ) || undefined ;
292+ return React . createElement ( type , { ...properties , className, key, tokens : childArray } ) ;
264293 } else {
265294 // @ts -ignore
266295 return node . value ;
267296 }
268297}
269298
270- function renderChildren ( children : ( HastNode | HastTextNode ) [ ] , key : string , links ?: Links ) {
271- let childArray : ReactNode [ ] = [ ] ;
299+ function renderChildren ( children : ( HastNode | HastTextNode ) [ ] , key : string , links ?: Links ) : Token [ ] {
300+ let childArray : Token [ ] = [ ] ;
301+ let type = - 1 ;
302+ let stringIndex = - 1 ;
272303 for ( let [ i , child ] of children . entries ( ) ) {
273- let indent = i === 1 && typeof childArray [ 0 ] === 'string' && / ^ \s + $ / . test ( childArray [ 0 ] ) ? childArray [ 0 ] : '' ;
304+ let indent = i === 1 && stringIndex >= 0 && / ^ \s + $ / . test ( childArray [ stringIndex ] as string ) ? childArray [ stringIndex ] as string : '' ;
274305 let childNode = renderHast ( child , `${ key } .${ i } ` , links , indent ) ;
275306 let childNodes = Array . isArray ( childNode ) ? childNode : [ childNode ] ;
276- for ( let childNode of childNodes ) {
277- if ( typeof childNode === 'string' && typeof childArray . at ( - 1 ) === 'string' ) {
278- childArray [ childArray . length - 1 ] += childNode ;
307+ let childIndex = 0 ;
308+ while ( childIndex < childNodes . length ) {
309+ let child = childNodes [ childIndex ++ ] ;
310+ let childType = - 1 ;
311+ if ( typeof child === 'number' ) {
312+ // A number represents a token type. Consume the next value.
313+ childType = child ;
314+ child = childNodes [ childIndex ++ ] ;
315+ if ( childType !== type ) {
316+ childArray . push ( childType ) ;
317+ }
318+ }
319+
320+ // If this is a string, either append to the previous string if it is
321+ // the same token type, or push a new string.
322+ if ( typeof child === 'string' ) {
323+ if ( childType !== type ) {
324+ type = childType ;
325+ stringIndex = childArray . length ;
326+ childArray . push ( child ) ;
327+ } else if ( stringIndex >= 0 ) {
328+ childArray [ stringIndex ] += child ;
329+ } else {
330+ stringIndex = childArray . length ;
331+ childArray . push ( child ) ;
332+ }
279333 } else {
280- childArray . push ( childNode ) ;
334+ type = - 1 ;
335+ stringIndex = - 1 ;
336+ childArray . push ( child ) ;
281337 }
282338 }
283339 }
0 commit comments