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) == numand num > 0and num < math.hugeendlocal function substituteParams(msg, ...)return mw.message.newRawMessage(msg, ...):plain()endlocal function makeUrlLink(url, display)return string.format('[%s %s]', url, display)endlocal 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' thenreturn val(...)elsereturn valendendlocal function renderImage(image, caption, size)if caption thencaption = '|' .. captionelsecaption = ''endreturn string.format('[[File:%s|%s%s]]', image, size, caption)endlocal 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) doclass[name] = methodendend--------------------------------------------------------------------------------- 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 thenreturn substituteParams(msg, ...)elsereturn msgendendfunction 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 errorTextif help thenerrorText = self:message('error-message-help', msg, help)elseerrorText = self:message('error-message-nohelp', msg)enderror(errorText, 0)endfunction 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 warningTextif help thenwarningText = self:message('warning-help', msg, help)elsewarningText = self:message('warning-nohelp', msg)endtable.insert(self.warnings, warningText)endfunction 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.cfgobj.currentTitle = data.currentTitleobj.makeData = data.makeData -- used by Row:getDatareturn objendfunction 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 resultif self[errorCacheKey] thenreturn nilendlocal ret = self[cacheKey]if ret thenreturn retelseif ret == false thenreturn nilendlocal successif DEBUG_MODE thensuccess = trueret = func()elsesuccess, ret = pcall(func)endif success thenif ret thenself[cacheKey] = retreturn retelseself[cacheKey] = falsereturn nilendelseself[errorCacheKey] = true-- We have already formatted the error message, so no need to format it-- again.error(ret, 0)endendfunction Row:getData(articleHistoryObj)return self:_cachedTry('_dataCache', '_isDataError', function ()return self.makeData(articleHistoryObj)end)endfunction Row:setIconValues(icon, caption, size)self.icon = iconself.iconCaption = captionself.iconSize = sizeendfunction Row:getIcon(articleHistoryObj)return maybeCallFunc(self.icon, articleHistoryObj, self)endfunction Row:getIconCaption(articleHistoryObj)return maybeCallFunc(self.iconCaption, articleHistoryObj, self)endfunction Row:getIconSize()return self.iconSize or self.cfg.defaultIconSize or '30px'endfunction Row:renderIcon(articleHistoryObj)local icon = self:getIcon(articleHistoryObj)if not icon thenreturn nilendreturn renderImage(icon,self:getIconCaption(articleHistoryObj),self:getIconSize())endfunction Row:setNoticeBarIconValues(icon, caption, size)self.noticeBarIcon = iconself.noticeBarIconCaption = captionself.noticeBarIconSize = sizeendfunction Row:getNoticeBarIcon(articleHistoryObj)local icon = maybeCallFunc(self.noticeBarIcon, articleHistoryObj, self)if icon == true thenicon = self:getIcon(articleHistoryObj)if not icon thenself:raiseError(self:message('row-error-missing-icon'),self:message('row-error-missing-icon-help'))endendreturn iconendfunction Row:getNoticeBarIconCaption(articleHistoryObj)local caption = maybeCallFunc(self.noticeBarIconCaption,articleHistoryObj,self)if not caption thencaption = self:getIconCaption(articleHistoryObj)endreturn captionendfunction Row:getNoticeBarIconSize()return self.noticeBarIconSize or self.cfg.defaultNoticeBarIconSize or '15px'endfunction Row:exportNoticeBarIcon(articleHistoryObj)local icon = self:getNoticeBarIcon(articleHistoryObj)if not icon thenreturn nilendreturn renderImage(icon,self:getNoticeBarIconCaption(articleHistoryObj),self:getNoticeBarIconSize())endfunction Row:setText(text)self.text = textendfunction Row:getText(articleHistoryObj)return maybeCallFunc(self.text, articleHistoryObj, self)endfunction Row:exportHtml(articleHistoryObj)if self._html thenreturn self._htmlendlocal text = self:getText(articleHistoryObj)if not text thenreturn nilendlocal 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 = htmlreturn htmlendfunction 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 = valendfunction Row:getCategories(articleHistoryObj)local ret = {}if type(self.categories) == 'table' thenfor _, cat in ipairs(self.categories) doret[#ret + 1] = Category.new(cat)endelseif type(self.categories) == 'function' thenlocal t = self.categories(articleHistoryObj, self) or {}for _, categoryObj in ipairs(t) doret[#ret + 1] = categoryObjendendreturn retend--------------------------------------------------------------------------------- Status class-- Status objects deal with possible current statuses of the article.-------------------------------------------------------------------------------local Status = setmetatable({}, Row)Status.__index = Statusfunction Status.new(data)local obj = Row.new(data)setmetatable(obj, Status)obj.id = data.idobj.statusCfg = obj.cfg.statuses[obj.id]obj.name = obj.statusCfg.nameobj: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 objendfunction Status:getIconSize()return self.iconSizeor self.statusCfg.iconSizeor self.cfg.defaultStatusIconSizeor '50px'endfunction Status:getText(articleHistoryObj)local text = Row.getText(self, articleHistoryObj)if text thenreturn 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 = MultiStatusfunction MultiStatus.new(data)local obj = Row.new(data)setmetatable(obj, MultiStatus)obj.id = data.idobj.statusCfg = obj.cfg.statuses[data.id]obj.name = obj.statusCfg.name-- Set child status objectslocal function getChildStatusData(data, id, iconSize)local ret = {}for k, v in pairs(data) doret[k] = vendret.id = idret.iconSize = iconSizereturn retendobj.statuses = {}local defaultIconSize = obj.cfg.defaultMultiStatusIconSize or '30px'for _, id in ipairs(obj.statusCfg.statuses) dotable.insert(obj.statuses, Status.new(getChildStatusData(data,id,obj.cfg.statuses[id].iconMultiSize or defaultIconSize)))endreturn objendfunction MultiStatus:exportHtml(articleHistoryObj)local ret = mw.html.create()for _, obj in ipairs(self.statuses) doret:node(obj:exportHtml(articleHistoryObj))endreturn retendfunction MultiStatus:getCategories(articleHistoryObj)local ret = {}for _, obj in ipairs(self.statuses) dofor _, categoryObj in ipairs(obj:getCategories(articleHistoryObj)) doret[#ret + 1] = categoryObjendendreturn retendfunction MultiStatus:exportNoticeBarIcon()local ret = {}for _, obj in ipairs(self.statuses) doret[#ret + 1] = obj:exportNoticeBarIcon()endreturn table.concat(ret)endfunction MultiStatus:getWarnings()local ret = {}for _, obj in ipairs(self.statuses) dofor _, msg in ipairs(obj:getWarnings()) doret[#ret + 1] = msgendendreturn 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 = Noticefunction 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 = Actionfunction Action.new(data)local obj = Row.new(data)setmetatable(obj, Action)obj.paramNum = data.paramNum-- Set the IDdoif not data.code thenobj:raiseError(obj:message('action-error-no-code', obj:getParameter('code')),obj:message('action-error-no-code-help'))endlocal code = mw.ustring.upper(data.code)obj.id = obj.cfg.actions[code] and obj.cfg.actions[code].idif not obj.id thenobj:raiseError(obj:message('action-error-invalid-code',data.code,obj:getParameter('code')),obj:message('action-error-invalid-code-help'))endend-- Add a shortcut for this action's config.obj.actionCfg = obj.cfg.actions[obj.id]-- Set the linkobj.link = data.link or obj.currentTitle.talkPageTitle.prefixedText-- Set the result IDdolocal resultCode = data.resultCodeand mw.ustring.lower(data.resultCode)or '_BLANK'if obj.actionCfg.results[resultCode] thenobj.resultId = obj.actionCfg.results[resultCode].idelseif resultCode == '_BLANK' thenobj:raiseError(obj:message('action-error-blank-result',obj.id,obj:getParameter('resultCode')),obj:message('action-error-blank-result-help'))elseobj:raiseError(obj:message('action-error-invalid-result',data.resultCode,obj.id,obj:getParameter('resultCode')),obj:message('action-error-invalid-result-help'))endend-- Set the dateif data.date thenlocal success, date = pcall(lang.formatDate,lang,obj:message('action-date-format'),data.date)if success and date thenobj.date = dateelseobj:addWarning(obj:message('action-warning-invalid-date',data.date,obj:getParameter('date')),obj:message('action-warning-invalid-date-help'))endelseobj:addWarning(obj:message('action-warning-no-date',obj.paramNum,obj:getParameter('date'),obj:getParameter('code')),obj:message('action-warning-no-date-help'))endobj.date = obj.date or obj:message('action-date-missing')-- Set the oldidobj.oldid = tonumber(data.oldid)if data.oldid and (not obj.oldid or not isPositiveInteger(obj.oldid)) thenobj.oldid = nilobj: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 valuesobj:setNoticeBarIconValues(data.noticeBarIcon,data.noticeBarIconCaption,data.noticeBarIconSize)-- Set the categoriesobj:setCategories(obj.actionCfg.categories)return objendfunction Action:getParameter(key)-- Finds the original parameter name for the given key that was passed to-- Action.new.local prefix = self.cfg.actionParamPrefixlocal suffixfor k, v in pairs(self.cfg.actionParamSuffixes) doif v == key thensuffix = kbreakendendif not suffix thenerror('invalid key "' .. tostring(key) .. '" passed to Action:getParameter', 2)endreturn prefix .. tostring(self.paramNum) .. suffixendfunction Action:getName(articleHistoryObj)return maybeCallFunc(self.actionCfg.name, articleHistoryObj, self)endfunction Action:getResult(articleHistoryObj)return maybeCallFunc(self.actionCfg.results[self.resultId].text,articleHistoryObj,self)endfunction Action:exportHtml(articleHistoryObj)if self._html thenreturn self._htmlendlocal row = mw.html.create('tr')-- Date celllocal dateCell = row:tag('td')if self.oldid thendateCell:tag('span'):addClass('plainlinks'):wikitext(makeUrlLink(self.currentTitle.subjectPageTitle:fullUrl{oldid = self.oldid},self.date))elsedateCell:wikitext(self.date)end-- Process cellrow:tag('td'):wikitext(string.format("'''[[%s|%s]]'''",self.link,self:getName(articleHistoryObj)))-- Result cellrow:tag('td'):wikitext(self:getResult(articleHistoryObj))self._html = rowreturn 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 = CollapsibleNoticefunction 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 objendfunction CollapsibleNotice:setCollapsibleText(s)self.collapsibleText = sendfunction CollapsibleNotice:getCollapsibleText(articleHistoryObj)return maybeCallFunc(self.collapsibleText, articleHistoryObj, self)endfunction CollapsibleNotice:getIconSize()return self.iconSizeor self.cfg.defaultCollapsibleNoticeIconSizeor '20px'endfunction CollapsibleNotice:exportHtml(articleHistoryObj, isInCollapsibleTable)local cacheKey = isInCollapsibleTableand '_htmlCacheCollapsible'or '_htmlCacheDefault'return self:_cachedTry(cacheKey, '_isHtmlError', function ()local text = self:getText(articleHistoryObj)if not text thenreturn nilendlocal 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 thencell: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)elsecell:wikitext(text)endendlocal html = mw.html.create('tr')local icon = self:renderIcon(articleHistoryObj)local collapsibleText = self:getCollapsibleText(articleHistoryObj)if isInCollapsibleTable thenlocal textCell = html:tag('td'):attr('colspan', 3):css('width', '100%')local rowTextif icon thenrowText = icon .. ' ' .. textelserowText = textendmaybeMakeCollapsibleTable(textCell, rowText, collapsibleText)elselocal textCell = html:tag('td'):addClass('mbox-image'):wikitext(icon):done():tag('td'):addClass('mbox-text')maybeMakeCollapsibleTable(textCell, text, collapsibleText)endreturn htmlend)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 inputobj.args = args or {}obj.currentTitle = currentTitle or mw.title.getCurrentTitle()-- Define object structure.obj._errors = {}obj._allObjectsCache = {}-- Format the configlocal 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) doif k ~= 'aliases' thenif type(v) == 'table' thenlocal newRet = {}ret[k] = newRetif v.aliases thenfor _, alias in ipairs(v.aliases) doret[alias] = newRetendendsubstituteAliases(v, newRet)elseret[k] = vendendendreturn retendobj.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'}--     }--   }--]]dolocal prefixArgs = {}for k, v in pairs(obj.args) doif type(k) == 'string' thenlocal prefix, num, suffix = k:match('^(.-)([1-9][0-9]*)(.*)$')if prefix thennum = tonumber(num)prefixArgs[prefix] = prefixArgs[prefix] or {}prefixArgs[prefix][num] = prefixArgs[prefix][num] or {}prefixArgs[prefix][num][suffix] = vprefixArgs[prefix][num][1] = numendendend-- Remove the gapslocal prefixArrays = {}for prefix, prefixTable in pairs(prefixArgs) doprefixArrays[prefix] = {}local numKeys = {}for num in pairs(prefixTable) donumKeys[#numKeys + 1] = numendtable.sort(numKeys)for _, num in ipairs(numKeys) dotable.insert(prefixArrays[prefix], prefixTable[num])endendobj.prefixArgs = prefixArraysendreturn objendfunction ArticleHistory:try(func, ...)if DEBUG_MODE thenlocal val = func(...)return valelselocal success, val = pcall(func, ...)if success thenreturn valelsetable.insert(self._errors, val)return nilendendendfunction 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 thenreturn self.actionsend-- Get the action args, and exit if they don't exist.local actionArgs = self.prefixArgs[self.cfg.actionParamPrefix]if not actionArgs thenself.actions = {}return self.actionsend-- Make the objects.local actions = {}local suffixes = self.cfg.actionParamSuffixesfor _, t in ipairs(actionArgs) dolocal objArgs = {}for k, v in pairs(t) dolocal newK = suffixes[k]if newK thenobjArgs[newK] = vendendobjArgs.paramNum = t[1]objArgs.cfg = self.cfgobjArgs.currentTitle = self.currentTitlelocal actionObj = self:try(Action.new, objArgs)table.insert(actions, actionObj)endself.actions = actionsreturn actionsendfunction 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 thenreturn nilendlocal statuses = self.cfg.statuseslocal codeUpper = mw.ustring.upper(code)if statuses[codeUpper] thenreturn statuses[codeUpper].idelseself:addWarning(self:message('articlehistory-warning-invalid-status', code),self:message('articlehistory-warning-invalid-status-help'))return nilendendfunction ArticleHistory:getStatusObj()-- Get the status object for the current status.if self.statusObj == false thenreturn nilelseif self.statusObj ~= nil thenreturn self.statusObjendlocal statusIdif self.cfg.getStatusIdFunction thenstatusId = self:try(self.cfg.getStatusIdFunction, self)elsestatusId = self:try(self.getStatusIdForCode, self,self.args[self.cfg.currentStatusParam])endif not statusId thenself.statusObj = falsereturn nilend-- Check that some actions were specified, and if not add a warning.local actions = self:getActionObjects()if #actions < 1 thenself: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].isMultilocal initFunc = isMulti and MultiStatus.new or Status.newlocal statusObj = self:try(initFunc, statusObjData)self.statusObj = statusObj or falsereturn self.statusObj or nilendfunction ArticleHistory:getStatusId()local statusObj = self:getStatusObj()return statusObj and statusObj.idendfunction ArticleHistory:_noticeFactory(memoizeKey, configKey, class)-- This holds the logic for fetching tables of Notice and CollapsibleNotice-- objects.if self[memoizeKey] thenreturn self[memoizeKey]endlocal ret = {}for _, t in ipairs(self.cfg[configKey] or {}) doif t.isActive(self) thenlocal data = {}for k, v in pairs(t) doif k ~= 'isActive' thendata[k] = vendenddata.cfg = self.cfgdata.currentTitle = self.currentTitleret[#ret + 1] = class.new(data)endendself[memoizeKey] = retreturn retendfunction ArticleHistory:getNoticeObjects()return self:_noticeFactory('notices', 'notices', Notice)endfunction ArticleHistory:getCollapsibleNoticeObjects()return self:_noticeFactory('collapsibleNotices','collapsibleNotices',CollapsibleNotice)endfunction ArticleHistory:getAllObjects(addSelf)local cacheKey = addSelf and 'addSelf' or 'default'local ret = self._allObjectsCache[cacheKey]if not ret thenret = {}local statusObj = self:getStatusObj()if statusObj thenret[#ret + 1] = statusObjendlocal objTables = {self:getNoticeObjects(),self:getActionObjects(),self:getCollapsibleNoticeObjects()}for _, t in ipairs(objTables) dofor _, obj in ipairs(t) doret[#ret + 1] = objendendif addSelf thenret[#ret + 1] = selfendself._allObjectsCache[cacheKey] = retendreturn retendfunction ArticleHistory:getNoticeBarIcons()local ret = {}-- Icons that aren't part of a row.if self.cfg.noticeBarIcons thenfor _, data in ipairs(self.cfg.noticeBarIcons) doif data.isActive(self) thenret[#ret + 1] = renderImage(data.icon,nil,data.size or self.cfg.defaultNoticeBarIconSize)endendend-- Icons in row objects.for _, obj in ipairs(self:getAllObjects()) doret[#ret + 1] = obj:exportNoticeBarIcon(self)endreturn retendfunction ArticleHistory:getErrorMessages()-- Returns an array of error/warning strings. Error strings come first.local ret = {}for _, msg in ipairs(self._errors) doret[#ret + 1] = msgendfor _, obj in ipairs(self:getAllObjects(true)) dofor _, msg in ipairs(obj:getWarnings()) doret[#ret + 1] = msgendendreturn retendfunction ArticleHistory:categoriesAreActive()-- Returns a boolean indicating whether categories should be output or not.local title = self.currentTitlelocal ns = title.namespacereturn title.isTalkPageand ns ~= 3 -- not user talkand ns ~= 119 -- not draft talkendfunction ArticleHistory:renderCategories()local ret = {}if self:categoriesAreActive() then-- Child object categoriesfor _, obj in ipairs(self:getAllObjects()) dolocal categories = self:try(obj.getCategories, obj, self)for _, categoryObj in ipairs(categories or {}) doret[#ret + 1] = tostring(categoryObj)endend-- Extra categoriesfor _, func in ipairs(self.cfg.extraCategories or {}) dolocal cats = func(self) or {}for _, categoryObj in ipairs(cats) doret[#ret + 1] = tostring(categoryObj)endendendreturn table.concat(ret)endfunction ArticleHistory:__tostring()local root = mw.html.create()-- Table rootlocal tableRoot = root:tag('table')tableRoot:addClass('article-history tmbox tmbox-notice')-- Statuslocal statusObj = self:getStatusObj()if statusObj thentableRoot:node(self:try(statusObj.exportHtml, statusObj, self))end-- Noticeslocal notices = self:getNoticeObjects()for _, noticeObj in ipairs(notices) dotableRoot: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) dotable.insert(actionHtmlObjects,self:try(obj.exportHtml, obj, self))endfor _, obj in ipairs(collapsibleNotices) dotable.insert(collapsibleNoticeHtmlObjects,self:try(obj.exportHtml, obj, self, true) -- Render the collapsed version)endlocal nActionRows = #actionHtmlObjectslocal nCollapsibleRows = nActionRows + #collapsibleNoticeHtmlObjects-- Find out if we are collapsed or not.local isCollapsed = yesno(self.args.collapse)if isCollapsed == nil thenif self.cfg.uncollapsedRows == 'all' thenisCollapsed = falseelseif nCollapsibleRows == 1 thenisCollapsed = falseelseisCollapsed = nCollapsibleRows > (tonumber(self.cfg.uncollapsedRows) or 3)endend-- If we are not collapsed, re-render the collapsible notices in the-- non-collapsed version.if not isCollapsed thencollapsibleNoticeHtmlObjects = {}for _, obj in ipairs(collapsibleNotices) dotable.insert(collapsibleNoticeHtmlObjects,self:try(obj.exportHtml, obj, self, false))endend-- 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 collapsibleTableif isCollapsed or nActionRows > 0 then-- Collapsible table basecollapsibleTable = 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 rowlocal ctHeader = collapsibleTable:tag('tr'):tag('th'):attr('colspan', 3):css('font-size', '110%')-- Notice barif isCollapsed thenlocal noticeBarIcons = self:getNoticeBarIcons()if #noticeBarIcons > 0 thenlocal noticeBar = ctHeader:tag('span'):css('float', 'left')for _, icon in ipairs(noticeBarIcons) donoticeBar:wikitext(icon)endctHeader:wikitext(' ')endend-- Header textif mw.site.namespaces[self.currentTitle.namespace].subject.id == 0 thenctHeader:wikitext(self:message('milestones-header'))elsectHeader:wikitext(self:message('milestones-header-other-ns',self.currentTitle.subjectNsText))end-- Subheadingsif nActionRows > 0 thencollapsibleTable: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-- Actionsfor _, htmlObj in ipairs(actionHtmlObjects) docollapsibleTable:node(htmlObj)endend-- 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.dolocal tableNode, statusColspanif isCollapsed thentableNode = collapsibleTablestatusColspan = 3elsetableNode = tableRootstatusColspan = 2end-- Collapsible noticesfor _, obj in ipairs(collapsibleNotices) dotableNode:node(self:try(obj.exportHtml, obj, self, isCollapsed))end-- Current statusif statusObj and nActionRows > 1 thentableNode:tag('tr'):tag('td'):attr('colspan', statusColspan):wikitext(self:message('status-blurb', statusObj.name))endend-- 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 categorylocal errors = self:getErrorMessages()local errorCategoryif #errors > 0 thenlocal 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) doerrorList:tag('li'):wikitext(msg)endif self:categoriesAreActive() thenerrorCategory = 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 thenreturn ''end-- Add the categoriesroot: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)endfunction p.main(frame)local args = require('Module:Arguments').getArgs(frame, {wrappers = WRAPPER_TEMPLATE})if frame:getTitle():find('sandbox', 1, true) thenCONFIG_PAGE = CONFIG_PAGE .. '/sandbox'endreturn p._main(args)endfunction p._exportClasses()return {Message = Message,Row = Row,Status = Status,MultiStatus = MultiStatus,Notice = Notice,Action = Action,CollapsibleNotice = CollapsibleNotice,ArticleHistory = ArticleHistory}endreturn p