-------------------------------------------------------------------------------
-- 頁面歷史
--
-- 此模塊允許編者連入頁面歷史的重要事件,
-- 像是優良或特色內容的提名。
-- 它也展示該頁面的現狀以及一樣好的其他訊息,
-- 像是通過的提名之後展示於首頁上的日期。
-------------------------------------------------------------------------------
local CONFIG_PAGE = 'Module:Article history/config'
local WRAPPER_TEMPLATE = 'Template:Article history'
local DEBUG_MODE = false -- If true, errors are not caught.
-- Load required modules.
require('strict')
local Category = require('Module:Article history/Category')
local yesno = require('Module:Yesno')
local lang = mw.language.getContentLanguage()
-------------------------------------------------------------------------------
-- Helper functions
-------------------------------------------------------------------------------
local function isPositiveInteger(num)
return type(num) == 'number'
and math.floor(num) == num
and num > 0
and num < math.huge
end
-- lua cannot interpret xxxx年yy月zz日
local function convertDate(dateString)
for y,m,d in string.gmatch(dateString, "(%d+)年(%d+)月(%d+)日") do
return y .. "-" .. m .. "-" .. d
end
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|link=]]', 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.isSmall = data.isSmall
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, smallSize)
self.icon = icon
self.iconCaption = caption
self.iconSize = size
self.iconSmallSize = smallSize
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()
if self.isSmall then
return self.iconSmallSize or self.cfg.defaultSmallIconSize or '15px'
else
return self.iconSize or self.cfg.defaultIconSize or '30px'
end
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()
if self.isSmall then
return self.statusCfg.smallIconSize
or self.cfg.defaultSmallStatusIconSize
or '30px'
else
return self.iconSize
or self.statusCfg.iconSize
or self.cfg.defaultStatusIconSize
or '50px'
end
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.defaultSmallStatusIconSize or '30px'
for i, 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 i, obj in ipairs(self.statuses) do
ret:node(obj:exportHtml(articleHistoryObj))
end
return ret
end
function MultiStatus:getCategories(articleHistoryObj)
local ret = {}
for i, obj in ipairs(self.statuses) do
for j, categoryObj in ipairs(obj:getCategories(articleHistoryObj)) do
ret[#ret + 1] = categoryObj
end
end
return ret
end
function MultiStatus:exportNoticeBarIcon()
local ret = {}
for i, obj in ipairs(self.statuses) do
ret[#ret + 1] = obj:exportNoticeBarIcon()
end
return table.concat(ret)
end
function MultiStatus:getWarnings()
local ret = {}
for i, obj in ipairs(self.statuses) do
for j, 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,
data.iconSmallSize
)
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 convertDate
if data.date then
local isChineseDate, tempChineseDate = pcall(convertDate, data.date)
if isChineseDate and tempChineseDate then
data.date = tempChineseDate
end
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)
-- Set ignore action for calculation
obj.ignore = data.ignore
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,
data.iconSmallSize
)
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()
if self.isSmall then
return self.iconSmallSize
or self.cfg.defaultSmallCollapsibleNoticeIconSize
or '15px'
else
return self.iconSize
or self.cfg.defaultCollapsibleNoticeIconSize
or '20px'
end
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 var(--border-color-subtle,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()
-- Set isSmall
obj.isSmall = yesno(obj.args.small) or false
-- 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 i, 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: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 = self:try(self.cfg.getStatusIdFunction, self)
if not statusId or statusId == "" then
self.statusObj = false
return nil
elseif not self.cfg.statuses[statusId] then
self:addWarning(
self:message('articlehistory-warning-invalid-status'),
self:message('articlehistory-warning-invalid-status-help')
)
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,
isSmall = self.isSmall
}
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 i, 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
data.isSmall = self.isSmall
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 i, t in ipairs(objTables) do
for j, 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 i, obj in ipairs(self:getAllObjects(true)) do
for j, 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 i, obj in ipairs(self:getAllObjects()) do
local categories = self:try(obj.getCategories, obj, self)
for j, categoryObj in ipairs(categories or {}) do
ret[#ret + 1] = tostring(categoryObj)
end
end
-- Extra categories
for i, func in ipairs(self.cfg.extraCategories or {}) do
local cats = func(self) or {}
for i, 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')
if self.isSmall then
tableRoot:addClass('mbox-small')
else
tableRoot:css('width', '80%')
end
-- 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
if self.cfg.uncollapsedRows == 'all' then
isCollapsed = false
elseif nCollapsibleRows == 1 then
isCollapsed = false
else
isCollapsed = nCollapsibleRows > (tonumber(self.cfg.uncollapsedRows) or 3)
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('AH-milestones')
:addClass(isCollapsed and 'mw-collapsible mw-collapsed' or nil)
:css('width', '100%')
:css('font-size', '90%')
if nCollapsibleRows > 1 then
-- 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
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 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
})
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