From Wikipedia, the free encyclopedia

-------------------------------------------------------------------------------

--                            Article history

--

-- This module allows editors to link to all the significant events in an

-- article's history, such as good article nominations and featured article

-- nominations. It also displays its current status, as well as other

-- information, such as the date it was featured on the main page.

-------------------------------------------------------------------------------



local CONFIG_PAGE = 'Module:Article history/config'

local WRAPPER_TEMPLATE = 'Template:Article history'

local DEBUG_MODE = false -- If true, errors are not caught.



-- Load required modules.

require('strict')

local Category = require('Module:Article history/Category')

local yesno = require('Module:Yesno')

local lang = mw.language.getContentLanguage()



-------------------------------------------------------------------------------

-- Helper functions

-------------------------------------------------------------------------------



local function isPositiveInteger(num)

	return type(num) == 'number'

		and math.floor(num) == num

		and num > 0

		and num < math.huge

end



local function substituteParams(msg, ...)

	return mw.message.newRawMessage(msg, ...):plain()

end



local function makeUrlLink(url, display)

	return string.format('[%s %s]', url, display)

end



local function maybeCallFunc(val, ...)

	-- Checks whether val is a function, and if so calls it with the specified

	-- arguments. Otherwise val is returned as-is.

	if type(val) == 'function' then

		return val(...)

	else

		return val

	end

end



local function renderImage(image, caption, size)

	if caption then

		caption = '|' .. caption

	else

		caption = ''

	end

	return string.format('[[File:%s|%s%s]]', image, size, caption)

end



local function addMixin(class, mixin)

	-- Add a mixin to a class. The functions will be shared across classes, so

	-- don't use it for functions that keep state.

	for name, method in pairs(mixin) do

		classname = method

	end

end



-------------------------------------------------------------------------------

-- Message mixin

-- This mixin is used by all classes to add message-related methods.

-------------------------------------------------------------------------------



local Message = {}



function Message:message(key, ...)

	-- This fetches the message from the config with the specified key, and

	-- substitutes parameters $1, $2 etc. with the subsequent values it is

	-- passed.

	local msg = self.cfg.msgkey

	if select('#', ...) > 0 then

		return substituteParams(msg, ...)

	else

		return msg

	end

end



function Message:raiseError(msg, help)

	-- Raises an error with the specified message and help link. Execution

	-- stops unless the error is caught. This is used for errors where

	-- subsequent processing becomes impossible.

	local errorText

	if help then

		errorText = self:message('error-message-help', msg, help)

	else

		errorText = self:message('error-message-nohelp', msg)

	end

	error(errorText, 0)

end



function Message:addWarning(msg, help)

	-- Adds a warning to the object's warnings table. Execution continues as

	-- normal. This is used for errors that should be fixed but that do not

	-- prevent the module from outputting something useful.

	self.warnings = self.warnings or {}

	local warningText

	if help then

		warningText = self:message('warning-help', msg, help)

	else

		warningText = self:message('warning-nohelp', msg)

	end

	table.insert(self.warnings, warningText)

end



function Message:getWarnings()

	return self.warnings or {}

end



-------------------------------------------------------------------------------

-- Row class

-- This class represents one row in the template.

-------------------------------------------------------------------------------



local Row = {}

Row.__index = Row

addMixin(Row, Message)



function Row.new(data)

	local obj = setmetatable({}, Row)

	obj.cfg = data.cfg

	obj.currentTitle = data.currentTitle

	obj.makeData = data.makeData -- used by Row:getData

	return obj

end



function Row:_cachedTry(cacheKey, errorCacheKey, func)

	-- This method is for use in Row object methods that are called more than

	-- once. The results of such methods should be cached to avoid unnecessary

	-- processing. We also cache any errors found and abort if an error was

	-- raised previously, otherwise error messages could be displayed multiple

	-- times.

	--

	-- We use false as a key to cache nil results, so func cannot return false.

	--

	-- @param cacheKey The key to cache successful results with

	-- @param errorCacheKey The key to cache errors with

	-- @param func an anonymous function that returns the method result

	if selferrorCacheKey then

		return nil

	end

	local ret = selfcacheKey

	if ret then

		return ret

	elseif ret == false then

		return nil

	end

	local success

	if DEBUG_MODE then

		success = true

		ret = func()

	else

		success, ret = pcall(func)

	end

	if success then

		if ret then

			selfcacheKey = ret

			return ret

		else

			selfcacheKey = false

			return nil

		end

	else

		selferrorCacheKey = true

		-- We have already formatted the error message, so no need to format it

		-- again.

		error(ret, 0)

	end

end



function Row:getData(articleHistoryObj)

	return self:_cachedTry('_dataCache', '_isDataError', function ()

		return self.makeData(articleHistoryObj)

	end)

end



function Row:setIconValues(icon, caption, size)

	self.icon = icon

	self.iconCaption = caption

	self.iconSize = size

end



function Row:getIcon(articleHistoryObj)

	return maybeCallFunc(self.icon, articleHistoryObj, self)

end



function Row:getIconCaption(articleHistoryObj)

	return maybeCallFunc(self.iconCaption, articleHistoryObj, self)

end



function Row:getIconSize()

	return self.iconSize or self.cfg.defaultIconSize or '30px'

end



function Row:renderIcon(articleHistoryObj)

	local icon = self:getIcon(articleHistoryObj)

	if not icon then

		return nil

	end

	return renderImage(

		icon,

		self:getIconCaption(articleHistoryObj),

		self:getIconSize()

	)

end



function Row:setNoticeBarIconValues(icon, caption, size)

	self.noticeBarIcon = icon

	self.noticeBarIconCaption = caption

	self.noticeBarIconSize = size

end



function Row:getNoticeBarIcon(articleHistoryObj)

	local icon = maybeCallFunc(self.noticeBarIcon, articleHistoryObj, self)

	if icon == true then

		icon = self:getIcon(articleHistoryObj)

		if not icon then

			self:raiseError(

				self:message('row-error-missing-icon'),

				self:message('row-error-missing-icon-help')

			)

		end

	end

	return icon

end



function Row:getNoticeBarIconCaption(articleHistoryObj)

	local caption = maybeCallFunc(

		self.noticeBarIconCaption,

		articleHistoryObj,

		self

	)

	if not caption then

		caption = self:getIconCaption(articleHistoryObj)

	end

	return caption

end



function Row:getNoticeBarIconSize()

	return self.noticeBarIconSize or self.cfg.defaultNoticeBarIconSize or '15px'

end



function Row:exportNoticeBarIcon(articleHistoryObj)

	local icon = self:getNoticeBarIcon(articleHistoryObj)

	if not icon then

		return nil

	end

	return renderImage(

		icon,

		self:getNoticeBarIconCaption(articleHistoryObj),

		self:getNoticeBarIconSize()

	)

end



function Row:setText(text)

	self.text = text

end



function Row:getText(articleHistoryObj)

	return maybeCallFunc(self.text, articleHistoryObj, self)

end



function Row:exportHtml(articleHistoryObj)

	if self._html then

		return self._html

	end

	local text = self:getText(articleHistoryObj)

	if not text then

		return nil

	end

	local html = mw.html.create('tr')

	html

		:tag('td')

			:addClass('mbox-image')

			:wikitext(self:renderIcon(articleHistoryObj))

			:done()

		:tag('td')

			:addClass('mbox-text')

			:wikitext(text)

	self._html = html

	return html

end



function Row:setCategories(val)

	-- Set the categories from the object's config. val can be either an array

	-- of strings or a function returning an array of category objects.

	self.categories = val

end



function Row:getCategories(articleHistoryObj)

	local ret = {}

	if type(self.categories) == 'table' then

		for _, cat in ipairs(self.categories) do

			ret#ret + 1 = Category.new(cat)

		end

	elseif type(self.categories) == 'function' then

		local t = self.categories(articleHistoryObj, self) or {}

		for _, categoryObj in ipairs(t) do

			ret#ret + 1 = categoryObj

		end

	end

	return ret

end



-------------------------------------------------------------------------------

-- Status class

-- Status objects deal with possible current statuses of the article.

-------------------------------------------------------------------------------



local Status = setmetatable({}, Row)

Status.__index = Status



function Status.new(data)

	local obj = Row.new(data)

	setmetatable(obj, Status)



	obj.id = data.id

	obj.statusCfg = obj.cfg.statusesobj.id

	obj.name = obj.statusCfg.name

	obj:setIconValues(

		obj.statusCfg.icon,

		obj.statusCfg.iconCaption or obj.name,

		data.iconSize

	)

	obj:setNoticeBarIconValues(

		obj.statusCfg.noticeBarIcon,

		obj.statusCfg.noticeBarIconCaption or obj.name,

		obj.statusCfg.noticeBarIconSize

	)

	obj:setText(obj.statusCfg.text)

	obj:setCategories(obj.statusCfg.categories)



	return obj

end



function Status:getIconSize()

	return self.iconSize

		or self.statusCfg.iconSize

		or self.cfg.defaultStatusIconSize

		or '50px'

end



function Status:getText(articleHistoryObj)

	local text = Row.getText(self, articleHistoryObj)

	if text then

		return substituteParams(

			text,

			self.currentTitle.subjectPageTitle.prefixedText,

			self.currentTitle.text

		)

	end

end



-------------------------------------------------------------------------------

-- MultiStatus class

