Jump to content

Module:Archive list

From Wikisource

-- This module implements [[Template:Archive list]] in Lua, and adds a few new features.
require('strict')

local p = {}

local getArgs = require('Module:Arguments').getArgs
local yesno = require('Module:Yesno')
local function error_text(message)
	return require('Module:Error')['error']({message = message})
end

-- Process a numeric argument to make sure it is a positive integer.
local function processNumArg(num)
	num = tonumber(num)
	if not num then
		return nil
	end
	num = math.floor(num)
    return (num >= 0 and num) or nil
end

-- Checks whether a page exists, going through pcall in case we are over the expensive function limit.
local function checkPageExists(title)
	if not title then
		error('No title passed to checkArchiveExists', 2)
	end
	local noError, titleObject = pcall(mw.title.new, title)
	if not noError then
		-- If we are over the expensive function limit then assume that the page doesn't exist.
		return false
	elseif titleObject then
		return titleObject.exists
	else
		return false -- Return false if given a bad title.
    end
end

--[=[
Checks every nth archive to see if it exists,
and returns the number of the first archive that doesn't exist.
It is necessary to do this in batches
because each check is an expensive function call,
and we want to avoid making too many of them
so as not to go over the expensive function limit.
]=]
local function checkArchives(prefix, n, start)
    local i = start
    local exists = true
    while exists do
        exists = checkPageExists( prefix .. tostring(i))
        if exists then
            i = i + n
        end
    end
    return i
end

--[=[
-- Return the biggest archive number, using checkArchives() and starting in intervals of 1000.
This should get us a maximum of 500,000 possible archives before we hit the expensive function limit.
]=]
local function getBiggestArchiveNum(prefix, start, max)
    -- Return the value for max if it is specified.
    max = processNumArg(max)
    if max then
        return max
    end
    
    -- Otherwise, detect the largest archive number.
    start = start or 1
    local check1000 = checkArchives(prefix, 1000, start)
    if check1000 == start then
        return 0 -- Return 0 if no archives were found.
    end
    local check200 = checkArchives(prefix, 200, check1000 - 1000)
    local check50 = checkArchives(prefix, 50, check200 - 200)
    local check10 = checkArchives(prefix, 10, check50 - 50)
    local check1 = checkArchives(prefix, 1, check10 - 10)
    -- check1 is the first page that doesn't exist, so we want to
    -- subtract it by one to find the biggest existing archive.
    return check1 - 1
end

-- Get the archive link prefix (the title of the archive pages minus the number).
local function getPrefix( root, prefix, prefixSpace )
    local ret = root or mw.title.getCurrentTitle().prefixedText
    ret = ret .. '/'
    if prefix then
        ret = ret .. prefix
        if prefixSpace == 'yes' then
            ret = ret .. ' '
        end
    else
        ret = ret .. 'Archive '
    end
    return ret
end

-- Get the number of archives to put on one line.
-- Set to math.huge if there should be no line breaks.
local function getLineNum( links, nobr, isLong )
    local linksToNum = tonumber( links )
    local lineNum
    if nobr or (links and not linksToNum) then
        lineNum = math.huge
    -- If links is a number, process it. Negative values and expressions
    -- such as links=8/2 produced some interesting values with the old
    -- template, but we will ignore those for simplicity.
    elseif type(linksToNum) == 'number' and linksToNum >= 0 then
        -- The old template rounded down decimals to the nearest integer.
        lineNum = math.floor( linksToNum )
        if lineNum == 0 then
            -- In the old template, values of links between 0 and 0.999
            -- suppressed line breaks.
            lineNum = math.huge
        end
    else
    	if isLong then
    		lineNum = 3 -- Default to 3 links on long
    	else
        	lineNum = 10 -- Default to 10 on short
        end
    end
    return lineNum
end

-- Gets the prefix to put before the archive links.
local function getLinkPrefix( prefix, space, isLong )
    -- Get the link prefix.
    local ret = ''
    if isLong then ---- Default of old template for long is 'Archive '
    	if type(prefix) == 'string' then
    		if prefix == 'none' then -- 'none' overrides to empty prefix
    			ret = ''
    		 else
    		 	ret = prefix
    		 	if space == 'yes' then
    		 		ret = ret .. ' '
    		 	end
		 	end
	 	else
	 		ret = 'Archive '
		end
	else --type is not long
		if type(prefix) == 'string' then
        	ret = prefix
        	if space == 'yes' then
        	    ret = ret .. ' '
        	end
    	end
    end
    return ret
end

-- Process the separator parameter.
local function getSeparator(sep)
    if sep and type(sep) == 'string' then
        if sep == 'dot' or sep == 'pipe' or sep == 'comma' then
            return mw.message.new(sep .. '-separator' ):plain()
        else
            return sep
        end
    else
        return nil
    end
end

