Skip to content

Commit d694a16

Browse files
committed
first commit
0 parents  commit d694a16

File tree

3 files changed

+288
-0
lines changed

3 files changed

+288
-0
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/luarocks
2+
/lua
3+
/lua_modules
4+
/.luarocks

luaxmlgenerator-dev-1.rockspec

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package = "LuaXMLGenerator"
2+
version = "dev-1"
3+
source = {
4+
url = "*** please add URL for source tarball, zip or repository here ***"
5+
}
6+
description = {
7+
summary = "DSL to generate XML/HTML",
8+
homepage = "*** please enter a project homepage ***",
9+
license = "MIT"
10+
}
11+
dependencies = {
12+
"lua >= 5.1, < 5.5"
13+
}
14+
build = {
15+
type = "builtin",
16+
modules = {}
17+
}

xml-generator.lua

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
-- https://leafo.net/guides/setfenv-in-lua52-and-above.html
2+
local setfenv = setfenv or function(fn, env)
3+
local i = 1
4+
while true do
5+
local name = debug.getupvalue(fn, i)
6+
if name == "_ENV" then
7+
debug.upvaluejoin(fn, i, (function() return env end), 1)
8+
break
9+
elseif not name then
10+
break
11+
end
12+
13+
i = i + 1
14+
end
15+
16+
return fn
17+
end
18+
19+
20+
---@class xml-generator
21+
local export = {}
22+
23+
---@class XML.Node
24+
---@field tag string
25+
---@field children (XML.Node | string | fun(): XML.Node)[]
26+
---@field attributes { [string] : (string | boolean) }
27+
28+
---quotes are allowed in text, not in attributes
29+
---@param str string
30+
---@return string
31+
function export.sanitize_text(str)
32+
return (str:gsub("[<>&]", {
33+
["<"] = "&lt;",
34+
[">"] = "&gt;",
35+
["&"] = "&amp;"
36+
}))
37+
end
38+
39+
---@param str string
40+
---@return string
41+
function export.sanitize_attributes(str)
42+
return (export.sanitize_text(str):gsub("\"", "&quot;"):gsub("'", "&#39;"))
43+
end
44+
45+
---@param x any
46+
---@return type | string
47+
local function typename(x)
48+
local mt = getmetatable(x)
49+
if mt and mt.__type then
50+
return mt.__type
51+
else
52+
return type(x)
53+
end
54+
end
55+
56+
---@param node XML.Node
57+
---@return string
58+
function export.node_to_string(node)
59+
local html = "<" .. node.tag
60+
61+
for k, v in pairs(node.attributes) do
62+
if type(v) == "boolean" then
63+
if v then html = html .. " " .. k end
64+
else
65+
html = html .. " " .. k .. "=\"" .. export.sanitize_attributes(v) .. "\""
66+
end
67+
end
68+
69+
html = html .. ">"
70+
71+
for i, v in ipairs(node.children) do
72+
if type(v) ~= "table" then
73+
html = html .. export.sanitize_text(tostring(v))
74+
else
75+
html = html .. export.node_to_string(v)
76+
end
77+
end
78+
79+
html = html .. "</" .. node.tag .. ">"
80+
81+
return html
82+
end
83+
84+
---@generic T
85+
---@param fn T
86+
---@return T
87+
function export.declare_xml_generator(fn)
88+
local tbl = setmetatable({}, {
89+
---@param self table
90+
---@param tag_name string
91+
__index = function(self, tag_name)
92+
---@param attributes { [string] : string, [integer] : (XML.Node | string | fun(): XML.Node) } | string
93+
---@return table | fun(children: (XML.Node | string | fun(): XML.Node)[]): XML.Node
94+
return function(attributes)
95+
---@type XML.Node
96+
local node = {
97+
tag = tag_name,
98+
children = {},
99+
attributes = {}
100+
}
101+
102+
--if we have a situation such as
103+
--[[
104+
tag "string"
105+
]]
106+
--
107+
--then the content is the `string`
108+
local tname = typename(attributes)
109+
if tname ~= "table" and tname ~= "HTML.Node" then
110+
node.attributes = attributes and { tostring(attributes) } or {}
111+
elseif tname == "XML.Node" then
112+
---local tag = div { p "hi" }
113+
---div(tag)
114+
node.children = { attributes }
115+
attributes = {}
116+
else
117+
node.attributes = attributes --[[@as any]]
118+
end
119+
120+
for i, v in ipairs(node.attributes) do
121+
if type(v) == "function" then
122+
export.declare_xml_generator(v)
123+
v = coroutine.wrap(v)
124+
for sub in v do
125+
node.children[#node.children + 1] = sub
126+
end
127+
else
128+
node.children[#node.children + 1] = v
129+
end
130+
131+
node.attributes[i] = nil
132+
end
133+
134+
return setmetatable(node, {
135+
__type = "XML.Node",
136+
137+
__tostring = export.node_to_string,
138+
139+
---@param self XML.Node
140+
---@param children (XML.Node | string | fun(): XML.Node)[]
141+
__call = function(self, children)
142+
if type(children) ~= "table" then
143+
children = { tostring(children) }
144+
end
145+
146+
for _, v in ipairs(children) do
147+
if type(v) == "function" then
148+
export.declare_xml_generator(v)
149+
v = coroutine.wrap(v)
150+
for sub in v do
151+
self.children[#self.children + 1] = sub
152+
end
153+
else
154+
self.children[#self.children + 1] = v
155+
end
156+
end
157+
158+
return self
159+
end
160+
})
161+
end
162+
end
163+
})
164+
165+
setfenv(fn, tbl)
166+
return fn
167+
end
168+
169+
---Usage:
170+
--[=[
171+
```lua
172+
local generate_html = require("html")
173+
174+
local str = generate_html(function()
175+
return html {
176+
head {
177+
title "Hello"
178+
},
179+
body {
180+
div { id = "main" } {
181+
h1 "Hello",
182+
img { src = "http://leafo.net/hi" }
183+
p [[This is a paragraph]]
184+
}
185+
}
186+
}
187+
end)
188+
189+
```
190+
]=]
191+
---@param ctx fun(): table
192+
---@return string
193+
function export.generate_xml(ctx)
194+
return export.node_to_string(export.declare_xml_generator(ctx)())
195+
end
196+
197+
---@param ctx fun(): table
198+
---@return XML.Node
199+
function export.generate_xml_node(ctx)
200+
return export.declare_xml_generator(ctx)()
201+
end
202+
203+
---Turns a lua table into an html table, recursively, with multiple levels of nesting
204+
---@param tbl table
205+
---@return XML.Node
206+
function export.table(tbl)
207+
---@diagnostic disable: undefined-global
208+
return table {
209+
function()
210+
local function getval(v)
211+
if type(v) ~= "table" or (getmetatable(v) or {}).__tostring then
212+
return tostring(v)
213+
end
214+
return html_table(v)
215+
end
216+
217+
for i, v in ipairs(tbl) do
218+
yield(
219+
tr {
220+
td(tostring(i)),
221+
td(getval(v)),
222+
}
223+
)
224+
225+
tbl[i] = nil
226+
end
227+
228+
for k, v in pairs(tbl) do
229+
yield(
230+
tr {
231+
td(tostring(k)),
232+
td(getval(v)),
233+
}
234+
)
235+
end
236+
end
237+
}
238+
---@diagnostic enable: undefined-global
239+
end
240+
241+
export.declare_xml_generator(export.table)
242+
243+
---@alias OptionalStringCollection string | string[]
244+
---@param css { [OptionalStringCollection] : { [OptionalStringCollection] : (OptionalStringCollection) } }
245+
---@return XML.Node
246+
function export.style(css)
247+
local css_str = ""
248+
for selector, properties in pairs(css) do
249+
if type(selector) == "table" then selector = table.concat(selector, ", ") end
250+
251+
css_str = css_str .. selector .. " {\n"
252+
for property, value in pairs(properties) do
253+
if type(value) == "table" then value = table.concat(value, ", ") end
254+
255+
css_str = css_str .. " " .. property .. ": " .. value .. ";\n"
256+
end
257+
css_str = css_str .. "}\n"
258+
end
259+
260+
return export.generate_xml_node(function()
261+
---@diagnostic disable: undefined-global
262+
return style(css_str)
263+
---@diagnostic enable: undefined-global
264+
end)
265+
end
266+
267+
return export

0 commit comments

Comments
 (0)