-- For when an article can have multiple distinct statuses, e.g. former

-- featured article status and good article status.

-------------------------------------------------------------------------------



local MultiStatus = setmetatable({}, Row)

MultiStatus.__index = MultiStatus



function MultiStatus.new(data)

	local obj = Row.new(data)

	setmetatable(obj, MultiStatus)



	obj.id = data.id

	obj.statusCfg = obj.cfg.statusesdata.id

	obj.name = obj.statusCfg.name



	-- Set child status objects

	local function getChildStatusData(data, id, iconSize)

		local ret = {}

		for k, v in pairs(data) do

			retk = v

		end

		ret.id = id

		ret.iconSize = iconSize

		return ret

	end

	obj.statuses = {}

	local defaultIconSize = obj.cfg.defaultMultiStatusIconSize or '30px'

	for _, id in ipairs(obj.statusCfg.statuses) do

		table.insert(obj.statuses, Status.new(getChildStatusData(

			data,

			id,

			obj.cfg.statusesid].iconMultiSize or defaultIconSize

		)))

	end



	return obj

end



function MultiStatus:exportHtml(articleHistoryObj)

	local ret = mw.html.create()

	for _, obj in ipairs(self.statuses) do

		ret:node(obj:exportHtml(articleHistoryObj))

	end

	return ret

end



function MultiStatus:getCategories(articleHistoryObj)

	local ret = {}

	for _, obj in ipairs(self.statuses) do

		for _, categoryObj in ipairs(obj:getCategories(articleHistoryObj)) do

			ret#ret + 1 = categoryObj

		end

	end

	return ret

end



function MultiStatus:exportNoticeBarIcon()

	local ret = {}

	for _, obj in ipairs(self.statuses) do

		ret#ret + 1 = obj:exportNoticeBarIcon()

	end

	return table.concat(ret)

end



function MultiStatus:getWarnings()

	local ret = {}

	for _, obj in ipairs(self.statuses) do

		for _, msg in ipairs(obj:getWarnings()) do

			ret#ret + 1 = msg

		end

	end

	return ret

end



-------------------------------------------------------------------------------

-- Notice class

-- Notice objects contain notices about an article that aren't part of its

-- current status, e.g. the date an article was featured on the main page.

-------------------------------------------------------------------------------



local Notice = setmetatable({}, Row)

Notice.__index = Notice



function Notice.new(data)

	local obj = Row.new(data)

	setmetatable(obj, Notice)



	obj:setIconValues(

		data.icon,

		data.iconCaption,

		data.iconSize

	)

	obj:setNoticeBarIconValues(

		data.noticeBarIcon,

		data.noticeBarIconCaption,

		data.noticeBarIconSize

	)

	obj:setText(data.text)

	obj:setCategories(data.categories)



	return obj

end



-------------------------------------------------------------------------------

-- Action class

-- Action objects deal with a single action in the history of the article. We

-- use getter methods rather than properties for the name and result, etc., as

-- their processing needs to be delayed until after the status object has been

-- initialised. The status object needs to parse the action objects when it is

-- initialised, and the value of some names, etc., in the action objects depend

-- on the status object, so this is necessary to avoid errors/infinite loops.

-------------------------------------------------------------------------------



local Action = setmetatable({}, Row)

Action.__index = Action



function Action.new(data)

	local obj = Row.new(data)

	setmetatable(obj, Action)



	obj.paramNum = data.paramNum



	-- Set the ID

	do

		if not data.code then

			obj:raiseError(

				obj:message('action-error-no-code', obj:getParameter('code')),

				obj:message('action-error-no-code-help')

			)

		end

		local code = mw.ustring.upper(data.code)

		obj.id = obj.cfg.actionscode and obj.cfg.actionscode].id

		if not obj.id then

			obj:raiseError(

				obj:message(

					'action-error-invalid-code',

					data.code,

					obj:getParameter('code')

				),

				obj:message('action-error-invalid-code-help')

			)

		end

	end



	-- Add a shortcut for this action's config.

	obj.actionCfg = obj.cfg.actionsobj.id



	-- Set the link

	obj.link = data.link or obj.currentTitle.talkPageTitle.prefixedText



	-- Set the result ID

	do

		local resultCode = data.resultCode

			and mw.ustring.lower(data.resultCode)

			or '_BLANK'

		if obj.actionCfg.resultsresultCode then

			obj.resultId = obj.actionCfg.resultsresultCode].id

		elseif resultCode == '_BLANK' then

			obj:raiseError(

				obj:message(

					'action-error-blank-result',

					obj.id,

					obj:getParameter('resultCode')

				),

				obj:message('action-error-blank-result-help')

			)

		else

			obj:raiseError(

				obj:message(

					'action-error-invalid-result',

					data.resultCode,

					obj.id,

					obj:getParameter('resultCode')

				),

				obj:message('action-error-invalid-result-help')

			)

		end

	end



	-- Set the date

	if data.date then

		local success, date = pcall(

			lang.formatDate,

			lang,

			obj:message('action-date-format'),

			data.date

		)

		if success and date then

			obj.date = date

		else

			obj:addWarning(

				obj:message(

					'action-warning-invalid-date',

					data.date,

					obj:getParameter('date')

				),

				obj:message('action-warning-invalid-date-help')

			)

		end

	else

		obj:addWarning(

			obj:message(

				'action-warning-no-date',

				obj.paramNum,

				obj:getParameter('date'),

				obj:getParameter('code')

			),

			obj:message('action-warning-no-date-help')

		)

	end

	obj.date = obj.date or obj:message('action-date-missing')



	-- Set the oldid

	obj.oldid = tonumber(data.oldid)

	if data.oldid and (not obj.oldid or not isPositiveInteger(obj.oldid)) then

		obj.oldid = nil

		obj:addWarning(

			obj:message(

				'action-warning-invalid-oldid',

				data.oldid,

				obj:getParameter('oldid')

			),

			obj:message('action-warning-invalid-oldid-help')

		)

	end



	-- Set the notice bar icon values

	obj:setNoticeBarIconValues(

		data.noticeBarIcon,

		data.noticeBarIconCaption,

		data.noticeBarIconSize

	)



	-- Set the categories

	obj:setCategories(obj.actionCfg.categories)



	return obj

end



function Action:getParameter(key)

	-- Finds the original parameter name for the given key that was passed to

	-- Action.new.

	local prefix = self.cfg.actionParamPrefix

	local suffix

	for k, v in pairs(self.cfg.actionParamSuffixes) do

		if v == key then

			suffix = k

			break

		end

	end

	if not suffix then

		error('invalid key "' .. tostring(key) .. '" passed to Action:getParameter', 2)

	end

	return prefix .. tostring(self.paramNum) .. suffix

end



function Action:getName(articleHistoryObj)

	return maybeCallFunc(self.actionCfg.name, articleHistoryObj, self)

end



function Action:getResult(articleHistoryObj)

	return maybeCallFunc(

		self.actionCfg.resultsself.resultId].text,

		articleHistoryObj,

		self

	)

end



function Action:exportHtml(articleHistoryObj)

	if self._html then

		return self._html

	end



	local row = mw.html.create('tr')



	-- Date cell

	local dateCell = row:tag('td')

	if self.oldid then

		dateCell

			:tag('span')

				:addClass('plainlinks')

				:wikitext(makeUrlLink(

					self.currentTitle.subjectPageTitle:fullUrl{oldid = self.oldid},

					self.date

				))

	else

		dateCell:wikitext(self.date)

	end



	-- Process cell

	row

		:tag('td')

			:wikitext(string.format(

				"'''[[%s|%s]]'''",

				self.link,

				self:getName(articleHistoryObj)

			))



	-- Result cell

	row

		:tag('td')

			:wikitext(self:getResult(articleHistoryObj))



	self._html = row

	return row

end



-------------------------------------------------------------------------------

-- CollapsibleNotice class

-- This class makes notices that go in the collapsible part of the template,

-- underneath the list of actions.

-------------------------------------------------------------------------------



local CollapsibleNotice = setmetatable({}, Row)

CollapsibleNotice.__index = CollapsibleNotice



function CollapsibleNotice.new(data)

	local obj = Row.new(data)

	setmetatable(obj, CollapsibleNotice)



	obj:setIconValues(

		data.icon,

		data.iconCaption,

		data.iconSize

	)

	obj:setNoticeBarIconValues(

		data.noticeBarIcon,

		data.noticeBarIconCaption,

		data.noticeBarIconSize

	)

	obj:setText(data.text)

	obj:setCollapsibleText(data.collapsibleText)

	obj:setCategories(data.categories)



	return obj

end



function CollapsibleNotice:setCollapsibleText(s)

	self.collapsibleText = s

end



function CollapsibleNotice:getCollapsibleText(articleHistoryObj)

	return maybeCallFunc(self.collapsibleText, articleHistoryObj, self)

end



function CollapsibleNotice:getIconSize()

	return self.iconSize

		or self.cfg.defaultCollapsibleNoticeIconSize

		or '20px'

end



