en>Mr. Stradivarius |
|
Line 1: |
Line 1: |
| -- This is a meta-module for producing message box templates, including {{mbox}}, {{ambox}}, {{imbox}}, {{tmbox}}, {{ombox}}, {{cmbox}} and {{fmbox}}.
| | {{#ifeq:<!--test first character for lower-case letter-->{{#invoke:string|find|1={{{1|}}}|2=^%l|plain=false}}|1 |
| | | |<!-- first character is a lower case letter; test against whitelist |
| -- Require necessary modules.
| | -->{{#switch: {{First word|{{{1|}}}}}<!--begin whitelist--> |
| local htmlBuilder = require('Module:HtmlBuilder')
| | |c. <!--for circa--> |
| local categoryHandler = require('Module:Category handler').main
| | |gTLD |
| local yesno = require('Module:Yesno')
| | |iMac |
| | | |iOS |
| -- Load the configuration page. | | |iOS, |
| local cfgTables = mw.loadData('Module:Message box/configuration')
| | |iPad |
| | | |iPhone |
| -- Get a language object for formatDate and ucfirst.
| | |iTunes |
| local lang = mw.language.getContentLanguage()
| | |macOS |
| | | |none |
| -- Set aliases for often-used functions to reduce table lookups. | | |pH |
| local format = mw.ustring.format
| | |pH-dependent=<!-- end whitelist; short description starts with an allowed lower-case string; whitelist matched; do nothing --> |
| local tinsert = table.insert
| | |#default=<!-- apply category to track lower-case short descriptions -->{{main other|[[Category:Pages with lower-case short description|{{trim|{{{1|}}}}}]]}}{{Testcases other|{{red|CATEGORY APPLIED}}}}<!-- end whitelist test -->}} |
| local tconcat = table.concat
| | |<!-- short description does not start with lower-case letter; do nothing; end lower-case test --> |
| local trim = mw.text.trim
| | }}<noinclude> |
| | | {{documentation}} |
| local box = {}
| | </noinclude> |
| | |
| 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 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.
| |
| self.pageTitle = getTitleObject(args.page ~= '' and args.page)
| |
| self.title = self.pageTitle or mw.title.getCurrentTitle()
| |
| self.demospace = args.demospace ~= '' and args.demospace or nil
| |
| self.nsid = box.getNamespaceId(self.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 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
| |
| if v ~= '' then
| |
| newArgs[k] = v
| |
| end
| |
| end
| |
| for i, param in ipairs(cfg.allowBlankParams or {}) do
| |
| newArgs[param] = args[param]
| |
| end
| |
| return newArgs
| |
| end
| |
| | |
| function box:setBoxParameters(cfg, args)
| |
| -- Get type data.
| |
| self.type = args.type
| |
| local typeData = cfg.types[self.type]
| |
| self.invalidTypeError = cfg.showInvalidTypeError and 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.
| |
| if cfg.allowSmall and (
| |
| cfg.smallParam and args.small == cfg.smallParam
| |
| or not cfg.smallParam and yesno(args.small)
| |
| )
| |
| then
| |
| self.isSmall = true
| |
| else
| |
| self.isSmall = false
| |
| end
| |
| | |
| -- Add attributes, classes and styles.
| |
| if cfg.useId then
| |
| self.id = args.id
| |
| end
| |
| 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:addClass(args.class)
| |
| 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(self.name)
| |
| self.isTemplatePage = nameTitle and self.title.prefixedText == ('Template:' .. nameTitle.text) and true or false
| |
| | |
| -- Get the self.issue value.
| |
| if self.isSmall and args.smalltext then
| |
| self.issue = args.smalltext
| |
| else
| |
| local sect
| |
| if args.sect == '' then
| |
| sect = 'This ' .. (cfg.sectionDefault or 'page')
| |
| elseif type(args.sect) == 'string' then
| |
| sect = 'This ' .. args.sect
| |
| 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, ' ')
| |
| end
| |
| | |
| -- Get the self.talk value.
| |
| local talk = args.talk
| |
| if 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(self.title.talkPageTitle, self.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
| |
| if args.date and args.date ~= '' then
| |
| date = args.date
| |
| elseif args.date == '' and self.isTemplatePage then
| |
| date = lang:formatDate('F Y')
| |
| end
| |
| if date then
| |
| self.date = format(" <small>''(%s)''</small>", date)
| |
| 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
| |
| self.text = args.smalltext or args.text
| |
| 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 imageLeft = self.isSmall and args.smallimage or args.image
| |
| if cfg.imageCheckBlank and imageLeft ~= 'blank' and imageLeft ~= 'none'
| |
| or not cfg.imageCheckBlank and imageLeft ~= 'none'
| |
| 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)
| |
| if args.all then
| |
| args.all1 = args.all
| |
| end
| |
| -- 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 self.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 self.title.prefixedText == ('Template:' .. self.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.invalidTypeError then
| |
| local allSort = (nsid == 0 and 'Main:' or '') .. self.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,
| |
| page = self.pageTitle and self.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[[Template:%s|%s]]%s</code> has been incorrectly substituted.',
| |
| mw.text.nowiki('{{'), self.name, 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.
| |
| -- Divs use style="width: 52px;", which limits the image width to 52px. If any
| |
| -- images in a div are wider than that, they may overlap with the text or cause
| |
| -- other display problems.
| |
| 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 self.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 self.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 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.invalidTypeError then
| |
| root
| |
| .tag('div')
| |
| .css('text-align', 'center')
| |
| .wikitext(format('This message box is using an invalid "type=%s" parameter 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 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.
| |
| local args = {}
| |
| for k, v in pairs(origArgs) do
| |
| if type(v) == 'string' then
| |
| v = trim(v)
| |
| end
| |
| args[k] = v
| |
| end
| |
| return makeBox(boxType, args)
| |
| end
| |
| end
| |
| | |
| local p = {
| |
| box = box,
| |
| makeBox = makeBox,
| |
| mbox = makeWrapper('mbox')
| |
| } | |
| | |
| for boxType in pairs(cfgTables) do
| |
| p[boxType] = makeWrapper(boxType)
| |
| end
| |
| | |
| return p
| |