Module:Article history/sandbox explained

--------------------------------------------------------------------------------- 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.hugeend

local function substituteParams(msg, ...) return mw.message.newRawMessage(msg, ...):plainend

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 endend

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 endend

--------------------------------------------------------------------------------- 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 endend

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 = RowaddMixin(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 objend

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) endend

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 = sizeend

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 = sizeend

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 iconend

function Row:getNoticeBarIconCaption(articleHistoryObj) local caption = maybeCallFunc(self.noticeBarIconCaption, articleHistoryObj, self ) if not caption then caption = self:getIconCaption(articleHistoryObj) end return captionend

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 = textend

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 htmlend

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 = valend

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 retend

--------------------------------------------------------------------------------- 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 objend

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 ) endend

--------------------------------------------------------------------------------- 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 objend

function MultiStatus:exportHtml(articleHistoryObj) local ret = mw.html.create for _, obj in ipairs(self.statuses) do ret:node(obj:exportHtml(articleHistoryObj)) end return retend

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 retend

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 retend

--------------------------------------------------------------------------------- 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 objend

--------------------------------------------------------------------------------- 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 objend

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) .. suffixend

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, 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 rowend

--------------------------------------------------------------------------------- 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 objend

function CollapsibleNotice:setCollapsibleText(s) self.collapsibleText = send

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 = ArticleHistoryaddMixin(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))

-- 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 objend

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 endend

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 actionsend

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 endend

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 = 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 nilend

function ArticleHistory:getStatusId local statusObj = self:getStatusObj return statusObj and statusObj.idend

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 retend

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 = 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 retend

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 retend

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 retend

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 talkend

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('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 .. frame:extensionTag .. 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,) if frame:getTitle:find('sandbox', 1, true) then CONFIG_PAGE = CONFIG_PAGE .. '/sandbox' end return p._main(args)end

function p._exportClasses return end

return p