Hopp til innhold

Modul:Smartbox

Fra Wikipedia, den frie encyklopedi

-- module for generating smart infoboxes
-- © John Erling Blad, Creative Commons by Attribution 3.0

-- don't pollute with globals
require('strict')

--- Library to parse paths and and get values from entities
local wikibase = nil

--- @fixme
local classes = {}

--- Extensions to override the default behavior
local extensions = {}

--- The local language
local lang = mw.getContentLanguage()

--- Warning messages
local warnings = mw.loadData( 'Module:Smartbox/warnings' )

--- Formatter messages
local messages = mw.loadData( 'Module:Smartbox/messages' )

--- Preformatted units
local units = mw.loadData( 'Module:Units' )

--- Parsers for arguments
local parsers = require( 'Module:Smartbox/parsers' )

--- Initializer for parsers
-- This list must contain all types
local types = { 'unknown', 'number', 'string', 'boolean', 'date', 'url',
	'wiki-page-name', 'wiki-file-name', 'wiki-user-name', 'wiki-template-name',
	'content', 'line', 'unbalanced-wikitext' }
for k,_ in pairs( parsers) do
	if not types[k] then
		types[k] = {}
	end
end
--for k,v in pairs( parserTypes) do
--	parsers[v] = {}
--end

local function ucFirst( str )
	return mw.ustring.upper( mw.ustring.sub( str, 1, 1 ) ) .. mw.ustring.sub( str, 2 )
end

--- Escape strings to make them safe to use as identifiers and class names
-- Because strings are non-mutable this will create a copy, and not change the source
-- @param str to be escaped
-- @result string escaped
local function escape( str )
	if str then
		str = string.gsub( str, "[%s]", "_" )
		str = string.gsub( str,
			"([^%w%-%_])",
			function( char )
				return string.format ( "$%02X", string.byte( char ) )
			end
		)
	end
	return str	
end

--- Generate a specific warning message
-- This will build a proper warning message for the column (can also be 'row')
-- name, and type. Can be chained.
local function failed( col, name, type )
	local escapedCol = escape( col )
	local escapedType = escape( type )
	local html = mw.html.create( 'span' ):addClass( 'sb-warning' )
	local msg = mw.message.newFallbackSequence( 'smartbox-' .. escapedCol .. '-unknown-' .. type, 'smartbox-' .. escapedCol .. '-unknown' )
	if warnings['smartbox-' .. escapedCol .. '-unknown'] and not msg:exists() then
		msg = mw.message.newRawMessage( warnings['smartbox-' .. escapedCol .. '-unknown'] )
	end
	html:wikitext( msg:params( name, type ):plain() )
	return html
end

--- Merge two tables
-- This merges to tables, typically so the new table keeps the values from the
-- last table.
local function merge( t1, t2 )
	for k,v in pairs( t2 ) do
		if (type(v) == "table") and (type(t1[k] or false) == "table") then
			merge( t1[k], t2[k] )
		elseif tonumber( k ) then
			table.insert( t1, v )
		else
			t1[k] = v
		end
	end
	return t1
end

--- An internal extension to change the behavior when rendering an image
extensions['image'] = function( args, params, extract )
	local str = extract.argument.value
	local title, value
	if str then
		title = mw.text.nowiki( mw.text.unstrip( str ) )
		value = ('[[File:' .. title .. '|frameless|upright=1|alt=' .. title .. ']]')
	end
	local td = mw.html.create( 'td' )
		:attr( 'colspan', 4 )
	if value then
		td:wikitext( value )
	else
		td:node( failed( 'title', extract.name, 'image' ) )
	end
	local tr = mw.html.create( 'tr' )
		:addClass( escape( extract.name ) )
		:node( td )
	return tr
end

