Edge Rewrite
Jump to content

Module:Format

From Wikipedia, the free encyclopedia

--- This module provides a powerful formatting engine for
-- template links, module links, wikilinks, external links, etc.
--
-- @module format
-- @alias p
-- @release alpha
-- @require Module:Yesno
-- @see [[Module:Template link with magic]]


local p = {}

local yn = require( "Module:Yesno" )

local magic = mw.loadData( "Module:Format/magiclinks" )


--- Global flags
local link = true
local wrapperTags = {}

function wrapperTags:wrap( text )
	local open = ''
	local close = ''
	for _,v in ipairs( self ) do
		open = open .. '<' .. v .. '>'
		close = '</' .. mw.text.split( v, ' ', true )[ 1 ] .. '>'
	end
	return open .. text .. close
end

local Linker = { linkedParamNumber = nil, namespace = 0, numberedParamOffset = math.huge, namedargs = false, forcens = false }

Linker.__index = Linker

function Linker:construct( linkedParamNumber, namespace, numberedParamOffset, namedargs, forcens )
	local o = setmetatable({}, Linker)
	o.linkedParamNumber = linkedParamNumber
	o.namespace = namespace or self.namespace
	o.numberedParamOffset = numberedParamOffset or self.numberedParamOffset
	o.namedargs = namedargs or self.namedargs
	o.forcens = forcens or self.forcens
	return o
end

function Linker:run( args )
	local title = mw.title.new( ( self.forcens and mw.site.namespaces[ self.namespace ].name .. ':' or '')
		.. args[ self.linkedParamNumber ], mw.site.namespaces[ self.namespace ].id )
	local outTable = {}
	local currArg = 1
	while args[currArg] ~= nil do
		if ( currArg == self.linkedParamNumber and link ) then
			outTable[currArg] = '[[:' .. title.fullText .. '|' .. args[currArg] .. ']]'
		else
			outTable[currArg] = args[currArg]
		end
		currArg = currArg + 1
	end
	if self.namedargs then
		local maxArg = currArg
		for k,v in pairs(args) do
			if tonumber(k) and (( tonumber(k) >= 1 + self.numberedParamOffset and tonumber(k) < maxArg ) or ( tonumber(k) <= self.linkedParamNumber and tonumber(k) >= 1)) then
			else
				if tonumber(k) and tonumber(k) >= self.numberedParamOffset then
					outTable[currArg] = tonumber(k) - self.numberedParamOffset .. ' = ' .. v
				else
					outTable[currArg] = k .. ' = ' .. v
				end
				currArg = currArg + 1
			end
		end
	end
	return outTable
end

--- Pagename linker function
--
-- @param {table} args Module arguments
-- @return {table} Resulting table
local pagenameLinker = Linker:construct( 1 )

local tagLinker = Linker:construct( 1 )
function tagLinker:run( args )
	local tag = args[ 1 ]
	local tagContent = args[ 2 ]
	local tagLink = magic.xmlTags[ tag ] or magic.htmltags[ tag ]
	local outTable = {}
	local currArg = 2
	outTable[1] = tagLink and link and '[[:' .. tagLink .. '|' .. args[currArg] .. ']]' or args[currArg]
	if tagContent then
		outTable[2] = tagContent
		currArg = 3
	end
	for k,v in pairs( args ) do
		if tonumber(k) and (tonumber(k) <= 2 and tonumber(k) >= 1) then
		else
			if tonumber(k) and tonumber(k) > 2 then
				outTable[currArg] = (tonumber(k) - 2) .. ' = ' .. v
			else
				outTable[currArg] = k .. ' = ' .. v
			end
			currArg = currArg + 1
		end
	end
	return outTable
end

local templateLinker = Linker:construct( 1, 10, 1, true )

local plainLinker = Linker:construct( 1, 0, 1, true )

