Skip to content

Commit

Permalink
fix: 序列化未对结点__tostring元方法返回的结果转义。顺便增加其他测试
Browse files Browse the repository at this point in the history
close #11
  • Loading branch information
AmeroHan committed Nov 19, 2024
1 parent f807f9d commit a84ecee
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 72 deletions.
83 changes: 43 additions & 40 deletions acandy.lua
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ local VOID_ELEMS, HTML_ELEMS, RAW_TEXT_ELEMS = (function ()
local config = require('.elem_config')
return config.VOID_ELEMS, config.HTML_ELEMS, config.RAW_TEXT_ELEMS
end)()

local classes = require('.classes')
local node_mts = classes.node_mts

---@class Symbol

Expand Down Expand Up @@ -61,47 +62,51 @@ end
---@type metatable
local Fragment_mt

---Append the serialized string of the Fragment to `strs`.
---Use len to avoid calling `#strs` repeatedly. This improves performance by
---Append the serialized string of the Fragment to `buff`.
---Use len to avoid calling `#buff` repeatedly. This improves performance by
---~1/3.
---@param strs table
---@param buff table
---@param frag table
---@param strs_len? integer number of `strs`, used to optimize performance.
---@param buff_len? integer length of `buff`, 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)
local function extend_str_buff_with_frag(buff, frag, buff_len, no_encode)
if #frag == 0 then return end
strs_len = strs_len or #strs
buff_len = buff_len or #buff

local function append_serialized(node)
local node_type = type(node)
if container_level_of(node) >= 1 then -- Fragment
if container_level_of(node) >= 1 then -- Fragment, list
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)
buff_len = buff_len + 1
buff[buff_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)
local str = tostring(node)
if not (node_mts[getmt(node)] or no_encode) then
str = utils.html_encode(str)
end
buff_len = buff_len + 1
buff[buff_len] = str
end
end

append_serialized(frag)
end

