initial commit

This commit is contained in:
cyclic
2025-07-20 20:23:49 -06:00
commit 6948e46327
19 changed files with 447 additions and 0 deletions

183
lib/html/src/init.luau Normal file
View File

@@ -0,0 +1,183 @@
export type Node = string | () -> string -- Nodes are what user defined functions return
type Element = () -> string -- Elements are what Components return
type ChildlessElement = () -> Element -- ChildlessElements are when a component is called without children
type Child = string | Element | ChildlessElement
type Component<T> = (props: T) -> (children: { Child }?) -> Element
local function escapeHtml(attribute: string): string
attribute = string.gsub(attribute, "&", "&amp;")
attribute = string.gsub(attribute, "<", "&lt;")
attribute = string.gsub(attribute, ">", "&gt;")
attribute = string.gsub(attribute, "'", "&apos;")
attribute = string.gsub(attribute, '"', "&quot;")
return attribute
end
--[[
Wraps a two-argument function into a "component" nested function.
local Text = createComponent(function(props: { bold: boolean }, children: { string }?)
local contents = table.concat(children, "\n")
if props.bold then
return strong() {
contents
}
else
return contents
end
end)
local boldText = Text({ bold = true }) {
"Hello World",
}
print(boldText()) -- <strong>Hello World</strong>
]]
--
local function createComponent<T>(component: (props: T, children: { string }?) -> Node): Component<T>
return function(props: T)
return function(children: { Child }?)
-- Turn the Child array into a string array. Child may be an Element or Childless Element.
local stringChildren: { string }?
if children then
stringChildren = {}
for i, child in children do
if typeof(child) == "function" then -- Is an Element or Childless Element
-- Child may still be a function, this is when children haven't been passed:
-- Component(props) {
-- OtherComponent(), <-- OtherComponent to be unwrapped twice! Once to get the Element and once to get the string
-- }
local wrappedElement = child()
if typeof(wrappedElement) == "function" then
wrappedElement = wrappedElement()
end
stringChildren[i] = wrappedElement
else
-- Any raw strings passed in as children are escaped.
stringChildren[i] = escapeHtml(child)
end
end
end
-- Return final Element
return function()
local result = component(props, stringChildren)
if typeof(result) == "function" then
return result()
else
return result
end
end
end
end
end
local function createHtmlElement(tag: string)
local function htmlElement(props: { [string]: string }?, children: { string }?)
local attributes = ""
if props then
for key, value in props do
attributes ..= ` {key}="{escapeHtml(value)}"`
end
end
if children then
return `<{tag}{attributes}>{table.concat(children, "\n")}</{tag}>`
else
return `<{tag}{attributes}></{tag}>`
end
end
return createComponent(htmlElement)
end
return {
createComponent = createComponent,
div = createHtmlElement("div"),
span = createHtmlElement("span"),
h1 = createHtmlElement("h1"),
h2 = createHtmlElement("h2"),
h3 = createHtmlElement("h3"),
h4 = createHtmlElement("h4"),
h5 = createHtmlElement("h5"),
h6 = createHtmlElement("h6"),
p = createHtmlElement("p"),
a = createHtmlElement("a"),
img = createHtmlElement("img"),
button = createHtmlElement("button"),
input = createHtmlElement("input"),
label = createHtmlElement("label"),
textarea = createHtmlElement("textarea"),
select = createHtmlElement("select"),
option = createHtmlElement("option"),
ul = createHtmlElement("ul"),
ol = createHtmlElement("ol"),
li = createHtmlElement("li"),
table = createHtmlElement("table"),
tr = createHtmlElement("tr"),
td = createHtmlElement("td"),
th = createHtmlElement("th"),
thead = createHtmlElement("thead"),
tbody = createHtmlElement("tbody"),
tfoot = createHtmlElement("tfoot"),
form = createHtmlElement("form"),
br = createHtmlElement("br"),
hr = createHtmlElement("hr"),
strong = createHtmlElement("strong"),
b = createHtmlElement("b"),
em = createHtmlElement("em"),
i = createHtmlElement("i"),
u = createHtmlElement("u"),
s = createHtmlElement("s"),
sup = createHtmlElement("sup"),
sub = createHtmlElement("sub"),
small = createHtmlElement("small"),
code = createHtmlElement("code"),
pre = createHtmlElement("pre"),
blockquote = createHtmlElement("blockquote"),
nav = createHtmlElement("nav"),
header = createHtmlElement("header"),
footer = createHtmlElement("footer"),
section = createHtmlElement("section"),
article = createHtmlElement("article"),
aside = createHtmlElement("aside"),
main = createHtmlElement("main"),
details = createHtmlElement("details"),
summary = createHtmlElement("summary"),
dialog = createHtmlElement("dialog"),
time = createHtmlElement("time"),
address = createHtmlElement("address"),
mark = createHtmlElement("mark"),
progress = createHtmlElement("progress"),
meter = createHtmlElement("meter"),
caption = createHtmlElement("caption"),
figure = createHtmlElement("figure"),
figcaption = createHtmlElement("figcaption"),
legend = createHtmlElement("legend"),
fieldset = createHtmlElement("fieldset"),
dfn = createHtmlElement("dfn"),
kbd = createHtmlElement("kbd"),
var = createHtmlElement("var"),
cite = createHtmlElement("cite"),
q = createHtmlElement("q"),
html = createHtmlElement("html"),
head = createHtmlElement("head"),
title = createHtmlElement("title"),
meta = createHtmlElement("meta"),
link = createHtmlElement("link"),
style = createHtmlElement("style"),
body = createHtmlElement("body"),
script = createHtmlElement("script"),
noscript = createHtmlElement("noscript"),
audio = createHtmlElement("audio"),
video = createHtmlElement("video"),
source = createHtmlElement("source"),
track = createHtmlElement("track"),
iframe = createHtmlElement("iframe"),
canvas = createHtmlElement("canvas"),
svg = createHtmlElement("svg"),
}