-- Generates the list of archive links. args.max must be either zero (for no archives) or a positive integer value.
local function generateLinks(args)
	if type(args) ~= 'table' or not args.max or not args.prefix then
		error('insufficient arguments passed to generateLinks', 2)
	end
	
	-- If there are no archives yet, return a message and a link to create Archive 1.
	if args.max == 0 then
		if args.isLong then
			args.max = 1 -- One archive redlink is displayed for Long format
		else -- Short error and a create link is displayed for short
			return 'no archives yet ([[' .. args.prefix .. '1|create]])'
		end
	end
	
	-- Return an HTML error if the start number is greater than the maximum number.
	local start = args.start or 1
	if start > args.max then
		return error_message('Start value \"' .. tostring(start) .. '\" is greater than the most recent archive number \"' .. tostring(args.max) .. '\".')
	end
	
	-- Generate the archive links
	local link_container
	local link_line
	local isLong = args.isLong
	local sep = args.sep
	local lineSep = args.lineSep
	local linkPrefix = args.linkPrefix or ''
	local lineNum = args.lineNum or 10
	
	if isLong then
		link_container = mw.html.create('table')
			:css({['width'] = '100%', ['padding'] = '0', ['text-align'] = 'center', ['background-color'] = 'transparent', ['color'] = 'var(--color-base, #202122)'})
		link_line = link_container:tag('tr')
	else
		sep = sep or mw.message.new('comma-separator'):plain()
		lineSep = lineSep or mw.html.create('br')
		link_container = mw.html.create('span')
	end
	
	local lineCounter = 1 -- The counter to see whether we need a line break or not
	
	if args.leaveFirstCellBlank then
		-- Leave a blank cell in the first row and column.
		-- If links start at 1, and lineNum is a round number, this aligns the first column to start on a multiple of lineNum, which may be a nice round number.
		if isLong then
			link_line:tag('td'):wikitext(sep)
		elseif type(sep) == 'string' then
			link_container:wikitext(sep)
		else
			link_container:node(sep)
		end
		lineCounter = lineCounter + 1
	end
	
	for archiveNum = start, args.max do
		local link = mw.ustring.format('[[%s%d|%s%d]]', args.prefix, archiveNum, linkPrefix, archiveNum)
		
		if isLong then
			local link_cell = link_line:tag('td'):wikitext(link)
			
			-- If we don't need a new line, output a comma. We don't need a comma after the last link.
			if lineCounter < lineNum and archiveNum < args.max then
				link_cell:wikitext(sep)
				lineCounter = lineCounter + 1
			-- Output new lines if needed. We don't need a new line after the last link.
			elseif archiveNum < args.max then
				link_cell:wikitext(lineSep)
				link_line = link_container:tag('tr')
				lineCounter = 1
			end
		else
			link_container:wikitext(link)
			
			-- If we don't need a new line, output a comma. We don't need a comma after the last link.
			if lineCounter < lineNum and archiveNum < args.max then
				if type(sep) == 'string' then
					link_container:wikitext(sep)
				else
					link_container:node(sep)
				end
				lineCounter = lineCounter + 1
			-- Output new lines if needed. We don't need a new line after the last link.
			elseif archiveNum < args.max then
				if type(lineSep) == 'string' then
					link_container:wikitext(lineSep)
				else
					link_container:node(lineSep)
				end
				lineCounter = 1
			end
		end
	end
	
	return tostring(link_container)
end

-- Get the archive data and pass it to generateLinks().
local function _main(args)
	local start = processNumArg(args.start) or 1
	local isLong = args.auto == 'long'
	local prefix = getPrefix(args.root, args.prefix, args.prefixspace)
    return generateLinks({
        start = start,
        max = getBiggestArchiveNum(prefix, start, args.max),
        prefix = prefix,
        linkPrefix = getLinkPrefix(args.linkprefix, args.linkprefixspace, isLong),
        isLong = isLong,
        sep = getSeparator(args.sep),
        lineNum = getLineNum(args.links, yesno(args.nobr) or false, isLong),
        lineSep = getSeparator(args.linesep),
        leaveFirstCellBlank = yesno(args.leavefirstcellblank) or false
    })
end

-- A wrapper function to make getBiggestArchiveNum() available from #invoke.
local function _count(args)
	return getBiggestArchiveNum(getPrefix(args.root, args.prefix, args.prefixspace))
end

local function makeWrapper(func)
	return function(frame)
		local origArgs = getArgs(frame, {removeBlanks = false})
        local args = {}
        
        -- Ignore blank values for parameters other than "links", which functions differently depending on whether it is blank or absent.
        for k, v in pairs(origArgs) do
        	if k == 'links' or v ~= '' then
        		args[k] = v
        	end
        end
        
		return func(args)
	end
end

p.main = makeWrapper(_main)
p.count = makeWrapper(_count)

return p