Module:Portal: Difference between revisions

Content deleted Content added
disable tracking on some pagenames which contain "/archive", "/doc" or "/test"
Reapply fixed version
 
(34 intermediate revisions by 8 users not shown)
Line 42:
local p = {}
 
-- determine whether we're being called from a sandbox
local trackingEnabled = true
local isSandbox = mw.getCurrentFrame():getTitle():find('sandbox', 1, true)
local sandbox = isSandbox and '/sandbox' or ''
 
local function sandboxVersion(s)
return isSandbox and s..'-sand' or s
end
 
local templatestyles = 'Module:Portal'..sandbox..'/styles.css'
 
local getArgs = require('Module:Arguments').getArgs
local yesno = require('Module:Yesno')
 
-- List of non-talk namespaces which should not be tracked (Talk pages are never tracked)
local badNamespaces = {'user','template','draft','wikipedia'}
 
-- Check whether to do tracking in this namespace
-- Returns true unless the page is one of the banned namespaces
local function checkTrackingNamespacecheckTracking(title)
local thisPage = title or mw.title.getCurrentTitle()
if (thisPage.namespaceisTalkPage == 1) -- Talkthen
or (thisPage.namespace == 2) -- User
or (thisPage.namespace == 3) -- User talk
or (thisPage.namespace == 5) -- Wikipedia talk
or (thisPage.namespace == 7) -- File talk
or (thisPage.namespace == 11) -- Template talk
or (thisPage.namespace == 15) -- Category talk
or (thisPage.namespace == 101) -- Portal talk
or (thisPage.namespace == 118) -- Draft
or (thisPage.namespace == 119) -- Draft talk
or (thisPage.namespace == 829) -- Module talk
then
return false
end
local ns = thisPage.nsText:lower()
return true
for _, v in ipairs(badNamespaces) do
end
if ns == v then
 
return false
-- Check whether to do tracking on this pagename
end
-- Returns false if the page title matches one of the banned strings
-- Otherwise returns true
local function checkTrackingPagename()
local thisPage = mw.title.getCurrentTitle()
local thisPageLC = mw.ustring.lower(thisPage.text)
if (string.match(thisPageLC, "/archive") ~= nil) then
return false
end
if (string.match(thisPageLC, "/doc") ~= nil) then
return false
end
if (string.match(thisPageLC, "/test") ~= nil) then
return false
end
return true
end
 
 
local function matchImagePage(s)
Line 91 ⟶ 81:
local imagePage
if mw.ustring.find(firstLetter, '^[a-z]') then
imagePage = 'Module:Portal/images/' .. firstLetter .. sandbox
else
imagePage = 'Module:Portal/images/other' .. sandbox
end
return mw.loadData(imagePage)[s]
Line 100 ⟶ 90:
local function getAlias(s)
-- Gets an alias from the image alias data page.
local aliasData = mw.loadData('Module:Portal/images/aliases'..sandbox)
for portal, aliases in pairs(aliasData) do
for _, alias in ipairs(aliases) do
Line 109 ⟶ 99:
end
end
 
local defaultImage = 'Portal-puzzle.svg|link=|alt='
 
local function getImageName(s)
-- Gets the image name for a given string.
local default = 'Portal-puzzle.svg|link=|alt='
if type(s) ~= 'string' or #s < 1 then
return defaultdefaultImage
end
s = mw.ustring.lower(s)
returnlocal image = matchImagePage(s) or matchImagePage(getAlias(s)) or defaultdefaultImage
image = mw.ustring.gsub(image,'^File:','') --- strip mistaken leading File: or Image:
image = mw.ustring.gsub(image,'^Image:','')
return image
end
 
local function checkPortalExistsexists(portaltitle)
local success, exists = pcall(function() return title.exists end)
return not (mw.title.makeTitle(100, portal).id == 0)
-- If success = false, then we're out of expensive parser function calls and can't check whether it exists
-- in that case, don't throw a Lua error
return not success or exists
end
 
-- Function to check argument portals for errors, generate tracking categories if needed
function p._portal(portals, args)
-- Function first checks for too few/many portals provided
-- This function builds the portal box used by the {{portal}} template.
-- Then checks the portal list to purge any portals that don't exist
local root = mw.html.create('div')
-- Arguments:
:attr('role', 'navigation')
-- portals: raw list of portals
:attr('aria-label', 'Portals')
-- args.tracking: is tracking requested? (will not track on bad titles or namespaces)
:addClass('noprint portal plainlist')
-- args.redlinks: should redlinks be displayed?
:addClass(args.left and 'tleft' or 'tright')
-- args.minPortals: minimum number of portal arguments
:css('margin', args.margin or (args.left == 'yes' and '0.5em 1em 0.5em 0') or '0.5em 0 0.5em 1em')
-- args.maxPortals: maximum number of portal arguments
:css('border', 'solid #aaa 1px')
-- Returns:
:newline()
-- portals = list of portals, with redlinks purged (if args.redlinks=false)
 