local lsthLinker = Linker:construct( 1 )
function lsthLinker:run( args )
	local title = mw.title.new( args[ 1 ], 0 )
	local outTable = {}
	local currArg = 1
	while args[currArg] ~= nil do
		if ( currArg == 1 and link ) then
			outTable[currArg] = '[[:' .. title.fullText .. '|' .. args[currArg] .. ']]'
		elseif ( currArg == 2 and link ) then
			outTable[currArg] = '[[:' .. title.fullText .. '#' .. args[currArg] .. '|' .. args[currArg] .. ']]'
		else
			outTable[currArg] = args[currArg]
		end
		currArg = currArg + 1
	end
	return outTable
end

--- Linkers for various parser functions
--
-- @table linkers
local linkers = {
	["#invoke"] = Linker:construct( 1, 'Module', 2, true ),

	["#lst"] = pagenameLinker,
	["#lsth"] = lsthLinker,
	["#lstx"] = pagenameLinker,

	["int"] = Linker:construct( 1, 8 ),
	["GENDER"] = Linker:construct( 1, 2 ),

	["PROTECTIONLEVEL"] = Linker:construct( 2 ),
	["#contentmodel"] = Linker:construct( 2 ),

	["PAGESINCATEGORY"] = Linker:construct( 1, 14 ),
	["PAGESINCAT"] = Linker:construct( 1, 14 ),

	["FULLPAGENAME"] = pagenameLinker,
	["PAGENAME"] = pagenameLinker,
	["BASEPAGENAME"] = pagenameLinker,
	["SUBPAGENAME"] = pagenameLinker,
	["SUBJECTPAGENAME"] = pagenameLinker,
	["ARTICLEPAGENAME"] = pagenameLinker,
	["TALKPAGENAME"] = pagenameLinker,
	["ROOTPAGENAME"] = pagenameLinker,
	["NAMESPACE"] = pagenameLinker,
	["SUBJECTSPACE"] = pagenameLinker,
	["TALKSPACE"] = pagenameLinker,

	["FULLPAGENAMEE"] = pagenameLinker,
	["PAGENAMEE"] = pagenameLinker,
	["BASEPAGENAMEE"] = pagenameLinker,
	["SUBPAGENAMEE"] = pagenameLinker,
	["SUBJECTPAGENAMEE"] = pagenameLinker,
	["ARTICLEPAGENAMEE"] = pagenameLinker,
	["TALKPAGENAMEE"] = pagenameLinker,
	["ROOTPAGENAMEE"] = pagenameLinker,
	["NAMESPACEE"] = pagenameLinker,
	["SUBJECTSPACEE"] = pagenameLinker,
	["TALKSPACEE"] = pagenameLinker,

	["localurl"] = pagenameLinker,
	["localurle"] = pagenameLinker,
	["fullurl"] = pagenameLinker,
	["fullurle"] = pagenameLinker,
	["canonicalurl"] = pagenameLinker,
	["canonicalurle"] = pagenameLinker,
	["filepath"] = Linker:construct( 1, 6, 1 ),

	["PAGEID"] = pagenameLinker,
	["PAGESIZE"] = pagenameLinker,
	["CASCADINGSOURCES"] = pagenameLinker,
	["REVISIONID"] = pagenameLinker,
	["REVISIONDAY"] = pagenameLinker,
	["REVISIONDAY2"] = pagenameLinker,
	["REVISIONMONTH"] = pagenameLinker,
	["REVISIONMONTH1"] = pagenameLinker,
	["REVISIONYEAR"] = pagenameLinker,
	["REVISIONTIMESTAMP"] = pagenameLinker,
	["REVISIONUSER"] = pagenameLinker,

	["#ifexist"] = pagenameLinker,

	["#tag"] = tagLinker
}

p.linkers = linkers -- debugging