function CollapsibleNotice:exportHtml(articleHistoryObj, isInCollapsibleTable)

	local cacheKey = isInCollapsibleTable

		and '_htmlCacheCollapsible'

		or '_htmlCacheDefault'

	return self:_cachedTry(cacheKey, '_isHtmlError', function ()

		local text = self:getText(articleHistoryObj)

		if not text then

			return nil

		end



		local function maybeMakeCollapsibleTable(cell, text, collapsibleText)

			-- If collapsible text is specified, makes a collapsible table

			-- inside the cell with two rows, a header row with one cell and a

			-- collapsed row with one cell. These are filled with text and

			-- collapsedText, respectively. If no collapsible text is

			-- specified, the text is added to the cell as-is.

			if collapsibleText then

				cell

					:tag('div')

						:addClass('mw-collapsible mw-collapsed')

						:tag('div')

							:wikitext(text)

							:done()

						:tag('div')

							:addClass('mw-collapsible-content')

							:css('border', '1px silver solid')

							:wikitext(collapsibleText)

			else

				cell:wikitext(text)

			end

		end



		local html = mw.html.create('tr')

		local icon = self:renderIcon(articleHistoryObj)

		local collapsibleText = self:getCollapsibleText(articleHistoryObj)

		if isInCollapsibleTable then

			local textCell = html:tag('td')

				:attr('colspan', 3)

				:css('width', '100%')

			local rowText

			if icon then

				rowText = icon .. ' ' .. text

			else

				rowText = text

			end

			maybeMakeCollapsibleTable(textCell, rowText, collapsibleText)

		else

			local textCell = html

				:tag('td')

					:addClass('mbox-image')

					:wikitext(icon)

					:done()

				:tag('td')

					:addClass('mbox-text')

			maybeMakeCollapsibleTable(textCell, text, collapsibleText)

		end



		return html

	end)

end



-------------------------------------------------------------------------------

-- ArticleHistory class

-- This class represents the whole template.

-------------------------------------------------------------------------------



local ArticleHistory = {}

ArticleHistory.__index = ArticleHistory

addMixin(ArticleHistory, Message)



function ArticleHistory.new(args, cfg, currentTitle)

	local obj = setmetatable({}, ArticleHistory)



	-- Set input

	obj.args = args or {}

	obj.currentTitle = currentTitle or mw.title.getCurrentTitle()



	-- Define object structure.

	obj._errors = {}

	obj._allObjectsCache = {}



	-- Format the config

	local function substituteAliases(t, ret)

		-- This function substitutes strings found in an "aliases" subtable

		-- as keys in the parent table. It works recursively, so "aliases"

		-- subtables can be placed at any level. It assumes that tables will

		-- not be nested recursively, which should be true in the case of our

		-- config file.

		ret = ret or {}

		for k, v in pairs(t) do

			if k ~= 'aliases' then

				if type(v) == 'table' then

					local newRet = {}

					retk = newRet

					if v.aliases then

						for _, alias in ipairs(v.aliases) do

							retalias = newRet

						end

					end

					substituteAliases(v, newRet)

				else

					retk = v

				end

			end

		end

		return ret

	end

	obj.cfg = substituteAliases(cfg or require(CONFIG_PAGE))



	--[[

	-- Get a table of the arguments sorted by prefix and number. Non-string

	-- keys and keys that don't contain a number are ignored. (This means that

	-- positional parameters are ignored, as they are numbers, not strings.)

	-- The parameter numbers are stored in the first positional parameter of

	-- the subtables, and any gaps are removed so that the tables can be

	-- iterated over with ipairs.

	--

	-- For example, these arguments:

	--   {a1x = 'eggs', a1y = 'spam', a2x = 'chips', b1z = 'beans', b3x = 'bacon'}

	-- would translate into this prefixArgs table.

	--   {

	--     a = {

	--       {1, x = 'eggs', y = 'spam'},

	--       {2, x = 'chips'}

	--     },

	--     b = {

	--       {1, z = 'beans'},

	--       {3, x = 'bacon'}

	--     }

	--   }

	--]]

	do

		local prefixArgs = {}

		for k, v in pairs(obj.args) do

			if type(k) == 'string' then

				local prefix, num, suffix = k:match('^(.-)([1-9][0-9]*)(.*)$')

				if prefix then

					num = tonumber(num)

					prefixArgsprefix = prefixArgsprefix or {}

					prefixArgsprefix][num = prefixArgsprefix][num or {}

					prefixArgsprefix][num][suffix = v

					prefixArgsprefix][num][1 = num

				end

			end

		end

		-- Remove the gaps

		local prefixArrays = {}

		for prefix, prefixTable in pairs(prefixArgs) do

			prefixArraysprefix = {}

			local numKeys = {}

			for num in pairs(prefixTable) do

				numKeys#numKeys + 1 = num

			end

			table.sort(numKeys)

			for _, num in ipairs(numKeys) do

				table.insert(prefixArraysprefix], prefixTablenum])

			end

		end

		obj.prefixArgs = prefixArrays

	end



	return obj

end



function ArticleHistory:try(func, ...)

	if DEBUG_MODE then

		local val = func(...)

		return val

	else

		local success, val = pcall(func, ...)

		if success then

			return val

		else

			table.insert(self._errors, val)

			return nil

		end

	end

end



function ArticleHistory:getActionObjects()

	-- Gets an array of action objects for the parameters specified by the

	-- user. We memoise this so that the parameters only have to be processed

	-- once.

	if self.actions then

		return self.actions

	end



	-- Get the action args, and exit if they don't exist.

	local actionArgs = self.prefixArgsself.cfg.actionParamPrefix

	if not actionArgs then

		self.actions = {}

		return self.actions

	end



	-- Make the objects.

	local actions = {}

	local suffixes = self.cfg.actionParamSuffixes

	for _, t in ipairs(actionArgs) do

		local objArgs = {}

		for k, v in pairs(t) do

			local newK = suffixesk

			if newK then

				objArgsnewK = v

			end

		end

		objArgs.paramNum = t1

		objArgs.cfg = self.cfg

		objArgs.currentTitle = self.currentTitle

		local actionObj = self:try(Action.new, objArgs)

		table.insert(actions, actionObj)

	end

	self.actions = actions

	return actions

end



function ArticleHistory:getStatusIdForCode(code)

	-- Gets a status ID given a status code. If no code is specified, returns

	-- nil, and if the code is invalid, raises an error.

	if not code then

		return nil

	end

	local statuses = self.cfg.statuses

	local codeUpper = mw.ustring.upper(code)

	if statusescodeUpper then

		return statusescodeUpper].id

	else

		self:addWarning(

			self:message('articlehistory-warning-invalid-status', code),

			self:message('articlehistory-warning-invalid-status-help')

		)

		return nil

	end

end



function ArticleHistory:getStatusObj()

	-- Get the status object for the current status.

	if self.statusObj == false then

		return nil

	elseif self.statusObj ~= nil then

		return self.statusObj

	end

	local statusId

	if self.cfg.getStatusIdFunction then

		statusId = self:try(self.cfg.getStatusIdFunction, self)

	else

		statusId = self:try(

			self.getStatusIdForCode, self,

			self.argsself.cfg.currentStatusParam

		)

	end

	if not statusId then

		self.statusObj = false

		return nil

	end



	-- Check that some actions were specified, and if not add a warning.

	local actions = self:getActionObjects()

	if #actions < 1 then

		self:addWarning(

			self:message('articlehistory-warning-status-no-actions'),

			self:message('articlehistory-warning-status-no-actions-help')

		)

	end



	-- Make a new status object.

	local statusObjData = {

		id = statusId,

		currentTitle = self.currentTitle,

		cfg = self.cfg

	}

	local isMulti = self.cfg.statusesstatusId].isMulti

	local initFunc = isMulti and MultiStatus.new or Status.new

	local statusObj = self:try(initFunc, statusObjData)

	self.statusObj = statusObj or false

	return self.statusObj or nil

end



function ArticleHistory:getStatusId()

	local statusObj = self:getStatusObj()

	return statusObj and statusObj.id

end



function ArticleHistory:_noticeFactory(memoizeKey, configKey, class)

	-- This holds the logic for fetching tables of Notice and CollapsibleNotice

	-- objects.

	if selfmemoizeKey then

		return selfmemoizeKey

	end

	local ret = {}

	for _, t in ipairs(self.cfgconfigKey or {}) do

		if t.isActive(self) then

			local data = {}

			for k, v in pairs(t) do

				if k ~= 'isActive' then

					datak = v

				end

			end

			data.cfg = self.cfg

			data.currentTitle = self.currentTitle

			ret#ret + 1 = class.new(data)

		end

	end

	selfmemoizeKey = ret

	return ret

end



function ArticleHistory:getNoticeObjects()

	return self:_noticeFactory('notices', 'notices', Notice)

end



function ArticleHistory:getCollapsibleNoticeObjects()

	return self:_noticeFactory(

		'collapsibleNotices',

		'collapsibleNotices',

		CollapsibleNotice

	)

end



function ArticleHistory:getAllObjects(addSelf)

	local cacheKey = addSelf and 'addSelf' or 'default'

	local ret = self._allObjectsCachecacheKey

	if not ret then

		ret = {}

		local statusObj = self:getStatusObj()

		if statusObj then

			ret#ret + 1 = statusObj

		end

		local objTables = {

			self:getNoticeObjects(),

			self:getActionObjects(),

			self:getCollapsibleNoticeObjects()

		}

		for _, t in ipairs(objTables) do

			for _, obj in ipairs(t) do

				ret#ret + 1 = obj

			end

		end

		if addSelf then

			ret#ret + 1 = self

		end

		self._allObjectsCachecacheKey = ret

	end

	return ret

