├── .gitattributes ├── .gitignore ├── .luarc.json ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── accuratetime.c ├── api-test.lua ├── component-test.lua ├── luaxmlgenerator-0.1.0-1.rockspec ├── luaxmlgenerator-0.2.0-1.rockspec ├── luaxmlgenerator-0.2.1-1.rockspec ├── luaxmlgenerator-0.2.2-1.rockspec ├── luaxmlgenerator-0.2.3-1.rockspec ├── luaxmlgenerator-0.3.0-1.rockspec ├── luaxmlgenerator-0.4.0-1.rockspec ├── luaxmlgenerator-0.5.0-1.rockspec ├── luaxmlgenerator-0.5.1-1.rockspec ├── luaxmlgenerator-0.6.0-1.rockspec ├── luaxmlgenerator-1.0.0-1.rockspec ├── luaxmlgenerator-1.1.0-1.rockspec ├── luaxmlgenerator-1.1.1-1.rockspec ├── luaxmlgenerator-dev-1.rockspec ├── test.lua └── xml-generator.lua /.gitattributes: -------------------------------------------------------------------------------- 1 | # Make `items-raw.html` vendored: 2 | item-raw.html -linguist-vendored 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /luarocks 2 | /lua 3 | /lua_modules 4 | /.luarocks 5 | /test 6 | 7 | /*.rock 8 | -------------------------------------------------------------------------------- /.luarc.json: -------------------------------------------------------------------------------- 1 | { 2 | "Lua.workspace.checkThirdParty": false, 3 | "diagnostics.groupSeverity": { 4 | "ambiguity": "Error", 5 | "await": "Error", 6 | "codestyle": "Error", 7 | "duplicate": "Hint", 8 | "global": "Error", 9 | "luadoc": "Error", 10 | "redefined": "Information", 11 | "strict": "Error", 12 | "strong": "Error", 13 | "type-check": "Error", 14 | "unbalanced": "Error", 15 | "unused": "Hint", 16 | "conventions": "Error" 17 | }, 18 | "diagnostics.disable": [ 19 | "duplicate-doc-alias", 20 | "duplicate-doc-field" 21 | ], 22 | "runtime.path": [ 23 | "?.lua", 24 | "?/init.lua", 25 | ], 26 | "runtime.pathStrict": true, 27 | "runtime.version": "LuaJIT", 28 | "workspace.ignoreDir": [ 29 | ".vscode", 30 | ".luarocks" 31 | ], 32 | "workspace.library": [ 33 | "deps/", 34 | "lua_modules/lib/lua/5.1/", 35 | "lua_modules/share/lua/5.1/", 36 | ], 37 | "runtime.special" : { 38 | "typename" : "type", 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "licenser.license": "MIT" 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Amrit Bhogal 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 | # LuaXMLGenerator 2 | 3 | Library to easily generate XML with a clean Lua DSL. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | luarocks install luaxmlgenerator 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```lua 14 | local xml_gen = require("xml-generator") 15 | local xml = xml_gen.xml 16 | 17 | local doc = xml.html {charset="utf-8", lang="en"} { 18 | xml.head { 19 | xml.title "Hello World" 20 | }, 21 | xml.body { 22 | xml.h1 "Hello World", 23 | 24 | xml.div {id="numbers"} { 25 | function() --run as a coroutine 26 | for i = 1, 10 do 27 | coroutine.yield(xml.p(i)) 28 | end 29 | end 30 | } 31 | } 32 | } 33 | 34 | print(doc) 35 | ``` 36 | 37 | ## Options 38 | 39 | ### `xml_gen.no_sanitize` 40 | Table of tags that should not be sanitized. By default, it contains `script` and `style`. 41 | 42 | ```lua 43 | local xml_gen = require("xml-generator") 44 | xml_gen.no_sanitize["mytag"] = true 45 | 46 | local doc = xml_gen.xml.mytag [[ 47 | Thsi will not be sanitized! <><><><><><%%%<>%<>%<>% you can use all of this! 48 | ]] 49 | 50 | ``` 51 | 52 | ### `xml_gen.lua_is_global` 53 | 54 | By default, within `xml_gen.xml` there is a special key called `lua` which just allows for `_G` to be accessed. This is useful if you use `xml_gen.declare_generator`, which overloads the `_ENV` (`setfenv` on 5.1), where the `_G` would not be ordinarily accessible. 55 | 56 | ```lua 57 | local xml_gen = require("xml-generator") 58 | 59 | local gen = xml_gen.declare_generator(function() 60 | return html { 61 | head { 62 | title "Hello World"; 63 | }; 64 | 65 | body { 66 | p { "The time of generation is ", lua.os.date() } 67 | }; 68 | } 69 | end) 70 | 71 | print(gen()) 72 | ``` 73 | 74 | ## Components 75 | 76 | Using `xml_gen.component` you can create your own components. Here is an example of a `random_number` component 77 | ```lua 78 | ---@param context fun(args: { [string] : any }, children: XML.Children): XML.Node? 79 | ---@return XML.Component 80 | function export.component(context) 81 | ``` 82 | 83 | ```lua 84 | local xml_gen = require("xml-generator") 85 | local xml = xml_gen.xml 86 | 87 | math.randomseed(os.time()) 88 | 89 | local random_number = xml_gen.component(function(args, children) 90 | local min = args.min or 0 91 | --remove these from the args so they dont show up in our HTML attributes later 92 | args.min = nil 93 | local max = args.max or 100 94 | args.max = nil 95 | 96 | coroutine.yield(xml.p "This is a valid coroutine too!") 97 | 98 | return xml.span(args) { 99 | math.random(min, max), 100 | children --children is a table of all the children passed to the component, this may be empty 101 | } 102 | end) 103 | 104 | local doc = xml.html { 105 | xml.body { 106 | random_number {min = 0, max = 100}; 107 | random_number {max=10} { 108 | xml.p "This is inside the span!" 109 | }; 110 | random_number; 111 | } 112 | } 113 | 114 | print(doc) 115 | ``` 116 | 117 | ## Utilities 118 | 119 | ### `xml_gen,declare_generator` 120 | ```lua 121 | ---@generic T 122 | ---@param func fun(...: T): XML.Node 123 | ---@return fun(...: T): XML.Node 124 | function export.declare_generator(func) 125 | ``` 126 | 127 | Allows you to create a function in which the `_ENV` is overloaded with the `xml` table. This allows you to write XML more concisely (see example above). 128 | 129 | ### `xml_gen.style` 130 | ```lua 131 | ---@param css { [string | string[]] : { [string | string[]] : (number | string | string[]) } } 132 | ---@return XML.Node 133 | function export.style(css) 134 | ``` 135 | 136 | Creates an HTML `style` tag with the given table 137 | 138 | ```lua 139 | local xml_gen = require("xml-generator") 140 | 141 | local style = xml_gen.style { 142 | [{ "body", "html" }] = { 143 | margin = 0, 144 | padding = 0, 145 | }, 146 | 147 | body = { 148 | background = "#000", 149 | color = "#fff", 150 | } 151 | 152 | --etc 153 | } 154 | 155 | print(style) 156 | ``` 157 | 158 | ### `xml_gen.raw` 159 | ```lua 160 | ---WILL NOT BE SANITIZED 161 | ---@param ... string 162 | ---@return string[] 163 | function export.raw(...) end 164 | ``` 165 | 166 | Inserts raw (NOT SANITIZED) strings into the document. 167 | 168 | ```lua 169 | 170 | ## API 171 | 172 | You do not need to generate XML with this library, instead, you can use an `XML.Node` as its own object. 173 | 174 | ```lua 175 | ---@class XML.Children 176 | ---@field [integer] XML.Node | string | fun(): XML.Node 177 | 178 | ---@class XML.AttributeTable : XML.Children 179 | ---@field [string] string | boolean | number 180 | 181 | ---@class XML.Node 182 | ---@operator call(XML.AttributeTable): XML.Node 183 | ---@field tag string 184 | ---@field children XML.Children 185 | ---@field attributes XML.AttributeTable 186 | 187 | ---@class XML.Component : XML.Node 188 | ---@field attributes { [string] : any } The attributes can be any type for `component`s, but not for `node`s 189 | ---@field context fun(args: { [string] : any }, children: XML.Children): XML.Node? 190 | 191 | ``` 192 | 193 | ### `XML.Node` 194 | 195 | ```lua 196 | local xml_gen = require("xml-generator") 197 | local xml = xml_gen.xml 198 | 199 | local my_node = xml.div {id="my-div"} { 200 | xml.p {id="p-1"} "Hello World"; 201 | xml.p {id="p-2"} "Hello World"; 202 | xml.p {id="p-3"} "Hello World"; 203 | } 204 | 205 | print(my_node.tag) --div 206 | print(my_node.attributes.id) --my-div 207 | 208 | for i, child in ipairs(my_node.children) do 209 | print(i, child.tag, child.attributes.id) 210 | end 211 | 212 | print(my_node) 213 | ``` 214 | 215 | `attributes` and `children` can be empty, but will never be `nil`. 216 | 217 | `tag` will be `nil` if the node is a `component`. 218 | -------------------------------------------------------------------------------- /accuratetime.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | 8 | static int accurate_clock(lua_State *L) 9 | { 10 | struct timespec spec; 11 | 12 | if (clock_gettime(CLOCK_MONOTONIC, &spec) != 0) { 13 | lua_pushnil(L); 14 | lua_pushstring(L, "Failed to get time"); 15 | return 2; 16 | } 17 | 18 | double time_in_seconds = spec.tv_sec + spec.tv_nsec / 1.0e9; 19 | 20 | lua_pushnumber(L, time_in_seconds); 21 | return 1; 22 | } 23 | 24 | static luaL_Reg LIBRARY[] = { 25 | {"clock", accurate_clock}, 26 | {0} 27 | }; 28 | 29 | int luaopen_accuratetime(lua_State *L) 30 | { 31 | luaL_newlib(L, LIBRARY); 32 | return 1; 33 | } 34 | -------------------------------------------------------------------------------- /api-test.lua: -------------------------------------------------------------------------------- 1 | local xml_gen = require("xml-generator") 2 | local xml = xml_gen.xml 3 | 4 | local my_node = xml.div {id="my-div"} { 5 | xml.p {id="p-1"} "Hello World"; 6 | xml.p {id="p-2"} "Hello World"; 7 | xml.p {id="p-3"} "Hello World"; 8 | } 9 | 10 | print(my_node.tag, my_node.attributes.id) 11 | 12 | for i, child in ipairs(my_node.children) do 13 | print(i, child.tag, child.attributes.id) 14 | end 15 | 16 | -- print(my_node) 17 | -------------------------------------------------------------------------------- /component-test.lua: -------------------------------------------------------------------------------- 1 | local xml_gen = require("xml-generator") 2 | local xml = xml_gen.xml 3 | local ns = xml_gen.namespace "ns" 4 | math.randomseed(os.time()) 5 | 6 | local header = xml_gen.component(function (args, ...) 7 | return xml.head { 8 | xml.title(args.title); 9 | xml.meta { 10 | name="viewport", 11 | content="width=device-width, initial-scale=1" 12 | }; 13 | {...}; 14 | args.css_framework; 15 | } 16 | end) 17 | 18 | local random_number = xml_gen.component(function (args) 19 | return xml.p(math.random(args.min, args.max)) 20 | end) 21 | 22 | local rand_size = 10000000 23 | local yield = coroutine.yield 24 | local doc = xml_gen.declare_generator(function () 25 | ---@diagnostic disable: undefined-global 26 | return html {charset="utf8"} { 27 | header {title="Hello, World!", css_framework=link {rel="stylesheet", href="..."}}; 28 | 29 | body { 30 | h1 {class="text-center"} "Fritsite"; 31 | main {class="container"} { 32 | p "Hello, World!"; 33 | button {onclick="say_hi()"} "Say Hi!"; 34 | }; 35 | 36 | function () 37 | for i = 1, rand_size do 38 | yield(random_number {min=1, max=i}) 39 | end 40 | end; 41 | 42 | ns.div {id="test div"} "hello" 43 | }; 44 | } 45 | ---@diagnostic enable: undefined-global 46 | end) 47 | 48 | local accuratetime 49 | if jit then 50 | local ffi = require("ffi") 51 | ffi.cdef [[ 52 | typedef long time_t; 53 | struct timespec { 54 | time_t tv_sec; 55 | long tv_nsec; 56 | }; 57 | typedef enum { 58 | CLOCK_REALTIME = 0, 59 | 60 | CLOCK_MONOTONIC = 6, 61 | 62 | 63 | CLOCK_MONOTONIC_RAW = 4, 64 | 65 | CLOCK_MONOTONIC_RAW_APPROX = 5, 66 | 67 | CLOCK_UPTIME_RAW = 8, 68 | 69 | CLOCK_UPTIME_RAW_APPROX = 9, 70 | 71 | 72 | CLOCK_PROCESS_CPUTIME_ID = 12, 73 | 74 | CLOCK_THREAD_CPUTIME_ID = 16 75 | } clockid_t; 76 | 77 | int clock_gettime(clockid_t _CLOCK_id, struct timespec *__tp); 78 | ]] 79 | 80 | accuratetime = { 81 | clock = function () 82 | local spec = ffi.new("struct timespec[1]") 83 | if ffi.C.clock_gettime(ffi.C.CLOCK_MONOTONIC, spec) ~= 0 then return nil, "clock_gettime failed" end 84 | 85 | return tonumber(spec[0].tv_sec) + tonumber(spec[0].tv_nsec) * 1e-9 86 | end 87 | } 88 | else 89 | accuratetime = require("accuratetime") 90 | end 91 | 92 | collectgarbage("stop") 93 | collectgarbage("stop") 94 | 95 | local start_time = accuratetime.clock() 96 | xml_gen.expand_node(doc()) 97 | local end_time = accuratetime.clock() 98 | io.stderr:write(string.format("Took %.2fs to expand", end_time-start_time), '\n') 99 | io.stderr:write(string.format("%.2fKB used", collectgarbage("count")), '\n') 100 | io.stderr:flush() 101 | 102 | start_time = accuratetime.clock() 103 | collectgarbage("collect") 104 | end_time = accuratetime.clock() 105 | 106 | io.stderr:write(string.format("Took %.2fs to collect garbage", end_time-start_time), '\n') 107 | io.stderr:write(string.format("%.2fKB used", collectgarbage("count")), '\n') 108 | io.stderr:flush() 109 | 110 | start_time = accuratetime.clock() 111 | _=tostring(doc()) 112 | end_time = accuratetime.clock() 113 | 114 | io.stderr:write(string.format("Took %.2fs to expand+stringify", end_time-start_time), '\n') 115 | io.stderr:write(string.format("%.2fKB used", collectgarbage("count")), '\n') 116 | io.stderr:flush() 117 | 118 | -------------------------------------------------------------------------------- /luaxmlgenerator-0.1.0-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "LuaXMLGenerator" 2 | version = "0.1.0-1" 3 | source = { 4 | url = "git+https://github.com/Frityet/LuaXMLGenerator", 5 | tag = "0.1.0" 6 | } 7 | description = { 8 | summary = "DSL to generate XML/HTML", 9 | homepage = "https://github.com/Frityet/LuaXMLGenerator", 10 | license = "MIT" 11 | } 12 | dependencies = { 13 | "lua >= 5.1, < 5.5" 14 | } 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ["xml-generator"] = "xml-generator.lua" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /luaxmlgenerator-0.2.0-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "LuaXMLGenerator" 2 | version = "0.2.0-1" 3 | source = { 4 | url = "git+https://github.com/Frityet/LuaXMLGenerator", 5 | tag = "0.2.0" 6 | } 7 | description = { 8 | summary = "DSL to generate XML/HTML", 9 | homepage = "https://github.com/Frityet/LuaXMLGenerator", 10 | license = "MIT" 11 | } 12 | dependencies = { 13 | "lua >= 5.1, < 5.5" 14 | } 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ["xml-generator"] = "xml-generator.lua" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /luaxmlgenerator-0.2.1-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "LuaXMLGenerator" 2 | version = "0.2.1-1" 3 | source = { 4 | url = "git+https://github.com/Frityet/LuaXMLGenerator", 5 | tag = "0.2.1" 6 | } 7 | description = { 8 | summary = "DSL to generate XML/HTML", 9 | homepage = "https://github.com/Frityet/LuaXMLGenerator", 10 | license = "MIT" 11 | } 12 | dependencies = { 13 | "lua >= 5.1, < 5.5" 14 | } 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ["xml-generator"] = "xml-generator.lua" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /luaxmlgenerator-0.2.2-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "LuaXMLGenerator" 2 | version = "0.2.2-1" 3 | source = { 4 | url = "git+https://github.com/Frityet/LuaXMLGenerator", 5 | tag = "0.2.2" 6 | } 7 | description = { 8 | summary = "DSL to generate XML/HTML", 9 | homepage = "https://github.com/Frityet/LuaXMLGenerator", 10 | license = "MIT" 11 | } 12 | dependencies = { 13 | "lua >= 5.1, < 5.5" 14 | } 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ["xml-generator"] = "xml-generator.lua" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /luaxmlgenerator-0.2.3-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "LuaXMLGenerator" 2 | version = "0.2.3-1" 3 | source = { 4 | url = "git+https://github.com/Frityet/LuaXMLGenerator", 5 | tag = "0.2.3" 6 | } 7 | description = { 8 | summary = "DSL to generate XML/HTML", 9 | homepage = "https://github.com/Frityet/LuaXMLGenerator", 10 | license = "MIT" 11 | } 12 | dependencies = { 13 | "lua >= 5.1, < 5.5" 14 | } 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ["xml-generator"] = "xml-generator.lua" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /luaxmlgenerator-0.3.0-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "LuaXMLGenerator" 2 | version = "0.3.0-1" 3 | source = { 4 | url = "git+https://github.com/Frityet/LuaXMLGenerator", 5 | tag = "0.3.0" 6 | } 7 | description = { 8 | summary = "DSL to generate XML/HTML", 9 | homepage = "https://github.com/Frityet/LuaXMLGenerator", 10 | license = "MIT" 11 | } 12 | dependencies = { 13 | "lua >= 5.1, < 5.5" 14 | } 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ["xml-generator"] = "xml-generator.lua" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /luaxmlgenerator-0.4.0-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "LuaXMLGenerator" 2 | version = "0.4.0-1" 3 | source = { 4 | url = "git+https://github.com/Frityet/LuaXMLGenerator", 5 | tag = "0.4.0" 6 | } 7 | description = { 8 | summary = "DSL to generate XML/HTML", 9 | homepage = "https://github.com/Frityet/LuaXMLGenerator", 10 | license = "MIT" 11 | } 12 | dependencies = { 13 | "lua >= 5.1, < 5.5" 14 | } 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ["xml-generator"] = "xml-generator.lua" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /luaxmlgenerator-0.5.0-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "LuaXMLGenerator" 2 | version = "0.5.0-1" 3 | source = { 4 | url = "git+https://github.com/Frityet/LuaXMLGenerator", 5 | tag = "0.5.0" 6 | } 7 | description = { 8 | summary = "DSL to generate XML/HTML", 9 | homepage = "https://github.com/Frityet/LuaXMLGenerator", 10 | license = "MIT" 11 | } 12 | dependencies = { 13 | "lua >= 5.1, < 5.5" 14 | } 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ["xml-generator"] = "xml-generator.lua" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /luaxmlgenerator-0.5.1-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "LuaXMLGenerator" 2 | version = "0.5.1-1" 3 | source = { 4 | url = "git+https://github.com/Frityet/LuaXMLGenerator", 5 | tag = "0.5.1" 6 | } 7 | description = { 8 | summary = "DSL to generate XML/HTML", 9 | homepage = "https://github.com/Frityet/LuaXMLGenerator", 10 | license = "MIT" 11 | } 12 | dependencies = { 13 | "lua >= 5.1, < 5.5" 14 | } 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ["xml-generator"] = "xml-generator.lua" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /luaxmlgenerator-0.6.0-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "LuaXMLGenerator" 2 | version = "0.6.0-1" 3 | source = { 4 | url = "git+https://github.com/Frityet/LuaXMLGenerator", 5 | tag = "0.6.0" 6 | } 7 | description = { 8 | summary = "DSL to generate XML/HTML", 9 | homepage = "https://github.com/Frityet/LuaXMLGenerator", 10 | license = "MIT" 11 | } 12 | dependencies = { 13 | "lua >= 5.1, < 5.5" 14 | } 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ["xml-generator"] = "xml-generator.lua" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /luaxmlgenerator-1.0.0-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "LuaXMLGenerator" 2 | version = "1.0.0-1" 3 | source = { 4 | url = "git+https://github.com/Frityet/LuaXMLGenerator", 5 | tag = "1.0.0" 6 | } 7 | description = { 8 | summary = "DSL to generate XML/HTML", 9 | homepage = "https://github.com/Frityet/LuaXMLGenerator", 10 | license = "MIT" 11 | } 12 | dependencies = { 13 | "lua >= 5.1, < 5.5" 14 | } 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ["xml-generator"] = "xml-generator.lua" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /luaxmlgenerator-1.1.0-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "LuaXMLGenerator" 2 | version = "1.1.0-1" 3 | source = { 4 | url = "git+https://github.com/Frityet/LuaXMLGenerator", 5 | tag = "1.1.0" 6 | } 7 | description = { 8 | summary = "DSL to generate XML/HTML", 9 | homepage = "https://github.com/Frityet/LuaXMLGenerator", 10 | license = "MIT" 11 | } 12 | dependencies = { 13 | "lua >= 5.1, < 5.5" 14 | } 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ["xml-generator"] = "xml-generator.lua" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /luaxmlgenerator-1.1.1-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "LuaXMLGenerator" 2 | version = "1.1.1-1" 3 | source = { 4 | url = "git+https://github.com/Frityet/LuaXMLGenerator", 5 | tag = "1.1.1" 6 | } 7 | description = { 8 | summary = "DSL to generate XML/HTML", 9 | homepage = "https://github.com/Frityet/LuaXMLGenerator", 10 | license = "MIT" 11 | } 12 | dependencies = { 13 | "lua >= 5.1, < 5.5" 14 | } 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ["xml-generator"] = "xml-generator.lua" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /luaxmlgenerator-dev-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "LuaXMLGenerator" 2 | version = "dev-1" 3 | source = { 4 | url = "git+https://github.com/Frityet/LuaXMLGenerator" 5 | } 6 | description = { 7 | summary = "DSL to generate XML/HTML", 8 | homepage = "https://github.com/Frityet/LuaXMLGenerator", 9 | license = "MIT" 10 | } 11 | dependencies = { 12 | "lua >= 5.1, < 5.5" 13 | } 14 | build = { 15 | type = "builtin", 16 | modules = { 17 | ["xml-generator"] = "xml-generator.lua" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test.lua: -------------------------------------------------------------------------------- 1 | local xml_gen = require("xml-generator") 2 | local xml = xml_gen.xml 3 | 4 | local doc = xml.html {charset="utf-8", lang="en"} { 5 | xml.head { 6 | xml.title "Hello World" 7 | }, 8 | xml.body { 9 | xml.h1 "Hello World", 10 | 11 | xml.div {id="numbers"} { 12 | function() --run as a coroutine 13 | for i = 1, 10 do 14 | coroutine.yield(xml.p(i)) 15 | end 16 | end 17 | } 18 | }; 19 | 20 | xml_gen.raw [[ 21 | 24 | ]] 25 | } 26 | 27 | print(doc) 28 | -------------------------------------------------------------------------------- /xml-generator.lua: -------------------------------------------------------------------------------- 1 | local has_strbuf, strbuf = pcall(require, "string.buffer") 2 | 3 | ---@diagnostic disable: invisible 4 | -- https://leafo.net/guides/setfenv-in-lua52-and-above.html 5 | local setfenv = setfenv or function(fn, env) 6 | local i = 1 7 | while true do 8 | local name = debug.getupvalue(fn, i) 9 | if name == "_ENV" then 10 | debug.upvaluejoin(fn, i, (function() return env end), 1) 11 | break 12 | elseif not name then break end 13 | 14 | i = i + 1 15 | end 16 | 17 | return fn 18 | end 19 | 20 | local unpack = table.unpack or unpack 21 | 22 | ---@generic T: table 23 | ---@param x T 24 | ---@param copy_metatable boolean? 25 | ---@return T 26 | local function deep_copy(x, copy_metatable) 27 | local tname = type(x) 28 | if tname == "table" then 29 | local new = {} 30 | for k, v in pairs(x) do 31 | new[k] = deep_copy(v, copy_metatable) 32 | end 33 | 34 | if copy_metatable then setmetatable(new, deep_copy(getmetatable(x), true)) end 35 | 36 | return new 37 | else return x end 38 | end 39 | 40 | ---@param x table 41 | ---@return metatable 42 | local function metatable(x) 43 | local mt = getmetatable(x) or {} 44 | setmetatable(x, mt) 45 | return mt 46 | end 47 | 48 | ---@generic T 49 | ---@param x T[] 50 | ---@return T[] 51 | local function stringable_array(x) 52 | metatable(x).__tostring = function (self) 53 | local parts = {} 54 | 55 | for i, v in ipairs(self) do 56 | parts[i] = tostring(v) 57 | end 58 | 59 | return table.concat(parts) 60 | end 61 | (metatable(x) --[[@as table]]).__name = "XML.StringableArray" 62 | 63 | return x 64 | end 65 | 66 | if has_strbuf then 67 | ---@generic T 68 | ---@param x T[] 69 | ---@return T[] 70 | function stringable_array(x) 71 | --[[@cast x table]] 72 | function x:tostring_stringbuf(buf) 73 | for i, v in ipairs(self) do 74 | buf:put(tostring(v)) 75 | end 76 | end 77 | 78 | metatable(x).__tostring = function (self) 79 | --[[@cast self table]] 80 | local buf = strbuf.new(#x) 81 | self:tostring_stringbuf(buf) 82 | return buf:tostring() 83 | end 84 | (metatable(x) --[[@as table]]).__name = "XML.StringableArray" 85 | 86 | return x 87 | end 88 | end 89 | 90 | ---@class xml-generator 91 | local export = { 92 | ---Attributes that should not be sanitized 93 | no_sanitize = { ["style"] = true, ["script"] = true }, 94 | 95 | ---Sets the value of the `lua` variable available in generators 96 | --- 97 | ---This is so if you use `declare_generator` you can use `lua` to access the whatever value is specified, usually `_G` 98 | ---@type table? 99 | lua = _G 100 | } 101 | 102 | ---@class XML.Children 103 | ---@field [integer] XML.Node | string | fun(): XML.Node` 104 | 105 | ---@class XML.AttributeTable : XML.Children 106 | ---@field [string] string | boolean | number 107 | 108 | ---@class XML.Node 109 | ---@operator call(XML.AttributeTable): XML.Node 110 | ---@operator concat(XML.Node | string): XML.Node 111 | ---@field tag string 112 | ---@field children XML.Children 113 | ---@field attributes XML.AttributeTable 114 | 115 | ---quotes are allowed in text, not in attributes 116 | ---@param str string 117 | ---@return string 118 | function export.sanitize_text(str) 119 | return (str:gsub("[<>&]", { 120 | ["<"] = "<", 121 | [">"] = ">", 122 | ["&"] = "&" 123 | })) 124 | end 125 | 126 | ---@param str string 127 | ---@return string 128 | function export.sanitize_attributes(str) 129 | return (export.sanitize_text(str):gsub("\"", """):gsub("'", "'")) 130 | end 131 | 132 | ---@generic T 133 | ---@param x T 134 | ---@return type | `T` 135 | function export.typename(x) 136 | local mt = getmetatable(x) 137 | if mt and mt.__name then return mt.__name else return type(x) end 138 | end 139 | local typename = export.typename 140 | 141 | 142 | 143 | local insert = table.insert 144 | local concat = table.concat 145 | local tostring = tostring 146 | ---@param node XML.Node | XML.Component 147 | ---@return string 148 | function export.node_to_string(node) 149 | if typename(node) == "XML.Component" then return tostring(node) end 150 | 151 | local sanitize = not export.no_sanitize[node.tag:lower()] 152 | local sanitize_text = sanitize and export.sanitize_text or function (...) return ... end 153 | 154 | local parts = { "<", node.tag } 155 | 156 | for k, v in pairs(node.attributes) do 157 | if type(v) == "boolean" then 158 | if v then insert(parts, " "..k) end 159 | else 160 | insert(parts, " "..k.."=\""..export.sanitize_attributes(tostring(v)).."\"") 161 | end 162 | end 163 | 164 | insert(parts, ">") 165 | 166 | for i, v in ipairs(node.children) do 167 | local tname = typename(v) 168 | if tname == "XML.Node" or tname == "XML.Component" or tname == "XML.StringableArray" then 169 | insert(parts, tostring(v)) 170 | elseif tname == "function" then 171 | local f = coroutine.wrap(v) 172 | for elem in f do 173 | insert(parts, export.node_to_string(elem)) 174 | end 175 | elseif tname == "table" and not (getmetatable(v) or {}).__tostring then 176 | for _, elem in ipairs(v) do 177 | insert(parts, sanitize_text(tostring(elem))) 178 | end 179 | else 180 | insert(parts, sanitize_text(tostring(v))) 181 | end 182 | end 183 | 184 | insert(parts, "") 185 | 186 | return concat(parts) 187 | end 188 | 189 | if has_strbuf then 190 | ---@overload fun(node: XML.Node | XML.Component, buf: string.buffer): nil 191 | ---@param node XML.Node | XML.Component 192 | ---@return string 193 | function export.node_to_string(node, buf) 194 | if not buf then 195 | buf = strbuf.new() 196 | export.node_to_string(node, buf) 197 | return buf:tostring() 198 | end 199 | 200 | if typename(node) == "XML.Component" then 201 | return export.component_to_string(node, buf) 202 | end 203 | 204 | local sanitize = not export.no_sanitize[node.tag:lower()] 205 | local sanitize_text = sanitize and export.sanitize_text or function (...) return ... end 206 | 207 | buf:putf("<%s", node.tag) 208 | 209 | for k, v in pairs(node.attributes) do 210 | if type(v) == "boolean" then 211 | if v then buf:putf(" %s", k) end 212 | else 213 | buf:putf(" %s=\"%s\"", k, export.sanitize_attributes(tostring(v))) 214 | end 215 | end 216 | 217 | buf:put(">") 218 | 219 | 220 | for i, v in ipairs(node.children) do 221 | local tname = typename(v) 222 | 223 | if tname == "XML.StringableArray" then 224 | v:tostring_stringbuf(buf) 225 | elseif tname == "XML.Node" then 226 | export.node_to_string(v, buf) 227 | elseif tname == "XML.Component" then 228 | export.component_to_string(v, buf) 229 | elseif tname == "function" then 230 | local f = coroutine.wrap(v) 231 | for elem in f do 232 | export.node_to_string(elem, buf) 233 | end 234 | elseif tname == "table" and not (getmetatable(v) or {}).__tostring then 235 | for _, elem in ipairs(v) do 236 | buf:put(sanitize_text(tostring(elem))) 237 | end 238 | else 239 | buf:put(sanitize_text(tostring(v))) 240 | end 241 | end 242 | 243 | buf:putf("", node.tag) 244 | end 245 | end 246 | 247 | ---Expands all components and functions in a node 248 | ---@param node XML.Node | XML.Component 249 | ---@return XML.Node 250 | function export.expand_node(node) 251 | if typename(node) == "XML.Component" then 252 | return export.expand_component(node) 253 | end 254 | 255 | local new = export.create_node(node.tag, deep_copy(node.attributes, true)) --[[@as XML.Node]] 256 | 257 | for i, v in ipairs(node.children) do 258 | local tname = typename(v) 259 | if tname == "XML.Node" then 260 | -- new.children[i] = export.expand_node(v) 261 | insert(new.children, export.expand_node(v)) 262 | elseif tname == "function" then 263 | local f = coroutine.wrap(v) 264 | for elem in f do 265 | tname = typename(elem) 266 | if tname == "XML.Node" or tname == "XML.Component" then 267 | insert(new.children, export.expand_node(elem)) 268 | else 269 | insert(new.children, elem) 270 | end 271 | end 272 | end 273 | end 274 | 275 | return new 276 | end 277 | 278 | export.node_metatable = { 279 | ---@param self XML.Node 280 | ---@param attribs XML.AttributeTable | string | XML.Node 281 | ---@return XML.Node 282 | __call = function (self, attribs) 283 | local new = export.create_node(self.tag, deep_copy(self.attributes, true), deep_copy(self.children, true)) --[[@as XML.Node]] 284 | local tname = typename(attribs) 285 | if tname == "table" then 286 | for i, v in pairs(attribs) do 287 | if type(i) == "number" then 288 | new.children[#new.children+1] = v 289 | else 290 | new.attributes[i] = v 291 | end 292 | end 293 | else 294 | new.children[#new.children+1] = attribs --[[@as string|function]] 295 | end 296 | 297 | return new 298 | end; 299 | 300 | __tostring = export.node_to_string; 301 | __concat = function(self, x) return self(x) end; 302 | __name = "XML.Node"; 303 | } 304 | 305 | ---@overload fun(tag_name: "lua", attributes: XML.AttributeTable?, children: XML.Children?): _G 306 | ---@param tag_name string 307 | ---@param attributes XML.AttributeTable? 308 | ---@param children XML.Children? 309 | ---@return XML.Node 310 | function export.create_node(tag_name, attributes, children) 311 | if tag_name == "lua" and export.lua then return export.lua end 312 | 313 | return setmetatable({ 314 | tag = tag_name, 315 | children = children or {}, 316 | attributes = attributes or {}, 317 | }, export.node_metatable) 318 | end 319 | 320 | ---@class XML.GeneratorTable 321 | ---@field lua _G 322 | ---@field [string] XML.Node 323 | export.xml = setmetatable({}, { 324 | ---@param _ XML.GeneratorTable 325 | ---@param tag_name string 326 | __index = function(_, tag_name) 327 | return export.create_node(tag_name) 328 | end 329 | }) 330 | 331 | ---@generic T 332 | ---@param func fun(...: T): XML.Node 333 | ---@return fun(...: T): XML.Node 334 | function export.declare_generator(func) return setfenv(func, export.xml) end 335 | 336 | ---Creates a style tag with the given lua table 337 | ---@param css { [string | string[]] : { [string | string[]] : (number | string | string[]) } } 338 | ---@return XML.Node 339 | function export.style(css) 340 | local css_str = "" 341 | for selector, properties in pairs(css) do 342 | if type(selector) == "table" then selector = table.concat(selector, ", ") end 343 | 344 | css_str = css_str..selector.." {\n" 345 | for property, value in pairs(properties) do 346 | if type(value) == "table" then value = table.concat(value, ", ") end 347 | 348 | css_str = css_str.." "..property..": "..value..";\n" 349 | end 350 | css_str = css_str.."}\n" 351 | end 352 | 353 | return export.xml.style(css_str) 354 | end 355 | 356 | ---@param component XML.Component 357 | ---@return XML.Node[] 358 | function export.expand_component(component) 359 | local f = coroutine.create(component.context) 360 | 361 | ---@type XML.Node[] 362 | local arr = stringable_array {} 363 | local ok, res = coroutine.resume(f, component.attributes, unpack(component.children)) 364 | if not ok then error(res) end 365 | table.insert(arr, res) 366 | 367 | while coroutine.status(f) ~= "dead" do 368 | ok, res = coroutine.resume(f) 369 | if not ok then error(res) end 370 | table.insert(arr, res) 371 | end 372 | 373 | return arr 374 | end 375 | 376 | ---@param component XML.Component 377 | ---@return string 378 | function export.component_to_string(component) 379 | local f = coroutine.create(component.context) 380 | 381 | ---@type XML.Node[] 382 | local arr = stringable_array {} 383 | local ok, res = coroutine.resume(f, component.attributes, unpack(component.children)) 384 | if not ok then error(res) end 385 | table.insert(arr, res) 386 | 387 | while coroutine.status(f) ~= "dead" do 388 | ok, res = coroutine.resume(f) 389 | if not ok then error(res) end 390 | table.insert(arr, res) 391 | end 392 | 393 | return tostring(arr) 394 | end 395 | 396 | if has_strbuf then 397 | ---@overload fun(component: XML.Component, buf: string.buffer): nil 398 | ---@param component XML.Component 399 | ---@return string 400 | function export.component_to_string(component, buf) 401 | if not buf then 402 | local buf = strbuf.new() 403 | export.component_to_string(component, buf) 404 | return buf:tostring() 405 | end 406 | 407 | local f = coroutine.create(component.context) 408 | 409 | ---@type table 410 | local arr = stringable_array {} 411 | local ok, res = coroutine.resume(f, component.attributes, unpack(component.children)) 412 | if not ok then error(res) end 413 | table.insert(arr, res) 414 | 415 | while coroutine.status(f) ~= "dead" do 416 | ok, res = coroutine.resume(f) 417 | if not ok then error(res) end 418 | table.insert(arr, res) 419 | end 420 | 421 | arr:tostring_stringbuf(buf) 422 | end 423 | end 424 | 425 | ---@class XML.Component : XML.Node 426 | ---@field attributes { [string] : any } The attributes can be any type for `component`s, but not for `node`s 427 | ---@field context fun(args: { [string] : any }, children: XML.Children?): XML.Node? 428 | export.component_metatable = { 429 | ---@param self XML.Component 430 | ---@param args { [string] : any, [integer] : XML.Children } 431 | __call = function (self, args) 432 | ---@type XML.Component 433 | local new = setmetatable({ 434 | attributes = deep_copy(self.attributes, true), 435 | children = deep_copy(self.children or stringable_array {}, true), 436 | context = self.context 437 | }, getmetatable(self)) 438 | 439 | if type(args) == "table" and not (getmetatable(args) or {}).__tostring then 440 | for k, v in pairs(args) do 441 | if type(k) == "number" then 442 | insert(new.children, v) 443 | else 444 | new.attributes[k] = v 445 | end 446 | end 447 | else 448 | insert(new.children, args) 449 | end 450 | 451 | return new 452 | end; 453 | 454 | __tostring = export.component_to_string; 455 | __concat = function(self, x) return self(x) end; 456 | __name = "XML.Component"; 457 | } 458 | 459 | --[[ 460 | ```lua 461 | local xml_generator = require("xml-generator") 462 | local xml = xml_generator.xml 463 | 464 | local my_component = xml_generator.component(function(args, ...) 465 | local number = args.number + 10 466 | 467 | coroutine.yield(xml.div { 468 | xml.h1 "Hello, World!"; 469 | {...}; 470 | xml.p("Number: "..number); 471 | }) 472 | end) 473 | 474 | print(my_component {number=1}{ 475 | xml.p "This is a child" 476 | }) 477 | ``` 478 | ]] 479 | ---@param context fun(args: { [string] : any }, children: XML.Children): XML.Node? 480 | ---@return XML.Component 481 | function export.component(context) 482 | return setmetatable({ attributes = {}, children = stringable_array {}, context = context }, export.component_metatable) 483 | end 484 | 485 | ---@param ns string 486 | ---@param sep string? 487 | ---@return XML.GeneratorTable 488 | function export.namespace(ns, sep) 489 | return setmetatable({ namespace = ns, seperator = sep or "-" }, { 490 | __index = function(self, tag_name) 491 | return export.create_node(self.namespace..self.seperator..tag_name) 492 | end 493 | }) 494 | end 495 | 496 | ---WILL NOT BE SANITIZED 497 | ---@param ... string 498 | ---@return string[] 499 | function export.raw(...) return stringable_array { ... } end 500 | 501 | return export 502 | --------------------------------------------------------------------------------