From 6948e4632795db97d7a8c3dc7ca775092a084cad Mon Sep 17 00:00:00 2001 From: cyclic Date: Sun, 20 Jul 2025 20:23:49 -0600 Subject: [PATCH] initial commit --- .gitignore | 1 + .gitmodules | 3 + .luaurc | 6 ++ LICENSE | 18 ++++ README.md | 3 + lib/html/.vscode/settings.json | 3 + lib/html/LICENSE | 21 ++++ lib/html/README.md | 28 +++++ lib/html/examples/cards.luau | 52 ++++++++++ lib/html/examples/html.luau | 38 +++++++ lib/html/foreman.toml | 3 + lib/html/init.luau | 1 + lib/html/src/init.luau | 183 +++++++++++++++++++++++++++++++++ lib/html/stylua.toml | 1 + rokit.toml | 7 ++ src/build.luau | 5 + src/dev.luau | 14 +++ src/init.luau | 53 ++++++++++ test.luau | 7 ++ 19 files changed, 447 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 .luaurc create mode 100644 LICENSE create mode 100644 README.md create mode 100644 lib/html/.vscode/settings.json create mode 100644 lib/html/LICENSE create mode 100644 lib/html/README.md create mode 100644 lib/html/examples/cards.luau create mode 100644 lib/html/examples/html.luau create mode 100644 lib/html/foreman.toml create mode 100644 lib/html/init.luau create mode 100644 lib/html/src/init.luau create mode 100644 lib/html/stylua.toml create mode 100644 rokit.toml create mode 100644 src/build.luau create mode 100644 src/dev.luau create mode 100644 src/init.luau create mode 100644 test.luau diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7773828 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +dist/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..c4a759a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "html"] + path = html + url = https://github.com/Nicell/htmluau.git diff --git a/.luaurc b/.luaurc new file mode 100644 index 0000000..d3d04cc --- /dev/null +++ b/.luaurc @@ -0,0 +1,6 @@ +{ + "aliases": { + "lune": "~/.lune/.typedefs/0.10.1/", + "lib": "./lib/" + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e60de0f --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..78a99e6 --- /dev/null +++ b/README.md @@ -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. \ No newline at end of file diff --git a/lib/html/.vscode/settings.json b/lib/html/.vscode/settings.json new file mode 100644 index 0000000..6f50920 --- /dev/null +++ b/lib/html/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "luau-lsp.fflags.enableNewSolver": true +} \ No newline at end of file diff --git a/lib/html/LICENSE b/lib/html/LICENSE new file mode 100644 index 0000000..fc113a2 --- /dev/null +++ b/lib/html/LICENSE @@ -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. diff --git a/lib/html/README.md b/lib/html/README.md new file mode 100644 index 0000000..d7e099d --- /dev/null +++ b/lib/html/README.md @@ -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.", + " is properly excaped.", + }, + button(), + }, +} +``` diff --git a/lib/html/examples/cards.luau b/lib/html/examples/cards.luau new file mode 100644 index 0000000..18c6c7f --- /dev/null +++ b/lib/html/examples/cards.luau @@ -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) diff --git a/lib/html/examples/html.luau b/lib/html/examples/html.luau new file mode 100644 index 0000000..46e0775 --- /dev/null +++ b/lib/html/examples/html.luau @@ -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.", + " is properly excaped.", + }, + button(), + }, +} + +net.serve(function() + return website() +end) diff --git a/lib/html/foreman.toml b/lib/html/foreman.toml new file mode 100644 index 0000000..1c1f43c --- /dev/null +++ b/lib/html/foreman.toml @@ -0,0 +1,3 @@ +[tools] +stylua = { github = "JohnnyMorganz/StyLua", version = "2.0.2" } +lute = { github = "luau-lang/lute", version = "0.1.0-nightly.20250508" } diff --git a/lib/html/init.luau b/lib/html/init.luau new file mode 100644 index 0000000..59d36b2 --- /dev/null +++ b/lib/html/init.luau @@ -0,0 +1 @@ +return require('./html/src') \ No newline at end of file diff --git a/lib/html/src/init.luau b/lib/html/src/init.luau new file mode 100644 index 0000000..fbf25c9 --- /dev/null +++ b/lib/html/src/init.luau @@ -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 = (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()) -- Hello World +]] +-- +local function createComponent(component: (props: T, children: { string }?) -> Node): Component + 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")}` + else + return `<{tag}{attributes}>` + 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"), +} diff --git a/lib/html/stylua.toml b/lib/html/stylua.toml new file mode 100644 index 0000000..8dd9993 --- /dev/null +++ b/lib/html/stylua.toml @@ -0,0 +1 @@ +call_parentheses = "Input" diff --git a/rokit.toml b/rokit.toml new file mode 100644 index 0000000..e43e038 --- /dev/null +++ b/rokit.toml @@ -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 ` in a terminal. + +[tools] +lune = "lune-org/lune@0.10.1" diff --git a/src/build.luau b/src/build.luau new file mode 100644 index 0000000..b3f84c2 --- /dev/null +++ b/src/build.luau @@ -0,0 +1,5 @@ +local fs = require("@lune/fs") +local site = require("./") + +fs.writeDir("dist") +fs.writeFile("dist/index.html", site()) \ No newline at end of file diff --git a/src/dev.luau b/src/dev.luau new file mode 100644 index 0000000..67f2bea --- /dev/null +++ b/src/dev.luau @@ -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`) \ No newline at end of file diff --git a/src/init.luau b/src/init.luau new file mode 100644 index 0000000..0691ba4 --- /dev/null +++ b/src/init.luau @@ -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', + }), + }), + }), + }) + }), +}) diff --git a/test.luau b/test.luau new file mode 100644 index 0000000..39f6d90 --- /dev/null +++ b/test.luau @@ -0,0 +1,7 @@ +local success, response = pcall(require, './src') + +if not success then + return print(response) +end + +print('website compiles fine') \ No newline at end of file