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

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
dist/

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "html"]
path = html
url = https://github.com/Nicell/htmluau.git

6
.luaurc Normal file
View File

@@ -0,0 +1,6 @@
{
"aliases": {
"lune": "~/.lune/.typedefs/0.10.1/",
"lib": "./lib/"
}
}

18
LICENSE Normal file
View File

@@ -0,0 +1,18 @@
MIT License
Copyright (c) 2025 cyclic
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.

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
[Install Lune](https://lune-org.github.io/docs/getting-started/1-installation/), then in this directory run `lune run src/build`. A `index.html` file will be created in `dist/`. You can either open that directly in your browser, or run `lune run src/dev` which will host it's contents on `http://localhost:3000`.
I essentially just forked [this website](https://luau.page/), because I too like making websites in Luau.

3
lib/html/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"luau-lsp.fflags.enableNewSolver": true
}

21
lib/html/LICENSE Normal file
View 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
View 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(),
},
}
```

View 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)

View 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
View 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
View File

@@ -0,0 +1 @@
return require('./html/src')

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"),
}

1
lib/html/stylua.toml Normal file
View File

@@ -0,0 +1 @@
call_parentheses = "Input"

7
rokit.toml Normal file
View File

@@ -0,0 +1,7 @@
# This file lists tools managed by Rokit, a toolchain manager for Roblox projects.
# For more information, see https://github.com/rojo-rbx/rokit
# New tools can be added by running `rokit add <tool>` in a terminal.
[tools]
lune = "lune-org/lune@0.10.1"

5
src/build.luau Normal file
View File

@@ -0,0 +1,5 @@
local fs = require("@lune/fs")
local site = require("./")
fs.writeDir("dist")
fs.writeFile("dist/index.html", site())

14
src/dev.luau Normal file
View File

@@ -0,0 +1,14 @@
local net = require("@lune/net")
local fs = require("@lune/fs")
local server = net.serve(3000, function()
return {
status = 200,
body = fs.readFile("dist/index.html"),
headers = {
['Application-Type'] = 'html'
}
}
end)
print(`Serving on http://localhost:3000`)

53
src/init.luau Normal file
View File

@@ -0,0 +1,53 @@
local h = require("@lib/html")
return h.html({ lang = "en" })({
h.head()({
h.meta({ charset = "utf-8" }),
h.meta({ name = "viewport", content = "width=device-width, initial-scale=1" }),
h.meta({ name = "color-scheme", content = "dark light" }),
h.title()({
"luau software",
}),
h.link({
rel = "icon",
href = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAGFBMVEUAov4Aov4Aof4Aov7l8/6l0v5ctP4Fof7s4QukAAAABHRSTlMBRY/MU7dEywAAALZJREFUeNqNk0EOwyAMBGvs3f3/j1sXWkKDK+biQ0YwisXjFPdm/76bJL0cq4VBdVBTp5RcSS2ZbqxJTQVe3QBwEfRLJJRakcCPYEUCIqkTugCpVQkEqoQJJdn5XwB1Y7khtoJ9BYR2zEUAkkBxmzBuYOQkuEnIbwJyAgFxSWgpAATSec8QMRM8ZYIIBIGUIewW0c9g9JkJA7N2lSKTx6on00nrsupC4txkLdWPzNxrYR7kp8/+CUlvDvtDdoJIAAAAAElFTkSuQmCC",
}),
h.link({
rel = "stylesheet",
href = "https://cdn.jsdelivr.net/npm/@picocss/pico@2.1.1/css/pico.classless.min.css",
}),
}),
h.body()({
h.header()({
h.h1()({
"luau software",
}),
h.p()({
"my personal website. i make software using luau. this website is written in luau.",
}),
}),
h.main()({
h.p()({
"you can find most of what you're looking for here:",
}),
h.div({ style = "display: flex; flex-direction: row; flex-wrap: wrap; column-gap: 10px;" })({
h.article()({
h.header()({
"my git (including all my foss work)",
}),
h.a({ href = 'https://git.luau.software/cyclic' })({
'profile',
}),
}),
h.article()({
h.header()({
"my email",
}),
h.p()({
'cyclic@luau.software',
}),
}),
}),
})
}),
})

7
test.luau Normal file
View File

@@ -0,0 +1,7 @@
local success, response = pcall(require, './src')
if not success then
return print(response)
end
print('website compiles fine')