Skip to content

Commit b6d5ee9

Browse files
Feat: Enable highlight for syntax and identation.
1 parent ace22fa commit b6d5ee9

3 files changed

Lines changed: 100 additions & 21 deletions

File tree

demo/index.html

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -78,15 +78,9 @@ <h1 class="font-semibold">Edge Python Playground</h1>
7878
<div class="flex flex-1 code-font text-sm overflow-hidden">
7979
<pre id="ln" aria-hidden="true" class="no-scrollbar pt-2 pb-2 text-[#525252] text-center bg-[#2d2d2d] select-none border-r border-[#2d2d2d] overflow-y-hidden flex-none w-[35px]"></pre>
8080

81-
<label for="ed" class="sr-only">Python source code</label>
82-
<textarea id="ed"
83-
class="no-scrollbar w-full bg-transparent p-2 outline-none resize-none text-[#c2c2c2] placeholder-[#404040] overflow-y-auto"
84-
spellcheck="false"
85-
autocomplete="off"
86-
autocorrect="off"
87-
autocapitalize="off"
88-
aria-label="Python source code editor"
89-
placeholder="Type your Python code here..."></textarea>
81+
<div id="ed"
82+
class="no-scrollbar flex-1 p-2 text-[#c2c2c2] overflow-y-auto"
83+
aria-label="Python source code editor"></div>
9084
</div>
9185
</section>
9286

demo/main.js

Lines changed: 87 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { CodeJar } from 'https://esm.sh/codejar@4';
2+
13
const SZ = 1 << 20;
24
const DEV = !['demo.edgepython.com'].includes(location.hostname);
35
const WASM_SOURCES = DEV
@@ -12,6 +14,53 @@ const MAX_LINES = 99;
1214
const $ = (id) => document.getElementById(id);
1315
const [ed, ln, btn, term, statusEl] = ['ed', 'ln', 'run', 'term', 'status'].map($);
1416

17+
const KW = new Set([
18+
'as','if','in','is','or',
19+
'and','def','del','for','not','try',
20+
'case','elif','else','from','pass','type','with',
21+
'async','await','break','class','match','raise','while','yield',
22+
'assert','except','global','import','lambda','return',
23+
'finally',
24+
'continue','nonlocal'
25+
]);
26+
const BI = new Set([
27+
'print','len','range','int','str','float','list','dict','tuple','set',
28+
'bool','isinstance','issubclass','enumerate','zip','map','filter',
29+
'abs','min','max','sum','round','pow','divmod','hash','id','repr',
30+
'ord','chr','hex','oct','bin','open','input','iter','next','reversed','sorted',
31+
'any','all','format','frozenset','bytearray','bytes','complex','memoryview',
32+
'object','property','staticmethod','classmethod','super','slice',
33+
'callable','getattr','setattr','hasattr','delattr','dir','vars','globals','locals',
34+
'NotImplemented','Ellipsis','self','cls'
35+
]);
36+
const LIT = new Set(['True','False','None']);
37+
38+
const TOKEN_RE = /(#[^\n]*)|((?:\b[fFrRbBuU]{1,2})?(?:"""[\s\S]*?"""|'''[\s\S]*?'''|"(?:\\.|[^"\\\n])*"|'(?:\\.|[^'\\\n])*'))|(0[xX][\da-fA-F_]+|0[oO][0-7_]+|0[bB][01_]+|\d[\d_]*(?:\.[\d_]*)?(?:[eE][+-]?\d+)?[jJ]?|\.\d[\d_]*(?:[eE][+-]?\d+)?[jJ]?)|([A-Za-z_]\w*)/g;
39+
40+
const esc = (s) => s.replace(/[&<>]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c]));
41+
42+
const highlight = (src) => esc(src).replace(TOKEN_RE, (m, com, str, num, word) => {
43+
if (com) return `<span class="tk-com">${com}</span>`;
44+
if (str) return `<span class="tk-str">${str}</span>`;
45+
if (num) return `<span class="tk-num">${num}</span>`;
46+
if (word) {
47+
if (KW.has(word)) return `<span class="tk-kw">${word}</span>`;
48+
if (LIT.has(word)) return `<span class="tk-lit">${word}</span>`;
49+
if (BI.has(word)) return `<span class="tk-bi">${word}</span>`;
50+
return word;
51+
}
52+
return m;
53+
});
54+
55+
const jar = CodeJar(ed, (editor) => {
56+
editor.innerHTML = highlight(editor.textContent);
57+
}, {
58+
tab: ' ',
59+
indentOn: /:\s*$/,
60+
spellcheck: false,
61+
addClosing: false,
62+
});
63+
1564
let wasm;
1665