-- trackingCat = possible tracking category
-- errorMsg = error message
function p._checkPortals(portals, args)
local trackingCat = ''
local errMsg = nil
-- Tracking is on by default.
-- It is disabled if any of the following is true
-- 1/ the parameter "tracking" is set to 'no, 'n', or 'false'
-- 2/ the current page fails the namespace testsor inpagename tests checkTrackingNamespace()
local trackingEnabled = args.tracking and checkTracking()
-- 3/ the current page fails the pagename tests in checkTrackingPagename()
if (args.tracking == 'no') or (args.tracking == 'n') or (args.tracking == 'false') then
args.minPortals = args.minPortals or 1
trackingEnabled = false
args.maxPortals = args.maxPortals or -1
-- check for too few portals
if #portals < args.minPortals then
errMsg = 'please specify at least '..args.minPortals..' portal'..(args.minPortals > 1 and 's' or '')
trackingCat = (trackingEnabled and '[[Category:Portal templates with too few portals]]' or '')
return portals, trackingCat, errMsg
end
-- check for too many portals
if (checkTrackingNamespace() == false) then
if args.maxPortals >= 0 and #portals > args.maxPortals then
trackingEnabled = false
errMsg = 'too many portals (maximum = '..args.maxPortals..')'
trackingCat = (trackingEnabled and '[[Category:Portal templates with too many portals]]' or '')
return portals, trackingCat, errMsg
end
if not args.redlinks or trackingEnabled then
if (checkTrackingPagename() == false) then
-- make new list of portals that exist
trackingEnabled = false
local existingPortals = {}
end
for _, portal in ipairs(portals) do
 
local portalTitle = mw.title.new(portal,"Portal")
-- If no portals have been specified, display an error and add the page to a tracking category.
-- if portal exists, put it into list
if not portals[1] then
if portalTitle and exists(portalTitle) then
if (args.nominimum == 'yes') or (args.nominimum == 'y') or (args.nominimum == 'true') then
table.insert(existingPortals,portal)
-- if nominimum as been set to yes (or similar), omit the warning
-- otherwise set tracking cat
elseif trackingEnabled then
else
trackingCat = "[[Category:Portal templates with redlinked portals]]"
root:wikitext('<strong class="error">No portals specified: please specify at least one portal</strong>')
end
end
-- If redlinks is off, use portal list purged of redlinks
if (trackingEnabled) then
portals = args.redlinks and portals or existingPortals
root:wikitext('[[Category:Portal templates without a parameter]]')
-- if nothing left after purge, set tracking cat
if #portals == 0 and trackingEnabled then
trackingCat = trackingCat.."[[Category:Pages with empty portal template]]"
end
return tostring(root)
end
return portals, trackingCat, errMsg
end
-- scan for nonexistent portals, if they exist remove them from the portals table. If redlinks=yes, then don't remove
 