--- The core parser before dispatching
local function parse( str, params, lang )
	if parsers[params.type] then
		for _,v in ipairs( parsers[params.type] ) do
			if str:match( v[1] ) then
				local keys = {}
				local key, parts = v[2]( str, params )
				if params.classes then
					for _,cls in ipairs(params.classes) do
						keys[1+#keys] = 'smartbox-' .. cls .. '-' .. key
					end
				else
					keys[1+#keys] = 'smartbox-' .. key
				end
				local msg = mw.message.newFallbackSequence( unpack( keys ) )
				if lang then
					msg:inLanguage( lang )
				elseif not msg:exists() then
					msg = mw.message.newRawMessage( messages['smartbox-' .. key] or ('<smartbox-' .. key .. '>'))
				end
				return msg:params( unpack( parts ) ):plain()
			end
		end
	end
	return str
end

local renders = {}

--- The renderer for unknown type
renders['unknown'] = function( args, params, extract )
	local value = parse( extract.argument.value, params ) or nil

	local th = mw.html.create( 'th' )
		:attr( 'colspan', 2 )
	if params.label then
		th:wikitext( params.label )
	else
		th:node( failed( 'title', extract.name, 'unknown' ) )
	end

	local td = mw.html.create( 'td' )
		:attr( 'colspan', 2 )
	if value then
		td:wikitext( value )
	else
		td:node( failed( 'value', extract.name, 'unknown' ) )
	end

	local tr = mw.html.create( 'tr' )
		:addClass( escape( extract.name ) )
		:node( th )
		:node( td )

	return tr
end

--- The renderer for number type
renders['number'] = function( args, params, extract )
	local value = parse( extract.argument.value, params ) or nil

	local th = mw.html.create( 'th' )
		:attr( 'colspan', 2 )
	if params.label then
		th:wikitext( params.label )
	else
		th:node( failed( 'title', extract.name, 'number' ) )
	end

	local td = mw.html.create( 'td' )
		:attr( 'colspan', 2 )
	if value then
		td:wikitext( value )
	else
		td:node( failed( 'value', extract.name, 'number' ) )
	end

	local tr = mw.html.create( 'tr' )
		:addClass( escape( extract.name ) )
		:node( th )
		:node( td )

	return tr
end

--- The renderer for string type
renders['string'] = function( args, params, extract )
	local value = parse( extract.argument.value, params ) or nil

	local th = mw.html.create( 'th' )
		:attr( 'colspan', 2 )
	if params.label then
		th:wikitext( params.label )
	else
		th:node( failed( 'title', extract.name, 'string' ) )
	end

	local td = mw.html.create( 'td' )
		:attr( 'colspan', 2 )
	if value then
		td:wikitext( value )
	else
		td:node( failed( 'value', extract.name, 'string') )
	end

	local tr = mw.html.create( 'tr' )
		:addClass( escape( extract.name ) )
		:node( th )
		:node( td )

	return tr
end

--- The renderer for unknown type
renders['boolean'] = function( args, params, extract )
	local value = mw.text.nowiki( extract.argument.value )

	local th = mw.html.create( 'th' )
		:attr( 'colspan', 2 )
	if params.label then
		th:wikitext( params.label )
	else
		th:node( failed( 'title', extract.name, 'boolean' ) )
	end

	local td = mw.html.create( 'td' )
		:attr( 'colspan', 2 )
	if value then
		td:wikitext( value )
	else
		td:node( failed( 'value', extract.name, 'boolean') )
	end

	local tr = mw.html.create( 'tr' )
		:addClass( escape( extract.name ) )
		:node( th )
		:node( td )

	return tr
end

--- The renderer for date type
renders['date'] = function( args, params, extract )
	local str = extract.argument.value
	local value = str and lang:formatDate( 'j. M Y', str, true ) or nil

	local th = mw.html.create( 'th' )
		:attr( 'colspan', 2 )
	if params.label then
		th:wikitext( params.label )
	else
		th:node( failed( 'title', extract.name, 'date' ) )
	end

	local td = mw.html.create( 'td' )
		:attr( 'colspan', 2 )
	if value then
		td:wikitext( value )
	else
		td:node( failed( 'value', extract.name, 'date') )
	end

	local tr = mw.html.create( 'tr' )
		:addClass( escape( extract.name ) )
		:node( th )
		:node( td )

	return tr
end

--- The renderer for wiki-page-name type
renders['wiki-page-name'] = function( args, params, extract )
	local str = extract.argument.value
	local title, text, value
	if str then
		title = mw.text.nowiki( mw.text.unstrip( str ) )
		text = title:gsub( '%s*%(.-%)%s*$', '' )
		value = title == text and ('[[' .. title .. ']]') or ('[[' .. title .. '|' .. text .. ']]')
	end

	local th = mw.html.create( 'th' )
		:attr( 'colspan', 2 )
	if params.label then
		th:wikitext( params.label )
	else
		th:node( failed( 'title', extract.name, 'wiki-page-name' ) )
	end

	local td = mw.html.create( 'td' )
		:attr( 'colspan', 2 )
	if value then
		td:wikitext( value )
	else
		td:node( failed( 'value', extract.name, 'wiki-page-name') )
	end

	local tr = mw.html.create( 'tr' )
		:addClass( escape( extract.name ) )
		:node( th )
		:node( td )

	return tr
end

--- The renderer for wiki-file-name type
renders['wiki-file-name'] = function( args, params, extract )
	local str = extract.argument.value
	local title, value
	if str then
		title = mw.text.nowiki( mw.text.unstrip( str ) )
		value = ('[[File:' .. title .. '|frameless|upright|alt=' .. title .. ']]')
	end

	local th = mw.html.create( 'th' )
		:attr( 'colspan', 2 )
	if params.label then
		th:wikitext( params.label )
	else
		th:node( failed( 'title', extract.name, 'wiki-file-name' ) )
	end

	local td = mw.html.create( 'td' )
		:attr( 'colspan', 2 )
	if value then
		td:wikitext( value )
	else
		td:node( failed( 'value', extract.name, 'wiki-file-name') )
	end

	local tr = mw.html.create( 'tr' )
		:addClass( escape( extract.name ) )
		:node( th )
		:node( td )

	return tr
end

--- The renderer for wiki-user-name type
renders['wiki-user-name'] = function( args, params, extract )
	local value = "Not implemented"

	local th = mw.html.create( 'th' )
		:attr( 'colspan', 2 )
	if params.label then
		th:wikitext( params.label )
	else
		th:node( failed( 'title', extract.name, 'wiki-user-name' ) )
	end

	local td = mw.html.create( 'td' )
		:attr( 'colspan', 2 )
	if value then
		td:wikitext( value )
	else
		td:node( failed( 'value', extract.name, 'wiki-user-name') )
	end

	local tr = mw.html.create( 'tr' )
		:addClass( escape( extract.name ) )
		:node( th )

	return tr
end

--- The renderer for content type
renders['content'] = function( args, params, extract )
	local value = mw.text.nowiki( extract.argument.value )

	local th = mw.html.create( 'th' )
		:attr( 'colspan', 2 )
	if params.label then
		th:wikitext( params.label )
	else
		th:node( failed( 'title', extract.name, 'content' ) )
	end

	local td = mw.html.create( 'td' )
		:attr( 'colspan', 2 )
	if value then
		td:wikitext( value )
	else
		td:node( failed( 'value', extract.name, 'content') )
	end
		
	local tr = mw.html.create( 'tr' )
		:addClass( escape( extract.name ) )
		:node( th )
		:node( td )

	return tr
end

--- The renderer for unbalanced-wikitext type
renders['unbalanced-wikitext'] = function( args, params, extract )
	local value = "Should not be used"

	local th = mw.html.create( 'th' )
		:attr( 'colspan', 2 )
	if params.label then
		th:wikitext( params.label )
	else
		td:node( failed( 'title', extract.name, 'unbalanced-wikitext') )
	end

	local td = mw.html.create( 'td' )
		:attr( 'colspan', 2 )
	if value then
		td:wikitext( value )
	else
		td:node( failed( 'value', extract.name, 'unbalanced-wikitext') )
	end

	local tr = mw.html.create( 'tr' )
		:addClass( escape( extract.name ) )
		:node( th )

	return tr
end

--- The renderer for line type
renders['line'] = function( args, params, extract )
	local value = mw.text.nowiki( extract.argument.value )

	local th = mw.html.create( 'th' )
		:attr( 'colspan', 2 )
	if params.label then
		th:wikitext( params.label )
	else
		th:node( failed( 'title', extract.name, 'line' ) )
	end

	local td = mw.html.create( 'td' )
		:attr( 'colspan', 2 )
	if value then
		td:wikitext( value )
	else
		td:node( failed( 'value', extract.name, 'line') )
	end
		
	local tr = mw.html.create( 'tr' )
		:addClass( escape( extract.name ) )
		:node( th )
		:node( td )

	return tr
end

--- Report a message as a full-width row
-- @param msg to be wrapped in html code
-- @return html-wrapped string
local function report( msg )
	local th = mw.html.create( 'th' )
		:attr( 'colspan', 4 )
		:wikitext( msg )
	local tr = mw.html.create( 'tr' )
		:node( th )
	return tr
end

local function usePreferredName( key, arr, params )
	return arr[key] and { found = arr[key] } or nil
end
	
local function useAlternateName( key, arr, params )
	local aliases = params.aliases
	if aliases then
		for _,alias in ipairs( aliases ) do
			if arr[alias] then
				return { name = alias, found = arr[alias] }
			end
		end
	end
	return nil
end

local function useWikibaseLookup( params )
	local stuff = wikibase.run( params.path )
	return stuff and { found = stuff } or nil
end

local function useFallbackType( arr, params )
	local type = params.type or 'unknown'
	return arr[type] and { found = renders[type] } or nil
end

local function render( data, args )
	local params = data.params
	if not params then
		return "''TemplateData: Missing params section''"
	end
	
	local rows = {}
	
	local unhandled = {}
	for k,v in pairs( args ) do
		if type(k) == 'string' then
			unhandled[k] = true
		end
	end

	local infobox = data.sets and data.maps.infobox or {}
	local exclude = {}
	if infobox.exclude then
		for i,v in ipairs( infobox.exclude ) do
			unhandled[v] = nil
			exclude[v] = true
		end
	end
	
	local render, argument
	
	local function helper( key, params )
		render = usePreferredName( key, extensions, params ) or useAlternateName( key, extensions, params )
		argument = usePreferredName( key, args, params ) or useAlternateName( key, args, params )
		if not argument and params.path then
			argument = useWikibaseLookup( params )
		end
		if argument and not render then
			render = useFallbackType( renders, params )
		end
		if argument and render and not exclude[argument.name or key] then
			unhandled[argument.name or key] = nil
			argument['value'] = argument['found']
			argument['found'] = nil
			render['func'] = render['found']
			render['found'] = nil
			local extract = {}
			extract['name'] = key
			extract['argument'] = argument
			extract['render'] = render
			extract['classes'] = classes
			return render.func( args, params, extract )
		end
		return nil
	end
	
	if data.paramOrder then
		local paramOrder = data.paramOrder
		for i,v in ipairs( paramOrder ) do
			if params[v] then
				local node = helper( v, params[v] )
				if node then
					rows[1+#rows] = node
				end
			end
		end
	else
		for k,v in pairs( params ) do
			if v then
				local node = helper( k, v )
				if node then
					rows[1+#rows] = node
				end
			end
		end
	end
	
	local html = mw.html.create( 'table' )
		:addClass('infobox')
		
	for _,v in ipairs( classes ) do
		html:addClass( escape( v ) )
	end
	
	for _,v in ipairs( rows ) do
		html:node( v )
	end
	
	for k,v in pairs ( unhandled ) do
		html:node( report( tostring( failed( 'row', k, "undefined" ) ) ) )
	end
	
	return html
end

local function compile( namespace, text )
	
	local title = mw.title.new( text, namespace )
	if not title then
		return nil
	end
	
	local content = title:getContent()
	if not content then
		return nil
	end
	
	local json = content:match('<templatedata[^>]*>(.-)</templatedata>')
	if not json then
		return nil
	end
	
	local data = mw.text.jsonDecode( json )
	if not data then
		return nil
	end
	
	return data
end

-- @var exported table
local Smartbox = {}

--- Build the box
-- This is the only exposed library function unless the library
-- are loaded inside module or module discussion namespaces
function Smartbox.build( frame )
	local data, extra
	data = {}
	for _,v in ipairs( frame.args ) do
		local str = mw.text.trim( v )
		if str == '' then
			-- do nothing
		elseif v:match( '^%s*%{' ) then
			-- inline json, mostly for testing
			extra = mw.text.jsonDecode( v )
		else
			if str:match( '^[^:/%s]+$' ) then
				-- seems like class markers, register them
				classes[1+#classes] = str
			elseif str:match( '^/' ) then
				-- assumed to contain extension code
				local lib = require( 'Module:Smartbox' .. str )
				for k,v in pairs( lib ) do
					if not k:match( '^_' ) then
						extensions[k] = v
					end
				end
			elseif str:match( '^[mM]al:' ) or str:match( '^[tT]emplate:' ) then
				-- assumed to contain shared template data, compile and keep unless already done
				--if data then
				--	return "''Smartbox: TemplateData can only be registered once''"
				--end
				local namespace = str:match( '^(%S-)%s*:' )
				local text = str:match( ':%s*(.-)%s*$' )
				if namespace and text then
					local tmpData = compile( namespace, text )
					if tmpData then
						extra = tmpData
					end
				end
			else
				--report this?
			end
		end
		if extra then
			merge( data, extra )
		end
	end

	if not data then
		return "''Smartbox: TemplateData can not be found''"
	end
	
	local paths = 0
	for k,v in pairs( frame.args ) do
		if data.params[k] then
			data.params[k].path = v
			paths = 1 + paths
		end
	end
	if paths > 0 then
		wikibase = require('Module:Wikibase')
	end
	
	local html = render(data, frame:getParent().args)
	if not html then
		return "''Smartbox: Content can not be rendered''"
	end
	
	return html
end

-- This little snippet will make itpossible to interactively
-- test the functions from the test console
local title = mw.title.getCurrentTitle()
if title:inNamespaces( 828, 829) then -- module and module discussion
	Smartbox._ucFirst = ucFirst
	Smartbox._replaceTerms = replaceTerms
	Smartbox._escape = escape
	Smartbox._parse = parse
	Smartbox._merge = merge
	Smartbox._report = report
	Smartbox._usePreferredName = usePreferredName
	Smartbox._useAlternateName = useAlternateName
	Smartbox._useWikibaseLookup = useWikibaseLookup
	Smartbox._useFallbackType = useFallbackType
	Smartbox._render = render
	Smartbox._compile = compile
end

-- export the accesspoint
return Smartbox