mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/Scribunto
synced 2024-11-27 09:40:12 +00:00
ac62e34952
A module for building complex HTML from Lua using a fluent interface. The module is originally from enwiki, but the authors allowed us to reuse it under GPLv2+ (as stated in the file). The module will be loaded per default and comes with unit tests. As discussed on wikitech-l: http://lists.wikimedia.org/pipermail/wikitech-l/2013-December/073320.html Change-Id: I7c8d4378091c13d5ace0dd1fcbb4e27163e8c896
373 lines
8.4 KiB
Lua
373 lines
8.4 KiB
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 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)
|
|
--
|
|
-- @param name
|
|
local function getAttr( t, name )
|
|
for i, attr in ipairs( t.attributes ) do
|
|
if attr.name == name then
|
|
return attr
|
|
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-Z]+$' )
|
|
end
|
|
|
|
-- Escape a value, for use in HTML
|
|
--
|
|
-- @param s
|
|
local function htmlEncode( s )
|
|
return string.gsub( s, '[<>&"]', htmlencodeMap )
|
|
end
|
|
|
|
local function cssEncode( s )
|
|
-- XXX: I'm not sure this character set is complete.
|
|
return mw.ustring.gsub( s, '[;:%z\1-\31\127-\244\143\191\191]', function ( m )
|
|
return string.format( '\\%X ', mw.ustring.codepoint( m ) )
|
|
end )
|
|
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="' )
|
|
for i, prop in ipairs( t.styles ) do
|
|
if type( prop ) == 'string' then -- added with cssText()
|
|
table.insert( ret, htmlEncode( prop ) .. ';' )
|
|
else -- added with css()
|
|
table.insert(
|
|
ret,
|
|
htmlEncode( cssEncode( prop.name ) .. ':' .. cssEncode( prop.val ) ) .. ';'
|
|
)
|
|
end
|
|
end
|
|
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 )
|
|
if t.selfClosing then
|
|
error( "Self-closing tags can't have child nodes" )
|
|
end
|
|
|
|
if builder then
|
|
table.insert( t.nodes, builder )
|
|
end
|
|
return t
|
|
end
|
|
|
|
-- Appends some markup to the node. This will be treated as wikitext.
|
|
methodtable.wikitext = function( t, ... )
|
|
local vals = {...}
|
|
for i = 1, #vals do
|
|
if type( vals[i] ) ~= 'string' and type( vals[i] ) ~= 'number' then
|
|
error( 'Invalid wikitext given: Must be either a string or a number' )
|
|
end
|
|
|
|
t:node( 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 )
|
|
args = args or {}
|
|
args.parent = t
|
|
local builder = HtmlBuilder.create( tagName, args )
|
|
t:node( builder )
|
|
return builder
|
|
end
|
|
|
|
-- Get the value of an html attribute
|
|
--
|
|
-- @param name
|
|
methodtable.getAttr = function( t, name )
|
|
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
|
|
methodtable.attr = function( t, name, val )
|
|
if type( name ) == 'table' then
|
|
if val ~= nil then
|
|
error( 'If a key->value table is given as first parameter, value must be left empty' )
|
|
end
|
|
|
|
local callForTable = function()
|
|
for attrName, attrValue in pairs( name ) do
|
|
t:attr( attrName, attrValue )
|
|
end
|
|
end
|
|
|
|
if not pcall( callForTable ) then
|
|
error( 'Invalid table given: Must be name (string) value (string|number) pairs' )
|
|
end
|
|
|
|
return t
|
|
end
|
|
|
|
if type( name ) ~= 'string' and type( name ) ~= 'number' then
|
|
error( 'Invalid name given: The name must be either a string or a number' )
|
|
end
|
|
if type( val ) ~= 'string' and type( val ) ~= 'number' then
|
|
error( 'Invalid value given: The value must be either a string or a number' )
|
|
end
|
|
|
|
-- 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( "Invalid attribute name: " .. name )
|
|
end
|
|
|
|
local attr = getAttr( t, name )
|
|
if attr then
|
|
attr.val = val
|
|
else
|
|
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 )
|
|
if type( class ) ~= 'string' and type( class ) ~= 'number' then
|
|
error( 'Invalid class given: The name must be either a string or a number' )
|
|
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
|
|
methodtable.css = function( t, name, val )
|
|
if type( name ) == 'table' then
|
|
if val ~= nil then
|
|
error( 'If a key->value table is given as first parameter, value must be left empty' )
|
|
end
|
|
|
|
local callForTable = function()
|
|
for attrName, attrValue in pairs( name ) do
|
|
t:css( attrName, attrValue )
|
|
end
|
|
end
|
|
|
|
if not pcall( callForTable ) then
|
|
error( 'Invalid table given: Must be name (string|number) value (string|number) pairs' )
|
|
end
|
|
|
|
return t
|
|
end
|
|
|
|
if type( name ) ~= 'string' and type( name ) ~= 'number' then
|
|
error( 'Invalid CSS given: The name must be either a string or a number' )
|
|
end
|
|
if type( val ) ~= 'string' and type( val ) ~= 'number' then
|
|
error( 'Invalid CSS given: The value must be either a string or a number' )
|
|
end
|
|
|
|
for i, prop in ipairs( t.styles ) do
|
|
if prop.name == name then
|
|
prop.val = val
|
|
return t
|
|
end
|
|
end
|
|
|
|
table.insert( t.styles, { name = name, val = val } )
|
|
|
|
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 )
|
|
if type( css ) ~= 'string' and type( css ) ~= 'number' then
|
|
error( 'Invalid CSS given: Must be either a string or a number' )
|
|
end
|
|
|
|
if css 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 )
|
|
if tagName ~= '' and not isValidTag( tagName ) then
|
|
error( "Invalid tag name: " .. tagName )
|
|
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
|
|
|
|
mw_interface = nil
|
|
|
|
-- Register this library in the "mw" global
|
|
mw = mw or {}
|
|
mw.html = HtmlBuilder
|
|
|
|
package.loaded['mw.html'] = HtmlBuilder
|
|
|
|
return HtmlBuilder
|