├── .gitignore ├── .stylua.toml ├── LICENSE ├── README.md ├── redbean.meta.lua ├── run.sh └── src ├── .init.lua ├── .lua └── htmlua.lua └── pages └── index.html.lua /.gitignore: -------------------------------------------------------------------------------- 1 | *.com 2 | -------------------------------------------------------------------------------- /.stylua.toml: -------------------------------------------------------------------------------- 1 | indent_type = "Spaces" 2 | indent_width = 2 3 | column_width = 80 4 | call_parentheses = "Input" 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 NotNite 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # htmlua 2 | 3 | htmlua converts this: 4 | 5 | ```lua 6 | document { 7 | html { 8 | head { 9 | meta { charset = "utf-8" }, 10 | title "Hello, world!" 11 | }, 12 | 13 | body { 14 | h1 "Hello, world!", 15 | p "This is a paragraph." 16 | } 17 | } 18 | } 19 | ``` 20 | 21 | into this: 22 | 23 | ```html 24 | 25 | 26 |
27 | 28 |This is a paragraph.
33 | 34 | 35 | ``` 36 | 37 | ...all dynamically at request time. It is effectively serverside rendering where your output file is being rendered from Lua functions. It is written in 100% vanilla Lua and can be integrated into any Lua environment. It supports a component-like system for reusing blocks of HTML. It runs on six operating systems with a single file thanks to the [redbean](https://redbean.dev/) web server. 38 | 39 | It is suggested to read `src/pages/index.html.lua`, as it is heavily commented. 40 | 41 | ## Why? 42 | 43 | I recently read [Ben Visness](https://bvisness.me/luax/)'s article about their custom Lua dialect for JSX-like elements. I thought I could build a system just as flexible to use in vanilla Lua. 44 | 45 | ## Installation 46 | 47 | - Copy the contents of this repository into a new folder. 48 | - Download `redbean.com` from the redbean website. 49 | - Execute `./run.sh` to pack the web server and run it. 50 | 51 | ## How it works 52 | 53 | This abuses a few tricks in the Lua language spec: 54 | 55 | - Functions that take a single argument can be called without parenthesis (`ident("a")` = `ident "a"`) 56 | - Key value pairs and array-like entries into tables can be mixed (`{ a = "b", "c" }`) 57 | 58 | The keywords are actually functions in the global scope that create the HTML element and return a string. Props are identified by key/value pairs and children are identified as entries in the table. Valueluess props (such as `autoplay` on the `video` element) can be specified with `_ = { "autoplay" }`. 59 | 60 | ## Using htmlua 61 | 62 | ### Creating your first page 63 | 64 | Create `pages/index.html.lua`: 65 | 66 | ```lua 67 | return function() 68 | return document { 69 | html { 70 | head { 71 | meta { charset = "utf-8" }, 72 | title "Hello, world!" 73 | }, 74 | 75 | body { 76 | h1 "Hello, world!", 77 | p "This is a paragraph." 78 | } 79 | } 80 | } 81 | end 82 | ``` 83 | 84 | ### Adding more HTML elements 85 | 86 | Put a line like this in your `.init.lua`: 87 | 88 | ```lua 89 | h2 = htmlua.elem("h2") 90 | ``` 91 | 92 | You can provide a second argument for options for this element: 93 | 94 | - `close: boolean = true` - whether to close the element with `` 95 | - `empty: boolean = false` - whether to close the element wiith `/>` if there are no children 96 | 97 | ### Creating a reusable component 98 | 99 | ```lua 100 | local htmlua = require("htmlua") 101 | 102 | local MyComponent = htmlua.component(function(props, children) 103 | return { 104 | h1("Hello, " .. props.name .. "!"), 105 | p "This is a reusable component." 106 | } 107 | end) 108 | 109 | return function() 110 | return document { 111 | html { 112 | head { 113 | meta { charset = "utf-8" } 114 | }, 115 | 116 | body { 117 | MyComponent { name = "world" }, 118 | hr {}, 119 | MyComponent { name = "Lua" } 120 | } 121 | } 122 | } 123 | end 124 | ``` 125 | 126 | ### Routing 127 | 128 | Paths get tried in this order: 129 | 130 | - `pages/:path/index.lua` 131 | - `pages/:path/index.html.lua` 132 | - `pages/:path.html.lua` 133 | - `pages/:path.lua` 134 | 135 | ## Testimonials 136 | 137 | - "this is based i like this" -`P` | `C` | Displayable 7 | 8 | -- LuaLS generics seem kinda broken right now lol 9 | ---@alias Element
fun(args: PropsOrChildren<`P`, `C`>?): Displayable 10 | ---@alias Component
fun(props: `P`, children: `C`): Displayable | Displayable[]
11 |
12 | ---@class ElementConfig
13 | ---@field close boolean?
14 | ---@field empty boolean?
15 |
16 | ---@generic P: {}
17 | ---@generic C: {}
18 | ---@param args PropsOrChildren<`P`, `C`>?
19 | ---@return { props: `P`, children: `C`, boolean_props: string[] }
20 | function htmlua.parse(args)
21 | local props = {}
22 | local children = {}
23 | local boolean_props = {}
24 |
25 | if type(args) == "table" then
26 | for k, v in pairs(args) do
27 | if type(k) == "number" then
28 | table.insert(children, v)
29 | elseif k == "_" then
30 | for _, prop in ipairs(v) do
31 | table.insert(boolean_props, prop)
32 | end
33 | else
34 | props[k] = tostring(v)
35 | end
36 | end
37 | elseif args then
38 | table.insert(children, args)
39 | end
40 |
41 | return {
42 | props = props,
43 | children = children,
44 | boolean_props = boolean_props,
45 | }
46 | end
47 |
48 | ---@param str string
49 | ---@return string
50 | function htmlua.escape(str)
51 | local ret = str:gsub("&", "&")
52 | ret = ret:gsub("<", "<")
53 | ret = ret:gsub(">", ">")
54 | ret = ret:gsub('"', """)
55 | ret = ret:gsub("'", "'")
56 | return ret
57 | end
58 |
59 | ---@param config ElementConfig?
60 | ---@return ElementConfig
61 | local function assign_config(config)
62 | local default = {
63 | close = true,
64 | empty = false,
65 | }
66 |
67 | if config then
68 | for k, v in pairs(config) do
69 | default[k] = v
70 | end
71 | end
72 |
73 | return default
74 | end
75 |
76 | ---@generic P: { _props: string[]? }
77 | ---@generic C: {}
78 | ---@param name string
79 | ---@param config ElementConfig?
80 | ---@return Element<`P`, `C`>
81 | function htmlua.elem(name, config)
82 | local cfg = assign_config(config)
83 | ---@param args PropsOrChildren<`P`, `C`>?
84 | return function(args)
85 | local str = "<" .. name
86 | local parsed = htmlua.parse(args)
87 |
88 | for k, v in pairs(parsed.props) do
89 | local display = htmlua.display(v)
90 | if display then
91 | display = htmlua.escape(display)
92 | local entry = k .. '="' .. display .. '"'
93 | str = str .. " " .. entry
94 | end
95 | end
96 |
97 | for _, prop in ipairs(parsed.boolean_props) do
98 | str = str .. " " .. prop
99 | end
100 |
101 | if cfg.empty and #parsed.children == 0 then
102 | str = str .. " />"
103 | return str
104 | end
105 |
106 | str = str .. ">"
107 |
108 | for _, child in ipairs(parsed.children) do
109 | local display = htmlua.display(child)
110 | if display then
111 | str = str .. display
112 | end
113 | end
114 |
115 | if cfg.close then
116 | str = str .. "" .. name .. ">"
117 | end
118 |
119 | return str
120 | end
121 | end
122 |
123 | ---@param tbl (Displayable | Displayable[])[]
124 | ---@return string
125 | local function recursive_concat(tbl)
126 | local str = ""
127 |
128 | for _, v in ipairs(tbl) do
129 | str = str .. htmlua.display(v)
130 | end
131 |
132 | return str
133 | end
134 |
135 | ---@return string?
136 | function htmlua.display(value)
137 | if type(value) == "table" then
138 | return recursive_concat(value)
139 | elseif
140 | type(value) == "string"
141 | or type(value) == "number"
142 | or type(value) == "boolean"
143 | then
144 | return tostring(value)
145 | end
146 | end
147 |
148 | ---@generic P
149 | ---@generic C
150 | ---@param func Component<`P`, `C`>
151 | ---@return Element<`P`, `C`>
152 | function htmlua.component(func)
153 | ---@param args PropsOrChildren<`P`, `C`>
154 | return function(args)
155 | local parsed = htmlua.parse(args)
156 | return htmlua.display(func(parsed.props, parsed.children))
157 | end
158 | end
159 |
160 | return htmlua
161 |
--------------------------------------------------------------------------------
/src/pages/index.html.lua:
--------------------------------------------------------------------------------
1 | local htmlua = require("htmlua")
2 |
3 | -- Components can be defined with htmlua.component. They return a wrapper
4 | -- function that parses the arguments into separate props and children.
5 | -- It is suggested to store reused components in src/.lua/components, which can
6 | -- be loaded with require("components.