Module:Template test case: Difference between revisions

    From Nonbinary Wiki
    (if the first template is specified but the second is not, make the second the sandbox of the first rather than the sandbox of the base page; simplify the title-getting code in the process)
    (add TestCase:renderColumns and some more supporting methods)
    Line 20: Line 20:
    -- Set input
    -- Set input
    for k, v in pairs(options or {}) do
    for k, v in pairs(options or {}) do
    obj[k] = v
    if not Template[k] then
    obj[k] = v
    end
    end
    end
    obj.invocation = invocationObj
    obj.invocation = invocationObj
    Line 69: Line 71:
    local link = self:makeLink(display)
    local link = self:makeLink(display)
    return mw.text.nowiki('{{') .. link .. mw.text.nowiki('}}')
    return mw.text.nowiki('{{') .. link .. mw.text.nowiki('}}')
    end
    function Template:makeHeading()
    return self.heading or self:makeBraceLink()
    end
    end


    Line 144: Line 150:


    return obj
    return obj
    end
    function TestCase:getTemplateOutput(templateObj)
    local output = templateObj:getOutput()
    if self.options.resetRefs then
    mw.getCurrentFrame():extensionTag('references')
    end
    return output
    end
    function TestCase:renderColumns()
    local root = mw.html.create('table')
    root
    :addClass(self.options.class)
    :cssText(self.options.style)
    :tag('caption')
    :wikitext(self.options.caption or 'Side by side comparison')
    -- Headings
    local headingRow = root:tag('tr')
    if self.options.rowheader then
    headingRow:tag('th'):wikitext(self.options.heading0)
    end
    local width = tostring(math.floor(100 / #self.templates)) .. '%'
    for i, obj in ipairs(self.templates) do
    headingRow
    :tag('th')
    :css('width', width)
    :wikitext(obj:makeHeading())
    end
    -- Row header
    local dataRow = root:tag('tr'):css('vertical-align', 'top')
    if self.options.rowheader then
    dataRow:tag('th')
    :attr('scope', 'row')
    :wikitext(self.options.rowheader)
    end
    -- Template output
    for i, obj in ipairs(self.templates) do
    dataRow:tag('td')
    :newline()
    :wikitext(self:getTemplateOutput(obj))
    :wikitext(self.options.after)
    end
    return tostring(root)
    end
    end


    Line 152: Line 206:
    ret[#ret + 1] = '<div style="clear: both;"></div>'
    ret[#ret + 1] = '<div style="clear: both;"></div>'
    ret[#ret + 1] = obj:makeBraceLink()
    ret[#ret + 1] = obj:makeBraceLink()
    ret[#ret + 1] = obj:getOutput()
    ret[#ret + 1] = self:getTemplateOutput(obj)
    end
    end
    return table.concat(ret, '\n\n')
    return table.concat(ret, '\n\n')

    Revision as of 07:06, 25 November 2014

    Documentation for this module may be created at Module:Template test case/doc

    -- This module provides several methods to generate test cases.
    
    local mTableTools = require('Module:TableTools')
    local libraryUtil = require('libraryUtil')
    local checkType = libraryUtil.checkType
    
    local TEMPLATE_NAME_MAGIC_WORD = '<TEMPLATE_NAME>'
    local TEMPLATE_NAME_MAGIC_WORD_ESCAPED = TEMPLATE_NAME_MAGIC_WORD:gsub('%p', '%%%0')
    
    -------------------------------------------------------------------------------
    -- Template class
    -------------------------------------------------------------------------------
    
    local Template = {}
    Template.__index = Template
    
    function Template.new(invocationObj, options)
    	local obj = setmetatable({}, Template)
    
    	-- Set input
    	for k, v in pairs(options or {}) do
    		if not Template[k] then
    			obj[k] = v
    		end
    	end
    	obj.invocation = invocationObj
    
    	-- Validate input
    	if not obj.template and not obj.title then
    		error('no template or title specified', 2)
    	end
    
    	return obj
    end
    
    function Template:getFullPage()
    	if self.template then
    		local strippedTemplate, hasColon = self.template:gsub('^:', '', 1)
    		local ns = strippedTemplate:match('^(.-):')
    		ns = ns and mw.site.namespaces[ns]
    		if ns then
    			return strippedTemplate
    		elseif hasColon then
    			return strippedTemplate -- Main namespace
    		else
    			return mw.site.namespaces[10].name .. ':' .. strippedTemplate
    		end
    	else
    		return self.title.prefixedText
    	end
    end
    
    function Template:getName()
    	if self.template then
    		return self.template
    	else
    		return require('Module:Template invocation').name(self.title)
    	end
    end
    
    function Template:makeLink(display)
    	if display then
    		return string.format('[[:%s|%s]]', self:getFullPage(), display)
    	else
    		return string.format('[[:%s]]', self:getFullPage())
    	end
    end
    
    function Template:makeBraceLink(display)
    	display = display or self:getName()
    	local link = self:makeLink(display)
    	return mw.text.nowiki('{{') .. link .. mw.text.nowiki('}}')
    end
    
    function Template:makeHeading()
    	return self.heading or self:makeBraceLink()
    end
    
    function Template:getInvocation(format)
    	local invocation = self.invocation:getInvocation(self:getName())
    	invocation = mw.text.nowiki(invocation)
    	if format == 'code' then
    		invocation = '<code>' .. invocation .. '</code>'
    	elseif format == 'pre' then
    		invocation = '<pre style="white-space: pre-wrap;">' .. invocation .. '</pre>'
    		invocation = mw.getCurrentFrame():preprocess(invocation)
    	end
    	return invocation
    end
    
    function Template:getOutput()
    	return self.invocation:getOutput(self:getName())
    end
    
    -------------------------------------------------------------------------------
    -- TestCase class
    -------------------------------------------------------------------------------
    
    local TestCase = {}
    TestCase.__index = TestCase
    
    function TestCase.new(invocationObj, options)
    	local obj = setmetatable({}, TestCase)
    
    	-- Validate options
    	do
    		local highestNum = 0
    		for k in pairs(options) do
    			if type(k) == 'string' then
    				local num = k:match('([1-9][0-9]*)$')
    				num = tonumber(num)
    				if num and num > highestNum then
    					highestNum = num
    				end
    			end
    		end
    		for i = 3, highestNum do
    			if not options['template' .. i] then
    				error(string.format(
    					"one or more options ending in '%d' were " ..
    					"detected, but no 'template%d' option was found",
    					i, i
    				), 2)
    			end
    		end
    	end
    
    	-- Separate general options from options for specific templates
    	local templateOptions = mTableTools.numData(options, true)
    	obj.options = templateOptions.other or {}
    
    	-- Add default template options
    	templateOptions[1] = templateOptions[1] or {}
    	templateOptions[2] = templateOptions[2] or {}
    	if templateOptions[1].template and not templateOptions[2].template then
    		templateOptions[2].template = templateOptions[1].template .. '/sandbox'
    	end
    	if not templateOptions[1].template then
    		templateOptions[1].title = mw.title.getCurrentTitle().basePageTitle
    	end
    	if not templateOptions[2].template then
    		templateOptions[2].title = templateOptions[1].title:subPageTitle('sandbox')
    	end
    
    	-- Make the template objects
    	obj.templates = {}
    	for i, t in ipairs(templateOptions) do
    		table.insert(obj.templates, Template.new(invocationObj, t))
    	end
    
    	return obj
    end
    
    function TestCase:getTemplateOutput(templateObj)
    	local output = templateObj:getOutput()
    	if self.options.resetRefs then
    		mw.getCurrentFrame():extensionTag('references')
    	end
    	return output
    end
    
    function TestCase:renderColumns()
    	local root = mw.html.create('table')
    	root
    		:addClass(self.options.class)
    		:cssText(self.options.style)
    		:tag('caption')
    			:wikitext(self.options.caption or 'Side by side comparison')
    
    	-- Headings
    	local headingRow = root:tag('tr')
    	if self.options.rowheader then
    		headingRow:tag('th'):wikitext(self.options.heading0)
    	end
    	local width = tostring(math.floor(100 / #self.templates)) .. '%'
    	for i, obj in ipairs(self.templates) do
    		headingRow
    			:tag('th')
    				:css('width', width)
    				:wikitext(obj:makeHeading())
    	end
    
    	-- Row header
    	local dataRow = root:tag('tr'):css('vertical-align', 'top')
    	if self.options.rowheader then
    		dataRow:tag('th')
    			:attr('scope', 'row')
    			:wikitext(self.options.rowheader)
    	end
    	
    	-- Template output
    	for i, obj in ipairs(self.templates) do
    		dataRow:tag('td')
    			:newline()
    			:wikitext(self:getTemplateOutput(obj))
    			:wikitext(self.options.after)
    	end
    	
    	return tostring(root)
    end
    
    function TestCase:renderDefault()
    	local ret = {}
    	ret[#ret + 1] = self.templates[1]:getInvocation('code')
    	for i, obj in ipairs(self.templates) do
    		ret[#ret + 1] = '<div style="clear: both;"></div>'
    		ret[#ret + 1] = obj:makeBraceLink()
    		ret[#ret + 1] = self:getTemplateOutput(obj)
    	end
    	return table.concat(ret, '\n\n')
    end
    
    function TestCase:__tostring()
    	local methods = {
    		columns = 'renderColumns',
    		rows = 'renderRows'
    	}
    	local format = self.options.format
    	local method = format and methods[format] or 'renderDefault'
    	return self[method](self)
    end
    
    -------------------------------------------------------------------------------
    -- Nowiki invocation class
    -------------------------------------------------------------------------------
    
    local NowikiInvocation = {}
    NowikiInvocation.__index = NowikiInvocation
    
    function NowikiInvocation.new(invocation)
    	local obj = setmetatable({}, NowikiInvocation)
    	obj.invocation = mw.text.unstrip(invocation)
    	return obj
    end
    
    function NowikiInvocation:getInvocation(template)
    	template = template:gsub('%%', '%%%%') -- Escape "%" with "%%"
    	local invocation, count = self.invocation:gsub(
    		TEMPLATE_NAME_MAGIC_WORD_ESCAPED,
    		template
    	)
    	if count < 1 then
    		error(string.format(
    			"the template invocation must include '%s' in place " ..
    			"of the template name",
    			TEMPLATE_NAME_MAGIC_WORD
    		))
    	end
    	return invocation
    end
    
    function NowikiInvocation:getOutput(template)
    	local invocation = self:getInvocation(template)
    	return mw.getCurrentFrame():preprocess(invocation)
    end
    
    -------------------------------------------------------------------------------
    -- Table invocation class
    -------------------------------------------------------------------------------
    
    local TableInvocation = {}
    TableInvocation.__index = TableInvocation
    
    function TableInvocation.new(invokeArgs)
    	local obj = setmetatable({}, TableInvocation)
    	obj.invokeArgs = invokeArgs
    	return obj
    end
    
    function TableInvocation:getInvocation(template)
    	return require('Module:Template invocation').invocation(
    		template,
    		self.invokeArgs
    	)
    end
    
    function TableInvocation:getOutput(template)
    	return mw.getCurrentFrame():expandTemplate{
    		title = template,
    		args = self.invokeArgs
    	}
    end
    
    -------------------------------------------------------------------------------
    -- Exports
    -------------------------------------------------------------------------------
    
    -- Table-based exports
    
    local function getTableArgs(frame, wrappers)
    	return require('Module:Arguments').getArgs(frame, {
    		wrappers = wrappers,
    		trim = false,
    		removeBlanks = false
    	})
    end
    
    local p = {}
    
    function p._table(args)
    	local options, invokeArgs = {}, {}
    	for k, v in pairs(args) do
    		local optionKey = type(k) == 'string' and k:match('^_(.*)$')
    		if optionKey then
    			if type(v) == 'string' then
    				v = v:match('^%s*(.-)%s*$') -- trim whitespace
    			end
    			if v ~= '' then
    				options[optionKey] = v
    			end
    		else
    			invokeArgs[k] = v
    		end
    	end
    	local invocationObj = TableInvocation.new(invokeArgs)
    	local testCaseObj = TestCase.new(invocationObj, options)
    	return tostring(testCaseObj)
    end
    
    function p.table(frame)
    	return p._table(getTableArgs(frame, 'Template:Test case from arguments'))
    end
    
    function p.columns(frame)
    	local args = getTableArgs(frame, 'Template:Testcase table')
    	args._format = 'columns'
    	return p._table(args)
    end
    
    function p.rows(frame)
    	local args = getTableArgs(frame, 'Template:Testcase rows')
    	args._format = 'rows'
    	return p._table(args)
    end
    
    -- Nowiki-based exports
    
    function p._nowiki(args)
    	local invocationObj = NowikiInvocation.new(args.invocation)
    	args.invocation = nil
    	local options = args
    	local testCaseObj = TestCase.new(invocationObj, options)
    	return tostring(testCaseObj)
    end
    
    function p.nowiki(frame)
    	local args = require('Module:Arguments').getArgs(frame, {
    		wrappers = 'Template:Test case from invocation'
    	})
    	return p._nowiki(args)
    end
    
    -- Exports for testing
    
    function p._exportClasses()
    	return {
    		TestCase = TestCase,
    		Invocation = Invocation,
    		NowikiInvocation = NowikiInvocation,
    		TableInvocation = TableInvocation
    	}
    end
    
    return p