Difference between revisions of "Module:HtmlBuilder"

From Minetest Wiki
Jump to navigation Jump to search
m
m
Line 1: Line 1:
 
--[[
 
--[[
 
     A module for building complex HTML from Lua using a
 
     A module for building complex HTML from Lua using a
fluent interface.
+
    fluent interface.
  
Originally written on the English Wikipedia by
+
    Originally written on the English Wikipedia by
Toohool and Mr. Stradivarius.
+
    Toohool and Mr. Stradivarius.
  
Code released under the GPL v2+ as per:
+
    Code released under the GPL v2+ as per:
https://en.wikipedia.org/w/index.php?diff=next&oldid=581399786
+
    https://en.wikipedia.org/w/index.php?diff=next&oldid=581399786
https://en.wikipedia.org/w/index.php?diff=next&oldid=581403025
+
    https://en.wikipedia.org/w/index.php?diff=next&oldid=581403025
  
@license GNU GPL v2+
+
    @license GNU GPL v2+
@author Marius Hoch < hoo@online.de >
+
    @author Marius Hoch < hoo@online.de >
 
]]
 
]]
  
Line 25: Line 25:
  
 
local selfClosingTags = {
 
local selfClosingTags = {
area = true,
+
    area = true,
base = true,
+
    base = true,
br = true,
+
    br = true,
col = true,
+
    col = true,
command = true,
+
    command = true,
embed = true,
+
    embed = true,
hr = true,
+
    hr = true,
img = true,
+
    img = true,
input = true,
+
    input = true,
keygen = true,
+
    keygen = true,
link = true,
+
    link = true,
meta = true,
+
    meta = true,
param = true,
+
    param = true,
source = true,
+
    source = true,
track = true,
+
    track = true,
wbr = true,
+
    wbr = true,
 
}
 
}
  
 
local htmlencodeMap = {
 
local htmlencodeMap = {
['>'] = '&gt;',
+
    ['>'] = '&gt;',
['<'] = '&lt;',
+
    ['<'] = '&lt;',
['&'] = '&amp;',
+
    ['&'] = '&amp;',
['"'] = '&quot;',
+
    ['"'] = '&quot;',
 
}
 
}
  
Line 53: Line 53:
  
 
metatable.__tostring = function( t )
 
metatable.__tostring = function( t )
local ret = {}
+
    local ret = {}
t:_build( ret )
+
    t:_build( ret )
return table.concat( ret )
+
    return table.concat( ret )
 
end
 
end
  
Line 62: Line 62:
 
-- @param name
 
-- @param name
 
local function getAttr( t, name )
 
local function getAttr( t, name )
for i, attr in ipairs( t.attributes ) do
+
    for i, attr in ipairs( t.attributes ) do
if attr.name == name then
+
        if attr.name == name then
return attr, i
+
            return attr, i
end
+
        end
end
+
    end
 
end
 
end
  
Line 73: Line 73:
 
-- @param s
 
-- @param s
 
local function isValidAttributeName( s )
 
local function isValidAttributeName( s )
-- Good estimate: http://www.w3.org/TR/2000/REC-xml-20001006#NT-Name
+
    -- Good estimate: http://www.w3.org/TR/2000/REC-xml-20001006#NT-Name
return s:match( '^[a-zA-Z_:][a-zA-Z0-9_.:-]*$' )
+
    return s:match( '^[a-zA-Z_:][a-zA-Z0-9_.:-]*$' )
 
end
 
end
  
Line 81: Line 81:
 
-- @param s
 
-- @param s
 
local function isValidTag( s )
 
local function isValidTag( s )
return s:match( '^[a-zA-Z0-9]+$' )
+
    return s:match( '^[a-zA-Z0-9]+$' )
 
end
 
end
  
Line 88: Line 88:
 
-- @param s
 
-- @param s
 
local function htmlEncode( s )
 
local function htmlEncode( s )
-- The parentheses ensure that there is only one return value
+
    -- The parentheses ensure that there is only one return value
local tmp = string.gsub( s, '[<>&"]', htmlencodeMap );
+
    local tmp = string.gsub( s, '[<>&"]', htmlencodeMap );
-- Don't encode strip markers here (T110143)
+
    -- Don't encode strip markers here (T110143)
