Module:Template test case: Difference between revisions
(add TestCase:renderColumns and some more supporting methods) |
m (87 revisions imported from wikipedia:Module:Template_test_case) |
||
(73 intermediate revisions by 11 users not shown) | |||
Line 1: | Line 1: | ||
-- | --[[ | ||
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. | |||
local | The "Testcase table" and "Testcase rows" modules are released under the | ||
local | 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 | |||
------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ||
Line 13: | Line 48: | ||
local Template = {} | local Template = {} | ||
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) | function Template.new(invocationObj, options) | ||
local obj = | local obj = {} | ||
-- Set input | -- Set input | ||
Line 24: | Line 67: | ||
end | end | ||
end | end | ||
obj. | obj._invocation = invocationObj | ||
-- Validate input | -- Validate input | ||
Line 31: | Line 74: | ||
end | end | ||
return obj | -- 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 | end | ||
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 | |||
local ns = strippedTemplate:match('^(.-):') | local ns = strippedTemplate:match('^(.-):') | ||
ns = ns and mw.site.namespaces[ns] | ns = ns and mw.site.namespaces[ns] | ||
Line 46: | Line 110: | ||
return mw.site.namespaces[10].name .. ':' .. strippedTemplate | return mw.site.namespaces[10].name .. ':' .. strippedTemplate | ||
end | end | ||
end | end | ||
end | end | ||
Line 73: | Line 135: | ||
end | end | ||
function Template: | 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. | local invocation = self._invocation:getInvocation{ | ||
template = self:getName(), | |||
requireMagicWord = self.requireMagicWord, | |||
} | |||
if format == 'code' then | if format == 'code' then | ||
invocation = '<code>' .. invocation .. '</code>' | invocation = '<code>' .. mw.text.nowiki(invocation) .. '</code>' | ||
elseif format == 'pre' | 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 = '<pre style="white-space: pre-wrap;">' .. invocation .. '</pre>' | ||
invocation = mw.getCurrentFrame():preprocess(invocation) | invocation = mw.getCurrentFrame():preprocess(invocation) | ||
Line 90: | Line 160: | ||
function Template:getOutput() | function Template:getOutput() | ||
return self. | local protect = require('Module:Protect') | ||
-- calling self._invocation:getOutput{...} | |||
return protect(self._invocation.getOutput)(self._invocation, { | |||
template = self:getName(), | |||
requireMagicWord = self.requireMagicWord, | |||
}) | |||
end | end | ||
Line 99: | Line 174: | ||
local TestCase = {} | local TestCase = {} | ||
TestCase.__index = 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) | function TestCase.new(invocationObj, options, cfg) | ||
local obj = setmetatable({}, TestCase) | 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 | end | ||
if prefix then | |||
num = tonumber(num) | |||
templateOptions[num] = templateOptions[num] or {} | |||
templateOptions[num][prefix] = v | |||
else | |||
generalOptions[k] = v | |||
end | end | ||
end | end | ||
-- | -- Set general options | ||
generalOptions.showcode = yesno(generalOptions.showcode) | |||
obj.options = templateOptions. | 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[1] = templateOptions[1] or {} | ||
templateOptions[2] = templateOptions[2] 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 | 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 140: | Line 245: | ||
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 | ||
-- 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 | -- Make the template objects | ||
obj.templates = {} | obj.templates = {} | ||
for i, | for i, options in ipairs(templateOptions) do | ||
table.insert(obj.templates, Template.new(invocationObj, | 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 160: | Line 318: | ||
end | end | ||
function TestCase: | 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') | local root = mw.html.create('table') | ||
if self.options.wantdiff then | |||
root | root | ||
:addClass(self.options. | :addClass('mw-collapsible') | ||
: | if self.options.notcollapsed == false then | ||
:tag(' | root | ||
:wikitext(self.options. | :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 | local root = mw.html.create() | ||
if self.options. | if self.options.showcode then | ||
root | |||
:wikitext(self.templates[1]:getInvocation()) | |||
:newline() | |||
end | end | ||
local width = tostring(math.floor(100 / #self.templates)) .. '%' | |||
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 | end | ||
-- Row header | -- Row header | ||
local dataRow = | local dataRow = tableroot:tag('tr'):css('vertical-align', 'top') | ||
if self.options.rowheader then | if self.options.rowheader then | ||
dataRow:tag('th') | dataRow:tag('th') | ||
Line 191: | 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() | |||
: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') | dataRow:tag('td') | ||
:newline() | :newline() | ||
:wikitext(self: | :wikitext(self:getInvocation('code')) | ||
end | 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) | return tostring(root) | ||
end | end | ||
Line 202: | Line 641: | ||
function TestCase:renderDefault() | function TestCase:renderDefault() | ||
local ret = {} | local ret = {} | ||
ret[#ret + 1] = self.templates[1]:getInvocation( | if self.options.showcode then | ||
ret[#ret + 1] = self.templates[1]:getInvocation() | |||
end | |||
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>' | ||
ret[#ret + 1] = obj: | if self.options.showheader then | ||
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 212: | Line 661: | ||
function TestCase:__tostring() | function TestCase:__tostring() | ||
local format = self.options.format | local format = self.options.format | ||
local method = format and | 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 | end | ||
Line 227: | Line 679: | ||
local NowikiInvocation = {} | local NowikiInvocation = {} | ||
NowikiInvocation.__index = NowikiInvocation | NowikiInvocation.__index = NowikiInvocation | ||
NowikiInvocation.message = message -- Add the message method | |||
function NowikiInvocation.new(invocation) | function NowikiInvocation.new(invocation, cfg) | ||
local obj = setmetatable({}, NowikiInvocation) | local obj = setmetatable({}, NowikiInvocation) | ||
obj.invocation = mw.text.unstrip(invocation) | 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 | return obj | ||
end | end | ||
function NowikiInvocation:getInvocation( | 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, | |||
template | template | ||
) | ) | ||
if count < 1 then | if options.requireMagicWord ~= false and count < 1 then | ||
error( | error(self:message( | ||
'nowiki-magic-word-error', | |||
self.cfg.templateNameMagicWord | |||
)) | )) | ||
end | end | ||
Line 250: | Line 711: | ||
end | end | ||
function NowikiInvocation:getOutput( | function NowikiInvocation:getOutput(options) | ||
local invocation = self:getInvocation( | local invocation = self:getInvocation(options) | ||
return mw.getCurrentFrame():preprocess(invocation) | return mw.getCurrentFrame():preprocess(invocation) | ||
end | end | ||
Line 261: | Line 722: | ||
local TableInvocation = {} | local TableInvocation = {} | ||
TableInvocation.__index = TableInvocation | TableInvocation.__index = TableInvocation | ||
TableInvocation.message = message -- Add the message method | |||
function TableInvocation.new(invokeArgs) | function TableInvocation.new(invokeArgs, nowikiCode, cfg) | ||
local obj = setmetatable({}, TableInvocation) | local obj = setmetatable({}, TableInvocation) | ||
obj.cfg = cfg | |||
obj.invokeArgs = invokeArgs | obj.invokeArgs = invokeArgs | ||
obj.code = nowikiCode | |||
return obj | return obj | ||
end | end | ||
function TableInvocation:getInvocation( | function TableInvocation:getInvocation(options) | ||
return require('Module:Template invocation').invocation( | 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 | 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 | ||
} | } | ||
Line 283: | Line 758: | ||
------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ||
-- | -- 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 = {}, {} | local options, invokeArgs = {}, {} | ||
for k, v in pairs(args) do | for k, v in pairs(args) do | ||
Line 313: | Line 783: | ||
end | end | ||
end | end | ||
local invocationObj = TableInvocation.new(invokeArgs) | |||
local testCaseObj = TestCase.new(invocationObj, options) | -- 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) | return tostring(testCaseObj) | ||
end | end | ||
function | 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 | end | ||
------------------------------------------------------------------------------- | |||
-- Exports | |||
------------------------------------------------------------------------------- | |||
local p = {} | |||
function p.main(frame, cfg) | |||
cfg = cfg or mw.loadData(DATA_MODULE) | |||
-- Load the wrapper config, if any. | |||
local | local wrapperConfig | ||
if frame.getParent then | |||
local title = frame:getParent():getTitle() | |||
local template = title:gsub(cfg.sandboxSubpagePattern, '') | |||
wrapperConfig = cfg.wrappers[template] | |||
end | end | ||
function | -- Work out the function we will call, use it to generate the config for | ||
local | -- 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' | |||
}) | }) | ||
return | |||
-- 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 | end | ||
function p._exportClasses() -- For testing | |||
function p._exportClasses() | |||
return { | return { | ||
Template = Template, | |||
TestCase = TestCase, | TestCase = TestCase, | ||
NowikiInvocation = NowikiInvocation, | NowikiInvocation = NowikiInvocation, | ||
TableInvocation = TableInvocation | TableInvocation = TableInvocation |
Latest revision as of 12:00, 21 May 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. &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(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 < 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