@@ -202,6 +202,104 @@ function setEditorCode(editor, code) {
202202 }
203203}
204204
205+ // ── Syntax highlighting ────────────────────────────────────────────
206+
207+ function escapeHtml ( text ) {
208+ return text
209+ . replace ( / & / g, "&" )
210+ . replace ( / < / g, "<" )
211+ . replace ( / > / g, ">" ) ;
212+ }
213+
214+ var HL_KEYWORDS =
215+ "import|from|export|default|const|let|var|function|return|if|else|for|" +
216+ "while|do|switch|case|break|continue|throw|try|catch|finally|new|class|" +
217+ "extends|implements|interface|type|enum|as|typeof|instanceof|in|of|void|" +
218+ "async|await|yield|static|get|set|super|delete|namespace" ;
219+ var HL_LITERALS = "true|false|null|undefined|this|NaN|Infinity" ;
220+ var HL_LITERALS_SET = { } ;
221+ HL_LITERALS . split ( "|" ) . forEach ( function ( w ) {
222+ HL_LITERALS_SET [ w ] = true ;
223+ } ) ;
224+
225+ var HL_REGEX = new RegExp (
226+ "(" +
227+ "\\/\\/[^\\n]*" +
228+ "|\\/\\*[\\s\\S]*?\\*\\/" +
229+ '|"(?:[^"\\\\]|\\\\.)*"' +
230+ "|'(?:[^'\\\\]|\\\\.)*'" +
231+ "|`(?:[^`\\\\]|\\\\.)*`" +
232+ "|\\b(?:" +
233+ HL_KEYWORDS +
234+ ")\\b" +
235+ "|\\b(?:" +
236+ HL_LITERALS +
237+ ")\\b" +
238+ "|\\b\\d+(?:\\.\\d+)?(?:e[+-]?\\d+)?\\b" +
239+ ")" ,
240+ "g" ,
241+ ) ;
242+
243+ function highlightCode ( code ) {
244+ var html = "" ;
245+ var lastIndex = 0 ;
246+ HL_REGEX . lastIndex = 0 ;
247+ var match ;
248+ while ( ( match = HL_REGEX . exec ( code ) ) !== null ) {
249+ if ( match . index > lastIndex ) {
250+ html += escapeHtml ( code . slice ( lastIndex , match . index ) ) ;
251+ }
252+ var token = match [ 0 ] ;
253+ var cls ;
254+ var ch = token . charAt ( 0 ) ;
255+ if ( ch === "/" && ( token . charAt ( 1 ) === "/" || token . charAt ( 1 ) === "*" ) ) {
256+ cls = "hl-comment" ;
257+ } else if ( ch === '"' || ch === "'" || ch === "`" ) {
258+ cls = "hl-string" ;
259+ } else if ( / ^ \d / . test ( token ) ) {
260+ cls = "hl-number" ;
261+ } else if ( HL_LITERALS_SET [ token ] ) {
262+ cls = "hl-literal" ;
263+ } else {
264+ cls = "hl-keyword" ;
265+ }
266+ html += '<span class="' + cls + '">' + escapeHtml ( token ) + "</span>" ;
267+ lastIndex = HL_REGEX . lastIndex ;
268+ }
269+ if ( lastIndex < code . length ) {
270+ html += escapeHtml ( code . slice ( lastIndex ) ) ;
271+ }
272+ return html ;
273+ }
274+
275+ function injectHighlightStyles ( ) {
276+ var style = document . createElement ( "style" ) ;
277+ style . textContent =
278+ ".editor-wrapper{position:relative}" +
279+ ".editor-highlight{" +
280+ "position:absolute;top:0;left:0;right:0;bottom:0;" +
281+ "margin:0;pointer-events:none;z-index:1;" +
282+ "background:transparent;" +
283+ "border:1px solid transparent;border-top:none;border-bottom:none;" +
284+ "border-radius:0;" +
285+ "padding:1rem;" +
286+ "font-family:var(--font-mono,'Cascadia Code','Fira Code','JetBrains Mono',monospace);" +
287+ "font-size:0.875rem;line-height:1.55;" +
288+ "white-space:pre;tab-size:2;overflow:hidden;" +
289+ "color:#e6edf3;" +
290+ "}" +
291+ ".playground-editor.editor-transparent{" +
292+ "color:transparent!important;" +
293+ "caret-color:#e6edf3;" +
294+ "}" +
295+ ".hl-keyword{color:#ff7b72}" +
296+ ".hl-string{color:#a5d6ff}" +
297+ ".hl-comment{color:#8b949e;font-style:italic}" +
298+ ".hl-number{color:#79c0ff}" +
299+ ".hl-literal{color:#79c0ff}" ;
300+ document . head . appendChild ( style ) ;
301+ }
302+
205303// ── Playground block setup ─────────────────────────────────────────
206304
207305function setupBlock ( block , ts ) {
@@ -214,6 +312,20 @@ function setupBlock(block, ts) {
214312
215313 var originalCode = getEditorCode ( editor ) ;
216314
315+ // Normalize: convert contenteditable <pre> to <textarea>
316+ if ( ! isTextarea ( editor ) ) {
317+ var ta = document . createElement ( "textarea" ) ;
318+ ta . className = editor . className ;
319+ ta . value = originalCode ;
320+ ta . spellcheck = false ;
321+ ta . setAttribute ( "autocomplete" , "off" ) ;
322+ ta . setAttribute ( "autocorrect" , "off" ) ;
323+ ta . setAttribute ( "autocapitalize" , "off" ) ;
324+ editor . parentNode . replaceChild ( ta , editor ) ;
325+ editor = ta ;
326+ }
327+ editor . spellcheck = false ;
328+
217329 // ── Python tab setup ───────────────────────────────
218330 if ( pythonSource ) {
219331 var pythonCode = getEditorCode ( pythonSource ) ;
@@ -250,11 +362,16 @@ function setupBlock(block, ts) {
250362 // Insert Python view after editor
251363 editor . parentNode . insertBefore ( pythonView , editor . nextSibling ) ;
252364
253- // Tab switching
365+ // Tab switching (use wrapper to also hide/show the highlight overlay)
254366 tsTab . addEventListener ( "click" , function ( ) {
255367 tsTab . classList . add ( "active" ) ;
256368 pyTab . classList . remove ( "active" ) ;
257- editor . style . display = "" ;
369+ var editorWrapper = editor . closest ( ".editor-wrapper" ) ;
370+ if ( editorWrapper ) {
371+ editorWrapper . style . display = "" ;
372+ } else {
373+ editor . style . display = "" ;
374+ }
258375 pythonView . classList . remove ( "active" ) ;
259376 runBtn . style . display = "" ;
260377 if ( resetBtn ) resetBtn . style . display = "" ;
@@ -263,18 +380,49 @@ function setupBlock(block, ts) {
263380 pyTab . addEventListener ( "click" , function ( ) {
264381 pyTab . classList . add ( "active" ) ;
265382 tsTab . classList . remove ( "active" ) ;
266- editor . style . display = "none" ;
383+ var editorWrapper = editor . closest ( ".editor-wrapper" ) ;
384+ if ( editorWrapper ) {
385+ editorWrapper . style . display = "none" ;
386+ } else {
387+ editor . style . display = "none" ;
388+ }
267389 pythonView . classList . add ( "active" ) ;
268390 runBtn . style . display = "none" ;
269391 if ( resetBtn ) resetBtn . style . display = "none" ;
270392 } ) ;
271393 }
272394
395+ // Wrap editor in a container and add a highlight overlay
396+ var wrapper = document . createElement ( "div" ) ;
397+ wrapper . className = "editor-wrapper" ;
398+ editor . parentNode . insertBefore ( wrapper , editor ) ;
399+ wrapper . appendChild ( editor ) ;
400+
401+ var highlightPre = document . createElement ( "pre" ) ;
402+ highlightPre . className = "editor-highlight" ;
403+ highlightPre . setAttribute ( "aria-hidden" , "true" ) ;
404+ wrapper . appendChild ( highlightPre ) ;
405+
406+ editor . classList . add ( "editor-transparent" ) ;
407+
408+ function syncHighlight ( ) {
409+ highlightPre . innerHTML = highlightCode ( editor . value ) + "\n" ;
410+ }
411+
412+ function syncScroll ( ) {
413+ highlightPre . scrollTop = editor . scrollTop ;
414+ highlightPre . scrollLeft = editor . scrollLeft ;
415+ }
416+
417+ editor . addEventListener ( "input" , syncHighlight ) ;
418+ editor . addEventListener ( "scroll" , syncScroll ) ;
419+ syncHighlight ( ) ;
420+
273421 // ── Auto-resize textarea to fit content ────────────
274422 function autoResize ( ) {
275- if ( ! isTextarea ( editor ) ) return ;
276423 editor . style . height = "auto" ;
277424 editor . style . height = editor . scrollHeight + 2 + "px" ;
425+ syncScroll ( ) ;
278426 }
279427 editor . addEventListener ( "input" , autoResize ) ;
280428 autoResize ( ) ;
@@ -283,17 +431,12 @@ function setupBlock(block, ts) {
283431 editor . addEventListener ( "keydown" , function ( e ) {
284432 if ( e . key === "Tab" ) {
285433 e . preventDefault ( ) ;
286- if ( isTextarea ( editor ) ) {
287- var start = editor . selectionStart ;
288- var end = editor . selectionEnd ;
289- editor . value =
290- editor . value . substring ( 0 , start ) +
291- " " +
292- editor . value . substring ( end ) ;
293- editor . selectionStart = editor . selectionEnd = start + 2 ;
294- } else {
295- document . execCommand ( "insertText" , false , " " ) ;
296- }
434+ var start = editor . selectionStart ;
435+ var end = editor . selectionEnd ;
436+ editor . value =
437+ editor . value . substring ( 0 , start ) + " " + editor . value . substring ( end ) ;
438+ editor . selectionStart = editor . selectionEnd = start + 2 ;
439+ syncHighlight ( ) ;
297440 }
298441 // Ctrl+Enter or Cmd+Enter runs the code
299442 if ( e . key === "Enter" && ( e . ctrlKey || e . metaKey ) ) {
@@ -316,7 +459,7 @@ function setupBlock(block, ts) {
316459 output . classList . add ( "active" ) ;
317460 timingEl . innerHTML = "" ;
318461 try {
319- var code = getEditorCode ( editor ) ;
462+ var code = editor . value ;
320463 var startTime = performance . now ( ) ;
321464 var js = transformCode ( code , ts ) ;
322465 var result = executeCode ( js ) ;
@@ -336,9 +479,10 @@ function setupBlock(block, ts) {
336479 // Reset button handler
337480 if ( resetBtn ) {
338481 resetBtn . addEventListener ( "click" , function ( ) {
339- setEditorCode ( editor , originalCode ) ;
482+ editor . value = originalCode ;
340483 output . textContent = "" ;
341484 output . classList . remove ( "error" , "active" ) ;
485+ syncHighlight ( ) ;
342486 timingEl . innerHTML = "" ;
343487 autoResize ( ) ;
344488 } ) ;
@@ -364,6 +508,7 @@ async function initPlayground() {
364508 var ts = await loadTypeScript ( ) ;
365509
366510 setStatus ( "Initializing editors\u2026" ) ;
511+ injectHighlightStyles ( ) ;
367512
368513 // Initialize all playground blocks on the page
369514 var blocks = document . querySelectorAll ( ".playground-block" ) ;
0 commit comments