tmp = string.gsub( tmp, options.encodedUniqPrefixPat, options.uniqPrefixRepl )
+
    tmp = string.gsub( tmp, options.encodedUniqPrefixPat, options.uniqPrefixRepl )
tmp = string.gsub( tmp, options.encodedUniqSuffixPat, options.uniqSuffixRepl )
+
    tmp = string.gsub( tmp, options.encodedUniqSuffixPat, options.uniqSuffixRepl )
return tmp
+
    return tmp
 
end
 
end
  
 
local function cssEncode( s )
 
local function cssEncode( s )
-- mw.ustring is so slow that it's worth searching the whole string
+
    -- mw.ustring is so slow that it's worth searching the whole string
-- for non-ASCII characters to avoid it if possible
+
    -- for non-ASCII characters to avoid it if possible
return ( string.find( s, '[^%z\1-\127]' ) and mw.ustring or string )
+
    return ( string.find( s, '[^%z\1-\127]' ) and mw.ustring or string )
-- XXX: I'm not sure this character set is complete.
+
        -- XXX: I'm not sure this character set is complete.
-- bug #68011: allow delete character (\127)
+
        -- bug #68011: allow delete character (\127)
.gsub( s, '[^\32-\57\60-\127]', function ( m )
+
        .gsub( s, '[^\32-\57\60-\127]', function ( m )
return string.format( '\\%X ', mw.ustring.codepoint( m ) )
+
            return string.format( '\\%X ', mw.ustring.codepoint( m ) )
end )
+
        end )
 
end
 
end
  
Line 113: Line 113:
 
-- @param args
 
-- @param args
 
local function createBuilder( tagName, args )
 
local function createBuilder( tagName, args )
if tagName ~= nil and tagName ~= '' and not isValidTag( tagName ) then
+
    if tagName ~= nil and tagName ~= '' and not isValidTag( tagName ) then
error( string.format( "invalid tag name '%s'", tagName ), 3 )
+
        error( string.format( "invalid tag name '%s'", tagName ), 3 )
end
+
    end
  
args = args or {}
+
    args = args or {}
local builder = {}
+
    local builder = {}
setmetatable( builder, metatable )
+
    setmetatable( builder, metatable )
builder.nodes = {}
+
    builder.nodes = {}
builder.attributes = {}
+
    builder.attributes = {}
builder.styles = {}
+
    builder.styles = {}
  
if tagName ~= '' then
+
    if tagName ~= '' then
builder.tagName = tagName
+
        builder.tagName = tagName
end
+
    end
  
builder.parent = args.parent
+
    builder.parent = args.parent
builder.selfClosing = selfClosingTags[tagName] or args.selfClosing or false
+
    builder.selfClosing = selfClosingTags[tagName] or args.selfClosing or false
return builder
+
    return builder
 
end
 
end
  
Line 139: Line 139:
 
-- @param builder
 
-- @param builder
 
local function appendBuilder( t, builder )
 
local function appendBuilder( t, builder )
if t.selfClosing then
+
    if t.selfClosing then
error( "self-closing tags can't have child nodes", 3 )
+
        error( "self-closing tags can't have child nodes", 3 )
end
+
    end
  
if builder then
+
    if builder then
table.insert( t.nodes, builder )
+
        table.insert( t.nodes, builder )
end
+
    end
return t
+
    return t
 
end
 
end
  
 
methodtable._build = function( t, ret )
 
methodtable._build = function( t, ret )
if t.tagName then
+
    if t.tagName then
table.insert( ret, '<' .. t.tagName )
+
        table.insert( ret, '<' .. t.tagName )
for i, attr in ipairs( t.attributes ) do
+
        for i, attr in ipairs( t.attributes ) do
table.insert(
+
            table.insert(
ret,
+
                ret,
-- Note: Attribute names have already been validated
+
                -- Note: Attribute names have already been validated
' ' .. attr.name .. '="' .. htmlEncode( attr.val ) .. '"'
+
                ' ' .. attr.name .. '="' .. htmlEncode( attr.val ) .. '"'
)
+
            )
end
+
        end
if #t.styles > 0 then
+
        if #t.styles > 0 then
table.insert( ret, ' style="' )
+
            table.insert( ret, ' style="' )
local css = {}
+
            local css = {}
for i, prop in ipairs( t.styles ) do
+
            for i, prop in ipairs( t.styles ) do