end



function ArticleHistory:getNoticeBarIcons()

	local ret = {}

	-- Icons that aren't part of a row.

	if self.cfg.noticeBarIcons then

		for _, data in ipairs(self.cfg.noticeBarIcons) do

			if data.isActive(self) then

				ret#ret + 1 = renderImage(

					data.icon,

					nil,

					data.size or self.cfg.defaultNoticeBarIconSize

				)

			end

		end

	end

	-- Icons in row objects.

	for _, obj in ipairs(self:getAllObjects()) do

		ret#ret + 1 = obj:exportNoticeBarIcon(self)

	end

	return ret

end



function ArticleHistory:getErrorMessages()

	-- Returns an array of error/warning strings. Error strings come first.

	local ret = {}

	for _, msg in ipairs(self._errors) do

		ret#ret + 1 = msg

	end

	for _, obj in ipairs(self:getAllObjects(true)) do

		for _, msg in ipairs(obj:getWarnings()) do

			ret#ret + 1 = msg

		end

	end

	return ret

end



function ArticleHistory:categoriesAreActive()

	-- Returns a boolean indicating whether categories should be output or not.

	local title = self.currentTitle

	local ns = title.namespace

	return title.isTalkPage

		and ns ~= 3 -- not user talk

		and ns ~= 119 -- not draft talk

end



function ArticleHistory:renderCategories()

	local ret = {}



	if self:categoriesAreActive() then

		-- Child object categories

		for _, obj in ipairs(self:getAllObjects()) do

			local categories = self:try(obj.getCategories, obj, self)

			for _, categoryObj in ipairs(categories or {}) do

				ret#ret + 1 = tostring(categoryObj)

			end

		end



		-- Extra categories

		for _, func in ipairs(self.cfg.extraCategories or {}) do

			local cats = func(self) or {}

			for _, categoryObj in ipairs(cats) do

				ret#ret + 1 = tostring(categoryObj)

			end

		end

	end



	return table.concat(ret)

end



function ArticleHistory:__tostring()

	local root = mw.html.create()



	-- Table root

	local tableRoot = root:tag('table')

	tableRoot:addClass('article-history tmbox tmbox-notice')



	-- Status

	local statusObj = self:getStatusObj()

	if statusObj then

		tableRoot:node(self:try(statusObj.exportHtml, statusObj, self))

	end



	-- Notices

	local notices = self:getNoticeObjects()

	for _, noticeObj in ipairs(notices) do

		tableRoot:node(self:try(noticeObj.exportHtml, noticeObj, self))

	end



	-- Get action objects and the collapsible notice objects, and generate the

	-- HTML objects for the action objects. We need the action HTML objects so

	-- that we can accurately calculate the number of collapsible rows, as some

	-- action objects may generate errors when the HTML is generated.

	local actions = self:getActionObjects() or {}

	local collapsibleNotices = self:getCollapsibleNoticeObjects() or {}

	local collapsibleNoticeHtmlObjects, actionHtmlObjects = {}, {}

	for _, obj in ipairs(actions) do

		table.insert(

			actionHtmlObjects,

			self:try(obj.exportHtml, obj, self)

		)

	end

	for _, obj in ipairs(collapsibleNotices) do

		table.insert(

			collapsibleNoticeHtmlObjects,

			self:try(obj.exportHtml, obj, self, true) -- Render the collapsed version

		)

	end

	local nActionRows = #actionHtmlObjects

	local nCollapsibleRows = nActionRows + #collapsibleNoticeHtmlObjects



	-- Find out if we are collapsed or not.

	local isCollapsed = yesno(self.args.collapse)

	if isCollapsed == nil then

		if self.cfg.uncollapsedRows == 'all' then

			isCollapsed = false

		elseif nCollapsibleRows == 1 then

			isCollapsed = false

		else

			isCollapsed = nCollapsibleRows > (tonumber(self.cfg.uncollapsedRows) or 3)

		end

	end



	-- If we are not collapsed, re-render the collapsible notices in the

	-- non-collapsed version.

	if not isCollapsed then

		collapsibleNoticeHtmlObjects = {}

		for _, obj in ipairs(collapsibleNotices) do

			table.insert(

				collapsibleNoticeHtmlObjects,

				self:try(obj.exportHtml, obj, self, false)

			)

		end

	end



	-- Collapsible table for actions and collapsible notices. Collapsible

	-- notices are only included in the table if it is collapsed. Action rows

	-- are always included.

	local collapsibleTable

	if isCollapsed or nActionRows > 0 then

		-- Collapsible table base

		collapsibleTable = tableRoot

			:tag('tr')

				:tag('td')

					:attr('colspan', 2)

					:css('width', '100%')

					:tag('table')

						:addClass('article-history-milestones')

						:addClass(isCollapsed and 'mw-collapsible mw-collapsed' or nil)

						:css('width', '100%')

						:css('background', 'transparent')

						:css('font-size', '90%')



		-- Header row

		local ctHeader = collapsibleTable

			:tag('tr')

				:tag('th')

					:attr('colspan', 3)

					:css('font-size', '110%')



		-- Notice bar

		if isCollapsed then

			local noticeBarIcons = self:getNoticeBarIcons()

			if #noticeBarIcons > 0 then

				local noticeBar = ctHeader:tag('span'):css('float', 'left')

				for _, icon in ipairs(noticeBarIcons) do

					noticeBar:wikitext(icon)

				end

				ctHeader:wikitext(' ')

			end

		end



		-- Header text

		if mw.site.namespacesself.currentTitle.namespace].subject.id == 0 then

			ctHeader:wikitext(self:message('milestones-header'))

		else

			ctHeader:wikitext(self:message(

				'milestones-header-other-ns',

				self.currentTitle.subjectNsText

			))

		end



		-- Subheadings

		if nActionRows > 0 then

			collapsibleTable

				:tag('tr')

					:css('text-align', 'left')

					:tag('th')

						:wikitext(self:message('milestones-date-header'))

						:done()

					:tag('th')

						:wikitext(self:message('milestones-process-header'))

						:done()

					:tag('th')

						:wikitext(self:message('milestones-result-header'))

		end



		-- Actions

		for _, htmlObj in ipairs(actionHtmlObjects) do

			collapsibleTable:node(htmlObj)

		end

	end



	-- Collapsible notices and current status

	-- These are only included in the collapsible table if it is collapsed.

	-- Otherwise, they are added afterwards, so that they align with the

	-- notices.

	do

		local tableNode, statusColspan

		if isCollapsed then

			tableNode = collapsibleTable

			statusColspan = 3

		else

			tableNode = tableRoot

			statusColspan = 2

		end



		-- Collapsible notices

		for _, obj in ipairs(collapsibleNotices) do

			tableNode:node(self:try(obj.exportHtml, obj, self, isCollapsed))

		end



		-- Current status

		if statusObj and nActionRows > 1 then

			tableNode

				:tag('tr')

					:tag('td')

						:attr('colspan', statusColspan)

						:wikitext(self:message('status-blurb', statusObj.name))

		end

	end



	-- Get the categories. We have to do this before the error row, so that

	-- category errors display.

	local categories = self:renderCategories()



	-- Error row and error category

	local errors = self:getErrorMessages()

	local errorCategory

	if #errors > 0 then

		local errorList = tableRoot

			:tag('tr')

				:tag('td')

					:attr('colspan', 2)

					:addClass('mbox-text')

					:tag('ul')

						:addClass('error')

						:css('font-weight', 'bold')

		for _, msg in ipairs(errors) do

			errorList:tag('li'):wikitext(msg)

		end

		if self:categoriesAreActive() then

			errorCategory = tostring(Category.new(self:message(

				'error-category'

			)))

		end



	-- If there are no errors and no active objects, then exit. We can't make

	-- this check earlier as we don't know where the errors may be until we

	-- have finished rendering the banner.

	elseif #self:getAllObjects() < 1 then

		return ''

	end



	-- Add the categories

	root:wikitext(categories)

	root:wikitext(errorCategory)

	

	local frame = mw.getCurrentFrame()

	return frame:extensionTag{

		name = 'templatestyles', args = { src = 'Module:Message box/tmbox.css' }

	} .. frame:extensionTag{

		name = 'templatestyles', args = { src = 'Module:Article history/styles.css' }

	} .. tostring(root)

end



-------------------------------------------------------------------------------

-- Exports

-- These functions are called from Lua and from wikitext.

-------------------------------------------------------------------------------



local p = {}



function p._main(args, cfg, currentTitle)

	local articleHistoryObj = ArticleHistory.new(args, cfg, currentTitle)

	return tostring(articleHistoryObj)

end



function p.main(frame)

	local args = require('Module:Arguments').getArgs(frame, {

		wrappers = WRAPPER_TEMPLATE

	})

	if frame:getTitle():find('sandbox', 1, true) then

		CONFIG_PAGE = CONFIG_PAGE .. '/sandbox'

	end

	return p._main(args)

end



function p._exportClasses()

	return {

		Message = Message,

		Row = Row,

		Status = Status,

		MultiStatus = MultiStatus,

		Notice = Notice,

		Action = Action,

		CollapsibleNotice = CollapsibleNotice,

		ArticleHistory = ArticleHistory

	}

end



return p
From Wikipedia, the free encyclopedia

-------------------------------------------------------------------------------

