Jump to content

Module:Archive: Difference between revisions

From Wikipedia, the free encyclopedia
Content deleted Content added
merging sandbox changes per discussion on talk page
moved sandbox changes live per talk page notes and TfD discussion; this allows the template to better handle annually archived pages
 
Line 41: Line 41:
return archiveTable
return archiveTable
end
end

-- Check to see if the page is likely an annual archive. No talk pages exist
-- before 2001. Some pages have the next year created in advance.
local function isYearlyArchive(num)
local currentYear = tonumber(os.date("%Y"))
return num and num >= 2001 and num <= currentYear + 1 -- possible years
end
local function detectArchiveFormat(title, args)
-- Check if next/previous are set. Some archives swap between annual and
-- sequential archives at some point and will need to accept whatever an
-- editor says the next/previous link should be for these kind of weird
-- or unsual orders.
if args and (args.prev or args.next) then
return nil, nil, nil
end
-- Check if "/Archive 2" exists to prevent false positives on noticeboards
local archiveBase = title.baseText
local archive2Title = mw.title.new(archiveBase .. "/Archive 2")
if archive2Title and archive2Title.exists then
return nil, nil, nil -- Exit early for sequential archives
end

-- How is the year formatted?
local patterns = {
{pattern = "^(.-)/Archive (%d+)$", prefix = nil}, -- "Talk:Base page/Archive YYYY"
-- "nil" triggers the default. There is some kind of quirk with how
-- the module give spaces that Template:Yearly archive list cannot
-- read with either a space or the HTML space entity
{pattern = "^(.-)/Archive(%d+)$", prefix = "/Archive"}, -- "Talk:Base page/ArchiveYYYY"
{pattern = "^(.-)/Archive/(%d+)$", prefix = "/Archive/"}, -- "Talk:Base page/Archive/YYYY"
{pattern = "^(.-)/Archives/(%d+)$", prefix = "/Archives/"}, -- "Talk:Base page/Archives/YYYY"
{pattern = "^(.-)/(%d+)$", prefix = "/"} -- "Talk:Base page/YYYY"
}

for _, p in ipairs(patterns) do
local basePage, archiveNum = title.prefixedText:match(p.pattern)
archiveNum = tonumber(archiveNum)
if basePage and isYearlyArchive(archiveNum) then
return basePage, true, p.prefix
end
end
return nil, nil, nil
end





Line 94: Line 139:
return msg
return msg
end
end
end

function Navigator:getNamespacePreposition()
-- Most talk archives are about a subject. Some will be "with" an editor
-- or "on" a noticeboard.
-- Function to get the namespace preposition
-- Namespace number where transcluded
local namespaceNumber = mw.title.getCurrentTitle().namespace
-- Preposition from table to make it easy for wikis to translate or ignore
local namespacePrepositionTable = self.cfg['namespace-prepositions']

-- Default preposition if not exception from above
return namespacePrepositionTable[namespaceNumber] or self.cfg["preposition-default"]
end
end


Line 135: Line 194:
end
end
-- What kind of blurb to use in the message box?
-- Most talk pages are "about" an article. For user talk page archives
local function getBlurbKey(args)
-- use "with" instead as User:X will also be a participant. User talk
if args.type == 'index' then
-- archives will be found in both User and User talk namespaces (2,3).
-- For manually-indexed archives only
-- For noticeboards and wikiprojects use "on" as the discussions are
return 'blurb-index', args.type
-- typically not about the noticeboard itself (4,5).
elseif args.type == 'annual' then
local namespacePreposition = "about"
-- Grab the year of the current archive.
if current:inNamespaces(2, 3) == true then
return 'blurb-annual', mw.getCurrentFrame():expandTemplate {
namespacePreposition = "with"
title = 'Title year', args = { page = current.fullText }
elseif current:inNamespaces(4, 5) == true then
}
namespacePreposition = "on"
elseif args.period then
end
return 'blurb-period', args.period
else
if args.type == 'index' then
return 'blurb-noperiod', ''
-- For manually-indexed archives only
end
ret = self:message('blurb-index', talkPageTitle, pageUnderDiscussion,
args.type, namespace, namespacePreposition)
elseif args.period then
ret = self:message('blurb-period', talkPageTitle, pageUnderDiscussion,
args.period, namespace, namespacePreposition)
else
ret = self:message('blurb-noperiod', talkPageTitle, pageUnderDiscussion,
'', namespace, namespacePreposition)
end
end

