Module:Template test case: Difference between revisions

    From Nonbinary Wiki
    (memoize expensive method calls in the Template objects)
    (when doing Template class memoization, return wrapper functions for the values rather than the values themselves)
    Line 40: Line 40:


    -- Memoize expensive method calls
    -- Memoize expensive method calls
    local memoizedValues = {}
    local memoFuncs = {}
    return setmetatable(obj, {
    return setmetatable(obj, {
    __index = function (t, key)
    __index = function (t, key)
    if Template.memoizedMethods[key] then
    if Template.memoizedMethods[key] then
    local val = memoizedValues[key]
    local func = memoFuncs[key]
    if val then
    if not func then
    return val
    local val = Template[key](t)
    else
    func = function () return val end
    val = Template[key](t)
    memoFuncs[key] = func
    memoizedValues[key] = val
    return val
    end
    end
    return func
    else
    else
    return Template[key]
    return Template[key]

    Revision as of 15:17, 25 November 2014

    Documentation for this module may be created at Module:Template test case/doc

    -- This module provides several methods to generate test cases.
    
    local mTableTools = require('Module:TableTools')
    local libraryUtil = require('libraryUtil')
    local checkType = libraryUtil.checkType
    
    local TEMPLATE_NAME_MAGIC_WORD = '__TEMPLATENAME__'
    local TEMPLATE_NAME_MAGIC_WORD_ESCAPED = TEMPLATE_NAME_MAGIC_WORD:gsub('%p', '%%%0')
    
    -------------------------------------------------------------------------------
    -- Template class
    -------------------------------------------------------------------------------
    
    local Template = {}
    
    Template.memoizedMethods = {
    	-- Names of methods to be memoized in each object. This table should only
    	-- hold methods with no parameters.
    	getFullPage = true,
    	getName = true,
    	makeHeading = true,
    	getOutput = true
    }
    
    function Template.new(invocationObj, options)
    	local obj = {}
    
    	-- Set input
    	for k, v in pairs(options or {}) do
    		if not Template[k] then
    			obj[k] = v
    		end
    	end
    	obj._invocation = invocationObj
    
    	-- Validate input
    	if not obj.template and not obj.title then
    		error('no template or title specified', 2)
    	end
    
    	-- Memoize expensive method calls
    	local memoFuncs = {}
    	return setmetatable(obj, {
    		__index = function (t, key)
    			if Template.memoizedMethods[key] then
    				local func = memoFuncs[key]
    				if not func then
    					local val = Template[key](t)
    					func = function () return val end
    					memoFuncs[key] = func
    				end
    				return func
    			else
    				return Template[key]
    			end
    		end
    	})
    end
    
    function Template:getFullPage()
    	if self.template then
    		local strippedTemplate, hasColon = self.template:gsub('^:', '', 1)
    		local ns = strippedTemplate:match('^(.-):')
    		ns = ns and mw.site.namespaces[ns]
    		if ns then
    			return strippedTemplate
    		elseif hasColon then
    			return strippedTemplate -- Main namespace
    		else
    			return mw.site.namespaces[10].name .. ':' .. strippedTemplate
    		end
    	else
    		return self.title.prefixedText
    	end
    end
    
    function Template:getName()
    	if self.template then
    		return self.template
    	else
    		return require('Module:Template invocation').name(self.title)
    	end
    end
    
    function Template:makeLink(display)
    	if display then
    		return string.format('[[:%s|%s]]', self:getFullPage(), display)
    	else
    		return string.format('[[:%s]]', self:getFullPage())
    	end
    end
    
    function Template:makeBraceLink(display)
    	display = display or self:getName()
    	local link = self:makeLink(display)
    	return mw.text.nowiki('{{') .. link .. mw.text.nowiki('}}')
    end
    
    function Template:makeHeading()
    	return self.heading or self:makeBraceLink()
    end
    
    function Template:getInvocation(format)
    	local invocation = self._invocation:getInvocation(self:getName())
    	invocation = mw.text.nowiki(invocation)
    	if format == 'code' then
    		invocation = '<code>' .. invocation .. '</code>'
    	elseif format == 'pre' then
    		invocation = '<pre style="white-space: pre-wrap;">' .. invocation .. '</pre>'
    		invocation = mw.getCurrentFrame():preprocess(invocation)
    	end
    	return invocation
    end
    
    function Template:getOutput()
    	return self._invocation:getOutput(self:getName())
    end
    
    -------------------------------------------------------------------------------
    -- TestCase class
    -------------------------------------------------------------------------------
    
    local TestCase = {}
    TestCase.__index = TestCase
    
    function TestCase.new(invocationObj, options)
    	local obj = setmetatable({}, TestCase)
    
    	-- Validate options
    	do
    		local highestNum = 0
    		for k in pairs(options) do
    			if type(k) == 'string' then
    				local num = k:match('([1-9][0-9]*)$')
    				num = tonumber(num)
    				if num and num > highestNum then
    					highestNum = num
    				end
    			end
    		end
    		for i = 3, highestNum do
    			if not options['template' .. i] then
    				error(string.format(
    					"one or more options ending in '%d' were " ..
    					"detected, but no 'template%d' option was found",
    					i, i
    				), 2)
    			end
    		end
    	end
    
    	-- Separate general options from options for specific templates
    	local templateOptions = mTableTools.numData(options, true)
    	obj.options = templateOptions.other or {}
    
    	-- Add default template options
    	templateOptions[1] = templateOptions[1] or {}
    	templateOptions[2] = templateOptions[2] or {}
    	if templateOptions[1].template and not templateOptions[2].template then
    		templateOptions[2].template = templateOptions[1].template .. '/sandbox'
    	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('sandbox')
    	end
    
    	-- Make the template objects
    	obj.templates = {}
    	for i, t in ipairs(templateOptions) do
    		table.insert(obj.templates, Template.new(invocationObj, t))
    	end
    
    	return obj
    end
    
    function TestCase:getTemplateOutput(templateObj)
    	local output = templateObj:getOutput()
    	if self.options.resetRefs then
    		mw.getCurrentFrame():extensionTag('references')
    	end
    	return output
    end
    
    function TestCase:renderColumns()
    	local root = mw.html.create('table')
    	root
    		:addClass(self.options.class)
    		:cssText(self.options.style)
    		:tag('caption')
    			:wikitext(self.options.caption or 'Side by side comparison')
    
    	-- Headings
    	local headingRow = root:tag('tr')
    	if self.options.rowheader then
    		-- rowheader is correct here. We need to add another th cell if
    		-- rowheader is set further down, even if heading0 is missing.
    		headingRow:tag('th'):wikitext(self.options.heading0)
    	end
    	local width
    	if #self.templates > 0 then
    		width = tostring(math.floor(100 / #self.templates)) .. '%'
    	else
    		width = '100%'
    	end
    	for i, obj in ipairs(self.templates) do
    		headingRow
    			:tag('th')
    				:css('width', width)
    				:wikitext(obj:makeHeading())
    	end
    
    	-- Row header
    	local dataRow = root:tag('tr'):css('vertical-align', 'top')
    	if self.options.rowheader then
    		dataRow:tag('th')
    			:attr('scope', 'row')
    			:wikitext(self.options.rowheader)
    	end
    	
    	-- Template output
    	for i, obj in ipairs(self.templates) do
    		dataRow:tag('td')
    			:newline()
    			:wikitext(self:getTemplateOutput(obj))
    			:wikitext(self.options.after)
    	end
    	
    	return tostring(root)
    end
    
    function TestCase:renderRows()
    	local root = mw.html.create('table')
    	root
    		:addClass(self.options.class)
    		:cssText(self.options.style)
    
    	if self.options.caption then
    		root
    			:tag('caption')
    				:wikitext(self.options.caption)
    	end
    
    	for _, obj in ipairs(self.templates) do
    		-- Build the row HTML
    		root
    			:tag('tr')
    				:tag('td')
    					:css('text-align', 'center')
    					:css('font-weight', 'bold')
    					:wikitext(obj:makeHeading())
    					:done()
    				:done()
    			:tag('tr')
    				:tag('td')
    					:newline()
    					:wikitext(self:getTemplateOutput(obj))
    	end
    
    	return tostring(root)
    end
    
    function TestCase:renderDefault()
    	local ret = {}
    	ret[#ret + 1] = self.templates[1]:getInvocation('code')
    	for i, obj in ipairs(self.templates) do
    		ret[#ret + 1] = '<div style="clear: both;"></div>'
    		ret[#ret + 1] = obj:makeBraceLink()
    		ret[#ret + 1] = self:getTemplateOutput(obj)
    	end
    	return table.concat(ret, '\n\n')
    end
    
    function TestCase:__tostring()
    	local methods = {
    		columns = 'renderColumns',
    		rows = 'renderRows'
    	}
    	local format = self.options.format
    	local method = format and methods[format] or 'renderDefault'
    	return self[method](self)
    end
    
    -------------------------------------------------------------------------------
    -- Nowiki invocation class
    -------------------------------------------------------------------------------
    
    local NowikiInvocation = {}
    NowikiInvocation.__index = NowikiInvocation
    
    function NowikiInvocation.new(invocation)
    	local obj = setmetatable({}, NowikiInvocation)
    	obj.invocation = mw.text.unstrip(invocation)
    	return obj
    end
    
    function NowikiInvocation:getInvocation(template)
    	template = template:gsub('%%', '%%%%') -- Escape "%" with "%%"
    	local invocation, count = self.invocation:gsub(
    		TEMPLATE_NAME_MAGIC_WORD_ESCAPED,
    		template
    	)
    	if count < 1 then
    		error(string.format(
    			"the template invocation must include '%s' in place " ..
    			"of the template name",
    			TEMPLATE_NAME_MAGIC_WORD
    		))
    	end
    	return invocation
    end
    
    function NowikiInvocation:getOutput(template)
    	local invocation = self:getInvocation(template)
    	return mw.getCurrentFrame():preprocess(invocation)
    end
    
    -------------------------------------------------------------------------------
    -- Table invocation class
    -------------------------------------------------------------------------------
    
    local TableInvocation = {}
    TableInvocation.__index = TableInvocation
    
    function TableInvocation.new(invokeArgs)
    	local obj = setmetatable({}, TableInvocation)
    	obj.invokeArgs = invokeArgs
    	return obj
    end
    
    function TableInvocation:getInvocation(template)
    	return require('Module:Template invocation').invocation(
    		template,
    		self.invokeArgs
    	)
    end
    
    function TableInvocation:getOutput(template)
    	return mw.getCurrentFrame():expandTemplate{
    		title = template,
    		args = self.invokeArgs
    	}
    end
    
    -------------------------------------------------------------------------------
    -- Exports
    -------------------------------------------------------------------------------
    
    -- Table-based exports
    
    local function getTableArgs(frame, wrappers)
    	return require('Module:Arguments').getArgs(frame, {
    		wrappers = wrappers,
    		trim = false,
    		removeBlanks = false
    	})
    end
    
    local p = {}
    
    function p._table(args)
    	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
    	local invocationObj = TableInvocation.new(invokeArgs)
    	local testCaseObj = TestCase.new(invocationObj, options)
    	return tostring(testCaseObj)
    end
    
    function p.table(frame)
    	return p._table(getTableArgs(frame, 'Template:Test case from arguments'))
    end
    
    function p.columns(frame)
    	local args = getTableArgs(frame, 'Template:Testcase table')
    	args._format = 'columns'
    	return p._table(args)
    end
    
    function p.rows(frame)
    	local args = getTableArgs(frame, 'Template:Testcase rows')
    	args._format = 'rows'
    	return p._table(args)
    end
    
    -- Nowiki-based exports
    
    function p._nowiki(args)
    	local invocationObj = NowikiInvocation.new(args.invocation)
    	args.invocation = nil
    	local options = args
    	local testCaseObj = TestCase.new(invocationObj, options)
    	return tostring(testCaseObj)
    end
    
    function p.nowiki(frame)
    	local args = require('Module:Arguments').getArgs(frame, {
    		wrappers = 'Template:Test case from invocation'
    	})
    	return p._nowiki(args)
    end
    
    -- Exports for testing
    
    function p._exportClasses()
    	return {
    		TestCase = TestCase,
    		Invocation = Invocation,
    		NowikiInvocation = NowikiInvocation,
    		TableInvocation = TableInvocation
    	}
    end
    
    return p