ACandy 是一个构建 HTML 的纯 Lua 模块。利用 Lua 的语法糖和元表,ACandy 提供了一个易用的 DSL 来从 Lua 构建 HTML。
local acandy = require 'acandy'
local a, some, Fragment = acandy.a, acandy.some, acandy.Fragment
local example = 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 acandy = require('acandy')
local a = acandy.a
a
是所有元素的入口,这是因为:
a
是 ACandy 的首字母;a
很短,打起来方便;a.xxx
可以理解为英语的“一个 xxx”。
local elem = a.p {
class="my-paragraph", style="color: #114514;",
'This sentence is inside a ', a.code('<p>'), ' element.',
}
print(elem)
在这段代码中,a.p
是一个返回<p>
元素的函数,参数为一个表,表的键值对和序列分别表示元素的属性和子结点,其他元素同理。若仅有一个子结点且不需要设置属性,可以直接将该结点作为函数参数,所以 a.code('...')
和 a.code({ '...' })
是等价的。
该代码的输出,格式化后(下同)如下。
<p class="my-paragraph" style="color: #114514;">
This sentence is inside a <code><p></code> element.
</p>
Tip
- 你不需要在字符串中处理 HTML 转义。如果不期望自动的转义,可以将内容放在
acandy.Raw
中。 - 子结点并不必须是元素或字符串——虽然这里只展示了这两类,一切能
tostring
的值均可作为子结点。
a.xxx
是 ASCII 大小写不敏感的,因此 a.div
、a.Div
、a.DIV
……是同一个值(即 rawequal(a.div, a.Div) == true
、rawequal(a.div, a.DIV) == true
),它们都将变成<div></div>
。
通过表的键值对为元素提供属性。值可以是以下内容:
nil
和false
表示没有此属性;true
表示此为布尔值属性,例如,a.script { async=true }
表示<script async></script>
;- 其余值,将会对其
tostring
,并转义其中的&
、<
、>
和 NBSP。
通过表的序列部分为元素提供子结点。除 nil
之外的值均可作为子结点。当序列化时,它们遵循以下规则。
元素、字符串、数字、布尔值等后文没有提到的值均适用于以下规则。
在元素字符串化时,对这些值尝试 tostring
,并转义 &
、<
、>
和 NBSP。如果不期望自动的转义,可以将内容放在 acandy.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 将递归序列化列表中的子结点。
顺便一提,由 acandy.Fragment
返回的表(如 Fragment { 1, 2, 3 }
)是类列表的,因为它们的元表的 '__acandy_list_like'
字段被设置为 true
。
特别地,如果一个表不被认为是类列表的,如 a.p { 1, 2, 3 }
返回的表,根据默认规则,它将直接通过 tostring
转换为字符串,所以确保它实现了 __tostring
元方法。
local list1 = { '3', '4' }
local list2 = { '2', list1 }
local elem = a.div { '1', list2 }
print(elem)
<p>1234</p>
可以将函数作为子结点,这相当于调用函数,并将返回值作为子结点,唯一的区别在于函数将被推迟到 tostring
时调用。
local elem = a.ul {
a.li 'item 1',
a.li {
function () -- returns string
return 'item 2'
end,
},
function () -- returns element
return a.li 'item 3'
end,
function () -- returns list
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>
Tip
子结点是递归处理的,所以你可以在函数里返回函数。
在方括号内放置字符串可以快速设置 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>
elem1 / elem2 / ... / elemN / tail_value
相当于:
elem1(
elem2(
...(
elemN(tail_value)
)
)
)
有点像 CSS 的子组合器 >
,只不过它用于创建元素而不是选择元素。
例子:
local link_item = a.li / a.a
local text = 'More coming soon...'
local elem = (
a.header['site-header'] / a.nav / a.ul {
link_item { href="/home", 'Home' },
link_item { href="/posts", 'Posts' },
link_item { href="/about", 'About' },
a.li / text,
}
)
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>
<li>More coming soon...</li>
</ul>
</nav>
</header>
Tip
面包屑可以缓存,就像上面这个例子中的 link_item
。
local frag1 = some.xxx(arg1, arg2, ...)
local frag2 = some.xxx[attr](arg1, arg2, ...)
相当于:
local frag1 = Fragment {
a.xxx(arg1),
a.xxx(arg2),
...,
}
local frag2 = Fragment {
a.xxx[attr](arg1),
a.xxx[attr](arg2),
...,
}
例子:
local some = acandy.some
local items = a.ul {
some.li['my-li']('item 1', 'item 2'),
some.li('item 3', 'item 4'),
}
print(items)
<ul>
<li class="my-li">item 1</li>
<li class="my-li">item 2</li>
<li>item 3</li>
<li>item 4</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>
Fragment
承载多个元素。Fragment
和普通表的仅有的区别就是:
- 设置了
__tostring
,可以得到 HTML 字符串; - 设置了
__index
,可以以类似面向对象的形式调用table.insert
、table.remove
等table
库中所有以表为第一个参数的方法。
可以通过 Fragment()
或 Fragment({})
创建一个空的 Fragment。
当仅有一个元素时,Fragment(<child>)
与 Fragment({ <child> })
等价。
例子:
local Fragment = acandy.Fragment
local frag = Fragment {
a.p 'First paragraph.',
a.p 'Second paragraph.',
}
frag:insert(a.p('Third paragraph.'))
print(frag)
<p>First paragraph.</p>
<p>Second paragraph.</p>
<p>Third paragraph.</p>
Raw
用于使字符串在最终不被转义。它接收任意类型的值,并调用 tostring
,存储于内部。
- 设置了
__tostring
,可以通过tostring
得到对应字符串; - 设置了
__concat
,可以通过..
连接两个由Raw
得到的对象。
例子:
local Raw = acandy.Raw
local elem = a.ul {
a.li 'foo <br> bar',
a.li(Raw 'foo <br> bar'),
a.li(Raw('foo <b')..Raw('r> bar')),
a.li { Raw('foo <b'), 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>
Comment
创建一个注释结点。
local elem = a.p {
'Hello, ',
acandy.Comment 'This is a comment.',
'world!',
acandy.Comment(),
}
print(elem)
<p>Hello, <!--This is a comment.-->world!<!----></p>
目前仅支持 HTML5 的 doctype,通过 Doctype.HTML
获取。
tostring(acandy.Doctype.HTML) --> '<!DOCTYPE html>'
function acandy.extend_env(env: table): ()
使用 acandy.a
作为 __index
来扩展传入的环境,例如 _ENV
。这使得能够直接使用元素名不需要显式地使用 a.
,除非与局部变量或全局变量有命名冲突。
Warning
不建议对全局环境使用此方法,因为可能会造成难以察觉的命名冲突。
local acandy = require 'acandy'
local a = acandy.a
acandy.extend_env(_ENV)
print(
-- normally you can access an element without `a.`
div {
-- use `a.table` to avoid the naming conflict with Lua's `table` module (a global value)
a.table {
tr { td 'foo' },
},
-- or use a different case
TABLE {
tr { td 'bar' },
}
ul {
-- use `a.a` to avoid the naming conflict with `a` from `acandy` (a local value)
li / a.a { href="/home", 'Home' },
-- or use a different case
li / A { href="/about", 'About' },
}
}
)
<div>
<table>
<tr><td>foo</td></tr>
</table>
<table>
<tr><td>bar</td></tr>
</table>
<ul>
<li><a href="/home">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</div>
function acandy.to_extended_env(env: table): table
类似于 acandy.extend_env
,但返回一个新表而不是修改原表。
-- on Lua 5.2+
local function get_article()
local _ENV = acandy.to_extended_env(_ENV)
return (
article {
header / h2 'Title',
main {
p 'Paragraph 1',
p 'Paragraph 2',
}
}
)
end
-- on Lua 5.1
local get_article = setfenv(function ()
return (
article {
header / h2 'Title',
main {
p 'Paragraph 1',
p 'Paragraph 2',
}
}
)
end, acandy.to_extended_env(_G))
print(get_article())
<article>
<header>
<h2>Title</h2>
</header>
<main>
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</main>
</article>
ACandy 默认为 HTML 模式(目前只有 HTML 模式,以后将会支持 XML),并预定义了一些 HTML 空元素和原始文本元素(见 config.lua)。
ACandy 不支持修改全局配置,要修改配置,请创建一个配置后的 ACandy
实例,其函数签名如下。
type Config = {
void_elements: { [string]: true },
raw_text_elements: { [string]: true },
}
function acandy.ACandy(output_type: 'html', modify_config?: (config: Config) -> ()): table
其中,output_type
参数目前只能传入 'html'
。modify_config
参数(可选)是一个函数,接收一个表作为参数,无返回值。传入该函数的表是新配置的基础,你可以在函数中修改这个值,例如:
local acandy = require('acandy').ACandy('html', function (config)
-- add a void element
config.void_elements['my-void-element'] = true
-- remove `br` from void elements
config.void_elements.br = nil
-- add a raw text element
config.raw_text_elements['my-raw-text-element'] = true
-- remove `script` from raw text elements
config.raw_text_elements.script = nil
end)
local a = acandy.a
print(
acandy.Fragment {
a['my-void-element'],
a.br,
a['my-raw-text-element'] '< > &',
a.script 'let val = 2 > 1',
}
)
<my-void-element>
<br></br>
<my-raw-text-element>< > &</my-raw-text-element>
<script>let val = 2 > 1</script>
要想在整个项目使用这个配置,可以导出配置后的 ACandy
实例,然后在其他文件中导入这个实例。
-- my_acandy.lua
return require('acandy').ACandy('html', function (config)
-- ...
end)
-- other files
local acandy = require('my_acandy')
类表(table-like)值是指可以当作表来读取的值。当且仅当一个值 t
符合以下条件时,该值被认为是类表值:
-
满足任意一条:
-
t
是一个表,且未设置元表。 -
t
的元表的'__acandy_table_like'
字段为true
(可通过getmetatable(t).__acandy_table_like = true
设置)。使用者需要确保t
能够:- 通过
t[k]
读取内容; - 通过
#t
获取序列长度; - 通过
pairs(t)
和ipairs(t)
遍历键值。
ACandy 仅检查元表
'__acandy_table_like'
字段,不会检查t
是否满足上述条件。 - 通过
-
类列表(list-like)值是指可以当作序列来读取的值。当且仅当一个值 t
符合以下条件时,该值被认为是类列表值:
-
满足任意一条:
-
t
是一个类表值。 -
t
的元表的'__acandy_list_like'
字段为true
(可通过getmetatable(t).__acandy_table_like = true
设置)。使用者需要确保t
能够:- 通过
t[k]
读取内容; - 通过
#t
获取序列长度; - 通过
ipairs(t)
遍历值。
ACandy 仅检查元表
'__acandy_list_like'
字段,不会检查t
是否满足上述条件。 - 通过
-
欢迎任何形式的贡献!包括但不限于汇报缺陷、提出功能建议、完善文档、优化代码。