if type( prop ) ~= 'table' then -- added with cssText()
+
                if type( prop ) ~= 'table' then -- added with cssText()
table.insert( css, htmlEncode( prop ) )
+
                    table.insert( css, htmlEncode( prop ) )
else -- added with css()
+
                else -- added with css()
table.insert(
+
                    table.insert(
css,
+
                        css,
htmlEncode( cssEncode( prop.name ) .. ':' .. cssEncode( prop.val ) )
+
                        htmlEncode( cssEncode( prop.name ) .. ':' .. cssEncode( prop.val ) )
)
+
                    )
end
+
                end
end
+
            end
table.insert( ret, table.concat( css, ';' ) )
+
            table.insert( ret, table.concat( css, ';' ) )
table.insert( ret, '"' )
+
            table.insert( ret, '"' )
end
+
        end
if t.selfClosing then
+
        if t.selfClosing then
table.insert( ret, ' />' )
+
            table.insert( ret, ' />' )
return
+
            return
end
+
        end
table.insert( ret, '>' )
+
        table.insert( ret, '>' )
end
+
    end
for i, node in ipairs( t.nodes ) do
+
    for i, node in ipairs( t.nodes ) do
if node then
+
        if node then
if type( node ) == 'table' then
+
            if type( node ) == 'table' then
node:_build( ret )
+
                node:_build( ret )
else
+
            else
table.insert( ret, tostring( node ) )
+
                table.insert( ret, tostring( node ) )
end
+
            end
end
+
        end
end
+
    end
if t.tagName then
+
    if t.tagName then
table.insert( ret, '</' .. t.tagName .. '>' )
+
        table.insert( ret, '</' .. t.tagName .. '>' )
end
+
    end
 
end
 
end
  
Line 199: Line 199:
 
-- @param builder
 
-- @param builder
 
methodtable.node = function( t, builder )
 
methodtable.node = function( t, builder )
return appendBuilder( t, builder )
+
    return appendBuilder( t, builder )
 
end
 
end
  
 
-- Appends some markup to the node. This will be treated as wikitext.
 
-- Appends some markup to the node. This will be treated as wikitext.
 
methodtable.wikitext = function( t, ... )
 
methodtable.wikitext = function( t, ... )
for k,v in ipairs{...} do
+
    for k,v in ipairs{...} do
checkTypeMulti( 'wikitext', k, v, { 'string', 'number' } )
+
        checkTypeMulti( 'wikitext', k, v, { 'string', 'number' } )
appendBuilder( t, v )
+
        appendBuilder( t, v )
end
+
    end
return t
+
    return t
 
end
 
end
  
 
-- Appends a newline character to the node.
 
-- Appends a newline character to the node.
 
methodtable.newline = function( t )
 
methodtable.newline = function( t )
return t:wikitext( '\n' )
+
    return t:wikitext( '\n' )
 
end
 
end
  
Line 222: Line 222:
 
-- @param args
 
-- @param args
 
methodtable.tag = function( t, tagName, args )
 
methodtable.tag = function( t, tagName, args )
checkType( 'tag', 1, tagName, 'string' )
+
    checkType( 'tag', 1, tagName, 'string' )
checkType( 'tag', 2, args, 'table', true )
+
    checkType( 'tag', 2, args, 'table', true )
args = args or {}
+
    args = args or {}
  
args.parent = t
+
    args.parent = t
local builder = createBuilder( tagName, args )
+
    local builder = createBuilder( tagName, args )
t:node( builder )
+
    t:node( builder )
return builder
+
    return builder
 
end
 
end
  
Line 236: Line 236:
 
-- @param name
 
-- @param name
 
methodtable.getAttr = function( t, name )
 
methodtable.getAttr = function( t, name )
checkType( 'getAttr', 1, name, 'string' )
+
    checkType( 'getAttr', 1, name, 'string' )
  
local attr = getAttr( t, name )
+
    local attr = getAttr( t, name )
return attr and attr.val
+
    return attr and attr.val
 
end
 
end
  
Line 247: Line 247:
 
-- @param val Value of the attribute. Nil causes the attribute to be unset
 
-- @param val Value of the attribute. Nil causes the attribute to be unset
 
methodtable.attr = function( t, name, val )
 
