| This is the module sandbox page for Module:Clickable button (diff). |
| This module is rated as ready for general use. It has reached a mature state, is considered relatively stable and bug-free, and may be used wherever appropriate. It can be mentioned on help pages and other Wikipedia resources as an option for new users. To minimise server load and avoid disruptive output, improvements should be developed through sandbox testing rather than repeated trial-and-error editing. |
| This Lua module is used in system messages, and on approximately 1,130,000 pages, or roughly 2% of all pages. Changes to it can cause immediate changes to the Wikipedia user interface. To avoid major disruption and server load, any changes should be tested in the module's /sandbox or /testcases subpages, or in your own module sandbox. The tested changes can be added to this page in a single edit. Please discuss changes on the talk page before implementing them. |
| This module depends on the following other modules: |
This module implements the {{Clickable button}} template.
Usage from wikitext
editTo use this module from wikitext, you should normally use the {{Clickable button}} template. However, it can also be used with the syntax {{#invoke:Clickable button|main|arguments}}. Please see the template page for a list of available parameters.
Usage from Lua modules
editTo use this module from other Lua modules, first load the module.
local mClickableButton = require('Module:Clickable button')
You can then generate a button using the .main function.
mClickableButton.main(args)
The args variable should be a table containing the arguments to pass to the module. To see the different arguments that can be specified and how they affect the module output, please refer to the {{Clickable button}} template documentation.
--- @module 'Clickable button'
--- Creates clickable Codex button.
---
--- Outputs wikitext to render the [button component](https://doc.wikimedia.org/codex/latest/components/demos/button.html)
--- from the [Codex design system for Wikimedia](https://doc.wikimedia.org/codex/latest).
--- Options to:
--- - include an icon or create an icon-only button
--- - target a URL or a wikilink
--- - set the weight, size and state
--- - create a dummy or disabled button
--- - add custom CSS classes and inline styles
--- - include ARIA attributes for accessibility.
---
--- Dummy buttons are disabled by default. Includes helper functions for URL parsing and cleaning,
--- and adding tracking categories. Intended for use in templates and other modules.
--- Supports legacy parameters. To add icons, see CSS file links in `C`.
---
--- @diagnostic disable: duplicate-doc-field
--- @class args: frame Template arguments.
--- @field label? string Visible text label.
--- @field link? string Target wikilink.
--- @field url? string Target external URL.
--- @field icon? string Name of the icon to display as found in CSS file after the class's icon prefix, i.e. `search` for `cdx-demo-css-icon--search`.
--- @field weight? string Visual weight of the button.
--- @field size? string Size of the button.
--- @field action? string Action type of the button.
--- @field disabled? boolean|string Whether the button is disabled/greyed out. `true` if: `link` = `'no'` or `false`, or `disabled` = `'1'` or `true`.
--- @field nocat? boolean|string If `true`, suppresses tracking categories. Additional category, if defined, will still be added.
--- @field category? string An additional category to add.
--- @field aria-label? string The ARIA label for accessibility DOM.
--- @field class? string Custom CSS classes for the button. Do _not_ nest in "".
--- @field style? string Custom inline CSS styles. Do _not_ nest in "".
--- @field arialabel? string (alias for aria-label)
--- @field aria_label? string (alias for aria-label)
--- @field text string (alias for label)
--- @field ['1']? string Positional argument 1 (alias for link, can be label too if label is not defined).
--- @field ['2']? string Positional argument 2 (alias for label).
--- @field color? string Legacy color parameter.
--- @field private categories? string|boolean Categories to add.
--- @field private ariaDisabled? boolean Internal flag indicating if the button should be functionally disabled to ARIA.
--- @field private oldClassMatched string|boolean Internal flag for outdated classes if used.
--- @field private isUrl boolean Whether the target is a URL.
--- @field private errorText string|nil Internal string used both as error indicator, and error message text.
--- @field private tblClasses table Classes for the button span tag.
--- @field private pageTitleObject mw.title Title object of the current page.
--- @field private linkTitleObject mw.title Title object of the target wikilink.
--- @field private frame frame The current frame.
--- @field private rawArgs table Arguments passed to the module before parsing.
--- @field private parsedArgs table Parsed arguments.
--- @field private iconSpan mw.html Icon span element for the button.
require( 'strict' )
local M = {}
-- If your wiki uses non-ASCII/UTF-8 characters in any input text, then replace use of "string.lower" with "mw.ustring.lower". NOTE: "mw.ustring.lower" may be _much_ slower but respects Unicode codepoints rather than just bytes.
local _lower = string.lower
local getArgs = require( 'Module:Arguments' ).getArgs
local checkForUnknowns = require( 'Module:Check for unknown parameters' )._check
local _gsub = mw.ustring.gsub
local _mw_lower = mw.ustring.lower -- Still loaded, as instances where Unicode support is required use it.
local _tonumber = tonumber
local _format = mw.ustring.format
local _type = type
local _table_insert = table.insert
--- Requires @wikimedia/codex (check [[Special:Version]]).
--- @todo Check not in User/Draft namespaces.
--- @todo Is checkForUnknowns checking validity of input?
--- @todo Check if being subst'd via {{subst:#invoke:}} by checking mw.isSubsting() then output template call not the subst, i.e. unsubst.
--- @todo Check verbose output with mw.dumpObject( type.object ).
--- @todo Check knownArgs.
local C = { --- 'Constants'
lowercaseArgs = { --- Arguments whose inputs are case-insensitive, and are converted to lowercase.
[ 'action' ] = true,
[ 'color' ] = true,
[ 'weight' ] = true,
[ 'size' ] = true,
[ 'icon' ] = true,
},
knownArgs = { --- Valid argument keys.
'class',
'color',
'weight',
'size',
'icon',
'link',
'action',
'url',
'disabled',
'label',
'aria-label',
'nocat',
'text',
'1',
'2',
'url',
'errorText',
'arialabel',
'aria_label',
checkpositional = 'y', --- Other options for unknown parameters check.
ignoreblank = 'y',
unknown = '[[Category:Pages using Module:Clickable button with unknown parameters|_VALUE_]]',
preview = '<span class="error" style="font-size:inherit;"><strong>Preview warning:</strong>' ..
'Using undocumented parameter(s): "_VALUE_".</span>',
},
wrapperTemplates = { --- Wrapper templates that only require reading from `parentFrame()`. Positional arguments using template parameters (e.g., `{{{var|}}}`) are ignored, as `currentFrame()` is not used. Improves performance by avoiding argument checks in both frames.
'Template:Clickable button', 'Template:Clickable button/sandbox',
'Template:Cdx-button', 'Template:Cdx-button/sandbox',
},
trackingCategories = { --- Tracking category pagenames with namespace.
dummyButton = 'Category:Pages using clickable dummy button',
disabledButton = 'Category:Pages using disabled button',
externalLinks = 'Category:Pages using clickable button with external links',
outdatedClasses = 'Category:Pages using clickable button with outdated classes',
errors = 'Category:Errors reported by Module:Clickable button',
unknownParams = 'Category:Pages using Module:Clickable button with unknown parameters',
},
unknownArgsPreviewText = '<span class="error"><strong>Preview warning:</strong>' .. --- Preview warning text for unknown arguments.
' Using undocumented parameter(s): "_VALUE_".</span>',
noAriaLabelWarningText = '<span class="error" style="font-size:inherit;">' .. --- No ARIA-label preview warning text.
'<strong>Preview warning:</strong> A button without a visible label needs an [' ..
'[WAI-ARIA|ARIA]] label, please define it using "aria-label".</span>',
labelLengthWarningText = '<span class="error" style="font-size:inherit;">' .. --- "Visible label is too long" preview warning text.
'<strong>Preview warning:</strong> A button label should ideally be shorter th' ..
'an 38 characters, see [[en:Template:Clickable button/doc#Button label length|' ..
'documentation]].</span>',
noArgsWarningText = '<span class="error" style="font-size:inherit;">' .. --- No arguments preview warning text.
'<strong>Preview warning:</strong> No parameters were passed to clickable button.</span>',
baseCSS = 'Template:Clickable button/styles.css', --- Base CSS file for button styles.
iconsCSS = 'Template:Clickable button/icons.css', --- CSS file for button icons.
buttonDefaults = { --- Default values for button options
weight = 'normal',
size = 'medium',
action = 'default',
},
cssClasses = { -- CSS class prefixes for button.
base = 'cdx-button',
disabled = 'cdx-button--fake-button--disabled',
wordWrap = 'cdx-button--word-wrap',
enabled = 'cdx-button--fake-button--enabled',
iconOnly = 'cdx-button--icon-only',
shortLabel = 'cdx-button--short-label',
icon = 'cdx-button__icon',
iconPrefix = 'cdx-demo-css-icon--',
sizePrefix = 'cdx-button--size-',
weightPrefix = 'cdx-button--weight-',
samePage = 'cdx-button--same-page',
actionPrefix = 'cdx-button--action-',
fakeButton = 'cdx-button--fake-button',
},
labelLimits = { maxLength = 38, minLength = 3 }, --- Label length limits.
excludedNamespaces = { 'User', 'Draft' }, --- Namespace exclusions for tracking categories.
legacyClassSets = {
progressive = { --- Aliases for CSS class: `.progressive`.
[ 'blue' ] = true,
[ 'green' ] = true,
[ 'ui-button-green' ] = true,
[ 'ui-button-blue' ] = true,
[ 'mw-ui-constructive' ] = true,
[ 'mw-ui-progressive' ] = true,
[ 'progressive' ] = true,
},
destructive = { --- Aliases for CSS class: `.destructive`.
[ 'red' ] = true,
[ 'ui-button-red' ] = true,
[ 'mw-ui-destructive' ] = true,
[ 'destructive' ] = true,
},
},
booleanMap = {
-- Explicit true values
[ 'yes' ] = true,
[ 'y' ] = true,
[ 'true' ] = true,
[ 't' ] = true,
[ 'on' ] = true,
[ '1' ] = true,
[ 'enable' ] = true,
[ 'enabled' ] = true,
-- Explicit false values
[ 'no' ] = false,
[ 'n' ] = false,
[ 'false' ] = false,
[ 'f' ] = false,
[ 'off' ] = false,
[ '0' ] = false,
[ 'disable' ] = false,
[ 'disabled' ] = false,
},
defaultResponse = nil,
}
--- Allows for consistent treatment of boolean-like wikitext input.
---
--- Uses lookup table for efficiency, unlike [[Module:Yesno]] which uses chained if-elseif statements.
--- - Returns `nil` if input is `nil`.
--- - Checks for boolean type and returns as-is.
--- - For strings, looks up a normalized (lowercased) value in a lookup table (`C.booleanMap`).
--- - If not found, attempts to convert to a number: returns `true` for `1`, `false` for `0`.
--- - If still unrecognized, returns `defaultResponse` (or a constant fallback; default: `nil`).
--- @param value any Value to evaluate as truthy or falsy.
--- @param defaultResponse? any Value to return if input is unrecognized, i.e. neither truthy/falsy. Defaults to nil.
--- @return any valueBoolean Boolean true if truthy, or false if falsy, or nil if nil. defaultResponse or nil if input is unrecognized.
function M.yesno( value, defaultResponse )
if value == nil then
return nil
end
local valueType = _type( value )
if valueType == 'boolean' then
return value
elseif valueType == 'string' then
local lookupResult = C.booleanMap[ _lower( value ) ] -- Unicode doesn't matter here.
if lookupResult ~= nil then
return lookupResult
end -- Not found in lookup table. Fallback to numeric check.
end
-- Numeric check works for both numbers and numeric strings.
-- Numeric 1 is truthy, and 0 is falsy.
local number = _tonumber( value ) or nil
if number == 1 then
return true
elseif number == 0 then
return false
end -- Not 1 or 0, fallback to defaultResponse.
if not defaultResponse then
defaultResponse = C.defaultResponse
end
return defaultResponse
end
--- Parse a wikilink and return its component parts.
---
--- @class linkData, table
--- @field pageName string? The pagename part, with namespace if present
--- @field sectionHeading string? The section heading after `#`
--- @field displayText string? Display text after pipe `|`
--- @field isSectionLink boolean Whether wikilink is a section-only link in current page, i.e. `[[#Section]]`.
--- @param wikilinkText string|nil Wikitext to parse.
--- @return linkData|nil wikilink Components of wikilink, or nil if invalid.
local function parseWikilink( wikilinkText )
-- @class wikilink: table<string, any>
-- @field pageName string The pagename with namespace, if present
-- @field sectionHeading string The section heading
-- @field displayText string Display text, as given or as generated
-- @field isSectionLinkOnly boolean Whether wikilink is a section-only link in current page, i.e. `[[#Section]]`
-- @param wikilinkText string|nil Wikitext to parse.
-- @return table<string, string>|nil wikilink Components of wikilink, or nil if invalid.
if not wikilinkText or wikilinkText == '' then
return nil
end
-- Remove outer square brackets if present: `[[:Help:Foo#Bar|Flog]]` → `Help:Foo#Bar|Flog`
wikilinkText = _gsub( wikilinkText, '^%[%[', '' )
wikilinkText = _gsub( wikilinkText, '%]%]$', '' )
-- Remove initial colon if present
wikilinkText = wikilinkText and string.match( wikilinkText, '^:?(.*)' ) -- Remove initial colon if present.
-- Split on pipe `|` to separate link from display text
local link, displayText = wikilinkText, wikilinkText and string.match( wikilinkText, '^(.-)|(.*)$' )
wikilinkText = link or wikilinkText
-- Split link on hash/pound sign `#` to separate page from section
local pageName, sectionHeading = wikilinkText, wikilinkText and string.match( wikilinkText, '^(.-)#(.*)$' )
local isSectionLink = false
if not pageName and sectionHeading then
isSectionLink = true -- It is a section link to current page, i.e. `[[#Bar]]`.
pageName = nil
-- pageName = FORMAT('#%s', sectionHeading)
elseif not pageName and not sectionHeading then
isSectionLink = false
pageName = wikilinkText
elseif pageName and not sectionHeading then
isSectionLink = false
sectionHeading = nil
pageName = wikilinkText
end
if not displayText and sectionHeading and pageName then
displayText = _format( '%s § %s', pageName, sectionHeading )
elseif not displayText and sectionHeading and not pageName then
displayText = _format( '§ %s', sectionHeading )
elseif not displayText and not sectionHeading and pageName then
displayText = pageName
end
return {
pageName = pageName,
sectionHeading = sectionHeading,
displayText = displayText,
isSectionLink = isSectionLink,
}
end
--- Safely creates a [mw.uri object](lua://mw.uri) from a string, returning `nil` if invalid.
--- See [mw.uri in Lua reference manual](https://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#mw.uri).
---
--- @param s string The URL to check.
--- @return mw.uri|nil uri The URI of the given URL.
local function safeUri( s )
local success, uri = pcall( function ( s )
return mw.uri.new( s )
end )
if success then
return uri
else
return nil
end
end
--- Attempts to extract and normalize a URL from a string.
---
--- @param extract string String from which the URL must be obtained.
--- @return string|nil url The raw URL.
local function extractUrl( extract )
local url = extract
url = _gsub( url, '^([Hh]?[Tt]?[Tt]?[Pp]?[Ss]?:/*)(.+)', 'https://%2' )
local uri = safeUri( url );
if uri and uri.host then
return url
end
return nil
end
--- Cleans and encodes a URL, generates a display label (domain-only if no label provided),
--- and adds word break opportunities for better display.
---
--- @param url string The URL
--- @param text? string|nil The display text for the URL if one must not be generated
--- @return string|nil url The URL, returns nil if URL invalid
--- @return string|nil text The display text for the URL
local function _url( url, text )
-- @TODO cache some of these values.
url = mw.text.trim( url or '' )
text = mw.text.trim( text or '' )
if url == '' or not url then
return nil, text
end
-- If the URL contains any unencoded spaces, encode them,
-- because MediaWiki will otherwise interpret a space as the end of the URL.
url = _gsub( url, '%s', function ( s )
return mw.uri.encode( s, 'PATH' )
end )
-- If there is an empty query string or fragment ID,
-- remove it as it will cause mw.uri.new to throw an error
url = _gsub( url, '#$', '' )
url = _gsub( url, '%?$', '' )
-- If it's an http(s) URL without the double slash, fix it.
url = _gsub( url, '^[Hh][Tt][Tt][Pp]([Ss]?):(/?)([^/])', 'http%1://%3' )
local uri = safeUri( url )
-- Handle URLs without a protocol or who are protocol-relative.
-- e.g., www.example.com/foo or www.example.com:8080/foo, and //www.example.com/foo
if uri
and (not uri.protocol
or (uri.protocol and not uri.host))
and url:sub( 1, 2 ) ~= '//' then
url = 'http://' .. url
uri = safeUri( url )
end
if text == '' or not text then
if uri then
-- Generate clean domain-only text (e.g., "en.wikipedia.org")
local host = _lower( uri.host or '' )
-- Remove www. prefix for cleaner display
host = _gsub( host, '^www%.', '' )
-- For URLs like "http://en.wikipedia.org/wiki/Article_Name"
-- Only want en.wikipedia.org
text = host
-- Add port if present and not standard
if uri.port and uri.port ~= 80 and uri.port ~= 443 then
text = text .. ':' .. uri.port
end
-- Add word break opportunities for better display. Add `<wbr>` before `_/.-#` sequences. This entry _must_ be the first. `<wbr/>` has a `/` in it, you know.
text = _gsub( text, '(/+)', '<wbr/>%1' )
text = _gsub( text, '(%.+)', '<wbr/>%1' )
-- _Disabled_ for now.
---- text = gsub(text,"(%-+)","<wbr/>%1")
text = _gsub( text, '(%#+)', '<wbr/>%1' )
text = _gsub( text, '(_+)', '<wbr/>%1' )
else
-- URL is badly-formed, so just display whatever was given.
text = url
end
end
return url, text
end
--- Strips HTML/wikilink markup, ensures protocol, and returns a cleaned URL and display label.
--- Cleans and normalises a URL string.
---
--- @param url string Raw URL.
--- @param text string Optional display text.
--- @return string|nil cleanUrl Cleaned URL for linking.
--- @return string|nil displayText Display label for the URL.
function M.url( url, text )
local localUrl = url
localUrl = localUrl or extractUrl( localUrl ) or extractUrl( text ) or ''
-- Strip out HTML tags and wikilink brackets
localUrl = _gsub( localUrl, '<[^>]*>', '' ) or ''
localUrl = _gsub( localUrl, '[%[%]]', '' ) or ''
-- Handle common URL prefixes and ensure proper protocol
localUrl = _gsub( localUrl, '^[Ww][Ww][Ww]%.', 'http://www.' ) or ''
-- Process the URL and generate label
local cleanUrl, displayText = _url( localUrl, text )
-- Enhanced label generation for URLs - domain-only format
if cleanUrl and not text then
local uri = safeUri( cleanUrl )
if uri and uri.host then
-- Generate clean domain label (e.g., "en.wikipedia.org")
displayText = _lower( uri.host )
-- Remove 'www.' prefix for cleaner display
displayText = _gsub( displayText, '^www%.', '' )
end
end
return cleanUrl, displayText
end
--- Generate tracking categories.
--- Checks for unknown parameter use and validates input arguments.
---
--- @param oldClassMatched string|nil Whether the parser matched any legacy classes in input.
--- @param rawArgs table Raw arguments passed to the module.
--- @return string categories Category wikitext.
local function renderTrackingCategories( category, class, nocat, link, url, disabled, oldClassMatched, rawArgs )
local categories = ''
class = _type( class ) == 'string' and _lower( class ) or ''
--- Don't add categories if `nocat==true` or `category` is falsy,
--- but still add any custom category passed in.
if category and category ~= '' and M.yesno( category ) ~= false then
-- Extract category name if in wikilink format like [[:Category:Foo Bar|Display]]
local parsed = parseWikilink( category )
if parsed and parsed.pageName then
categories = _format( '[[%s]]', parsed.pageName )
end
end
if M.yesno( nocat ) == true then
return categories
elseif M.yesno( category ) == false then -- Legacy `category=no`.
return categories
end
--- Add categories for outdated classes, dummy buttons, disabled buttons,
--- and external links.
do
--- Dummy button is:
--- - Clickable (i.e. not disabled visually)
--- - No target link and no URL
--- - Gives feedback it'll do something, but does nothing.
--- All matches to if-statements below should all have `ariaDisabled == true`,
--- and therefore `aria-disabled = true`.
if (not link or (M.yesno( link ) == false)) -- Checks for falsy or `link == 'no'`
and not url and not disabled then
categories = _format( '%s [[%s]]', categories, C.trackingCategories.dummyButton )
end
--- Disabled button is:
---- Greyed out (`data.disabled == true`).
---- Also disabled to accessibility API (`aria-disabled = true`).
if disabled then
categories = _format( '%s [[%s]]', categories, C.trackingCategories.disabledButton )
end
if class and oldClassMatched then
categories = _format( '%s [[%s]]', categories, C.trackingCategories.outdatedClasses )
end
if url then
categories = _format( '%s [[%s]]', categories, C.trackingCategories.externalLinks )
end
end
--- Check for unknown parameters and add appropriate categories
local unknownParamCategories = checkForUnknowns( C.knownArgs, rawArgs ) or ''
categories = categories .. unknownParamCategories
return categories
end
---
--- Renders the wikitext span tags for the button.
---
--- @class mw.html: table MediaWiki DOM document content model based on HTML and RDFa.
--- @param data args table Arguments table.
--- @param iconSpan mw.html|nil Icon span element for the button.
--- @param isUrl boolean Whether target is URL
--- @param ariaDisabled boolean Whether button is disabled for ARIA API.
--- @param categories string Categories for the button.
--- @param errorText string|nil Internal string used both as error indicator, and error message text.
--- @param tblClasses table
--- @return string link Wikitext span tags for the button.
local function renderLink( data, iconSpan, isUrl, ariaDisabled, categories, errorText, tblClasses )
--- @type mw.html: Span tag that creates the button.
local displaySpan = mw.html.create( 'span' )
--- @type string|nil Custom CSS style attributes for parent span node (not including plainlinks span tag if URL used).
local styleAttributes = _type( data.style ) == 'string' and data.style or nil
--- @future Additional ARIA attributes for button. If implement 'fake' button for use in collapsible/accordion component, don't forget to declare:
--- displaySpan:attr('aria-haspopup', 'true') --- displaySpan:attr('aria-expanded', 'false')
--- Classes, ARIA `role` and `aria-label`, and `style` attributes for button span tag.
for _, aClass in ipairs( tblClasses or {} ) do
displaySpan:addClass( aClass )
end
displaySpan:attr( 'role', 'button' )
if data.aria_label then
displaySpan:attr( 'aria-label', data.aria_label )
end
if styleAttributes then
displaySpan:attr( 'style', styleAttributes )
end
if iconSpan ~= '' then
displaySpan:node( iconSpan )
end
if data.label then
displaySpan:wikitext( data.label )
end
--- @type string Wikilink that wraps around button wikitext.
local link
if data.disabled or ariaDisabled then
-- `aria-disabled` attribute for no-link/dummy buttons.
-- `aria-disabled` attribute for disabled buttons.
displaySpan:attr( 'aria-disabled', 'true' )
link = _format( '%s %s', tostring( displaySpan ), categories or '' )
else
displaySpan:attr( 'aria-disabled', 'false' )
if isUrl then
link = _format( '<span class="plainlinks">[%s %s]</span> %s', data.url, tostring( displaySpan ),
categories or '' )
elseif isUrl == false and data.link then
link = _format( '[[:%s|%s]] %s', data.link, tostring( displaySpan ), categories or '' )
else -- `isUrl` should be `nil` to get here, or data.link is nil.
-- Dummy/disabled button.
link = _format( '%s %s', tostring( displaySpan ), categories or '' )
end
end
if errorText then
--- Generate error message when viewed in preview mode of an edit.
--[[ --- If previewing an edit displays first argument, otherwise second.
--- @class ifPreview
--- @field main function
--- @type ifPreview
local ifPreview = require('Module:If preview') ]]
if M.yesno( data.nocat ) ~= true then -- Don't add category if `nocat=true`
link = _format( '%s [[%s]]', link, C.trackingCategories.errors )
end -- Add error message to the link if viewing in preview mode.
mw.addWarning( errorText )
end
return link
end
--- Parses arguments from old template parameters. For backward compatibility.
--- Subfunction of parseParameters() for efficiency.
--- @param color? string `color` argument.
--- @param class? string `class` argument.
--- @param action? 'progressive'|'destructive'|'default'|string `action` argument.
--- @return string class String with class that did not match, likely custom class(es).
--- @return string action Returns action resolved.
--- @return string|nil matched Value of matched class if any of the arguments matched.
local function checkColorAndClass( color, class, action )
local actionValue = (_type( action ) == 'string' and action) or ''
color = (_type( color ) == 'string' and color) or ''
class = (_type( class ) == 'string' and _lower( class )) or ''
if color == '' and class == '' then
return '', actionValue, nil
end
-- Resolve action, check against set constants.
for actionName, set in pairs( C.legacyClassSets ) do
if set[ color ] and not C.legacyClassSets[ actionName ][ actionValue ] then
return class, actionName, actionValue -- Found `color`.
end
if set[ class ] and not C.legacyClassSets[ actionName ][ actionValue ] then
return '', actionName, actionValue -- Found `class`.
end
if set[ actionValue ] then
return class, actionName, actionValue -- Found `action`.
end
end
-- No match.
return class, '', nil
end
--- Constructs the attributes for the wikitext/HTML elements.
--- @param parsedArgs args Parsed arguments.
--- @param ariaDisabled boolean Whether button is disabled for ARIA API.
--- @return args data Data, such as attributes, ready to be assembled.
--- @return mw.html|nil iconSpan
--- @return boolean isUrl
--- @return boolean ariaDisabled
--- @return string|nil oldClassMatched
--- @return string|nil errorText Internal string used as both an indicator of an error, and error message text.
--- @return table tblClasses
--- @return mw.title pageTitleObject
local function makeLinkData( parsedArgs, ariaDisabled )
local data = {}
local tblClasses = { C.cssClasses.base, C.cssClasses.fakeButton }
local iconSpan = nil
local isUrl = false
--- @type string|nil
local errorText = nil
local isSamePage = false
local pageTitleObject = mw.title.getCurrentTitle()
--- @todo do i need string check -- type(parsedArgs.icon) == 'string'
data.icon = parsedArgs.icon or nil
data.disabled = parsedArgs.disabled
-- Decide link vs. URL vs. none
-- URL has priority over link if both provided.
if parsedArgs.url then
isUrl = true
local generatedLabel
-- Process URL with enhanced cleaning and label generation
data.url, generatedLabel = M.url( parsedArgs.url, parsedArgs.label )
-- Use provided label or fall back to derived label.
data.label = parsedArgs.label or generatedLabel
elseif parsedArgs.link then
isUrl = false
data.link = parsedArgs.link
data.label = parsedArgs.label
-- Same-page detection
local linkTitleObject = mw.title.new( data.link )
if linkTitleObject and pageTitleObject then
isSamePage = (linkTitleObject.fullText == pageTitleObject.fullText)
end
elseif not parsedArgs.url and not parsedArgs.link then
data.label = parsedArgs.label -- Dummy button as has no link or URL.
end
local class, action, oldClassMatched = checkColorAndClass( parsedArgs.color, parsedArgs.class, parsedArgs.action )
local weight = _type( parsedArgs.weight ) == 'string' and parsedArgs.weight or C.buttonDefaults.weight
local size = _type( parsedArgs.size ) == 'string' and parsedArgs.size or C.buttonDefaults.size
_table_insert( tblClasses, C.cssClasses.actionPrefix .. action )
_table_insert( tblClasses, C.cssClasses.weightPrefix .. weight )
_table_insert( tblClasses, C.cssClasses.sizePrefix .. size )
if (class and class ~= '') then
_table_insert( tblClasses, class ) -- Custom class.
data.class = class
end
if data.disabled then
_table_insert( tblClasses, C.cssClasses.disabled )
else
_table_insert( tblClasses, C.cssClasses.enabled )
end
mw.log( 'Debug classes: ' .. table.concat( tblClasses, ' ' ) )
mw.log( 'Debug action: ' .. (action or 'nil') )
mw.log( 'Debug label: ' .. (data.label or 'nil') )
-- Cannot check length earlier as value changes above.
local labelLength = (_type( data.label ) == 'string' and mw.ustring.len( data.label )) or 0
if data.label and labelLength > C.labelLimits.maxLength then
_table_insert( tblClasses, C.cssClasses.wordWrap )
end
--- @TODO Check if current page is the target link, if so, make button darker.
--- @TODO Must still actually use this in the CSS file.
if isSamePage then
_table_insert( tblClasses, C.cssClasses.samePage )
end
if data.icon then -- Store until end of module for icons CSS output logic.
iconSpan = mw.html.create( 'span' )
iconSpan:addClass( C.cssClasses.icon )
iconSpan:addClass( _format( '%s%s', C.cssClasses.iconPrefix, data.icon ) )
iconSpan:attr( 'aria-hidden', 'true' )
if not data.label then
-- Icon-only button, add extra class for styling.
_table_insert( tblClasses, C.cssClasses.iconOnly )
end
end
-- Label length checks.
if data.label then
if labelLength > C.labelLimits.maxLength then
errorText = C.labelLengthWarningText
elseif labelLength < C.labelLimits.minLength then
_table_insert( tblClasses, C.cssClasses.shortLabel )
end
end
local hasNoLabel = not data.label and not parsedArgs.aria_label
-- Error if no aria-label and no visible label, for any non-disabled button
-- (whether it has a link/URL or is a dummy button)
if hasNoLabel and not parsedArgs.disabled then
errorText = errorText and _format( '%s %s', errorText, C.noAriaLabelWarningText ) or
C.noAriaLabelWarningText
end
data.aria_label = parsedArgs.aria_label
return data, iconSpan, isUrl, ariaDisabled, oldClassMatched, errorText, tblClasses, pageTitleObject
end
--- Parses the module's arguments for backward compatibility.
--- Validates module arguments and returns parsed arguments.
--- With deprecated parameters from old templates and modules.
--- @param rawArgs args table Module arguments.
--- @return args parsedArgs Parsed arguments.
--- @return boolean ariaDisabled Whether button is disabled for ARIA API.
local function parseParameters( rawArgs )
--- It's weird that we may make a link a label, but if we truly
--- only got positional argument `1`, then that would mean it's
--- intentional to make both the link and label the same.
--- `label` value priority: `label` > `text` > `2` > `1`
rawArgs.label = rawArgs.label or rawArgs.text or rawArgs[ 2 ] or rawArgs[ 1 ] or nil
rawArgs.disabled = M.yesno( rawArgs.disabled ) or (M.yesno( rawArgs.link ) == false) or false
rawArgs.link = rawArgs.link or rawArgs[ 1 ] or nil
rawArgs[ 1 ] = nil -- Remove positional rawArgs after assigning
rawArgs[ 2 ] = nil
if rawArgs.disabled then
-- If disabled, must not generate link. Usually doesn't but in case.
rawArgs.link = nil
rawArgs.url = nil
end
local parsedLink = rawArgs.link and parseWikilink( rawArgs.link )
--- @TODO double check next five lines of code
-- If had no label, give autogenerated label.
rawArgs.label = rawArgs.label or (parsedLink and parsedLink.displayText)
-- Try assign newly cleaned link. Fallback if needed.
rawArgs.link = (parsedLink and parsedLink.pageName) or rawArgs.link
if rawArgs.link == '' then
rawArgs.link = nil -- Invalid wikilink, remove it.
end
if rawArgs.link and parsedLink then
-- Fallback to displayText if there was any in wikilink, or `Foo § Bar` or just pagename.
rawArgs.label = rawArgs.label or parsedLink.displayText or nil
rawArgs.link = parsedLink.pageName or rawArgs.link or nil
if rawArgs.link == '' then -- If no link leftover, remove it.
rawArgs.link = nil
end
if parsedLink.pageName and parsedLink.sectionHeading then
rawArgs.link = _format( '%s#%s', parsedLink.pageName, parsedLink.sectionHeading )
elseif parsedLink.isSectionLink and parsedLink.sectionHeading then
rawArgs.link = _format( '#%s', parsedLink.sectionHeading )
elseif parsedLink.pageName then
rawArgs.link = parsedLink.pageName
else
rawArgs.link = nil
end
end
--- `aria-disabled = true` if no link whatsoever, always. Make dummy button. But for accessibility,
--- ARIA must know it won't do anything.
local ariaDisabled = false
if rawArgs.disabled or (not rawArgs.link and not rawArgs.url) then
ariaDisabled = true
end
--- @TODO _OPTION_ to forcefully disable dummy buttons by setting:
---- rawArgs.disabled = true
if rawArgs.label then
--- @TODO refactor: decide if we want to allow [[ or ]] in label, and if so, how to handle it.
--[=[ -- Plain search if [[ or ]] present, to wrap <nowiki> tags.
if string.find(rawArgs.label, '[[', 1, true) or string.find(rawArgs.label, ']]', 1, true) then
rawArgs.label = GSUB(rawArgs.label, '%[%[', '')
rawArgs.label = GSUB(rawArgs.label, '%]%]', '')
if rawArgs.label == '' then -- If no label leftover, remove it.
rawArgs.label = nil
end
end ]=]
rawArgs.label = mw.text.nowiki( rawArgs.label )
else
rawArgs.label = nil
end
rawArgs.nocat = M.yesno( rawArgs.nocat )
-- Normalize ARIA label keys
rawArgs.aria_label = rawArgs.aria_label or rawArgs[ 'aria-label' ] or rawArgs.arialabel
rawArgs[ 'aria-label' ] = nil
rawArgs.arialabel = nil
return rawArgs, ariaDisabled
end
--- Interface for other Lua modules.
--- Function can be called by other Lua modules to generate wikitext.
--- Note: Does not render CSS files or pre-process arguments like `M.main()`.
---
--- @param rawArgs args Module's arguments.
--- @return string data Wikitext that renders button, without CSS files.
function M._main( rawArgs )
local parsedArgs, ariaDisabled = parseParameters( rawArgs )
local data, iconSpan, isUrl, oldClassMatched, errorText, tblClasses, pageTitleObject
data, iconSpan, isUrl, ariaDisabled, oldClassMatched, errorText, tblClasses, pageTitleObject = makeLinkData(
parsedArgs, ariaDisabled )
local isExcludedNamespace = false
for _, namespace in ipairs( C.excludedNamespaces ) do -- Don't add tracking categories in excluded namespaces.
if pageTitleObject.nsText == namespace then
isExcludedNamespace = true
parsedArgs.nocat = true -- Redundant, but whatever.
break
end
end
local categories --- @type string
if not isExcludedNamespace then
categories = renderTrackingCategories( parsedArgs.category, data.class, parsedArgs.nocat,
data.link, data.url, data.disabled, oldClassMatched, rawArgs )
end
return renderLink( data, iconSpan, isUrl, ariaDisabled, categories, errorText, tblClasses )
end
--- Module entry point. Interface for templates and modules.
---
--- Pre-processes arguments, inserts CSS files, and renders the button.
--- @usage Called via the `{{#invoke: Clickable button | main }}` parser function.
--- @param frame args Frame object passed by the MediaWiki parser.
--- @return string|nil wikitextOutput Wikitext for insertion on a wiki page.
function M.main( frame )
local rawArgs = getArgs( frame, {
wrappers = C.wrapperTemplates,
valueFunc = function ( key, value ) -- Custom formatting function for arguments.
value = mw.text.trim( value ) -- Remove whitespace.
if not value or value == '' then -- Remove blank arguments.
return nil
end
if C.lowercaseArgs[ key ] then -- Convert to lowercase.
return _mw_lower( value )
else
return value
end
end,
} )
-- Return empty string, and preview warning if no arguments supplied.
do
local hasInput = false
for _, v in pairs( rawArgs ) do
if v and v ~= '' then
hasInput = true
break
end
end
if not hasInput then
mw.addWarning( C.noArgsWarningText )
return ''
end
end
local output = M._main( rawArgs )
local outputCSS = frame:extensionTag( 'templatestyles', '', { -- Insert CSS files into the output.
src = C.baseCSS, -- Duplicates are de-duplicated by Parsoid.
} )
if rawArgs.icon then
outputCSS = _format( '%s%s', outputCSS, frame:extensionTag( 'templatestyles', '', {
src = C.iconsCSS,
} ) )
end
return _format( '%s%s', outputCSS, output )
end
return M