Compare commits

..

5 Commits

Author SHA1 Message Date
4ff6368a8c add magical style tags 2025-11-05 16:01:19 -08:00
260e2c7480 use data files for tag info 2025-11-05 15:50:08 -08:00
6114e5cf5e false/null = no attr 2025-11-05 15:46:25 -08:00
56de9709e9 update shrimp 2025-11-05 15:45:14 -08:00
201cba6542 dirty 2025-11-05 15:45:10 -08:00
6 changed files with 594 additions and 28 deletions

View File

@ -18,8 +18,10 @@
- [x] Serve static files in pub/ - [x] Serve static files in pub/
- [x] Working CLI - [x] Working CLI
- [x] Nice error messages - [x] Nice error messages
- [ ] dev mode / prod mode (caching, errors) - [x] attrs vs styles (h1 color=blue -> style="color:blue")
- [ ] Form body parsing for POST - [x] GET params (`params.whatever`)
- [ ] auto-serve index.sh for subdirectories (`/users` -> `/users/index.sh` if dir) - [x] POST params (`params.whatever`)
- [x] Layouts - [x] Layouts
- [ ] dev mode / prod mode (caching, errors)
- [ ] auto-serve index.sh for subdirectories (`/users` -> `/users/index.sh` if dir)
- [ ] Caching - [ ] Caching

View File

@ -61,9 +61,9 @@
"hono": ["hono@4.10.4", "", {}, "sha512-YG/fo7zlU3KwrBL5vDpWKisLYiM+nVstBQqfr7gCPbSYURnNEP9BDxEMz8KfsDR9JX0lJWDRNc6nXX31v7ZEyg=="], "hono": ["hono@4.10.4", "", {}, "sha512-YG/fo7zlU3KwrBL5vDpWKisLYiM+nVstBQqfr7gCPbSYURnNEP9BDxEMz8KfsDR9JX0lJWDRNc6nXX31v7ZEyg=="],
"reefvm": ["reefvm@git+https://git.nose.space/defunkt/reefvm#bd1736b474b1f3d89c4095dbfa0ff8d858f91241", { "peerDependencies": { "typescript": "^5" } }, "bd1736b474b1f3d89c4095dbfa0ff8d858f91241"], "reefvm": ["reefvm@git+https://git.nose.space/defunkt/reefvm#bffb83a5280a4d74e424c4e0f4fbd46f790227a3", { "peerDependencies": { "typescript": "^5" } }, "bffb83a5280a4d74e424c4e0f4fbd46f790227a3"],
"shrimp": ["shrimp@git+https://git.nose.space/probablycorey/shrimp#f4a065beae7736c1aad99bef5e39c77f84678a7b", { "dependencies": { "@codemirror/view": "^6.38.3", "@lezer/generator": "^1.8.0", "bun-plugin-tailwind": "^0.0.15", "codemirror": "^6.0.2", "hono": "^4.9.8", "reefvm": "git+https://git.nose.space/defunkt/reefvm", "tailwindcss": "^4.1.11" } }, "f4a065beae7736c1aad99bef5e39c77f84678a7b"], "shrimp": ["shrimp@git+https://git.nose.space/probablycorey/shrimp#5ff78d49c1431e55eecb0ce78c644205e7db5a90", { "dependencies": { "@codemirror/view": "^6.38.3", "@lezer/generator": "^1.8.0", "bun-plugin-tailwind": "^0.0.15", "codemirror": "^6.0.2", "hono": "^4.9.8", "reefvm": "git+https://git.nose.space/defunkt/reefvm", "tailwindcss": "^4.1.11" } }, "5ff78d49c1431e55eecb0ce78c644205e7db5a90"],
"style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="], "style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="],

155
data/attrs.json Normal file
View File