methodtable.attr = function( t, name, val )
if type( name ) == 'table' then
+
    if type( name ) == 'table' then
if val ~= nil then
+
        if val ~= nil then
error(
+
            error(
"bad argument #2 to 'attr' " ..
+
                "bad argument #2 to 'attr' " ..
'(if argument #1 is a table, argument #2 must be left empty)',
+
                '(if argument #1 is a table, argument #2 must be left empty)',
2
+
                2
)
+
            )
end
+
        end
  
local callForTable = function()
+
        local callForTable = function()
for attrName, attrValue in pairs( name ) do
+
            for attrName, attrValue in pairs( name ) do
t:attr( attrName, attrValue )
+
                t:attr( attrName, attrValue )
end
+
            end
end
+
        end
  
if not pcall( callForTable ) then
+
        if not pcall( callForTable ) then
error(
+
            error(
"bad argument #1 to 'attr' " ..
+
                "bad argument #1 to 'attr' " ..
'(table keys must be strings, and values must be strings or numbers)',
+
                '(table keys must be strings, and values must be strings or numbers)',
2
+
                2
)
+
            )
end
+
        end
  
return t
+
        return t
end
+
    end
  
checkType( 'attr', 1, name, 'string' )
+
    checkType( 'attr', 1, name, 'string' )
checkTypeMulti( 'attr', 2, val, { 'string', 'number', 'nil' } )
+
    checkTypeMulti( 'attr', 2, val, { 'string', 'number', 'nil' } )
  
-- if caller sets the style attribute explicitly, then replace all styles
+
    -- if caller sets the style attribute explicitly, then replace all styles
-- previously added with css() and cssText()
+
    -- previously added with css() and cssText()
if name == 'style' then
+
    if name == 'style' then
t.styles = { val }
+
        t.styles = { val }
return t
+
        return t
end
+
    end
  
if not isValidAttributeName( name ) then
+
    if not isValidAttributeName( name ) then
error( string.format(
+
        error( string.format(
"bad argument #1 to 'attr' (invalid attribute name '%s')",
+
            "bad argument #1 to 'attr' (invalid attribute name '%s')",
name
+
            name
), 2 )
+
        ), 2 )
end
+
    end
  
local attr, i = getAttr( t, name )
+
    local attr, i = getAttr( t, name )
if attr then
+
    if attr then
if val ~= nil then
+
        if val ~= nil then
attr.val = val
+
            attr.val = val
else
+
        else
table.remove( t.attributes, i )
+
            table.remove( t.attributes, i )
end
+
        end
elseif val ~= nil then
+
    elseif val ~= nil then
table.insert( t.attributes, { name = name, val = val } )
+
        table.insert( t.attributes, { name = name, val = val } )
end
+
    end
  
return t
+
    return t
 
end
 
end
  
Line 309: Line 309:
 
-- @param class
 
-- @param class
 
methodtable.addClass = function( t, class )
 
methodtable.addClass = function( t, class )
checkTypeMulti( 'addClass', 1, class, { 'string', 'number', 'nil' } )
+
    checkTypeMulti( 'addClass', 1, class, { 'string', 'number', 'nil' } )
  
if class ~= nil then
+
    if class ~= nil then
local attr = getAttr( t, 'class' )
+
        local attr = getAttr( t, 'class' )
if attr then
+
        if attr then
attr.val = attr.val .. ' ' .. class
+
            attr.val = attr.val .. ' ' .. class
else
+
        else
t:attr( 'class', class )
+
            t:attr( 'class', class )
end
+
        end
end
+
    end
return t
+
    return t
 
end
 
end
  
Line 327: Line 327:
 
-- @param val The value to set. Nil causes it to be unset
 
-- @param val The value to set. Nil causes it to be unset
 
methodtable.css = function( t, name, val )
 
methodtable.css = function( t, name, val )
if type( name ) == 'table' then
+
    if type( name ) == 'table' then
if val ~= nil then
+
        if val ~= nil then
error(
+
            error(
"bad argument #2 to 'css' " ..
+
                "bad argument #2 to 'css' " ..
'(if argument #1 is a table, argument #2 must be left empty)',
+
                '(if argument #1 is a table, argument #2 must be left empty)',
2
+
                2
)
+
            )
end
+
        end
  
local callForTable = function()
+
        local callForTable = function()
