From 4ff6368a8c8495c1f6ee12e19b9086eea40dbc8e Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Wed, 5 Nov 2025 16:01:19 -0800 Subject: [PATCH] add magical style tags --- README.md | 8 +++++--- src/ribbit.ts | 36 ++++++++++++++++++++++++++---------- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 909a53b..8cb4267 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,10 @@ - [x] Serve static files in pub/ - [x] Working CLI - [x] Nice error messages -- [ ] dev mode / prod mode (caching, errors) -- [ ] Form body parsing for POST -- [ ] auto-serve index.sh for subdirectories (`/users` -> `/users/index.sh` if dir) +- [x] attrs vs styles (h1 color=blue -> style="color:blue") +- [x] GET params (`params.whatever`) +- [x] POST params (`params.whatever`) - [x] Layouts +- [ ] dev mode / prod mode (caching, errors) +- [ ] auto-serve index.sh for subdirectories (`/users` -> `/users/index.sh` if dir) - [ ] Caching \ No newline at end of file diff --git a/src/ribbit.ts b/src/ribbit.ts index d78de72..48bab62 100644 --- a/src/ribbit.ts +++ b/src/ribbit.ts @@ -4,9 +4,9 @@ import { AnsiUp } from 'ansi_up' const buffer: string[] = [] const NOSPACE_TOKEN = '!!ribbit-nospace!!' const TAG_TOKEN = '!!ribbit-tag!!' -const SELF_CLOSING = await Bun.file('./data/self-closing.json').json() +const SELF_CLOSING = new Set(await Bun.file('./data/self-closing.json').json()) const HTML5_TAGS = await Bun.file('./data/tags.json').json() -const HTML5_ATTRS = await Bun.file('./data/attrs.json').json() +const HTML5_ATTRS = new Set(await Bun.file('./data/attrs.json').json()) export function wrapCode(code: string): string { return "ribbit do:\n " + code + "\nend" @@ -55,10 +55,10 @@ const tag = async (tagName: string, atNamed = {}, ...args: any[]) => { } const tagBlock = async (tagName: string, props = {}, fn: Function) => { - const attrs = Object.entries(props).map(([key, value]) => `${key}="${value}"`) + const attrs = propsToAttrs(tagName, props) const space = attrs.length ? ' ' : '' - buffer.push(`<${tagName}${space}${attrs.join(' ')}>`) + buffer.push(`<${tagName}${space}${attrs}>`) await fn() buffer.push(``) } @@ -66,9 +66,7 @@ const tagBlock = async (tagName: string, props = {}, fn: Function) => { const tagCall = (tagName: string, atNamed = {}, ...args: any[]) => { args = args.map(arg => typeof arg === 'function' ? arg.tagName : arg) - const attrs = Object.entries(atNamed) - .filter(([key, value]) => value) - .map(([key, value]) => `${key}="${typeof value === 'function' ? (value as any).tagName : value}"`) + const attrs = propsToAttrs(tagName, atNamed) const space = attrs.length ? ' ' : '' const children = args .reverse() @@ -76,8 +74,26 @@ const tagCall = (tagName: string, atNamed = {}, ...args: any[]) => { .reverse().join(' ') .replaceAll(` ${NOSPACE_TOKEN} `, '') - if (SELF_CLOSING.includes(tagName)) - buffer.push(`<${tagName}${space}${attrs.join(' ')} />`) + if (SELF_CLOSING.has(tagName)) + buffer.push(`<${tagName}${space}${attrs} />`) else - buffer.push(`<${tagName}${space}${attrs.join(' ')}>${children}`) + buffer.push(`<${tagName}${space}${attrs}>${children}`) +} + +const propsToAttrs = (tagName: string, props: Record): string => { + let attrs = [] + let styles = [] + + for (const [key, value] of Object.entries(props).filter(([_, value]) => value)) { + if (HTML5_ATTRS.has(key) || HTML5_TAGS[tagName].includes(key)) { + attrs.push(`${key}="${typeof value === 'function' ? (value as any).tagName : value}"`) + } else { + styles.push(`${key}: ${value}`) + } + } + + if (styles.length) + attrs.push(`style="${styles.join('; ')}"`) + + return attrs.join(' ') } \ No newline at end of file