Skip to content

Latest commit

 

History

History
655 lines (513 loc) · 16.4 KB

README.zh.md

File metadata and controls

655 lines (513 loc) · 16.4 KB

ACandy:一个甜的构建 HTML 的 Lua 模块

🌏 English | 中文

本项目使用语义化版本

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>&lt;p&gt;</code> element.
</p>

Tip

  • 你不需要在字符串中处理 HTML 转义。如果不期望自动的转义,可以将内容放在 acandy.Raw 中。
  • 子结点并不必须是元素或字符串——虽然这里只展示了这两类,一切能 tostring 的值均可作为子结点。

a.xxxASCII 大小写不敏感的,因此 a.diva.Diva.DIV……是同一个值(即 rawequal(a.div, a.Div) == truerawequal(a.div, a.DIV) == true),它们都将变成<div></div>

属性

通过表的键值对为元素提供属性。值可以是以下内容:

  • nilfalse 表示没有此属性;
  • 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

子结点是递归处理的,所以你可以在函数里返回函数。

方括号语法(设置元素属性)

在方括号内放置字符串可以快速设置 idclass

local elem = a.div['#my-id my-class-1 my-class-2'] {
   a.p 'You know what it is.',
}
print(elem)

在方括号内放置类表值可以设置元素属性,属性不局限于 idclass。这让复用属性变得更方便。

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 的子组合器 >,只不过它用于创建元素而不是选择元素。

前提是 elem1elemN 不是空元素已构建元素

例子:

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

acandy.some

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.diva.div[...] 则不属于已构建元素。

对于一个已构建的元素 elem,它有如下属性:

  • elem.tag_name:元素的标签名,可以重新赋值。
  • elem.attributes:一个表,存储着元素的所有属性,对此表的更改会生效于元素本身;不可重新赋值。
  • elem.children:一个 Fragment,存储着元素的所有子结点,对此表的更改会生效于元素本身;不可重新赋值。
  • elem.some_attributesome_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.Fragment

Fragment 承载多个元素。Fragment 和普通表的仅有的区别就是:

  • 设置了 __tostring,可以得到 HTML 字符串;
  • 设置了 __index,可以以类似面向对象的形式调用 table.inserttable.removetable 库中所有以表为第一个参数的方法。

可以通过 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>

acandy.Raw

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 &lt;br&gt; bar</li>
   <li>foo <br> bar</li>
   <li>foo <br> bar</li>
   <li>foo <br> bar</li>
</ul>

acandy.Comment

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>

acandy.Doctype

目前仅支持 HTML5 的 doctype,通过 Doctype.HTML 获取。

tostring(acandy.Doctype.HTML)  --> '<!DOCTYPE html>'

环境方法

acandy.extend_env

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>

acandy.to_extended_env

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 &gt; 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 是否满足上述条件。

贡献

欢迎任何形式的贡献!包括但不限于汇报缺陷、提出功能建议、完善文档、优化代码。