diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index 4c68954..aa821cf 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -164,7 +164,11 @@ export class Compiler { return [[`PUSH`, value === 'true']] } - case terms.RegExp: { + case terms.Null: { + return [[`PUSH`, null]] + } + + case terms.Regex: { // remove the surrounding slashes and any flags const [_, pattern, flags] = value.match(/^\/\/(.*)\/\/([gimsuy]*)$/) || [] if (!pattern) { @@ -216,6 +220,7 @@ export class Compiler { const { identifier, right } = getAssignmentParts(node) const instructions: ProgramItem[] = [] instructions.push(...this.#compileNode(right, input)) + instructions.push(['DUP']) // Keep a copy on the stack after storing const identifierName = input.slice(identifier.from, identifier.to) instructions.push(['STORE', identifierName]) diff --git a/src/compiler/tests/compiler.test.ts b/src/compiler/tests/compiler.test.ts index 3fbe62c..afb59df 100644 --- a/src/compiler/tests/compiler.test.ts +++ b/src/compiler/tests/compiler.test.ts @@ -39,7 +39,7 @@ describe('compiler', () => { }) test('assign number', () => { - expect('x = 5; x').toEvaluateTo(5) + expect('x = 5').toEvaluateTo(5) }) test('emoji assignment to number', () => { @@ -182,7 +182,7 @@ describe('string interpolation', () => { expect(`'price is \\$10'`).toEvaluateTo('price is $10') }) - test.only('string with mixed interpolation and escapes', () => { + test('string with mixed interpolation and escapes', () => { expect(`x = 5; 'value: $x\\ntotal: $(x * 2)'`).toEvaluateTo('value: 5\ntotal: 10') }) @@ -195,7 +195,7 @@ describe('string interpolation', () => { }) }) -describe('RegExp', () => { +describe('Regex', () => { test('simple regex', () => { expect('//hello//').toEvaluateTo(/hello/) }) diff --git a/src/editor/editor.css b/src/editor/editor.css index 129ec7b..29ec6f6 100644 --- a/src/editor/editor.css +++ b/src/editor/editor.css @@ -1,7 +1,7 @@ #output { flex: 1; - background: #40318D; - color: #7C70DA; + background: var(--bg-output); + color: var(--text-output); padding: 20px; overflow-y: auto; white-space: pre-wrap; @@ -9,20 +9,16 @@ font-size: 18px; } -#output.error { - color: #FF6E6E; -} - #status-bar { height: 30px; - background: #1E2A4A; - color: #B3A9FF55; + background: var(--bg-status-bar); + color: var(--text-status); display: flex; align-items: center; padding: 0 10px; font-size: 14px; - border-top: 3px solid #0E1A3A; - border-bottom: 3px solid #0E1A3A; + border-top: 3px solid var(--bg-status-border); + border-bottom: 3px solid var(--bg-status-border); display: flex; justify-content: space-between; @@ -44,7 +40,7 @@ } .active { - color: #C3E88D; + color: var(--color-string); } .inactive { @@ -53,5 +49,5 @@ } .syntax-error { - text-decoration: underline dotted #FF6E6E; + text-decoration: underline dotted var(--color-error); } \ No newline at end of file diff --git a/src/editor/editor.tsx b/src/editor/editor.tsx index 5bfac6c..a37cd7d 100644 --- a/src/editor/editor.tsx +++ b/src/editor/editor.tsx @@ -4,7 +4,7 @@ import { shrimpTheme } from '#editor/plugins/theme' import { shrimpLanguage } from '#/editor/plugins/shrimpLanguage' import { shrimpHighlighting } from '#editor/plugins/theme' import { shrimpKeymap } from '#editor/plugins/keymap' -import { log, toElement } from '#utils/utils' +import { asciiEscapeToHtml, log, toElement } from '#utils/utils' import { Signal } from '#utils/signal' import { shrimpErrors } from '#editor/plugins/errors' import { debugTags } from '#editor/plugins/debugTags' @@ -53,7 +53,7 @@ let outputTimeout: ReturnType outputSignal.connect((output) => { const el = document.querySelector('#output')! - el.textContent = '' + el.innerHTML = '' let content if ('error' in output) { el.classList.add('error') @@ -63,15 +63,7 @@ outputSignal.connect((output) => { content = output.output } - clearInterval(outputTimeout) - const totalTime = 100 - const speed = totalTime / content.length - let i = 0 - outputTimeout = setInterval(() => { - el.textContent += content[i] - i++ - if (i >= content.length) clearInterval(outputTimeout) - }, speed) + el.innerHTML = asciiEscapeToHtml(content) }) type StatusBarMessage = { diff --git a/src/editor/plugins/errors.ts b/src/editor/plugins/errors.ts index 22b58d8..6536145 100644 --- a/src/editor/plugins/errors.ts +++ b/src/editor/plugins/errors.ts @@ -32,7 +32,7 @@ export const shrimpErrors = ViewPlugin.fromClass( tree.iterate({ enter: (node) => { if (!node.type.isError) return - + // Skip empty error nodes if (node.from === node.to) return @@ -45,7 +45,7 @@ export const shrimpErrors = ViewPlugin.fromClass( }) this.decorations = Decoration.set(decorations) - requestAnimationFrame(() => view.dispatch({})) + // requestAnimationFrame(() => view.dispatch({})) } catch (e) { console.error('🙈 Error parsing document', e) } diff --git a/src/editor/plugins/keymap.tsx b/src/editor/plugins/keymap.tsx index abbb0b8..7c006e9 100644 --- a/src/editor/plugins/keymap.tsx +++ b/src/editor/plugins/keymap.tsx @@ -1,9 +1,7 @@ -import { outputSignal, statusBarSignal } from '#editor/editor' +import { statusBarSignal } from '#editor/editor' +import { run } from '#editor/runCode' import { EditorState } from '@codemirror/state' -import { Compiler } from '#compiler/compiler' -import { errorMessage, log } from '#utils/utils' import { keymap } from '@codemirror/view' -import { VM } from 'reefvm' let multilineMode = false const customKeymap = keymap.of([ @@ -39,9 +37,19 @@ const customKeymap = keymap.of([ }, ]) +let firstTime = true const singleLineFilter = EditorState.transactionFilter.of((transaction) => { if (multilineMode) return transaction // Allow everything in multiline mode + if (firstTime) { + firstTime = false + if (transaction.newDoc.toString().includes('\n')) { + multilineMode = true + updateStatusMessage() + return transaction + } + } + transaction.changes.iterChanges((fromA, toA, fromB, toB, inserted) => { if (inserted.toString().includes('\n')) { multilineMode = true @@ -74,15 +82,3 @@ const updateStatusMessage = () => { } requestAnimationFrame(() => updateStatusMessage()) - -const run = async (input: string) => { - try { - const compiler = new Compiler(input) - const vm = new VM(compiler.bytecode) - const output = await vm.run() - outputSignal.emit({ output: String(output.value) }) - } catch (error) { - log.error(error) - outputSignal.emit({ error: `${errorMessage(error)}` }) - } -} diff --git a/src/editor/plugins/theme.tsx b/src/editor/plugins/theme.tsx index ee9d4a7..f131591 100644 --- a/src/editor/plugins/theme.tsx +++ b/src/editor/plugins/theme.tsx @@ -3,19 +3,20 @@ import { HighlightStyle, syntaxHighlighting } from '@codemirror/language' import { tags } from '@lezer/highlight' const highlightStyle = HighlightStyle.define([ - { tag: tags.keyword, color: '#C792EA' }, - { tag: tags.name, color: '#82AAFF' }, - { tag: tags.string, color: '#C3E88D' }, - { tag: tags.number, color: '#F78C6C' }, - { tag: tags.bool, color: '#FF5370' }, - { tag: tags.operator, color: '#89DDFF' }, - { tag: tags.paren, color: '#676E95' }, - { tag: tags.function(tags.variableName), color: '#FF9CAC' }, + { tag: tags.keyword, color: 'var(--color-keyword)' }, + { tag: tags.name, color: 'var(--color-function)' }, + { tag: tags.string, color: 'var(--color-string)' }, + { tag: tags.number, color: 'var(--color-number)' }, + { tag: tags.bool, color: 'var(--color-bool)' }, + { tag: tags.operator, color: 'var(--color-operator)' }, + { tag: tags.paren, color: 'var(--color-paren)' }, + { tag: tags.regexp, color: 'var(--color-regex)' }, + { tag: tags.function(tags.variableName), color: 'var(--color-function-call)' }, { tag: tags.function(tags.invalid), color: 'white' }, { tag: tags.definition(tags.variableName), - color: '#FFCB6B', - backgroundColor: '#1E2A4A', + color: 'var(--color-variable-def)', + backgroundColor: 'var(--bg-variable-def)', padding: '1px 2px', borderRadius: '2px', fontWeight: '500', @@ -27,24 +28,24 @@ export const shrimpHighlighting = syntaxHighlighting(highlightStyle) export const shrimpTheme = EditorView.theme( { '&': { - color: '#D6DEEB', // Night Owl text color - backgroundColor: '#011627', // Night Owl dark blue + color: 'var(--text-editor)', + backgroundColor: 'var(--bg-editor)', height: '100%', fontSize: '18px', }, '.cm-content': { fontFamily: '"Pixeloid Mono", "Courier New", monospace', - caretColor: '#80A4C2', // soft blue caret + caretColor: 'var(--caret)', padding: '0px', }, '.cm-activeLine': { backgroundColor: 'transparent', }, '&.cm-focused .cm-cursor': { - borderLeftColor: '#80A4C2', + borderLeftColor: 'var(--caret)', }, '&.cm-focused .cm-selectionBackground, ::selection': { - backgroundColor: '#1D3B53', // darker blue selection + backgroundColor: 'var(--bg-selection)', }, '.cm-gutters': { display: 'none', @@ -55,10 +56,10 @@ export const shrimpTheme = EditorView.theme( height: '100%', }, '.cm-matchingBracket': { - backgroundColor: '#FF5370', + backgroundColor: 'var(--color-bool)', }, '.cm-nonmatchingBracket': { - backgroundColor: '#C3E88D', + backgroundColor: 'var(--color-string)', }, }, { dark: true } diff --git a/src/editor/runCode.tsx b/src/editor/runCode.tsx new file mode 100644 index 0000000..bbe6229 --- /dev/null +++ b/src/editor/runCode.tsx @@ -0,0 +1,16 @@ +import { outputSignal } from '#editor/editor' +import { Compiler } from '#compiler/compiler' +import { errorMessage, log } from '#utils/utils' +import { VM } from 'reefvm' + +export const run = async (input: string) => { + try { + const compiler = new Compiler(input) + const vm = new VM(compiler.bytecode) + const output = await vm.run() + outputSignal.emit({ output: String(output.value) }) + } catch (error) { + log.error(error) + outputSignal.emit({ error: `${errorMessage(error)}` }) + } +} diff --git a/src/parser/highlight.ts b/src/parser/highlight.ts index 2e3c052..f873868 100644 --- a/src/parser/highlight.ts +++ b/src/parser/highlight.ts @@ -8,6 +8,7 @@ export const highlighting = styleTags({ fn: tags.keyword, end: tags.keyword, ':': tags.keyword, + Regex: tags.regexp, Operator: tags.operator, Command: tags.function(tags.variableName), 'Params/Identifier': tags.definition(tags.variableName), diff --git a/src/parser/shrimp.grammar b/src/parser/shrimp.grammar index 2d1a409..6cd94ce 100644 --- a/src/parser/shrimp.grammar +++ b/src/parser/shrimp.grammar @@ -5,7 +5,7 @@ @top Program { item* } @tokens { - @precedence { Number "-" RegExp "/"} + @precedence { Number "-" Regex "/"} StringFragment { !['\\$]+ } NamedArgPrefix { $[a-z]+ "=" } @@ -19,7 +19,8 @@ colon[closedBy="end", @name="colon"] { ":" } end[openedBy="colon", @name="end"] { "end" } Underscore { "_" } - RegExp { "//" (![/\\\n[] | "\\" ![\n] | "[" (![\n\\\]] | "\\" ![\n])* "]")+ ("//" $[gimsuy]*)? } // Stolen from the lezer JavaScript grammar + Null { "null" } + Regex { "//" (![/\\\n[] | "\\" ![\n] | "[" (![\n\\\]] | "\\" ![\n])* "]")+ ("//" $[gimsuy]*)? } // Stolen from the lezer JavaScript grammar "fn" [@name=keyword] "if" [@name=keyword] "elsif" [@name=keyword] @@ -198,7 +199,7 @@ EscapeSeq { // to go through ambiguousFunctionCall (which is what we want semantically). // Yes, it is annoying and I gave up trying to use GLR to fix it. expressionWithoutIdentifier { - ParenExpr | Word | String | Number | Boolean | RegExp + ParenExpr | Word | String | Number | Boolean | Regex | Null } block { diff --git a/src/parser/shrimp.terms.ts b/src/parser/shrimp.terms.ts index a3e3ca1..6ecdf01 100644 --- a/src/parser/shrimp.terms.ts +++ b/src/parser/shrimp.terms.ts @@ -16,16 +16,17 @@ export const EscapeSeq = 26, Number = 27, Boolean = 28, - RegExp = 29, - FunctionDef = 30, - Params = 32, - colon = 33, - end = 34, - Underscore = 35, - NamedArg = 36, - NamedArgPrefix = 37, - IfExpr = 39, - ThenBlock = 42, - ElsifExpr = 43, - ElseExpr = 45, - Assign = 47 + Regex = 29, + Null = 30, + FunctionDef = 31, + Params = 33, + colon = 34, + end = 35, + Underscore = 36, + NamedArg = 37, + NamedArgPrefix = 38, + IfExpr = 40, + ThenBlock = 43, + ElsifExpr = 44, + ElseExpr = 46, + Assign = 48 diff --git a/src/parser/shrimp.ts b/src/parser/shrimp.ts index e62de4b..fa92a29 100644 --- a/src/parser/shrimp.ts +++ b/src/parser/shrimp.ts @@ -4,20 +4,20 @@ import {tokenizer} from "./tokenizer" import {highlighting} from "./highlight" export const parser = LRParser.deserialize({ version: 14, - states: ".WQVQaOOO!xQbO'#CdO#YQPO'#CeO#hQPO'#DiO$eQaO'#CcO$lOSO'#CsOOQ`'#Dm'#DmO$zQPO'#DlO%cQaO'#DwOOQ`'#Cz'#CzOOQO'#Dj'#DjO%kQPO'#DiO%yQaO'#D{OOQO'#DT'#DTOOQO'#Di'#DiO&QQPO'#DhOOQ`'#Dh'#DhOOQ`'#D^'#D^QVQaOOOOQ`'#Dl'#DlOOQ`'#Cb'#CbO&YQaO'#DQOOQ`'#Dk'#DkOOQ`'#D_'#D_O&gQbO,58{O'WQaO,59wO%yQaO,59PO%yQaO,59PO'eQbO'#CdO(pQPO'#CeO)QQPO,58}O)cQPO,58}O)^QPO,58}O*^QPO,58}O*fQaO'#CuO*nQWO'#CvOOOO'#Dq'#DqOOOO'#D`'#D`O+SOSO,59_OOQ`,59_,59_OOQ`'#Da'#DaO+bQaO'#C|O+jQPO,5:cO+oQaO'#DcO+tQPO,58zO,VQPO,5:gO,^QPO,5:gOOQ`,5:S,5:SOOQ`-E7[-E7[OOQ`,59l,59lOOQ`-E7]-E7]OOQO1G/c1G/cOOQO1G.k1G.kO,cQPO1G.kO%yQaO,59UO%yQaO,59UOOQ`1G.i1G.iOOOO,59a,59aOOOO,59b,59bOOOO-E7^-E7^OOQ`1G.y1G.yOOQ`-E7_-E7_O,}QaO1G/}O-_QbO'#CdOOQO,59},59}OOQO-E7a-E7aO.OQaO1G0ROOQO1G.p1G.pO.`QPO1G.pO.jQPO7+%iO.oQaO7+%jOOQO'#DV'#DVOOQO7+%m7+%mO/PQaO7+%nOOQ`<pAN>pO%yQaO'#DXOOQO'#Dd'#DdO0dQPOAN>tO0oQPO'#DZOOQOAN>tAN>tO0tQPOAN>tO0yQPO,59sO1QQPO,59sOOQO-E7b-E7bOOQOG24`G24`O1VQPOG24`O1[QPO,59uO1aQPO1G/_OOQOLD)zLD)zO.oQaO1G/aO/PQaO7+$yOOQO7+${7+${OOQO<qAN>qO&SQaO'#DYOOQO'#De'#DeO0mQPOAN>uO0xQPO'#D[OOQOAN>uAN>uO0}QPOAN>uO1SQPO,59tO1ZQPO,59tOOQO-E7c-E7cOOQOG24aG24aO1`QPOG24aO1eQPO,59vO1jQPO1G/`OOQOLD){LD){O.xQaO1G/bO/YQaO7+$zOOQO7+$|7+$|OOQO<T#a#b:m#b#cBh#c#o:m#o;'S$_;'S;=`$v<%lO$_V>Y[hSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#g:m#g#h?O#h#o:m#o;'S$_;'S;=`$v<%lO$_V?T^hSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#X:m#X#Y@P#Y#]:m#]#^@v#^#o:m#o;'S$_;'S;=`$v<%lO$_V@WY!OPhSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#o:m#o;'S$_;'S;=`$v<%lO$_V@{[hSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#Y:m#Y#ZAq#Z#o:m#o;'S$_;'S;=`$v<%lO$_VAxY|PhSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#o:m#o;'S$_;'S;=`$v<%lO$_VBm[hSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#W:m#W#XCc#X#o:m#o;'S$_;'S;=`$v<%lO$_VCjYhSrROt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#o:m#o;'S$_;'S;=`$v<%lO$_VD_]hSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#UEW#U#b:m#b#cHn#c#o:m#o;'S$_;'S;=`$v<%lO$_VE][hSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#`:m#`#aFR#a#o:m#o;'S$_;'S;=`$v<%lO$_VFW[hSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#g:m#g#hF|#h#o:m#o;'S$_;'S;=`$v<%lO$_VGR[hSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#X:m#X#YGw#Y#o:m#o;'S$_;'S;=`$v<%lO$_VHOYlRhSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#o:m#o;'S$_;'S;=`$v<%lO$_VHuYoRhSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#o:m#o;'S$_;'S;=`$v<%lO$_VIj[hSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#Y:m#Y#ZJ`#Z#o:m#o;'S$_;'S;=`$v<%lO$_VJgYxPhSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#o:m#o;'S$_;'S;=`$v<%lO$_^K^Y!hWhSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#o:m#o;'S$_;'S;=`$v<%lO$_VLR[hSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#f:m#f#gLw#g#o:m#o;'S$_;'S;=`$v<%lO$_VMOYfRhSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#o:m#o;'S$_;'S;=`$v<%lO$_^MuY!jWhSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#o:m#o;'S$_;'S;=`$v<%lO$__Nl[!iWhSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#f:m#f#g! b#g#o:m#o;'S$_;'S;=`$v<%lO$_V! g[hSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#i:m#i#jF|#j#o:m#o;'S$_;'S;=`$v<%lO$_V!!dUvRhSOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_~!!{O!q~", + tokenData: "!%n~R!SOX$_XY$|YZ%gZp$_pq$|qr&Qrt$_tu'Yuw$_wx'_xy'dyz'}z{(h{|)R|}$_}!O)l!O!P$_!P!Q,b!Q![*]![!]5P!]!^%g!^!_5j!_!`6t!`!a7_!a#O$_#O#P8i#P#R$_#R#S8n#S#T$_#T#U9X#U#X:m#X#Y=S#Y#ZDY#Z#]:m#]#^Ie#^#b:m#b#cKV#c#dNo#d#f:m#f#g!!a#g#h:m#h#i!#W#i#o:m#o#p$_#p#q!%O#q;'S$_;'S;=`$v<%l~$_~O$_~~!%iS$dUhSOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_S$yP;=`<%l$__%TUhS![ZOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_V%nUhS!nROt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_V&VWhSOt$_uw$_x!_$_!_!`&o!`#O$_#P;'S$_;'S;=`$v<%lO$_V&vU`RhSOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_~'_O!g~~'dO!e~V'kUhS!cROt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_V(UUhS!dROt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_V(oUYRhSOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_V)YU[RhSOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_V)sWhS]ROt$_uw$_x!Q$_!Q![*]![#O$_#P;'S$_;'S;=`$v<%lO$_V*dYhSkROt$_uw$_x!O$_!O!P+S!P!Q$_!Q![*]![#O$_#P;'S$_;'S;=`$v<%lO$_V+XWhSOt$_uw$_x!Q$_!Q![+q![#O$_#P;'S$_;'S;=`$v<%lO$_V+xWhSkROt$_uw$_x!Q$_!Q![+q![#O$_#P;'S$_;'S;=`$v<%lO$_V,iWhSZROt$_uw$_x!P$_!P!Q-R!Q#O$_#P;'S$_;'S;=`$v<%lO$_V-W^hSOY.SYZ$_Zt.Stu/Vuw.Swx/Vx!P.S!P!Q$_!Q!}.S!}#O3x#O#P1e#P;'S.S;'S;=`4y<%lO.SV.Z^hSmROY.SYZ$_Zt.Stu/Vuw.Swx/Vx!P.S!P!Q1z!Q!}.S!}#O3x#O#P1e#P;'S.S;'S;=`4y<%lO.SR/[XmROY/VZ!P/V!P!Q/w!Q!}/V!}#O0f#O#P1e#P;'S/V;'S;=`1t<%lO/VR/zP!P!Q/}R0SUmR#Z#[/}#]#^/}#a#b/}#g#h/}#i#j/}#m#n/}R0iVOY0fZ#O0f#O#P1O#P#Q/V#Q;'S0f;'S;=`1_<%lO0fR1RSOY0fZ;'S0f;'S;=`1_<%lO0fR1bP;=`<%l0fR1hSOY/VZ;'S/V;'S;=`1t<%lO/VR1wP;=`<%l/VV2PWhSOt$_uw$_x!P$_!P!Q2i!Q#O$_#P;'S$_;'S;=`$v<%lO$_V2pbhSmROt$_uw$_x#O$_#P#Z$_#Z#[2i#[#]$_#]#^2i#^#a$_#a#b2i#b#g$_#g#h2i#h#i$_#i#j2i#j#m$_#m#n2i#n;'S$_;'S;=`$v<%lO$_V3}[hSOY3xYZ$_Zt3xtu0fuw3xwx0fx#O3x#O#P1O#P#Q.S#Q;'S3x;'S;=`4s<%lO3xV4vP;=`<%l3xV4|P;=`<%l.ST5WUhSrPOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_V5qWaRhSOt$_uw$_x!_$_!_!`6Z!`#O$_#P;'S$_;'S;=`$v<%lO$_V6bUbRhSOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_V6{U_RhSOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_V7fWcRhSOt$_uw$_x!_$_!_!`8O!`#O$_#P;'S$_;'S;=`$v<%lO$_V8VUdRhSOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_~8nO!h~V8uUhStROt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_V9^[hSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#b:m#b#c;b#c#o:m#o;'S$_;'S;=`$v<%lO$_U:ZUvQhSOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_U:rYhSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#o:m#o;'S$_;'S;=`$v<%lO$_V;g[hSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#W:m#W#X<]#X#o:m#o;'S$_;'S;=`$v<%lO$_VT#a#b:m#b#cBh#c#o:m#o;'S$_;'S;=`$v<%lO$_V>Y[hSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#g:m#g#h?O#h#o:m#o;'S$_;'S;=`$v<%lO$_V?T^hSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#X:m#X#Y@P#Y#]:m#]#^@v#^#o:m#o;'S$_;'S;=`$v<%lO$_V@WY!PPhSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#o:m#o;'S$_;'S;=`$v<%lO$_V@{[hSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#Y:m#Y#ZAq#Z#o:m#o;'S$_;'S;=`$v<%lO$_VAxY}PhSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#o:m#o;'S$_;'S;=`$v<%lO$_VBm[hSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#W:m#W#XCc#X#o:m#o;'S$_;'S;=`$v<%lO$_VCjYhSsROt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#o:m#o;'S$_;'S;=`$v<%lO$_VD_]hSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#UEW#U#b:m#b#cHn#c#o:m#o;'S$_;'S;=`$v<%lO$_VE][hSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#`:m#`#aFR#a#o:m#o;'S$_;'S;=`$v<%lO$_VFW[hSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#g:m#g#hF|#h#o:m#o;'S$_;'S;=`$v<%lO$_VGR[hSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#X:m#X#YGw#Y#o:m#o;'S$_;'S;=`$v<%lO$_VHOYlRhSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#o:m#o;'S$_;'S;=`$v<%lO$_VHuYpRhSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#o:m#o;'S$_;'S;=`$v<%lO$_VIj[hSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#Y:m#Y#ZJ`#Z#o:m#o;'S$_;'S;=`$v<%lO$_VJgYyPhSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#o:m#o;'S$_;'S;=`$v<%lO$__K^[!iWhSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#i:m#i#jLS#j#o:m#o;'S$_;'S;=`$v<%lO$_VLX[hSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#`:m#`#aL}#a#o:m#o;'S$_;'S;=`$v<%lO$_VMS[hSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#`:m#`#aMx#a#o:m#o;'S$_;'S;=`$v<%lO$_VNPYnRhSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#o:m#o;'S$_;'S;=`$v<%lO$_VNt[hSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#f:m#f#g! j#g#o:m#o;'S$_;'S;=`$v<%lO$_V! qYfRhSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#o:m#o;'S$_;'S;=`$v<%lO$_^!!hY!kWhSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#o:m#o;'S$_;'S;=`$v<%lO$__!#_[!jWhSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#f:m#f#g!$T#g#o:m#o;'S$_;'S;=`$v<%lO$_V!$Y[hSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#i:m#i#jF|#j#o:m#o;'S$_;'S;=`$v<%lO$_V!%VUwRhSOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_~!%nO!r~", tokenizers: [0, 1, 2, 3, tokenizer], topRules: {"Program":[0,3]}, - tokenPrec: 758 + tokenPrec: 767 }) diff --git a/src/parser/tests/basics.test.ts b/src/parser/tests/basics.test.ts index cdbba7c..94f84db 100644 --- a/src/parser/tests/basics.test.ts +++ b/src/parser/tests/basics.test.ts @@ -2,6 +2,20 @@ import { expect, describe, test } from 'bun:test' import '../shrimp.grammar' // Importing this so changes cause it to retest! +describe('null', () => { + test('parses null', () => { + expect('null').toMatchTree(`Null null`) + }) + + test('parses null in assignments', () => { + expect('a = null').toMatchTree(` + Assign + Identifier a + operator = + Null null`) + }) +}) + describe('Identifier', () => { test('parses identifiers with emojis and dashes', () => { expect('moo-😊-34').toMatchTree(` diff --git a/src/server/index.css b/src/server/index.css index 15d6200..240bd2e 100644 --- a/src/server/index.css +++ b/src/server/index.css @@ -1,3 +1,52 @@ +:root { + /* Background colors */ + --bg-editor: #011627; + --bg-output: #40318D; + --bg-status-bar: #1E2A4A; + --bg-status-border: #0E1A3A; + --bg-selection: #1D3B53; + --bg-variable-def: #1E2A4A; + + /* Text colors */ + --text-editor: #D6DEEB; + --text-output: #7C70DA; + --text-status: #B3A9FF55; + --caret: #80A4C2; + + /* Syntax highlighting colors */ + --color-keyword: #C792EA; + --color-function: #82AAFF; + --color-string: #C3E88D; + --color-number: #F78C6C; + --color-bool: #FF5370; + --color-operator: #89DDFF; + --color-paren: #676E95; + --color-function-call: #FF9CAC; + --color-variable-def: #FFCB6B; + --color-error: #FF6E6E; + --color-regex: #E1ACFF; + + /* ANSI terminal colors */ + --ansi-black: #011627; + --ansi-red: #FF5370; + --ansi-green: #C3E88D; + --ansi-yellow: #FFCB6B; + --ansi-blue: #82AAFF; + --ansi-magenta: #C792EA; + --ansi-cyan: #89DDFF; + --ansi-white: #D6DEEB; + + /* ANSI bright colors (slightly more vibrant) */ + --ansi-bright-black: #676E95; + --ansi-bright-red: #FF6E90; + --ansi-bright-green: #D4F6A8; + --ansi-bright-yellow: #FFE082; + --ansi-bright-blue: #A8C7FA; + --ansi-bright-magenta: #E1ACFF; + --ansi-bright-cyan: #A8F5FF; + --ansi-bright-white: #FFFFFF; +} + * { margin: 0; padding: 0; @@ -5,8 +54,8 @@ } body { - background: #40318D; - color: #7C70DA; + background: var(--bg-output); + color: var(--text-output); font-family: 'Pixeloid Mono', 'Courier New', monospace; font-size: 18px; height: 100vh; @@ -15,7 +64,7 @@ body { #root { height: 100vh; - background: #40318D; + background: var(--bg-output); display: flex; flex-direction: column; } \ No newline at end of file diff --git a/src/utils/utils.tsx b/src/utils/utils.tsx index b6d535f..a33cef5 100644 --- a/src/utils/utils.tsx +++ b/src/utils/utils.tsx @@ -27,3 +27,112 @@ export const toElement = (node: any): HTMLElement => { export const assertNever = (x: never): never => { throw new Error(`Unexpected object: ${x}`) } + +type HtmlEscapedString = string & { __htmlEscaped: true } + +const ansiCodeToCssVar = (code: number): string | null => { + // Foreground colors (30-37) + if (code === 30) return '--ansi-black' + if (code === 31) return '--ansi-red' + if (code === 32) return '--ansi-green' + if (code === 33) return '--ansi-yellow' + if (code === 34) return '--ansi-blue' + if (code === 35) return '--ansi-magenta' + if (code === 36) return '--ansi-cyan' + if (code === 37) return '--ansi-white' + + // Background colors (40-47) + if (code === 40) return '--ansi-black' + if (code === 41) return '--ansi-red' + if (code === 42) return '--ansi-green' + if (code === 43) return '--ansi-yellow' + if (code === 44) return '--ansi-blue' + if (code === 45) return '--ansi-magenta' + if (code === 46) return '--ansi-cyan' + if (code === 47) return '--ansi-white' + + // Bright foreground colors (90-97) + if (code === 90) return '--ansi-bright-black' + if (code === 91) return '--ansi-bright-red' + if (code === 92) return '--ansi-bright-green' + if (code === 93) return '--ansi-bright-yellow' + if (code === 94) return '--ansi-bright-blue' + if (code === 95) return '--ansi-bright-magenta' + if (code === 96) return '--ansi-bright-cyan' + if (code === 97) return '--ansi-bright-white' + + // Bright background colors (100-107) + if (code === 100) return '--ansi-bright-black' + if (code === 101) return '--ansi-bright-red' + if (code === 102) return '--ansi-bright-green' + if (code === 103) return '--ansi-bright-yellow' + if (code === 104) return '--ansi-bright-blue' + if (code === 105) return '--ansi-bright-magenta' + if (code === 106) return '--ansi-bright-cyan' + if (code === 107) return '--ansi-bright-white' + + return null +} + +export const asciiEscapeToHtml = (str: string): HtmlEscapedString => { + let result = '' + let openSpans = 0 + + const parts = str.split(/\x1b\[(.*?)m/) + + for (let i = 0; i < parts.length; i++) { + if (i % 2 === 0) { + // Regular text + result += parts[i] + continue + } + + // ANSI escape code + const codes = parts[i]!.split(';').map((code) => parseInt(code, 10)) + + for (const code of codes) { + if (code === 0) { + // Reset - close all open spans + result += ''.repeat(openSpans) + openSpans = 0 + } else if (code === 1) { + // Bold + result += '' + openSpans++ + } else if (code >= 30 && code <= 37) { + // Foreground color + const cssVar = ansiCodeToCssVar(code) + if (cssVar) { + result += `` + openSpans++ + } + } else if (code >= 40 && code <= 47) { + // Background color + const cssVar = ansiCodeToCssVar(code) + if (cssVar) { + result += `` + openSpans++ + } + } else if (code >= 90 && code <= 97) { + // Bright foreground color + const cssVar = ansiCodeToCssVar(code) + if (cssVar) { + result += `` + openSpans++ + } + } else if (code >= 100 && code <= 107) { + // Bright background color + const cssVar = ansiCodeToCssVar(code) + if (cssVar) { + result += `` + openSpans++ + } + } + } + } + + // Close any remaining spans + result += ''.repeat(openSpans) + + return result as HtmlEscapedString +}