--                            Article history

--

-- This module allows editors to link to all the significant events in an

-- article's history, such as good article nominations and featured article

-- nominations. It also displays its current status, as well as other

-- information, such as the date it was featured on the main page.

-------------------------------------------------------------------------------



local CONFIG_PAGE = 'Module:Article history/config'

local WRAPPER_TEMPLATE = 'Template:Article history'

local DEBUG_MODE = false -- If true, errors are not caught.



-- Load required modules.

require('strict')

local Category = require('Module:Article history/Category')

local yesno = require('Module:Yesno')

local lang = mw.language.getContentLanguage()



-------------------------------------------------------------------------------

-- Helper functions

-------------------------------------------------------------------------------



local function isPositiveInteger(num)

	return type(num) == 'number'

		and math.floor(num) == num

		and num > 0

		and num < math.huge

end



local function substituteParams(msg, ...)

	return mw.message.newRawMessage(msg, ...):plain()

end



local function makeUrlLink(url, display)

	return string.format('[%s %s]', url, display)

end



local function maybeCallFunc(val, ...)

	-- Checks whether val is a function, and if so calls it with the specified

	-- arguments. Otherwise val is returned as-is.

	if type(val) == 'function' then

		return val(...)

	else

		return val

	end

end



local function renderImage(image, caption, size)

	if caption then

		caption = '|' .. caption

	else

		caption = ''

	end

	return string.format('[[File:%s|%s%s]]', image, size, caption)

end



local function addMixin(class, mixin)

	-- Add a mixin to a class. The functions will be shared across classes, so

	-- don't use it for functions that keep state.

	for name, method in pairs(mixin) do

		classname = method

	end

end



-------------------------------------------------------------------------------

-- Message mixin

-- This mixin is used by all classes to add message-related methods.

-------------------------------------------------------------------------------



local Message = {}



function Message:message(key, ...)

	-- This fetches the message from the config with the specified key, and

	-- substitutes parameters $1, $2 etc. with the subsequent values it is

	-- passed.

	local msg = self.cfg.msgkey

	if select('#', ...) > 0 then

		return substituteParams(msg, ...)

	else

		return msg

	end

end



function Message:raiseError(msg, help)

	-- Raises an error with the specified message and help link. Execution

	-- stops unless the error is caught. This is used for errors where

	-- subsequent processing becomes impossible.

	local errorText

	if help then

		errorText = self:message('error-message-help', msg, help)

	else

		errorText = self:message('error-message-nohelp', msg)

	end

	error(errorText, 0)

end



function Message:addWarning(msg, help)

	-- Adds a warning to the object's warnings table. Execution continues as

	-- normal. This is used for errors that should be fixed but that do not

	-- prevent the module from outputting something useful.

	self.warnings = self.warnings or {}

	local warningText

	if help then

		warningText = self:message('warning-help', msg, help)

	else

		warningText = self:message('warning-nohelp', msg)

	end

	table.insert(self.warnings, warningText)

end



function Message:getWarnings()

	return self.warnings or {}

end



-------------------------------------------------------------------------------

-- Row class

-- This class represents one row in the template.

-------------------------------------------------------------------------------



local Row = {}

Row.__index = Row

addMixin(Row, Message)



function Row.new(data)

	local obj = setmetatable({}, Row)

	obj.cfg = data.cfg

	obj.currentTitle = data.currentTitle

	obj.makeData = data.makeData -- used by Row:getData

	return obj

end



function Row:_cachedTry(cacheKey, errorCacheKey, func)

	-- This method is for use in Row object methods that are called more than

	-- once. The results of such methods should be cached to avoid unnecessary

	-- processing. We also cache any errors found and abort if an error was

	-- raised previously, otherwise error messages could be displayed multiple

	-- times.

	--

	-- We use false as a key to cache nil results, so func cannot return false.

	--

	-- @param cacheKey The key to cache successful results with

	-- @param errorCacheKey The key to cache errors with

	-- @param func an anonymous function that returns the method result

	if selferrorCacheKey then

		return nil

	end

	local ret = selfcacheKey

	if ret then

		return ret

	elseif ret == false then

		return nil

	end

	local success

	if DEBUG_MODE then

		success = true

		ret = func()

	else

		success, ret = pcall(func)

	end

	if success then

		if ret then

			selfcacheKey = ret

			return ret

		else

			selfcacheKey = false

			return nil

		end

	else

		selferrorCacheKey = true

		-- We have already formatted the error message, so no need to format it

		-- again.

		error(ret, 0)

	end

end



function Row:getData(articleHistoryObj)

	return self:_cachedTry('_dataCache', '_isDataError', function ()

		return self.makeData(articleHistoryObj)

	end)

end



function Row:setIconValues(icon, caption, size)

	self.icon = icon

	self.iconCaption = caption

	self.iconSize = size

end



function Row:getIcon(articleHistoryObj)

	return maybeCallFunc(self.icon, articleHistoryObj, self)

end



function Row:getIconCaption(articleHistoryObj)

	return maybeCallFunc(self.iconCaption, articleHistoryObj, self)

end



function Row:getIconSize()

	return self.iconSize or self.cfg.defaultIconSize or '30px'

end



function Row:renderIcon(articleHistoryObj)

	local icon = self:getIcon(articleHistoryObj)

	if not icon then

		return nil

	end

	return renderImage(

		icon,

		self:getIconCaption(articleHistoryObj),

		self:getIconSize()

	)

end



function Row:setNoticeBarIconValues(icon, caption, size)

	self.noticeBarIcon = icon

	self.noticeBarIconCaption = caption

	self.noticeBarIconSize = size

end



function Row:getNoticeBarIcon(articleHistoryObj)

	local icon = maybeCallFunc(self.noticeBarIcon, articleHistoryObj, self)

	if icon == true then

		icon = self:getIcon(articleHistoryObj)

		if not icon then

			self:raiseError(

				self:message('row-error-missing-icon'),

				self:message('row-error-missing-icon-help')

			)

		end

	end

	return icon

end



function Row:getNoticeBarIconCaption(articleHistoryObj)

	local caption = maybeCallFunc(

		self.noticeBarIconCaption,

		articleHistoryObj,

		self

	)

	if not caption then

		caption = self:getIconCaption(articleHistoryObj)

	end

	return caption

end



function Row:getNoticeBarIconSize()

	return self.noticeBarIconSize or self.cfg.defaultNoticeBarIconSize or '15px'

end



function Row:exportNoticeBarIcon(articleHistoryObj)

	local icon = self:getNoticeBarIcon(articleHistoryObj)

	if not icon then

		return nil

	end

	return renderImage(

		icon,

		self:getNoticeBarIconCaption(articleHistoryObj),

		self:getNoticeBarIconSize()

	)

end



function Row:setText(text)

	self.text = text

end



function Row:getText(articleHistoryObj)

	return maybeCallFunc(self.text, articleHistoryObj, self)

end



function Row:exportHtml(articleHistoryObj)

	if self._html then

		return self._html

	end

	local text = self:getText(articleHistoryObj)

	if not text then

		return nil

	end

	local html = mw.html.create('tr')

	html

		:tag('td')

			:addClass('mbox-image')

			:wikitext(self:renderIcon(articleHistoryObj))

			:done()

		:tag('td')

			:addClass('mbox-text')

			:wikitext(text)

	self._html = html

	return html

end



function Row:setCategories(val)

	-- Set the categories from the object's config. val can be either an array

	-- of strings or a function returning an array of category objects.

	self.categories = val

end



function Row:getCategories(articleHistoryObj)

	local ret = {}

	if type(self.categories) == 'table' then

		for _, cat in ipairs(self.categories) do

			ret#ret + 1 = Category.new(cat)

		end

	elseif type(self.categories) == 'function' then

		local t = self.categories(articleHistoryObj, self) or {}

		for _, categoryObj in ipairs(t) do

			ret#ret + 1 = categoryObj

		end

	end

	return ret

end



-------------------------------------------------------------------------------

-- Status class

-- Status objects deal with possible current statuses of the article.

-------------------------------------------------------------------------------



local Status = setmetatable({}, Row)

Status.__index = Status



function Status.new(data)

	local obj = Row.new(data)

	setmetatable(obj, Status)



	obj.id = data.id

	obj.statusCfg = obj.cfg.statusesobj.id

	obj.name = obj.statusCfg.name

	obj:setIconValues(

		obj.statusCfg.icon,

		obj.statusCfg.iconCaption or obj.name,

		data.iconSize

	)

	obj:setNoticeBarIconValues(

		obj.statusCfg.noticeBarIcon,

		obj.statusCfg.noticeBarIconCaption or obj.name,

		obj.statusCfg.noticeBarIconSize

	)

	obj:setText(obj.statusCfg.text)

	obj:setCategories(obj.statusCfg.categories)



	return obj

end



function Status:getIconSize()

	return self.iconSize

		or self.statusCfg.iconSize

		or self.cfg.defaultStatusIconSize

		or '50px'

end



function Status:getText(articleHistoryObj)

	local text = Row.getText(self, articleHistoryObj)

	if text then

		return substituteParams(

			text,

			self.currentTitle.subjectPageTitle.prefixedText,

			self.currentTitle.text

		)

	end

end



-------------------------------------------------------------------------------

-- MultiStatus class

-- For when an article can have multiple distinct statuses, e.g. former

