Module:Message box

    From Nonbinary Wiki
    Revision as of 12:46, 26 September 2013 by m>Mr. Stradivarius (add mbox, various other fixes)

    Documentation for this module may be created at Module:Message box/doc

    -- This is a meta-module for producing message box templates, including {{mbox}}, {{ambox}}, {{imbox}}, {{tmbox}}, {{ombox}}, {{cmbox}} and {{fmbox}}.
    
    -- Require necessary modules.
    local htmlBuilder = require('Module:HtmlBuilder')
    local categoryHandler = require('Module:Category handler').main
    local yesno = require('Module:Yesno')
    
    -- Get a language object for formatDate and ucfirst.
    local lang = mw.language.getContentLanguage()
    
    -- Set aliases for often-used functions to reduce table lookups.
    local format = mw.ustring.format
    local tinsert = table.insert
    local tconcat = table.concat
    
    local p = {}
    
    local function getTitleObject(page)
        if type(page) == 'string' then
            -- Get the title object, passing the function through pcall 
            -- in case we are over the expensive function count limit.
            local success, title = pcall(mw.title.new, page)
            if success then
                return title
            end
        end
    end
    
    local function presentButBlank(s)
        if type(s) ~= 'string' then return end
        if s and not mw.ustring.find(s, '%S') then
            return true
        else
            return false
        end
    end
    
    local function formatCategory(cat, date, all)
        local ret = {}
        cat = type(cat) == 'string' and cat
        date = type(date) == 'string' and date
        all = type(all) == 'string' and all
        local preposition = 'from'
        if cat and date then
            local catTitle = format('Category:%s %s %s', cat, preposition, date)
            tinsert(ret, format('[[%s]]', catTitle))
            catTitle = getTitleObject(catTitle)
            if not catTitle or not catTitle.exists then
                tinsert(ret, '[[Category:Articles with invalid date parameter in template]]')
            end
        elseif cat and not date then
            tinsert(ret, format('[[Category:%s]]', cat))
        end
        if all then
            tinsert(ret, format('[[Category:%s]]', all))
        end
        return tconcat(ret)
    end
    
    local function union(t1, t2)
        -- Returns the union of two arrays.
        local vals = {}
        for i, v in ipairs(t1) do
            vals[v] = true
        end
        for i, v in ipairs(t2) do
            vals[v] = true
        end
        local ret = {}
        for k in pairs(vals) do
            tinsert(ret, k)
        end
        table.sort(ret)
        return ret
    end
    
    local function getArgNums(args, prefix)
        local nums = {}
        for k, v in pairs(args) do
            local num = mw.ustring.match(tostring(k), '^' .. prefix .. '([1-9]%d*)$')
            if num then
                tinsert(nums, tonumber(num))
            end
        end
        table.sort(nums)
        return nums
    end
    
    local function getNamespaceId(ns)
        if type(ns) == 'string' then
            ns = lang:ucfirst(mw.ustring.lower(ns))
            if ns == 'Main' then
                ns = 0
            end
        end
        local nsTable = mw.site.namespaces[ns]
        if nsTable then
            return nsTable.id
        end
    end
    
    local function getMboxType(nsid)
        -- Gets the mbox type from a namespace number.
        if nsid == 0 then
            return 'ambox' -- main namespace
        elseif nsid == 6 then
            return 'imbox' -- file namespace
        elseif nsid == 14 then
            return 'cmbox' -- category namespace
        else
            local nsTable = mw.site.namespaces[nsid]
            if nsTable and nsTable.isTalk then
                return 'tmbox' -- any talk namespace
            else
                return 'ombox' -- other namespaces or invalid input
            end
        end
    end
    
    function p.build(boxType, args)
        if type(args) ~= 'table' then
            error(format('invalid "args" parameter type; expected type "table", got type "%s"', type(args)), 2)
        end
    
        -- Get the title object and the namespace.
        local title = getTitleObject(args.page) or mw.title.getCurrentTitle()
        local nsid = getNamespaceId(args.demospace) or title.namespace
    
        -- Get the box config data from the data page.
        if boxType == 'mbox' then
            boxType = getMboxType(nsid)
        end
        local dataTables = mw.loadData('Module:Message box/data')
        local data = dataTables[boxType]
        if not data then
            local boxTypes = {}
            for k, v in pairs(dataTables) do
                tinsert(boxTypes, format('"%s"', k))
            end
            tinsert(boxTypes, '"mbox"')
            error(format('invalid message box type "%s"; valid types are %s', tostring(boxType), mw.text.listToText(boxTypes)), 2)
        end
    
        ------------------------ Process config data ----------------------------
    
        -- Type data.
        local typeData = data.types[args.type]
        local invalidType = args.type and not typeData and true or false
        typeData = typeData or data.types[data.default]
    
        -- Process data for collapsible text fields
        local name, issue, talk, fix, date, info
        if data.useCollapsibleTextFields then
            name = args.name
            local nameTitle = getTitleObject(name)
            local isTemplatePage = nameTitle and title.prefixedText == ('Template:' .. nameTitle.text) and true or false
            local sect = args.sect
            if presentButBlank(sect) then
                sect = format('This %s ', data.sectionDefault or 'page')
            elseif type(sect) == 'string' then
                sect = 'This ' .. sect .. ' '
            end
            issue = (sect or '') .. (args.issue or '') .. ' ' .. (args.text or '')
            talk = args.talk
            if presentButBlank(talk) and isTemplatePage then
                talk = '#'
            end
            fix = args.fix
            date = args.date
            if presentButBlank(date) and isTemplatePage then
                date = lang:formatDate('F Y')
            end
            info = args.info
        end
    
        -- Process the talk link, if present.
        if talk then
            -- See if the talk link exists and is for a talk or a content namespace.
            local talkTitle = type(talk) == 'string' and getTitleObject(talk)
            if not talkTitle or not talkTitle.isTalkPage then
                -- If we couldn't process the talk page link, get the talk page of the current page.
                local success
                success, talkTitle = pcall(title.talkPageTitle, title)
                if not success then
                    talkTitle = nil
                end
            end
            if talkTitle and talkTitle.exists then
                local talkText = ' Relevant discussion may be found on'
                if talkTitle.isTalkPage then
                    talkText = format('%s [[%s|%s]].', talkText, talk, talkTitle.prefixedText)
                else
                    talkText = format('%s the [[%s#%s|talk page]].', talkText, talkTitle.prefixedText, talk)
                end
                talk = talkText
            end
        end
    
        -- Find whether we are using a small message box and process our data accordingly.
        local isSmall = data.allowSmall and (args.small == 'yes' or args.small == true) and true or false
        local smallClass, image, imageRight, text, imageSize
        if isSmall then
            smallClass = data.smallClass or 'mbox-small'
            image = args.smallimage or args.image
            imageRight = args.smallimageright or args.imageright
            if data.useCollapsibleTextFields then
                text = args.smalltext or issue
            else
                text = args.smalltext or args.text
            end
            imageSize = data.imageSmallSize or '30x30px'
        else
            image = args.image
            imageRight = args.imageright
            imageSize = '40x40px'
            text = args.text
        end
    
        -- Process mainspace categories.
        local mainCats = {}
        local origCategoryNums -- origCategoryNums might be used in computing the template error category.
        if data.allowMainspaceCategories then
            -- Categories for the main namespace.
            local origCatNums = getArgNums(args, 'cat')
            local origCategoryNums = getArgNums(args, 'category')
            local catNums = union(origCatNums, origCategoryNums)
            for _, num in ipairs(catNums) do
                local cat = args['cat' .. tostring(num)] or args['category' .. tostring(num)]
                local all = args['all' .. tostring(num)]
                tinsert(mainCats, formatCategory(cat, args.date, all))
            end
        end
    
        -- Process template namespace categories
        local templateCats = {}
        if data.templateCategory and not title.isSubpage and not yesno(args.nocat) then
            tinsert(templateCats, format('[[Category:%s]]', data.templateCategory))
        end
    
        -- Add an error category for the template namespace if appropriate.
        if data.templateErrorCategory then
            local catName = data.templateErrorCategory
            local templateCat
            if not name and not title.isSubpage then
                templateCat = format('[[Category:%s]]', catName)
            elseif type(name) == 'string' and title.prefixedText == ('Template:' .. name) then
                local paramsToCheck = data.templateErrorParamsToCheck or {}
                local count = 0
                for i, param in ipairs(paramsToCheck) do
                    if not args[param] then
                        count = count + 1
                    end
                end
                if count > 0 then
                    templateCat = format('[[Category:%s|%d]]', catName, count)
                end
                if origCategoryNums and #origCategoryNums > 0 then
                    templateCat = format('[[Category:%s|C]]', catName)
                end
            end
            tinsert(templateCats, templatecat)
        end
    
        -- Categories for all namespaces.
        local allCats = {}
        if invalidType then
            local catsort = (nsid == 0 and 'Main:' or '') .. title.prefixedText
            tinsert(allCats, format('[[Category:Wikipedia message box parameter needs fixing|%s]]', catsort))
        end
    
        ------------------------ Build the box ----------------------------
        
        local root = htmlBuilder.create()
    
        -- Do the subst check.
        if data.substCheck and args.subst == 'SUBST' then
            if type(name) == 'string' then
                root
                    .tag('b')
                        .addClass('error')
                        .wikitext(format(
                            'Template <code>%s%s%s</code> has been incorrectly substituted.',
                            mw.text.nowiki('{{'), name, mw.text.nowiki('}}')
                        ))
            end
            tinsert(allCats, '[[Category:Pages with incorrectly substituted templates]]')
        end
    
        -- Create the box table.
        local box = root.tag('table')
        box
            .attr('id', args.id)
        for i, class in ipairs(data.classes) do
            box
                .addClass(class)
        end
        box
            .addClass(isSmall and smallClass)
            .addClass(data.classPlainlinksYesno and yesno(args.plainlinks or true) and 'plainlinks')
            .addClass(typeData.class)
            .addClass(args.class)
            .cssText(args.style)
            .attr('role', 'presentation')
    
        -- Add the left-hand image.
        local row = box.tag('tr')
        local imageCheckBlank = data.imageCheckBlank
        if image ~= 'none' and not imageCheckBlank or image ~= 'none' and imageCheckBlank and image ~= 'blank' then
            local imageLeftCell = row.tag('td').addClass('mbox-image')
            if not isSmall and data.imageCellDiv then
                imageLeftCell = imageLeftCell.tag('div').css('width', '52px') -- If we are using a div, redefine imageLeftCell so that the image is inside it.
            end
            imageLeftCell
                .wikitext(image or format('[[File:%s|%s|link=|alt=]]', typeData.image, imageSize))
        elseif data.imageEmptyCell then
            row.tag('td')
                .addClass('mbox-empty-cell') -- No image. Cell with some width or padding necessary for text cell to have 100% width.
                .cssText(data.imageEmptyCellStyle and 'border:none;padding:0px;width:1px')
        end
    
        -- Add the text.
        local textCell = row.tag('td').addClass('mbox-text')
        if data.useCollapsibleTextFields then
            textCell
                .cssText(args.textstyle)
            local textCellSpan = textCell.tag('span')
            textCellSpan
                .addClass('mbox-text-span')
                .wikitext(issue)
            if not isSmall then
                textCellSpan
                    .tag('span')
                        .addClass('hide-when-compact')
                        .wikitext(talk)
                        .wikitext(' ')
                        .wikitext(fix)
                        .done()
            end
            textCellSpan
                .wikitext(date and format(" <small>''(%s)''</small>", date))
            if not isSmall then
                textCellSpan
                    .tag('span')
                        .addClass('hide-when-compact')
                        .wikitext(info and ' ' .. info)
            end
        else
            textCell
                .cssText(args.textstyle)
                .wikitext(text)
        end
    
        -- Add the right-hand image.
        if imageRight and not (data.imageRightNone and imageRight == 'none') then
            local imageRightCell = row.tag('td').addClass('mbox-imageright')
            if not isSmall and data.imageCellDiv then
                imageRightCell = imageRightCell.tag('div').css('width', '52px') -- If we are using a div, redefine imageRightCell so that the image is inside it.
            end
            imageRightCell
                .wikitext(imageRight)
        end
    
        -- Add the below row.
        if data.below and args.below then
            box.tag('tr')
                .tag('td')
                    .attr('colspan', args.imageright and '3' or '2')
                    .addClass('mbox-text')
                    .cssText(args.textstyle)
                    .wikitext(args.below)
        end
    
        ------------------------ Error messages and categories ----------------------------
    
        -- Add error message for invalid type parameters.
        if invalidType then
            root
                .tag('div')
                    .addClass('error')
                    .css('text-align', 'center')
                    .wikitext(format('This message box is using an invalid type parameter (<code>type=%s</code>) and needs fixing.', args.type or ''))
        end
    
        -- Add categories using categoryHandler.
        root
            .wikitext(categoryHandler{
                main = tconcat(mainCats),
                template = tconcat(templateCats),
                all = tconcat(allCats)
            })
        
        return tostring(root)
    end
    
    local function makeWrapper(boxType)
        return function (frame)
            -- If called via #invoke, use the args passed into the invoking
            -- 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.
            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
            -- 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 ~= '' or k == 'talk' or k == 'sect' or k == 'date' then
                    args[k] = v
                end
            end
            return p.build(boxType, args)
        end
    end
    
    p.mbox = makeWrapper('mbox')
    p.ambox = makeWrapper('ambox')
    p.cmbox = makeWrapper('cmbox')
    p.fmbox = makeWrapper('fmbox')
    p.imbox = makeWrapper('imbox')
    p.ombox = makeWrapper('ombox')
    p.tmbox = makeWrapper('tmbox')
    
    return p