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']]
}
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])

View File

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

View File

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

View File

@ -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<typeof setTimeout>
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 = {

View File

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

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

View File

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

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,
end: tags.keyword,
':': tags.keyword,
Regex: tags.regexp,
Operator: tags.operator,
Command: tags.function(tags.variableName),
'Params/Identifier': tags.definition(tags.variableName),

View File

@ -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 {

View File

@ -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

View File

@ -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`<<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",
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~",
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",
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",
maxTerm: 79,
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: "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: "+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 Regex Null FunctionDef keyword Params colon end Underscore NamedArg NamedArgPrefix operator IfExpr keyword ThenBlock ThenBlock ElsifExpr keyword ElseExpr keyword Assign",
maxTerm: 80,
nodeProps: [
["closedBy", 33,"end"],
["openedBy", 34,"colon"]
["closedBy", 34,"end"],
["openedBy", 35,"colon"]
],
propSources: [highlighting],
skippedNodes: [0],
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],
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!
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(`

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;
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;
}

View File

@ -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 += '</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
}