Модуль:Template test case — Википедия

Документация

This module provides a framework for making templates which produce a template test case. While test cases can be made manually, using Lua-based templates such as the ones provided by this module has the advantage that the template arguments only need to be input once, thus reducing the effort involved in making test cases and reducing the possibility of errors in the input.

Usage[править код]

This module should not usually be called directly. Instead, you should use one of the following templates:

Parameter-based templates:

The only difference between these templates is their default arguments. For example, it is possible to display test cases side by side in Template:Testcase rows by specifying |_format = columns

Nowiki-based templates:

  • Template:Test case nowiki - for test cases created from template code wrapped in nowiki tags (useful for displaying complex template invocations).

It is also possible to use a format of {{#invoke:template test case|main|parameters}}. This uses the same defaults as Template:Test case; please see that page for documentation of the parameters.

There is no direct interface to this module for other Lua modules. Lua modules should generally use Lua-based test case modules such as Module:UnitTests or Module:ScribuntoUnit. If it is really necessary to use this module, you can use frame:expandTemplate with one of the templates listed above.

Configuration[править код]

This module has a configuration module at Module:Template test case/config. You can edit it to add new wrapper templates, or to change the messages that the module outputs.


--[[    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 = '<syntaxhighlight lang="wikitext">' .. invocation .. '</syntaxhighlight>' 		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[^\127]*UNIQ%-%-%l+%-)%x+(%-%-?QINU[^\127]*\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('div') 	root 		:addClass('mw-collapsible') 		:css('width', '100%') 		:css('border', 'solid silver 1px') 		:css('padding', '0.2em') 		:css('clear', 'both') 		:addClass(self.options.notcollapsed == false and 'mw-collapsed' or nil) 	if self.options.wantdiff then 		root 			:tag('div') 				:css('background-color', isEqual and 'yellow' or '#90a8ee') 				:css('font-weight', 'bold') 				:css('padding', '0.2em') 				:wikitext(title) 				:done() 	else 		if self.options.notcollapsed ~= true or false then 			root 				:addClass(isEqual and 'mw-collapsed' or nil) 		end 		root 			:tag('div') 				:css('background-color', isEqual and 'lightgreen' or 'yellow') 				:css('font-weight', 'bold') 				:css('padding', '0.2em') 				:wikitext(title) 				:done() 	end 	root 		:tag('div') 			:addClass('mw-collapsible-content') 			:newline() 			:wikitext(s) 			:newline() 	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.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: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.options.before or "" 			line[#line + 1] = self:getTemplateOutput(obj) 			line[#line + 1] = self.options.after or "" 			line[#line + 1] = '<pre style="white-space: pre-wrap;">' 			line[#line + 1] = mw.text.nowiki(self.options.before or "") 			line[#line + 1] = mw.text.nowiki(self:getTemplateOutput(obj)) 			line[#line + 1] = mw.text.nowiki(self.options.after or "") 			line[#line + 1] = '</pre>' 		elseif self.options.output == 'nowiki' then 			line[#line + 1] = mw.text.nowiki(self.options.before or "") 			line[#line + 1] = mw.text.nowiki(self:getTemplateOutput(obj)) 			line[#line + 1] = mw.text.nowiki(self.options.after or "") 		else 			line[#line + 1] = self.options.before or "" 			line[#line + 1] = self:getTemplateOutput(obj) 			line[#line + 1] = self.options.after or "" 		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.options.before or "") .. 			self:getTemplateOutput(obj) .. 			(self.options.after or "") .. 			'<pre style="white-space: pre-wrap;">' .. 			mw.text.nowiki(self.options.before or "") .. 			mw.text.nowiki(self:getTemplateOutput(obj)) .. 			mw.text.nowiki(self.options.after or "") .. '</pre>' 		elseif self.options.output == 'nowiki' then 			ret[#ret + 1] = mw.text.nowiki(self.options.before or "") .. 			mw.text.nowiki(self:getTemplateOutput(obj)) .. 			mw.text.nowiki(self.options.after or "") 		else 			ret[#ret + 1] = (self.options.before or "") .. 			self:getTemplateOutput(obj) .. 			(self.options.after or "") 		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. &amp;lt;, which is unfortunate, 	-- but it is the best we can do as the distinction between <, >, " and &lt;, 	-- &gt;, &quot; is lost during the original nowiki operation. 	invocation = invocation:gsub('&lt;', '<') 	invocation = invocation:gsub('&gt;', '>') 	invocation = invocation:gsub('&quot;', '"') 	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 &lt; 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) 	 	-- Convert args beginning with _ for consistency with the normal bridge 	local newArgs = {} 	for k, v in pairs(args) do 		local normalName = type(k) == "string" and string.match(k, "^_(.*)$") 		if normalName then 			newArgs[normalName] = v 		else 			newArgs[k] = v 		end 	end  	local code = newArgs.code or newArgs[1] 	local invocationObj = NowikiInvocation.new(code, cfg) 	newArgs.code = nil 	newArgs[1] = nil 	-- Assume we want to see the code as we already passed it in. 	newArgs.showcode = newArgs.showcode or true 	local testCaseObj = TestCase.new(invocationObj, newArgs, 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