Module:Mods overview table
Jump to navigation
Jump to search
Documentation The documentation below is transcluded from Module:Mods overview table/doc. (edit | history)
This module is used to assemble the large table on Terraria Mods Wiki:Overview. The criteria for deletion as well as the limit for the "last edit" data fetching can be adjusted at the top of the source code.
The module is invoked as follows: {{#invoke:Mods overview table|go}}
.
---Criteria any of which a mod must meet in order not to be marked as pending for deletion.
local deletionCriteria = {
---Minimum number of pages pertaining to the mod.
minPages = 15,
---Maximum number of days since the latest edit to any of the mod's pages.
lasteditMaxDays = 14
}
---Retrieving the data about the latest edit of a mod is a very expensive operation
---that causes an error if performed too many times. Therefore, the number of mods
---for which the "lastedit data" can be retrieved each day must be limited. This
---variable specifies that limit.
local modsToCheckForLasteditedPerDay = 75
---First index of the mods for which lastedit data can be retrieved. Needs to be
---computed via `computeStartIndex()`.
---@type integer
local startIndexForLasteditData
---Default content language of the wiki (i.e. `$wgLanguageCode`, not the value of the
---`uselang` URL parameter, and not the user's language preference setting).
local contentLanguage = mw.getContentLanguage()
---A cached version of the current frame, the interface to the parser.
local currentFrame
---The current date (in UTC) as a string, e.g. "20220131".
local todayStr = os.date('!%Y%m%d')
---The current time in seconds since the epoch.
local now = os.time()
---List of all mod main pages. Needs to be filled via `fillMainpages()`.
---@type string[]
local mainpages
---Cached expansion of `{{yes}}` / `{{no}}`.
---@type string
local yes, no
---Return `x` rounded to the nearest integer.
---@param x number
---@return integer
local function round(x)
return math.floor(x + 0.5)
end
---Split the `str` on each `div` in it and return the result as a table.
---This is much much faster then `mw.text.split`.
---Credit: http://richard.warburton.it.
---@param div string
---@param str string
---@return table|boolean
local function explode(div,str)
if (div=='') then return false end
local pos,arr = 0,{}
-- for each divider found
for st,sp in function() return string.find(str,div,pos,true) end do
arr[#arr + 1] = string.sub(str,pos,st-1) -- Attach chars left of current divider
pos = sp + 1 -- Jump past current divider
end
arr[#arr + 1] = string.sub(str,pos) -- Attach chars right of last divider
return arr
end
---Call `{{note}}`.
---@param text string Note text
---@param paren? boolean Whether to add parentheses
---@param small? boolean Whether to use small text
local function note(text, paren, small)
return currentFrame:expandTemplate{
title = 'note',
args = {
text,
paren = paren and 'y' or nil,
small = small and 'y' or nil
}
}
end
---Call `{{duration}}`.
---@param seconds integer Number of seconds to format
---@return string
local function duration(seconds)
return currentFrame:expandTemplate{
title = 'duration' ,
args = { seconds, 'weeks' }
}
end
---Return the duration between now and the timestamp described by `seconds`.
---Round down the duration to days and format it.
---@param seconds integer
---@return string
local function daysDiff(seconds)
local secondsDiff = math.abs(now - seconds)
-- round the seconds down to days
secondsDiff = math.floor(secondsDiff / (3600*24)) * (3600*24)
local durationStr = 'less than 1 day'
if secondsDiff > 0 then
durationStr = duration(secondsDiff)
end
return durationStr
end
---Perform a query with Extension:DynamicPageList (DPL).
---@param ... table Named parameters for the query
---@return string
local function dpl(...)
local dplArgs = ...
-- callParserFunction requires a first unnamed parameter
table.insert(dplArgs, 1, '')
return currentFrame:callParserFunction{
name = '#dpl',
args = dplArgs
}
end
---Convert a timestamp from a DPL query result to an integer of seconds.
---@param timestampFromDpl string
---@return integer
local function dplTimestampToSeconds(timestampFromDpl)
if type(timestampFromDpl) ~= 'string' or timestampFromDpl == '' then
timestampFromDpl = '1970-01-01 00:00:00'
end
local year, month, day, hour, min, sec = string.match(
timestampFromDpl,
'^(%d%d%d%d)%-(%d%d)%-(%d%d) (%d%d):(%d%d):(%d%d)$'
)
return os.time{
year = year, month = month, day = day,
hour = hour, min = min, sec = sec
}
end
---Check whether the given page exists.
---@param title string Page title with namespace
---@return boolean
local function pageExists(title)
return '1' ~= dpl{
title = title,
redirects = 'include',
mode = 'userformat',
noresultsheader = '1'
}
end
---Fetch all mod main pages and put them in the global `mainpages` list.
local function fillMainpages()
local mainpagesString = currentFrame:expandTemplate{ title = 'modlist', args = { sep = '<br/>' } }
local mainpagesTable = explode('<br/>', mainpagesString)
---@cast mainpagesTable string[]
mainpages = mainpagesTable
end
---Compute the `startIndexForLasteditData` variable. Requires that `mainpages`
---is filled.
local function computeStartIndex()
local yearday = os.date('!*t').yday
-- yearday is the current day of the year, e.g. 32 if today is 01 February.
-- using this ensures that we advance the startIndex by exactly
-- `modsToCheckForLasteditedPerDay` mods each day. for example, if #mainpages=12
-- and modsToCheckForLasteditedPerDay=5, then startIndexForLasteditData would be
-- 5 on 01 January,
-- 10 on 02 January,
-- 3 on 03 January,
-- 8 on 04 January,
-- 1 on 05 January, etc.
-- the startIndex is increased by 5 each day and wraps around when it exceeds 12.
startIndexForLasteditData = (yearday * modsToCheckForLasteditedPerDay) % #mainpages
end
---@class Mod
---@field name string Mod name
---@field index integer Mod index in the list of all mods
---@field daysUntilLasteditDataAvailable integer Number of days to wait until the data about the mod's last edit is available
local Mod = {}
---Create a new `Mod` instance.
---@param index integer Index of the mod in the list of all mods
---@param name string Name of the mod
---@return Mod
function Mod:new(index, name)
local newModInstance = {}
setmetatable(newModInstance, { __index = self })
newModInstance:_init(index, name)
return newModInstance
end
---Initialize the new `Mod` instance's attributes.
function Mod:_init(index, name)
self.name = name or ''
self.index = index or 0
self._pagesnumber = -1
self._mainpageCreated = -1
self.daysUntilLasteditDataAvailable = -1
self._lastedit = { time = 0, revision = -1 }
self._pendingDeletionNow = nil
self._pendingDeletionSoon = nil
end
---Return the number of pages in the mod.
---@return integer
function Mod:getPagesnumber()
if self._pagesnumber == -1 then
-- number of subpages of the mod main page
local pagesnumber = tonumber(dpl{
namespace = '',
titlematch = self.name .. '/%',
mode = 'userformat',
resultsheader = '%TOTALPAGES%',
noresultsheader = '0'
})
-- add the mod main page
self._pagesnumber = 1 + pagesnumber
end
return self._pagesnumber
end
---Return the creation date of the mod's main page, in seconds.
---@return integer
function Mod:getMainpageCreated()
if self._mainpageCreated == -1 then
local timestampString = dpl{
title = self.name,
ordermethod = 'firstedit',
addeditdate = 'true',
format = ',%DATE%,,'
}
self._mainpageCreated = dplTimestampToSeconds(timestampString)
end
return self._mainpageCreated
end
---Return whether lastedit data is available for the mod, and
---compute `daysUntilLasteditDataAvailable` in the process.
---@return boolean
function Mod:isLasteditDataAvailable()
if self.daysUntilLasteditDataAvailable == -1 then
-- the startIndex must be subtracted from the mod's index, but that would
-- yield negative numbers in some cases, so "shift" all indices
local modIndexShifted = self.index + #mainpages - startIndexForLasteditData
self.daysUntilLasteditDataAvailable = math.floor(
(modIndexShifted % #mainpages) / modsToCheckForLasteditedPerDay
)
-- example: #mainpages=12, modsToCheckForLasteditedPerDay=5, startIndexForLasteditData=10.
-- we expect the mods with indices 10, 11, 12, 1, 2 to have lastedit data available today.
-- we expect the mods with indices 3, 4, 5, 6, 7 to have the data available in 1 day.
-- we expect the mods with indices 8, 9 to have the data available in 2 days.
-- check for self.index=12:
-- modIndexShifted==14,
-- (modIndexShifted % #mainpages) / modsToCheckForLasteditedPerDay==0.4.
-- check for self.index=7:
-- modIndexShifted==9,
-- (modIndexShifted % #mainpages) / modsToCheckForLasteditedPerDay==1.8.
-- check for self.index=8:
-- modIndexShifted==10,
-- (modIndexShifted % #mainpages) / modsToCheckForLasteditedPerDay==2.
end
return self.daysUntilLasteditDataAvailable == 0
end
---Return timestamp in seconds and revision ID of the last edit to any of the mod's pages.
---@return { time: integer, revision: integer }
function Mod:getLastedit()
if self._lastedit.time == 0 and self._lastedit.revision == -1 then
local dplResult = dpl{
titlematch = self.name .. '¦' .. self.name .. '/%',
allrevisionsbefore = todayStr,
count = 1,
format = ',%DATE%§%REVISION%,,'
}
if dplResult ~= '' and dplResult ~= '§' then
local exploded = explode('§', dplResult)
self._lastedit.time = dplTimestampToSeconds(exploded[1])
self._lastedit.revision = exploded[2]
end
end
return self._lastedit
end
---Check whether the mod is currently pending for deletion.
---@return boolean
function Mod:isPendingDeletionNow()
if self._pendingDeletionNow == nil then
if self:getPagesnumber() >= deletionCriteria.minPages or not self:isLasteditDataAvailable() then
-- mod has at least the minimum number of pages, so it's not pending deletion
-- or lastedit data is unavailable, so it's unknown if it's pending deletion
self._pendingDeletionNow = false
else
-- mod has fewer than the minimum number of pages and the lastedit data is available, so:
-- if the last edit was more than the maximum number of days ago, then it's pending deletion
self._pendingDeletionNow = now - self:getLastedit().time > (3600*24*deletionCriteria.lasteditMaxDays)
end
end
return self._pendingDeletionNow
end
---Check whether the mod will be pending for deletion soon (unless it is edited before then).
---@return boolean
function Mod:isPendingDeletionSoon()
if self._pendingDeletionSoon == nil then
if self:getPagesnumber() >= deletionCriteria.minPages or not self:isLasteditDataAvailable() then
-- mod has at least the minimum number of pages, so it's not pending deletion
-- or lastedit data is unavailable, so it's unknown if it's pending deletion
self._pendingDeletionSoon = false
else
-- mod has fewer than the minimum number of pages and the lastedit data is available, so:
-- if the mod is not already pending for deletion, then it will soon
self._pendingDeletionSoon = not self:isPendingDeletionNow()
end
end
return self._pendingDeletionSoon
end
---Fill the `tableRow` with HTML, taking information from the `mod`.
---@param tableRow any `<tr>` object that will be filled
---@param mod Mod Mod object for which to display information in this row
local function makeTableRow(tableRow, mod)
local td
-- column: "Mod name"
tableRow:tag('td')
:wikitext('[[' .. mod.name .. ']]')
-- column: "Pages"
tableRow:tag('td')
:wikitext(
'[[Special:PrefixIndex/' .. mod.name .. '|' ..
contentLanguage:formatNum(mod:getPagesnumber()) .. ']]'
)
-- column: "Created"
td = tableRow:tag('td')
:attr('data-sort-value', mod:getMainpageCreated())
:wikitext(contentLanguage:formatDate('d M Y', '@' .. mod:getMainpageCreated()))
td:tag('div')
:wikitext(note(daysDiff(mod:getMainpageCreated()) .. ' ago', true))
-- column: "Last edited"
td = tableRow:tag('td')
if mod:isLasteditDataAvailable() then
td:attr('data-sort-value', mod:getLastedit().time)
:wikitext(
'[[Special:Diff/' .. mod:getLastedit().revision .. '|' ..
contentLanguage:formatDate('d M Y', '@' .. mod:getLastedit().time) .. ']]'
)
td:tag('div')
:wikitext(note(daysDiff(mod:getLastedit().time) .. ' ago', true))
else
local noteText = 'Currently unknown,<br/>check back in ' .. mod.daysUntilLasteditDataAvailable .. ' day'
if mod.daysUntilLasteditDataAvailable ~= 1 then
noteText = noteText .. 's'
end
td:attr('data-sort-value', now + mod.daysUntilLasteditDataAvailable * (3600*24))
:tag('div')
:wikitext(note("<p style='line-height:1.2'>''" .. noteText .. ".''</p>", false, true))
end
-- column: "Issues"
td = tableRow:tag('td')
local issueStrings = {}
if mod:isPendingDeletionNow() then
table.insert(issueStrings, note('Wiki is pending deletion.'))
end
if mod:isPendingDeletionSoon() then
local timeUntilDeletion = daysDiff(mod:getLastedit().time + (3600*24*deletionCriteria.lasteditMaxDays))
local noteText = 'Wiki will be pending deletion if not worked on within ' .. timeUntilDeletion .. '.'
table.insert(issueStrings, note(noteText))
end
if not pageExists('File:Logo (' .. mod.name .. ').png') then
table.insert(issueStrings, "Can't find [[:File:Logo (" .. mod.name .. ').png]]!')
end
if not pageExists('Category:' .. mod.name) then
table.insert(issueStrings, "Can't find [[:Category:" .. mod.name .. ']]!')
end
if #issueStrings > 0 then
td:wikitext(table.concat(issueStrings, '<hr class="incell-border"/>'))
else
td:wikitext("''None''")
end
-- column: "OK"
tableRow:tag('td')
:wikitext(mod:isPendingDeletionNow() and no or yes)
end
---Create a `<caption>` tag and put it in the `ovTable`.
---@param ovTable any `<table>` object
---@param mods Mod[] List of mods
local function makeTableCaption(ovTable, mods)
local caption = ovTable:tag('caption')
caption:tag('big')
:wikitext('Information about all mods')
local purge = currentFrame:expandTemplate{ title = 'purge' }
caption:tag('div')
:wikitext(note(purge .. ' to refresh', true, true))
local pagesNumbersSum = 0
for _, mod in ipairs(mods) do
pagesNumbersSum = pagesNumbersSum + mod:getPagesnumber()
end
local dotlist = currentFrame:expandTemplate{ title = 'dotlist', args = {
'Total number of mods: ' .. contentLanguage:formatNum(#mods),
'Total number of mod pages: ' .. contentLanguage:formatNum(pagesNumbersSum),
'Avg. number of pages: ' .. contentLanguage:formatNum(round(pagesNumbersSum / #mods)),
space = 'xl'
} }
caption:tag('small')
:wikitext(dotlist)
end
---Create a `<tr>` tag with header cells and put it in the `ovTable`.
---@param ovTable any `<table>` object
local function makeTableHeader(ovTable)
local tr = ovTable:tag('tr')
tr:tag('th'):wikitext('Mod name')
tr:tag('th'):wikitext('Pages')
tr:tag('th'):wikitext('Created')
tr:tag('th'):wikitext('Last edited')
tr:tag('th'):wikitext('Issues')
local refTag = currentFrame:extensionTag('ref',
'Does the wiki have at least ' .. deletionCriteria.minPages .. ' pages' ..
' or was worked on in ' .. deletionCriteria.lasteditMaxDays .. ' days?'
)
tr:tag('th'):wikitext('OK' .. refTag)
end
-----------------------------------------------------------------
---main return object
local p = {}
p.go = function(frame)
currentFrame = frame
-- cache the expansions of these templates so that they don't
-- have to be expanded over and over again
yes = currentFrame:expandTemplate{ title = 'yes' }
no = currentFrame:expandTemplate{ title = 'no' }
fillMainpages()
computeStartIndex()
---List of all `Mod` objects
---@type Mod[]
local mods = {}
-- create a `Mod` object for each mod mainpage
for i, mainpageName in ipairs(mainpages) do
mods[i] = Mod:new(i, mainpageName)
end
---Table HTML element
local ovTable = mw.html.create('table')
:addClass('terraria')
:addClass('sortable')
:addClass('lined')
makeTableCaption(ovTable, mods)
makeTableHeader(ovTable)
for _, mod in ipairs(mods) do
local tr = ovTable:tag('tr')
makeTableRow(tr, mod)
end
return ovTable
end
return p