regex and null

This commit is contained in:
Corey Johnson 2025-10-16 13:51:50 -07:00
parent 80e489f55d
commit e8a1befdcc
15 changed files with 271 additions and 90 deletions

View File

@ -164,7 +164,11 @@ export class Compiler {
return [[`PUSH`, value === 'true']] return [[`PUSH`, value === 'true']]
} }
case terms.RegExp: { case terms.Null: {
return [[`PUSH`, null]]
}
case terms.Regex: {
// remove the surrounding slashes and any flags // remove the surrounding slashes and any flags
const [_, pattern, flags] = value.match(/^\/\/(.*)\/\/([gimsuy]*)$/) || [] const [_, pattern, flags] = value.match(/^\/\/(.*)\/\/([gimsuy]*)$/) || []
if (!pattern) { if (!pattern) {
@ -216,6 +220,7 @@ export class Compiler {
const { identifier, right } = getAssignmentParts(node) const { identifier, right } = getAssignmentParts(node)
const instructions: ProgramItem[] = [] const instructions: ProgramItem[] = []
instructions.push(...this.#compileNode(right, input)) 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) const identifierName = input.slice(identifier.from, identifier.to)
instructions.push(['STORE', identifierName]) instructions.push(['STORE', identifierName])

View File

@ -39,7 +39,7 @@ describe('compiler', () => {
}) })
test('assign number', () => { test('assign number', () => {
expect('x = 5; x').toEvaluateTo(5) expect('x = 5').toEvaluateTo(5)
}) })
test('emoji assignment to number', () => { test('emoji assignment to number', () => {
@ -182,7 +182,7 @@ describe('string interpolation', () => {
expect(`'price is \\$10'`).toEvaluateTo('price is $10') 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') 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', () => { test('simple regex', () => {
expect('//hello//').toEvaluateTo(/hello/) expect('//hello//').toEvaluateTo(/hello/)
}) })

View File

@ -1,7 +1,7 @@
#output { #output {
flex: 1; flex: 1;
background: #40318D; background: var(--bg-output);
color: #7C70DA; color: var(--text-output);
padding: 20px; padding: 20px;
overflow-y: auto; overflow-y: auto;
white-space: pre-wrap; white-space: pre-wrap;
@ -9,20 +9,16 @@
font-size: 18px; font-size: 18px;
} }
#output.error {
color: #FF6E6E;
}
#status-bar { #status-bar {
height: 30px; height: 30px;
background: #1E2A4A; background: var(--bg-status-bar);
color: #B3A9FF55; color: var(--text-status);
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0 10px; padding: 0 10px;
font-size: 14px; font-size: 14px;
border-top: 3px solid #0E1A3A; border-top: 3px solid var(--bg-status-border);
border-bottom: 3px solid #0E1A3A; border-bottom: 3px solid var(--bg-status-border);
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -44,7 +40,7 @@
} }
.active { .active {
color: #C3E88D; color: var(--color-string);
} }
.inactive { .inactive {
@ -53,5 +49,5 @@
} }
.syntax-error { .syntax-error {
text-decoration: underline dotted #FF6E6E; text-decoration: underline dotted var(--color-error);
} }

View File

@ -4,7 +4,7 @@ import { shrimpTheme } from '#editor/plugins/theme'
import { shrimpLanguage } from '#/editor/plugins/shrimpLanguage' import { shrimpLanguage } from '#/editor/plugins/shrimpLanguage'
import { shrimpHighlighting } from '#editor/plugins/theme' import { shrimpHighlighting } from '#editor/plugins/theme'
import { shrimpKeymap } from '#editor/plugins/keymap' 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 { Signal } from '#utils/signal'
import { shrimpErrors } from '#editor/plugins/errors' import { shrimpErrors } from '#editor/plugins/errors'
import { debugTags } from '#editor/plugins/debugTags' import { debugTags } from '#editor/plugins/debugTags'
@ -53,7 +53,7 @@ let outputTimeout: ReturnType<typeof setTimeout>
outputSignal.connect((output) => { outputSignal.connect((output) => {
const el = document.querySelector('#output')! const el = document.querySelector('#output')!
el.textContent = '' el.innerHTML = ''
let content let content
if ('error' in output) { if ('error' in output) {
el.classList.add('error') el.classList.add('error')
@ -63,15 +63,7 @@ outputSignal.connect((output) => {
content = output.output content = output.output
} }
clearInterval(outputTimeout) el.innerHTML = asciiEscapeToHtml(content)
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)
}) })
type StatusBarMessage = { type StatusBarMessage = {

View File

@ -32,7 +32,7 @@ export const shrimpErrors = ViewPlugin.fromClass(
tree.iterate({ tree.iterate({
enter: (node) => { enter: (node) => {
if (!node.type.isError) return if (!node.type.isError) return
// Skip empty error nodes // Skip empty error nodes
if (node.from === node.to) return if (node.from === node.to) return
@ -45,7 +45,7 @@ export const shrimpErrors = ViewPlugin.fromClass(
}) })
this.decorations = Decoration.set(decorations) this.decorations = Decoration.set(decorations)
requestAnimationFrame(() => view.dispatch({})) // requestAnimationFrame(() => view.dispatch({}))
} catch (e) { } catch (e) {
console.error('🙈 Error parsing document', e) console.error('🙈 Error parsing document', e)
} }

View File

@ -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 { EditorState } from '@codemirror/state'
import { Compiler } from '#compiler/compiler'
import { errorMessage, log } from '#utils/utils'
import { keymap } from '@codemirror/view' import { keymap } from '@codemirror/view'
import { VM } from 'reefvm'
let multilineMode = false let multilineMode = false
const customKeymap = keymap.of([ const customKeymap = keymap.of([
@ -39,9 +37,19 @@ const customKeymap = keymap.of([
}, },
]) ])
let firstTime = true
const singleLineFilter = EditorState.transactionFilter.of((transaction) => { const singleLineFilter = EditorState.transactionFilter.of((transaction) => {
if (multilineMode) return transaction // Allow everything in multiline mode 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) => { transaction.changes.iterChanges((fromA, toA, fromB, toB, inserted) => {
if (inserted.toString().includes('\n')) { if (inserted.toString().includes('\n')) {
multilineMode = true multilineMode = true
@ -74,15 +82,3 @@ const updateStatusMessage = () => {
} }
requestAnimationFrame(() => 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)}` })
}
}

View File

@ -3,19 +3,20 @@ import { HighlightStyle, syntaxHighlighting } from '@codemirror/language'
import { tags } from '@lezer/highlight' import { tags } from '@lezer/highlight'
const highlightStyle = HighlightStyle.define([ const highlightStyle = HighlightStyle.define([
{ tag: tags.keyword, color: '#C792EA' }, { tag: tags.keyword, color: 'var(--color-keyword)' },
{ tag: tags.name, color: '#82AAFF' }, { tag: tags.name, color: 'var(--color-function)' },
{ tag: tags.string, color: '#C3E88D' }, { tag: tags.string, color: 'var(--color-string)' },
{ tag: tags.number, color: '#F78C6C' }, { tag: tags.number, color: 'var(--color-number)' },
{ tag: tags.bool, color: '#FF5370' }, { tag: tags.bool, color: 'var(--color-bool)' },
{ tag: tags.operator, color: '#89DDFF' }, { tag: tags.operator, color: 'var(--color-operator)' },
{ tag: tags.paren, color: '#676E95' }, { tag: tags.paren, color: 'var(--color-paren)' },
{ tag: tags.function(tags.variableName), color: '#FF9CAC' }, { 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.function(tags.invalid), color: 'white' },
{ {
tag: tags.definition(tags.variableName), tag: tags.definition(tags.variableName),
color: '#FFCB6B', color: 'var(--color-variable-def)',
backgroundColor: '#1E2A4A', backgroundColor: 'var(--bg-variable-def)',
padding: '1px 2px', padding: '1px 2px',
borderRadius: '2px', borderRadius: '2px',
fontWeight: '500', fontWeight: '500',
@ -27,24 +28,24 @@ export const shrimpHighlighting = syntaxHighlighting(highlightStyle)
export const shrimpTheme = EditorView.theme( export const shrimpTheme = EditorView.theme(
{ {
'&': { '&': {
color: '#D6DEEB', // Night Owl text color color: 'var(--text-editor)',
backgroundColor: '#011627', // Night Owl dark blue backgroundColor: 'var(--bg-editor)',
height: '100%', height: '100%',
fontSize: '18px', fontSize: '18px',
}, },
'.cm-content': { '.cm-content': {
fontFamily: '"Pixeloid Mono", "Courier New", monospace', fontFamily: '"Pixeloid Mono", "Courier New", monospace',
caretColor: '#80A4C2', // soft blue caret caretColor: 'var(--caret)',
padding: '0px', padding: '0px',
}, },
'.cm-activeLine': { '.cm-activeLine': {
backgroundColor: 'transparent', backgroundColor: 'transparent',
}, },
'&.cm-focused .cm-cursor': { '&.cm-focused .cm-cursor': {
borderLeftColor: '#80A4C2', borderLeftColor: 'var(--caret)',
}, },
'&.cm-focused .cm-selectionBackground, ::selection': { '&.cm-focused .cm-selectionBackground, ::selection': {
backgroundColor: '#1D3B53', // darker blue selection backgroundColor: 'var(--bg-selection)',
}, },
'.cm-gutters': { '.cm-gutters': {
display: 'none', display: 'none',
@ -55,10 +56,10 @@ export const shrimpTheme = EditorView.theme(
height: '100%', height: '100%',
}, },
'.cm-matchingBracket': { '.cm-matchingBracket': {
backgroundColor: '#FF5370', backgroundColor: 'var(--color-bool)',
}, },
'.cm-nonmatchingBracket': { '.cm-nonmatchingBracket': {
backgroundColor: '#C3E88D', backgroundColor: 'var(--color-string)',
}, },
}, },
{ dark: true } { dark: true }

16
src/editor/runCode.tsx Normal file
View File

@ -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)}` })
}
}

View File

@ -8,6 +8,7 @@ export const highlighting = styleTags({
fn: tags.keyword, fn: tags.keyword,
end: tags.keyword, end: tags.keyword,
':': tags.keyword, ':': tags.keyword,
Regex: tags.regexp,
Operator: tags.operator, Operator: tags.operator,
Command: tags.function(tags.variableName), Command: tags.function(tags.variableName),
'Params/Identifier': tags.definition(tags.variableName), 'Params/Identifier': tags.definition(tags.variableName),

View File

@ -5,7 +5,7 @@
@top Program { item* } @top Program { item* }
@tokens { @tokens {
@precedence { Number "-" RegExp "/"} @precedence { Number "-" Regex "/"}
StringFragment { !['\\$]+ } StringFragment { !['\\$]+ }
NamedArgPrefix { $[a-z]+ "=" } NamedArgPrefix { $[a-z]+ "=" }
@ -19,7 +19,8 @@
colon[closedBy="end", @name="colon"] { ":" } colon[closedBy="end", @name="colon"] { ":" }
end[openedBy="colon", @name="end"] { "end" } end[openedBy="colon", @name="end"] { "end" }
Underscore { "_" } 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] "fn" [@name=keyword]
"if" [@name=keyword] "if" [@name=keyword]
"elsif" [@name=keyword] "elsif" [@name=keyword]
@ -198,7 +199,7 @@ EscapeSeq {
// to go through ambiguousFunctionCall (which is what we want semantically). // 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. // Yes, it is annoying and I gave up trying to use GLR to fix it.
expressionWithoutIdentifier { expressionWithoutIdentifier {
ParenExpr | Word | String | Number | Boolean | RegExp ParenExpr | Word | String | Number | Boolean | Regex | Null
} }
block { block {

View File

@ -16,16 +16,17 @@ export const
EscapeSeq = 26, EscapeSeq = 26,
Number = 27, Number = 27,
Boolean = 28, Boolean = 28,
RegExp = 29, Regex = 29,
FunctionDef = 30, Null = 30,
Params = 32, FunctionDef = 31,
colon = 33, Params = 33,
end = 34, colon = 34,
Underscore = 35, end = 35,
NamedArg = 36, Underscore = 36,
NamedArgPrefix = 37, NamedArg = 37,
IfExpr = 39, NamedArgPrefix = 38,
ThenBlock = 42, IfExpr = 40,
ElsifExpr = 43, ThenBlock = 43,
ElseExpr = 45, ElsifExpr = 44,
Assign = 47 ElseExpr = 46,
Assign = 48

View File

@ -4,20 +4,20 @@ import {tokenizer} from "./tokenizer"
import {highlighting} from "./highlight" import {highlighting} from "./highlight"
export const parser = LRParser.deserialize({ export const parser = LRParser.deserialize({
version: 14, 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`<<IT<<ITO/gQPO'#DbO/lQaO'#DzO0SQPO<<IUOOQO'#DW'#DWO0XQPO<<IYOOQ`,59|,59|OOQ`-E7`-E7`OOQ`AN>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<<He<<He", states: ".WQVQaOOO#OQbO'#CdO#`QPO'#CeO#nQPO'#DjO$nQaO'#CcO$uOSO'#CsOOQ`'#Dn'#DnO%TQPO'#DmO%lQaO'#DxOOQ`'#C{'#C{OOQO'#Dk'#DkO%tQPO'#DjO&SQaO'#D|OOQO'#DU'#DUOOQO'#Dj'#DjO&ZQPO'#DiOOQ`'#Di'#DiOOQ`'#D_'#D_QVQaOOOOQ`'#Dm'#DmOOQ`'#Cb'#CbO&cQaO'#DROOQ`'#Dl'#DlOOQ`'#D`'#D`O&pQbO,58{O'aQaO,59xO&SQaO,59PO&SQaO,59PO'nQbO'#CdO(yQPO'#CeO)ZQPO,58}O)lQPO,58}O)gQPO,58}O*gQPO,58}O*oQaO'#CuO*wQWO'#CvOOOO'#Dr'#DrOOOO'#Da'#DaO+]OSO,59_OOQ`,59_,59_OOQ`'#Db'#DbO+kQaO'#C}O+sQPO,5:dO+xQaO'#DdO+}QPO,58zO,`QPO,5:hO,gQPO,5:hOOQ`,5:T,5:TOOQ`-E7]-E7]OOQ`,59m,59mOOQ`-E7^-E7^OOQO1G/d1G/dOOQO1G.k1G.kO,lQPO1G.kO&SQaO,59UO&SQaO,59UOOQ`1G.i1G.iOOOO,59a,59aOOOO,59b,59bOOOO-E7_-E7_OOQ`1G.y1G.yOOQ`-E7`-E7`O-WQaO1G0OO-hQbO'#CdOOQO,5:O,5:OOOQO-E7b-E7bO.XQaO1G0SOOQO1G.p1G.pO.iQPO1G.pO.sQPO7+%jO.xQaO7+%kOOQO'#DW'#DWOOQO7+%n7+%nO/YQaO7+%oOOQ`<<IU<<IUO/pQPO'#DcO/uQaO'#D{O0]QPO<<IVOOQO'#DX'#DXO0bQPO<<IZOOQ`,59},59}OOQ`-E7a-E7aOOQ`AN>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<<Hf<<Hf",
stateData: "1l~O!ZOS~OPPOQUOkUOlUOmUOoWOx[O!bSO!dTO!m`O~OPcOQUOkUOlUOmUOoWOsdOueO!bSO!dTOY!`XZ!`X[!`X]!`XvWX~O_iO!mWX!qWXrWX~PwOYjOZjO[kO]kO~OYjOZjO[kO]kO!m!]X!q!]Xr!]X~OQUOkUOlUOmUO!bSO!dTO~OPlO~P$POhtO!dwO!frO!gsO~OY!`XZ!`X[!`X]!`X!m!]X!q!]Xr!]X~OPxOqpP~Ov{O!m!]X!q!]Xr!]X~OPcO~P$PO!m!PO!q!PO~OPcOoWOs!RO~P$POPcOoWOsdOueOvTa!mTa!qTa!cTarTa~P$POPPOoWOx[O~P$PO_!`X`!`Xa!`Xb!`Xc!`Xd!`Xe!`Xf!`X!cWX~PwO_!WO`!WOa!WOb!WOc!WOd!WOe!XOf!XO~OYjOZjO[kO]kO~P(UOYjOZjO[kO]kO!c!YO~O!c!YOY!`XZ!`X[!`X]!`X_!`X`!`Xa!`Xb!`Xc!`Xd!`Xe!`Xf!`X~Ov{O!c!YO~OP!ZO!bSO~O!d![O!f![O!g![O!h![O!i![O!j![O~OhtO!d!^O!frO!gsO~OPxOqpX~Oq!`O~OP!aO~Ov{O!mSa!qSa!cSarSa~Oq!dO~P(UOq!dO~OYjOZjO[Xi]Xi!mXi!qXi!cXirXi~OPPOoWOx[O!m!hO~P$POPcOoWOsdOueOvWX!mWX!qWX!cWXrWX~P$POPPOoWOx[O!m!kO~P$PO!c^iq^i~P(UOr!lO~OPPOoWOx[Or!nP~P$POPPOoWOx[Or!nP|!nP!O!nP~P$PO!m!rO~OPPOoWOx[Or!nX|!nX!O!nX~P$POr!tO~Or!yO|!uO!O!xO~Or#OO|!uO!O!xO~Oq#QO~Or#OO~Oq#RO~P(UOq#RO~Or#SO~O!m#TO~O!m#UO~Ok]mZm~", stateData: "1u~O![OS~OPPOQUOkUOlUOmUOnUOpWOy[O!cSO!eTO!n`O~OPcOQUOkUOlUOmUOnUOpWOtdOveO!cSO!eTOY!aXZ!aX[!aX]!aXwWX~O_iO!nWX!rWXsWX~PzOYjOZjO[kO]kO~OYjOZjO[kO]kO!n!^X!r!^Xs!^X~OQUOkUOlUOmUOnUO!cSO!eTO~OPlO~P$VOhtO!ewO!grO!hsO~OY!aXZ!aX[!aX]!aX!n!^X!r!^Xs!^X~OPxOrqP~Ow{O!n!^X!r!^Xs!^X~OPcO~P$VO!n!PO!r!PO~OPcOpWOt!RO~P$VOPcOpWOtdOveOwTa!nTa!rTa!dTasTa~P$VOPPOpWOy[O~P$VO_!aX`!aXa!aXb!aXc!aXd!aXe!aXf!aX!dWX~PzO_!WO`!WOa!WOb!WOc!WOd!WOe!XOf!XO~OYjOZjO[kO]kO~P(_OYjOZjO[kO]kO!d!YO~O!d!YOY!aXZ!aX[!aX]!aX_!aX`!aXa!aXb!aXc!aXd!aXe!aXf!aX~Ow{O!d!YO~OP!ZO!cSO~O!e![O!g![O!h![O!i![O!j![O!k![O~OhtO!e!^O!grO!hsO~OPxOrqX~Or!`O~OP!aO~Ow{O!nSa!rSa!dSasSa~Or!dO~P(_Or!dO~OYjOZjO[Xi]Xi!nXi!rXi!dXisXi~OPPOpWOy[O!n!hO~P$VOPcOpWOtdOveOwWX!nWX!rWX!dWXsWX~P$VOPPOpWOy[O!n!kO~P$VO!d^ir^i~P(_Os!lO~OPPOpWOy[Os!oP~P$VOPPOpWOy[Os!oP}!oP!P!oP~P$VO!n!rO~OPPOpWOy[Os!oX}!oX!P!oX~P$VOs!tO~Os!yO}!uO!P!xO~Os#OO}!uO!P!xO~Or#QO~Os#OO~Or#RO~P(_Or#RO~Os#SO~O!n#TO~O!n#UO~Ok]mZm~",
goto: "+W!qPPPP!r#R#a#g#R$SPPPP$iPPPPPPPP$uP%_%_PPP%cP%xPPP#aPP%{P&X&[&eP&iP%{&o&u&}'T'Z'd'kPPP'q'u(Z(m(s)oPPP*]PPPPP*a*aP*r*z*zd^Obi!`!d!h!k!n#T#URpSiYOSbi{!`!d!h!k!n#T#UXfPhl!a|UOPS[behijkl!W!X!`!a!d!h!k!n!u#T#UR!ZrdRObi!`!d!h!k!n#T#UQnSQ!UjR!VkQpSQ!O[Q!e!XR!|!u}UOPS[behijkl!W!X!`!a!d!h!k!n!u#T#UTtTvd^Obi!`!d!h!k!n#T#UWdPhl!aR!ReRzWe^Obi!`!d!h!k!n#T#UR!j!dQ!q!kQ#V#TR#W#UT!v!q!wQ!z!qR#P!wQbOR!QbUhPl!aR!ShQvTR!]vQyWR!_yW!n!h!k#T#UR!s!nS|ZqR!c|Q!w!qR!}!wTaObS_ObQ!TiQ!g!`Q!i!dZ!m!h!k!n#T#UdZObi!`!d!h!k!n#T#UQqSR!b{XgPhl!adQObi!`!d!h!k!n#T#UWdPhl!aQmSQ}[Q!ReQ!UjQ!VkQ!e!WQ!f!XR!{!udVObi!`!d!h!k!n#T#UfcP[ehjkl!W!X!a!uRoSTuTvoXOPbehil!`!a!d!h!k!n#T#UQ!o!hV!p!k#T#Ue]Obi!`!d!h!k!n#T#U", goto: "+X!rPPPP!s#S#b#h#S$TPPPP$jPPPPPPPP$vP%`%`PPPP%dP%yPPP#bPP%|P&Y&]&fP&jP%|&p&v'O'U'['e'lPPP'r'v([(n(t)pPPP*^PPPPP*b*bP*s*{*{d^Obi!`!d!h!k!n#T#URpSiYOSbi{!`!d!h!k!n#T#UXfPhl!a|UOPS[behijkl!W!X!`!a!d!h!k!n!u#T#UR!ZrdRObi!`!d!h!k!n#T#UQnSQ!UjR!VkQpSQ!O[Q!e!XR!|!u}UOPS[behijkl!W!X!`!a!d!h!k!n!u#T#UTtTvd^Obi!`!d!h!k!n#T#UWdPhl!aR!ReRzWe^Obi!`!d!h!k!n#T#UR!j!dQ!q!kQ#V#TR#W#UT!v!q!wQ!z!qR#P!wQbOR!QbUhPl!aR!ShQvTR!]vQyWR!_yW!n!h!k#T#UR!s!nS|ZqR!c|Q!w!qR!}!wTaObS_ObQ!TiQ!g!`Q!i!dZ!m!h!k!n#T#UdZObi!`!d!h!k!n#T#UQqSR!b{XgPhl!adQObi!`!d!h!k!n#T#UWdPhl!aQmSQ}[Q!ReQ!UjQ!VkQ!e!WQ!f!XR!{!udVObi!`!d!h!k!n#T#UfcP[ehjkl!W!X!a!uRoSTuTvoXOPbehil!`!a!d!h!k!n#T#UQ!o!hV!p!k#T#Ue]Obi!`!d!h!k!n#T#U",
nodeNames: "⚠ Identifier Word Program PipeExpr FunctionCall PositionalArg ParenExpr FunctionCallOrIdentifier BinOp operator operator operator operator ConditionalOp operator operator operator operator operator operator operator operator String StringFragment Interpolation EscapeSeq Number Boolean RegExp FunctionDef keyword Params colon end Underscore NamedArg NamedArgPrefix operator IfExpr keyword ThenBlock ThenBlock ElsifExpr keyword ElseExpr keyword Assign", nodeNames: "⚠ Identifier Word Program PipeExpr FunctionCall PositionalArg ParenExpr FunctionCallOrIdentifier BinOp operator operator operator operator ConditionalOp operator operator operator operator operator operator operator operator String StringFragment Interpolation EscapeSeq Number Boolean Regex Null FunctionDef keyword Params colon end Underscore NamedArg NamedArgPrefix operator IfExpr keyword ThenBlock ThenBlock ElsifExpr keyword ElseExpr keyword Assign",
maxTerm: 79, maxTerm: 80,
nodeProps: [ nodeProps: [
["closedBy", 33,"end"], ["closedBy", 34,"end"],
["openedBy", 34,"colon"] ["openedBy", 35,"colon"]
], ],
propSources: [highlighting], propSources: [highlighting],
skippedNodes: [0], skippedNodes: [0],
repeatNodeCount: 7, repeatNodeCount: 7,
tokenData: "!!{~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#dK|#d#f:m#f#gMn#g#h:m#h#iNe#i#o:m#o#p$_#p#q!!]#q;'S$_;'S;=`$v<%l~$_~O$_~~!!vS$dUhSOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_S$yP;=`<%l$__%TUhS!ZZOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_V%nUhS!mROt$_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!f~~'dO!d~V'kUhS!bROt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_V(UUhS!cROt$_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.ST5WUhSqPOt$_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!g~V8uUhSsROt$_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:ZUuQhSOt$_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$_V<dYeRhSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#o:m#o;'S$_;'S;=`$v<%lO$_V=X^hSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#`:m#`#a>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$_V<dYeRhSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#o:m#o;'S$_;'S;=`$v<%lO$_V=X^hSOt$_uw$_x!_$_!_!`:S!`#O$_#P#T$_#T#`:m#`#a>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!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], tokenizers: [0, 1, 2, 3, tokenizer],
topRules: {"Program":[0,3]}, topRules: {"Program":[0,3]},
tokenPrec: 758 tokenPrec: 767
}) })

View File

@ -2,6 +2,20 @@ import { expect, describe, test } from 'bun:test'
import '../shrimp.grammar' // Importing this so changes cause it to retest! 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', () => { describe('Identifier', () => {
test('parses identifiers with emojis and dashes', () => { test('parses identifiers with emojis and dashes', () => {
expect('moo-😊-34').toMatchTree(` expect('moo-😊-34').toMatchTree(`

View File

@ -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; margin: 0;
padding: 0; padding: 0;
@ -5,8 +54,8 @@
} }
body { body {
background: #40318D; background: var(--bg-output);
color: #7C70DA; color: var(--text-output);
font-family: 'Pixeloid Mono', 'Courier New', monospace; font-family: 'Pixeloid Mono', 'Courier New', monospace;
font-size: 18px; font-size: 18px;
height: 100vh; height: 100vh;
@ -15,7 +64,7 @@ body {
#root { #root {
height: 100vh; height: 100vh;
background: #40318D; background: var(--bg-output);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }

View File

@ -27,3 +27,112 @@ export const toElement = (node: any): HTMLElement => {
export const assertNever = (x: never): never => { export const assertNever = (x: never): never => {
throw new Error(`Unexpected object: ${x}`) 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 += '</span>'.repeat(openSpans)
openSpans = 0
} else if (code === 1) {
// Bold
result += '<span style="font-weight: bold;">'
openSpans++
} else if (code >= 30 && code <= 37) {
// Foreground color
const cssVar = ansiCodeToCssVar(code)
if (cssVar) {
result += `<span style="color: var(${cssVar});">`
openSpans++
}
} else if (code >= 40 && code <= 47) {
// Background color
const cssVar = ansiCodeToCssVar(code)
if (cssVar) {
result += `<span style="background-color: var(${cssVar});">`
openSpans++
}
} else if (code >= 90 && code <= 97) {
// Bright foreground color
const cssVar = ansiCodeToCssVar(code)
if (cssVar) {
result += `<span style="color: var(${cssVar});">`
openSpans++
}
} else if (code >= 100 && code <= 107) {
// Bright background color
const cssVar = ansiCodeToCssVar(code)
if (cssVar) {
result += `<span style="background-color: var(${cssVar});">`
openSpans++
}
}
}
}
// Close any remaining spans
result += '</span>'.repeat(openSpans)
return result as HtmlEscapedString
}