--- Metatable in case linker is not explicitly defined.
setmetatable(linkers, {
	__index = function( tbl, key )
		local altKey = mw.text.split( key, ':', true )
		if (linkers[altKey][1]) then return linkers[altKey] end
		local default = Linker:construct( 1 )
		function default:run( args )
			--- Default builder for magic words and parser functions, etc.
			local outTable = {}
			local currArg = 1
			while args[currArg] ~= nil do
				outTable[currArg] = args[currArg]
				currArg = currArg + 1
			end
			local maxArg = currArg
			for k,v in pairs(args) do
				if tonumber(k) and tonumber(k) >= 1 and tonumber(k) < maxArg then
				else
					if tonumber(k) then
						outTable[currArg] = tonumber(k) .. ' = ' .. v
					else
						outTable[currArg] = k .. ' = ' .. v
					end
					currArg = currArg + 1
				end
			end
			return outTable
		end
		return default
	end
})

--- Gets the magic word or parser function specified if it exists
--
-- @param {string} word argument to test
-- @return Magic word if it exists
local function getMagicWordOrParserFunction( word )
	local func = mw.text.split( word, ':', true )[1]
	local lowerFunc = mw.ustring.lower( func )
	if magic.parserFunctions[func] or magic.variables_about_other_pages[func] or magic.variables[func]
		or magic.parserFunctions[lowerFunc] or magic.variables_about_other_pages[lowerFunc] or magic.variables[lowerFunc] then
		return func, ( magic.parserFunctions[func] or ( magic.variables_about_other_pages[func] and not magic.variables[func] ) )
			or ( magic.parserFunctions[lowerFunc] or ( magic.variables_about_other_pages[lowerFunc] and not magic.variables[lowerFunc] ) )
	end
	local success, _ = pcall( function( w )
		return mw.getCurrentFrame():callParserFunction( w )
	end, func )
	success, _ = success, _ or pcall( function( w )
		return mw.getCurrentFrame():callParserFunction( w )
	end, mw.ustring.lower( func ) )
	if success then return func, false else return nil, false end
end

--- Checks if a transclusion modifier is present
--
-- @param {string} arg Argument
-- @param {string} name Name of modifier
-- @return The name of the modifier, whether the modifier is present, and the new value of the argument
local function hasTransclusionModifier( arg, name )
	local modifier = mw.ustring.sub( arg, 1, mw.ustring.len( name .. ":" ))
	if ( mw.ustring.lower( modifier ) == name .. ':' ) then
		return modifier, true, mw.ustring.sub( arg, mw.ustring.len( name .. ":" ) + 1 )
	else
		return nil, false, arg
	end
end

local function hasOtherArgs( fn, args )
	for k,v in pairs( args ) do
		if tonumber(k) ~= 1 then
			return true
		else
			if mw.ustring.sub( v, mw.ustring.len( fn ) + 1, mw.ustring.len( fn ) + 1 ) == ":" then
				return true
			end
		end
	end
	return false
end

--- Left shifts argument names which begin with an underscore
--
-- @param {table} args Module arguments
-- @param {string} argName argument name
-- @return {table} New arguments
local function leftShiftUnderscore( args, argName )
	local us = '_'
	while args[ us .. '_' .. argName ] ~= nil do
		args[ us .. argName ] = nil
		args[ us .. argName ] = args[ us .. '_' .. argName ]
		us = us .. '_'
		mw.logObject( args )
	end
	args[ us .. argName ] = nil
	return args
end

