--[[
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', 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('')
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>' .. 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 = {}