-- Generate a blurb from Module:Archive/config
local blurbKey, argValue = getBlurbKey(args)
local namespacePreposition = self:getNamespacePreposition()
ret = self:message(blurbKey, talkPageTitle, pageUnderDiscussion, argValue, namespace, namespacePreposition)

end
end
return ret
return ret
Line 362: Line 420:
-- Is |omit filled? If not, make the whole box.
-- Is |omit filled? If not, make the whole box.
if args.omit == nil then
if args.omit == nil then

boxComponents = self:makeMessageBox() .. '\n' .. self:makeArchiveLinksWikitable()
-- Check for annual archives
-- We're omitting the banner, so we should only make the links table.
local currentTitle = self.currentTitle
local yearlyBase, isYearly, yearlyPattern = detectArchiveFormat(currentTitle, args)

if isYearly then
-- Use a yearly archive format
local linksYearlyListSeparator = "&#32;" -- Space separator
local linksYearlyList = mw.getCurrentFrame():expandTemplate {
title = 'Yearly archive list',
args = {
root = yearlyBase,
sep = linksYearlyListSeparator,
prefix = yearlyPattern
}
}
boxComponents = self:makeMessageBox() .. '\n' ..
'<div style="font-size:115%; width:100%; word-spacing:1em; text-align:center;">' .. linksYearlyList .. '</div>'
else
-- Default numbered archive format
boxComponents = self:makeMessageBox() .. '\n' .. self:makeArchiveLinksWikitable()
end -- We're omitting the banner, so we should only make the links table.
elseif args.omit == 'banner' then
elseif args.omit == 'banner' then
boxComponents = self:makeArchiveLinksWikitable()
boxComponents = self:makeArchiveLinksWikitable()

Latest revision as of 15:40, 3 May 2025

-------------------------------------------------------------------------------
--                       Automatic archive navigator
--
-- This module produces a talk archive banner, together with an automatically-
-- generated list of navigation links to other archives of the talk page in
-- question. It implements {{Archive}}.
-------------------------------------------------------------------------------

local yesno = require('Module:Yesno')

-------------------------------------------------------------------------------
-- Helper functions
-------------------------------------------------------------------------------

local function makeWikilink(page, display)
	if display then
		return string.format('[[%s|%s]]', page, display)
	else
		return string.format('[[%s]]', page)
	end
end

local function escapePattern(s)
	-- Escape punctuation in a string so it can be used in a Lua pattern.
	s = s:gsub('%p', '%%%0')
	return s
end

local function makeTable(width)
	local archiveTable = mw.html.create('table')
	archiveTable
		:css({
			['max-width'] = width,
			['margin'] = '0 auto 0.5em',
			['text-align'] = 'center'
		})
		-- Set width so that the table doesn't spill out on narrower skins
		-- or when zooming in. It has to be defined multiple times because 
		-- "stretch" is experimental.
		:cssText('width:100%;width:-moz-available;width:-webkit-fill-available;width:stretch')
	return archiveTable
end

-- Check to see if the page is likely an annual archive. No talk pages exist 
-- before 2001. Some pages have the next year created in advance.
local function isYearlyArchive(num)
    local currentYear = tonumber(os.date("%Y"))
    return num and num >= 2001 and num <= currentYear + 1 -- possible years
end
local function detectArchiveFormat(title, args)
	-- Check if next/previous are set. Some archives swap between annual and 
	-- sequential archives at some point and will need to accept whatever an
	-- editor says the next/previous link should be for these kind of weird 
	-- or unsual orders.
    if args and (args.prev or args.next) then
        return nil, nil, nil
    end
    
    -- Check if "/Archive 2" exists to prevent false positives on noticeboards
    local archiveBase = title.baseText
    local archive2Title = mw.title.new(archiveBase .. "/Archive 2")
    if archive2Title and archive2Title.exists then
        return nil, nil, nil -- Exit early for sequential archives
    end

    -- How is the year formatted?
    local patterns = {
        {pattern = "^(.-)/Archive (%d+)$", prefix = nil}, -- "Talk:Base page/Archive YYYY"
        -- "nil" triggers the default. There is some kind of quirk with how
        -- the module give spaces that Template:Yearly archive list cannot
        -- read with either a space or the HTML space entity
        {pattern = "^(.-)/Archive(%d+)$", prefix = "/Archive"}, -- "Talk:Base page/ArchiveYYYY"
        {pattern = "^(.-)/Archive/(%d+)$", prefix = "/Archive/"}, -- "Talk:Base page/Archive/YYYY"
        {pattern = "^(.-)/Archives/(%d+)$", prefix = "/Archives/"}, -- "Talk:Base page/Archives/YYYY"
        {pattern = "^(.-)/(%d+)$", prefix = "/"} -- "Talk:Base page/YYYY"
    }

    for _, p in ipairs(patterns) do
        local basePage, archiveNum = title.prefixedText:match(p.pattern)
        archiveNum = tonumber(archiveNum)
        if basePage and isYearlyArchive(archiveNum) then
            return basePage, true, p.prefix
        end
    end
    return nil, nil, nil
