This commit is contained in:
Chris Wanstrath 2025-11-01 22:40:25 -07:00 committed by Chris Wanstrath
parent da77bf1686
commit a161679903
2 changed files with 113 additions and 81 deletions

View File

@ -53,84 +53,3 @@ signals.trap 'EXIT':
end`).toEvaluateTo(['EXIT', true])
})
})
describe('ribbit', () => {
test('head tag', () => {
expect(`
head:
title What up
meta charSet=UTF-8
meta name=viewport content='width=device-width, initial-scale=1, viewport-fit=cover'
end`).toEvaluateTo(`head`, {
head: () => 'head'
})
})
test('li', () => {
expect(`
list:
li border-bottom='1px solid black' one
li two
li three
end`).toEvaluateTo(`list`, {
list: () => 'list'
})
})
test('inline expressions', () => {
const buffer: string[] = []
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 === null ? buffer.pop() : a)
.reverse().join(' ')
.replaceAll(' !!ribbit-nospace!! ', '')
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)
}
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>`, {
ribbit: async (cb: Function) => {
await cb()
return buffer.join("\n")
},
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),
nospace: () => '!!ribbit-nospace!!',
join: (...args: string[]) => args.join(''),
})
})
})

View File

@ -0,0 +1,113 @@
import { expect, describe, test, beforeEach } from 'bun:test'
import { type Value } from 'reefvm'
const buffer: string[] = []
const ribbitGlobals = {
ribbit: async (cb: Function) => {
await cb()
return buffer.join("\n")
},
tag: (tagName: string, atDefaults = {}) => {
return (atNamed = {}, ...args: any[]) => tag(tagName, 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 === null ? 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)
}
const NOSPACE_TOKEN = '!!ribbit-nospace!!'
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)
})
})