@@ -267,6 +267,151 @@ export default function AnimatedTerminal({ version }: AnimatedTerminalProps) {
267267 return "text-gray-700 dark:text-gray-300" ;
268268 } ;
269269
270+ /**
271+ * Highlight Python syntax in a line of code
272+ */
273+ const highlightPython = ( text : string ) : JSX . Element [ ] => {
274+ if ( ! text . trim ( ) ) {
275+ return [ < span key = "empty" > { text || "\u00A0" } </ span > ] ;
276+ }
277+
278+ const keywords = [
279+ "def" ,
280+ "async" ,
281+ "await" ,
282+ "import" ,
283+ "from" ,
284+ "if" ,
285+ "elif" ,
286+ "else" ,
287+ "return" ,
288+ "for" ,
289+ "in" ,
290+ "as" ,
291+ "and" ,
292+ "or" ,
293+ "not" ,
294+ "True" ,
295+ "False" ,
296+ "None" ,
297+ ] ;
298+
299+ // Find comment position (comments take priority)
300+ const commentMatch = text . match ( / # .* $ / ) ;
301+ const commentIndex = commentMatch ? commentMatch . index ! : text . length ;
302+ const codeText = text . slice ( 0 , commentIndex ) ;
303+ const commentText = commentMatch ? commentMatch [ 0 ] : "" ;
304+
305+ // Match strings (single and double quoted)
306+ const stringRegex = / ( [ ' " ] ) (?: (? = ( \\ ? ) ) \2.) * ?\1/ g;
307+ let stringMatch ;
308+ const stringMatches : Array < { start : number ; end : number ; text : string } > = [ ] ;
309+ while ( ( stringMatch = stringRegex . exec ( codeText ) ) !== null ) {
310+ stringMatches . push ( {
311+ start : stringMatch . index ,
312+ end : stringMatch . index + stringMatch [ 0 ] . length ,
313+ text : stringMatch [ 0 ] ,
314+ } ) ;
315+ }
316+
317+ // Match numbers
318+ const numberRegex = / \b \d + \. ? \d * \b / g;
319+ let numberMatch ;
320+ const numberMatches : Array < { start : number ; end : number ; text : string } > = [ ] ;
321+ while ( ( numberMatch = numberRegex . exec ( codeText ) ) !== null ) {
322+ numberMatches . push ( {
323+ start : numberMatch . index ,
324+ end : numberMatch . index + numberMatch [ 0 ] . length ,
325+ text : numberMatch [ 0 ] ,
326+ } ) ;
327+ }
328+
329+ // Match keywords
330+ const keywordRegex = new RegExp ( `\\b(${ keywords . join ( "|" ) } )\\b` , "g" ) ;
331+ let keywordMatch ;
332+ const keywordMatches : Array < { start : number ; end : number ; text : string } > = [ ] ;
333+ while ( ( keywordMatch = keywordRegex . exec ( codeText ) ) !== null ) {
334+ keywordMatches . push ( {
335+ start : keywordMatch . index ,
336+ end : keywordMatch . index + keywordMatch [ 0 ] . length ,
337+ text : keywordMatch [ 0 ] ,
338+ } ) ;
339+ }
340+
341+ // Combine all matches and sort by position
342+ const allMatches = [
343+ ...stringMatches . map ( ( m ) => ( { ...m , type : "string" } ) ) ,
344+ ...numberMatches . map ( ( m ) => ( { ...m , type : "number" } ) ) ,
345+ ...keywordMatches . map ( ( m ) => ( { ...m , type : "keyword" } ) ) ,
346+ ] . sort ( ( a , b ) => a . start - b . start ) ;
347+
348+ // Remove overlapping matches (strings take priority, then numbers, then keywords)
349+ const filteredMatches : typeof allMatches = [ ] ;
350+ for ( const match of allMatches ) {
351+ const overlaps = filteredMatches . some (
352+ ( existing ) =>
353+ ( match . start >= existing . start && match . start < existing . end ) ||
354+ ( match . end > existing . start && match . end <= existing . end ) ||
355+ ( match . start <= existing . start && match . end >= existing . end )
356+ ) ;
357+ if ( ! overlaps ) {
358+ filteredMatches . push ( match ) ;
359+ }
360+ }
361+
362+ // Build parts array for the code part (before comment)
363+ const resultParts : Array < { text : string ; type : string } > = [ ] ;
364+ let currentIndex = 0 ;
365+ filteredMatches . forEach ( ( match ) => {
366+ if ( match . start > currentIndex ) {
367+ resultParts . push ( {
368+ text : codeText . slice ( currentIndex , match . start ) ,
369+ type : "text" ,
370+ } ) ;
371+ }
372+ resultParts . push ( { text : match . text , type : match . type } ) ;
373+ currentIndex = match . end ;
374+ } ) ;
375+ if ( currentIndex < codeText . length ) {
376+ resultParts . push ( {
377+ text : codeText . slice ( currentIndex ) ,
378+ type : "text" ,
379+ } ) ;
380+ }
381+
382+ // If no matches were found, add the whole codeText as text
383+ if ( resultParts . length === 0 && codeText . length > 0 ) {
384+ resultParts . push ( { text : codeText , type : "text" } ) ;
385+ }
386+
387+ // Add comment if it exists
388+ if ( commentText ) {
389+ resultParts . push ( { text : commentText , type : "comment" } ) ;
390+ }
391+
392+ if ( resultParts . length === 0 ) {
393+ resultParts . push ( { text, type : "text" } ) ;
394+ }
395+
396+ return resultParts . map ( ( part , index ) => {
397+ const className =
398+ part . type === "keyword"
399+ ? "text-theme-primary"
400+ : part . type === "string"
401+ ? "text-theme-secondary"
402+ : part . type === "number"
403+ ? "text-theme-accent"
404+ : part . type === "comment"
405+ ? "text-gray-500 dark:text-gray-400 italic opacity-75"
406+ : "" ;
407+ return (
408+ < span key = { index } className = { className } >
409+ { part . text }
410+ </ span >
411+ ) ;
412+ } ) ;
413+ } ;
414+
270415 // Calculate max height based on the longest example
271416 const maxLines = Math . max ( ...examples . map ( ( ex ) => ex . lines . length ) ) ;
272417 const lineHeight = 1.25 ; // rem (20px for text-sm)
@@ -292,11 +437,19 @@ export default function AnimatedTerminal({ version }: AnimatedTerminalProps) {
292437 { displayedLines . map ( ( line , index ) => {
293438 const { prefix, content, prefixColor } = getLinePrefix ( line ) ;
294439 const lineColor = getLineColor ( line ) ;
440+
441+ // Check if this line should have syntax highlighting (Python code)
442+ const isPythonCode =
443+ line . startsWith ( "In [" ) ||
444+ line . startsWith ( " ...:" ) ||
445+ ( line . startsWith ( "Out[" ) && content . trim ( ) . length > 0 ) ;
295446
296447 return (
297448 < div key = { index } className = { `${ lineColor } whitespace-pre` } >
298449 { prefix && < span className = { prefixColor } > { prefix } </ span > }
299- { content || "\u00A0" }
450+ { isPythonCode && content . trim ( )
451+ ? highlightPython ( content )
452+ : content || "\u00A0" }
300453 </ div >
301454 ) ;
302455 } ) }
0 commit comments