Module:Article history

--- --                           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('', 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		class[name] = 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.msg[key] 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 self[errorCacheKey] then return nil end local ret = self[cacheKey] 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 self[cacheKey] = ret return ret else self[cacheKey] = false return nil end else self[errorCacheKey] = 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.statuses[obj.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.statuses[data.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			ret[k] = 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.statuses[id].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.actions[code] and obj.cfg.actions[code].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.actions[obj.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.results[resultCode] then obj.resultId = obj.actionCfg.results[resultCode].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.results[self.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", 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 = {} ret[k] = newRet if v.aliases then for _, alias in ipairs(v.aliases) do							ret[alias] = newRet end end substituteAliases(v, newRet) else ret[k] = 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) prefixArgs[prefix] = prefixArgs[prefix] or {} prefixArgs[prefix][num] = prefixArgs[prefix][num] or {} prefixArgs[prefix][num][suffix] = v					prefixArgs[prefix][num][1] = num end end end -- Remove the gaps local prefixArrays = {} for prefix, prefixTable in pairs(prefixArgs) do			prefixArrays[prefix] = {} local numKeys = {} for num in pairs(prefixTable) do				numKeys[#numKeys + 1] = num end table.sort(numKeys) for _, num in ipairs(numKeys) do				table.insert(prefixArrays[prefix], prefixTable[num]) 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.prefixArgs[self.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 = suffixes[k] if newK then objArgs[newK] = v			end end objArgs.paramNum = t[1] 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 statuses[codeUpper] then return statuses[codeUpper].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.args[self.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.statuses[statusId].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 self[memoizeKey] then return self[memoizeKey] end local ret = {} for _, t in ipairs(self.cfg[configKey] or {}) do		if t.isActive(self) then local data = {} for k, v in pairs(t) do				if k ~= 'isActive' then data[k] = v				end end data.cfg = self.cfg data.currentTitle = self.currentTitle ret[#ret + 1] = class.new(data) end end self[memoizeKey] = 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._allObjectsCache[cacheKey] 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._allObjectsCache[cacheKey] = 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.namespaces[self.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