Fragment_mt = {
Fragment_mt = node_mts:register {
---Flat and concat the Fragment, returns string.
---@param self Fragment
---@return string
__tostring = function (self)
if #self == 0 then return '' end

local children = {}
extend_strings_with_fragment(children, self, 0)
return concat(children)
local buff = {}
extend_str_buff_with_frag(buff, self, 0)
return concat(buff)
end,
__index = {
concat = table.concat,
Expand All @@ -111,8 +116,7 @@ Fragment_mt = {
move = table.move,
remove = table.remove,
sort = table.sort,
---@diagnostic disable-next-line: deprecated
unpack = table.unpack or unpack,
unpack = table.unpack or unpack, ---@diagnostic disable-line: deprecated
},
[KEY_LIST_LIKE] = true,
}
Expand Down Expand Up @@ -172,29 +176,29 @@ end
---@class Breadcrumb


---@param strs string[]
---@param buff string[]
---@param attr_map {[string]: string | number | boolean}
---@param strs_len integer?
---@param buff_len integer?
---@return integer
local function extend_strings_with_attrs(strs, attr_map, strs_len)
strs_len = strs_len or #strs
local function extend_str_buff_with_attrs(buff, attr_map, buff_len)
buff_len = buff_len or #buff
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
buff_len = buff_len + 2
buff[buff_len - 1] = ' '
buff[buff_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] = '"'
buff_len = buff_len + 5
buff[buff_len - 4] = ' '
buff[buff_len - 3] = k
buff[buff_len - 2] = '="'
buff[buff_len - 1] = utils.attr_encode(tostring(v))
buff[buff_len] = '"'
end
end
return strs_len
return buff_len
end


Expand Down Expand Up @@ -277,7 +281,7 @@ local function elem_to_string(self)

-- format open tag
local result = {'<', tag_name}
extend_strings_with_attrs(result, self[SYM_ATTR_MAP])
extend_str_buff_with_attrs(result, self[SYM_ATTR_MAP])
result[#result+1] = '>'

-- return without children or close tag when being a void element
Expand All @@ -287,7 +291,7 @@ local function elem_to_string(self)
end

-- format children
extend_strings_with_fragment(result, self[SYM_CHILDREN], nil, RAW_TEXT_ELEMS[tag_name])
extend_str_buff_with_frag(result, self[SYM_CHILDREN], nil, RAW_TEXT_ELEMS[tag_name])
-- format close tag
result[#result+1] = '</'
result[#result+1] = tag_name
Expand Down Expand Up @@ -495,7 +499,7 @@ local function breadcrumb_to_string(self)
result[#result+1] = '><'
result[#result+1] = tag_name
if attr_maps[i] then
extend_strings_with_attrs(result, attr_maps[i])
extend_str_buff_with_attrs(result, attr_maps[i])
end
end
result[1] = '<'
Expand Down Expand Up @@ -581,29 +585,29 @@ local function error_wrong_newindex()
end


BareElement_mt = {
BareElement_mt = node_mts:register {
__tostring = bare_elem_to_string, --> string
__index = new_building_elem_by_shorthand_attrs, --> BuildingElement
__call = new_built_elem_from_props, --> BuiltElement
__div = elem_div, --> Breadcrumb | BuiltElement
__newindex = error_wrong_newindex,
}
BuildingElement_mt = {
BuildingElement_mt = node_mts:register {
__tostring = elem_to_string, --> string
__call = new_built_elem_from_props, --> BuiltElement
__div = elem_div, --> Breadcrumb | BuiltElement
__index = error_wrong_index,
__newindex = error_wrong_newindex,
}
BuiltElement_mt = {
BuiltElement_mt = node_mts:register {
__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,
}
Breadcrumb_mt = {
Breadcrumb_mt = node_mts:register {
__tostring = breadcrumb_to_string, --> string
__call = function (self, props) --> BuiltElement
local root_elem, leaf_elem = breadcrumb_to_built_elem(self)
Expand Down Expand Up @@ -715,7 +719,6 @@ local function to_extended_env(env)
return setmt(utils.raw_shallow_copy(env), new_mt)
end

local classes = require('.classes')

return {
-- namespaces
Expand Down
39 changes: 29 additions & 10 deletions classes.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,25 @@ local tostring = tostring

local classes = {}

---@type {[metatable]: true, register: fun(self: self, mt: metatable): metatable}
local node_mts = {
---Register a metatable as a node metatable.
register = function (self, mt)
self[mt] = true
return mt
end,
}
---To judge when a table with `__tostring` is being serialized, whether or not
---escape the string. If the table is an acandy node, it should not be escaped,
---as the node itself will handle the escaping.
classes.node_mts = node_mts

local SYM_CONTENT = {} ---@type Symbol


---@class Comment

local Comment_mt = {
local Comment_mt = node_mts:register {
__tostring = function (self)
return '<!--'..(self[SYM_CONTENT] or '')..'-->'
end,
Expand All @@ -29,7 +43,7 @@ function classes.Comment(content)
or s:find('-->', 1, true)
or content:find('--!>', 1, true)
then
error('invalid comment content: '..content, 2)
error('invalid comment content: '..('%q'):format(content), 2)
end
end
return setmt({[SYM_CONTENT] = content}, Comment_mt)
Expand All @@ -43,17 +57,22 @@ end
-- XML: https://www.w3.org/TR/xml/#sec-prolog-dtd
-- TODO: support any doctype

local Doctype_mt = node_mts:register {
__tostring = function ()
return '<!DOCTYPE html>'
end,
}
classes.Doctype = {
---HTML5 doctype shortcut. It is a table rather than string for future extension.
---HTML5 doctype shortcut.
---@type Doctype
HTML = setmetatable({}, {
__tostring = function () return '<!DOCTYPE html>' end,
}),
HTML = setmetatable({}, Doctype_mt),
}


local Raw_mt
Raw_mt = { ---@type metatable
---@class Raw

local Raw_mt ---@type metatable
Raw_mt = node_mts:register {
__tostring = function (self)
return self[SYM_CONTENT]
end,
Expand All @@ -64,13 +83,13 @@ Raw_mt = { ---@type metatable
return setmt({[SYM_CONTENT] = left[SYM_CONTENT]..right[SYM_CONTENT]}, Raw_mt)
end,
__newindex = function ()
error('Raw object is not mutable', 2)
error('Raw object is immutable', 2)
end,
}

---Create a Raw object, which would not be encoded when converted to string.
---@param content any value to be converted to string by `tostring()`
---@return table
---@return Raw
function classes.Raw(content)
return setmt({[SYM_CONTENT] = tostring(content)}, Raw_mt)
end
Expand Down
Loading

0 comments on commit a84ecee

Please sign in to comment.