Module:Mock title/sandbox explained

require('strict')local libraryUtil = require('libraryUtil')local checkType = libraryUtil.checkTypelocal checkTypeMulti = libraryUtil.checkTypeMultilocal mRepr = require('Module:Repr')

local p = local mockTitleRegistry = local mockCurrentTitle = nil

-- Keep a reference to the original mw.title.new function so that we can use it-- when mw.title.new is patched with our custom function.local titleNew = mw.title.new

---- Capitalize a string.--local function capitalize(s) return s:sub(1, 1):upper .. s:sub(2, -1)end

---- Check that a named argument is one of multiple types.--local function checkTypeForNamedArgMulti(name, argName, arg, expectTypes) local argType = type(arg) for _, expectedType in ipairs(expectTypes) do if argType

expectedType then return end end error(string.format("bad named argument %s to '%s' (%s expected, got %s)", argName, name, mw.text.listToText(expectTypes, ', ', ' or '), argType ), 3 )end

---- Set a property on an object to the given value, if that value is not nil.--local function maybeSetProperty(object, property, value) if value ~= nil then rawset(object, property, value) endend

---- Construct a mock title from a string, an ID or an options table. If we were-- passed a mock title object to start with, return it as-is.--local function constructMockTitle(titleOrOptions) if titleOrOptions

nil then return nil end local titleOrOptionsType = type(titleOrOptions) if titleOrOptionsType

'string' or titleOrOptionsType

'number' then return p.MockTitle elseif titleOrOptionsType

'table' then if type(titleOrOptions.getContent)

'function' then return titleOrOptions else return p.MockTitle(titleOrOptions) end else error(string.format('Invalid type specified to constructMockTitle (expected string, number, table or nil; received %s)', titleOrOptionsType ), 2 ) endend

--local function getProtectionLevels(options, makeOptionsKey) local levels = local isSet = false for action in pairs(levels) do local level = options[makeOptionsKey(action)] if level then levels[action][1] = level isSet = true end end return levels, isSetend

---- Set protection levels--local function setProtectionLevels(title, options) local protectionLevels, isProtectionSet = getProtectionLevels(options, function (action) return action .. 'Protection' end ) if isProtectionSet then rawset(title, 'protectionLevels', protectionLevels) endend

---- Set cascading protection--local function setCascadingProtection(title, options) local cascadingProtectionLevels, isCascadingProtectionSet = getProtectionLevels(options, function (action) return string.format('cascading%sProtection', capitalize(action)) end ) local cascadingSourcesExist = options.cascadingProtectionSources and #options.cascadingProtectionSources >= 1 if isCascadingProtectionSet and cascadingSourcesExist then rawset(title, 'cascadingProtection', ) elseif isCascadingProtectionSet then error('a cascading protection argument was given but the cascadingProtectionSources argument was missing or empty', 2) elseif cascadingSourcesExist then error('the cascadingProtectionSources argument was given, but no cascading protection argument was given', 2) endend

---- Set page content, if specified--local function maybeSetContent(titleObject, content) if content then rawset(titleObject, 'getContent', function return content end) endend

---- Set properties in the file table, as well as the fileExists property--local function setFileProperties(title, options) if title.file then for _, property in ipairs do local optionName = 'file' .. capitalize(property) maybeSetProperty(title.file, property, options[optionName]) end endend

---- Changes the associated titles to be patched if applicable--local function patchAssociatedTitleObjects(title) for _, property in ipairs do local mockTitle = mockTitleRegistry[title[property] and title[property].prefixedText] if mockTitle then rawset(title, property, mockTitle) end end rawset(title, 'subPageTitle', function(text) return mw.title.makeTitle(title.namespace, title.text .. '/' .. text) end)end

---- Patch an existing title object with the given options.--function p.patchTitleObject(title, options) checkType('patchTitleObject', 1, title, 'table') checkType('patchTitleObject', 2, options, 'table') -- Set simple properties for _, property in ipairs do maybeSetProperty(title, property, options[property]) end -- Set redirect title maybeSetProperty(title, 'redirectTarget', constructMockTitle(options.redirectTarget)) -- Set complex properties setProtectionLevels(title, options) setCascadingProtection(title, options) maybeSetContent(title, options.content) setFileProperties(title, options) return titleend

---- Construct a new mock title.--function p.MockTitle(options) checkType('MockTitle', 1, options, 'table') checkTypeForNamedArgMulti('MockTitle', 'title', options.title,) -- Create the title object with the original mw.title.new so that we don't -- look in the mock title registry here when mw.title.new is patched. local title = titleNew(options.title) return p.patchTitleObject(title, options)end

---- Register a mock title.-- This can be a mock title object or a table of options for MockTitle.--function p.registerMockTitle(titleOrOptions) checkType('registerMockTitle', 1, titleOrOptions, 'table') local title = constructMockTitle(titleOrOptions) mockTitleRegistry[title.prefixedText] = titleend

---- Remove a title from the mock title registry.-- Returns the title that was removed.--function p.deregisterMockTitle(titleOrOptions) checkTypeMulti('deregisterMockTitle', 1, titleOrOptions,) local title = constructMockTitle(titleOrOptions) if not title then return nil end local registeredTitle = mockTitleRegistry[title.prefixedText] mockTitleRegistry[title.prefixedText] = nil return registeredTitleend

