Module:Template test case: Difference between revisions

Nothing to hide, but nothing to show you either.
Jump to navigation Jump to search
Content added Content deleted
(Allow custom headings to work in default format, same as in rows or columns format. See Template:Test case/testcases#Default format with custom headings and Template:Test case nowiki/testcases#Default format with custom headings.)
m (87 revisions imported from wikipedia:Module:Template_test_case)
 
(31 intermediate revisions by 11 users not shown)
Line 54: Line 54:
getFullPage = true,
getFullPage = true,
getName = true,
getName = true,
makeHeading = true,
makeHeader = true,
getOutput = true
getOutput = true
}
}
Line 94: Line 94:


function Template:getFullPage()
function Template:getFullPage()
if self.template then
if not self.template then
return self.title.prefixedText
elseif self.template:sub(1, 7) == '#invoke' then
return 'Module' .. self.template:sub(8):gsub('|.*', '')
else
local strippedTemplate, hasColon = self.template:gsub('^:', '', 1)
local strippedTemplate, hasColon = self.template:gsub('^:', '', 1)
hasColon = hasColon > 0
hasColon = hasColon > 0
Line 106: Line 110:
return mw.site.namespaces[10].name .. ':' .. strippedTemplate
return mw.site.namespaces[10].name .. ':' .. strippedTemplate
end
end
else
return self.title.prefixedText
end
end
end
end
Line 133: Line 135:
end
end


function Template:makeHeading()
function Template:makeHeader()
return self.heading or self:makeBraceLink()
return self.heading or self:makeBraceLink()
end
end


function Template:getInvocation(format)
function Template:getInvocation(format)
local invocation = self._invocation:getInvocation(self:getName())
local invocation = self._invocation:getInvocation{
template = self:getName(),
requireMagicWord = self.requireMagicWord,
}
if format == 'code' then
if format == 'code' then
invocation = '<code>' .. mw.text.nowiki(invocation) .. '</code>'
invocation = '<code>' .. mw.text.nowiki(invocation) .. '</code>'
elseif format == 'kbd' then
invocation = '<kbd>' .. mw.text.nowiki(invocation) .. '</kbd>'
elseif format == 'plain' then
elseif format == 'plain' then
invocation = mw.text.nowiki(invocation)
invocation = mw.text.nowiki(invocation)
Line 153: Line 160:


function Template:getOutput()
function Template:getOutput()
local protect = require('Module:Protect')
return self._invocation:getOutput(self:getName())
-- calling self._invocation:getOutput{...}
return protect(self._invocation.getOutput)(self._invocation, {
template = self:getName(),
requireMagicWord = self.requireMagicWord,
})
end
end


Line 169: Line 181:
columns = 'renderColumns',
columns = 'renderColumns',
rows = 'renderRows',
rows = 'renderRows',
tablerows = 'renderRows',
inline = 'renderInline',
inline = 'renderInline',
cells = 'renderCells',
default = 'renderDefault'
default = 'renderDefault'
}
}
Line 180: Line 194:
-- numbered, whereas general options are not.
-- numbered, whereas general options are not.
local generalOptions, templateOptions = {}, {}
local generalOptions, templateOptions = {}, {}
for k, v in pairs(options) do
do
local prefix, num
local optionNum = {} -- a unique key for option numbers inside templateOptions
if type(k) == 'string' then
local rawTemplateOptions = {}
prefix, num = k:match('^(.-)([1-9][0-9]*)$')
for k, v in pairs(options) do
local prefix, num
if type(k) == 'string' then
prefix, num = k:match('^(.-)([1-9][0-9]*)$')
end
if prefix then
num = tonumber(num)
rawTemplateOptions[num] = rawTemplateOptions[num] or {}
rawTemplateOptions[num][prefix] = v
rawTemplateOptions[num][optionNum] = num -- record for use in error messages
else
generalOptions[k] = v
end
end
end
if prefix then
num = tonumber(num)
templateOptions[num] = templateOptions[num] or {}
templateOptions[num][prefix] = v
else
generalOptions[k] = v
end
end


-- Set up first two template options tables, so that if only the
-- Set general options
generalOptions.showcode = yesno(generalOptions.showcode)
-- "template3" is specified it isn't made the first template when the
generalOptions.showheader = yesno(generalOptions.showheader) ~= false
-- the table options array is compressed.
generalOptions.showcaption = yesno(generalOptions.showcaption) ~= false
rawTemplateOptions[1] = rawTemplateOptions[1] or {}
generalOptions.collapsible = yesno(generalOptions.collapsible)
rawTemplateOptions[2] = rawTemplateOptions[2] or {}
generalOptions.notcollapsed = yesno(generalOptions.notcollapsed)
generalOptions.wantdiff = yesno(generalOptions.wantdiff)
obj.options = generalOptions


-- Preprocess template args
-- Allow the "template" option to override the "template1" option for
for num, t in pairs(templateOptions) do
-- backwards compatibility with [[Module:Testcase table]].
if t.showtemplate ~= nil then
rawTemplateOptions[1].template = generalOptions.template
t.showtemplate = yesno(t.showtemplate)
or rawTemplateOptions[1].template
end
end

-- Set up first two template options tables, so that if only the
-- "template3" is specified it isn't made the first template when the
-- the table options array is compressed.
templateOptions[1] = templateOptions[1] or {}
templateOptions[2] = templateOptions[2] or {}

-- Allow the "template" option to override the "template1" option for
-- backwards compatibility with [[Module:Testcase table]].
if generalOptions.template then
templateOptions[1].template = generalOptions.template
end

-- Add default template options
if templateOptions[1].template and not templateOptions[2].template then
templateOptions[2].template = templateOptions[1].template ..
'/' .. obj.cfg.sandboxSubpage
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(
obj.cfg.sandboxSubpage
)
end


