bruh
Version:
The thinnest possible layer between development and production for the modern web.
281 lines (226 loc) • 7.53 kB
JavaScript
const isMetaNode = Symbol.for("bruh meta node")
const isMetaTextNode = Symbol.for("bruh meta text node")
const isMetaElement = Symbol.for("bruh meta element")
const isMetaRawString = Symbol.for("bruh meta raw string")
//#region HTML syntax functions
// https://html.spec.whatwg.org/multipage/syntax.html#void-elements
const voidElements = [
"base",
"link",
"meta",
"hr",
"br",
"wbr",
"area",
"img",
"track",
"embed",
"param",
"source",
"col",
"input"
]
const isVoidElement = element =>
voidElements.includes(element)
// https://html.spec.whatwg.org/multipage/syntax.html#elements-2
// https://html.spec.whatwg.org/multipage/syntax.html#cdata-rcdata-restrictions
// Does not work for https://html.spec.whatwg.org/multipage/syntax.html#raw-text-elements (script and style)
const escapeForElement = x =>
(x + "")
.replace(/&/g, "&")
.replace(/</g, "<")
// https://html.spec.whatwg.org/multipage/syntax.html#syntax-attribute-value
const escapeForDoubleQuotedAttribute = x =>
(x + "")
.replace(/&/g, "&")
.replace(/"/g, """)
// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
const attributesToString = attributes =>
Object.entries(attributes)
.map(([name, value]) =>
value === ""
? ` ${name}`
: ` ${name}="${escapeForDoubleQuotedAttribute(value)}"`
).join("")
//#endregion
// A basic check for if a value is allowed as a meta node's child
// It's responsible for quickly checking the type, not deep validation
const isMetaChild = x =>
// meta nodes, reactives, and DOM nodes
x?.[isMetaNode] ||
x?.[isMetaRawString] ||
// Any array, just assume it contains valid children
Array.isArray(x) ||
// Allow nullish
x == null ||
// Disallow functions and objects
!(typeof x === "function" || typeof x === "object")
// Everything else can be a child when stringified
//#region Meta Nodes that act like lightweight rendering-oriented DOM nodes
// Text nodes have no individual HTML representation
// We emulate this with a custom element <bruh-textnode> with an inline style reset
// These elements can be hydrated very quickly and even be marked with a tag
export class MetaTextNode {
[isMetaNode] = true;
[isMetaTextNode] = true
textContent
tag
constructor(textContent) {
this.textContent = textContent
}
toString() {
const tag = this.tag
? ` tag="${escapeForDoubleQuotedAttribute(this.tag)}"`
: ""
return `<bruh-textnode style="all:unset;display:inline"${tag}>${
escapeForElement(this.textContent)
}</bruh-textnode>`
}
setTag(tag) {
this.tag = tag
return this
}
}
// A light model of an element
export class MetaElement {
[isMetaNode] = true;
[isMetaElement] = true
name
attributes = {}
children = []
constructor(name) {
this.name = name
}
toString() {
const attributes = attributesToString(this.attributes)
// https://html.spec.whatwg.org/multipage/syntax.html#syntax-start-tag
const startTag = `<${this.name}${attributes}>`
if (isVoidElement(this.name))
return startTag
const contents = this.children
.flat(Infinity)
.filter(x => typeof x !== "boolean" && x !== undefined && x !== null)
.map(child =>
(child[isMetaNode] || child[isMetaRawString])
? child.toString()
: escapeForElement(child)
)
.join("")
// https://html.spec.whatwg.org/multipage/syntax.html#end-tags
const endTag = `</${this.name}>`
return startTag + contents + endTag
}
}
// Raw strings can be meta element children, where they bypass string escaping
// This should be avoided in general, but is needed for unsupported HTML features
export class MetaRawString extends String {
[isMetaRawString] = true
constructor(string) {
super(string)
}
}
//#endregion
//#region Meta element helper functions e.g. applyAttributes()
// Merge style rules with an object
export const applyStyles = (element, styles) => {
// Doesn't support proper escaping
// https://www.w3.org/TR/css-syntax-3/#ref-for-parse-a-list-of-declarations%E2%91%A0
// https://www.w3.org/TR/css-syntax-3/#typedef-ident-token
const currentStyles = Object.fromEntries(
(element.attributes.style || "")
.split(";").filter(s => s.length)
.map(declaration => declaration.split(":").map(s => s.trim()))
)
Object.entries(styles)
.forEach(([property, value]) => {
if (value !== undefined)
currentStyles[property] = value
else
delete currentStyles[property]
})
element.attributes.style =
Object.entries(currentStyles)
.map(([property, value]) => `${property}:${value}`)
.join(";")
}
// Merge classes with an object mapping from class names to booleans
export const applyClasses = (element, classes) => {
// Doesn't support proper escaping
// https://html.spec.whatwg.org/multipage/dom.html#global-attributes:classes-2
const currentClasses = new Set(
(element.attributes.class || "")
.split(/\s+/).filter(s => s.length)
)
Object.entries(classes)
.forEach(([name, value]) => {
if (value)
currentClasses.add(name)
else
currentClasses.delete(name)
})
element.attributes.class = [...currentClasses].join(" ")
}
// Merge attributes with an object
export const applyAttributes = (element, attributes) => {
Object.entries(attributes)
.forEach(([name, value]) => {
if (value !== undefined)
element.attributes[name] = value
else
delete element.attributes[name]
})
}
//#endregion
//#region rawString(), t(), and e()
export const rawString = string =>
new MetaRawString(string)
export const t = textContent =>
new MetaTextNode(textContent)
export const e = name => (...variadic) => {
const element = new MetaElement(name)
if (variadic.length === 0)
return element
// If there are no props
if (isMetaChild(variadic[0])) {
element.children.push(...variadic)
return element
}
// If props exist as the first variadic argument
const [props, ...children] = variadic
// The bruh prop is reserved for future use
delete props.bruh
// Apply overloaded props, if possible
if (typeof props.style === "object") {
applyStyles(element, props.style)
delete props.style
}
if (typeof props.class === "object") {
applyClasses(element, props.class)
delete props.class
}
// The rest of the props are attributes
applyAttributes(element, props)
// Add the children to the element
element.children.push(...children)
return element
}
//#endregion
//#region JSX integration
// The function that jsx tags (except fragments) compile to
export const h = (nameOrComponent, props, ...children) => {
// If we are making an element, this is just a wrapper of e()
// This is likely when the JSX tag name begins with a lowercase character
if (typeof nameOrComponent === "string") {
const makeElement = e(nameOrComponent)
return props
? makeElement(props, ...children)
: makeElement(...children)
}
// It must be a component, then, as bruh components are just functions
// Due to JSX, this would mean a function with only one parameter - props
// This object includes the all of the normal props and a "children" key
return nameOrComponent({ ...props, children })
}
// The JSX fragment is made into a bruh fragment (just an array)
export const JSXFragment = ({ children }) => children
//#endregion