diff --git a/acandy.lua b/acandy.lua index 3a16a36..bf10a7e 100644 --- a/acandy.lua +++ b/acandy.lua @@ -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 @@ -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 ')), '') + end) + it('has properties', function () local li = a.li 'item 1' local list = a.ol {id = "my-id", li} @@ -249,6 +257,11 @@ end) describe('String escaping', function () local str = '& \194\160 " \' < > &   " ' < >' + local function func_returns_string() + return str + end + -- issue: #11 + local table_with_tostring = setmetatable({}, {__tostring = func_returns_string}) it('replaces `& NBSP " < >` in attribute values with named references', function () local answer = '
' assert.are.equal(tostring(a.div[str]), answer) assert.are.equal(tostring(a.div[{class = str}]), answer) + assert.are.equal(tostring(a.div[{class = table_with_tostring}]), answer) assert.are.equal(tostring(a.div {class = str}), answer) + assert.are.equal(tostring(a.div {class = table_with_tostring}), answer) end) it('replaces `& NBSP < >` in texts other than attribute value with named references', function () @@ -266,6 +281,20 @@ describe('String escaping', function () ..'&amp; &nbsp; &quot; &apos; &lt; &gt;' ..'' assert.are.equal(tostring(a.div(str)), answer) + assert.are.equal(tostring(a.div(func_returns_string)), answer) + assert.are.equal(tostring(a.div(table_with_tostring)), answer) + end) + + it('does not escape string from acandy nodes', function () + local node1 = a.div + assert.are.equal(tostring(a.div(node1)), '
'..tostring(node1)..'
') + local node2 = acandy.Doctype.HTML + assert.are.equal( + tostring(Fragment {node2, a.html {a.head, a.body}}), + tostring(node2)..'' + ) + local node3 = acandy.Comment(str) + assert.are.equal(tostring(a.div(node3)), '
'..tostring(node3)..'
') end) it("does not replace characters in an object's properties", function () @@ -276,18 +305,94 @@ describe('String escaping', function () assert.are.equal(elem.class, str) assert.are.equal(elem[1], str) end) + + it("does not encode any character in raw text elements", function () + assert.are.equal(tostring(a.style(str)), '') + assert.are.equal(tostring(a.script(str)), '') + end) end) -describe('Raw text element', function () +describe('`acandy.Comment`', function () + -- spec: https://html.spec.whatwg.org/#comments + + local Comment = acandy.Comment + it('arg is content', function () + local comment = Comment('<>&"') + assert.are.equal(tostring(comment), '') + assert.are.equal(tostring(a.div(comment)), '
') + end) + + it('raises an error when content is invalid', function () + -- the text must not start with the string ">" or "->", + -- nor contain the strings "", or "--!>", + -- nor end with the string "', '>text', '->', '->text', + '', 'text-->', '-->text', 'text-->text', + '--!>', 'text--!>', '--!>text', 'text--!>text', + '', 'text>text', 'text->', 'text->text', + ' and . + '') + end + end) +end) + +describe('`acandy.Doctype`', function () + -- spec: https://html.spec.whatwg.org/#the-doctype + + local Doctype = acandy.Doctype + it('has HTML5 doctype', function () + assert.are.equal(tostring(Doctype.HTML), '') + assert.are.equal(tostring(Fragment {Doctype.HTML, a.html}), '') + end) +end) + +describe('`acandy.Raw`', function () local str = '& \194\160 " \' < > &   " ' < >' + it('is not escaped as a child node', function () + local raw = acandy.Raw(str) + assert.are.equal(tostring(raw), str) + assert.are.equal(tostring(a.div(raw)), '
'..str..'
') + assert.are.equal(tostring(a.div {raw}), '
'..str..'
') + end) - it("does not encode any character in text child", function () - assert.are.equal(tostring(a.style(str)), '') - assert.are.equal(tostring(a.script(str)), '') + it('can concat with another `Raw`, returns `Raw`', function () + local str1, str2 = '1'..str, '2'..str + local raw1 = acandy.Raw(str1) + local raw2 = acandy.Raw(str2) + assert.are.equal(getmetatable(raw1), getmetatable(raw2), getmetatable(raw1..raw2)) + assert.are.equal(tostring(raw1..raw2), str1..str2) + assert.are.equal(tostring(a.div(raw1..raw2)), '
'..str1..str2..'
') + assert.are.equal(tostring(raw1..raw2..raw1), str1..str2..str1) + assert.are.equal(tostring(a.div(raw1..raw2..raw1)), '
'..str1..str2..str1..'
') end) - it("does not check end tag", function () - assert.are.equal(tostring(a.style('')), '') - assert.are.equal(tostring(a.script('')), '') + it('cannot concat with a string or number', function () + local raw = acandy.Raw(str) + assert.has.error(function () + local never = raw..str + end) + assert.has.error(function () + local never = str..raw + end) + assert.has.error(function () + local never = raw..1 + end) + assert.has.error(function () + local never = (1)..raw + end) end) end) diff --git a/utils.lua b/utils.lua index 0c83015..78e5d0e 100644 --- a/utils.lua +++ b/utils.lua @@ -203,10 +203,10 @@ function utils.parse_shorthand_attrs(str) local id = nil str = s_gsub(str, '#([^%s#]*)', function (s) if s == '' then - error('empty id found in '..string.format('%q', str), 4) + error('empty id found in '..('%q'):format(str), 4) end if id then - error('two or more ids found in '..string.format('%q', str), 4) + error('two or more ids found in '..('%q'):format(str), 4) end id = s return ''