Module:Message box: Difference between revisions

    From Nonbinary Wiki
    m>Mr. Stradivarius
    (pass nocat, page, and demospace parameters to Module:Category handler, and add a choice for which arguments to allow as blank)
    m>Mr. Stradivarius
    (rewrite with a "box" object to make the code a little less spaghetti-like)
    Line 14: Line 14:
    local tconcat = table.concat
    local tconcat = table.concat


    local p = {}
    local box = {}


    local function getTitleObject(page)
    local function getTitleObject(page)
    Line 34: Line 34:
             return false
             return false
         end
         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
    end


    Line 87: Line 65:
    end
    end


    local function getNamespaceId(ns)
    function box.getNamespaceId(ns)
         if not ns then return end
         if not ns then return end
         if type(ns) == 'string' then
         if type(ns) == 'string' then
    Line 101: Line 79:
    end
    end


    local function getMboxType(nsid)
    function box.getMboxType(nsid)
         -- Gets the mbox type from a namespace number.
         -- Gets the mbox type from a namespace number.
         if nsid == 0 then
         if nsid == 0 then
    Line 119: Line 97:
    end
    end


    function p.build(boxType, args)
    function box:addCat(ns, cat, sort)
         if type(args) ~= 'table' then
         if type(cat) ~= 'string' then return end
            error(format('invalid "args" parameter type; expected type "table", got type "%s"', type(args)), 2)
        local nsVals = {'main', 'template', 'all'}
        local tname
        for i, val in ipairs(nsVals) do
            if ns == val then
                tname = ns .. 'Cats'
            end
         end
         end
        if not tname then
            for i, val in ipairs(nsVals) do
                nsVals[i] = format('"%s"', val)
            end
            error('invalid ns parameter passed to box:addCat; valid values are ' .. mw.text.listToText(nsVals, nil, ' or '))
        end
        self[tname] = self[tname] or {}
        if type(sort) == 'string' then
            tinsert(self[tname], format('[[Category:%s|%s]]', cat, sort))
        else
            tinsert(self[tname], format('[[Category:%s]]', cat))
        end
    end


    function box:addClass(class)
        if type(class) ~= 'string' then return end
        self.classes = self.classes or {}
        tinsert(self.classes, class)
    end
    function box:setTitle(args)
         -- Get the title object and the namespace.
         -- Get the title object and the namespace.
         local pageTitle = getTitleObject(args.page ~= '' and args.page)
         local pageTitle = getTitleObject(args.page ~= '' and args.page)
         local title = pageTitle or mw.title.getCurrentTitle()
         self.title = pageTitle or mw.title.getCurrentTitle()
         local demospace = getNamespaceId(args.demospace ~= '' and args.demospace)
         local demospace = box.getNamespaceId(args.demospace ~= '' and args.demospace)
         local nsid = demospace or title.namespace
         self.nsid = demospace or self.title.namespace
    end


    function box:getConfig(boxType)
         -- Get the box config data from the data page.
         -- Get the box config data from the data page.
         if boxType == 'mbox' then
         if boxType == 'mbox' then
             boxType = getMboxType(nsid)
             boxType = box.getMboxType(self.nsid)
         end
         end
         local dataTables = mw.loadData('Module:Message box/data')
         local cfgTables = mw.loadData('Module:Message box/configuration')
         local data = dataTables[boxType]
         local cfg = cfgTables[boxType]
         if not data then
         if not cfg then
             local boxTypes = {}
             local boxTypes = {}
             for k, v in pairs(dataTables) do
             for k, v in pairs(dataTables) do
    Line 144: Line 149:
             error(format('invalid message box type "%s"; valid types are %s', tostring(boxType), mw.text.listToText(boxTypes)), 2)
             error(format('invalid message box type "%s"; valid types are %s', tostring(boxType), mw.text.listToText(boxTypes)), 2)
         end
         end
        return cfg
    end
          
          
         -- Only allow blank arguments for the parameter names listed in data.allowBlankParams.
    function box:removeBlankArgs(cfg, args)
         -- Only allow blank arguments for the parameter names listed in cfg.allowBlankParams.
         local newArgs = {}
         local newArgs = {}
         for k, v in pairs(args) do
         for k, v in pairs(args) do
             for i, param in ipairs(data.allowBlankParams or {}) do
             for i, param in ipairs(cfg.allowBlankParams or {}) do
                 if v ~= '' or k == param then
                 if v ~= '' or k == param then
                     newArgs[k] = v
                     newArgs[k] = v
    Line 154: Line 162:
             end
             end
         end
         end
         args = newArgs
         return newArgs
         newArgs = nil
    end
     
    function box:setBoxParameters(cfg, args)
        -- Get type data.
        self.type = args.type
        local typeData = cfg.types[self.type]
        self.invalidType = self.type and not typeData and true or false
        typeData = typeData or cfg.types[cfg.default]
        self.typeClass = typeData.class
        self.typeImage = typeData.image
     
        -- Find if the box has been wrongly substituted.
        if cfg.substCheck and args.subst == 'SUBST' then
            self.isSubstituted = true
        end
     
        -- Find whether we are using a small message box.
         self.isSmall = cfg.allowSmall and (args.small == 'yes' or args.small == true) and true or false


         ------------------------ Process config data ----------------------------
         -- Add attributes, classes and styles.
        self.id = args.id
        self:addClass(cfg.usePlainlinksParam and yesno(args.plainlinks or true) and 'plainlinks')
        for _, class in ipairs(cfg.classes or {}) do
            self:addClass(class)
        end
        if self.isSmall then
            self:addClass(cfg.smallClass or 'mbox-small')
        end
        self:addClass(self.typeClass)
        self.style = args.style


         -- Type data.
         -- Set text style.
         local typeData = data.types[args.type]
         self.textstyle = args.textstyle
        local invalidType = args.type and not typeData and true or false
        typeData = typeData or data.types[data.default]


         -- Process data for collapsible text fields
         -- Process data for collapsible text fields. At the moment these are only used in {{ambox}}.
         local name, issue, talk, fix, date, info
         self.useCollapsibleTextFields = cfg.useCollapsibleTextFields
         if data.useCollapsibleTextFields then
         if self.useCollapsibleTextFields then
             name = args.name
             self.name = args.name
             local nameTitle = getTitleObject(name)
             local nameTitle = getTitleObject(name)
             local isTemplatePage = nameTitle and title.prefixedText == ('Template:' .. nameTitle.text) and true or false
             self.isTemplatePage = nameTitle and title.prefixedText == ('Template:' .. nameTitle.text) and true or false
     
            -- Get the self.issue value.
             local sect = args.sect
             local sect = args.sect
             if presentButBlank(sect) then
             if presentButBlank(sect) then
                 sect = format('This %s ', data.sectionDefault or 'page')
                 sect = 'This ' .. (cfg.sectionDefault or 'page')
             elseif type(sect) == 'string' then
             elseif type(sect) == 'string' then
                 sect = 'This ' .. sect .. ' '
                 sect = 'This ' .. sect
            else
                sect = nil
             end
             end
             issue = (sect or '') .. (args.issue or '') .. ' ' .. (args.text or '')
             local issue = args.issue
             talk = args.talk
            issue = type(issue) == 'string' and issue or nil
             if presentButBlank(talk) and isTemplatePage then
            local text = args.text
            text = type(text) == 'string' and text or nil
            local issues = {}
            tinsert(issues, sect)
            tinsert(issues, issue)
            tinsert(issues, text)
            self.issue = tconcat(issues, ' ')
     
            -- Get the self.talk value.
             local talk = args.talk
             if presentButBlank(talk) and self.isTemplatePage then
                 talk = '#'
                 talk = '#'
             end
             end
             fix = args.fix
             if talk then
            date = args.date
                -- See if the talk link exists and is for a talk or a content namespace.
            if presentButBlank(date) and isTemplatePage then
                local talkTitle = getTitleObject(talk)
                date = lang:formatDate('F Y')
                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
                    self.talk = talkText
                end
             end
             end
            info = args.info
        end


        -- Process the talk link, if present.
            -- Get other values.
        if talk then
            self.fix = args.fix
             -- See if the talk link exists and is for a talk or a content namespace.
             local date = args.date
             local talkTitle = type(talk) == 'string' and getTitleObject(talk)
             self.date = date and format(" <small>''(%s)''</small>", date)
             if not talkTitle or not talkTitle.isTalkPage then
             if presentButBlank(self.date) and self.isTemplatePage then
                -- If we couldn't process the talk page link, get the talk page of the current page.
                 self.date = lang:formatDate('F Y')
                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
            self.info = args.info
         end
         end


         -- Find whether we are using a small message box and process our data accordingly.
         -- Set the non-collapsible text field. At the moment this is used by all box types other than ambox,
         local isSmall = data.allowSmall and (args.small == 'yes' or args.small == true) and true or false
         -- and also by ambox when small=yes.
        local smallClass, image, imageRight, text, imageSize
         if self.isSmall then
         if isSmall then
             if self.useCollapsibleTextFields then
            smallClass = data.smallClass or 'mbox-small'
                 self.text = args.smalltext or self.issue
            image = args.smallimage or args.image
            imageRight = args.smallimageright or args.imageright
             if data.useCollapsibleTextFields then
                 text = args.smalltext or issue
             else
             else
                 text = args.smalltext or args.text
                 self.text = args.smalltext or args.text
             end
             end
            imageSize = data.imageSmallSize or '30x30px'
         else
         else
             image = args.image
             self.text = args.text
            imageRight = args.imageright
        end
             imageSize = '40x40px'
     
            text = args.text
        -- Set the below row.
        self.below = cfg.below and args.below
     
        -- General image settings.
        self.imageCellDiv = not self.isSmall and cfg.imageCellDiv and true or false
        self.imageEmptyCell = cfg.imageEmptyCell
        if cfg.imageEmptyCellStyle then
             self.imageEmptyCellStyle = 'border:none;padding:0px;width:1px'
         end
         end


         -- Process mainspace categories.
         -- Left image settings.
         local mainCats = {}
         local imageCheckBlank = cfg.imageCheckBlank
         local origCategoryNums -- origCategoryNums might be used in computing the template error category.
         local imageLeft = self.isSmall and args.smallimage or args.image
         if data.allowMainspaceCategories then
        if imageLeft ~= 'none' and not imageCheckBlank or imageLeft ~= 'none' and imageCheckBlank and image ~= 'blank' then
             -- Categories for the main namespace.
            self.imageLeft = imageLeft
            if not imageLeft then
                local imageSize = self.isSmall and (cfg.imageSmallSize or '30x30px') or '40x40px'
                self.imageLeft = format('[[File:%s|%s|link=|alt=]]', self.typeImage or 'Imbox notice.png', imageSize)
            end
        end
     
        -- Right image settings.
        local imageRight = self.isSmall and args.smallimageright or args.imageright
         if not (cfg.imageRightNone and imageRight == 'none') then
             self.imageRight = imageRight
        end
     
        -- Add mainspace categories. At the moment these are only used in {{ambox}}.
        if cfg.allowMainspaceCategories then
             if args.cat then
             if args.cat then
                 args.cat1 = args.cat
                 args.cat1 = args.cat
             end
             end
             local origCatNums = getArgNums(args, 'cat')
             self.catNums = getArgNums(args, 'cat')
             if args.category then
             if args.category then
                 args.category1 = args.category
                 args.category1 = args.category
             end
             end
             local origCategoryNums = getArgNums(args, 'category')
             self.categoryNums = getArgNums(args, 'category')
             local catNums = union(origCatNums, origCategoryNums)
             self.categoryParamNums = union(self.catNums, self.categoryNums)
             for _, num in ipairs(catNums) do
            -- The following is roughly equivalent to the old {{Ambox/category}}.
                 local cat = args['cat' .. tostring(num)] or args['category' .. tostring(num)]
            local date = args.date
                 local all = args['all' .. tostring(num)]
            date = type(date) == 'string' and date
                 tinsert(mainCats, formatCategory(cat, args.date, all))
            local preposition = 'from'
             for _, num in ipairs(self.categoryParamNums) do
                 local mainCat = args['cat' .. tostring(num)] or args['category' .. tostring(num)]
                 local allCat = args['all' .. tostring(num)]
                 mainCat = type(mainCat) == 'string' and mainCat
                allCat = type(allCat) == 'string' and allCat
                if mainCat and date then
                    local catTitle = format('%s %s %s', mainCat, preposition, date)
                    self:addCat('main', catTitle)
                    catTitle = getTitleObject('Category:' .. catTitle)
                    if not catTitle or not catTitle.exists then
                        self:addCat('main', 'Articles with invalid date parameter in template')
                    end
                elseif mainCat and not date then
                    self:addCat('main', mainCat)
                end
                if allCat then
                    self:addCat('main', allCat)
                end
             end
             end
         end
         end


         -- Process template namespace categories
         -- Add template-namespace categories.
         local isTemplatePage = type(name) == 'string' and title.prefixedText == ('Template:' .. name)
         self.isTemplatePage = type(self.name) == 'string' and title.prefixedText == ('Template:' .. self.name)
        local templateCats = {}
         if cfg.templateCategory then
         if data.templateCategory then
             if self.name then
             if name then
                 if self.isTemplatePage then
                 if isTemplatePage then
                     self:addCat('template', cfg.templateCategory)
                     tinsert(templateCats, format('[[Category:%s]]', data.templateCategory))
                 end
                 end
             elseif not title.isSubpage then
             elseif not self.title.isSubpage then
                 tinsert(templateCats, format('[[Category:%s]]', data.templateCategory))
                 self:addCat('template', cfg.templateCategory)
             end
             end
         end
         end
          
          
         -- Add an error category for the template namespace if appropriate.
         -- Add template error category.
         if data.templateErrorCategory then
         if cfg.templateErrorCategory then
             local catName = data.templateErrorCategory
             local templateErrorCategory = cfg.templateErrorCategory
             local templateCat
             local templateCat, templateSort
             if not name and not title.isSubpage then
             if not self.name and not self.title.isSubpage then
                 templateCat = format('[[Category:%s]]', catName)
                 templateCat = templateErrorCategory
             elseif type(name) == 'string' and title.prefixedText == ('Template:' .. name) then
             elseif type(self.name) == 'string' and title.prefixedText == ('Template:' .. name) then
                 local paramsToCheck = data.templateErrorParamsToCheck or {}
                 local paramsToCheck = cfg.templateErrorParamsToCheck or {}
                 local count = 0
                 local count = 0
                 for i, param in ipairs(paramsToCheck) do
                 for i, param in ipairs(paramsToCheck) do
    Line 281: Line 362:
                 end
                 end
                 if count > 0 then
                 if count > 0 then
                     templateCat = format('[[Category:%s|%d]]', catName, count)
                     templateCat = templateErrorCategory
                    templateSort = tostring(count)
                 end
                 end
                 if origCategoryNums and #origCategoryNums > 0 then
                 if self.categoryNums and #self.categoryNums > 0 then
                     templateCat = format('[[Category:%s|C]]', catName)
                     templateCat = templateErrorCategory
                    templateSort = 'C'
                 end
                 end
             end
             end
             tinsert(templateCats, templatecat)
             self:addCat('template', templateCat, templateSort)
         end
         end


         -- Categories for all namespaces.
         -- Categories for all namespaces.
        local allCats = {}
         if self.invalidType then
         if invalidType then
             local allSort = (nsid == 0 and 'Main:' or '') .. title.prefixedText
             local catsort = (nsid == 0 and 'Main:' or '') .. title.prefixedText
             self:addCat('all', 'Wikipedia message box parameter needs fixing', allSort)
             tinsert(allCats, format('[[Category:Wikipedia message box parameter needs fixing|%s]]', catsort))
        end
        if self.isSubstituted then
            self:addCat('all', 'Pages with incorrectly substituted templates')
         end
         end


         ------------------------ Build the box ----------------------------
         -- Convert category tables to strings and pass them through [[Module:Category handler]].
          
        self.categories = categoryHandler{
            main = tconcat(self.mainCats or {}),
            template = tconcat(self.templateCats or {}),
            all = tconcat(self.allCats or {}),
            nocat = args.nocat,
            demospace = self.demospace and args.demospace or nil,
            page = self.pageTitle and pageTitle.prefixedText or nil
         }
    end
     
    function box:export()
         local root = htmlBuilder.create()
         local root = htmlBuilder.create()


         -- Do the subst check.
         -- Add the subst check error.
         if data.substCheck and args.subst == 'SUBST' then
         if self.isSubstituted and self.name then
             if type(name) == 'string' then
             root
                 root
                 .tag('b')
                    .tag('b')
                    .addClass('error')
                        .addClass('error')
                    .wikitext(format(
                        .wikitext(format(
                        'Template <code>%s%s%s</code> has been incorrectly substituted.',
                            'Template <code>%s%s%s</code> has been incorrectly substituted.',
                        mw.text.nowiki('{{'), self.name, mw.text.nowiki('}}')
                            mw.text.nowiki('{{'), name, mw.text.nowiki('}}')
                    ))
                        ))
            end
            tinsert(allCats, '[[Category:Pages with incorrectly substituted templates]]')
         end
         end


         -- Create the box table.
         -- Create the box table.
         local box = root.tag('table')
         local boxTable = root.tag('table')
         box
         boxTable
             .attr('id', args.id)
             .attr('id', self.id)
         for i, class in ipairs(data.classes) do
         for i, class in ipairs(self.classes or {}) do
             box
             boxTable
                 .addClass(class)
                 .addClass(class)
         end
         end
         box
         boxTable
            .addClass(isSmall and smallClass)
             .cssText(self.style)
            .addClass(data.classPlainlinksYesno and yesno(args.plainlinks or true) and 'plainlinks')
            .addClass(typeData.class)
            .addClass(args.class)
             .cssText(args.style)
             .attr('role', 'presentation')
             .attr('role', 'presentation')


         -- Add the left-hand image.
         -- Add the left-hand image.
         local row = box.tag('tr')
         local row = boxTable.tag('tr')
         local imageCheckBlank = data.imageCheckBlank
         if self.imageLeft then
        if image ~= 'none' and not imageCheckBlank or image ~= 'none' and imageCheckBlank and image ~= 'blank' then
             local imageLeftCell = row.tag('td').addClass('mbox-image')
             local imageLeftCell = row.tag('td').addClass('mbox-image')
             if not isSmall and data.imageCellDiv then
             if self.imageCellDiv then
                 imageLeftCell = imageLeftCell.tag('div').css('width', '52px') -- If we are using a div, redefine imageLeftCell so that the image is inside it.
                -- If we are using a div, redefine imageLeftCell so that the image is inside it.
                -- Not sure why only some box types use divs, but it probably has something to do
                -- with that style="width: 52px;". @TODO: find out exactly what this does and fix this comment.
                 imageLeftCell = imageLeftCell.tag('div').css('width', '52px')  
             end
             end
             imageLeftCell
             imageLeftCell
                 .wikitext(image or format('[[File:%s|%s|link=|alt=]]', typeData.image, imageSize))
                 .wikitext(self.imageLeft)
         elseif data.imageEmptyCell then
         elseif self.imageEmptyCell then
            -- Some message boxes define an empty cell if no image is specified, and some don't.
            -- The old template code in templates where empty cells are specified gives the following hint:
            -- "No image. Cell with some width or padding necessary for text cell to have 100% width."
             row.tag('td')
             row.tag('td')
                 .addClass('mbox-empty-cell') -- No image. Cell with some width or padding necessary for text cell to have 100% width.
                 .addClass('mbox-empty-cell')  
                 .cssText(data.imageEmptyCellStyle and 'border:none;padding:0px;width:1px')
                 .cssText(self.imageEmptyCellStyle)
         end
         end


         -- Add the text.
         -- Add the text.
         local textCell = row.tag('td').addClass('mbox-text')
         local textCell = row.tag('td').addClass('mbox-text')
         if data.useCollapsibleTextFields then
         if self.useCollapsibleTextFields then
            -- The message box uses advanced text parameters that allow things to be collapsible. At the
            -- moment, only ambox uses this.
             textCell
             textCell
                 .cssText(args.textstyle)
                 .cssText(self.textstyle)
             local textCellSpan = textCell.tag('span')
             local textCellSpan = textCell.tag('span')
             textCellSpan
             textCellSpan
                 .addClass('mbox-text-span')
                 .addClass('mbox-text-span')
                 .wikitext(issue)
                 .wikitext(self.issue)
             if not isSmall then
             if not isSmall then
                 textCellSpan
                 textCellSpan
                     .tag('span')
                     .tag('span')
                         .addClass('hide-when-compact')
                         .addClass('hide-when-compact')
                         .wikitext(talk)
                         .wikitext(self.talk and ' ' .. self.talk)
                         .wikitext(' ')
                         .wikitext(self.fix and ' ' .. self.fix)
                        .wikitext(fix)
                        .done()
             end
             end
             textCellSpan
             textCellSpan
                 .wikitext(date and format(" <small>''(%s)''</small>", date))
                 .wikitext(self.date and ' ' .. self.date)
             if not isSmall then
             if not isSmall then
                 textCellSpan
                 textCellSpan
                     .tag('span')
                     .tag('span')
                         .addClass('hide-when-compact')
                         .addClass('hide-when-compact')
                         .wikitext(info and ' ' .. info)
                         .wikitext(self.info and ' ' .. self.info)
             end
             end
         else
         else
            -- Default text formatting - anything goes.
             textCell
             textCell
                 .cssText(args.textstyle)
                 .cssText(self.textstyle)
                 .wikitext(text)
                 .wikitext(self.text)
         end
         end


         -- Add the right-hand image.
         -- Add the right-hand image.
         if imageRight and not (data.imageRightNone and imageRight == 'none') then
         if self.imageRight then
             local imageRightCell = row.tag('td').addClass('mbox-imageright')
             local imageRightCell = row.tag('td').addClass('mbox-imageright')
             if not isSmall and data.imageCellDiv then
             if not self.imageCellDiv then
                 imageRightCell = imageRightCell.tag('div').css('width', '52px') -- If we are using a div, redefine imageRightCell so that the image is inside it.
                 imageRightCell = imageRightCell.tag('div').css('width', '52px') -- If we are using a div, redefine imageRightCell so that the image is inside it.
             end
             end
             imageRightCell
             imageRightCell
                 .wikitext(imageRight)
                 .wikitext(self.imageRight)
         end
         end


         -- Add the below row.
         -- Add the below row.
         if data.below and args.below then
         if self.below then
             box.tag('tr')
             boxTable.tag('tr')
                 .tag('td')
                 .tag('td')
                     .attr('colspan', args.imageright and '3' or '2')
                     .attr('colspan', self.imageRight and '3' or '2')
                     .addClass('mbox-text')
                     .addClass('mbox-text')
                     .cssText(args.textstyle)
                     .cssText(self.textstyle)
                     .wikitext(args.below)
                     .wikitext(self.below)
         end
         end
        ------------------------ Error messages and categories ----------------------------


         -- Add error message for invalid type parameters.
         -- Add error message for invalid type parameters.
         if invalidType then
         if self.invalidType then
             root
             root
                 .tag('div')
                 .tag('div')
                     .addClass('error')
                     .addClass('error')
                     .css('text-align', 'center')
                     .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 ''))
                     .wikitext(format('This message box is using an invalid type parameter (<code>type=%s</code>) and needs fixing.', self.type or ''))
         end
         end


         -- Add categories using categoryHandler.
         -- Add categories.
         root
         root
             .wikitext(categoryHandler{
             .wikitext(self.categories)
                main = tconcat(mainCats),
     
                template = tconcat(templateCats),
                all = tconcat(allCats),
                nocat = args.nocat,
                demospace = demospace and args.demospace or nil,
                page = pageTitle and pageTitle.prefixedText or nil
            })
       
         return tostring(root)
         return tostring(root)
    end
    local function makeBox(boxType, args)
        box:setTitle(args)
        local cfg = box:getConfig(boxType)
        args = box:removeBlankArgs(cfg, args)
        box:setBoxParameters(cfg, args)
        return box:export()
    end
    end


    Line 430: Line 523:
             -- assume args are being passed directly in from the debug console
             -- assume args are being passed directly in from the debug console
             -- or from another Lua module.
             -- or from another Lua module.
             local origArgs
             local args
             if frame == mw.getCurrentFrame() then
             if frame == mw.getCurrentFrame() then
                 origArgs = frame:getParent().args
                 args = frame:getParent().args
                 for k, v in pairs(frame.args) do
                 for k, v in pairs(frame.args) do
                     origArgs = frame.args
                     args = frame.args
                     break
                     break
                 end
                 end
             else
             else
                 origArgs = frame
                 args = frame
             end
             end
            -- Trim whitespace and remove blank arguments.
             return makeBox(boxType, args)
            local args = {}
            for k, v in pairs(origArgs) do
                if type(v) == 'string' then
                    v = mw.text.trim(v)
                end
                args[k] = v
            end
             return p.build(boxType, args)
         end
         end
    end
    end


    p.mbox = makeWrapper('mbox')
    return {
    p.ambox = makeWrapper('ambox')
        box = box,
    p.cmbox = makeWrapper('cmbox')
        makeBox = makeBox,
    p.fmbox = makeWrapper('fmbox')
        mbox = makeWrapper('mbox'),
    p.imbox = makeWrapper('imbox')
        ambox = makeWrapper('ambox'),
    p.ombox = makeWrapper('ombox')
        cmbox = makeWrapper('cmbox'),
    p.tmbox = makeWrapper('tmbox')
        fmbox = makeWrapper('fmbox'),
     
        imbox = makeWrapper('imbox'),
    return p
        ombox = makeWrapper('ombox'),
        tmbox = makeWrapper('tmbox')
    }

    Revision as of 07:15, 28 September 2013

    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 box = {}
    
    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 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
    
    function box.getNamespaceId(ns)
        if not ns then return end
        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
    
    function box.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 box:addCat(ns, cat, sort)
        if type(cat) ~= 'string' then return end
        local nsVals = {'main', 'template', 'all'}
        local tname
        for i, val in ipairs(nsVals) do
            if ns == val then
                tname = ns .. 'Cats'
            end
        end
        if not tname then
            for i, val in ipairs(nsVals) do
                nsVals[i] = format('"%s"', val)
            end
            error('invalid ns parameter passed to box:addCat; valid values are ' .. mw.text.listToText(nsVals, nil, ' or '))
        end
        self[tname] = self[tname] or {}
        if type(sort) == 'string' then
            tinsert(self[tname], format('[[Category:%s|%s]]', cat, sort))
        else
            tinsert(self[tname], format('[[Category:%s]]', cat))
        end
    end
    
    function box:addClass(class)
        if type(class) ~= 'string' then return end
        self.classes = self.classes or {}
        tinsert(self.classes, class)
    end
    
    function box:setTitle(args)
        -- Get the title object and the namespace.
        local pageTitle = getTitleObject(args.page ~= '' and args.page)
        self.title = pageTitle or mw.title.getCurrentTitle()
        local demospace = box.getNamespaceId(args.demospace ~= '' and args.demospace)
        self.nsid = demospace or self.title.namespace
    end
    
    function box:getConfig(boxType)
        -- Get the box config data from the data page.
        if boxType == 'mbox' then
            boxType = box.getMboxType(self.nsid)
        end
        local cfgTables = mw.loadData('Module:Message box/configuration')
        local cfg = cfgTables[boxType]
        if not cfg 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
        return cfg
    end
        
    function box:removeBlankArgs(cfg, args)
        -- Only allow blank arguments for the parameter names listed in cfg.allowBlankParams.
        local newArgs = {}
        for k, v in pairs(args) do
            for i, param in ipairs(cfg.allowBlankParams or {}) do
                if v ~= '' or k == param then
                    newArgs[k] = v
                end
            end
        end
        return newArgs
    end
    
    function box:setBoxParameters(cfg, args)
        -- Get type data.
        self.type = args.type
        local typeData = cfg.types[self.type]
        self.invalidType = self.type and not typeData and true or false
        typeData = typeData or cfg.types[cfg.default]
        self.typeClass = typeData.class
        self.typeImage = typeData.image
    
        -- Find if the box has been wrongly substituted.
        if cfg.substCheck and args.subst == 'SUBST' then
            self.isSubstituted = true
        end
    
        -- Find whether we are using a small message box.
        self.isSmall = cfg.allowSmall and (args.small == 'yes' or args.small == true) and true or false
    
        -- Add attributes, classes and styles.
        self.id = args.id
        self:addClass(cfg.usePlainlinksParam and yesno(args.plainlinks or true) and 'plainlinks')
        for _, class in ipairs(cfg.classes or {}) do
            self:addClass(class)
        end
        if self.isSmall then
            self:addClass(cfg.smallClass or 'mbox-small')
        end
        self:addClass(self.typeClass)
        self.style = args.style
    
        -- Set text style.
        self.textstyle = args.textstyle
    
        -- Process data for collapsible text fields. At the moment these are only used in {{ambox}}.
        self.useCollapsibleTextFields = cfg.useCollapsibleTextFields 
        if self.useCollapsibleTextFields then
            self.name = args.name
            local nameTitle = getTitleObject(name)
            self.isTemplatePage = nameTitle and title.prefixedText == ('Template:' .. nameTitle.text) and true or false
    
            -- Get the self.issue value.
            local sect = args.sect
            if presentButBlank(sect) then
                sect = 'This ' .. (cfg.sectionDefault or 'page')
            elseif type(sect) == 'string' then
                sect = 'This ' .. sect
            else
                sect = nil
            end
            local issue = args.issue
            issue = type(issue) == 'string' and issue or nil
            local text = args.text
            text = type(text) == 'string' and text or nil
            local issues = {}
            tinsert(issues, sect)
            tinsert(issues, issue)
            tinsert(issues, text)
            self.issue = tconcat(issues, ' ')
    
            -- Get the self.talk value.
            local talk = args.talk
            if presentButBlank(talk) and self.isTemplatePage then
                talk = '#'
            end
            if talk then
                -- See if the talk link exists and is for a talk or a content namespace.
                local talkTitle = 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
                    self.talk = talkText
                end
            end
    
            -- Get other values.
            self.fix = args.fix
            local date = args.date
            self.date = date and format(" <small>''(%s)''</small>", date)
            if presentButBlank(self.date) and self.isTemplatePage then
                self.date = lang:formatDate('F Y')
            end
            self.info = args.info
        end
    
        -- Set the non-collapsible text field. At the moment this is used by all box types other than ambox,
        -- and also by ambox when small=yes.
        if self.isSmall then
            if self.useCollapsibleTextFields then
                self.text = args.smalltext or self.issue
            else
                self.text = args.smalltext or args.text
            end
        else
            self.text = args.text
        end
    
        -- Set the below row.
        self.below = cfg.below and args.below
    
        -- General image settings.
        self.imageCellDiv = not self.isSmall and cfg.imageCellDiv and true or false
        self.imageEmptyCell = cfg.imageEmptyCell
        if cfg.imageEmptyCellStyle then
            self.imageEmptyCellStyle = 'border:none;padding:0px;width:1px'
        end
    
        -- Left image settings.
        local imageCheckBlank = cfg.imageCheckBlank
        local imageLeft = self.isSmall and args.smallimage or args.image
        if imageLeft ~= 'none' and not imageCheckBlank or imageLeft ~= 'none' and imageCheckBlank and image ~= 'blank' then
            self.imageLeft = imageLeft
            if not imageLeft then
                local imageSize = self.isSmall and (cfg.imageSmallSize or '30x30px') or '40x40px'
                self.imageLeft = format('[[File:%s|%s|link=|alt=]]', self.typeImage or 'Imbox notice.png', imageSize)
            end
        end
    
        -- Right image settings.
        local imageRight = self.isSmall and args.smallimageright or args.imageright
        if not (cfg.imageRightNone and imageRight == 'none') then
            self.imageRight = imageRight
        end
    
        -- Add mainspace categories. At the moment these are only used in {{ambox}}.
        if cfg.allowMainspaceCategories then
            if args.cat then
                args.cat1 = args.cat
            end
            self.catNums = getArgNums(args, 'cat')
            if args.category then
                args.category1 = args.category
            end
            self.categoryNums = getArgNums(args, 'category')
            self.categoryParamNums = union(self.catNums, self.categoryNums)
            -- The following is roughly equivalent to the old {{Ambox/category}}.
            local date = args.date
            date = type(date) == 'string' and date
            local preposition = 'from'
            for _, num in ipairs(self.categoryParamNums) do
                local mainCat = args['cat' .. tostring(num)] or args['category' .. tostring(num)]
                local allCat = args['all' .. tostring(num)]
                mainCat = type(mainCat) == 'string' and mainCat
                allCat = type(allCat) == 'string' and allCat
                if mainCat and date then
                    local catTitle = format('%s %s %s', mainCat, preposition, date)
                    self:addCat('main', catTitle)
                    catTitle = getTitleObject('Category:' .. catTitle)
                    if not catTitle or not catTitle.exists then
                        self:addCat('main', 'Articles with invalid date parameter in template')
                    end
                elseif mainCat and not date then
                    self:addCat('main', mainCat)
                end
                if allCat then
                    self:addCat('main', allCat)
                end
            end
        end
    
        -- Add template-namespace categories.
        self.isTemplatePage = type(self.name) == 'string' and title.prefixedText == ('Template:' .. self.name)
        if cfg.templateCategory then
            if self.name then
                if self.isTemplatePage then
                    self:addCat('template', cfg.templateCategory)
                end
            elseif not self.title.isSubpage then
                self:addCat('template', cfg.templateCategory)
            end
        end
        
        -- Add template error category.
        if cfg.templateErrorCategory then
            local templateErrorCategory = cfg.templateErrorCategory
            local templateCat, templateSort
            if not self.name and not self.title.isSubpage then
                templateCat = templateErrorCategory
            elseif type(self.name) == 'string' and title.prefixedText == ('Template:' .. name) then
                local paramsToCheck = cfg.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 = templateErrorCategory
                    templateSort = tostring(count)
                end
                if self.categoryNums and #self.categoryNums > 0 then
                    templateCat = templateErrorCategory
                    templateSort = 'C'
                end
            end
            self:addCat('template', templateCat, templateSort)
        end
    
        -- Categories for all namespaces.
        if self.invalidType then
            local allSort = (nsid == 0 and 'Main:' or '') .. title.prefixedText
            self:addCat('all', 'Wikipedia message box parameter needs fixing', allSort)
        end
        if self.isSubstituted then
            self:addCat('all', 'Pages with incorrectly substituted templates')
        end
    
        -- Convert category tables to strings and pass them through [[Module:Category handler]].
        self.categories = categoryHandler{
            main = tconcat(self.mainCats or {}),
            template = tconcat(self.templateCats or {}),
            all = tconcat(self.allCats or {}),
            nocat = args.nocat,
            demospace = self.demospace and args.demospace or nil,
            page = self.pageTitle and pageTitle.prefixedText or nil
        }
    end
    
    function box:export()
        local root = htmlBuilder.create()
    
        -- Add the subst check error.
        if self.isSubstituted and self.name then
            root
                .tag('b')
                    .addClass('error')
                    .wikitext(format(
                        'Template <code>%s%s%s</code> has been incorrectly substituted.',
                        mw.text.nowiki('{{'), self.name, mw.text.nowiki('}}')
                    ))
        end
    
        -- Create the box table.
        local boxTable = root.tag('table')
        boxTable
            .attr('id', self.id)
        for i, class in ipairs(self.classes or {}) do
            boxTable
                .addClass(class)
        end
        boxTable
            .cssText(self.style)
            .attr('role', 'presentation')
    
        -- Add the left-hand image.
        local row = boxTable.tag('tr')
        if self.imageLeft then
            local imageLeftCell = row.tag('td').addClass('mbox-image')
            if self.imageCellDiv then
                -- If we are using a div, redefine imageLeftCell so that the image is inside it.
                -- Not sure why only some box types use divs, but it probably has something to do
                -- with that style="width: 52px;". @TODO: find out exactly what this does and fix this comment.
                imageLeftCell = imageLeftCell.tag('div').css('width', '52px') 
            end
            imageLeftCell
                .wikitext(self.imageLeft)
        elseif self.imageEmptyCell then
            -- Some message boxes define an empty cell if no image is specified, and some don't.
            -- The old template code in templates where empty cells are specified gives the following hint:
            -- "No image. Cell with some width or padding necessary for text cell to have 100% width."
            row.tag('td')
                .addClass('mbox-empty-cell') 
                .cssText(self.imageEmptyCellStyle)
        end
    
        -- Add the text.
        local textCell = row.tag('td').addClass('mbox-text')
        if self.useCollapsibleTextFields then
            -- The message box uses advanced text parameters that allow things to be collapsible. At the
            -- moment, only ambox uses this.
            textCell
                .cssText(self.textstyle)
            local textCellSpan = textCell.tag('span')
            textCellSpan
                .addClass('mbox-text-span')
                .wikitext(self.issue)
            if not isSmall then
                textCellSpan
                    .tag('span')
                        .addClass('hide-when-compact')
                        .wikitext(self.talk and ' ' .. self.talk)
                        .wikitext(self.fix and ' ' .. self.fix)
            end
            textCellSpan
                .wikitext(self.date and ' ' .. self.date)
            if not isSmall then
                textCellSpan
                    .tag('span')
                        .addClass('hide-when-compact')
                        .wikitext(self.info and ' ' .. self.info)
            end
        else
            -- Default text formatting - anything goes.
            textCell
                .cssText(self.textstyle)
                .wikitext(self.text)
        end
    
        -- Add the right-hand image.
        if self.imageRight then
            local imageRightCell = row.tag('td').addClass('mbox-imageright')
            if not self.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(self.imageRight)
        end
    
        -- Add the below row.
        if self.below then
            boxTable.tag('tr')
                .tag('td')
                    .attr('colspan', self.imageRight and '3' or '2')
                    .addClass('mbox-text')
                    .cssText(self.textstyle)
                    .wikitext(self.below)
        end
    
        -- Add error message for invalid type parameters.
        if self.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.', self.type or ''))
        end
    
        -- Add categories.
        root
            .wikitext(self.categories)
    
        return tostring(root)
    end
    
    local function makeBox(boxType, args)
        box:setTitle(args)
        local cfg = box:getConfig(boxType)
        args = box:removeBlankArgs(cfg, args)
        box:setBoxParameters(cfg, args)
        return box:export()
    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 args
            if frame == mw.getCurrentFrame() then
                args = frame:getParent().args
                for k, v in pairs(frame.args) do
                    args = frame.args
                    break
                end
            else
                args = frame
            end
            return makeBox(boxType, args)
        end
    end
    
    return {
        box = box,
        makeBox = makeBox,
        mbox = makeWrapper('mbox'),
        ambox = makeWrapper('ambox'),
        cmbox = makeWrapper('cmbox'),
        fmbox = makeWrapper('fmbox'),
        imbox = makeWrapper('imbox'),
        ombox = makeWrapper('ombox'),
        tmbox = makeWrapper('tmbox')
    }