@ -0,0 +1,155 @@
[
"accesskey",
"autocapitalize",
"autofocus",
"class",
"contenteditable",
"dir",
"draggable",
"enterkeyhint",
"hidden",
"id",
"inert",
"inputmode",
"is",
"itemid",
"itemprop",
"itemref",
"itemscope",
"itemtype",
"lang",
"nonce",
"part",
"popover",
"slot",
"spellcheck",
"style",
"tabindex",
"title",
"translate",
"data-*",
"aria-activedescendant",
"aria-atomic",
"aria-autocomplete",
"aria-busy",
"aria-checked",
"aria-colcount",
"aria-colindex",
"aria-colspan",
"aria-controls",
"aria-current",
"aria-describedby",
"aria-details",
"aria-disabled",
"aria-errormessage",
"aria-expanded",
"aria-flowto",
"aria-haspopup",
"aria-hidden",
"aria-invalid",
"aria-keyshortcuts",
"aria-label",
"aria-labelledby",
"aria-level",
"aria-live",
"aria-modal",
"aria-multiline",
"aria-multiselectable",
"aria-orientation",
"aria-owns",
"aria-placeholder",
"aria-posinset",
"aria-pressed",
"aria-readonly",
"aria-relevant",
"aria-required",
"aria-roledescription",
"aria-rowcount",
"aria-rowindex",
"aria-rowspan",
"aria-selected",
"aria-setsize",
"aria-sort",
"aria-valuemax",
"aria-valuemin",
"aria-valuenow",
"aria-valuetext",
"onabort",
"onauxclick",
"onbeforeinput",
"onbeforematch",
"onbeforetoggle",
"onblur",
"oncancel",
"oncanplay",
"oncanplaythrough",
"onchange",
"onclick",
"onclose",
"oncontextlost",
"oncontextmenu",
"oncontextrestored",
"oncopy",
"oncuechange",
"oncut",
"ondblclick",
"ondrag",
"ondragend",
"ondragenter",
"ondragleave",
"ondragover",
"ondragstart",
"ondrop",
"ondurationchange",
"onemptied",
"onended",
"onerror",
"onfocus",
"onformdata",
"oninput",
"oninvalid",
"onkeydown",
"onkeypress",
"onkeyup",
"onload",
"onloadeddata",
"onloadedmetadata",
"onloadstart",
"onmousedown",
"onmouseenter",
"onmouseleave",
"onmousemove",
"onmouseout",
"onmouseover",
"onmouseup",
"onmousewheel",
"onpaste",
"onpause",
"onplay",
"onplaying",
"onprogress",
"onratechange",
"onreset",
"onresize",
"onscroll",
"onscrollend",
"onsecuritypolicyviolation",
"onseeked",
"onseeking",
"onselect",
"onselectionchange",
"onselectstart",
"onslotchange",
"onstalled",
"onsubmit",
"onsuspend",
"ontimeupdate",
"ontoggle",
"onvolumechange",
"onwaiting",
"onwebkitanimationend",
"onwebkitanimationiteration",
"onwebkitanimationstart",
"onwebkittransitionend",
"onwheel"
]

16
data/self-closing.json Normal file
View File

@ -0,0 +1,16 @@
[
"area",
"base",
"br",
"col",
"embed",
"hr",
"img",
"input",
"link",
"meta",
"param",
"source",
"track",
"wbr"
]

387
data/tags.json Normal file
View File

