Module:Template test case: Difference between revisions
(add a shared message method) |
(add more messages and use the sandboxSubpage config value to generate sandbox template names) |
||
Line 170: | Line 170: | ||
for i = 3, highestNum do | for i = 3, highestNum do | ||
if not options['template' .. i] then | if not options['template' .. i] then | ||
error( | error(self:message( | ||
'missing-template-option-error', | |||
i, i | i, i | ||
), 2) | ), 2) | ||
Line 191: | Line 190: | ||
templateOptions[2] = templateOptions[2] or {} | templateOptions[2] = templateOptions[2] or {} | ||
if templateOptions[1].template and not templateOptions[2].template then | if templateOptions[1].template and not templateOptions[2].template then | ||
templateOptions[2].template = templateOptions[1].template .. '/ | templateOptions[2].template = templateOptions[1].template | ||
.. '/' .. obj.cfg.sandboxSubpage | |||
end | end | ||
if not templateOptions[1].template then | if not templateOptions[1].template then | ||
Line 197: | Line 197: | ||
end | end | ||
if not templateOptions[2].template then | if not templateOptions[2].template then | ||
templateOptions[2].title = templateOptions[1].title:subPageTitle( | templateOptions[2].title = templateOptions[1].title:subPageTitle( | ||
obj.cfg.sandboxSubpage | |||
) | |||
end | end | ||
Line 273: | Line 275: | ||
:cssText(self.options.style) | :cssText(self.options.style) | ||
:tag('caption') | :tag('caption') | ||
:wikitext(self.options.caption or ' | :wikitext(self.options.caption or self:message('columns-header')) | ||
-- Headings | -- Headings |
Revision as of 01:43, 28 November 2014
Documentation for this module may be created at Module:Template test case/doc
-- This module provides several methods to generate test cases. -- Load required modules local yesno = require('Module:Yesno') local mTableTools = require('Module:TableTools') -- 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, makeHeading = 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 self.template then 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 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()) if format == 'code' then invocation = '<code>' .. mw.text.nowiki(invocation) .. '</code>' 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() return self._invocation:getOutput(self:getName()) 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', default = 'renderDefault' } function TestCase.new(invocationObj, options, cfg) local obj = setmetatable({}, TestCase) obj.cfg = cfg -- 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(self:message( 'missing-template-option-error', 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 {} -- Normalize boolean options obj.options.showcode = yesno(obj.options.showcode) obj.options.collapsible = yesno(obj.options.collapsible) -- 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 .. '/' .. 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 -- 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: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('(%cUNIQ).-(QINU%c)', '%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 isEqual = self:templateOutputIsEqual() local root = mw.html.create('table') root :addClass('collapsible') :addClass(isEqual and 'collapsed' or nil) :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(self.options.title or self.templates[1]:makeHeading()) :done() :done() :tag('tr') :tag('td') :wikitext(s) 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') tableroot :addClass(self.options.class) :cssText(self.options.style) :tag('caption') :wikitext(self.options.caption or self:message('columns-header')) -- Headings local headingRow = 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. headingRow: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 headingRow :tag('th') :css('width', width) :wikitext(obj:makeHeading()) 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 dataRow:tag('td') :newline() :wikitext(self:getTemplateOutput(obj)) :wikitext(self.options.after) 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 -- Build the row HTML tableroot :tag('tr') :tag('td') :css('text-align', 'center') :css('font-weight', 'bold') :wikitext(obj:makeHeading()) :done() :done() :tag('tr') :tag('td') :newline() :wikitext(self:getTemplateOutput(obj)) 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>' ret[#ret + 1] = obj:makeBraceLink() ret[#ret + 1] = self:getTemplateOutput(obj) 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 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. &lt;, which is unfortunate, -- but it is the best we can do as the distinction between <, >, " and <, -- >, " is lost during the original nowiki operation. invocation = invocation:gsub('<', '<') invocation = invocation:gsub('>', '>') invocation = invocation:gsub('"', '"') obj.invocation = invocation return obj end function NowikiInvocation:getInvocation(template) template = template:gsub('%%', '%%%%') -- Escape "%" with "%%" local invocation, count = self.invocation:gsub( self.cfg.templateNameMagicWordPattern, template ) if count < 1 then error(self:message( 'nowiki-magic-word-error', self.cfg.templateNameMagicWord )) 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 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(template) if self.code then local nowikiObj = NowikiInvocation(self.code, self.cfg) return nowikiObj:getInvocation(template) else return require('Module:Template invocation').invocation( template, self.invokeArgs ) end end function TableInvocation:getOutput(template) return mw.getCurrentFrame():expandTemplate{ title = template, args = self.invokeArgs } end ------------------------------------------------------------------------------- -- Exports ------------------------------------------------------------------------------- local p = {} function p.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 < 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 p.nowiki(args, cfg) cfg = cfg or mw.loadData(DATA_MODULE) local invocationObj = NowikiInvocation.new(args.code, cfg) args.code = 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 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 p[func](args, cfg) end function p._exportClasses() -- For testing return { Template = Template, TestCase = TestCase, NowikiInvocation = NowikiInvocation, TableInvocation = TableInvocation } end return p