Module:Article history
File:Full-protection-shackle.svg | This module is subject to page protection. It is a highly visible module in use by a very large number of pages, or is substituted very frequently. Because vandalism or mistakes would affect many pages, and even trivial editing might cause substantial load on the servers, it is protected from editing. |
File:Lua-Logo.svg | This module depends on the following other modules: |
Script error: No such module "Uses TemplateStyles". This module implements Template:Article history (edit | talk | history | links | watch | logs). Please see the template page for documentation of how to use it.
Technical details
This module has a configuration module at Module:Article history/config, which can be used to translate/modify the template for use on other wikis. See the config module for instructions on how to modify it. The configuration for the English Wikipedia is very complicated, but it is also possible to use it much more simply. If you want to use the more advanced features provided by the module, you can consult the class documentation below.
There is also a Category class at Module:Article history/Category, which is used by both the main module and the config module.
Class documentation
This is technical documentation for Lua programmers who are looking to adapt this module for use on different wikis.
Category
The Category class is used to generate all of the module's categories. It is loaded in both Module:Article history and Module:Article history/config. You can create a category with Category.new:
Category.new(cat, sort)
The cat variable is the category text, and sort is its sort key.
Once category objects are created, they have the following properties:
category
- the category namesortKey
- the sort key
They can be rendered into category links by calling tostring()
on them.
Message
The Message mixin contains common message-related methods which are available in all classes apart from the Category class. These methods are:
Message:message(key, ...)
- fetches a message with the key key from the config's msg table, and substitutes parameters $1, $2 etc. with the subsequent values it is passed.Message:raiseError(msg, help)
- formats msg and uses it to raise an error. help is an optional page that will provide help for the error that the user encountered. This is intended to be caught with ArticleHistory:try, and is for errors after which further processing in that object becomes impossible.Message:addWarning(msg, help)
- formats msg and uses it to add a warning. help is an optional page that will provide help for the error that the user encountered. This is for errors that should be fixed, but that do not prevent the module from outputting something useful for that object.Message:getWarnings()
- returns an array of all warnings added for the object.
ArticleHistory
An ArticleHistory object does the main work of the module. It fetches the different Row objects, renders the box, renders the error messages, and renders category links. ArticleHistory objects can use all methods from the Message mixin. They also have the following public properties:
args
- a table of the arguments passed to the module by the user.currentTitle
- the title object for the current page.cfg
- the module config table. This is taken from the config module at Module:Article history/config, but is structured slightly differently due to preprocessing by the main module. Any table with an "aliases" subtable has this table removed, and the aliases are added as keys that the table can be accessed from. Conceptually, the config table{ foo = {"a value", aliases = {"bar", "baz"} } }
would become{foo = {"a value"}, bar = {"a value"}, baz = {"a value"} }
. (Although "bar" and "baz" would actually be references to the "foo" table, rather than completely new tables.)prefixArgs
- a table of the arguments passed to the module by the user, sorted by their prefix and then their 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, the arguments{a1x = 'eggs', a1y = 'spam', a2x = 'chips', b1z = 'beans', b3x = 'bacon'}
would translate into the prefixArgs table{a = { {1, x = 'eggs', y = 'spam'}, {2, x = 'chips'} }, b = { {1, z = 'beans'}, {3, x = 'bacon'} } }
.
ArticleHistory objects have the following public methods:
ArticleHistory:try(func, ...)
- calls the function func with the arguments passed, and returns the first value produced by it. If any errors are encountered, they are caught and added to the object's internal errors table for later rendering by ArticleHistory:getErrorMessages.ArticleHistory:getActionObjects()
- returns an array containing the Action objects for any actions specified by the user.ArticleHistory:getStatusIdForCode(code)
- for the status code code, returns the canonical status ID.ArticleHistory:getStatusObj()
- gets the status object for the template. This returns nil if no current status can be found.ArticleHistory:getStatusId()
- returns the status ID for the template. This returns nil if no current status can be found.ArticleHistory:getNoticeObjects()
- returns an array containing the template's Notice objects.ArticleHistory:getCollapsibleNoticeObjects()
- returns an array containing the template's CollapsibleNotice objects.ArticleHistory:getAllObjects(addSelf)
- returns an array containing all Status/MultiStatus, Notice, Action and CollapsibleNotice objects. if addSelf is true, the ArticleHistory object is appended to the array as well.ArticleHistory:getNoticeBarIcons()
- returns an array of icons to be displayed on the notice bar (at the top-left of the collapsible table).ArticleHistory:getErrorMessages()
- returns an array containing all error and warning strings. Errors are typically raised with Message:raiseError and caught with ArticleHistory:try, and warnings are added to individual objects with Message:addWarning.ArticleHistory:renderHtml()
- renders the HTML table comprising all the visible output of the template, including status, notices, actions, collapsible notices, error messages and warnings. The result is returned as a string.ArticleHistory:renderCategories()
- renders all category links and returns them as a string.
Calling tostring()
on an ArticleHistory object gives you the HTML table made with ArticleHistory:renderHtml concatenated with the category links made with ArticleHistory:renderCategories.
Row
Row objects can use all methods from the Message mixin. They also have the following public properties:
currentTitle
- the same as ArticleHistory.currentTitle.cfg
- the same as ArticleHistory.cfg.
Row objects have the following public methods:
Row:getData(articleHistoryObj)
- get memoized data for the object that has been created with a makeData function in the module config page. This mechanism is used to stop config page functions from having to do the same data processing more than once. It must be passed an ArticleHistory object to find the data from. This returns nil if no data was generated or an error was encountered while generating the data.Row:setIconValues(icon, caption, size)
- set icon values for the object. icon is the icon filename without any namespace prefix, caption is a caption to use with the icon, and size is the size of the icon when output with a large banner. icon and caption can be functions which take an ArticleHistory object as input and return the icon or caption value respectively. Sizes should include any suffixes, e.g. "30px".Row:getIcon(articleHistoryObj)
- get the icon filename. It must be passed an ArticleHistory object. Returns nil if no icon was set.Row:getIconCaption(articleHistoryObj)
- get the icon caption. It must be passed an ArticleHistory object. Returns nil if no caption was set.Row:getIconSize()
- get the icon size.Row:renderIcon(articleHistoryObj)
- renders the icon for the object. Returns nil if no icon was set.Row:setNoticeBarIconValues(icon, caption, size)
- set notice bar icon values for the object. (The notice bar icons are the small icons that appear at the top left of the collapsible box containing the actions.) icon is the icon filename without any namespace prefix, caption is a caption to use with the icon, and size is the size of the icon. icon and caption can be functions which take an ArticleHistory object as input and return the icon or caption value respectively. Sizes should include any suffixes, e.g. "15px".Row:getNoticeBarIcon(articleHistoryObj)
- get the notice bar icon filename. It must be passed an ArticleHistory object. Returns nil if no icon was set.Row:getNoticeBarCaption(articleHistoryObj)
- get the notice bar icon caption. It must be passed an ArticleHistory object. Returns nil if no caption was set.Row:getNoticeBarIconSize()
- get the notice bar icon size.Row:exportNoticeBarIcon(articleHistoryObject)
- returns the rendered notice bar icon link. This method must be passed an ArticleHistory object.Row:setText(text)
- set the text for the object to output. This may be a string or a function that takes an ArticleHistory object as input and returns the text as output.Row:getText(articleHistoryObject)
- gets the row text. This method must be passed an ArticleHistory object.Row:exportHtml(articleHistoryObject)
- returns the rendered row. This method must be passed an ArticleHistory object.Row:setCategories(val)
- sets the objects categories. val may be either an array of strings, or a function that takes an ArticleHistory object as input and returns an array of Category objects as output.Row:getCategories(articleHistoryObj)
- returns an array containing the object's Category objects. This method must be passed an ArticleHistory object.
Status
Status objects generate the row detailing the current status of the article. They inherit all properties and methods from Row objects, including the Message mixin methods. They have the following additional properties:
id
- the status ID.name
- the status name.statusCfg
- the status config table for the object's status ID.
MultiStatus
MultiStatus objects are variations on Status objects for articles that can have multiple current statuses, e.g. Good Article and Former Featured Article. They can be used interchangeably with Status objects.
Notice
Notice objects generate rows containing notices about the article that aren't part of its current status, e.g. the date the article was featured on the Main Page. They inherit all the properties and methods from Row objects, including the Message mixin methods. They don't have any additional properties or methods.
Action
Action objects generate rows detailing a single action in the history of an article, e.g. a Featured Article candidacy. They inherit all properties and methods from Row objects, including the Message mixin methods. They have the following additional properties:
paramNum
- the parameter number for that action. Used to format error messages.id
- the action ID. For example, featured article candidacies have an ID of "FAC". There is only one ID for each kind of action.actionCfg
- the action config table for the object's action ID.link
- the link set for the action, or the current talk page name if not set.resultId
- the ID for the result of the action. For example, featured article candidates that were promoted have a resultId of "promoted". There is only one resultId for each possible result of an action.date
- the date the action was carried out, or the 'action-date-missing' message if a date was not set.oldid
- the oldid for the action. Nil if not set.
Action objects have the following additional methods:
Action:getParameter(key)
- finds the original parameter name for the key key that was passed to Action.new.Action:getName(articleHistoryObject)
- gets the name of the action. This method must be passed an ArticleHistory object.Action:getResult(articleHistoryObject)
- gets the result text of the action. This method must be passed an ArticleHistory object.
CollapsibleNotice
CollapsibleNotice objects generate rows containing notices that go in the collapsible part of the template, underneath the list of actions. They can also include a collapsible text field. This is used for notices that are not important enough to be displayed as a (more prominent) Notice object, e.g. DYK notices. CollapsibleNotice objects inherit all the properties and methods from Row objects, including the Message mixin methods. They don't have any additional properties, but they have the following additional methods:
CollapsibleNotice:setCollapsibleText(s)
- set's s as the text to be displayed in the object's collapsible text field.CollapsibleNotice:getCollapsibleText(s)
- returns the text to be displayed in the object's collapsible text field.
------------------------------------------------------------------------------- -- 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('Module:No globals') local Category = require('Module:Article history/Category') local yesno = require('Module:Yesno') local lang = mw.language.getContentLanguage() ------------------------------------------------------------------------------- -- Helper functions ------------------------------------------------------------------------------- local function isPositiveInteger(num) return type(num) == 'number' and math.floor(num) == num and num > 0 and num < math.huge end local function substituteParams(msg, ...) return mw.message.newRawMessage(msg, ...):plain() end local function makeUrlLink(url, display) return string.format('[%s %s]', url, display) end local function maybeCallFunc(val, ...) -- Checks whether val is a function, and if so calls it with the specified -- arguments. Otherwise val is returned as-is. if type(val) == 'function' then return val(...) else return val end end local function renderImage(image, caption, size) if caption then caption = '|' .. caption else caption = '' end return string.format('[[File:%s|%s%s]]', image, size, caption) end local function addMixin(class, mixin) -- Add a mixin to a class. The functions will be shared across classes, so -- don't use it for functions that keep state. for name, method in pairs(mixin) do 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|%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('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) return mw.getCurrentFrame():extensionTag{ name = 'templatestyles', args = { src = 'Module:Message box/tmbox.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