1766
const fmt = (ms) => ms < 1000 ? `${ms.toFixed(0)}ms` : `${(ms / 1000).toFixed(2)}s`;
@@ -43,13 +92,13 @@ const loadWasm = async () => {
4392

4493
const runCode = async () => {
4594
if (!wasm) return;
46-
const srcBytes = new TextEncoder().encode(ed.value);
95+
const srcBytes = new TextEncoder().encode(jar.toString());
4796
if (srcBytes.length > SZ) return void (term.textContent = `Error: Source exceeds ${SZ} bytes`);
4897

4998
setStatus('Running...', CLS.ok);
5099
btn.disabled = true;
51100

52-
await new Promise(resolve => setTimeout(resolve, 10));
101+
await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
53102

54103
const [out, t] = await time(() => {
55104
new Uint8Array(wasm.memory.buffer).set(srcBytes, wasm.src_ptr());
@@ -63,21 +112,48 @@ const runCode = async () => {
63112
};
64113

65114
const sync = () => {
66-
const lines = ed.value.split('\n');
67-
if (lines.length > MAX_LINES) ed.value = lines.slice(0, MAX_LINES).join('\n');
68-
const n = Math.min(ed.value.split('\n').length, MAX_LINES);
115+
const text = jar.toString().replace(/\n$/, '');
116+
const n = Math.max(1, Math.min(text.split('\n').length, MAX_LINES));
69117
ln.textContent = Array.from({ length: n }, (_, i) => String(i + 1).padStart(2, '0')).join('\n');
70118
ln.scrollTop = ed.scrollTop;
71119
};
72120

73121
btn.addEventListener('click', runCode);
122+
74123
ed.addEventListener('keydown', (e) => {
75-
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); runCode(); }
76-
else if (e.key === 'Enter' && ed.value.split('\n').length >= MAX_LINES) e.preventDefault();
77-
});
78-
ed.oninput = ed.onscroll = sync;
124+
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
125+
e.preventDefault();
126+
e.stopPropagation();
127+
runCode();
128+
return;
129+
}
130+
if (e.key === 'Enter' && jar.toString().split('\n').length >= MAX_LINES) {
131+
e.preventDefault();
132+
e.stopPropagation();
133+
return;
134+
}
135+
if (e.key === 'Backspace') {
136+
const pos = jar.save();
137+
if (pos.start !== pos.end) return;
138+
const caret = pos.start;
139+
if (caret === 0) return;
140+
const text = jar.toString();
141+
const lineStart = text.lastIndexOf('\n', caret - 1) + 1;
142+
const before = text.slice(lineStart, caret);
143+
if (before.length === 0 || !/^[ \t]+$/.test(before)) return;
144+
e.preventDefault();
145+
e.stopPropagation();
146+
const TAB = 4;
147+
const prevStop = Math.floor((before.length - 1) / TAB) * TAB;
148+
const del = before.length - prevStop;
149+
jar.restore({ start: caret - del, end: caret });
150+
document.execCommand('delete');
151+
}
152+
}, true);
153+
154+
ed.addEventListener('scroll', () => { ln.scrollTop = ed.scrollTop; });
79155

80-
ed.value = DEFAULT_CODE;
156+
jar.onUpdate(sync);
157+
jar.updateCode(DEFAULT_CODE);
81158

82-
sync();
83159
loadWasm();

demo/style.css

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,13 @@
44
.code-font {
55
font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
66
line-height: 1.625;
7-
}
7+
}
8+
9+
#ed { tab-size: 4; }
10+
11+
.tk-kw { color: #c586c0; }
12+
.tk-lit { color: #569cd6; }
13+
.tk-bi { color: #4ec9b0; }
14+
.tk-num { color: #b5cea8; }
15+
.tk-str { color: #ce9178; }
16+
.tk-com { color: #6a9955; font-style: italic; }

0 commit comments

Comments
 (0)