Module:Archive list

From finiki, the Canadian financial wiki
-- This module implements {{archive list}} in Lua, and adds a few
-- new features.

-- Process a numeric argument to make sure it is a positive
-- integer.
local function processNumArg( num )
    if num then
        num = tonumber( num )
        if type( num ) == 'number' then
            num = math.floor( num )
            if num >= 0 then
                return num
            end
        end
    end
    return 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
    else
        if titleObject then
            return titleObject.exists
        else
            return false -- Return false if given a bad title.
        end
    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 == 'yes' 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==true 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==true 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

-- Get the number to start listing archives from.
local function getStart( start )
    start = processNumArg( start )
    if start then
        return start
    else
        return 1
    end
end

-- Get whether to 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 
local function getLeaveFirstCellBlank( leaveFirstCellBlank ) 
    return leaveFirstCellBlank == 'yes' 
       or leaveFirstCellBlank == 'y' 
       or leaveFirstCellBlank == 1 
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'
            or sep == 'tpt-languages' then
            return mw.message.new( sep .. '-separator' ):plain()
        else
            return sep
        end
    else
        return nil
    end
end

-- Generates the list of archive links. glargs.max must be either zero (for
-- no archives) or a positive integer value.
local function generateLinks( glargs )
    if type( glargs ) ~= 'table' or not glargs.max or not glargs.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 one.
    if glargs.max == 0 then
    	if glargs.isLong == true then
    		glargs.max = 1 -- One archive redlink is displayed for Long format
    	else -- Short error and a creat link is displayed for short
        	return 'no archives yet ([[' .. glargs.prefix .. '1|create]])'
        end
    end
    -- Return an html error if the start number is greater than the 
    -- maximum number.
    local start = glargs.start or 1
    if start > glargs.max then
        return '<span class="error">Start value "' 
            .. tostring( start ) 
            .. '" is greater than the most recent archive number "' 
            .. tostring( glargs.max ) 
            .. '".</span>'
    end
    local linkPrefix = glargs.linkPrefix or ''
        local lineNum = glargs.lineNum or 10
    local sep = '' -- Long default separator is cell elements, short is ', '
    local lineSep = '' -- Long default linebreak is row elements short is '\n'
    if glargs.isLong==true then 
    	sep = glargs.sep or ''
    	sep = sep .. '</td><td>'
    	lineSep = glargs.lineSep or ''
		lineSep = lineSep .. '</td></tr><tr><td>'
    else
    	sep = glargs.sep or mw.message.new( 'comma-separator' ):plain()
    	lineSep = glargs.lineSep or '<br />'
    end
    -- Generate the archive links.
    local lineCounter = 1 -- The counter to see whether we need a line break or not.
    local ret = {} -- A table containing the strings to be returned.
    if glargs.isLong == true then --Long version is a table
    	table.insert(ret, "<table style=\"width: 100%; padding: 0px; text-align: center; background-color: transparent;\"><tr><td>")
    end
    if glargs.leaveFirstCellBlank then 
       -- An empty first cell aligns the first column on multiples of lineNum 
       table.insert(ret, sep) 
       lineCounter = lineCounter + 1
    end 
    for archiveNum = start, glargs.max do
        local link = mw.ustring.format(
            '[[%s%d|%s%d]]',
            glargs.prefix, archiveNum, linkPrefix, archiveNum
        )
        table.insert( ret, 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 < glargs.max then
            table.insert( ret, sep )
            lineCounter = lineCounter + 1
        -- Output new lines if needed. We don't need a new line after
        -- the last link.
        elseif lineCounter >= lineNum and archiveNum < glargs.max then
            table.insert( ret, lineSep )
            lineCounter = 1
        end
    end
    if glargs.isLong == true then --Long version is a table
    	table.insert(ret, "</td></tr></table>")
    end
    return table.concat( ret )
end

-- Determine if format should be long
local function findFormType( auto )
	if auto == nil or auto == '' then
		return false
	elseif auto == 'long' then
			return true
	else
		return false
	end
end

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

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

function makeWrapper( func )
    return function( frame )
        -- If we are being called from #invoke, get the args from #invoke
        -- if they exist, or else get the arguments passed to the parent
        -- frame. Otherwise, assume the arguments are being passed directly
        -- in from another module or from the debug console.
        local origArgs
        if frame == mw.getCurrentFrame() then
            origArgs = frame:getParent().args
            for k, v in pairs( frame.args ) do
                origArgs = frame.args
                break
            end
        else
            origArgs = frame
        end
        
        -- Ignore blank values for parameters other than "links",
        -- which functions differently depending on whether it is
        -- blank or absent.
        local args = {}
        for k, v in pairs( origArgs ) do
            if k == 'links' or v ~= '' then
                args[k] = v
            end
        end
        
        return func( args )
    end
end

return {
    main = makeWrapper( _main ),
    count = makeWrapper( _count )
}