-- featured article status and good article status.

-------------------------------------------------------------------------------



local MultiStatus = setmetatable({}, Row)

MultiStatus.__index = MultiStatus



function MultiStatus.new(data)

	local obj = Row.new(data)

	setmetatable(obj, MultiStatus)



	obj.id = data.id

	obj.statusCfg = obj.cfg.statusesdata.id

	obj.name = obj.statusCfg.name



	-- Set child status objects

	local function getChildStatusData(data, id, iconSize)

		local ret = {}

		for k, v in pairs(data) do

			retk = v

		end

		ret.id = id

		ret.iconSize = iconSize

		return ret

	end

	obj.statuses = {}

	local defaultIconSize = obj.cfg.defaultMultiStatusIconSize or '30px'

	for _, id in ipairs(obj.statusCfg.statuses) do

		table.insert(obj.statuses, Status.new(getChildStatusData(

			data,

			id,

			obj.cfg.statusesid].iconMultiSize or defaultIconSize

		)))

	end



	return obj

end



function MultiStatus:exportHtml(articleHistoryObj)

	local ret = mw.html.create()

	for _, obj in ipairs(self.statuses) do

		ret:node(obj:exportHtml(articleHistoryObj))

	end

	return ret

end



function MultiStatus:getCategories(articleHistoryObj)

	local ret = {}

	for _, obj in ipairs(self.statuses) do

		for _, categoryObj in ipairs(obj:getCategories(articleHistoryObj)) do

			ret#ret + 1 = categoryObj

		end

	end

	return ret

end



function MultiStatus:exportNoticeBarIcon()

	local ret = {}

	for _, obj in ipairs(self.statuses) do

		ret#ret + 1 = obj:exportNoticeBarIcon()

	end

	return table.concat(ret)

end



function MultiStatus:getWarnings()

	local ret = {}

	for _, obj in ipairs(self.statuses) do

		for _, msg in ipairs(obj:getWarnings()) do

			ret#ret + 1 = msg

		end

	end

	return ret

end



-------------------------------------------------------------------------------

-- Notice class

-- Notice objects contain notices about an article that aren't part of its

-- current status, e.g. the date an article was featured on the main page.

-------------------------------------------------------------------------------



local Notice = setmetatable({}, Row)

Notice.__index = Notice



function Notice.new(data)

	local obj = Row.new(data)

	setmetatable(obj, Notice)



	obj:setIconValues(

		data.icon,

		data.iconCaption,

		data.iconSize

	)

	obj:setNoticeBarIconValues(

		data.noticeBarIcon,

		data.noticeBarIconCaption,

		data.noticeBarIconSize

	)

	obj:setText(data.text)

	obj:setCategories(data.categories)



	return obj

end



-------------------------------------------------------------------------------

-- Action class

-- Action objects deal with a single action in the history of the article. We

-- use getter methods rather than properties for the name and result, etc., as

-- their processing needs to be delayed until after the status object has been

-- initialised. The status object needs to parse the action objects when it is

-- initialised, and the value of some names, etc., in the action objects depend

-- on the status object, so this is necessary to avoid errors/infinite loops.

-------------------------------------------------------------------------------



local Action = setmetatable({}, Row)

Action.__index = Action



function Action.new(data)

	local obj = Row.new(data)

	setmetatable(obj, Action)



	obj.paramNum = data.paramNum



	-- Set the ID

	do

		if not data.code then

			obj:raiseError(

				obj:message('action-error-no-code', obj:getParameter('code')),

				obj:message('action-error-no-code-help')

			)

		end

		local code = mw.ustring.upper(data.code)

		obj.id = obj.cfg.actionscode and obj.cfg.actionscode].id

		if not obj.id then

			obj:raiseError(

				obj:message(

					'action-error-invalid-code',

					data.code,

					obj:getParameter('code')

				),

				obj:message('action-error-invalid-code-help')

			)

		end

	end



	-- Add a shortcut for this action's config.

	obj.actionCfg = obj.cfg.actionsobj.id



	-- Set the link

	obj.link = data.link or obj.currentTitle.talkPageTitle.prefixedText



	-- Set the result ID

	do

		local resultCode = data.resultCode

			and mw.ustring.lower(data.resultCode)

			or '_BLANK'

		if obj.actionCfg.resultsresultCode then

			obj.resultId = obj.actionCfg.resultsresultCode].id

		elseif resultCode == '_BLANK' then

			obj:raiseError(

				obj:message(

					'action-error-blank-result',

					obj.id,

					obj:getParameter('resultCode')

				),

				obj:message('action-error-blank-result-help')

			)

		else

			obj:raiseError(

				obj:message(

					'action-error-invalid-result',

					data.resultCode,

					obj.id,

					obj:getParameter('resultCode')

				),

				obj:message('action-error-invalid-result-help')

			)

		end

	end



	-- Set the date

	if data.date then

		local success, date = pcall(

			lang.formatDate,

			lang,

			obj:message('action-date-format'),

			data.date

		)

		if success and date then

			obj.date = date

		else

			obj:addWarning(

				obj:message(

					'action-warning-invalid-date',

					data.date,

					obj:getParameter('date')

				),

				obj:message('action-warning-invalid-date-help')

			)

		end

	else

		obj:addWarning(

			obj:message(

				'action-warning-no-date',

				obj.paramNum,

				obj:getParameter('date'),

				obj:getParameter('code')

			),

			obj:message('action-warning-no-date-help')

		)

	end

	obj.date = obj.date or obj:message('action-date-missing')



	-- Set the oldid

	obj.oldid = tonumber(data.oldid)

	if data.oldid and (not obj.oldid or not isPositiveInteger(obj.oldid)) then

		obj.oldid = nil

		obj:addWarning(

			obj:message(

				'action-warning-invalid-oldid',

				data.oldid,

				obj:getParameter('oldid')

			),

			obj:message('action-warning-invalid-oldid-help')

		)

	end



	-- Set the notice bar icon values

	obj:setNoticeBarIconValues(

		data.noticeBarIcon,

		data.noticeBarIconCaption,

		data.noticeBarIconSize

	)



	-- Set the categories

	obj:setCategories(obj.actionCfg.categories)



	return obj

end



function Action:getParameter(key)

	-- Finds the original parameter name for the given key that was passed to

	-- Action.new.

	local prefix = self.cfg.actionParamPrefix

	local suffix

	for k, v in pairs(self.cfg.actionParamSuffixes) do

		if v == key then

			suffix = k

			break

		end

	end

	if not suffix then

		error('invalid key "' .. tostring(key) .. '" passed to Action:getParameter', 2)

	end

	return prefix .. tostring(self.paramNum) .. suffix

end



function Action:getName(articleHistoryObj)

	return maybeCallFunc(self.actionCfg.name, articleHistoryObj, self)

end



function Action:getResult(articleHistoryObj)

	return maybeCallFunc(

		self.actionCfg.resultsself.resultId].text,

		articleHistoryObj,

		self

	)

end



function Action:exportHtml(articleHistoryObj)

	if self._html then

		return self._html

	end



	local row = mw.html.create('tr')



	-- Date cell

	local dateCell = row:tag('td')

	if self.oldid then

		dateCell

			:tag('span')

				:addClass('plainlinks')

				:wikitext(makeUrlLink(

					self.currentTitle.subjectPageTitle:fullUrl{oldid = self.oldid},

					self.date

				))

	else

		dateCell:wikitext(self.date)

	end



	-- Process cell

	row

		:tag('td')

			:wikitext(string.format(

				"'''[[%s|%s]]'''",

				self.link,

				self:getName(articleHistoryObj)

			))



	-- Result cell

	row

		:tag('td')

			:wikitext(self:getResult(articleHistoryObj))



	self._html = row

	return row

end



-------------------------------------------------------------------------------

-- CollapsibleNotice class

-- This class makes notices that go in the collapsible part of the template,

-- underneath the list of actions.

-------------------------------------------------------------------------------



local CollapsibleNotice = setmetatable({}, Row)

CollapsibleNotice.__index = CollapsibleNotice



function CollapsibleNotice.new(data)

	local obj = Row.new(data)

	setmetatable(obj, CollapsibleNotice)



	obj:setIconValues(

		data.icon,

		data.iconCaption,

		data.iconSize

	)

	obj:setNoticeBarIconValues(

		data.noticeBarIcon,

		data.noticeBarIconCaption,

		data.noticeBarIconSize

	)

	obj:setText(data.text)

	obj:setCollapsibleText(data.collapsibleText)

	obj:setCategories(data.categories)



	return obj

end



function CollapsibleNotice:setCollapsibleText(s)

	self.collapsibleText = s

end



function CollapsibleNotice:getCollapsibleText(articleHistoryObj)

	return maybeCallFunc(self.collapsibleText, articleHistoryObj, self)

end



function CollapsibleNotice:getIconSize()

	return self.iconSize

		or self.cfg.defaultCollapsibleNoticeIconSize

		or '20px'

end