for attrName, attrValue in pairs( name ) do
+
            for attrName, attrValue in pairs( name ) do
t:css( attrName, attrValue )
+
                t:css( attrName, attrValue )
end
+
            end
end
+
        end
  
if not pcall( callForTable ) then
+
        if not pcall( callForTable ) then
error(
+
            error(
"bad argument #1 to 'css' " ..
+
                "bad argument #1 to 'css' " ..
'(table keys and values must be strings or numbers)',
+
                '(table keys and values must be strings or numbers)',
2
+
                2
)
+
            )
end
+
        end
  
return t
+
        return t
end
+
    end
  
checkTypeMulti( 'css', 1, name, { 'string', 'number' } )
+
    checkTypeMulti( 'css', 1, name, { 'string', 'number' } )
checkTypeMulti( 'css', 2, val, { 'string', 'number', 'nil' } )
+
    checkTypeMulti( 'css', 2, val, { 'string', 'number', 'nil' } )
  
for i, prop in ipairs( t.styles ) do
+
    for i, prop in ipairs( t.styles ) do
if prop.name == name then
+
        if prop.name == name then
if val ~= nil then
+
            if val ~= nil then
prop.val = val
+
                prop.val = val
else
+
            else
table.remove( t.styles, i )
+
                table.remove( t.styles, i )
end
+
            end
return t
+
            return t
end
+
        end
end
+
    end
  
if val ~= nil then
+
    if val ~= nil then
table.insert( t.styles, { name = name, val = val } )
+
        table.insert( t.styles, { name = name, val = val } )
end
+
    end
  
return t
+
    return t
 
end
 
end
  
Line 379: Line 379:
 
-- @param css
 
-- @param css
 
methodtable.cssText = function( t, css )
 
methodtable.cssText = function( t, css )
checkTypeMulti( 'cssText', 1, css, { 'string', 'number', 'nil' } )
+
    checkTypeMulti( 'cssText', 1, css, { 'string', 'number', 'nil' } )
table.insert( t.styles, css )
+
    table.insert( t.styles, css )
return t
+
    return t
 
end
 
end
  
Line 388: Line 388:
 
-- several child nodes to be chained together into a single statement.
 
-- several child nodes to be chained together into a single statement.
 
methodtable.done = function( t )
 
methodtable.done = function( t )
return t.parent or t
+
    return t.parent or t
 
end
 
end
  
Line 394: Line 394:
 
-- returns it.
 
-- returns it.
 
methodtable.allDone = function( t )
 
methodtable.allDone = function( t )
while t.parent do
+
    while t.parent do
t = t.parent
+
        t = t.parent
end
+
    end
return t
+
    return t
 
end
 
end
  
Line 405: Line 405:
 
-- @param args
 
-- @param args
 
function HtmlBuilder.create( tagName, args )
 
function HtmlBuilder.create( tagName, args )
checkType( 'HtmlBuilder.create', 1, tagName, 'string', true )
+
    checkType( 'HtmlBuilder.create', 1, tagName, 'string', true )
checkType( 'HtmlBuilder.create', 2, args, 'table', true )
+
    checkType( 'HtmlBuilder.create', 2, args, 'table', true )
return createBuilder( tagName, args )
+
    return createBuilder( tagName, args )
 
end
 
end
  
 
function HtmlBuilder.setupInterface( opts )
 
function HtmlBuilder.setupInterface( opts )
-- Boilerplate
+
    -- Boilerplate
HtmlBuilder.setupInterface = nil
+
    HtmlBuilder.setupInterface = nil
options = opts
+
    options = opts
  
-- Prepare patterns for unencoding strip markers
+
    -- Prepare patterns for unencoding strip markers
options.encodedUniqPrefixPat = string.gsub( options.uniqPrefix, '[<>&"]', htmlencodeMap );
+
    options.encodedUniqPrefixPat = string.gsub( options.uniqPrefix, '[<>&"]', htmlencodeMap );
options.encodedUniqPrefixPat = string.gsub( options.encodedUniqPrefixPat, '%p', '%%%0' );
+
    options.encodedUniqPrefixPat = string.gsub( options.encodedUniqPrefixPat, '%p', '%%%0' );
options.uniqPrefixRepl = string.gsub( options.uniqPrefix, '%%', '%%%0' );
+
    options.uniqPrefixRepl = string.gsub( options.uniqPrefix, '%%', '%%%0' );
