Latest revision |
Your text |
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"
| |
| 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
| | local mTableTools = require('Module:TableTools') |
| Author: Mr. Stradivarius
| | local libraryUtil = require('libraryUtil') |
| | local checkType = libraryUtil.checkType |
|
| |
|
| [1] https://en.wikipedia.org/wiki/Module:Testcase_table
| | local TEMPLATE_NAME_MAGIC_WORD = '<TEMPLATE_NAME>' |
| [2] https://en.wikipedia.org/wiki/User:Frietjes
| | local TEMPLATE_NAME_MAGIC_WORD_ESCAPED = TEMPLATE_NAME_MAGIC_WORD:gsub('%p', '%%%0') |
| [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 | | -- Helper functions |
| ------------------------------------------------------------------------------- | | ------------------------------------------------------------------------------- |
|
| |
|
| local function message(self, key, ...) | | --[[ |
| -- This method is added to classes that need to deal with messages from the | | local function validateTemplateOptions() |
| -- config module. | | -- Add the template names for the first two templates if they weren't |
| local msg = self.cfg.msg[key] | | -- specified. |
| if select(1, ...) then
| | do |
| return mw.message.newRawMessage(msg, ...):plain()
| | local title = mw.title.getCurrentTitle().basePageTitle |
| else
| | local template |
| return msg | | if title.namespace == 10 then |
| end
| | template = title.text |
| end
| | elseif title.namespace == 0 then |
| | | template = ':' .. title.prefixedText |
| -------------------------------------------------------------------------------
| | else |
| -- Template class
| | template = title.prefixedText |
| -------------------------------------------------------------------------------
| |
| | |
| 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 |
| end
| | local templatePage = title.prefixedText |
| obj._invocation = invocationObj
| | templateOptions[1] = templateOptions[1] or {} |
| | | templateOptions[1].templatePage = templateOptions[1].template or templatePage |
| -- Validate input
| | templateOptions[1].template = templateOptions[1].template or template |
| if not obj.template and not obj.title then
| | templateOptions[2] = templateOptions[2] or {} |
| error('no template or title specified', 2) | | templateOptions[2].templatePage = templateOptions[2].template or templatePage .. '/sandbox' |
| | templateOptions[2].template = templateOptions[2].template or template .. '/sandbox' |
| end | | end |
|
| |
|
| -- Memoize expensive method calls | | -- Validate options for three or more templates. |
| local memoFuncs = {} | | if #templateOptions >= 3 then |
| return setmetatable(obj, {
| | for i = 3, #templateOptions do |
| __index = function (t, key) | | local template = templateOptions[i].template |
| if Template.memoizedMethods[key] then | | if not template then |
| local func = memoFuncs[key]
| | error('arguments for a third or subsequent ' .. |
| if not func then
| | 'template were found, but no template name ' .. |
| local val = Template[key](t)
| | 'was specified', 3) |
| func = function () return val end
| |
| memoFuncs[key] = func
| |
| end | |
| return func | |
| else
| |
| return Template[key]
| |
| end | | end |
| 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 | | end |
| return invocation
| |
| end | | end |
| | --]] |
|
| |
|
| function Template:getOutput() | | local function parseTemplateOptions(origOptions) |
| local protect = require('Module:Protect') | | -- Given a table of raw options, returns a table of general options |
| -- calling self._invocation:getOutput{...} | | -- and template options. |
| return protect(self._invocation.getOutput)(self._invocation, { | | local templateOptions = mTableTools.numData(origOptions, true) |
| template = self:getName(),
| | local options = templateOptions.other |
| requireMagicWord = self.requireMagicWord,
| | templateOptions.other = nil |
| }) | | return options, templateOptions |
| end | | end |
|
| |
|
| ------------------------------------------------------------------------------- | | ------------------------------------------------------------------------------- |
| -- TestCase class | | -- Invocation class |
| ------------------------------------------------------------------------------- | | ------------------------------------------------------------------------------- |
|
| |
|
| local TestCase = {} | | local Invocation = {} |
| TestCase.__index = TestCase
| | Invocation.__index = Invocation |
| 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
| |
|
| |
|
| | function Invocation.new() |
| | local obj = setmetatable({}, TableInvocation) |
| return obj | | return obj |
| end | | end |
|
| |
|
| function TestCase:getTemplateOutput(templateObj) | | function Invocation:setOptions(t) |
| local output = templateObj:getOutput() | | self._options = t |
| if self.options.resetRefs then
| |
| mw.getCurrentFrame():extensionTag('references')
| |
| end
| |
| return output
| |
| end | | end |
|
| |
|
| function TestCase:templateOutputIsEqual() | | function Invocation:getOptions() |
| -- Returns a boolean showing whether all of the template outputs are equal.
| | return self._options |
| -- 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 | | end |
|
| |
|
Line 677: |
Line 82: |
| ------------------------------------------------------------------------------- | | ------------------------------------------------------------------------------- |
|
| |
|
| local NowikiInvocation = {} | | local NowikiInvocation = setmetatable({}, Invocation) |
| NowikiInvocation.__index = NowikiInvocation | | NowikiInvocation.__index = NowikiInvocation |
| NowikiInvocation.message = message -- Add the message method
| |
|
| |
|
| function NowikiInvocation.new(invocation, cfg) | | function NowikiInvocation.new(args) |
| local obj = setmetatable({}, NowikiInvocation) | | checkType('NowikiInvocation.new', 'args', args, 'table') |
| obj.cfg = cfg | | local obj = Invocation.new() |
| invocation = mw.text.unstrip(invocation)
| | setmetatable(obj, NowikiInvocation) |
| -- Decode HTML entities for <, >, and ". This means that HTML entities in | | |
| -- the original code must be escaped as e.g. &lt;, which is unfortunate, | | obj.invocation = mw.text.unstrip(args.invocation) |
| -- but it is the best we can do as the distinction between <, >, " and <, | | args.invocation = nil |
| -- >, " is lost during the original nowiki operation.
| | local options = {} |
| invocation = invocation:gsub('<', '<')
| | for k, v in pairs(args) do |
| invocation = invocation:gsub('>', '>')
| | options[k] = v |
| invocation = invocation:gsub('"', '"') | | end |
| obj.invocation = invocation | | obj.options = options |
| | |
| return obj | | return obj |
| end | | end |
|
| |
|
| function NowikiInvocation:getInvocation(options) | | function NowikiInvocation:getInvocation(template) |
| local template = options.template:gsub('%%', '%%%%') -- Escape "%" with "%%" | | template = template:gsub('%%', '%%%%') -- Escape a % with %% |
| local invocation, count = self.invocation:gsub( | | local invocation, count = self.invocation:gsub( |
| self.cfg.templateNameMagicWordPattern, | | TEMPLATE_NAME_MAGIC_WORD_ESCAPED, |
| template | | template |
| ) | | ) |
| if options.requireMagicWord ~= false and count < 1 then | | if count < 1 then |
| error(self:message( | | error(string.format( |
| '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 711: |
Line 117: |
| end | | end |
|
| |
|
| function NowikiInvocation:getOutput(options) | | function NowikiInvocation:getOutput(template) |
| local invocation = self:getInvocation(options) | | local invocation = self:getInvocation(template) |
| return mw.getCurrentFrame():preprocess(invocation) | | return mw.getCurrentFrame():preprocess(invocation) |
| end | | end |
Line 722: |
Line 128: |
| local TableInvocation = {} | | local TableInvocation = {} |
| TableInvocation.__index = TableInvocation | | TableInvocation.__index = TableInvocation |
| TableInvocation.message = message -- Add the message method
| |
|
| |
|
| function TableInvocation.new(invokeArgs, nowikiCode, cfg) | | function TableInvocation.new(args) |
| local obj = setmetatable({}, TableInvocation) | | checkType('TableInvocation.new', 'args', args, 'table') |
| obj.cfg = cfg
| | local obj = Invocation.new() |
| | setmetatable(obj, TableInvocation) |
| | |
| | local rawOptions, invokeArgs = {}, {} |
| | for k, v in pairs(args) do |
| | local optionKey = type(k) == 'string' and k:match('^_(.*)$') |
| | if optionKey then |
| | rawOptions[optionKey] = v |
| | else |
| | invokeArgs[k] = v |
| | end |
| | end |
| obj.invokeArgs = invokeArgs | | obj.invokeArgs = invokeArgs |
| obj.code = nowikiCode | | local options, templateOptions = parseTemplateOptions(rawOptions) |
| | obj:setOptions(options) |
| | obj:setTemplateOptions(templateOptions) |
| | |
| return obj | | return obj |
| end | | end |
|
| |
|
| function TableInvocation:getInvocation(options) | | function TableInvocation:getInvocation(template) |
| 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(options) | | function TableInvocation:getOutput(template) |
| 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 = options.template, | | title = template, |
| args = self.invokeArgs | | args = self.invokeArgs |
| } | | } |
Line 758: |
Line 166: |
|
| |
|
| ------------------------------------------------------------------------------- | | ------------------------------------------------------------------------------- |
| -- Bridge functions | | -- TestCase class |
| | ------------------------------------------------------------------------------- |
| | |
| | local TestCase = {} |
| | TestCase.__index = TestCase |
| | |
| | function TestCase.new(invocationObj) |
| | checkType('TestCase.new', 'invocationObj', invocationObj, 'table') |
| | local obj = setmetatable({}, TestCase) |
| | return obj |
| | end |
| | |
| | -------------------------------------------------------------------------------- |
| | -- Test case display functions |
| -- | | -- |
| -- These functions translate template arguments into forms that can be accepted | | -- Test case display functions produce the wikitext to display the template |
| -- by the different classes, and return the results. | | -- output for one test case. For example, one function might produce templates |
| | -- aligned horizontally, and another function might produce templates aligned |
| | -- one below the other. |
| | -- |
| | -- They are named functions that accept the following parameters: |
| | -- * templates - an array of subtables containing data about each template to be |
| | -- displayed. These subtables can contain the following values: |
| | -- * result - the expanded wikitext from the template. |
| | -- * invocation - the original unexpanded wikitext that the output was |
| | -- generated from. This may be nil if the invocation is not available. |
| | -- * name - the name of the template. |
| | -- * link - a normal wikilink to the template page (displays as |
| | -- "Template:Foo"). |
| | -- * braceLink - a wikilink to the template page formatted like the {{tl}} |
| | -- template, i.e. it displays as "{{Foo}}". |
| | -- * heading - a heading to display above the template output. |
| | -------------------------------------------------------------------------------- |
| | |
| | ------------------------------------------------------------------------------- |
| | -- Exports |
| ------------------------------------------------------------------------------- | | ------------------------------------------------------------------------------- |
|
| |
|
| local bridge = {}
| | -- Table-based exports |
|
| |
|
| function bridge.table(args, cfg) | | local function getTableArgs(frame, wrappers) |
| cfg = cfg or mw.loadData(DATA_MODULE) | | return require('Module:Arguments').getArgs(frame, { |
| | | wrappers = wrappers, |
| local options, invokeArgs = {}, {}
| | valueFunc = function (k, v) |
| for k, v in pairs(args) do
| | if type(k) == 'string' and k:find('^_') then |
| local optionKey = type(k) == 'string' and k:match('^_(.*)$')
| | v = mw.text.trim(v) |
| if optionKey then
| | if v ~= '' then |
| if type(v) == 'string' then
| | return v |
| v = v:match('^%s*(.-)%s*$') -- trim whitespace | | end |
| | else |
| | return v |
| end | | end |
| if v ~= '' then
| |
| options[optionKey] = v
| |
| end
| |
| else
| |
| invokeArgs[k] = v
| |
| end | | end |
| end | | }) |
| | end |
|
| |
|
| -- Allow passing a nowiki invocation as an option. While this means users
| | local p = {} |
| -- 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)
| | function p._table(args) |
| local testCaseObj = TestCase.new(invocationObj, options, cfg) | | local invocation = TableInvocation.new(args) |
| return tostring(testCaseObj) | | return invocation |
| end | | end |
|
| |
|
| function bridge.nowiki(args, cfg) | | function p.table(frame) |
| cfg = cfg or mw.loadData(DATA_MODULE) | | return p._table(getTableArgs(frame, 'Template:Test case')) |
| | 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.code = nil | | args._format = 'columns' |
| 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.main(frame, cfg) | | function p._nowiki(args) |
| cfg = cfg or mw.loadData(DATA_MODULE) | | local invocation = NowikiInvocation.new(args) |
| | return invocation |
| | 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' |
| 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() -- For testing | | function p._exportClasses() |
| return { | | return { |
| Template = Template,
| |
| TestCase = TestCase, | | TestCase = TestCase, |
| | Invocation = Invocation, |
| NowikiInvocation = NowikiInvocation, | | NowikiInvocation = NowikiInvocation, |
| TableInvocation = TableInvocation | | TableInvocation = TableInvocation |