--[[ 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 util = require '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 return ( string.gsub( s, '[<>&"]', htmlencodeMap ) ) end local function cssEncode( s ) -- XXX: I'm not sure this character set is complete. -- bug #68011: allow delete character (\127) return mw.ustring.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, '' ) 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( 'mw.html.create', 1, tagName, 'string', true ) checkType( 'mw.html.create', 2, args, 'table', true ) return createBuilder( tagName, args ) end mw_interface = nil -- Register this library in the "mw" global mw = mw or {} mw.html = HtmlBuilder package.loaded['mw.html'] = HtmlBuilder return HtmlBuilder