options.encodedUniqSuffixPat = string.gsub( options.uniqSuffix, '[<>&"]', htmlencodeMap );
+
    options.encodedUniqSuffixPat = string.gsub( options.uniqSuffix, '[<>&"]', htmlencodeMap );
options.encodedUniqSuffixPat = string.gsub( options.encodedUniqSuffixPat, '%p', '%%%0' );
+
    options.encodedUniqSuffixPat = string.gsub( options.encodedUniqSuffixPat, '%p', '%%%0' );
options.uniqSuffixRepl = string.gsub( options.uniqSuffix, '%%', '%%%0' );
+
    options.uniqSuffixRepl = string.gsub( options.uniqSuffix, '%%', '%%%0' );
 
end
 
end
  
 
HtmlBuilder.setupInterface({
 
HtmlBuilder.setupInterface({
uniqPrefix = '\x7fUNIQ-',
+
    uniqPrefix = '\x7fUNIQ-',
uniqSuffix = '-QINU\x7f'
+
    uniqSuffix = '-QINU\x7f'
 
})
 
})
  
 
return HtmlBuilder
 
return HtmlBuilder

Revision as of 06:03, 9 April 2017

--[[

   A module for building complex HTML from Lua using a
   fluent interface.
   Originally written on the English Wikipedia by
   Toohool and Mr. Stradivarius.
   Code released under the GPL v2+ as per:
   https://en.wikipedia.org/w/index.php?diff=next&oldid=581399786
   https://en.wikipedia.org/w/index.php?diff=next&oldid=581403025
   @license GNU GPL v2+
   @author Marius Hoch < hoo@online.de >

]]

local HtmlBuilder = {} local options

local util = require('Module:LibraryUtil') local checkType = util.checkType local checkTypeMulti = util.checkTypeMulti

local metatable = {} local methodtable = {}

local selfClosingTags = {

   area = true,
   base = true,
   br = true,
   col = true,
   command = true,
   embed = true,
   hr = true,
   img = true,
   input = true,
   keygen = true,
   link = true,
   meta = true,
   param = true,
   source = true,
   track = true,
   wbr = true,

}

local htmlencodeMap = {

   ['>'] = '>',
   ['<'] = '<',
   ['&'] = '&',
   ['"'] = '"',

}

metatable.__index = methodtable

metatable.__tostring = function( t )

   local ret = {}
   t:_build( ret )
   return table.concat( ret )

end

-- Get an attribute table (name, value) and its index -- -- @param name local function getAttr( t, name )

   for i, attr in ipairs( t.attributes ) do
       if attr.name == name then
           return attr, i
       end
   end

end

-- Is this a valid attribute name? -- -- @param s local function isValidAttributeName( s )

   -- Good estimate: http://www.w3.org/TR/2000/REC-xml-20001006#NT-Name
   return s:match( '^[a-zA-Z_:][a-zA-Z0-9_.:-]*$' )

end

-- Is this a valid tag name? -- -- @param s local function isValidTag( s )

   return s:match( '^[a-zA-Z0-9]+$' )

end

-- Escape a value, for use in HTML -- -- @param s local function htmlEncode( s )

   -- The parentheses ensure that there is only one return value
   local tmp = string.gsub( s, '[<>&"]', htmlencodeMap );
   -- Don't encode strip markers here (T110143)
   tmp = string.gsub( tmp, options.encodedUniqPrefixPat, options.uniqPrefixRepl )
   tmp = string.gsub( tmp, options.encodedUniqSuffixPat, options.uniqSuffixRepl )
   return tmp

end

local function cssEncode( s )

   -- mw.ustring is so slow that it's worth searching the whole string
   -- for non-ASCII characters to avoid it if possible
   return ( string.find( s, '[^%z\1-\127]' ) and mw.ustring or string )
       -- XXX: I'm not sure this character set is complete.
       -- bug #68011: allow delete character (\127)
       .gsub( s, '[^\32-\57\60-\127]', function ( m )
           return string.format( '\\%X ', mw.ustring.codepoint( m ) )
       end )

end