@ -0,0 +1,387 @@
{
"a": [
"href",
"target",
"download",
"rel",
"hreflang",
"referrerpolicy",
"ping"
],
"abbr": [],
"address": [],
"area": [
"alt",
"coords",
"shape",
"href",
"target",
"download",
"rel",
"referrerpolicy",
"ping"
],
"article": [],
"aside": [],
"audio": [
"src",
"crossorigin",
"preload",
"autoplay",
"loop",
"muted",
"controls"
],
"b": [],
"base": [
"href",
"target"
],
"bdi": [],
"bdo": [],
"blockquote": [
"cite"
],
"body": [],
"br": [],
"button": [
"disabled",
"form",
"formaction",
"formenctype",
"formmethod",
"formnovalidate",
"formtarget",
"name",
"type",
"value"
],
"canvas": [
"width",
"height"
],
"caption": [],
"cite": [],
"code": [],
"col": [
"span"
],
"colgroup": [
"span"
],
"data": [
"value"
],
"datalist": [],
"dd": [],
"del": [
"cite",
"datetime"
],
"details": [
"open"
],
"dfn": [],
"dialog": [
"open"
],
"div": [],
"dl": [],
"dt": [],
"em": [],
"embed": [
"src",
"type",
"width",
"height"
],
"fieldset": [
"disabled",
"form",
"name"
],
"figcaption": [],
"figure": [],
"footer": [],
"form": [
"accept-charset",
"action",
"autocomplete",
"enctype",
"method",
"name",
"novalidate",
"target"
],
"h1": [],
"h2": [],
"h3": [],
"h4": [],
"h5": [],
"h6": [],
"head": [],
"header": [],
"hr": [],
"html": [],
"i": [],
"iframe": [
"src",
"srcdoc",
"name",
"width",
"height",
"allow",
"loading",
"referrerpolicy",
"sandbox",
"csp"
],
"img": [
"alt",
"src",
"srcset",
"sizes",
"crossorigin",
"usemap",
"ismap",
"width",
"height",
"referrerpolicy",
"decoding",
"loading",
"fetchpriority"
],
"input": [
"accept",
"alt",
"autocomplete",
"checked",
"dirname",
"disabled",
"form",
"formaction",
"formenctype",
"formmethod",
"formnovalidate",
"formtarget",
"height",
"list",
"max",
"maxlength",
"min",
"minlength",
"multiple",
"name",
"pattern",
"placeholder",
"readonly",
"required",
"size",
"src",
"step",
"type",
"value",
"width"
],
"ins": [
"cite",
"datetime"
],
"kbd": [],
"label": [
"for",
"form"
],
"legend": [],
"li": [
"value"
],
"link": [
"href",
"crossorigin",
"rel",
"as",
"media",
"hreflang",
"integrity",
"referrerpolicy",
"sizes",
"imagesrcset",
"imagesizes",
"disabled",
"fetchpriority"
],
"main": [],
"map": [
"name"
],
"mark": [],
"meta": [
"name",
"http-equiv",
"content",
"charset",
"media"
],
"meter": [
"value",
"min",
"max",
"low",
"high",
"optimum",
"form"
],
"nav": [],
"noscript": [],
"object": [
"data",
"type",
"name",
"form",
"width",
"height",
"usemap",
"typemustmatch"
],
"ol": [
"reversed",
"start",
"type"
],
"optgroup": [
"disabled",
"label"
],
"option": [
"disabled",
"label",
"selected",
"value"
],
"output": [
"for",
"form",
"name"
],
"p": [],
"picture": [],
"pre": [],
"progress": [
"value",
"max"
],
"q": [
"cite"
],
"rp": [],
"rt": [],
"ruby": [],
"s": [],
"samp": [],
"script": [
"src",
"type",
"async",
"defer",
"crossorigin",
"integrity",
"nomodule",
"referrerpolicy",
"fetchpriority"
],
"section": [],
"select": [
"autocomplete",
"disabled",
"form",
"multiple",
"name",
"required",
"size"
],
"slot": [
"name"
],
"small": [],
"source": [
"src",
"type",
"srcset",
"sizes",
"media",
"width",
"height"
],
"span": [],
"strong": [],
"style": [
"media",
"nonce",
"type"
],
"sub": [],
"summary": [],
"sup": [],
"table": [],
"tbody": [],
"td": [
"colspan",
"rowspan",
"headers"
],
"template": [],
"textarea": [
"cols",
"rows",
"wrap",
"autocomplete",
"dirname",
"disabled",
"form",
"maxlength",
"minlength",
"name",
"placeholder",
"readonly",
"required"
],
"tfoot": [],
"th": [
"colspan",
"rowspan",
"headers",
"scope",
"abbr"
],
"thead": [],
"time": [
"datetime"
],
"title": [],
"tr": [],
"track": [
"default",
"kind",
"label",
"src",
"srclang"
],
"u": [],
"ul": [],
"var": [],
"video": [
"src",
"crossorigin",
"preload",
"autoplay",
"loop",
"muted",
"controls",
"width",
"height",
"poster",
"playsinline"
],
"wbr": []
}

View File

