Skip to content

Commit e2e9a00

Browse files
committed
highlight
1 parent 948ce9c commit e2e9a00

File tree

1 file changed

+154
-1
lines changed

1 file changed

+154
-1
lines changed

src/components/AnimatedTerminal.tsx

Lines changed: 154 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)