initial commit
This commit is contained in:
3
lib/html/.vscode/settings.json
vendored
Normal file
3
lib/html/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"luau-lsp.fflags.enableNewSolver": true
|
||||
}
|
21
lib/html/LICENSE
Normal file
21
lib/html/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Nick Winans
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
28
lib/html/README.md
Normal file
28
lib/html/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# HTMLuau
|
||||
|
||||
Simple HTML templating in Luau.
|
||||
|
||||
```luau
|
||||
html({ lang = "en" }) {
|
||||
head() {
|
||||
title() {
|
||||
"Hello World",
|
||||
},
|
||||
},
|
||||
body() {
|
||||
h1() {
|
||||
"Hello World",
|
||||
},
|
||||
button({ onclick = "alert('Hello World!')" }) {
|
||||
"Click Me",
|
||||
},
|
||||
p() {
|
||||
"This is a simple HTML page generated using Luau.",
|
||||
"Lines are concatenated.",
|
||||
"So you can write sentences on separate lines.",
|
||||
"<html> is properly excaped.",
|
||||
},
|
||||
button(),
|
||||
},
|
||||
}
|
||||
```
|
52
lib/html/examples/cards.luau
Normal file
52
lib/html/examples/cards.luau
Normal file
@@ -0,0 +1,52 @@
|
||||
local net = require("@lute/net")
|
||||
|
||||
local htmluau = require("../src")
|
||||
|
||||
local html = htmluau.html
|
||||
local div = htmluau.div
|
||||
local head = htmluau.head
|
||||
local title = htmluau.title
|
||||
local body = htmluau.body
|
||||
local img = htmluau.img
|
||||
local h2 = htmluau.h2
|
||||
local p = htmluau.p
|
||||
|
||||
local createComponent = htmluau.createComponent
|
||||
|
||||
local Card = createComponent(function(props: { image: string, title: string, content: string })
|
||||
return div({ style = "display: flex; flex-direction: column; width: 200px" }) {
|
||||
img({ src = props.image, alt = props.title }),
|
||||
h2() {
|
||||
props.title,
|
||||
},
|
||||
p() {
|
||||
props.content,
|
||||
},
|
||||
}
|
||||
end)
|
||||
|
||||
local website = html({ lang = "en" }) {
|
||||
head() {
|
||||
title() {
|
||||
"Card Example",
|
||||
},
|
||||
},
|
||||
body({ style = "font-family: sans-serif; margin: 0; padding: 20px" }) {
|
||||
div({ style = "display: flex; gap: 10px" }) {
|
||||
Card({
|
||||
image = "https://picsum.photos/id/237/200",
|
||||
title = "Card Title",
|
||||
content = "This is the content of the card.",
|
||||
}),
|
||||
Card({
|
||||
image = "https://picsum.photos/id/234/200",
|
||||
title = "Another Card Title",
|
||||
content = "This is the content of another card.",
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
net.serve(function()
|
||||
return website()
|
||||
end)
|
38
lib/html/examples/html.luau
Normal file
38
lib/html/examples/html.luau
Normal file
@@ -0,0 +1,38 @@
|
||||
local net = require("@lute/net")
|
||||
|
||||
local htmluau = require("../src")
|
||||
|
||||
local html = htmluau.html
|
||||
local head = htmluau.head
|
||||
local title = htmluau.title
|
||||
local body = htmluau.body
|
||||
local h1 = htmluau.h1
|
||||
local button = htmluau.button
|
||||
local p = htmluau.p
|
||||
|
||||
local website = html({ lang = "en" }) {
|
||||
head() {
|
||||
title() {
|
||||
"Hello World",
|
||||
},
|
||||
},
|
||||
body() {
|
||||
h1() {
|
||||
"Hello World",
|
||||
},
|
||||
button({ onclick = "alert('Hello World!')" }) {
|
||||
"Click Me",
|
||||
},
|
||||
p() {
|
||||
"This is a simple HTML page generated using Luau.",
|
||||
"Lines are concatenated.",
|
||||
"So you can write sentences on separate lines.",
|
||||
"<html> is properly excaped.",
|
||||
},
|
||||
button(),
|
||||
},
|
||||
}
|
||||
|
||||
net.serve(function()
|
||||
return website()
|
||||
end)
|
3
lib/html/foreman.toml
Normal file
3
lib/html/foreman.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
[tools]
|
||||
stylua = { github = "JohnnyMorganz/StyLua", version = "2.0.2" }
|
||||
lute = { github = "luau-lang/lute", version = "0.1.0-nightly.20250508" }
|
1
lib/html/init.luau
Normal file
1
lib/html/init.luau
Normal file
@@ -0,0 +1 @@
|
||||
return require('./html/src')
|
183
lib/html/src/init.luau
Normal file
183
lib/html/src/init.luau
Normal 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, "&", "&")
|
||||
attribute = string.gsub(attribute, "<", "<")
|
||||
attribute = string.gsub(attribute, ">", ">")
|
||||
attribute = string.gsub(attribute, "'", "'")
|
||||
attribute = string.gsub(attribute, '"', """)
|
||||
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"),
|
||||
}
|
1
lib/html/stylua.toml
Normal file
1
lib/html/stylua.toml
Normal file
@@ -0,0 +1 @@
|
||||
call_parentheses = "Input"
|
Reference in New Issue
Block a user