Module:Template test case: Difference between revisions
Jump to navigation
Jump to search
Content added Content deleted
(comment tweak and add a todo item) |
m (87 revisions imported from wikipedia:Module:Template_test_case) |
||
(75 intermediate revisions by 11 users not shown) | |||
Line 1: | Line 1: | ||
--[[ |
|||
-- This module provides several methods to generate test cases. |
|||
A module for generating test case templates. |
|||
This module incorporates code from the English Wikipedia's "Testcase table" |
|||
local mTableTools = require('Module:TableTools') |
|||
module,[1] written by Frietjes [2] with contributions by Mr. Stradivarius [3] |
|||
local libraryUtil = require('libraryUtil') |
|||
and Jackmcbarn,[4] and the English Wikipedia's "Testcase rows" module,[5] |
|||
local checkType = libraryUtil.checkType |
|||
written by Mr. Stradivarius. |
|||
The "Testcase table" and "Testcase rows" modules are released under the |
|||
local TEMPLATE_NAME_MAGIC_WORD = '<TEMPLATE_NAME>' |
|||
CC BY-SA 3.0 License [6] and the GFDL.[7] |
|||
local TEMPLATE_NAME_MAGIC_WORD_ESCAPED = TEMPLATE_NAME_MAGIC_WORD:gsub('%p', '%%%0') |
|||
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.__index = Template |
|||
Template.memoizedMethods = { |
|||
function Template.new(invocationObj, options, titleCallback) |
|||
-- Names of methods to be memoized in each object. This table should only |
|||
local obj = setmetatable({}, Template) |
|||
-- hold methods with no parameters. |
|||
getFullPage = true, |
|||
getName = true, |
|||
makeHeader = true, |
|||
getOutput = true |
|||
} |
|||
function Template.new(invocationObj, options) |
|||
local obj = {} |
|||
-- Set input |
-- Set input |
||
for k, v in pairs(options or {}) do |
for k, v in pairs(options or {}) do |
||
if not Template[k] then |
|||
obj[k] = v |
|||
end |
|||
end |
end |
||
obj. |
obj._invocation = invocationObj |
||
-- Validate |
-- Validate input |
||
if not obj.template then |
if not obj.template and not obj.title then |
||
error('no template or title specified', 2) |
|||
if titleCallback then |
|||
obj.title = titleCallback() |
|||
else |
|||
error('no template or title callback specified', 2) |
|||
end |
|||
end |
end |
||
-- Memoize expensive method calls |
|||
return obj |
|||
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 48: | 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 73: | Line 133: | ||
local link = self:makeLink(display) |
local link = self:makeLink(display) |
||
return mw.text.nowiki('{{') .. link .. mw.text.nowiki('}}') |
return mw.text.nowiki('{{') .. link .. mw.text.nowiki('}}') |
||
end |
|||
function Template:makeHeader() |
|||
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(), |
|||
invocation = mw.text.nowiki(invocation) |
|||
requireMagicWord = self.requireMagicWord, |
|||
} |
|||
if format == 'code' then |
if format == 'code' then |
||
invocation = '<code>' .. invocation .. '</code>' |
invocation = '<code>' .. mw.text.nowiki(invocation) .. '</code>' |
||
elseif format == ' |
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 88: | 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 97: | 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, cfg) |
|||
local obj = setmetatable({}, TestCase) |
local obj = setmetatable({}, TestCase) |
||
obj.cfg = cfg |
|||
-- Separate general options from template options. Template options are |
|||
-- Validate options |
|||
-- numbered, whereas general options are not. |
|||
do |
|||
local generalOptions, templateOptions = {}, {} |
|||
local highestNum = 0 |
|||
for k, v in pairs(options) do |
|||
local prefix, num |
|||
if type(k) == 'string' then |
|||
if type(k) == 'string' then |
|||
local num = k:match('([1-9][0-9]*)$') |
|||
prefix, num = k:match('^(.-)([1-9][0-9]*)$') |
|||
if num and num > highestNum then |
|||
highestNum = num |
|||
end |
|||
end |
|||
end |
end |
||
if prefix then |
|||
for i = 3, highestNum do |
|||
num = tonumber(num) |
|||
if not options['template' .. i] then |
|||
templateOptions[num] = templateOptions[num] or {} |
|||
error(string.format( |
|||
templateOptions[num][prefix] = v |
|||
"one or more options ending in '%d' were " .. |
|||
else |
|||
"detected, but no 'template%d' option was found", |
|||
generalOptions[k] = v |
|||
i, i |
|||
), 2) |
|||
end |
|||
end |
end |
||
end |
end |
||
-- |
-- Set general options |
||
generalOptions.showcode = yesno(generalOptions.showcode) |
|||
local templateOptions = mTableTools.numData(options, true) |
|||
generalOptions.showheader = yesno(generalOptions.showheader) ~= false |
|||
obj.options = templateOptions.other or {} |
|||
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 |
-- Make the template objects |
||
obj.templates = {} |
obj.templates = {} |
||
for i, options in ipairs(templateOptions) do |
|||
local function templateTitleCallback() |
|||
table.insert(obj.templates, Template.new(invocationObj, options)) |
|||
return mw.title.getCurrentTitle().basePageTitle |
|||
end |
end |
||
obj.templates[1] = Template.new( |
|||
-- Add tracking categories. At the moment we are only tracking templates |
|||
invocationObj, |
|||
-- that use any "heading" parameters or an "output" parameter. |
|||
templateOptions[1], |
|||
obj.categories = {} |
|||
templateTitleCallback |
|||
for k, v in pairs(options) do |
|||
) |
|||
if type(k) == 'string' and k:find('heading') then |
|||
-- @TODO: Use the sandbox of the first template if it is specified, |
|||
obj.categories['Test cases using heading parameters'] = true |
|||
-- rather than the sandbox of the base page. |
|||
elseif k == 'output' then |
|||
obj.templates[2] = Template.new( |
|||
obj.categories['Test cases using output parameter'] = true |
|||
invocationObj, |
|||
templateOptions[2], |
|||
function () |
|||
return templateTitleCallback():subPageTitle('sandbox') |
|||
end |
end |
||
) |
|||
for i = 3, #templateOptions do |
|||
table.insert(obj.templates, Template.new( |
|||
invocationObj, |
|||
templateOptions[i] |
|||
)) |
|||
end |
end |
||
return obj |
return obj |
||
end |
|||
function TestCase:getTemplateOutput(templateObj) |
|||
local output = templateObj:getOutput() |
|||
if self.options.resetRefs then |
|||
mw.getCurrentFrame():extensionTag('references') |
|||
end |
|||
return output |
|||
end |
|||
function TestCase: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 |
end |
||
function TestCase:renderDefault() |
function TestCase:renderDefault() |
||
local ret = {} |
local ret = {} |
||
if self.options.showcode then |
|||
ret[#ret + 1] = self.templates[1]:getInvocation('code') |
|||
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>' |
||
if self.options.showheader then |
|||
ret[#ret + 1] = obj:makeBraceLink() |
|||
ret[#ret + 1] = 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 169: | Line 661: | ||
function TestCase:__tostring() |
function TestCase:__tostring() |
||
local methods = { |
|||
columns = 'renderColumns', |
|||
rows = 'renderRows' |
|||
} |
|||
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 184: | 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.cfg = cfg |
|||
obj.invocation = mw.text.unstrip(invocation) |
|||
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_NAME_MAGIC_WORD_ESCAPED, |
|||
template |
template |
||
) |
) |
||
if count < 1 then |
if options.requireMagicWord ~= false and count < 1 then |
||
error( |
error(self:message( |
||
'nowiki-magic-word-error', |
|||
"the template invocation must include '%s' in place " .. |
|||
self.cfg.templateNameMagicWord |
|||
"of the template name", |
|||
TEMPLATE_NAME_MAGIC_WORD |
|||
)) |
)) |
||
end |
end |
||
Line 207: | 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 218: | 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) |
||
if self.code then |
|||
return require('Module:Template invocation').invocation( |
|||
local nowikiObj = NowikiInvocation.new(self.code, self.cfg) |
|||
template, |
|||
return nowikiObj:getInvocation(options) |
|||
self.invokeArgs |
|||
else |
|||
) |
|||
return require('Module:Template invocation').invocation( |
|||
options.template, |
|||
self.invokeArgs |
|||
) |
|||
end |
|||
end |
end |
||
function TableInvocation:getOutput( |
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 240: | Line 758: | ||
------------------------------------------------------------------------------- |
------------------------------------------------------------------------------- |
||
-- Bridge functions |
|||
-- Exports |
|||
-- |
|||
-- These functions translate template arguments into forms that can be accepted |
|||
-- by the different classes, and return the results. |
|||
------------------------------------------------------------------------------- |
------------------------------------------------------------------------------- |
||
local bridge = {} |
|||
-- Table-based exports |
|||
function bridge.table(args, cfg) |
|||
cfg = cfg or mw.loadData(DATA_MODULE) |
|||
return require('Module:Arguments').getArgs(frame, { |
|||
wrappers = wrappers, |
|||
trim = false, |
|||
removeBlanks = false |
|||
}) |
|||
end |
|||
local p = {} |
|||
function p._table(args) |
|||
local options, invokeArgs = {}, {} |
local options, invokeArgs = {}, {} |
||
for k, v in pairs(args) do |
for k, v in pairs(args) do |
||
Line 270: | Line 783: | ||
end |
end |
||
end |
end |
||
local invocationObj = TableInvocation.new(invokeArgs) |
|||
-- Allow passing a nowiki invocation as an option. While this means users |
|||
local testCaseObj = TestCase.new(invocationObj, options) |
|||
-- 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) |
|||
return p._table(getTableArgs(frame, 'Template:Test case from arguments')) |
|||
end |
|||
local code = args.code or args[1] |
|||
function p.columns(frame) |
|||
local invocationObj = NowikiInvocation.new(code, cfg) |
|||
local args = getTableArgs(frame, 'Template:Testcase table') |
|||
args. |
args.code = nil |
||
args[1] = nil |
|||
return p._table(args) |
|||
-- 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 |
||
------------------------------------------------------------------------------- |
|||
function p.rows(frame) |
|||
-- Exports |
|||
local args = getTableArgs(frame, 'Template:Testcase rows') |
|||
------------------------------------------------------------------------------- |
|||
args._format = 'rows' |
|||
return p._table(args) |
|||
end |
|||
local p = {} |
|||
-- Nowiki-based exports |
|||
function p. |
function p.main(frame, cfg) |
||
cfg = cfg or mw.loadData(DATA_MODULE) |
|||
local invocationObj = NowikiInvocation.new(args.invocation) |
|||
args.invocation = nil |
|||
local options = args |
|||
local testCaseObj = TestCase.new(invocationObj, options) |
|||
return tostring(testCaseObj) |
|||
end |
|||
-- Load the wrapper config, if any. |
|||
function p.nowiki(frame) |
|||
local wrapperConfig |
|||
local args = require('Module:Arguments').getArgs(frame, { |
|||
if frame.getParent then |
|||
wrappers = 'Template:Test case from invocation' |
|||
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' |
|||
}) |
}) |
||
return p._nowiki(args) |
|||
end |
|||
-- Get default args and build the args table. User-specified args overwrite |
|||
-- Exports for testing |
|||
-- 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() |
function p._exportClasses() -- For testing |
||
return { |
return { |
||
Template = Template, |
|||
TestCase = TestCase, |
TestCase = TestCase, |
||
Invocation = Invocation, |
|||
NowikiInvocation = NowikiInvocation, |
NowikiInvocation = NowikiInvocation, |
||
TableInvocation = TableInvocation |
TableInvocation = TableInvocation |
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. &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