Module:Sandbox/Jackmcbarn/mw.html.lua

--[[	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 function checkTypeMulti( name, argIdx, arg, expectTypes ) local argType = type( arg ) for _, expectType in ipairs( expectTypes ) do		if argType == expectType then return end end local n = #expectTypes local typeList if n > 1 then typeList = table.concat( expectTypes, ', ', 1, n - 1 ) .. ' or ' .. expectTypes[n] else typeList = expectTypes[1] end local msg = string.format( "bad argument #%d to '%s' (%s expected, got %s)",		argIdx,		name,		typeList,		type( arg )	) error( msg, 3 ) end

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 = { ['>'] = '&gt;', ['<'] = '&lt;', ['&'] = '&amp;', ['"'] = '&quot;', }

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 ( s:find( '[^%z\1-\127]' ) and mw.ustring or string ) .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, ... ) local vals = {...} for i = 1, #vals do		checkTypeMulti( 'wikitext', i, vals[i], { 'string', 'number' } ) appendBuilder( t, vals[i] ) end return t end

-- Appends a newline character to the node. methodtable.newline = function( t ) t:wikitext( '\n' ) return t 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 ) if attr then return attr.val end return nil 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 return t	end

local attr = getAttr( t, 'class' ) if attr then attr.val = attr.val .. ' ' .. class else t:attr( 'class', class ) 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' } ) if css ~= nil then table.insert( t.styles, css ) end 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