shrimp/src/compiler/tests/ribbit.test.ts
2025-11-06 21:26:37 -08:00

115 lines
3.5 KiB
TypeScript

import { expect, describe, test, beforeEach } from 'bun:test'
const buffer: string[] = []
const ribbitGlobals = {
ribbit: async (cb: Function) => {
await cb()
return buffer.join("\n")
},
tag: async (tagFn: Function, atDefaults = {}) => {
return (atNamed = {}, ...args: any[]) => tagFn(Object.assign({}, atDefaults, atNamed), ...args)
},
head: (atNamed: {}, ...args: any[]) => tag('head', atNamed, ...args),
title: (atNamed: {}, ...args: any[]) => tag('title', atNamed, ...args),
meta: (atNamed: {}, ...args: any[]) => tag('meta', atNamed, ...args),
p: (atNamed: {}, ...args: any[]) => tag('p', atNamed, ...args),
h1: (atNamed: {}, ...args: any[]) => tag('h1', atNamed, ...args),
h2: (atNamed: {}, ...args: any[]) => tag('h2', atNamed, ...args),
b: (atNamed: {}, ...args: any[]) => tag('b', atNamed, ...args),
ul: (atNamed: {}, ...args: any[]) => tag('ul', atNamed, ...args),
li: (atNamed: {}, ...args: any[]) => tag('li', atNamed, ...args),
nospace: () => NOSPACE_TOKEN,
echo: (...args: any[]) => console.log(...args)
}
function raw(fn: Function) { (fn as any).raw = true }
const tagBlock = async (tagName: string, props = {}, fn: Function) => {
const attrs = Object.entries(props).map(([key, value]) => `${key}="${value}"`)
const space = attrs.length ? ' ' : ''
buffer.push(`<${tagName}${space}${attrs.join(' ')}>`)
await fn()
buffer.push(`</${tagName}>`)
}
const tagCall = (tagName: string, atNamed = {}, ...args: any[]) => {
const attrs = Object.entries(atNamed).map(([key, value]) => `${key}="${value}"`)
const space = attrs.length ? ' ' : ''
const children = args
.reverse()
.map(a => a === TAG_TOKEN ? buffer.pop() : a)
.reverse().join(' ')
.replaceAll(` ${NOSPACE_TOKEN} `, '')
if (SELF_CLOSING.includes(tagName))
buffer.push(`<${tagName}${space}${attrs.join(' ')} />`)
else
buffer.push(`<${tagName}${space}${attrs.join(' ')}>${children}</${tagName}>`)
}
const tag = async (tagName: string, atNamed = {}, ...args: any[]) => {
if (typeof args[0] === 'function') {
await tagBlock(tagName, atNamed, args[0])
} else {
tagCall(tagName, atNamed, ...args)
return TAG_TOKEN
}
}
const NOSPACE_TOKEN = '!!ribbit-nospace!!'
const TAG_TOKEN = '!!ribbit-tag!!'
const SELF_CLOSING = ["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"]
describe('ribbit', () => {
beforeEach(() => buffer.length = 0)
test('head tag', () => {
expect(`
ribbit:
head:
title What up
meta charset=UTF-8
meta name=viewport content='width=device-width, initial-scale=1, viewport-fit=cover'
end
end
`).toEvaluateTo(`<head>
<title>What up</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
</head>`, ribbitGlobals)
})
test('custom tags', () => {
expect(`
list = tag ul class='list'
ribbit:
list:
li border-bottom='1px solid black' one
li two
li three
end
end`).toEvaluateTo(`<ul class="list">
<li border-bottom="1px solid black">one</li>
<li>two</li>
<li>three</li>
</ul>`, ribbitGlobals)
})
test('inline expressions', () => {
expect(`
ribbit:
p class=container:
h1 class=bright style='font-family: helvetica' Heya
h2 man that is (b wild) (nospace) !
p Double the fun.
end
end`).toEvaluateTo(
`<p class="container">
<h1 class="bright" style="font-family: helvetica">Heya</h1>
<h2>man that is <b>wild</b>!</h2>
<p>Double the fun.</p>
</p>`, ribbitGlobals)
})
})