-- Add default template options
-- Remove template options for any templates where the showtemplate
-- argument is false. This prevents any output for that template.
if rawTemplateOptions[1].template and not rawTemplateOptions[2].template then
for num, t in pairs(templateOptions) do
rawTemplateOptions[2].template = rawTemplateOptions[1].template ..
if t.showtemplate == false then
'/' .. obj.cfg.sandboxSubpage
templateOptions[num] = nil
end
end
end
if not rawTemplateOptions[1].template then

rawTemplateOptions[1].title = mw.title.getCurrentTitle().basePageTitle
-- Check for missing template names.
end
for num, t in pairs(templateOptions) do
if not rawTemplateOptions[2].template then
if not t.template and not t.title then
rawTemplateOptions[2].title = rawTemplateOptions[1].title:subPageTitle(
error(obj:message(
obj.cfg.sandboxSubpage
'missing-template-option-error',
)
num, num
), 2)
end
end
end


-- Compress templateOptions table so we can iterate over it with ipairs.
-- Remove gaps in the numbered options
templateOptions = (function (t)
local nums = {}
local nums = {}
for num in pairs(rawTemplateOptions) do
for num in pairs(t) do
nums[#nums + 1] = num
nums[#nums + 1] = num
end
end
table.sort(nums)
table.sort(nums)
local ret = {}
for i, num in ipairs(nums) do
for i, num in ipairs(nums) do
templateOptions[i] = rawTemplateOptions[num]
ret[i] = t[num]
end
end
return ret
end)(templateOptions)


-- Don't require the __TEMPLATENAME__ magic word for nowiki invocations if
-- Check that there are no missing template options.
-- there is only one template being output.
for i = 3, #templateOptions do -- Defaults have already been added for 1 and 2.
if #templateOptions <= 1 then
local t = templateOptions[i]
templateOptions[1].requireMagicWord = false
if not t.template then
local num = t[optionNum]
error(obj:message(
'missing-template-option-error',
num, num
), 2)
end
end
end
end


mw.logObject(templateOptions)
-- Set general options
generalOptions.showcode = yesno(generalOptions.showcode)
generalOptions.collapsible = yesno(generalOptions.collapsible)
obj.options = generalOptions


-- Make the template objects
-- Make the template objects
obj.templates = {}
obj.templates = {}
for i, t in ipairs(templateOptions) do
for i, options in ipairs(templateOptions) do
table.insert(obj.templates, Template.new(invocationObj, t))
table.insert(obj.templates, Template.new(invocationObj, options))
end

-- Add tracking categories. At the moment we are only tracking templates
-- that use any "heading" parameters or an "output" parameter.
obj.categories = {}
for k, v in pairs(options) do
if type(k) == 'string' and k:find('heading') then
obj.categories['Test cases using heading parameters'] = true
elseif k == 'output' then
obj.categories['Test cases using output parameter'] = true
end
end
end


Line 277: Line 327:
local out = obj:getOutput()
local out = obj:getOutput()
-- Remove the random parts from strip markers.
-- Remove the random parts from strip markers.
out = out:gsub('(%cUNIQ).-(QINU%c)', '%1%2')
out = out:gsub('(\127\'"`UNIQ.-)%-%x+%-(QINU`"\'\127)', '%1%2')
return out
return out
end
end
Line 291: Line 341:


function TestCase:makeCollapsible(s)
function TestCase:makeCollapsible(s)
local title = self.options.title or self.templates[1]:makeHeader()
if self.options.titlecode then
title = self.templates[1]:getInvocation('kbd')
end
local isEqual = self:templateOutputIsEqual()
local isEqual = self:templateOutputIsEqual()
local root = mw.html.create('table')
local root = mw.html.create('table')
if self.options.wantdiff then
root
root
:addClass('collapsible')
:addClass('mw-collapsible')
if self.options.notcollapsed == false then
:addClass(isEqual and 'collapsed' or nil)
root
:addClass('mw-collapsed')
end
root
:css('background-color', 'transparent')
:css('width', '100%')
:css('border', 'solid silver 1px')
:tag('tr')
:tag('th')
:css('background-color', isEqual and 'yellow' or '#90a8ee')
:wikitext(title)
:done()
:done()
:tag('tr')
:tag('td')
:newline()
:wikitext(s)
:newline()
else
root
:addClass('mw-collapsible')
if self.options.notcollapsed == false then
root
:addClass('mw-collapsed')
end
if self.options.notcollapsed ~= true or false then
root
:addClass(isEqual and 'mw-collapsed' or nil)
end
root
:css('background-color', 'transparent')
:css('background-color', 'transparent')
:css('width', '100%')
:css('width', '100%')
Line 302: Line 387:
:tag('th')
:tag('th')
:css('background-color', isEqual and 'lightgreen' or 'yellow')
:css('background-color', isEqual and 'lightgreen' or 'yellow')
:wikitext(self.options.title or self.templates[1]:makeHeading())
:wikitext(title)
:done()
:done()
:done()
:done()
Line 310: Line 395:
:wikitext(s)
:wikitext(s)
:newline()
:newline()
end
return tostring(root)
return tostring(root)
end
end
Line 322: Line 408:


local tableroot = root:tag('table')
local tableroot = root:tag('table')
tableroot
:addClass(self.options.class)
:cssText(self.options.style)
:tag('caption')
:wikitext(self.options.caption or self:message('columns-header'))


if self.options.showheader then
-- Headings
-- Caption
local headingRow = tableroot:tag('tr')
if self.options.rowheader then
if self.options.showcaption then
tableroot
-- rowheader is correct here. We need to add another th cell if
:addClass(self.options.class)
-- rowheader is set further down, even if heading0 is missing.
headingRow:tag('th'):wikitext(self.options.heading0)
:cssText(self.options.style)
:tag('caption')
end
:wikitext(self.options.caption or self:message('columns-header'))
local width
end
if #self.templates > 0 then

width = tostring(math.floor(100 / #self.templates)) .. '%'
-- Headers
else
local headerRow = tableroot:tag('tr')
width = '100%'
if self.options.rowheader then
end
-- rowheader is correct here. We need to add another th cell if
for i, obj in ipairs(self.templates) do
-- rowheader is set further down, even if heading0 is missing.
headingRow
:tag('th')
headerRow:tag('th'):wikitext(self.options.heading0)
end
:css('width', width)
local width
:wikitext(obj:makeHeading())
if #self.templates > 0 then
width = tostring(math.floor(100 / #self.templates)) .. '%'
else
width = '100%'
end
for i, obj in ipairs(self.templates) do
headerRow
:tag('th')
:css('width', width)
:wikitext(obj:makeHeader())
end
end
end


Line 358: Line 450:
-- Template output
-- Template output
for i, obj in ipairs(self.templates) do
for i, obj in ipairs(self.templates) do
if self.options.output == 'nowiki+' then
dataRow:tag('td')
:newline()
dataRow:tag('td')
:newline()
:wikitext(self:getTemplateOutput(obj))
:wikitext(self.options.after)
:wikitext(self.options.before)
:wikitext(self:getTemplateOutput(obj))
:wikitext(self.options.after)
:wikitext('<pre style="white-space: pre-wrap;">')
:wikitext(mw.text.nowiki(self.options.before or ""))
:wikitext(mw.text.nowiki(self:getTemplateOutput(obj)))
:wikitext(mw.text.nowiki(self.options.after or ""))
:wikitext('</pre>')
elseif self.options.output == 'nowiki' then
dataRow:tag('td')
:newline()
:wikitext(mw.text.nowiki(self.options.before or ""))
:wikitext(mw.text.nowiki(self:getTemplateOutput(obj)))
:wikitext(mw.text.nowiki(self.options.after or ""))
else
dataRow:tag('td')
:newline()
:wikitext(self.options.before)
:wikitext(self:getTemplateOutput(obj))
:wikitext(self.options.after)
end
end
end
Line 387: Line 499:


for _, obj in ipairs(self.templates) do
for _, obj in ipairs(self.templates) do
local dataRow = tableroot:tag('tr')
-- Build the row HTML
tableroot
-- Header
:tag('tr')
if self.options.showheader then
:tag('td')
if self.options.format == 'tablerows' then
dataRow:tag('th')
:attr('scope', 'row')
:css('vertical-align', 'top')
:css('text-align', 'left')
:wikitext(obj:makeHeader())
dataRow:tag('td')
:css('vertical-align', 'top')
:css('padding', '0 1em')
:wikitext('→')
else
dataRow:tag('td')
:css('text-align', 'center')
:css('text-align', 'center')
:css('font-weight', 'bold')
:css('font-weight', 'bold')
:wikitext(obj:makeHeading())
:wikitext(obj:makeHeader())
:done()
dataRow = tableroot:tag('tr')
:done()
end
end
:tag('tr')
:tag('td')
-- Template output
:newline()
if self.options.output == 'nowiki+' then
:wikitext(self:getTemplateOutput(obj))
dataRow:tag('td')
:newline()
:wikitext(self:getTemplateOutput(obj))
:wikitext('<pre style="white-space: pre-wrap;">')
:wikitext(mw.text.nowiki(self:getTemplateOutput(obj)))
:wikitext('</pre>')
elseif self.options.output == 'nowiki' then
dataRow:tag('td')
:newline()
:wikitext(mw.text.nowiki(self:getTemplateOutput(obj)))
else
dataRow:tag('td')
:newline()
:wikitext(self:getTemplateOutput(obj))
end
end
end


Line 410: Line 549:
for i, obj in ipairs(self.templates) do
for i, obj in ipairs(self.templates) do
local line = {}
local line = {}
line[#line + 1] = '* '
line[#line + 1] = self.options.prefix or '* '
if self.options.showcode then
if self.options.showcode then
line[#line + 1] = obj:getInvocation('code')
line[#line + 1] = obj:getInvocation('code')
Line 417: Line 556:
line[#line + 1] = ' '
line[#line + 1] = ' '
end
end
if self.options.output == 'nowiki+' then
line[#line + 1] = self:getTemplateOutput(obj)
line[#line + 1] = self:getTemplateOutput(obj)
line[#line + 1] = '<pre style="white-space: pre-wrap;">'
line[#line + 1] = mw.text.nowiki(self:getTemplateOutput(obj))
line[#line + 1] = '</pre>'
elseif self.options.output == 'nowiki' then
line[#line + 1] = mw.text.nowiki(self:getTemplateOutput(obj))
else
line[#line + 1] = self:getTemplateOutput(obj)
end
ret[#ret + 1] = table.concat(line)
end
if self.options.addline then
local line = {}
line[#line + 1] = self.options.prefix or '* '
line[#line + 1] = self.options.addline
ret[#ret + 1] = table.concat(line)
ret[#ret + 1] = table.concat(line)
end
end
return table.concat(ret, '\n')
return table.concat(ret, '\n')
end

function TestCase:renderCells()
local root = mw.html.create()
local dataRow = root:tag('tr')
dataRow
:css('vertical-align', 'top')
:addClass(self.options.class)
:cssText(self.options.style)

-- Row header
if self.options.rowheader then
dataRow:tag('th')
:attr('scope', 'row')
:newline()
:wikitext(self.options.rowheader or self:message('row-header'))
end
-- Caption
if self.options.showcaption then
dataRow:tag('th')
:attr('scope', 'row')
:newline()
:wikitext(self.options.caption or self:message('columns-header'))
end

-- Show code
if self.options.showcode then
dataRow:tag('td')
:newline()
:wikitext(self:getInvocation('code'))
end

-- Template output
for i, obj in ipairs(self.templates) do
if self.options.output == 'nowiki+' then
dataRow:tag('td')
:newline()
:wikitext(self.options.before)
:wikitext(self:getTemplateOutput(obj))
:wikitext(self.options.after)
:wikitext('<pre style="white-space: pre-wrap;">')
:wikitext(mw.text.nowiki(self.options.before or ""))
:wikitext(mw.text.nowiki(self:getTemplateOutput(obj)))
:wikitext(mw.text.nowiki(self.options.after or ""))
:wikitext('</pre>')
elseif self.options.output == 'nowiki' then
dataRow:tag('td')
:newline()
:wikitext(mw.text.nowiki(self.options.before or ""))
:wikitext(mw.text.nowiki(self:getTemplateOutput(obj)))
:wikitext(mw.text.nowiki(self.options.after or ""))
else
dataRow:tag('td')
:newline()
:wikitext(self.options.before)
:wikitext(self:getTemplateOutput(obj))
:wikitext(self.options.after)
end
end


return tostring(root)
end
end


Line 430: Line 646:
for i, obj in ipairs(self.templates) do
for i, obj in ipairs(self.templates) do
ret[#ret + 1] = '<div style="clear: both;"></div>'
ret[#ret + 1] = '<div style="clear: both;"></div>'
if self.options.showheader then
ret[#ret + 1] = obj:makeHeading()
ret[#ret + 1] = self:getTemplateOutput(obj)
ret[#ret + 1] = obj:makeHeader()
end
if self.options.output == 'nowiki+' then
ret[#ret + 1] = self:getTemplateOutput(obj) .. '<pre style="white-space: pre-wrap;">' .. mw.text.nowiki(self:getTemplateOutput(obj)) .. '</pre>'
elseif self.options.output == 'nowiki' then
ret[#ret + 1] = mw.text.nowiki(self:getTemplateOutput(obj))
else
ret[#ret + 1] = self:getTemplateOutput(obj)
end
end
end
return table.concat(ret, '\n\n')
return table.concat(ret, '\n\n')
Line 442: Line 666:
if self.options.collapsible then
if self.options.collapsible then
ret = self:makeCollapsible(ret)
ret = self:makeCollapsible(ret)
end
for cat in pairs(self.categories) do
ret = ret .. string.format('[[Category:%s]]', cat)
end
end
return ret
return ret
Line 469: Line 696:
end
end


function NowikiInvocation:getInvocation(template)
function NowikiInvocation:getInvocation(options)
template = template:gsub('%%', '%%%%') -- Escape "%" with "%%"
local template = options.template:gsub('%%', '%%%%') -- Escape "%" with "%%"
local invocation, count = self.invocation:gsub(
local invocation, count = self.invocation:gsub(
self.cfg.templateNameMagicWordPattern,
self.cfg.templateNameMagicWordPattern,
template
template
)
)
if count < 1 then
if options.requireMagicWord ~= false and count < 1 then
error(self:message(
error(self:message(
'nowiki-magic-word-error',
'nowiki-magic-word-error',
Line 484: Line 711:
end
end


function NowikiInvocation:getOutput(template)
function NowikiInvocation:getOutput(options)
local invocation = self:getInvocation(template)
local invocation = self:getInvocation(options)
return mw.getCurrentFrame():preprocess(invocation)
return mw.getCurrentFrame():preprocess(invocation)
end
end
Line 505: Line 732:
end
end


function TableInvocation:getInvocation(template)
function TableInvocation:getInvocation(options)
if self.code then
if self.code then
local nowikiObj = NowikiInvocation.new(self.code, self.cfg)
local nowikiObj = NowikiInvocation.new(self.code, self.cfg)
return nowikiObj:getInvocation(template)
return nowikiObj:getInvocation(options)
else
else
return require('Module:Template invocation').invocation(
return require('Module:Template invocation').invocation(
template,
options.template,
self.invokeArgs
self.invokeArgs
)
)
Line 517: Line 744:
end
end


function TableInvocation:getOutput(template)
function TableInvocation:getOutput(options)
if (options.template:sub(1, 7) == '#invoke') then
local moduleCall = mw.text.split(options.template, '|', true)
local args = mw.clone(self.invokeArgs)
table.insert(args, 1, moduleCall[2])
return mw.getCurrentFrame():callParserFunction(moduleCall[1], args)
end
return mw.getCurrentFrame():expandTemplate{
return mw.getCurrentFrame():expandTemplate{
title = template,
title = options.template,
args = self.invokeArgs
args = self.invokeArgs
}
}

Latest revision as of 23:24, 7 June 2021

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

--[[
   A module for generating test case templates.

   This module incorporates code from the English Wikipedia's "Testcase table"
   module,[1] written by Frietjes [2] with contributions by Mr. Stradivarius [3]
   and Jackmcbarn,[4] and the English Wikipedia's "Testcase rows" module,[5]
   written by Mr. Stradivarius.

   The "Testcase table" and "Testcase rows" modules are released under the
   CC BY-SA 3.0 License [6] and the GFDL.[7]

   License: CC BY-SA 3.0 and the GFDL
   Author: Mr. Stradivarius

   [1] https://en.wikipedia.org/wiki/Module:Testcase_table
   [2] https://en.wikipedia.org/wiki/User:Frietjes
   [3] https://en.wikipedia.org/wiki/User:Mr._Stradivarius
   [4] https://en.wikipedia.org/wiki/User:Jackmcbarn
   [5] https://en.wikipedia.org/wiki/Module:Testcase_rows
   [6] https://en.wikipedia.org/wiki/Wikipedia:Text_of_Creative_Commons_Attribution-ShareAlike_3.0_Unported_License
   [7] https://en.wikipedia.org/wiki/Wikipedia:Text_of_the_GNU_Free_Documentation_License
]]

-- Load required modules
local yesno = require('Module:Yesno')

-- Set constants
local DATA_MODULE = 'Module:Template test case/data'

-------------------------------------------------------------------------------
-- Shared methods
-------------------------------------------------------------------------------

local function message(self, key, ...)
	-- This method is added to classes that need to deal with messages from the
	-- config module.
	local msg = self.cfg.msg[key]
	if select(1, ...) then
		return mw.message.newRawMessage(msg, ...):plain()
	else
		return msg
	end
end

-------------------------------------------------------------------------------
-- Template class
-------------------------------------------------------------------------------

local Template = {}

Template.memoizedMethods = {
	-- Names of methods to be memoized in each object. This table should only
	-- hold methods with no parameters.
	getFullPage = true,
	getName = true,
	makeHeader = true,
	getOutput = true
}

function Template.new(invocationObj, options)
	local obj = {}

	-- 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

	-- Memoize expensive method calls
	local memoFuncs = {}
	return setmetatable(obj, {
		__index = function (t, key)
			if Template.memoizedMethods[key] then
				local func = memoFuncs[key]
				if not func then
					local val = Template[key](t)
					func = function () return val end
					memoFuncs[key] = func
				end
				return func
			else
				return Template[key]
			end
		end
	})
end

function Template:getFullPage()
	if not self.template then
		return self.title.prefixedText
	elseif self.template:sub(1, 7) == '#invoke' then
		return 'Module' .. self.template:sub(8):gsub('|.*', '')
	else
		local strippedTemplate, hasColon = self.template:gsub('^:', '', 1)
		hasColon = hasColon > 0
		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
	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:makeHeader()
	return self.heading or self:makeBraceLink()
end

function Template:getInvocation(format)
	local invocation = self._invocation:getInvocation{
		template = self:getName(),
		requireMagicWord = self.requireMagicWord,
	}
	if format == 'code' then
		invocation = '<code>' .. mw.text.nowiki(invocation) .. '</code>'
	elseif format == 'kbd' then
		invocation = '<kbd>' .. mw.text.nowiki(invocation) .. '</kbd>'
	elseif format == 'plain' then
		invocation = mw.text.nowiki(invocation)
	else
		-- Default is pre tags
		invocation = mw.text.encode(invocation, '&')
		invocation = '<pre style="white-space: pre-wrap;">' .. invocation .. '</pre>'
		invocation = mw.getCurrentFrame():preprocess(invocation)
	end
	return invocation
end

function Template:getOutput()
	local protect = require('Module:Protect')
	-- calling self._invocation:getOutput{...}
	return protect(self._invocation.getOutput)(self._invocation, {
		template = self:getName(),
		requireMagicWord = self.requireMagicWord,
	})
end

-------------------------------------------------------------------------------
-- TestCase class
-------------------------------------------------------------------------------

local TestCase = {}
TestCase.__index = TestCase
TestCase.message = message -- add the message method

TestCase.renderMethods = {
	-- Keys in this table are values of the "format" option, values are the
	-- method for rendering that format.
	columns = 'renderColumns',
	rows = 'renderRows',
	tablerows = 'renderRows',
	inline = 'renderInline',
	cells = 'renderCells',
	default = 'renderDefault'
}

function TestCase.new(invocationObj, options, cfg)
	local obj = setmetatable({}, TestCase)
	obj.cfg = cfg

	-- Separate general options from template options. Template options are
	-- numbered, whereas general options are not.
	local generalOptions, templateOptions = {}, {}
	for k, v in pairs(options) do
		local prefix, num
		if type(k) == 'string' then
			prefix, num = k:match('^(.-)([1-9][0-9]*)$')
		end
		if prefix then
			num = tonumber(num)
			templateOptions[num] = templateOptions[num] or {}
			templateOptions[num][prefix] = v
		else
			generalOptions[k] = v
		end
	end

	-- Set general options
	generalOptions.showcode = yesno(generalOptions.showcode)
	generalOptions.showheader = yesno(generalOptions.showheader) ~= false
	generalOptions.showcaption = yesno(generalOptions.showcaption) ~= false
	generalOptions.collapsible = yesno(generalOptions.collapsible)
	generalOptions.notcollapsed = yesno(generalOptions.notcollapsed)
	generalOptions.wantdiff = yesno(generalOptions.wantdiff) 
	obj.options = generalOptions

	-- Preprocess template args
	for num, t in pairs(templateOptions) do
		if t.showtemplate ~= nil then
			t.showtemplate = yesno(t.showtemplate)
		end
	end

	-- Set up first two template options tables, so that if only the
	-- "template3" is specified it isn't made the first template when the
	-- the table options array is compressed.
	templateOptions[1] = templateOptions[1] or {}
	templateOptions[2] = templateOptions[2] or {}

	-- Allow the "template" option to override the "template1" option for
	-- backwards compatibility with [[Module:Testcase table]].
	if generalOptions.template then
		templateOptions[1].template = generalOptions.template
	end

	-- Add default template options
	if templateOptions[1].template and not templateOptions[2].template then
		templateOptions[2].template = templateOptions[1].template ..
			'/' .. obj.cfg.sandboxSubpage
	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(
			obj.cfg.sandboxSubpage
		)
	end

	-- Remove template options for any templates where the showtemplate
	-- argument is false. This prevents any output for that template.
	for num, t in pairs(templateOptions) do
		if t.showtemplate == false then
			templateOptions[num] = nil
		end
	end

	-- Check for missing template names.
	for num, t in pairs(templateOptions) do
		if not t.template and not t.title then
			error(obj:message(
				'missing-template-option-error',
				num, num
			), 2)
		end
	end

	-- Compress templateOptions table so we can iterate over it with ipairs.
	templateOptions = (function (t)
		local nums = {}
		for num in pairs(t) do
			nums[#nums + 1] = num
		end
		table.sort(nums)
		local ret = {}
		for i, num in ipairs(nums) do
			ret[i] = t[num]
		end
		return ret
	end)(templateOptions)

	-- Don't require the __TEMPLATENAME__ magic word for nowiki invocations if
	-- there is only one template being output.
	if #templateOptions <= 1 then
		templateOptions[1].requireMagicWord = false
	end

	mw.logObject(templateOptions)

	-- Make the template objects
	obj.templates = {}
	for i, options in ipairs(templateOptions) do
		table.insert(obj.templates, Template.new(invocationObj, options))
	end

	-- Add tracking categories. At the moment we are only tracking templates
	-- that use any "heading" parameters or an "output" parameter.
	obj.categories = {}
	for k, v in pairs(options) do
		if type(k) == 'string' and k:find('heading') then
			obj.categories['Test cases using heading parameters'] = true
		elseif k == 'output' then
			obj.categories['Test cases using output parameter'] = true
		end
	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:templateOutputIsEqual()
	-- Returns a boolean showing whether all of the template outputs are equal.
	-- The random parts of strip markers (see [[Help:Strip markers]]) are
	-- removed before comparison. This means a strip marker can contain anything
	-- and still be treated as equal, but it solves the problem of otherwise
	-- identical wikitext not returning as exactly equal.
	local function normaliseOutput(obj)
		local out = obj:getOutput()
		-- Remove the random parts from strip markers.
		out = out:gsub('(\127\'"`UNIQ.-)%-%x+%-(QINU`"\'\127)', '%1%2')
		return out
	end
	local firstOutput = normaliseOutput(self.templates[1])
	for i = 2, #self.templates do
		local output = normaliseOutput(self.templates[i])
		if output ~= firstOutput then
			return false
		end
	end
	return true
end

function TestCase:makeCollapsible(s)
	local title = self.options.title or self.templates[1]:makeHeader()
	if self.options.titlecode then
		title = self.templates[1]:getInvocation('kbd')
	end
	local isEqual = self:templateOutputIsEqual()
	local root = mw.html.create('table')
	if self.options.wantdiff then
	root
		:addClass('mw-collapsible')
	if self.options.notcollapsed == false then
		root
			:addClass('mw-collapsed')
	end
	root
		:css('background-color', 'transparent')
		:css('width', '100%')
		:css('border', 'solid silver 1px')
		:tag('tr')
			:tag('th')
				:css('background-color', isEqual and 'yellow' or '#90a8ee')
				:wikitext(title)
				:done()
			:done()
		:tag('tr')
			:tag('td')
				:newline()
				:wikitext(s)
				:newline()
	else
		root
		:addClass('mw-collapsible')
		if self.options.notcollapsed == false then
			root
				:addClass('mw-collapsed')
		end
		if self.options.notcollapsed ~= true or false then
			root
				:addClass(isEqual and 'mw-collapsed' or nil)
		end
		root
		:css('background-color', 'transparent')
		:css('width', '100%')
		:css('border', 'solid silver 1px')
		:tag('tr')
			:tag('th')
				:css('background-color', isEqual and 'lightgreen' or 'yellow')
				:wikitext(title)
				:done()
			:done()
		:tag('tr')
			:tag('td')
				:newline()
				:wikitext(s)
				:newline()
	 end
	return tostring(root)
end

function TestCase:renderColumns()
	local root = mw.html.create()
	if self.options.showcode then
		root
			:wikitext(self.templates[1]:getInvocation())
			:newline()
	end

	local tableroot = root:tag('table')

	if self.options.showheader then
		-- Caption
		if self.options.showcaption then
			tableroot
				:addClass(self.options.class)
				:cssText(self.options.style)
				:tag('caption')
					:wikitext(self.options.caption or self:message('columns-header'))
		end

		-- Headers
		local headerRow = tableroot:tag('tr')
		if self.options.rowheader then
			-- rowheader is correct here. We need to add another th cell if
			-- rowheader is set further down, even if heading0 is missing.
			headerRow:tag('th'):wikitext(self.options.heading0)
		end
		local width
		if #self.templates > 0 then
			width = tostring(math.floor(100 / #self.templates)) .. '%'
		else
			width = '100%'
		end
		for i, obj in ipairs(self.templates) do
			headerRow
				:tag('th')
					:css('width', width)
					:wikitext(obj:makeHeader())
		end
	end

	-- Row header
	local dataRow = tableroot: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
		if self.options.output == 'nowiki+' then
			dataRow:tag('td')
				:newline()
				:wikitext(self.options.before)
				:wikitext(self:getTemplateOutput(obj))
				:wikitext(self.options.after)
				:wikitext('<pre style="white-space: pre-wrap;">')
				:wikitext(mw.text.nowiki(self.options.before or ""))
				:wikitext(mw.text.nowiki(self:getTemplateOutput(obj)))
				:wikitext(mw.text.nowiki(self.options.after or ""))
				:wikitext('</pre>')
		elseif self.options.output == 'nowiki' then
			dataRow:tag('td')
				:newline()
				:wikitext(mw.text.nowiki(self.options.before or ""))
				:wikitext(mw.text.nowiki(self:getTemplateOutput(obj)))
				:wikitext(mw.text.nowiki(self.options.after or ""))
		else
			dataRow:tag('td')
				:newline()
				:wikitext(self.options.before)
				:wikitext(self:getTemplateOutput(obj))
				:wikitext(self.options.after)
		end
	end
	
	return tostring(root)
end

function TestCase:renderRows()
	local root = mw.html.create()
	if self.options.showcode then
		root
			:wikitext(self.templates[1]:getInvocation())
			:newline()
	end

	local tableroot = root:tag('table')
	tableroot
		:addClass(self.options.class)
		:cssText(self.options.style)

	if self.options.caption then
		tableroot
			:tag('caption')
				:wikitext(self.options.caption)
	end

	for _, obj in ipairs(self.templates) do
		local dataRow = tableroot:tag('tr')
		
		-- Header
		if self.options.showheader then
			if self.options.format == 'tablerows' then
				dataRow:tag('th')
					:attr('scope', 'row')
					:css('vertical-align', 'top')
					:css('text-align', 'left')
					:wikitext(obj:makeHeader())
				dataRow:tag('td')
					:css('vertical-align', 'top')
					:css('padding', '0 1em')
					:wikitext('→')
			else
				dataRow:tag('td')
					:css('text-align', 'center')
					:css('font-weight', 'bold')
					:wikitext(obj:makeHeader())
				dataRow = tableroot:tag('tr')
			end
		end
		
		-- Template output
		if self.options.output == 'nowiki+' then
			dataRow:tag('td')
				:newline()
				:wikitext(self:getTemplateOutput(obj))
				:wikitext('<pre style="white-space: pre-wrap;">')
				:wikitext(mw.text.nowiki(self:getTemplateOutput(obj)))
				:wikitext('</pre>')
		elseif self.options.output == 'nowiki' then
			dataRow:tag('td')
				:newline()
				:wikitext(mw.text.nowiki(self:getTemplateOutput(obj)))
		else
			dataRow:tag('td')
				:newline()
				:wikitext(self:getTemplateOutput(obj))
		end
	end

	return tostring(root)
end

function TestCase:renderInline()
	local arrow = mw.language.getContentLanguage():getArrow('forwards')
	local ret = {}
	for i, obj in ipairs(self.templates) do
		local line = {}
		line[#line + 1] = self.options.prefix or '* '
		if self.options.showcode then
			line[#line + 1] = obj:getInvocation('code')
			line[#line + 1] = ' '
			line[#line + 1] = arrow
			line[#line + 1] = ' '
		end
		if self.options.output == 'nowiki+' then
			line[#line + 1] = self:getTemplateOutput(obj)
			line[#line + 1] = '<pre style="white-space: pre-wrap;">'
			line[#line + 1] = mw.text.nowiki(self:getTemplateOutput(obj))
			line[#line + 1] = '</pre>'
		elseif self.options.output == 'nowiki' then
			line[#line + 1] = mw.text.nowiki(self:getTemplateOutput(obj))
		else
			line[#line + 1] = self:getTemplateOutput(obj)
		end
		ret[#ret + 1] = table.concat(line)
	end
	if self.options.addline then
		local line = {}
		line[#line + 1] = self.options.prefix or '* '
		line[#line + 1] = self.options.addline
		ret[#ret + 1] = table.concat(line)
	end
	return table.concat(ret, '\n')
end

function TestCase:renderCells()
	local root = mw.html.create()
	local dataRow = root:tag('tr')
	dataRow
		:css('vertical-align', 'top')
		:addClass(self.options.class)
		:cssText(self.options.style)

	-- Row header
	if self.options.rowheader then
		dataRow:tag('th')
			:attr('scope', 'row')
			:newline()
			:wikitext(self.options.rowheader or self:message('row-header'))
	end
	-- Caption
	if self.options.showcaption then
		dataRow:tag('th')
			:attr('scope', 'row')
			:newline()
			:wikitext(self.options.caption or self:message('columns-header'))
	end

	-- Show code
	if self.options.showcode then
		dataRow:tag('td')
			:newline()
			:wikitext(self:getInvocation('code'))
	end

	-- Template output
	for i, obj in ipairs(self.templates) do
		if self.options.output == 'nowiki+' then
			dataRow:tag('td')
				:newline()
				:wikitext(self.options.before)
				:wikitext(self:getTemplateOutput(obj))
				:wikitext(self.options.after)
				:wikitext('<pre style="white-space: pre-wrap;">')
				:wikitext(mw.text.nowiki(self.options.before or ""))
				:wikitext(mw.text.nowiki(self:getTemplateOutput(obj)))
				:wikitext(mw.text.nowiki(self.options.after or ""))
				:wikitext('</pre>')
		elseif self.options.output == 'nowiki' then
			dataRow:tag('td')
				:newline()
				:wikitext(mw.text.nowiki(self.options.before or ""))
				:wikitext(mw.text.nowiki(self:getTemplateOutput(obj)))
				:wikitext(mw.text.nowiki(self.options.after or ""))
		else
			dataRow:tag('td')
				:newline()
				:wikitext(self.options.before)
				:wikitext(self:getTemplateOutput(obj))
				:wikitext(self.options.after)
		end
	end


	return tostring(root)
end

function TestCase:renderDefault()
	local ret = {}
	if self.options.showcode then
		ret[#ret + 1] = self.templates[1]:getInvocation()
	end
	for i, obj in ipairs(self.templates) do
		ret[#ret + 1] = '<div style="clear: both;"></div>'
		if self.options.showheader then
			ret[#ret + 1] = obj:makeHeader()
		end
		if self.options.output == 'nowiki+' then
			ret[#ret + 1] = self:getTemplateOutput(obj) .. '<pre style="white-space: pre-wrap;">' .. mw.text.nowiki(self:getTemplateOutput(obj)) .. '</pre>'
		elseif self.options.output == 'nowiki' then
			ret[#ret + 1] = mw.text.nowiki(self:getTemplateOutput(obj))
		else
			ret[#ret + 1] = self:getTemplateOutput(obj)
		end
	end
	return table.concat(ret, '\n\n')
end

function TestCase:__tostring()
	local format = self.options.format
	local method = format and TestCase.renderMethods[format] or 'renderDefault'
	local ret = self[method](self)
	if self.options.collapsible then
		ret = self:makeCollapsible(ret)
	end
	for cat in pairs(self.categories) do
		ret = ret .. string.format('[[Category:%s]]', cat)
	end
	return ret
end

-------------------------------------------------------------------------------
-- Nowiki invocation class
-------------------------------------------------------------------------------

local NowikiInvocation = {}
NowikiInvocation.__index = NowikiInvocation
NowikiInvocation.message = message -- Add the message method

function NowikiInvocation.new(invocation, cfg)
	local obj = setmetatable({}, NowikiInvocation)
	obj.cfg = cfg
	invocation = mw.text.unstrip(invocation)
	-- Decode HTML entities for <, >, and ". This means that HTML entities in
	-- the original code must be escaped as e.g. &amp;lt;, which is unfortunate,
	-- but it is the best we can do as the distinction between <, >, " and &lt;,
	-- &gt;, &quot; is lost during the original nowiki operation.
	invocation = invocation:gsub('&lt;', '<')
	invocation = invocation:gsub('&gt;', '>')
	invocation = invocation:gsub('&quot;', '"')
	obj.invocation = invocation
	return obj
end

function NowikiInvocation:getInvocation(options)
	local template = options.template:gsub('%%', '%%%%') -- Escape "%" with "%%"
	local invocation, count = self.invocation:gsub(
		self.cfg.templateNameMagicWordPattern,
		template
	)
	if options.requireMagicWord ~= false and count < 1 then
		error(self:message(
			'nowiki-magic-word-error',
			self.cfg.templateNameMagicWord
		))
	end
	return invocation
end

function NowikiInvocation:getOutput(options)
	local invocation = self:getInvocation(options)
	return mw.getCurrentFrame():preprocess(invocation)
end

-------------------------------------------------------------------------------
-- Table invocation class
-------------------------------------------------------------------------------

local TableInvocation = {}
TableInvocation.__index = TableInvocation
TableInvocation.message = message -- Add the message method

function TableInvocation.new(invokeArgs, nowikiCode, cfg)
	local obj = setmetatable({}, TableInvocation)
	obj.cfg = cfg
	obj.invokeArgs = invokeArgs
	obj.code = nowikiCode
	return obj
end

function TableInvocation:getInvocation(options)
	if self.code then
		local nowikiObj = NowikiInvocation.new(self.code, self.cfg)
		return nowikiObj:getInvocation(options)
	else
		return require('Module:Template invocation').invocation(
			options.template,
			self.invokeArgs
		)
	end
end

function TableInvocation:getOutput(options)
	if (options.template:sub(1, 7) == '#invoke') then
		local moduleCall = mw.text.split(options.template, '|', true)
		local args = mw.clone(self.invokeArgs)
		table.insert(args, 1, moduleCall[2])
		return mw.getCurrentFrame():callParserFunction(moduleCall[1], args)
	end
	return mw.getCurrentFrame():expandTemplate{
		title = options.template,
		args = self.invokeArgs
	}
end

-------------------------------------------------------------------------------
-- Bridge functions
--
-- These functions translate template arguments into forms that can be accepted
-- by the different classes, and return the results.
-------------------------------------------------------------------------------

local bridge = {}

function bridge.table(args, cfg)
	cfg = cfg or mw.loadData(DATA_MODULE)

	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

	-- Allow passing a nowiki invocation as an option. While this means users
	-- have to pass in the code twice, whitespace is preserved and &lt; etc.
	-- will work as intended.
	local nowikiCode = options.code
	options.code = nil

	local invocationObj = TableInvocation.new(invokeArgs, nowikiCode, cfg)
	local testCaseObj = TestCase.new(invocationObj, options, cfg)
	return tostring(testCaseObj)
end

function bridge.nowiki(args, cfg)
	cfg = cfg or mw.loadData(DATA_MODULE)

	local code = args.code or args[1]
	local invocationObj = NowikiInvocation.new(code, cfg)
	args.code = nil
	args[1] = nil
	-- Assume we want to see the code as we already passed it in.
	args.showcode = args.showcode or true
	local testCaseObj = TestCase.new(invocationObj, args, cfg)
	return tostring(testCaseObj)
end

-------------------------------------------------------------------------------
-- Exports
-------------------------------------------------------------------------------

local p = {}

function p.main(frame, cfg)
	cfg = cfg or mw.loadData(DATA_MODULE)

	-- Load the wrapper config, if any.
	local wrapperConfig
	if frame.getParent then
		local title = frame:getParent():getTitle()
		local template = title:gsub(cfg.sandboxSubpagePattern, '')
		wrapperConfig = cfg.wrappers[template]
	end

	-- Work out the function we will call, use it to generate the config for
	-- Module:Arguments, and use Module:Arguments to find the arguments passed
	-- by the user.
	local func = wrapperConfig and wrapperConfig.func or 'table'
	local userArgs = require('Module:Arguments').getArgs(frame, {
		parentOnly = wrapperConfig,
		frameOnly = not wrapperConfig,
		trim = func ~= 'table',
		removeBlanks = func ~= 'table'
	})

	-- Get default args and build the args table. User-specified args overwrite
	-- default args.
	local defaultArgs = wrapperConfig and wrapperConfig.args or {}
	local args = {}
	for k, v in pairs(defaultArgs) do
		args[k] = v
	end
	for k, v in pairs(userArgs) do
		args[k] = v
	end

	return bridge[func](args, cfg)
end

function p._exportClasses() -- For testing
	return {
		Template = Template,
		TestCase = TestCase,
		NowikiInvocation = NowikiInvocation,
		TableInvocation = TableInvocation
	}
end

return p