欢迎各位引航者访问卡拉彼丘WIKI,由于游客访问页面会有一定的页面旧版缓存,建议你登陆B站账号进行浏览,如果您觉得本WIKI有帮助到你,欢迎推荐给身边的引航者。
模块:ACandy
ACandy是一个构建HTML的Lua模块。利用Lua的语法糖和元表,ACandy提供了一个易用的方式来从Lua构建HTML。大概算是一个内部DSL。
瞄一瞄
local a = require "Module:ACandy"
local some = a.some
local example = a.Fragment {
a.h1["#top heading heading-1"] "Hello!",
a.div { class="container", style="margin: 0 auto;",
a.p {
"My name is ", a.dfn("ACandy"), ", a module for building HTML.",
a.br,
"Thank you for your visit.",
},
a.p "visitors:",
a.ul / some.li("Alice", "Bob", "Carol", "..."),
},
}
print(example)
输出(经过格式化,下同):
<h1 id="top" class="heading heading-1">Hello!</h1>
<div style="margin: 0 auto;" class="container">
<p>
My name is <dfn>ACandy</dfn>, a module for building HTML.<br>
Thank you for your visit.
</p>
<p>visitors:</p>
<ul>
<li>Alice</li>
<li>Bob</li>
<li>Carol</li>
<li>...</li>
</ul>
</div>
导入
local a = require("Module:ACandy")
建议使用a
来重命名acandy
,这是因为:
a
是ACandy的首字母;a
很短,打起来方便;a.xxx
可以理解为英语的“一个xxx”。
下文的a
均指代本模块。
创建元素
一个基本的例子如下:
local elem = a.p {
class="my-paragraph", style="color: #114514;",
"This sentence is inside a ", a.code("<p>"), " element.",
}
print(elem)
表的键值对和序列分别表示元素的属性和子结点,正如a.p
那样。若仅有一个子结点且不需要设置属性,可以直接将该结点作为函数参数,所以a.code("...")
和a.code({ "..." })
是等价的。
该代码的输出,格式化后(下同)如下。
<p class="my-paragraph" style="color: #114514;">
This sentence is inside a <code><p></code> element.
</p>
对于HTML元素,a.xxx
是不区分大小写的,因此a.div
、a.Div
、a.DIV
……是同一个值,它们都将变成<div></div>
。而对于其他元素,a.xxx
是大小写敏感的。
属性
通过表的键值对为元素提供属性。其中,键必须是合法的XML字符串(目前模块仅支持ASCII字符);值可以是以下内容:
nil
和false
表示没有此属性;true
表示此为布尔值属性,例如,a.script { async=true }
表示<script async></script>
;- 其余值,将会对其进行
tostring
,并转义其中的< > & "
。
子结点
通过表的序列部分为元素提供子结点。除nil
之外的值均可作为子结点。
元素、字符串、数字、布尔值等后文没有提到的值
在元素字符串化时,对这些值尝试tostring
,并转义其中的< > &
。如果不期望自动的转义,可以将内容放在a.Raw
。
在下面这个例子中,我们将三个元素(<p>
)作为<article>
的子结点,并分别将字符串、数字、布尔值作为<p>
的元素。结果显而易见。
local elem = a.article {
a.p "Lorem ipsum...", -- or `a.p { "Lorem ipsum..." }`
a.p(2), -- or `a.p { 2 }`
a.p(true), -- or `a.p { true }`
}
print(elem)
<article>
<p>Lorem ipsum...</p>
<p>2</p>
<p>true</p>
</article>
表
在元素字符串化时,表可能被当作序列,ACandy会递归地对序列中的元素尝试字符串化。
以下表将被视为序列:
- 未设置元表的表,如
{ 1, 2, 3 }
; - 由
a.Fragment
返回的表,如a.Fragment { 1, 2, 3 }
; - 元表的
"__acandy_fragment_like"
字段为true
的表,例如,可通过getmetatable(val).__acandy_fragment_like = true
使val
在字符串化时被视作序列。
除此之外的表(如a.p { 1, 2, 3 }
返回的表)会被直接通过tostring
转换为字符串,未定义__tostring
的表会引发错误。
local parts = {
"consectetur adipiscing elit, ",
"sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
}
local elem = a.div {
"Lorem ipsum dolor sit amet, ",
parts,
}
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
函数
可以将函数作为子结点,这相当于调用函数,并将返回值作为子结点,唯一的区别在于函数将被推迟到tostring
时调用。
local elem = a.ul {
a.li "item 1",
a.li {
function() -- function returning string
return "item 2"
end,
}
function() -- function returning element
return a.li "item 3"
end,
function() -- function returning sequence
local list = {}
for i = 4, 6 do
list[#list+1] = a.li("item "..i)
end
return list
end,
}
print(elem)
<ul>
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
<li>item 4</li>
<li>item 5</li>
<li>item 6</li>
</ul>
方括号语法(设置元素属性)
在方括号内放置字符串可以快速设置id
和class
。
local elem = a.div["#my-id my-class-1 my-class-2"] {
a.p "You know what it is.",
}
print(elem)
在方括号内放置表可以设置元素属性,不局限于id
和class
。这让复用属性变得更方便。
local attr = {
id="my-id",
class="my-class-1 my-class-2",
}
local elem = a.div[attr] {
a.p "You know what it is.",
}
print(elem)
上面两段代码的输出均为:
<div id="my-id" class="my-class-1 my-class-2">
<p>You know what it is.</p>
</div>
斜杠语法(元素链)
local syntax = <elem1> / <elem2> / <elem3>
local example = a.main / a.div / a.p { ... }
相当于
local syntax = <elem1>(<elem2>(<elem3>))
local example = a.main {
a.div {
a.p { ... }
}
}
前提是<elem1>
、<elem2>
不是空元素(如<br>
)或已构建元素。
local li_link = a.li / a.a
local elem = (
a.header["site-header"] / a.nav / a.ul {
li_link { href="/home", "Home" },
li_link { href="/posts", "Posts" },
li_link { href="/about", "About" },
}
)
print(elem)
<header class="site-header">
<nav>
<ul>
<li><a href="/home">Home</a></li>
<li><a href="/posts">Posts</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
</header>
acandy.Fragment
Fragment 承载多个元素。a.Fragment
和普通表的仅有的区别就是:
- 设置了
__tostring
,可以得到HTML字符串; - 设置了
__index
,可以以类似面向对象的形式调用table.insert
、table.remove
等table
库中所有以表为第一个参数的方法。
可以通过a.Fragment()
或a.Fragment({})
创建一个空的Fragment。
当仅有一个元素时,a.Fragment(<值>)
与a.Fragment({ <值> })
等价。
例子:
local frag = a.Fragment {
a.p "第一段。",
a.p "第二段。",
}
frag:insert(a.p("第三段。"))
print(frag)
<p>第一段。</p>
<p>第二段。</p>
<p>第三段。</p>
acandy.Raw
a.Raw
用于使字符串在最终不被转义。它接收任意类型的值,并调用tostring
,存储于内部。
- 设置了
__tostring
,可以得到对应字符串; - 设置了
__concat
,可以连接两个由a.Raw
得到的对象。
例子:
local elem = a.ul {
a.li "foo <br> bar",
a.li(a.Raw "foo <br> bar"),
a.li(a.Raw("foo <b")..a.Raw("r> bar")),
a.li { a.Raw("foo <b"), a.Raw("r> bar") },
}
<ul>
<li>foo <br> bar</li>
<li>foo <br> bar</li>
<li>foo <br> bar</li>
<li>foo <br> bar</li>
</ul>
acandy.some
local frag1 = a.some.<tag>(<arg1>, <arg2>, ...)
local frag2 = a.some.<tag>[<attr>](<arg1>, <arg2>, ...)
相当于
local frag1 = a.Fragment {
a.<tag>(<arg1>),
a.<tag>(<arg2>),
...,
}
local frag2 = a.Fragment {
a.<tag>[<attr>](<arg1>),
a.<tag>[<attr>](<arg2>),
...,
}
例子:
local some = a.some
local items = a.ul(some.li['my-li']("item 1", "item 2"))
print(items)
<ul>
<li class="my-li">item 1</li>
<li class="my-li">item 2</li>
</ul>
元素实例属性
如果一个元素是a.div(...)
、a.div[...](...)
这类进行函数调用得出的元素,则称它为“已构建元素”;已构建元素作为元素链末端的元素时,该元素链同样返回一个已构建元素;而a.div
、a.div[...]
则不属于已构建元素。
对于一个已构建的元素elem
,它有如下属性。
elem.tag_name
:元素的标签名,可以重新赋值。elem.attributes
:一个表,存储着元素的所有属性,对此表的更改会生效于元素本身;不可重新赋值。elem.children
:一个Fragment,存储着元素的所有子结点,对此表的更改会生效于元素本身;不可重新赋值。elem.some_attribute
(some_attribute
为字符串):相当于elem.attributes.some_attribute
。elem[n]
(n
为整数):相当于elem.children[n]
。
例子:
local elem = a.ol { id="my-id",
a.li "item 1",
}
-- get
elem.tag_name --> "ol"
elem.children[1] --> a.li "item 1"
elem[1] == elem.children[1] --> true
elem.attributes.id --> "my-id"
elem.id == elem.attributes.id --> true
-- set
elem.tag_name = 'ul'
elem.children:insert(a.li "item 2")
elem[3] = a.li "item 3"
elem.attributes.id = "new-id"
elem.style = "color: blue;"
print(elem)
<ul id="new-id" style="color: blue;">
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
</ul>
--[[
# ACandy
A module for building HTML.
一个用于构建HTML的模块。
Version requirement: Lua 5.1 or higher
GitHub: https://github.com/AmeroHan/ACandy
MIT License
Copyright (c) 2023 - 2024 AmeroHan
]]
local type = type
local pairs = pairs
local getmt = getmetatable
local setmt = setmetatable
local assert = assert
local concat = table.concat
local ipairs = ipairs
local rawget = rawget
local rawset = rawset
local tostring = tostring
local utils = require('Module:ACandy/utils')
local VOID_ELEMS, HTML_ELEMS, NO_ENCODE_ELEMS = (function ()
local config = require('Module:ACandy/elem_config')
return config.VOID_ELEMS, config.HTML_ELEMS, config.NO_ENCODE_ELEMS
end)()
---@class Symbol
local SYM_ATTR_MAP = {} ---@type Symbol
local SYM_STRING = {} ---@type Symbol
local SYM_CHILDREN = {} ---@type Symbol
local SYM_TAG_NAME = {} ---@type Symbol
local MTKEY_FRAG_LIKE = '__acandy_fragment_like'
local MTKEY_PROPS_LIKE = '__acandy_props_like'
local Raw_mt ---@type metatable
--- Create a Raw object, which would not be encoded when converted to string.
--- @param val any value to be converted to string by `tostring()`
--- @return table
local function Raw(val)
return setmt({[SYM_STRING] = tostring(val)}, Raw_mt)
end
Raw_mt = {
__tostring = function(self)
return self[SYM_STRING]
end,
__concat = function(left, right)
if getmt(left) ~= Raw_mt or getmt(right) ~= Raw_mt then
error('Raw object can only be concatenated with another Raw object', 2)
end
return setmt({[SYM_STRING] = left[SYM_STRING]..right[SYM_STRING]}, Raw_mt)
end,
__newindex = function ()
error('Raw object is not mutable', 2)
end,
}
--[[
## Fragment
A Fragment is an array-like table with metatable `Fragment_mt`.
]]
---@class Fragment: table
--- Metatable used by Fragment object.
---@type metatable
local Fragment_mt
--- Whether `t` can be treated as a Fragment.
---@param t table
---@return boolean
local function is_table_fragment_like(t)
local mt = getmt(t)
return not mt or mt == Fragment_mt or mt[MTKEY_FRAG_LIKE] == true
end
--- Append the serialized string of the Fragment to `strs`.
--- Use len to avoid calling `#strs` repeatedly. This improves performance by
--- ~1/3.
---@param strs table
---@param frag table
---@param strs_len? integer number of `strs`, used to optimize performance.
---@param no_encode? boolean true to prevent encoding strings, e.g. when in <script>.
local function extend_strings_with_fragment(strs, frag, strs_len, no_encode)
if #frag == 0 then return end
strs_len = strs_len or #strs
local function append_serialized(node)
local node_type = type(node)
if node_type == 'table' and is_table_fragment_like(node) then -- Fragment
for _, child_node in ipairs(node) do
append_serialized(child_node)
end
elseif node_type == 'function' then
append_serialized(node())
elseif node_type == 'string' then
strs_len = strs_len + 1
strs[strs_len] = no_encode and node or utils.html_encode(node)
else -- others: Raw, Element, boolean, number
strs_len = strs_len + 1
strs[strs_len] = tostring(node)
end
end
append_serialized(frag)
end
--- Flat and concat the Fragment, retruns string.
---@param frag table
---@return string
local function concat_fragment(frag)
if #frag == 0 then return '' end
local children = {}
extend_strings_with_fragment(children, frag, 0)
return concat(children)
end
Fragment_mt = {
__tostring = concat_fragment,
__index = {
concat = table.concat,
insert = table.insert,
---@diagnostic disable-next-line: deprecated
maxn = table.maxn, -- Lua 5.1 only
move = table.move,
remove = table.remove,
sort = table.sort,
---@diagnostic disable-next-line: deprecated
unpack = table.unpack or unpack,
},
}
--- Constructor of Fragment.
---@param children any?
---@return Fragment
local function Fragment(children)
if type(children) == 'table' and is_table_fragment_like(children) then
return setmt(utils.shallow_icopy(children), Fragment_mt)
end
return setmt({children}, Fragment_mt)
end
---@class BareElement
--[[
### BareElement
A BareElement is an Element without any properties except tag name, e.g.
`acandy.div`. It is immutable and can be cached and reused.
Indexing a BareElement would return a BuildingElement, and calling it would
return a BuiltElement. Both methods would not change the element itself.
```lua
local bare_div = acandy.div
```
]]
---@class BuildingElement
--[[
### BuildingElement
A BuildingElement is an Element derived from attribute shorthand syntex. The
shorthand is a string of id and space-separated class names, and the syntex is
to index the BareElement with a shorthand string, i.e. to put it inside the
brackets followed after the tag name, e.g. `acandy.div['#id cls1 cls2']`.
```lua
local building_div = acandy.div['#id cls1 cls2']
```
Similar to BareElements, a BuildingElement can be called to get a
BuiltElement with properties set.
]]
---@class BuiltElement
--[[
### BuiltElement
A BuiltElement is an Element derived from a BareElement or a BuildingElement by
calling it, which would return the BuiltElement with properties set.
```lua
local built_pre1 = acandy.pre {
class = 'lang-lua';
"print('Hello, ACandy!')",
}
local built_pre2 = acandy.pre['lang-lua'] "print('Hello, ACandy!')"
```
Although named "Built", it is still mutable. Its properties can be changed by
assigning.
]]
---@class ElementChain
---@param strs string[]
---@param attr_map {[string]: string | number | boolean}
---@param strs_len integer?
---@return integer
local function extend_strings_with_attrs(strs, attr_map, strs_len)
strs_len = strs_len or #strs
for k, v in pairs(attr_map) do
if v == true then
-- boolean attributes
-- https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes#boolean_attributes
strs_len = strs_len + 2
strs[strs_len-1] = ' '
strs[strs_len] = k
elseif v then -- exclude the case `v == false`
strs_len = strs_len + 5
strs[strs_len-4] = ' '
strs[strs_len-3] = k
strs[strs_len-2] = '="'
strs[strs_len-1] = utils.attr_encode(tostring(v))
strs[strs_len] = '"'
end
end
return strs_len
end
local BareElement_mt ---@type metatable
local BuiltElement_mt ---@type metatable
local BuildingElement_mt ---@type metatable
local ElementChain_mt ---@type metatable
---@param tag_name string
---@return BareElement
local function BareElement(tag_name)
local str
if VOID_ELEMS[tag_name] then
str = '<'..tag_name..'>'
else
str = '<'..tag_name..'></'..tag_name..'>'
end
local elem = {
[SYM_TAG_NAME] = tag_name, ---@type string
[SYM_STRING] = str, ---@type string
}
return setmt(elem, BareElement_mt)
end
---@param tag_name string
---@param attr_map {[string]: string | number | boolean}
---@return BuildingElement
local function BuildingElement(tag_name, attr_map)
local elem = {
[SYM_TAG_NAME] = tag_name,
[SYM_ATTR_MAP] = attr_map,
[SYM_CHILDREN] = not VOID_ELEMS[tag_name] and {} or nil,
}
return setmt(elem, BuildingElement_mt)
end
---@param tag_name string
---@param attr_map {[string]: string | number | boolean}
---@param children? any[]
---@return BuiltElement
local function BuiltElement(tag_name, attr_map, children)
assert(not (VOID_ELEMS[tag_name] and children), 'void elements cannot have children')
assert(VOID_ELEMS[tag_name] or type(children) == 'table', 'non-void elements must have children')
local elem = {
[SYM_TAG_NAME] = tag_name,
[SYM_ATTR_MAP] = attr_map,
[SYM_CHILDREN] = children,
}
return setmt(elem, BuiltElement_mt)
end
---@param tag_names string[]
---@param attr_maps {[string]: string | number | boolean}[]
---@return ElementChain
local function ElementChain(tag_names, attr_maps)
local elem_chain = {
[SYM_TAG_NAME] = tag_names,
[SYM_ATTR_MAP] = attr_maps,
}
return setmt(elem_chain, ElementChain_mt)
end
---@param self BareElement
---@return string
local function bare_elem_to_string(self)
return self[SYM_STRING]
end
--- Convert the object into HTML code.
---@param self BuildingElement | BuiltElement
---@return string
local function elem_to_string(self)
local tag_name = self[SYM_TAG_NAME]
-- format open tag
local result = {'<', tag_name}
extend_strings_with_attrs(result, self[SYM_ATTR_MAP])
result[#result+1] = '>'
-- retrun without children or close tag when being a void element
-- void element: https://developer.mozilla.org/en-US/docs/Glossary/Void_element
if VOID_ELEMS[tag_name] then
return concat(result)
end
-- format children
extend_strings_with_fragment(result, self[SYM_CHILDREN], nil, NO_ENCODE_ELEMS[tag_name])
-- format close tag
result[#result+1] = '</'
result[#result+1] = tag_name
result[#result+1] = '>'
return concat(result)
end
--- Return tag name, attribute or child node depending on the key.
---@param self BuiltElement
---@param key string | number
local function get_elem_prop(self, key)
if key == 'tag_name' then
return self[SYM_TAG_NAME]
elseif key == 'children' then
local children = rawget(self, SYM_CHILDREN)
return children and setmt(children, Fragment_mt)
elseif key == 'attributes' then
return self[SYM_ATTR_MAP]
elseif type(key) == 'string' then
-- e.g. `elem.class`
return self[SYM_ATTR_MAP][key]
elseif type(key) == 'number' then
-- e.g. `elem[1]`
local children = rawget(self, SYM_CHILDREN)
return children and children[key] -- no error for ipairs
end
error("element property key type is neither 'string' nor 'number'", 2)
end
local function is_table_props_like(t)
local mt = getmt(t)
return not mt or mt[MTKEY_PROPS_LIKE] == true
end
---@param self BareElement | BuildingElement | BuiltElement
---@param props_or_child any
---@return BuiltElement
local function new_built_elem_from_props(self, props_or_child)
local tag_name = self[SYM_TAG_NAME]
local attr_map = rawget(self, SYM_ATTR_MAP) or {}
local new_attr_map = utils.shallow_copy(attr_map)
local arg_is_props_like = type(props_or_child) == 'table' and is_table_props_like(props_or_child)
if VOID_ELEMS[tag_name] then -- void element, e.g. <br>, <img>
if arg_is_props_like then
-- props_or_child is props
-- set attributes
for k, v in pairs(props_or_child) do
if type(k) == 'string' then
if not utils.is_valid_xml_name(k) then
error('invalid attribute name: '..k, 2)
end
new_attr_map[k] = v;
end
end
end
return BuiltElement(tag_name, new_attr_map)
end
local new_children = {}
if arg_is_props_like then
-- props_or_child is props
for k, v in pairs(props_or_child) do
if type(k) == 'number' then
new_children[k] = v;
elseif type(k) == 'string' then
if not utils.is_valid_xml_name(k) then
error('invalid attribute name: '..k, 2)
end
new_attr_map[k] = v;
end
end
else -- props_or_child is child
new_children[1] = props_or_child
end
return BuiltElement(tag_name, new_attr_map, new_children)
end
--- Assign to tag name, attribute or child node depending on the key.
---@param self BuildingElement | BuiltElement
---@param key string | number
---@param val any
local function set_elem_prop(self, key, val)
if key == 'tag_name' then
-- e.g. elem.tag_name = 'div'
if not utils.is_valid_xml_name(val) then
error('invalid tag name: '..val, 2)
end
local lower = val:lower()
if HTML_ELEMS[lower] then
val = lower
end
if self[SYM_TAG_NAME] == val then return end
-- 根据元素类型,创建/删除子节点
if VOID_ELEMS[val] and rawget(self, SYM_CHILDREN) then
self[SYM_CHILDREN] = nil
elseif not (VOID_ELEMS[val] or rawget(self, SYM_CHILDREN)) then
rawset(self, SYM_CHILDREN, {})
end
-- 为tag_name赋值
self[SYM_TAG_NAME] = val
elseif key == 'children' or key == 'attributes' then
error('attempt to replace the '..key..' table of the element')
elseif type(key) == 'string' then
-- e.g. elem.class = 'content'
if not utils.is_valid_xml_name(key) then
error('invalid attribute name: '..key, 2)
end
self[SYM_ATTR_MAP][key] = val
elseif type(key) == 'number' then
-- e.g. elem[1] = 'Lorem ipsum dolor sit amet...'
local children = rawget(self, SYM_CHILDREN)
if not children then
error('attempt to assign child on a void element', 2)
end
children[key] = val
else
error('Element键类型只能是string或number', 2)
end
end
--- Sementic sugar for setting attributes.
--- e.g. `local elem = acandy.div['#id cls1 cls2']`
---@param self BareElement
---@param shorthand_attrs string | table
---@return BuildingElement
local function new_building_elem_by_shorthand_attrs(self, shorthand_attrs)
local attr_map
if type(shorthand_attrs) == 'string' then
attr_map = utils.parse_shorthand_attrs(shorthand_attrs)
elseif type(shorthand_attrs) == 'table' then
attr_map = shorthand_attrs
else
error('invalid attributes: '..tostring(shorthand_attrs), 2)
end
return BuildingElement(self[SYM_TAG_NAME], attr_map)
end
---@param self ElementChain
---@return ElementChain
local function copy_elem_chain(self)
local new_chain = {
[SYM_TAG_NAME] = utils.shallow_icopy(self[SYM_TAG_NAME]),
[SYM_ATTR_MAP] = utils.shallow_icopy(self[SYM_ATTR_MAP]),
}
return setmt(new_chain, ElementChain_mt)
end
---@param chain ElementChain
---@param tag_name string
---@param attr_map {[string]: string | number | boolean}?
local function append_elem_to_elem_chain(chain, tag_name, attr_map)
local new_len = #chain[SYM_TAG_NAME] + 1
chain[SYM_TAG_NAME][new_len] = tag_name
chain[SYM_ATTR_MAP][new_len] = attr_map
end
---@param chain1 ElementChain
---@param chain2 ElementChain
---@return ElementChain
local function connect_elem_chains(chain1, chain2)
local new_tag_names = {}
local new_attr_maps = {}
local attr_maps_to_copy_from = chain1[SYM_ATTR_MAP]
for i, tag_name in ipairs(chain1[SYM_TAG_NAME]) do
new_tag_names[i] = tag_name
new_attr_maps[i] = attr_maps_to_copy_from[i]
end
local len = #new_tag_names
attr_maps_to_copy_from = chain2[SYM_ATTR_MAP]
for i, tag_name in ipairs(chain2[SYM_TAG_NAME]) do
new_tag_names[len+i] = tag_name
new_attr_maps[len+i] = attr_maps_to_copy_from[i]
end
return ElementChain(new_tag_names, new_attr_maps)
end
---@param self ElementChain
---@return string
local function elem_chain_to_string(self)
local tag_names = self[SYM_TAG_NAME]
local attr_maps = self[SYM_ATTR_MAP]
local result = {}
for i, tag_name in ipairs(tag_names) do
result[#result+1] = '><'
result[#result+1] = tag_name
if attr_maps[i] then
extend_strings_with_attrs(result, attr_maps[i])
end
end
result[1] = '<'
for i = #tag_names, 1, -1 do
result[#result+1] = '></'
result[#result+1] = tag_names[i]
end
result[#result+1] = '>'
return concat(result)
end
---@param chain ElementChain
---@return BuiltElement root_elem, BuiltElement leaf_elem
local function elem_chain_to_built_elem(chain)
local tag_names = chain[SYM_TAG_NAME]
local attr_maps = chain[SYM_ATTR_MAP]
local leaf_elem
local function f(i)
if tag_names[i+1] then
return BuiltElement(tag_names[i], attr_maps[i] or {}, {f(i+1)})
end
leaf_elem = BuiltElement(tag_names[i], attr_maps[i] or {}, {})
return leaf_elem
end
return f(1), leaf_elem
end
---@param left ElementChain
---@param right any
---@return ElementChain | BuiltElement
local function elem_chain_div(left, right)
local right_mt = getmt(right)
if right_mt == BareElement_mt or right_mt == BuildingElement_mt then
local right_tag_name = right[SYM_TAG_NAME]
local right_attr_map = rawget(right, SYM_ATTR_MAP)
if VOID_ELEMS[right_tag_name] then
local root_elem, leaf_elem = elem_chain_to_built_elem(left)
leaf_elem[SYM_CHILDREN][1] = BuiltElement(right_tag_name, right_attr_map or {})
return root_elem
end
local new_chain = copy_elem_chain(left)
append_elem_to_elem_chain(new_chain, right_tag_name, right_attr_map)
return new_chain
elseif right_mt == ElementChain_mt then
return connect_elem_chains(left, right)
end
local root_elem, leaf_elem = elem_chain_to_built_elem(left)
leaf_elem[SYM_CHILDREN][1] = right
return root_elem
end
---@param left BareElement | BuildingElement | any
---@param right any | BareElement | BuildingElement
---@return ElementChain | BuiltElement
local function elem_div(left, right)
local left_mt = getmt(left)
if left_mt ~= BareElement_mt and left_mt ~= BuildingElement_mt then
error('attempt to div a '..type(left)..' with an element', 2)
end
local tag_name = left[SYM_TAG_NAME]
if VOID_ELEMS[tag_name] then
error('attempt to perform division on a void element', 2)
end
return elem_chain_div(ElementChain({tag_name}, {rawget(left, SYM_ATTR_MAP)}), right)
end
local function error_wrong_index()
error('attempt to access properties of a unbuilt element', 2)
end
local function error_wrong_newindex()
error('attempt to assign properties of a unbuilt element', 2)
end
BareElement_mt = {
__tostring = bare_elem_to_string, --> string
__index = new_building_elem_by_shorthand_attrs, --> BuildingElement
__call = new_built_elem_from_props, --> BuiltElement
__div = elem_div, --> ElementChain | BuiltElement
__newindex = error_wrong_newindex,
}
BuildingElement_mt = {
__tostring = elem_to_string, --> string
__call = new_built_elem_from_props, --> BuiltElement
__div = elem_div, --> ElementChain | BuiltElement
__index = error_wrong_index,
__newindex = error_wrong_newindex,
}
BuiltElement_mt = {
__tostring = elem_to_string, --> string
__index = get_elem_prop,
__newindex = set_elem_prop,
__div = function ()
error('attempt to perform division on a built element', 2)
end
}
ElementChain_mt = {
__tostring = elem_chain_to_string, --> string
__call = function(self, props) --> BuiltElement
local root_elem, leaf_elem = elem_chain_to_built_elem(self)
local new_leaf_elem = new_built_elem_from_props(leaf_elem, props)
leaf_elem[SYM_ATTR_MAP] = new_leaf_elem[SYM_ATTR_MAP]
leaf_elem[SYM_CHILDREN] = new_leaf_elem[SYM_CHILDREN]
return root_elem
end,
__div = function(left, right) --> ElementChain | BuiltElement
if getmt(left) ~= ElementChain_mt then
error('attempt to div a '..type(left)..' with an element chain', 2)
end
return elem_chain_div(left, right)
end,
__index = error_wrong_index,
__newindex = error_wrong_newindex,
}
local acandy
--- Metatable used by this module.
local acandy_mt = { ---@type metatable
--- When indexing a uncached tag name, return a constructor of that element.
---@param key string
---@return BareElement
__index = function(self, key)
if not utils.is_valid_xml_name(key) then
error('invalid tag name: '..tostring(key), 2)
end
local lower_key = key:lower()
local bare_elem
if lower_key ~= key and HTML_ELEMS[lower_key] then
bare_elem = rawget(self, lower_key)
if not bare_elem then
bare_elem = BareElement(lower_key)
self[lower_key] = bare_elem
end
else
bare_elem = BareElement(key)
end
self[key] = bare_elem
return bare_elem
end,
}
local some = setmt({}, {
__index = function(_, key)
local bare_elem = acandy[key]
local mt = {}
function mt:__index(shorthand)
local building_elem = bare_elem[shorthand]
return function(...)
return setmt(utils.map_varargs(building_elem, ...), Fragment_mt)
end
end
function mt:__call(...)
return setmt(utils.map_varargs(bare_elem, ...), Fragment_mt)
end
return setmt({}, mt)
end
})
acandy = setmt({
Fragment = Fragment,
Raw = Raw,
some = some,
}, acandy_mt)
return acandy