--- Generates a template link code
--
-- @param {table} args Arguments
-- @return {string} Wikitext for the template link
-- @usage {{#invoke:Format|templateLink|''template or parser function name'''|...subsequent template args...}}
function p.templateLink( args )
	local templateName = args[1]
	link = yn( args['_link'] ~= nil and args['_link'] or true )
	args = leftShiftUnderscore( args, 'link' )

	if not templateName then error( "No template or magic word specified", 0 ) end

	local transclusionModifiers = {
		{ "subst", "safesubst" },
		{ "msg", "msgnw" },
		{ "raw" }
	}
	local modifiersToApply = {}

	for k,v in pairs( transclusionModifiers ) do
		local modifier, hasModifier
		for l,w in pairs( v ) do
			modifier, hasModifier, args[1] = hasTransclusionModifier( args[1], w )
			if ( hasModifier ) then
				table.insert( modifiersToApply, modifier )
				break
			end
		end
	end

	local parserFunction, needsColon = getMagicWordOrParserFunction( args[1] )
	local lowerParserFunction = parserFunction ~= nil and mw.ustring.lower( parserFunction )
	local colon = parserFunction ~= nil and mw.ustring.sub( args[1], mw.ustring.len( parserFunction ) + 1, mw.ustring.len( parserFunction ) + 1 ) == ':'
 	if parserFunction and colon and hasOtherArgs( parserFunction, args ) then
		-- A parser function must always have a colon following
		local parserFunctionLink = link and ( magic.parserFunctions[ parserFunction ] or magic.variables_about_other_pages[ parserFunction ] or magic.parserFunctions[ lowerParserFunction ] or magic.variables_about_other_pages[ lowerParserFunction ] or nil ) or nil
		args[1] = mw.ustring.sub( args[1], mw.ustring.len( parserFunction ) + 2 )
		local outTable = type( linkers[ parserFunction ] == "table" ) and linkers[ parserFunction ]:run( args ) or linkers[ parserFunction ]( args )
		return mw.getCurrentFrame():preprocess('<nowiki>{{</nowiki>')
		.. table.concat( modifiersToApply )
		.. (parserFunctionLink and '[[' .. parserFunctionLink .. '|' .. parserFunction .. ']]' or parserFunction)
		.. (colon and ':' or '')
		.. table.concat( outTable, '|' ) .. mw.getCurrentFrame():preprocess('<nowiki>}}</nowiki>')
	elseif parserFunction and not colon and not needsColon and not hasOtherArgs( parserFunction, args ) then
		-- A static variable does not have a colon and does not have any other parameters
		local parserFunctionLink = link and ( magic.variables[ parserFunction ] or magic.variables[ lowerParserFunction ] or nil ) or nil
		return mw.getCurrentFrame():preprocess('<nowiki>{{</nowiki>')
		.. table.concat( modifiersToApply )
		.. (parserFunctionLink and '[[' .. parserFunctionLink .. '|' .. parserFunction .. ']]' or parserFunction)
		.. (colon and ':' or '')
		.. mw.getCurrentFrame():preprocess('<nowiki>}}</nowiki>')
	else
		-- Treat like template
		local outTable = templateLinker:run( args )
		return mw.getCurrentFrame():preprocess('<nowiki>{{</nowiki>')
		.. table.concat( modifiersToApply )
		.. table.concat( outTable, "|" ) .. mw.getCurrentFrame():preprocess('<nowiki>}}</nowiki>')
	end
end

--- Generates a wikilink code
--
-- @param {table} args Arguments
-- @return {string} Wikitext for the wikilink
-- @usage {{#invoke:Format|wikilink|''page name''|''link display''|...other link arguments...}}
function p.wikilink( args )
	local linkTitle = mw.title.new( args[1] )
	if not linkTitle then error( "No link target specified", 0 ) end
	link = yn( args['_link'] ~= nil and args['_link'] or true )
	args = leftShiftUnderscore( args, 'link' )
	local i = 1
	while args[ '_wrapperTag' .. i ] ~= nil do
		table.insert( wrapperTags, args[ '_wrapperTag' .. i ] )
		args = leftShiftUnderscore( args, 'wrapperTag' .. i )
		i = i + 1
	end
	local outTable = plainLinker:run( args )
	return mw.getCurrentFrame():preprocess('<nowiki>[[</nowiki>') .. table.concat(outTable, "|") .. mw.getCurrentFrame():preprocess('<nowiki>]]</nowiki>')
end

--- Generates an external link code
--
-- @param {table} args Arguments
-- @return {string} Wikitext for the external link
-- @usage {{#invoke:Format|exlink|''URL''|''display''}}
function p.exlink( args )
	local linkUrl = args[1]
	local linkDisplay = args[2]
	link = yn( args['_link'] ~= nil and args['_link'] or true )
	local i = 1
	while args[ '_wrapperTag' .. i ] ~= nil do
		table.insert( wrapperTags, args[ '_wrapperTag' .. i ] )
		i = i + 1
	end
	local outTable = {}
	if not linkUrl then error( "No link target specified", 0 ) end
	if link then
		if yn( args.plainlinks ) then
			table.insert( outTable, '<span class="plainlinks">' .. linkUrl .. '</span>' )
		else
			table.insert( outTable, linkUrl )
		end
	else
		table.insert( outTable, mw.getCurrentFrame():preprocess( '<nowiki>' .. linkUrl .. '</nowiki>' ) )
	end
	if linkDisplay then
		table.insert( outTable, mw.getCurrentFrame():preprocess( '<nowiki>' .. linkDisplay .. '</nowiki>' ) )
	end
	return mw.getCurrentFrame():preprocess('<nowiki>[</nowiki>') .. table.concat(outTable, ' ') .. mw.getCurrentFrame():preprocess('<nowiki>]</nowiki>')
end

--- Generates a tag code
--
-- @param {table} args Arguments
-- @return {string} Wikitext for tag link
-- @usage {{#invoke:Format|tag|''tagName''|''tagContent''|...tag arguments...}}
function p.tag( args )
	local tag = args[1] or error("No tag specified", 0)
	local tagContent = args[2]
	link = yn( args['_link'] ~= nil and args['_link'] or true )
	args = leftShiftUnderscore( args, 'link' )
	local i = 1
	while args[ '_wrapperTag' .. i ] ~= nil do
		table.insert( wrapperTags, args[ '_wrapperTag' .. i ] )
		args = leftShiftUnderscore( args, 'wrapperTag' .. i )
		i = i + 1
	end
	local splitFirstArg = mw.text.split( tag, " " )
	tag = splitFirstArg[1]
	local props = table.concat( splitFirstArg, " ", 2 )
	local tagLink = magic.xmlTags[ tag ] or magic.htmltags[ tag ]
	local outTable = {}
	local currArg = 1
	if mw.ustring.sub( args[1], 1, 3 ) == "!--" and mw.ustring.sub( args[1], -2 ) == "--" then
		return mw.getCurrentFrame():preprocess('<nowiki><</nowiki>') .. args[1] .. mw.getCurrentFrame():preprocess('<nowiki>></nowiki>')
	end
	for k,v in pairs( args ) do
		if tonumber(k) and tonumber(k) - 2 < 1 then
		else
			if tonumber(k) and tonumber(k) >= 3 then
				outTable[currArg] = (tonumber(k) - 2) .. '=' .. '"' .. mw.ustring.gsub( v, '"', '&amp;quot;' ) .. '"'
			else
				outTable[currArg] = k .. '=' .. '"' .. mw.ustring.gsub( v, '"', '&amp;quot;' ) .. '"'
			end
			currArg = currArg + 1
		end
	end
	if tagContent ~= nil then
		return mw.getCurrentFrame():preprocess('<nowiki><</nowiki>')
			.. ((link and tagLink) and ('[[' .. tagLink .. '|' .. tag .. ']]') or tag)
			.. (props ~= nil and props ~= '' and ' ' .. props or '')
			.. (#outTable >= 1 and ' ' or '') .. table.concat( outTable, " " ) .. mw.getCurrentFrame():preprocess('<nowiki>></nowiki>') .. tagContent .. mw.getCurrentFrame():preprocess('<nowiki></</nowiki>') .. tag .. mw.getCurrentFrame():preprocess('<nowiki>></nowiki>')
	else
		return mw.getCurrentFrame():preprocess('<nowiki><</nowiki>')
			.. ((link and tagLink) and ('[[' .. tagLink .. '|' .. tag .. ']]') or tag)
			.. (props ~= nil and props ~= '' and ' ' .. props or '')
			.. (#outTable >= 1 and ' ' or '') .. table.concat( outTable, " " ) .. mw.getCurrentFrame():preprocess('<nowiki>/></nowiki>')
	end
end

local function main( args, fn )
	--- Handle wrapper tags
	local i = 1
	while args[ '_wrapperTag' .. i ] ~= nil do
		table.insert( wrapperTags, args[ '_wrapperTag' .. i ] )
		args = leftShiftUnderscore( args, 'wrapperTag' .. i )
		i = i + 1
	end
	--- As well as evaluation
	local showResult = yn( args[ '_y_' ] or args[ '_show_result' ] )
	args = leftShiftUnderscore( args, 'show_result' )
	args = leftShiftUnderscore( args, 'y_' )
	local syntaxhighlight = yn(args[ '_syntax_highlight' ])
	args = leftShiftUnderscore( args, "syntax_highlight" )
	local code = p[ fn ]( args )
	local output
	if showResult then
		-- based on [[Module:Plain text]]
		output = mw.getCurrentFrame():preprocess(
			mw.text.unstripNoWiki( code
				:gsub('&nbsp;', ' ') --replace nbsp spaces with regular spaces
				:gsub('<br ?/?>', '\n') --replace br with line breaks
				:gsub('<span.->(.-)</span>', '%1') --remove spans while keeping text inside
				:gsub('<i.->(.-)</i>', '%1') --remove italics while keeping text inside
				:gsub('<b.->(.-)</b>', '%1') --remove bold while keeping text inside
				:gsub('<em.->(.-)</em>', '%1') --remove emphasis while keeping text inside
				:gsub('<strong.->(.-)</strong>', '%1') --remove strong while keeping text inside
				:gsub('<sub.->(.-)</sub>', '%1') --remove subscript markup; retain contents
				:gsub('<sup.->(.-)</sup>', '%1') --remove superscript markup; retain contents
				:gsub('<u.->(.-)</u>', '%1') --remove underline markup; retain contents
				:gsub('<.->.-<.->', '') --strip out remaining tags and the text inside
				:gsub('<.->', '') --remove any other tag markup
				:gsub('%[%[%s*[Ff][Ii][Ll][Ee]%s*:.-%]%]', '') --strip out files
				:gsub('%[%[%s*[Ii][Mm][Aa][Gg][Ee]%s*:.-%]%]', '') --strip out use of image:
				:gsub('%[%[%s*[Cc][Aa][Tt][Ee][Gg][Oo][Rr][Yy]%s*:.-%]%]', '') --strip out categories
				:gsub('%[%[[^%]]-|', '') --strip out piped link text
				:gsub('([^%[])%[[^%[%]][^%]]-%s', '%1') --strip out external link text
				:gsub('^%[[^%[%]][^%]]-%s', '') --strip out external link text
				:gsub('[%[%]]', '') --then strip out remaining [ and ]
				:gsub("'''''", "") --strip out bold italic markup
				:gsub("'''?", "") --not stripping out '''' gives correct output for bolded text in quotes
				:gsub('%-%-%-%-+', '') --remove ---- lines
				:gsub("^%s+", "") --strip leading
				:gsub("%s+$", "") --and trailing spaces
				:gsub("%s+", " ") --strip redundant spaces
			)
		)
	end
	if syntaxhighlight then
		return mw.getCurrentFrame():extensionTag( "syntaxhighlight", code, { lang="wikitext", "inline" } ) .. ( output and ' → ' .. output or '' )
	end
	return wrapperTags:wrap( code ) .. ( output and ' → ' .. output or '' )
end

return setmetatable( {
	[''] = main
}, {
	__index = function( _, tblKey )
		if type( p[ tblKey ] ) ~= "function" then
			return p[ tblKey ]
		end
		return function( frame )
			local args = require( "Module:Arguments" ).getArgs( frame, {
				parentFirst = true,
				valueFunc = function (key, value)
					if type(value) == 'string' then
						value = value:match('^%s*(.-)%s*$') -- Remove whitespace.
						if value ~= '' then
							return value
						else
							return nil
						end
					else
						return value
					end
				end
			} )
			return main( args, tblKey )
		end
	end
})