-- Create a builder object. This is a separate function so that we can show the -- correct error levels in both HtmlBuilder.create and metatable.tag. -- -- @param tagName -- @param args local function createBuilder( tagName, args )

   if tagName ~= nil and tagName ~=  and not isValidTag( tagName ) then
       error( string.format( "invalid tag name '%s'", tagName ), 3 )
   end
   args = args or {}
   local builder = {}
   setmetatable( builder, metatable )
   builder.nodes = {}
   builder.attributes = {}
   builder.styles = {}
   if tagName ~=  then
       builder.tagName = tagName
   end
   builder.parent = args.parent
   builder.selfClosing = selfClosingTags[tagName] or args.selfClosing or false
   return builder

end

-- Append a builder to the current node. This is separate from methodtable.node -- so that we can show the correct error level in both methodtable.node and -- methodtable.wikitext. -- -- @param builder local function appendBuilder( t, builder )

   if t.selfClosing then
       error( "self-closing tags can't have child nodes", 3 )
   end
   if builder then
       table.insert( t.nodes, builder )
   end
   return t

end

methodtable._build = function( t, ret )

   if t.tagName then
       table.insert( ret, '<' .. t.tagName )
       for i, attr in ipairs( t.attributes ) do
           table.insert(
               ret,
               -- Note: Attribute names have already been validated
               ' ' .. attr.name .. '="' .. htmlEncode( attr.val ) .. '"'
           )
       end
       if #t.styles > 0 then
           table.insert( ret, ' style="' )
           local css = {}
           for i, prop in ipairs( t.styles ) do
               if type( prop ) ~= 'table' then -- added with cssText()
                   table.insert( css, htmlEncode( prop ) )
               else -- added with css()
                   table.insert(
                       css,
                       htmlEncode( cssEncode( prop.name ) .. ':' .. cssEncode( prop.val ) )
                   )
               end
           end
           table.insert( ret, table.concat( css, ';' ) )
           table.insert( ret, '"' )
       end
       if t.selfClosing then
           table.insert( ret, ' />' )
           return
       end
       table.insert( ret, '>' )
   end
   for i, node in ipairs( t.nodes ) do
       if node then
           if type( node ) == 'table' then
               node:_build( ret )
           else
               table.insert( ret, tostring( node ) )
           end
       end
   end
   if t.tagName then
       table.insert( ret, '</' .. t.tagName .. '>' )
   end

end

-- Append a builder to the current node -- -- @param builder methodtable.node = function( t, builder )

   return appendBuilder( t, builder )

end

-- Appends some markup to the node. This will be treated as wikitext. methodtable.wikitext = function( t, ... )

   for k,v in ipairs{...} do
       checkTypeMulti( 'wikitext', k, v, { 'string', 'number' } )
       appendBuilder( t, v )
   end
   return t

end

-- Appends a newline character to the node. methodtable.newline = function( t )

   return t:wikitext( '\n' )

end

-- Appends a new child node to the builder, and returns an HtmlBuilder instance -- representing that new node. -- -- @param tagName -- @param args methodtable.tag = function( t, tagName, args )

   checkType( 'tag', 1, tagName, 'string' )
   checkType( 'tag', 2, args, 'table', true )
   args = args or {}
   args.parent = t
   local builder = createBuilder( tagName, args )
   t:node( builder )
   return builder

end

-- Get the value of an html attribute -- -- @param name methodtable.getAttr = function( t, name )

   checkType( 'getAttr', 1, name, 'string' )
   local attr = getAttr( t, name )
   return attr and attr.val

end

-- Set an HTML attribute on the node. -- -- @param name Attribute to set, alternative table of name-value pairs -- @param val Value of the attribute. Nil causes the attribute to be unset methodtable.attr = function( t, name, val )

   if type( name ) == 'table' then
       if val ~= nil then
           error(
               "bad argument #2 to 'attr' " ..
               '(if argument #1 is a table, argument #2 must be left empty)',
               2
           )
       end
       local callForTable = function()
           for attrName, attrValue in pairs( name ) do
               t:attr( attrName, attrValue )
           end
       end
       if not pcall( callForTable ) then
           error(
               "bad argument #1 to 'attr' " ..
               '(table keys must be strings, and values must be strings or numbers)',
               2
           )
       end
       return t
   end
   checkType( 'attr', 1, name, 'string' )
   checkTypeMulti( 'attr', 2, val, { 'string', 'number', 'nil' } )
   -- if caller sets the style attribute explicitly, then replace all styles
   -- previously added with css() and cssText()
   if name == 'style' then
       t.styles = { val }
       return t
   end
   if not isValidAttributeName( name ) then
       error( string.format(
           "bad argument #1 to 'attr' (invalid attribute name '%s')",
           name
       ), 2 )
   end
   local attr, i = getAttr( t, name )
   if attr then
       if val ~= nil then
           attr.val = val
       else
           table.remove( t.attributes, i )
       end
   elseif val ~= nil then
       table.insert( t.attributes, { name = name, val = val } )
   end
   return t

