Module:Template test case: Difference between revisions

    From Nonbinary Wiki
    (when doing Template class memoization, return wrapper functions for the values rather than the values themselves)
    (show the template invocation if the showcode option is set)
    Line 1: Line 1:
    -- This module provides several methods to generate test cases.
    -- This module provides several methods to generate test cases.


    local yesno = require('Module:Yesno')
    local mTableTools = require('Module:TableTools')
    local mTableTools = require('Module:TableTools')
    local libraryUtil = require('libraryUtil')
    local libraryUtil = require('libraryUtil')
    Line 106: Line 107:
    if format == 'code' then
    if format == 'code' then
    invocation = '<code>' .. invocation .. '</code>'
    invocation = '<code>' .. invocation .. '</code>'
    elseif format == 'pre' then
    elseif format ~= 'plain' then
    -- Default is pre tags
    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 153: Line 155:
    local templateOptions = mTableTools.numData(options, true)
    local templateOptions = mTableTools.numData(options, true)
    obj.options = templateOptions.other or {}
    obj.options = templateOptions.other or {}
    -- Normalize showcode option
    obj.options.showcode = yesno(obj.options.showcode)


    -- Add default template options
    -- Add default template options
    Line 185: Line 190:


    function TestCase:renderColumns()
    function TestCase:renderColumns()
    local root = mw.html.create('table')
    local root = mw.html.create()
    root
    if self.options.showcode then
    root
    :wikitext(self.templates[1]:getInvocation())
    :newline()
    end
     
    local tableroot = root:tag('table')
    tableroot
    :addClass(self.options.class)
    :addClass(self.options.class)
    :cssText(self.options.style)
    :cssText(self.options.style)
    Line 193: Line 205:


    -- Headings
    -- Headings
    local headingRow = root:tag('tr')
    local headingRow = tableroot:tag('tr')
    if self.options.rowheader then
    if self.options.rowheader then
    -- rowheader is correct here. We need to add another th cell if
    -- rowheader is correct here. We need to add another th cell if
    Line 213: Line 225:


    -- Row header
    -- Row header
    local dataRow = root:tag('tr'):css('vertical-align', 'top')
    local dataRow = tableroot:tag('tr'):css('vertical-align', 'top')
    if self.options.rowheader then
    if self.options.rowheader then
    dataRow:tag('th')
    dataRow:tag('th')
    Line 232: Line 244:


    function TestCase:renderRows()
    function TestCase:renderRows()
    local root = mw.html.create('table')
    local root = mw.html.create()
    root
    if self.options.showcode then
    root
    :wikitext(self.templates[1]:getInvocation())
    :newline()
    end
     
    local tableroot = root:tag('table')
    tableroot
    :addClass(self.options.class)
    :addClass(self.options.class)
    :cssText(self.options.style)
    :cssText(self.options.style)


    if self.options.caption then
    if self.options.caption then
    root
    tableroot
    :tag('caption')
    :tag('caption')
    :wikitext(self.options.caption)
    :wikitext(self.options.caption)
    Line 245: Line 264:
    for _, obj in ipairs(self.templates) do
    for _, obj in ipairs(self.templates) do
    -- Build the row HTML
    -- Build the row HTML
    root
    tableroot
    :tag('tr')
    :tag('tr')
    :tag('td')
    :tag('td')
    Line 264: Line 283:
    function TestCase:renderDefault()
    function TestCase:renderDefault()
    local ret = {}
    local ret = {}
    ret[#ret + 1] = self.templates[1]:getInvocation('code')
    if self.options.showcode then
    ret[#ret + 1] = self.templates[1]:getInvocation()
    end
    for i, obj in ipairs(self.templates) do
    for i, obj in ipairs(self.templates) do
    ret[#ret + 1] = '<div style="clear: both;"></div>'
    ret[#ret + 1] = '<div style="clear: both;"></div>'
    Line 399: Line 420:


    function p._nowiki(args)
    function p._nowiki(args)
    local invocationObj = NowikiInvocation.new(args.invocation)
    local invocationObj = NowikiInvocation.new(args.code)
    args.invocation = nil
    args.code = nil
    local options = args
    -- Assume we want to see the code as we already passed it in.
    local testCaseObj = TestCase.new(invocationObj, options)
    args.showcode = args.showcode or true
    local testCaseObj = TestCase.new(invocationObj, args)
    return tostring(testCaseObj)
    return tostring(testCaseObj)
    end
    end

    Revision as of 16:08, 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 yesno = require('Module:Yesno')
    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 ~= 'plain' then
    		-- Default is pre tags
    		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 {}
    
    	-- Normalize showcode option
    	obj.options.showcode = yesno(obj.options.showcode)
    
    	-- 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()
    	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)
    		:tag('caption')
    			:wikitext(self.options.caption or 'Side by side comparison')
    
    	-- Headings
    	local headingRow = 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.
    		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 = 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
    		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()
    	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
    		-- Build the row HTML
    		tableroot
    			: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 = {}
    	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>'
    		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.code)
    	args.code = 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)
    	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