@ -4,22 +4,9 @@ import { AnsiUp } from 'ansi_up'
const buffer: string[] = [] const buffer: string[] = []
const NOSPACE_TOKEN = '!!ribbit-nospace!!' const NOSPACE_TOKEN = '!!ribbit-nospace!!'
const TAG_TOKEN = '!!ribbit-tag!!' const TAG_TOKEN = '!!ribbit-tag!!'
const SELF_CLOSING = ["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"] const SELF_CLOSING = new Set(await Bun.file('./data/self-closing.json').json())
const HTML5_TAGS = [ const HTML5_TAGS = await Bun.file('./data/tags.json').json()
"a", "abbr", "address", "area", "article", "aside", "audio", "b", "base", const HTML5_ATTRS = new Set(await Bun.file('./data/attrs.json').json())
"bdi", "bdo", "blockquote", "body", "br", "button", "canvas", "caption",
"cite", "code", "col", "colgroup", "data", "datalist", "dd", "del",
"details", "dfn", "dialog", "div", "dl", "dt", "em", "embed", "fieldset",
"figcaption", "figure", "footer", "form", "h1", "h2", "h3", "h4", "h5",
"h6", "head", "header", "hgroup", "hr", "html", "i", "iframe", "img",
"input", "ins", "kbd", "label", "legend", "li", "link", "main", "map",
"mark", "meta", "meter", "nav", "noscript", "object", "ol", "optgroup",
"option", "output", "p", "picture", "pre", "progress", "q", "rp", "rt",
"ruby", "s", "samp", "script", "section", "select", "slot", "small",
"source", "span", "strong", "style", "sub", "summary", "sup", "svg",
"table", "tbody", "td", "template", "textarea", "tfoot", "th", "thead",
"time", "title", "tr", "track", "u", "ul", "var", "video", "wbr"
]
export function wrapCode(code: string): string { export function wrapCode(code: string): string {
return "ribbit do:\n " + code + "\nend" return "ribbit do:\n " + code + "\nend"
@ -52,10 +39,11 @@ export const ribbitGlobals = {
} }
} }
for (const name of HTML5_TAGS) { for (const name of Object.keys(HTML5_TAGS)) {
(ribbitGlobals as any)[name] = (atNamed: {}, ...args: any[]) => tag(name, atNamed, ...args) (ribbitGlobals as any)[name] = (atNamed: {}, ...args: any[]) => tag(name, atNamed, ...args)
; (ribbitGlobals as any)[name].tagName = name ; (ribbitGlobals as any)[name].tagName = name
} }
; (ribbitGlobals as any)['text'].tagName = 'text'
const tag = async (tagName: string, atNamed = {}, ...args: any[]) => { const tag = async (tagName: string, atNamed = {}, ...args: any[]) => {
if (typeof args[0] === 'function') if (typeof args[0] === 'function')
@ -67,10 +55,10 @@ const tag = async (tagName: string, atNamed = {}, ...args: any[]) => {
} }
const tagBlock = async (tagName: string, props = {}, fn: Function) => { 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 ? ' ' : '' const space = attrs.length ? ' ' : ''
buffer.push(`<${tagName}${space}${attrs.join(' ')}>`) buffer.push(`<${tagName}${space}${attrs}>`)
await fn() await fn()
buffer.push(`</${tagName}>`) buffer.push(`</${tagName}>`)
} }
@ -78,7 +66,7 @@ const tagBlock = async (tagName: string, props = {}, fn: Function) => {
const tagCall = (tagName: string, atNamed = {}, ...args: any[]) => { const tagCall = (tagName: string, atNamed = {}, ...args: any[]) => {
args = args.map(arg => typeof arg === 'function' ? arg.tagName : arg) args = args.map(arg => typeof arg === 'function' ? arg.tagName : arg)
const attrs = Object.entries(atNamed).map(([key, value]) => `${key}="${value}"`) const attrs = propsToAttrs(tagName, atNamed)
const space = attrs.length ? ' ' : '' const space = attrs.length ? ' ' : ''
const children = args const children = args
.reverse() .reverse()
@ -86,8 +74,26 @@ const tagCall = (tagName: string, atNamed = {}, ...args: any[]) => {
.reverse().join(' ') .reverse().join(' ')
.replaceAll(` ${NOSPACE_TOKEN} `, '') .replaceAll(` ${NOSPACE_TOKEN} `, '')
if (SELF_CLOSING.includes(tagName)) if (SELF_CLOSING.has(tagName))
buffer.push(`<${tagName}${space}${attrs.join(' ')} />`) buffer.push(`<${tagName}${space}${attrs} />`)
else else
buffer.push(`<${tagName}${space}${attrs.join(' ')}>${children}</${tagName}>`) buffer.push(`<${tagName}${space}${attrs}>${children}</${tagName}>`)
}
const propsToAttrs = (tagName: string, props: Record<string, any>): 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(' ')
} }