function CollapsibleNotice:exportHtml(articleHistoryObj, isInCollapsibleTable)

	local cacheKey = isInCollapsibleTable

		and '_htmlCacheCollapsible'

		or '_htmlCacheDefault'

	return self:_cachedTry(cacheKey, '_isHtmlError', function ()

		local text = self:getText(articleHistoryObj)

		if not text then

			return nil

		end



		local function maybeMakeCollapsibleTable(cell, text, collapsibleText)

			-- If collapsible text is specified, makes a collapsible table

			-- inside the cell with two rows, a header row with one cell and a

			-- collapsed row with one cell. These are filled with text and

			-- collapsedText, respectively. If no collapsible text is

			-- specified, the text is added to the cell as-is.

			if collapsibleText then

				cell

					:tag('div')

						:addClass('mw-collapsible mw-collapsed')

						:tag('div')

							:wikitext(text)

							:done()

						:tag('div')

							:addClass('mw-collapsible-content')

							:css('border', '1px silver solid')

							:wikitext(collapsibleText)

			else

				cell:wikitext(text)

			end

		end



		local html = mw.html.create('tr')

		local icon = self:renderIcon(articleHistoryObj)

		local collapsibleText = self:getCollapsibleText(articleHistoryObj)

		if isInCollapsibleTable then

			local textCell = html:tag('td')

				:attr('colspan', 3)

				:css('width', '100%')

			local rowText

			if icon then

				rowText = icon .. ' ' .. text

			else

				rowText = text

			end

			maybeMakeCollapsibleTable(textCell, rowText, collapsibleText)

		else

			local textCell = html

				:tag('td')

					:addClass('mbox-image')

					:wikitext(icon)

					:done()

				:tag('td')

					:addClass('mbox-text')

			maybeMakeCollapsibleTable(textCell, text, collapsibleText)

		end



		return html

	end)

end



-------------------------------------------------------------------------------

-- ArticleHistory class

-- This class represents the whole template.

-------------------------------------------------------------------------------



local ArticleHistory = {}

ArticleHistory.__index = ArticleHistory

addMixin(ArticleHistory, Message)



function ArticleHistory.new(args, cfg, currentTitle)

	local obj = setmetatable({}, ArticleHistory)



	-- Set input

	obj.args = args or {}

	obj.currentTitle = currentTitle or mw.title.getCurrentTitle()



	-- Define object structure.

	obj._errors = {}

	obj._allObjectsCache = {}



	-- Format the config

	local function substituteAliases(t, ret)

		-- This function substitutes strings found in an "aliases" subtable

		-- as keys in the parent table. It works recursively, so "aliases"

		-- subtables can be placed at any level. It assumes that tables will

		-- not be nested recursively, which should be true in the case of our

		-- config file.

		ret = ret or {}

		for k, v in pairs(t) do

			if k ~= 'aliases' then

				if type(v) == 'table' then

					local newRet = {}

					retk = newRet

					if v.aliases then

						for _, alias in ipairs(v.aliases) do

							retalias = newRet

						end

					end

					substituteAliases(v, newRet)

				else

					retk = v

				end

			end

		end

		return ret

	end

	obj.cfg = substituteAliases(cfg or require(CONFIG_PAGE))



	--[[

	-- Get a table of the arguments sorted by prefix and number. Non-string

	-- keys and keys that don't contain a number are ignored. (This means that

	-- positional parameters are ignored, as they are numbers, not strings.)

	-- The parameter numbers are stored in the first positional parameter of

	-- the subtables, and any gaps are removed so that the tables can be

	-- iterated over with ipairs.

	--

	-- For example, these arguments:

	--   {a1x = 'eggs', a1y = 'spam', a2x = 'chips', b1z = 'beans', b3x = 'bacon'}

	-- would translate into this prefixArgs table.

	--   {

	--     a = {

	--       {1, x = 'eggs', y = 'spam'},

	--       {2, x = 'chips'}

	--     },

	--     b = {

	--       {1, z = 'beans'},

	--       {3, x = 'bacon'}

	--     }

	--   }

	--]]

	do

		local prefixArgs = {}

		for k, v in pairs(obj.args) do

			if type(k) == 'string' then

				local prefix, num, suffix = k:match('^(.-)([1-9][0-9]*)(.*)$')

				if prefix then

					num = tonumber(num)

					prefixArgsprefix = prefixArgsprefix or {}

					prefixArgsprefix][num = prefixArgsprefix][num or {}

					prefixArgsprefix][num][suffix = v

					prefixArgsprefix][num][1 = num

				end

			end

		end

		-- Remove the gaps

		local prefixArrays = {}

		for prefix, prefixTable in pairs(prefixArgs) do

			prefixArraysprefix = {}

			local numKeys = {}

			for num in pairs(prefixTable) do

				numKeys#numKeys + 1 = num

			end

			table.sort(numKeys)

			for _, num in ipairs(numKeys) do

				table.insert(prefixArraysprefix], prefixTablenum])

			end

		end

		obj.prefixArgs = prefixArrays

	end



	return obj

end



function ArticleHistory:try(func, ...)

	if DEBUG_MODE then

		local val = func(...)

		return val

	else

		local success, val = pcall(func, ...)

		if success then

			return val

		else

			table.insert(self._errors, val)

			return nil

		end

	end

end



function ArticleHistory:getActionObjects()

	-- Gets an array of action objects for the parameters specified by the

	-- user. We memoise this so that the parameters only have to be processed

	-- once.

	if self.actions then

		return self.actions

	end



	-- Get the action args, and exit if they don't exist.

	local actionArgs = self.prefixArgsself.cfg.actionParamPrefix

	if not actionArgs then

		self.actions = {}

		return self.actions

	end



	-- Make the objects.

	local actions = {}

	local suffixes = self.cfg.actionParamSuffixes

	for _, t in ipairs(actionArgs) do

		local objArgs = {}

		for k, v in pairs(t) do

			local newK = suffixesk

			if newK then

				objArgsnewK = v

			end

		end

		objArgs.paramNum = t1

		objArgs.cfg = self.cfg

		objArgs.currentTitle = self.currentTitle

		local actionObj = self:try(Action.new, objArgs)

		table.insert(actions, actionObj)

	end

	self.actions = actions

	return actions

end



function ArticleHistory:getStatusIdForCode(code)

	-- Gets a status ID given a status code. If no code is specified, returns

	-- nil, and if the code is invalid, raises an error.

	if not code then

		return nil

	end

	local statuses = self.cfg.statuses

	local codeUpper = mw.ustring.upper(code)

	if statusescodeUpper then

		return statusescodeUpper].id

	else

		self:addWarning(

			self:message('articlehistory-warning-invalid-status', code),

			self:message('articlehistory-warning-invalid-status-help')

		)

		return nil

	end

end



function ArticleHistory:getStatusObj()

	-- Get the status object for the current status.

	if self.statusObj == false then

		return nil

	elseif self.statusObj ~= nil then

		return self.statusObj

	end

	local statusId

	if self.cfg.getStatusIdFunction then

		statusId = self:try(self.cfg.getStatusIdFunction, self)

	else

		statusId = self:try(

			self.getStatusIdForCode, self,

			self.argsself.cfg.currentStatusParam

		)

	end

	if not statusId then

		self.statusObj = false

		return nil

	end



	-- Check that some actions were specified, and if not add a warning.

	local actions = self:getActionObjects()

	if #actions < 1 then

		self:addWarning(

			self:message('articlehistory-warning-status-no-actions'),

			self:message('articlehistory-warning-status-no-actions-help')

		)

	end



	-- Make a new status object.

	local statusObjData = {

		id = statusId,

		currentTitle = self.currentTitle,

		cfg = self.cfg

	}

	local isMulti = self.cfg.statusesstatusId].isMulti

	local initFunc = isMulti and MultiStatus.new or Status.new

	local statusObj = self:try(initFunc, statusObjData)

	self.statusObj = statusObj or false

	return self.statusObj or nil

end



function ArticleHistory:getStatusId()

	local statusObj = self:getStatusObj()

	return statusObj and statusObj.id

end



function ArticleHistory:_noticeFactory(memoizeKey, configKey, class)

	-- This holds the logic for fetching tables of Notice and CollapsibleNotice

	-- objects.

	if selfmemoizeKey then

		return selfmemoizeKey

	end

	local ret = {}

	for _, t in ipairs(self.cfgconfigKey or {}) do

		if t.isActive(self) then

			local data = {}

			for k, v in pairs(t) do

				if k ~= 'isActive' then

					datak = v

				end

			end

			data.cfg = self.cfg

			data.currentTitle = self.currentTitle

			ret#ret + 1 = class.new(data)

		end

	end

	selfmemoizeKey = ret

	return ret

end



function ArticleHistory:getNoticeObjects()

	return self:_noticeFactory('notices', 'notices', Notice)

end



function ArticleHistory:getCollapsibleNoticeObjects()

	return self:_noticeFactory(

		'collapsibleNotices',

		'collapsibleNotices',

		CollapsibleNotice

	)

end



function ArticleHistory:getAllObjects(addSelf)

	local cacheKey = addSelf and 'addSelf' or 'default'

	local ret = self._allObjectsCachecacheKey

	if not ret then

		ret = {}

		local statusObj = self:getStatusObj()

		if statusObj then

			ret#ret + 1 = statusObj

		end

		local objTables = {

			self:getNoticeObjects(),

			self:getActionObjects(),

			self:getCollapsibleNoticeObjects()

		}

		for _, t in ipairs(objTables) do

			for _, obj in ipairs(t) do

				ret#ret + 1 = obj

			end

		end

		if addSelf then

			ret#ret + 1 = self

		end

		self._allObjectsCachecacheKey = ret

	end

	return ret

end