---- Register multiple mock titles.--function p.registerMockTitles(...) for i, titleOrOptions in ipairs do checkType('registerMockTitles', i, titleOrOptions, 'table') p.registerMockTitle(titleOrOptions) endend

---- Deregister multiple mock titles.-- Returns a sequence of titles that were removed.--function p.deregisterMockTitles(...) local removedTitles = for i, titleOrOptions in ipairs do checkTypeMulti('deregisterMockTitles', i, titleOrOptions,) table.insert(removedTitles, p.deregisterMockTitle(titleOrOptions)) end return removedTitlesend

---- Clear the mock title registry.--function p.clearMockTitleRegistry mockTitleRegistry = end

---- Register a mock title as the current title.-- This can be a string, a mock title object or a table of options for-- MockTitle.--function p.registerMockCurrentTitle(titleOrOptions) checkType('registerMockCurrentTitle', 1, titleOrOptions, 'table') local title = constructMockTitle(titleOrOptions) mockCurrentTitle = titleend

---- Deregister the registered current mock title.--function p.deregisterMockCurrentTitle mockCurrentTitle = nilend

---- Clear all registered mock titles.-- This clears the mock title registry and deregisters the current mock title.--function p.clearAllMockTitles p.clearMockTitleRegistry p.deregisterMockCurrentTitleend

---- Look up a title in the mock title registry.--local function lookUpTitleInRegistry(title) return mockTitleRegistry[title.prefixedText]end

---- Look up the registered mock current title.--local function lookUpMockCurrentTitle return mockCurrentTitleend

---- Patch the given title function.-- This replaces the title function with a function that looks up the title-- from some source. Exactly how the title is looked up is determined by the-- lookUpMockTitle argument. This should be a function that takes a title object-- as input and returns a mock title object as output.--local function patchTitleFunc(funcName, lookUpMockTitle) local oldFunc = mw.title[funcName] mw.title[funcName] = function(...) local title = oldFunc(...) if not title then error(string.format('Could not make title object from invocation %s', mRepr.invocationRepr ), 2 ) end local mockTitle = lookUpMockTitle(title) -- This type of patching has to be applied to all titles and after all -- mocks are defined to ensure it works if mockTitle then patchAssociatedTitleObjects(mockTitle) return mockTitle else patchAssociatedTitleObjects(title) return title end end return oldFuncend

---- Handle the patch process.-- This takes care of setting up before the patch, running the specified-- function after the patch, tearing down the patch, and returning the results.-- The setup process is handled using the setup function, which takes no-- parameters. Teardown is handled by the teardown function, which takes the-- values returned by the setup function as input. The user-supplied function-- is passed in as the func argument, which takes the remaining parameters as-- arguments.--local function patch(setup, teardown, func, ...) local setupResults = local results = local success = table.remove(results, 1) teardown(unpack(setupResults)) if success then return unpack(results) else error(results[1], 3) endend

---- Patch mw.title.new.-- The original mw.title.new is restored after running the given function.--function p.patchTitleNew(func, ...) checkType('patchTitleNew', 1, func, 'function')

local function setup return patchTitleFunc('new', lookUpTitleInRegistry) end local function teardown(oldTitleNew) mw.title.new = oldTitleNew end return patch(setup, teardown, func, ... )end

---- Patch mw.title.makeTitle.-- The original mw.title.makeTitle is restored after running the given function.--function p.patchMakeTitle(func, ...) checkType('patchMakeTitle', 1, func, 'function')

local function setup return patchTitleFunc('makeTitle', lookUpTitleInRegistry) end local function teardown(oldMakeTitle) mw.title.makeTitle = oldMakeTitle end return patch(setup, teardown, func, ... )end

---- Patch mw.title.getCurrentTitle.-- The original mw.title.getCurrentTitle is restored after running the given-- function.--function p.patchGetCurrentTitle(func, ...) checkType('patchGetCurrentTitle', 1, func, 'function')

local function setup return patchTitleFunc('getCurrentTitle', lookUpMockCurrentTitle) end local function teardown(oldGetCurrentTitle) mw.title.getCurrentTitle = oldGetCurrentTitle end return patch(setup, teardown, func, ... )end

---- Patch mw.title.new, mw.title.makeTitle and mw.title.getCurrentTitle.-- The original title functions are restored after running the given function.--function p.patchTitleConstructors(func, ...) checkType('patchTitleConstructors', 1, func, 'function')

local function setup local oldTitleNew = patchTitleFunc('new', lookUpTitleInRegistry) local oldMakeTitle = patchTitleFunc('makeTitle', lookUpTitleInRegistry) local oldGetCurrentTitle = patchTitleFunc('getCurrentTitle', lookUpMockCurrentTitle) return oldTitleNew, oldMakeTitle, oldGetCurrentTitle end local function teardown(oldTitleNew, oldMakeTitle, oldGetCurrentTitle) mw.title.new = oldTitleNew mw.title.makeTitle = oldMakeTitle mw.title.getCurrentTitle = oldGetCurrentTitle end return patch(setup, teardown, func, ... )end

return p