end



-------------------------------------------------------------------------------
-- Navigator class
-------------------------------------------------------------------------------

local Navigator = {}
Navigator.__index = Navigator

function Navigator.new(args, cfg, currentTitle)
	local obj = setmetatable({}, Navigator)
	
	-- Set inputs
	obj.args = args
	obj.cfg = cfg
	obj.currentTitle = currentTitle

	-- Archive prefix
	-- Decode HTML entities so users can enter things like "Archive&#32;" from
	-- wikitext.
	obj.archivePrefix = obj.args.prefix or obj:message('archive-prefix')
	obj.archivePrefix = mw.text.decode(obj.archivePrefix)

	-- Current archive number
	do
		local pattern = string.format(
			'^%s([1-9][0-9]*)$',
			escapePattern(obj.archivePrefix)
		)
		obj.currentArchiveNum = obj.currentTitle.subpageText:match(pattern)
		obj.currentArchiveNum = tonumber(obj.currentArchiveNum)
	end
	
	-- Highest archive number
	obj.highestArchiveNum = require('Module:Highest archive number')._main(
		 obj.currentTitle.nsText ..
		 	':' .. 
			obj.currentTitle.baseText .. 
			'/' .. 
			obj.archivePrefix,
		obj.currentArchiveNum
	)

	return obj
end

function Navigator:message(key, ...)
	local msg = self.cfg[key]
	if select('#', ...) > 0 then
		return mw.message.newRawMessage(msg, ...):plain()
	else
		return msg
	end
end

function Navigator:getNamespacePreposition()
	-- Most talk archives are about a subject. Some will be "with" an editor
	-- or "on" a noticeboard.
	-- Function to get the namespace preposition
		
	-- Namespace number where transcluded
	local namespaceNumber = mw.title.getCurrentTitle().namespace
	-- Preposition from table to make it easy for wikis to translate or ignore
	local namespacePrepositionTable = self.cfg['namespace-prepositions']

	-- Default preposition if not exception from above
    return namespacePrepositionTable[namespaceNumber] or self.cfg["preposition-default"]
end