function ArticleHistory:getNoticeBarIcons()

	local ret = {}

	-- Icons that aren't part of a row.

	if self.cfg.noticeBarIcons then

		for _, data in ipairs(self.cfg.noticeBarIcons) do

			if data.isActive(self) then

				ret#ret + 1 = renderImage(

					data.icon,

					nil,

					data.size or self.cfg.defaultNoticeBarIconSize

				)

			end

		end

	end

	-- Icons in row objects.

	for _, obj in ipairs(self:getAllObjects()) do

		ret#ret + 1 = obj:exportNoticeBarIcon(self)

	end

	return ret

end



function ArticleHistory:getErrorMessages()

	-- Returns an array of error/warning strings. Error strings come first.

	local ret = {}

	for _, msg in ipairs(self._errors) do

		ret#ret + 1 = msg

	end

	for _, obj in ipairs(self:getAllObjects(true)) do

		for _, msg in ipairs(obj:getWarnings()) do

			ret#ret + 1 = msg

		end

	end

	return ret

end



function ArticleHistory:categoriesAreActive()

	-- Returns a boolean indicating whether categories should be output or not.

	local title = self.currentTitle

	local ns = title.namespace

	return title.isTalkPage

		and ns ~= 3 -- not user talk

		and ns ~= 119 -- not draft talk

end



function ArticleHistory:renderCategories()

	local ret = {}



	if self:categoriesAreActive() then

		-- Child object categories

		for _, obj in ipairs(self:getAllObjects()) do

			local categories = self:try(obj.getCategories, obj, self)

			for _, categoryObj in ipairs(categories or {}) do

				ret#ret + 1 = tostring(categoryObj)

			end

		end



		-- Extra categories

		for _, func in ipairs(self.cfg.extraCategories or {}) do

			local cats = func(self) or {}

			for _, categoryObj in ipairs(cats) do

				ret#ret + 1 = tostring(categoryObj)

			end

		end

	end



	return table.concat(ret)

end



function ArticleHistory:__tostring()

	local root = mw.html.create()



	-- Table root

	local tableRoot = root:tag('table')

	tableRoot:addClass('article-history tmbox tmbox-notice')



	-- Status

	local statusObj = self:getStatusObj()

	if statusObj then

		tableRoot:node(self:try(statusObj.exportHtml, statusObj, self))

	end



	-- Notices

	local notices = self:getNoticeObjects()

	for _, noticeObj in ipairs(notices) do

		tableRoot:node(self:try(noticeObj.exportHtml, noticeObj, self))

	end



	-- Get action objects and the collapsible notice objects, and generate the

	-- HTML objects for the action objects. We need the action HTML objects so

	-- that we can accurately calculate the number of collapsible rows, as some

	-- action objects may generate errors when the HTML is generated.

	local actions = self:getActionObjects() or {}

	local collapsibleNotices = self:getCollapsibleNoticeObjects() or {}

	local collapsibleNoticeHtmlObjects, actionHtmlObjects = {}, {}

	for _, obj in ipairs(actions) do

		table.insert(

			actionHtmlObjects,

			self:try(obj.exportHtml, obj, self)

		)

	end

	for _, obj in ipairs(collapsibleNotices) do

		table.insert(

			collapsibleNoticeHtmlObjects,

			self:try(obj.exportHtml, obj, self, true) -- Render the collapsed version

		)

	end

	local nActionRows = #actionHtmlObjects

	local nCollapsibleRows = nActionRows + #collapsibleNoticeHtmlObjects



	-- Find out if we are collapsed or not.

	local isCollapsed = yesno(self.args.collapse)

	if isCollapsed == nil then

		if self.cfg.uncollapsedRows == 'all' then

			isCollapsed = false

		elseif nCollapsibleRows == 1 then

			isCollapsed = false

		else

			isCollapsed = nCollapsibleRows > (tonumber(self.cfg.uncollapsedRows) or 3)

		end

	end



	-- If we are not collapsed, re-render the collapsible notices in the

	-- non-collapsed version.

	if not isCollapsed then

		collapsibleNoticeHtmlObjects = {}

		for _, obj in ipairs(collapsibleNotices) do

			table.insert(

				collapsibleNoticeHtmlObjects,

				self:try(obj.exportHtml, obj, self, false)

			)

		end

	end



	-- Collapsible table for actions and collapsible notices. Collapsible

	-- notices are only included in the table if it is collapsed. Action rows

	-- are always included.

	local collapsibleTable

	if isCollapsed or nActionRows > 0 then

		-- Collapsible table base

		collapsibleTable = tableRoot

			:tag('tr')

				:tag('td')

					:attr('colspan', 2)

					:css('width', '100%')

					:tag('table')

						:addClass('article-history-milestones')

						:addClass(isCollapsed and 'mw-collapsible mw-collapsed' or nil)

						:css('width', '100%')

						:css('background', 'transparent')

						:css('font-size', '90%')



		-- Header row

		local ctHeader = collapsibleTable

			:tag('tr')

				:tag('th')

					:attr('colspan', 3)

					:css('font-size', '110%')



		-- Notice bar

		if isCollapsed then

			local noticeBarIcons = self:getNoticeBarIcons()

			if #noticeBarIcons > 0 then

				local noticeBar = ctHeader:tag('span'):css('float', 'left')

				for _, icon in ipairs(noticeBarIcons) do

					noticeBar:wikitext(icon)

				end

				ctHeader:wikitext(' ')

			end

		end



		-- Header text

		if mw.site.namespacesself.currentTitle.namespace].subject.id == 0 then

			ctHeader:wikitext(self:message('milestones-header'))

		else

			ctHeader:wikitext(self:message(

				'milestones-header-other-ns',

				self.currentTitle.subjectNsText

			))

		end



		-- Subheadings

		if nActionRows > 0 then

			collapsibleTable

				:tag('tr')

					:css('text-align', 'left')

					:tag('th')

						:wikitext(self:message('milestones-date-header'))

						:done()

					:tag('th')

						:wikitext(self:message('milestones-process-header'))

						:done()

					:tag('th')

						:wikitext(self:message('milestones-result-header'))

		end



		-- Actions

		for _, htmlObj in ipairs(actionHtmlObjects) do

			collapsibleTable:node(htmlObj)

		end

	end



	-- Collapsible notices and current status

	-- These are only included in the collapsible table if it is collapsed.

	-- Otherwise, they are added afterwards, so that they align with the

	-- notices.

	do

		local tableNode, statusColspan

		if isCollapsed then

			tableNode = collapsibleTable

			statusColspan = 3

		else

			tableNode = tableRoot

			statusColspan = 2

		end



		-- Collapsible notices

		for _, obj in ipairs(collapsibleNotices) do

			tableNode:node(self:try(obj.exportHtml, obj, self, isCollapsed))

		end



		-- Current status

		if statusObj and nActionRows > 1 then

			tableNode

				:tag('tr')

					:tag('td')

						:attr('colspan', statusColspan)

						:wikitext(self:message('status-blurb', statusObj.name))

		end

	end



	-- Get the categories. We have to do this before the error row, so that

	-- category errors display.

	local categories = self:renderCategories()



	-- Error row and error category

	local errors = self:getErrorMessages()

	local errorCategory

	if #errors > 0 then

		local errorList = tableRoot

			:tag('tr')

				:tag('td')

					:attr('colspan', 2)

					:addClass('mbox-text')

					:tag('ul')

						:addClass('error')

						:css('font-weight', 'bold')

		for _, msg in ipairs(errors) do

			errorList:tag('li'):wikitext(msg)

		end

		if self:categoriesAreActive() then

			errorCategory = tostring(Category.new(self:message(

				'error-category'

			)))

		end



	-- If there are no errors and no active objects, then exit. We can't make

	-- this check earlier as we don't know where the errors may be until we

	-- have finished rendering the banner.

	elseif #self:getAllObjects() < 1 then

		return ''

	end



	-- Add the categories

	root:wikitext(categories)

	root:wikitext(errorCategory)

	

	local frame = mw.getCurrentFrame()

	return frame:extensionTag{

		name = 'templatestyles', args = { src = 'Module:Message box/tmbox.css' }

	} .. frame:extensionTag{

		name = 'templatestyles', args = { src = 'Module:Article history/styles.css' }

	} .. tostring(root)

end



-------------------------------------------------------------------------------

-- Exports

-- These functions are called from Lua and from wikitext.

-------------------------------------------------------------------------------



local p = {}



function p._main(args, cfg, currentTitle)

	local articleHistoryObj = ArticleHistory.new(args, cfg, currentTitle)

	return tostring(articleHistoryObj)

end



function p.main(frame)

	local args = require('Module:Arguments').getArgs(frame, {

		wrappers = WRAPPER_TEMPLATE

	})

	if frame:getTitle():find('sandbox', 1, true) then

		CONFIG_PAGE = CONFIG_PAGE .. '/sandbox'

	end

	return p._main(args)

end



function p._exportClasses()

	return {

		Message = Message,

		Row = Row,

		Status = Status,

		MultiStatus = MultiStatus,

		Notice = Notice,

		Action = Action,

		CollapsibleNotice = CollapsibleNotice,

		ArticleHistory = ArticleHistory

	}

end



return p

Videos

Youtube | Vimeo | Bing

Websites

Google | Yahoo | Bing

Encyclopedia

Google | Yahoo | Bing

Facebook