local portallen = #portals
local function portalBox(args)
-- traverse the list backwards to ensure that no portals are missed (table.remove also moves down the portals in the list, so that the next portal isn't checked if going fowards.
return mw.html.create('ul')
-- going backwards allows us to circumvent this issue
:attr('role', 'navigation')
for i=portallen,1,-1 do
:attr('aria-label', 'Portals')
-- the use of pcall here catches any errors that may occour when attempting to locate pages when the page name is invalid
:addClass('noprint')
-- if pcall returns true, then rerun the function to find if the page exists
:addClass(args.error and '' or sandboxVersion('portalbox'))
if not pcall(checkPortalExists, portals[i]) or not checkPortalExists(portals[i]) then
:addClass(args.border and sandboxVersion('portalborder') or '')
-- Getting here means a redlinked portal has been found
:addClass(sandboxVersion(args.left and 'portalleft' or 'portalright'))
if (args.redlinks == 'yes') or (args.redlinks == 'y') or (args.redlinks == 'true') or (args.redlinks == 'include') then
:css('margin', args.margin or nil)
-- if redlinks as been set to yes (or similar), add the cleanup category and then break the loop before the portal is removed from the list
:newline()
if (trackingEnabled) then
end
root:wikitext('[[Category:Portal templates with redlinked portals]]')
 
end
local function fillBox(root, contents)
break
for _, item in ipairs(contents) do
end
local entry = root:tag('li')
-- remove the portal (this does not happen if redlinks=yes)
entry:addClass(sandboxVersion('portalbox-entry'))
table.remove(portals,i)
local image = entry:tag('span')
end
image:addClass(sandboxVersion('portalbox-image'))
image:wikitext(item[1])
local link = entry:tag('span')
link:addClass(sandboxVersion('portalbox-link'))
link:wikitext(item[2])
end
return root
end
 
local function noviewer(portalImage)
-- Function to add noviewer class to filespec for portalImage
local portalImage, hasClass = mw.ustring.gsub(portalImage, "class%s*=[^%|]+", "%0 noviewer")
if hasClass == 0 then
portalImage = portalImage.."|class=noviewer"
end
return portalImage
end
 
function p._portal(portals, args)
-- This function builds the portal box used by the {{portal}} template.
-- Normalize all arguments
-- if the length of the table is different, then rows were removed from the table, so portals were removed. If this is the case add the cleanup category
if not (portallenargs.redlinks == #portals)'include' then args.redlinks = true end
args.addBreak = args['break']
if (trackingEnabled) then
for key, default in pairs({left=false,tracking=true,nominimum=false,
if #portals == 0 then
redlinks=false,addBreak=false,border=true}) do
return '[[Category:Portal templates with all redlinked portals]]'
if args[key] == nil then args[key] = default end
else
args[key] = yesno(args[key], default)
root:wikitext('[[Category:Portal templates with redlinked portals]]')
end
end
end
 
local root = portalBox(args)
-- Start the list. This corresponds to the start of the wikitext table in the old [[Template:Portal]].
local listroot = root:tag('ul')
:css('display', 'table')
:css('box-sizing', 'border-box')
:css('padding', '0.1em')
:css('max-width', '175px')
:css('width', type(args.boxsize) == 'string' and (args.boxsize .. 'px') or nil)
:css('background', '#f9f9f9')
:css('font-size', '85%')
:css('line-height', '110%')
:css('font-style', 'italic')
:css('font-weight', 'bold')
 
local trackingCat = ''
local errMsg = nil
args.minPortals = args.nominimum and 0 or 1
args.maxPortals = -1
portals, trackingCat, errMsg = p._checkPortals(portals, args)
root:wikitext(trackingCat)
-- if error message, put it in the box and return
if errMsg then
if args.border then -- suppress error message when border=no
args.error = true -- recreate box without fancy formatting
root = portalBox(args)
root:wikitext(trackingCat)
local errTag = root:tag('strong')
errTag:addClass('error')
errTag:css('padding','0.2em')
errTag:wikitext('Error: '..errMsg)
end
return tostring(root)
end
-- if no portals (and no error), just return tracking category
if #portals == 0 then
return trackingCat
end
 
local contents = {}
-- Display the portals specified in the positional arguments.
local defaultUsed = nil
for _, portal in ipairs(portals) do
local imageportalImage = getImageName(portal)
if portalImage == defaultImage then
defaultUsed = portal
else
portalImage = noviewer(portalImage)
end
local image = string.format('[[File:%s|32x28px]]',
portalImage)
local link = string.format('[[Portal:%s|%s%sportal]]',
portal, portal, args.addBreak and '<br />' or ' ')
table.insert(contents, {image, link})
end
if defaultUsed and args.tracking and checkTracking() then
local cat = string.format('[[Category:Portal templates with default image|%s]]',
defaultUsed)
root:wikitext(cat)
end
return tostring(fillBox(root, contents))
end
 
function p._demo(imageList, args)
-- Generate the html for the image and the portal name.
for key, default in pairs({left=false,border=true}) do
listroot
if args[key] == nil then args[key] = default end
:newline()
args[key] = yesno(args[key], default)
:tag('li')
:css('display', 'table-row')
:tag('span')
:css('display', 'table-cell')
:css('padding', '0.2em')
:css('vertical-align', 'middle')
:css('text-align', 'center')
:wikitext(string.format('[[File:%s|32x28px|class=noviewer]]', image))
:done()
:tag('span')
:css('display', 'table-cell')
:css('padding', '0.2em 0.2em 0.2em 0.3em')
:css('vertical-align', 'middle')
:wikitext(string.format('[[Portal:%s|%s%sportal]]', portal, portal, args['break'] and '<br />' or ' '))
end
return tostring(root)
local root = portalBox(args)
 
local contents = {}
-- Display the portals specified in the positional arguments.
for _, fn in ipairs(imageList) do
local image = string.format('[[File:%s|32x28px]]',noviewer(fn))
local link = string.format('[[:File:%s|%s]]',fn,fn)
table.insert(contents,{image,link})
end
 
return tostring(fillBox(root,contents))
end
 
function p._image(portalsportal,keep)
-- Wrapper function to allow getImageName() to be accessed through #invoke.
-- backward compatibility: if table passed, take first element
local name = getImageName(portals[1])
if type(portal) == 'table' then
return name:match('^(.-)|') or name -- FIXME: use a more elegant way to separate borders etc. from the image name
portal = portal[1]
end
local name = getImageName(portal)
-- If keep is yes (or equivalent), then allow all metadata (like image borders) to be returned
-- also, add "noviewer" class to metadata
local keepargs = yesno(keep)
if keepargs then
return noviewer(name)
end
-- otherwise, just keep filename, plus optional category
local args = mw.text.split(name, "|", true)
local result = {args[1]} -- the filename always comes first
local category = ''
-- parse name, looking for category arguments
for i = 2,#args do
local m = mw.ustring.match(args[i], "^%s*category%s*=")
if m then
table.insert(result, args[i])
end
end
-- reassemble arguments
return table.concat(result,"|")
end
 
 
local function getAllImageTables()
local function getAllImageTable()
-- Returns an array containing all image subpages (minus aliases) as loaded by mw.loadData.
local images = {}
for i, subpage in ipairs{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'other'} do
images[i]local imageTable = mw.loadData('Module:Portal/images/' .. subpage .. sandbox)
for portal, image in pairs(imageTable) do
local args = mw.text.split(image,"|")
images[portal] = args[1] -- just use image filename
end
end
return images
Line 254 ⟶ 344:
-- names are capitalized, so the portal links may be broken.
local lang = mw.language.getContentLanguage()
portals = portals or {}
local count = 1
for _, imageTableportal in ipairspairs(getAllImageTablesgetAllImageTable()) do
table.insert(portals,lang:ucfirst(portal))
for portal in pairs(imageTable) do
portals[count] = lang:ucfirst(portal)
count = count + 1
end
end
table.sort(portals)
args.redlinks = args.redlinks or "yes"
return p._portal(portals, args)
end
Line 269 ⟶ 358:
-- should be moved to a portal alias for ease of maintenance.
local exists, dupes = {}, {}
for _portal, imageTableimage in ipairspairs(getAllImageTablesgetAllImageTable()) do
if not exists[image] then
for portal, image in pairs(imageTable) do
if not exists[image] then= portal
else
exists[image] = portal
table.insert(dupes, string.format('The image "[[:File:%s|%s]]" is used for both portals "%s" and "%s".', image, image, exists[image], portal))
else
table.insert(dupes, string.format('The image "[[:File:%s|%s]]" is used for both portals "%s" and "%s".', image, image, exists[image], portal))
end
end
end
Line 305 ⟶ 392:
end
return portals, namedArgs
end
 
-- Entry point for sorting portals from other named arguments
function p._processPortalArgs(args)
return processPortalArgs(args)
end
 
function p.image(frame)
local origArgs = getArgs(frame)
local portals, args = processPortalArgs(origArgs)
return p._image(portals[1],args.border)
end
 
function p.demo(frame)
local args = getArgs(frame)
local styles = frame:extensionTag{ name = 'templatestyles', args = { src = templatestyles} }
return styles..p._demo(args,args)
end
 
Line 313 ⟶ 417:
-- template, or the args passed to #invoke if any exist. Otherwise
-- assume args are being passed directly in from the debug console
-- or from another Lua module.
-- Also: trim whitespace and remove blank arguments
local origArgs
local origArgs = getArgs(frame)
if type(frame.getParent) == 'function' then
-- create two tables to pass to func: an array of portal names, and a table of named arguments.
origArgs = frame:getParent().args
local portals, args = processPortalArgs(origArgs)
for k, v in pairs(frame.args) do
local results = ''
origArgs = frame.args
if funcName == '_portal' or funcName == '_displayAll' then
break
results = frame:extensionTag{ name = 'templatestyles', args = { src = templatestyles} }
end
else
origArgs = frame
end
-- Trim whitespace and remove blank arguments.
local args = {}
for k, v in pairs(origArgs) do
if type(v) == 'string' then
v = mw.text.trim(v)
end
if v ~= '' then
args[k] = v
end
end
return results .. p[funcName](portals, args)
return p[funcName](processPortalArgs(args)) -- passes two tables to func: an array of portal names, and a table of named arguments.
end
end
 
for _, funcName in ipairs{'portal', 'image', 'imageDupes', 'displayAll'} do
p[funcName] = makeWrapper('_' .. funcName)
end