function Navigator:makeBlurb()
	local args = self.args
	local current = self.currentTitle
	local ret
	
	-- Skip if user provides their own blurb.
	if args.text then
		ret = args.text
	else
		-- Set parent talk page.
		local parentTalkPage = current.basePageTitle
		local talkPageTitle
		local pageUnderDiscussion
		
		-- If the parent talk page exists (and it's not the root talk page)
		-- we should link to it in both the "main talk page" and 
		-- "discussions about" parts of the blurb. 
		if args.prefix or (parentTalkPage.exists and parentTalkPage.isRedirect == false and current.baseText ~= current.rootText) then
			talkPageTitle = parentTalkPage.fullText
			pageUnderDiscussion = talkPageTitle
		-- If it doesn't, set "main talk page" to the root talk page
		-- and "discussions about" to the root subject.
		else
			talkPageTitle = current.nsText .. ':' .. parentTalkPage.rootText
			-- Set page under discussion.
			pageUnderDiscussion = current.subjectNsText .. ':' .. current.rootText
			
			-- Prepend colon for non-mainspace pages.
			if current.subjectNsText ~= '' then
				pageUnderDiscussion = ':' .. pageUnderDiscussion 
			end
		end
		
		-- Check current namespace for blurb.
		local namespace = 'main'
		if current.isTalkPage == true then
			namespace = 'talk'
		end
		
		-- What kind of blurb to use in the message box?
		local function getBlurbKey(args)
    		if args.type == 'index' then
    			-- For manually-indexed archives only
        		return 'blurb-index', args.type
        	elseif args.type == 'annual' then
			-- Grab the year of the current archive.
				return 'blurb-annual', mw.getCurrentFrame():expandTemplate {
					title = 'Title year', args = { page = current.fullText }
				}
    		elseif args.period then
        		return 'blurb-period', args.period
    		else
        		return 'blurb-noperiod', ''
    		end
		end

		-- Generate a blurb from Module:Archive/config
		local blurbKey, argValue = getBlurbKey(args)
		local namespacePreposition = self:getNamespacePreposition()
		ret = self:message(blurbKey, talkPageTitle, pageUnderDiscussion, argValue, namespace, namespacePreposition)

	end
	return ret
end

function Navigator:makeMessageBox()
	local args = self.args
	local image
	if args.image then
		image = args.image
	else
		local icon = args.icon or self:message('default-icon')
		image = string.format(
			'[[File:%s|%s|alt=|link=]]',
			icon,
			self:message('image-size')
		)
	end

	-- Hardcode tmbox style on the template's page.
	-- PS: Needs to be changed if the template is renamed!
	local mainTemplatePage = ''
	if self.currentTitle.fullText == 'Template:Archive' then
		mainTemplatePage = 'talk'
	end
	
	local mbox = require('Module:Message box').main('mbox', {
		demospace = args.demospace or mainTemplatePage,
		image = image,
		imageright = args.imageright,
		style = args.style or '',
		textstyle = args.textstyle or 'text-align:center',
		text = self:makeBlurb(),
	})

	return mbox
end

function Navigator:getArchiveNums()
	-- Returns an array of the archive numbers to format.
	local noLinks = tonumber(self.args.links) or self:message('default-link-count')
	noLinks = math.floor(noLinks)
	-- If |noredlinks is "yes", true or absent, don't allow red links. If it is 
	-- 'no' or false, allow red links.
	local allowRedLinks = yesno(self.args.noredlinks) == false
	
	local current = self.currentArchiveNum
	local highest = self.highestArchiveNum

	if not current or not highest or noLinks < 1 then
		return {}
	elseif noLinks == 1 then
		return {current}
	end

	local function getNum(i, current)
		-- Gets an archive number given i, the position in the array away from
		-- the current archive, and the current archive number. The first two
		-- offsets are consecutive; the third offset is rounded up to the
		-- nearest 5; and the fourth and subsequent offsets are rounded up to
		-- the nearest 10. The offsets are calculated in such a way that archive
		-- numbers will not be duplicated.
		if -2 <= i and i <= 2 then
			return current + i
		elseif -3 <= i and i <= 3 then
			return current + 2 - (current + 2) % 5 + (i / 3) * 5
		elseif 4 <= i then
			return current + 7 - (current + 7) % 10 + (i - 3) * 10
		else
			return current + 2 - (current + 2) % 10 + (i + 3) * 10
		end
	end

	local nums = {}

	-- Archive nums lower than the current page.
	for i = -1, -math.floor((noLinks - 1) / 2), -1 do
		local num = getNum(i, current)
		if num <= 1 then
			table.insert(nums, 1, 1)
			break
		else
			table.insert(nums, 1, num)
		end
	end

	-- Current page.
	if nums[#nums] < current then
		table.insert(nums, current)
	end

	-- Higher archive nums.
	for i = 1, math.ceil((noLinks - 1) / 2) do
		local num = getNum(i, current)
		if num <= highest then
			table.insert(nums, num)
		elseif allowRedLinks and (i <= 2 or i <= 3 and num == nums[#nums] + 1) then
			-- Only insert one red link, and only if it is consecutive.
			table.insert(nums, highest + 1)
			break
		elseif nums[#nums] < highest then
			-- Insert the highest archive number if it isn't already there.
			table.insert(nums, highest)
			break
		else
			break
		end
	end

	return nums
end

function Navigator:makeArchiveLinksWikitable()
	local args = self.args
	local lang = mw.language.getContentLanguage()
	local nums = self:getArchiveNums()
	local noLinks = #nums
	
	-- Skip number processing if |prev and |next are defined.
	if args.prev or args.next then
		local archives = {}
		if args.prev then archives[#archives + 1] = mw.title.new(args.prev) end
		archives[#archives + 1] = self.currentTitle
		if args.next then archives[#archives + 1] = mw.title.new(args.next) end
		
		local table = makeTable('30em')
		for _, title in ipairs(archives) do
			if tostring(title) == self.currentTitle.prefixedText then
				table:tag("td"):wikitext(string.format(
					'<span style="font-size:115%%;">%s</span>',
					makeWikilink(title.fullText, title.subpageText)
				))
			else
				table:tag("td"):wikitext(
					makeWikilink(title.fullText, title.subpageText)
				)
			end
		end
		return tostring(table)
	end
	if noLinks < 1 then
		return ''
	end

	-- Make the table of links.
	local links = {}
	local isCompact = noLinks > 7
	local currentIndex
	for i, num in ipairs(nums) do
		local subpage = self.archivePrefix .. tostring(num)
		local display
		if isCompact then
			display = tostring(num)
		else
			display = self:message('archive-link-display', num)
		end
		local link = makeWikilink('../' .. subpage, display)
		if num == self.currentArchiveNum then
			link = string.format('<span style="font-size:115%%;">%s</span>', link)
			currentIndex = i
		end
		table.insert(links, link)
	end

	-- Add the arrows.
	-- We must do the forwards arrow first as we are adding elements to the
	-- links table. If we did the backwards arrow first the index for the
	-- current archive would be wrong.
	currentIndex = currentIndex or math.ceil(#links / 2)
	for i = currentIndex + 1, #links do
		if nums[i] - nums[i - 1] > 1 then
			table.insert(links, i, lang:getArrow('forwards'))
			break
		end
	end
	for i = currentIndex - 1, 1, -1 do
		if nums[i + 1] - nums[i] > 1 then
			table.insert(links, i + 1, lang:getArrow('backwards'))
			break
		end
	end

	-- Output the wikitable.
	local width
	if noLinks <= 3 then
		width = string.format('%dem', noLinks * 10)
	elseif noLinks <= 7 then
		width = string.format('%dem', (noLinks + 3) * 5)
	else
		width = '37em'
	end
	local table = makeTable(width)
	for _, s in ipairs(links) do
		table:tag("td"):wikitext(s)
	end
	
	return tostring(table)
end

function Navigator:__tostring()
	local args = self.args
	local boxComponents
	
-- Is |omit filled? If not, make the whole box.
	if args.omit == nil then

-- Check for annual archives
	local currentTitle = self.currentTitle
	local yearlyBase, isYearly, yearlyPattern = detectArchiveFormat(currentTitle, args)

	if isYearly then
    	-- Use a yearly archive format
    	local linksYearlyListSeparator = "&#32;" -- Space separator
    	local linksYearlyList = mw.getCurrentFrame():expandTemplate {
    	    title = 'Yearly archive list',
        	args = { 
        		root = yearlyBase, 
        		sep = linksYearlyListSeparator,
        		prefix = yearlyPattern
        	}
    	}
    	boxComponents = self:makeMessageBox() .. '\n' ..
        '<div style="font-size:115%; width:100%; word-spacing:1em; text-align:center;">' .. linksYearlyList .. '</div>'
	else
    	-- Default numbered archive format
    	boxComponents = self:makeMessageBox() .. '\n' .. self:makeArchiveLinksWikitable()
	end	-- We're omitting the banner, so we should only make the links table.
	elseif args.omit == 'banner' then 
		boxComponents = self:makeArchiveLinksWikitable()
	-- We're omitting the archives, so we should only make the banner.
    elseif args.omit == 'archives' then
        boxComponents = self:makeMessageBox()
	end

	-- Allow for demo pages to be edited freely. 
	if not args.demospace then
		boxComponents = boxComponents .. ' __NONEWSECTIONLINK__ __NOEDITSECTION__ __ARCHIVEDTALK__'
	end

	return boxComponents
end

-------------------------------------------------------------------------------
-- Exports
-------------------------------------------------------------------------------

local p = {}

function p._exportClasses()
	return {
		Navigator = Navigator
	}
end

function p._aan(args, cfg, currentTitle)
	cfg = cfg or mw.loadData('Module:Archive/config')
	currentTitle = currentTitle or mw.title.getCurrentTitle()
	local aan = Navigator.new(args, cfg, currentTitle)
	return tostring(aan)
end

function p.aan(frame)
	local args = require('Module:Arguments').getArgs(frame, {
		wrappers = 'Template:Archive',
	})
	return p._aan(args)
end

return p