end

-- Adds a class name to the node's class attribute. Spaces will be -- automatically added to delimit each added class name. -- -- @param class methodtable.addClass = function( t, class )

   checkTypeMulti( 'addClass', 1, class, { 'string', 'number', 'nil' } )
   if class ~= nil then
       local attr = getAttr( t, 'class' )
       if attr then
           attr.val = attr.val .. ' ' .. class
       else
           t:attr( 'class', class )
       end
   end
   return t

end

-- Set a CSS property to be added to the node's style attribute. -- -- @param name CSS attribute to set, alternative table of name-value pairs -- @param val The value to set. Nil causes it to be unset methodtable.css = function( t, name, val )

   if type( name ) == 'table' then
       if val ~= nil then
           error(
               "bad argument #2 to 'css' " ..
               '(if argument #1 is a table, argument #2 must be left empty)',
               2
           )
       end
       local callForTable = function()
           for attrName, attrValue in pairs( name ) do
               t:css( attrName, attrValue )
           end
       end
       if not pcall( callForTable ) then
           error(
               "bad argument #1 to 'css' " ..
               '(table keys and values must be strings or numbers)',
               2
           )
       end
       return t
   end
   checkTypeMulti( 'css', 1, name, { 'string', 'number' } )
   checkTypeMulti( 'css', 2, val, { 'string', 'number', 'nil' } )
   for i, prop in ipairs( t.styles ) do
       if prop.name == name then
           if val ~= nil then
               prop.val = val
           else
               table.remove( t.styles, i )
           end
           return t
       end
   end
   if val ~= nil then
       table.insert( t.styles, { name = name, val = val } )
   end
   return t

end

-- Add some raw CSS to the node's style attribute. This is typically used -- when a template allows some CSS to be passed in as a parameter -- -- @param css methodtable.cssText = function( t, css )

   checkTypeMulti( 'cssText', 1, css, { 'string', 'number', 'nil' } )
   table.insert( t.styles, css )
   return t

end

-- Returns the parent node under which the current node was created. Like -- jQuery.end, this is a convenience function to allow the construction of -- several child nodes to be chained together into a single statement. methodtable.done = function( t )

   return t.parent or t

end

-- Like .done(), but traverses all the way to the root node of the tree and -- returns it. methodtable.allDone = function( t )

   while t.parent do
       t = t.parent
   end
   return t

end

-- Create a new instance -- -- @param tagName -- @param args function HtmlBuilder.create( tagName, args )

   checkType( 'HtmlBuilder.create', 1, tagName, 'string', true )
   checkType( 'HtmlBuilder.create', 2, args, 'table', true )
   return createBuilder( tagName, args )

end

function HtmlBuilder.setupInterface( opts )

   -- Boilerplate
   HtmlBuilder.setupInterface = nil
   options = opts
   -- Prepare patterns for unencoding strip markers
   options.encodedUniqPrefixPat = string.gsub( options.uniqPrefix, '[<>&"]', htmlencodeMap );
   options.encodedUniqPrefixPat = string.gsub( options.encodedUniqPrefixPat, '%p', '%%%0' );
   options.uniqPrefixRepl = string.gsub( options.uniqPrefix, '%%', '%%%0' );
   options.encodedUniqSuffixPat = string.gsub( options.uniqSuffix, '[<>&"]', htmlencodeMap );
   options.encodedUniqSuffixPat = string.gsub( options.encodedUniqSuffixPat, '%p', '%%%0' );
   options.uniqSuffixRepl = string.gsub( options.uniqSuffix, '%%', '%%%0' );

end

HtmlBuilder.setupInterface({

   uniqPrefix = '\x7fUNIQ-',
   uniqSuffix = '-QINU\x7f'

})

return HtmlBuilder