Jump to content
Toggle menu
  • 8 articles
  • 75 files
  • 7 users
  • 37.8K edits
Neodyland Wiki
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Module:Clickable button: Difference between revisions

From Neodyland Wiki
merge
sync enwiki
Line 1: Line 1:
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
--- @module 'Codex clickable button'
--- @module 'CodexClickableButton'
--- @author [[User:Waddie96]]
--- -@see [[en:Module:Clickable_button]]
--- @license CC-BY-SA-4.0/GFDL
--- @class args table
--- @field label string
--- Generates wikitext for clickable Codex button.
--- Generates wikitext for clickable Codex button.
--- Generates wikitext to render the button component from the (Codex design
---  
--- Outputs wikitext to render the button component from the (Codex design
--- system for Wikimedia)[https://doc.wikimedia.org/codex/latest].
--- system for Wikimedia)[https://doc.wikimedia.org/codex/latest].
--- - Options to include an icon
---- Options to include an icon or create an icon-only button.
--- - Create an icon-only or a dummy button
---- Target a URL or a wikilink
--- - Target a URL or a wikilink
---- Set the weight, size and state of the button (enabled or disabled).
--- - Set the weight, size and state of the button (enabled or disabled).
---- Dummy button creation can be disabled.
---
--- Includes helper functions for URL parsing and cleaning, adding tracking  
--- Includes helper functions for URL parsing and cleaning, adding tracking  
--- categories. Intended for use in templates and other modules.
--- categories. Intended for use in templates and other modules.
--- Implements [[Template:Clickable button]] and others.
--- Supports legacy parameters. To add icons, see CSS.
--- Supports legacy parameters.
---
--- To add icons: [[Template:Clickable button/styles.css]].
--- @author [[User:Waddie96]]
 
--- @license CC-BY-SA-4.0/GFDL
-- TRACKING CATEGORIES:
--- @class CodexClickableButton extends ClickableButton
-- [[Category:Pages using clickable dummy button]]
---  Table containing arguments for the button.
-- [[Category:Pages using disabled button]]
--- @class args table
-- [[Category:Pages using clickable button with external links]]
--- @field label? string The button's visible text label.
-- [[Category:Pages using clickable button with outdated classes]]
--- @field link? string|'no' The target wikilink for the button.
-- [[Category:Errors reported by Module:Clickable button]]
--- @field url? string The target external URL for the button.
-- unless nocat=true. Adds category= any custom category regardless of nocat=.
--- @field icon? string The name of the icon to display found in CSS file.
--- @field color? 'blue'|'green'|'red'|string Legacy color parameter.
--- @field class? string Custom CSS classes for the button.
--- @field weight? 'quiet'|'normal'|'primary' The visual weight of the button.
--- @field size? 'small'|'medium'|'large' The size of the button.
--- @field action? 'progressive'|'destructive'|'default'|string The action type of the button.
--- @field disabled? boolean|'1'|string Whether the button is disabled/greyed out. `disabled` is `true` if: `link` = `'no'` or `false` or `disabled` = `'1'` or `true`.
--- @field style? string Custom inline CSS styles.
--- @field nocat? boolean|string If `true`, suppresses tracking categories.
--- @field category? string An additional category to add.
--- @field aria-label? string The ARIA label for accessibility.
--- @field arialabel? string (alias for aria-label)
--- @field aria_label? string (alias for aria-label)
--- @field [1]? string Positional argument 1 (alias for link/label).
--- @field [2]? string Positional argument 2 (alias for label).
--- @var categories? string Additional categories to add.
--- @var ariaDisabled? boolean Internal flag indicating if the button is functionally disabled for ARIA.
--- @var oldClassMatched string|boolean Internal flag for outdated classes.
--- @var isUrl boolean Whether the link is a URL.
--- @var errorText string|nil Internal string used as both an indicator of an error, and error message text.
--- @var tblClasses table Classes for the button span tag.
--- @var pageTitle mw.title Title of the current page.
--- @todo [[Module:Neturl]] [[Module:Check for unknown parameters]]


-- DEPENDENCIES:
-- Dependencies.
----require('strict')
require('strict')
local yesno = require('Module:Yesno')
local yesno = require('Module:Yesno')
-- [[Template:Clickable button/styles.css]]
-- [[Module:Yesno]] [[Module:Arguments]] [[Module:Check for unknown parameters]]
-- [[Module:Yesno]] [[Module:Arguments]]
-- [[Special:Version]] must include @wikimedia/codex. [[Module:If preview]]
-- [[Module:Check for unknown parameters]]
-- [[Special:Version]] must include @wikimedia/codex.
-- [[Module:If preview]]


local DEFINITIONS = {
--- Tracking category constants.
trackingCategories = {
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',
unknownParams = 'Category:Pages using Module:Clickable button with unknown parameters',
errors = 'Category:Errors reported by Module:Clickable button',
},
--- Parameters whos inputs are converted to lowercase, and are case-insensitive.
lowercaseArgs = {'action', 'color', 'weight', 'size', 'icon'},
--- Valid arguments.
    knownArgs = {
        'class', 'color', 'weight', 'size', 'icon', 'link', 'action',
        'url', 'disabled', 'label', 'aria-label', 'arialabel', 'aria_label',
        'nocat', 'category', '1', '2'
    },
--- Preview warning text for unknown arguments.
    unknownArgsPreviewText = '<span class="error"><strong>Preview warning:</strong>'
        .. ' Using undocumented parameter(s): "_VALUE_".</span>',
--- No ARIA-label warning text.
noAriaLabelWarningText = '<span class="error"><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"><strong>Preview warning:'
.. '</strong> A button label should ideally be shorter '
.. 'than 38 characters, see [[en:Template:Clickable button/doc'
.. '#Button label length|documentation]].'
.. '</span>',
baseCSS = 'Template:Clickable button/styles.css',
iconsCSS = 'Template:Clickable button/icons.css',
legacyClassSets = { progressive = { ['blue'] = true, ['green'] = true,
['ui-button-green'] = true, ['ui-button-blue'] = true,
['mw-ui-constructive'] = true, ['mw-ui-progressive'] = true,
['progressive'] = true
},
destructive = { ['red'] = true, ['ui-button-red'] = true,
['mw-ui-destructive'] = true, ['destructive'] = true
}
}
}
local p = {}
local p = {}
local gsub = mw.ustring.gsub
local gsub = mw.ustring.gsub
local len = mw.ustring.len
local lower = mw.ustring.lower
local lower = mw.ustring.lower
local trim = mw.text.trim
local html = mw.html


--- Creates [URI object](lua://mw.uri).
--- @see https://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#mw.uri
---
--- Creates [URI object](lua://mw.uri) from URL.
--- Creates [URI object](lua://mw.uri) from URL.
--- Checks the URI is safe for use as a wikilink in MediaWiki.
--- Checks the URI is safe for use as a wikilink in MediaWiki.
---@class mw.uri: string
---@param s string The URL to check.
---@param s string The URL to check.
---@return mw.uri|nil uri The URI of the given URL.
---@return mw.uri|nil uri The URI of the given URL.
Line 83: Line 142:
local function _url(url, text)
local function _url(url, text)
---@type string URL with trailing whitespace removed
---@type string URL with trailing whitespace removed
url = trim(url or '')
url = mw.text.trim(url or '')
text = trim(text or '')
text = mw.text.trim(text or '')


if url == '' then
if url == '' then
Line 109: Line 168:


-- Handle URL's without a protocol or are protocol-relative.
-- Handle URL's without a protocol or are protocol-relative.
-- e.g., www.example.com/foo or www.example.com:8080/foo, and //www.example.com/foo
--e.g., www.example.com/foo or www.example.com:8080/foo,
if uri and (not uri.protocol or (uri.protocol and not uri.host)) and url:sub(1, 2) ~= '//' then
--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
url = 'http://' .. url
uri = safeUri(url)
uri = safeUri(url)
Line 143: Line 206:


--- Cleans and normalises a URL string.
--- Cleans and normalises a URL string.
--- Copied from [[en:Module:URL]] with minor modifications.
---
--- - Encodes `url`.
---- Encodes `url`.
--- - Removes empty query strings and fragement IDs.
---- Removes empty query strings and fragement IDs.
--- - Fixes the protocol and the double slash that follows, i.e. `https://`
---- Fixes the protocol and the double slash that follows, i.e. `https://`
--- - Handles URLs that have no protocol or are protocol-relative.
---- Handles URLs that have no protocol or are protocol-relative.
--- - Generates label from the URL if one is not given.
---- Generates label from the URL if one is not given.
---@param url string The raw URL to clean.
---@param url string The raw URL to clean.
---@param text string Optional link display text.
---@param text string Optional link display text.
---@return string|nil localUrl Cleaned URL for wikilink.
---@return string|nil localUrl Cleaned URL for wikilink.
---@return string|nil text Display label for wikilink.
---@return string|nil text Display label for wikilink.
----@deprecated `mw.uri` was deprecated in MW 1.43 in favour of the native browser `URL`.
---[!] @deprecated `mw.uri` class deprecated in MW 1.43 for native browser `URL`.
local function url(url, text)
function p.url(url, text)
local localUrl = url
local localUrl = url
localUrl = localUrl or extractUrl(localUrl) or extractUrl(text) or ''
localUrl = localUrl or extractUrl(localUrl) or extractUrl(text) or ''
Line 164: Line 227:
end
end


--- Renders tracking categories based on given parameters. Also checks for unknown parameter use,
--[[
--- validates given arguments, and categorizes accordingly.
local function netUrl(url, text)
---@param args table Original arguments given to module.
local localUrl = url or ''
local function renderTrackingCategories(args)
local localText = text or ''
local parsedUrl = nil
local moduleNetUrl = require('Module:Neturl')
if not localUrl and not localUrl ~= '' then return nil, nil end
if text ~= '' then
parsedUrl = moduleNetUrl:parse(localUrl)
localText = tostring(parsedUrl.host) .. tostring(parsedUrl.path)
localUrl = tostring(parsedUrl:normalize())
else
localUrl = tostring(moduleNetUrl:parse(localUrl):normalize())
end
return localUrl, localText
end
]]
 
--- Helper function for tracking categories.
---- Checks for unknown parameter use.
---- Validates given arguments.
---- Categorizes accordingly.
---@param data args Arguments table.
---@return args data Arguments table.
---@return string categories Category wikitext.
---@return mw.title pageTitle Current page title.
local function renderTrackingCategories(data, oldClassMatched)
local categories = ''
local categories = ''
local class = args.class or ''
local category = data.category or ''
---- local check_for_unknown_parameters = require("Module:Check for unknown parameters")._check
local class = type(data.class) == 'string' and lower(data.class) or ''
---- local title = mw.title.getCurrentTitle()
-- local checkForUnknowns = require("Module:Check for unknown parameters")._check
local pageTitle = mw.title.getCurrentTitle()
 
--- Don't add categories if `nocat=true`, but still add any custom category.
--- Don't add categories if `nocat=true`, but still add any custom category.
--- Custom category passed in
--- Custom category passed in.
if args.category --[[and yesno(args.nocat) == false]] then
if category and category ~= '' then
local s = args.category
local s = category
s = s:gsub('%[%[', ''):gsub('%]%]', ''):gsub('[Cc]ategory:', '')
s = s:gsub('%[', ''):gsub('%]', ''):gsub('[Cc]ategory:', '')
categories = categories .. '[[' .. 'Category:' .. s .. ']]'
categories = string.format(' [[Category:%s]]', s)
end
end
if yesno(args.nocat) == true then
if yesno(data.nocat) == true then
return categories or ''
return data, categories, pageTitle
end
end
--[=[ local unknownText = string.format('[[Category:%s]]',
DEFINITIONS.trackingCategories.unknownParams, pageTitle.fullText)
local unknownParams = checkForUnknowns({
checkpositional = 'y', unknown = unknownText,
preview = DEFINITIONS.unknownArgsPreviewText, ignoreblank = 'y',
'class', 'color', 'weight', 'size', 'icon', 'link', 'action',
'url', 'disabled', 'label', 'aria-label', 'arialabel', 'aria_label',
'nocat', 'category', '1', '2'
}, data
)
if unknownParams ~= '' then
categories = string.format('%s %s', categories, unknownParams)
end ]=]


--[=[
--[=[
Line 198: Line 301:
'label', 'aria-label', 'arialabel', 'aria_label', 'action', 'nocat',
'label', 'aria-label', 'arialabel', 'aria_label', 'action', 'nocat',
'category', '1', '2'
'category', '1', '2'
}, args) ]=]
}, data) ]=]


--- Dummy button is:
--- Add categories for outdated classes, dummy buttons, disabled buttons,
--- - Clickable (i.e. not disabled visually)
--- and external links.
--- - No target link/URL (i.e., gives feedback it'll do something,
do
---  but does nothing).
---Dummy button is:
--- They should all have `ariaDisabled == true`, therefore `aria-disabled = true`
---- Clickable (i.e. not disabled visually)
if (not args.link or yesno(args.link) == false)
---- No target link and no URL
and not args.url
---- Gives feedback it'll do something, but does nothing.
and not args.disabled then
---All matches to if-statements below should all have `ariaDisabled == true`,
categories = categories .. '[[Category:Pages using clickable dummy button]]'
---and therefore `aria-disabled = true`.
end
if (not data.link
--- Disabled button is:
or yesno(data.link) == false) -- Checks for falsy or `link == 'no'`
--- - Greyed out (`args.disabled == true`)
and not data.url
--- - Will likely have no link/URL
and not data.disabled
if args.disabled then
then
categories = categories .. '[[Category:Pages using disabled button]]'
categories = string.format('%s [[%s]]', categories,
end
DEFINITIONS.trackingCategories.dummyButton)
end
---Disabled button is:
--- - Greyed out (`data.disabled == true`)
if data.disabled then
categories = string.format('%s [[%s]]', categories,
DEFINITIONS.trackingCategories.disabledButton)
end


if class == 'ui-button-green'
if class and oldClassMatched then
or class == 'ui-button-blue'
categories = string.format('%s [[%s]]', categories,
or class == 'ui-button-red'
DEFINITIONS.trackingCategories.outdatedClasses)
or class == 'mw-ui-progressive'
end
or class == 'mw-ui-destructive' then
if data.url then
categories = categories .. '[[Category:Pages using clickable ' ..
categories = string.format('%s [[%s]]', categories,
'button with outdated classes]]'
DEFINITIONS.trackingCategories.externalLinks)
end
end
if args.url then
categories = categories .. '[[Category:Pages using clickable ' ..
'button with external links]]'
end
if class == 'mw-ui-constructive' then
categories = categories .. '[[Category:Pages using clickable ' ..
'button with deprecated parameters]]'
end
end
return categories
return data, categories, pageTitle
end
end


--- Renders the wikitext span tags for the button
--- Renders the wikitext span tags for the button.
--- @param data table
--- @param data args table Arguments table.
--- @return string link
--- @param iconSpan mw.html Icon span element for the button.
local function renderLink(data)
--- @param isUrl boolean Whether target is URL
---@class mw.html: string MediaWiki DOM document content model based on HTML and RDFa
--- @param ariaDisabled boolean Whether button is disabled for ARIA API.
---@type mw.html Span tag that creates the button
--- @param categories string Categories for the button.
local displaySpan = html.create('span')
--- @param errorText string Error message for the button, if needed.
for _, aClass in ipairs(data.classes or {}) do
--- @return string link Wikitext span tags for the button.
local function renderLink(data, iconSpan, isUrl, ariaDisabled, categories, errorText, tblClasses)
---@class mw.html: table MediaWiki DOM document content model based on HTML and RDFa.
---@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')
 
for _, aClass in ipairs(tblClasses or {}) do
displaySpan:addClass(aClass)
displaySpan:addClass(aClass)
end
end
--- ARIA role and label attributes for button.
displaySpan:attr('role', 'button')
displaySpan:attr('role', 'button')
if data.aria_label then
if data.aria_label then
displaySpan:attr('aria-label', data.aria_label)
displaySpan:attr('aria-label', data.aria_label)
end
if styleAttributes then
displaySpan:attr('style', styleAttributes)
end
end


if data.iconSpan then
if iconSpan ~= '' then
displaySpan:node(data.iconSpan)
displaySpan:node(iconSpan)
end
end
if data.label then
if data.label then
Line 258: Line 377:
end
end


--- Wikilink that wraps around button wikitext
---@type string Wikilink that wraps around button wikitext.
---@type string
local link
local link
if data.disabled then
if data.disabled then
-- ARIA disabled attribute for disabled buttons
-- ARIA disabled attribute for disabled buttons
displaySpan:attr('aria-disabled', 'true')
displaySpan:attr('aria-disabled', 'true')
link = string.format('%s %s', tostring(displaySpan), data.categories)
link = string.format('%s %s', tostring(displaySpan), categories)
elseif data.ariaDisabled then
elseif ariaDisabled then
-- ARIA disabled attribute for no-link/dummy buttons
-- ARIA disabled attribute for no-link/dummy buttons
displaySpan:attr('aria-disabled', 'true')
displaySpan:attr('aria-disabled', 'true')
link = string.format('%s %s', tostring(displaySpan), data.categories)
link = string.format('%s %s', tostring(displaySpan), categories)
else
else
displaySpan:attr('aria-disabled', 'false')
displaySpan:attr('aria-disabled', 'false')
if data.isUrl then
if isUrl then
link = string.format('<span class="plainlinks">[%s %s]</span> %s',
link = string.format('<span class="plainlinks">[%s %s]</span> %s',
data.url, tostring(displaySpan), data.categories)
data.url, tostring(displaySpan), categories)
elseif data.isUrl == false then
elseif isUrl == false then
link = string.format('[[%s|%s]] %s', data.link, tostring(displaySpan),
link = string.format('[[%s|%s]] %s', data.link, tostring(displaySpan),
data.categories)
categories)
else-- `data.isUrl` should be `nil` to get here.
else-- `isUrl` should be `nil` to get here.
-- Dummy/disabled button
-- Dummy/disabled button
link = string.format('%s %s', tostring(displaySpan), data.categories)
link = string.format('%s %s', tostring(displaySpan), categories)
end
end
end
end


if data.error then
if errorText then
--- Generate error message when viewed in preview.
--- Generate error message when viewed in preview mode of an edit.
--- Categorise into [[Category:Errors reported by Module:Clickable button]]
--- Categorise into [[Category:Errors reported by Module:Clickable button]]
---@class ifPreview
---@class ifPreview
---@field main function
---@field main function
---@type ifPreview
---@type ifPreview Module checks if previewing an edit.
local ifPreview = require('Module:If preview')
local ifPreview = require('Module:If preview')
if yesno(data.nocat) ~= true then
if yesno(data.nocat) == false then -- Don't add category if `nocat=true`
link = link .. ' ' .. '[[Category:Errors reported by Module:Clickable button]]'
link = string.format('%s [[%s]]', link, DEFINITIONS.trackingCategories.errors)
end
end -- Add error message to the link if viewing in preview mode.
return ifPreview.main( { data.error .. link, link } )
return ifPreview.main({ errorText .. link, link })
end
end


Line 299: Line 417:
end
end


--- Parses the module's arguments for backward compatibility with deprecated
--- Parses arguments from old template parameters. For backward compatibility.
--- parameters from old templates and modules.
---@param color? string `color` argument.
---@param args table Module arguments.
---@param class? string `class` argument.
---@return table args Parsed arguments.
---@param action? 'progressive'|'destructive'|'default'|string `action` argument.
local function parseParameters(args)
---@return string class String with class that did not match, likely custom class(es).
---@return string action Returns action resolved.
---@return string|boolean 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, false
    end
 
    -- Resolve action, check against set constants.
    for actionName, set in pairs(DEFINITIONS.legacyClassSets) do
        if set[color] and not DEFINITIONS.legacyClassSets[actionName][actionValue] then
            return class, actionName, actionValue  -- Found `color`.
        end
        if set[class] and not DEFINITIONS.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, '', false
end
 
--- Parses the module's arguments for backward compatibility.
--- 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)
local ariaDisabled = false
--- It's weird that we may make a link a label, but if we truly
--- 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
--- only got positional argument `1`, then that would mean it's
--- intentional to make both the link and label the same.
--- intentional to make both the link and label the same.
--- `label` value priority: `label` > `2` > `1`
--- `label` value priority: `label` > `2` > `1`
---@type string
rawArgs.label = rawArgs.label or rawArgs[2] or rawArgs[1]
args.label = args.label or args[2] or args[1]


--- `disabled` is `true` if:
---@todo Should `link == 'no'` disable dummy buttons?
--- - `link` = `'no'` or `false`
rawArgs.disabled = yesno(rawArgs.disabled) or (yesno(rawArgs.link) == false)
--- - `disabled` = `'1'` or `true`
---@TODO Should `link == 'no'` disable dummy
---      buttons?
---@type boolean
args.disabled = yesno(args.disabled) or (yesno(args.link) == false)
--- `link` value priority: `link` > `1`
--- `link` value priority: `link` > `1`
---@type string
rawArgs.link = rawArgs.link or rawArgs[1]
args.link = args.link or args[1]
if rawArgs.disabled then
if args.disabled then
-- If `link` was `'no'`, i.e. `true`, then must
-- If `link` was `'no'`, i.e. `true`, then must
-- not generate a link either. Clearing after assigning
-- not generate a link either. Clear positional `1`
-- positional arg[1].
-- after assigning.
args.link = nil
rawArgs.link = nil
args.url = nil
rawArgs.url = nil
end
end


-- Remove positional args after assigning
-- Remove positional rawArgs after assigning
args[1] = nil
rawArgs[1] = nil
args[2] = nil
rawArgs[2] = nil


if (args.link and yesno(args.link) ~= false) or args.url then
if (rawArgs.link and yesno(rawArgs.link) ~= false)
args.ariaDisabled = false
or rawArgs.url then
ariaDisabled = false
--[=[ -- Remove `[[` and `]]` if present in wikilink and label.
if rawArgs.link
and yesno(rawArgs.link) ~= false then
rawArgs.link = rawArgs.link:gsub('^%[%[', ''):gsub('%]%]$', '')
rawArgs.label = rawArgs.label:gsub('^%[%[', ''):gsub('%]%]', '')
end ]=]
else
else
--- `aria-disabled = true` if no link whatsoever, always.
--- `aria-disabled = true` if no link whatsoever, always.
--- Make dummy button. But for accessibility,
--- Make dummy button. But for accessibility,
--- ARIA must know it won't do anything.
--- ARIA must know it won't do anything.
--- _OPTION_ to forcefully disable dummy buttons
--- _OPTION_ to forcefully disable dummy buttons by setting:
--- by setting: `args.disabled = true`
--- rawArgs.disabled = true
args.ariaDisabled = true
ariaDisabled = true
end
end
-- Normalize ARIA label keys
-- Normalize ARIA label keys
args.aria_label = args.aria_label or args['aria-label'] or args.arialabel
rawArgs.aria_label = rawArgs.aria_label or rawArgs['aria-label'] or rawArgs.arialabel
 
    rawArgs['aria-label'] = nil
-- Determine action from old parameters color/class
    rawArgs.arialabel = nil
local color = type(args.color) == 'string' and args.color:lower() or ''
local class = type(args.class) == 'string' and lower(args.class) or ''
if ( color == 'blue'
or color == 'green'
or class == 'ui-button-green'
or class == 'ui-button-blue'
or class == 'mw-ui-constructive'
or class == 'mw-ui-progressive'
or class == 'progressive') then
args.action = "progressive"
args.class = nil
elseif (color == "red"
or class == 'ui-button-red'
or class == 'mw-ui-destructive'
or class == 'destructive') then
args.action = "destructive"
args.class = nil
end


return args
return rawArgs, ariaDisabled
end
end


--- Constructs the attributes for the wikitext/HTML elements.
--- Constructs the attributes for the wikitext/HTML elements.
---@param args table Parsed arguments.
---@param parsedArgs args Parsed arguments.
---@return table data Data, such as attributes, ready to be assembled.
---@return args data Data, such as attributes, ready to be assembled.
local function makeLinkData(args)
---@return mw.html iconSpan
---@return boolean isUrl
---@return boolean ariaDisabled
---@return boolean hasIcon
---@return string|boolean oldClassMatched
---@return string|nil errorText
---@return table tblClasses
local function makeLinkData(parsedArgs, ariaDisabled)
local data = {}
local data = {}
local iconSpan
local isUrl = false


-- Decide link vs. url vs. none
-- Decide link vs. url vs. none
-- URL has priority over link if both provided.
-- URL has priority over link if both provided.
-- Make pretty URL and label based on URL if no label.
-- Make pretty URL and label based on URL if no label.
if args.url then
if parsedArgs.url then
data.isUrl = true
isUrl = true
local label
local label
data.url, label = url(args.url, args.label)
data.url, label = p.url(parsedArgs.url, parsedArgs.label) -- netUrl(parsedArgs.url, parsedArgs.label)
data.label = args.label or label
data.label = parsedArgs.label or label
elseif args.link then
elseif parsedArgs.link then
data.isUrl = false
isUrl = false
data.link = args.link
data.link = parsedArgs.link
data.label = args.label
data.label = parsedArgs.label
-- Dummy button as has no link or url
elseif not parsedArgs.url and not parsedArgs.link then
elseif not args.url and not args.link then
data.label = parsedArgs.label -- Dummy button as has no link or url
data.label = args.label
end
 
local errorText = nil
local hasNoLabel = not data.label and not parsedArgs.aria_label
local isVisuallyActive = not parsedArgs.disabled and not ariaDisabled
if hasNoLabel and isVisuallyActive then --- Error if no aria-label and no visible label
errorText = DEFINITIONS.noAriaLabelWarningText
end
end


--- If error has occured contains error string, or `nil`.
local tblClasses = {}
---@type string|nil
tblClasses = { 'cdx-button', 'cdx-button--fake-button' }
data.error = nil
local class, action, oldClassMatched
--- Error if no aria-label and no visible label
= checkColorAndClass(parsedArgs.color, parsedArgs.class, parsedArgs.action)
if (not data.label and not args.aria_label
local weight = type(parsedArgs.weight) == 'string' and parsedArgs.weight or 'normal'
and not args.disabled and not args.ariaDisabled) then
local size  = type(parsedArgs.size) == 'string' and parsedArgs.size or 'medium'
data.error = '<span class="error"><strong>Preview warning:</strong>'
table.insert(tblClasses, 'cdx-button--action-' .. action)
.. ' A button without a visible label '
table.insert(tblClasses, 'cdx-button--weight-' .. weight)
.. 'needs an [[WAI-ARIA|ARIA]] label, please define it using '
table.insert(tblClasses, 'cdx-button--size-' .. size)
.. '"aria-label".</span>'
if (class and class ~= '') then
table.insert(tblClasses, class) -- Custom class.
data.class = class
end
end


--- Classes for button span tag
---@todo Check if current page is the target link, if so, make button darker.
---@type table
local isSamePage = false
data.classes = { 'cdx-button', 'cdx-button--fake-button' }
if not isUrl then
local class  = type(args.class) == 'string' and args.class
if data.link == data.fullText then
or ''
isSamePage = true
local action = type(args.action) == 'string' and args.action:lower()
end
or 'default' -- or 'default' by default
local weight = type(args.weight) == 'string' and args.weight:lower()
or 'normal' -- or 'normal' by default
local size  = type(args.size) == 'string' and args.size:lower()
or 'medium' -- or 'medium' by default
table.insert(data.classes, 'cdx-button--action-' .. action)
table.insert(data.classes, 'cdx-button--weight-' .. weight)
table.insert(data.classes, 'cdx-button--size-' .. size)
if class then
table.insert(data.classes, class) -- Custom class
end
end


--- Disabled, greyed-out state of button
data.disabled = parsedArgs.disabled
---@type boolean
local labelLength = (type(data.label) == 'string' and mw.ustring.len(data.label)) or 0
data.disabled = args.disabled
if data.disabled then
if data.disabled then
table.insert(data.classes, 'cdx-button--fake-button--disabled')
table.insert(tblClasses, 'cdx-button--fake-button--disabled')
else
else
table.insert(data.classes, 'cdx-button--fake-button--enabled')
table.insert(tblClasses, 'cdx-button--fake-button--enabled')
end
if data.label and labelLength > 38 then
table.insert(tblClasses, 'cdx-button--word-wrap')
end
end
---@todo Must still actually use this in the CSS file.
if isSamePage then
        table.insert(tblClasses, 'cdx-button--same-page')
    end


if type(args.icon) == 'string' then
local hasIcon = false
--- Icon for button
if type(parsedArgs.icon) == 'string' and parsedArgs.icon then
---@type string
---@type string Name of icon for button.
local icon = args.icon:lower()
local icon = parsedArgs.icon
data.iconSpan = html.create('span')
hasIcon = true -- Assign to carry to final return for CSS output.
data.iconSpan:addClass('cdx-button__icon cdx-demo-css-icon--' .. icon)
iconSpan = mw.html.create('span')
data.iconSpan:attr('aria-hidden', 'true')
iconSpan:addClass('cdx-button__icon cdx-demo-css-icon--' .. icon)
iconSpan:attr('aria-hidden', 'true')
if not data.label then
if not data.label then
-- Icon-only button, add extra class for styling
-- Icon-only button, add extra class for styling.
table.insert(data.classes, 'cdx-button--icon-only')
table.insert(tblClasses, 'cdx-button--icon-only')
end
end
end
end


-- Label length checks
-- Label length checks.
if data.label then
if data.label then
if len(data.label) > 38 then
if labelLength > 38 then
local errorMsg ='<span class="error"><strong>Preview warning:'
errorText = errorText
.. '</strong> A button label '
and string.format('%s %s', errorText, DEFINITIONS.labelLengthWarningText)
.. 'should ideally be shorter than 38 characters, see '
or DEFINITIONS.labelLengthWarningText
.. '[[en:Template:Clickable button/doc#Button label length|documentation]].'
elseif labelLength < 3 then
.. '</span>'
table.insert(tblClasses, 'cdx-button--short-label')
data.error = data.error and (data.error .. ' ' .. errorMsg) or errorMsg
elseif len(data.label) < 3 then
table.insert(data.classes, 'cdx-button--short-label')
end
end
end
end


data.aria_label = args.aria_label
data.aria_label = parsedArgs.aria_label
data.ariaDisabled = args.ariaDisabled


return data
return data, iconSpan, isUrl, ariaDisabled, hasIcon,
oldClassMatched, errorText, tblClasses
end
end


--- **Interface for other Lua modules.**
--- Interface for other Lua modules.
--- Function can be called by other Lua modules to generate wikitext
--- Function can be called by other Lua modules to generate wikitext.
--- without pre-processing arguments with [[Module:Arguments]], adding
--- Does not render CSS file or pre-process arguments.
--- TemplateStyles, and returning blank string if no arguments were given.
---  
---@param args table Module's arguments.
---@param rawArgs args Module's arguments.
---@return string data Wikitext that renders button, without CSS file.
---@return string data Wikitext that renders button, without CSS file.
function p._main(args)
---@return boolean hasIcon Whether the button has an icon.
---@type table Parsed arguments.
function p._main(rawArgs)
local parsedArgs = parseParameters(args)
---@type args Parsed arguments.
---@type table Raw data such as element attributes, values, and contents.
local parsedArgs, ariaDisabled = parseParameters(rawArgs)
local data = makeLinkData(parsedArgs)
parsedArgs.label = data.label
parsedArgs.ariaDisabled = data.ariaDisabled or false
data.categories = renderTrackingCategories(parsedArgs)


return renderLink(data)
---@type args HTML attributes with values, and contents.
local data, iconSpan, isUrl, hasIcon, oldClassMatched, errorText, tblClasses
data, iconSpan, isUrl, ariaDisabled, hasIcon, oldClassMatched, errorText, tblClasses
= makeLinkData(parsedArgs, ariaDisabled)
 
local categories
data, categories = renderTrackingCategories(data, oldClassMatched)
 
return renderLink(data, iconSpan, isUrl, ariaDisabled,
categories, errorText, tblClasses), hasIcon
end
end


--- **Interface for templates.**
--- Interface for templates.
--- Main function called by templates to use this module.
--- Called by the `{{#invoke: Clickable button | main }}` parser function.
--- Using the `{{#invoke:Clickable button|main|arguments}}` parser function.
--- Pre-processes arguments, inserts CSS file, and renders the button.
---@deprecated
---
---@class frame: string
---@param frame frame Module's arguments from template invocation.
---@param frame frame
---@return string wikitextOutput Wikitext for insertion on a wiki page.
---@return string Returns wikitext for insertion in a wiki page.
function p.main(frame)
function p.main(frame)
local args = require('Module:Arguments').getArgs(frame, {
    ---@type table<string, string> Parsed arguments.
wrappers = {
    -- If called from wrapper, don't look for parentFrame().
'Template:Clickable button',
    local rawArgs = require('Module:Arguments').getArgs(frame, {
'Template:Clickable button/sandbox',
            wrappers = {
'Template:Cdx-button'
                'Template:Clickable button',
}
                'Template:Clickable button/sandbox',
})
                'Template:Cdx-button', 'Template:Cdx-button/sandbox'
            }
        })
 
    -- Make arguments lowercase where appropriate.
    -- Except `class` as _HTML class names_ are case-sensitive.
    for _, key in ipairs(DEFINITIONS.lowercaseArgs) do
        if rawArgs[key] then
            rawArgs[key] = lower(rawArgs[key])
        end
    end


-- Return empty string if no arguments supplied
    -- Return empty string if no arguments supplied.
local hasInput = false
    do
for _, v in pairs(args) do
local hasInput = false
if v and v ~= "" then
for _, v in pairs(rawArgs) do
hasInput = true
if v and v ~= "" then
break
hasInput = true
break
end
end
if not hasInput then
return ''
end
end
end
if not hasInput then
return ''
end
end


--[[ TESTING
local output, hasIcon = p._main(rawArgs)
-- Insert CSS file into the output.
local returnValue = p._main(args)
    -- Note: This is not the most efficient way to include CSS,
local templateStyle = frame:extensionTag(
    -- but it's simple and avoids potential issues with caching.
'templatestyles', '', { src = 'Template:Clickable button/styles.css' }
local outputCSS = frame:extensionTag(
'templatestyles', '',
{ src = DEFINITIONS.baseCSS }
)
if hasIcon then
output = string.format('%s%s%s', outputCSS,
frame:extensionTag(
'templatestyles', '',
{ src = DEFINITIONS.iconsCSS }
),
output
)
)
do
else
local returnS1 = html.create('div')
output = string.format('%s%s', outputCSS, output)
local lexer = require('Module:Lua lexer')
returnS1:addClass('cdx-message')
returnS1:addClass('cdx-message--block')
local returnS2 = html.create('span')
returnS2:addClass('cdx-message__icon')
local returnS3 = html.create('div')
returnS3:addClass('cdx-message__content')
returnS3:attr('font-size', 'small')
returnValue = tostring(returnValue)
returnValue = tostring(lexer(returnValue))
local returnS4 = frame:extensionTag('syntaxhighlight',
returnValue , { lang = 'lua' } )
returnValue = templateStyle ..
tostring(returnS1:node(returnS2):done():node(returnS3):node(returnS4):allDone())
end
end


return returnValue
    return output
-- ]]
 
return frame:extensionTag(
'templatestyles', '', { src = 'Template:Clickable button/styles.css' }
) .. p._main(args)
end
end


return p
return p

Revision as of 05:07, 25 September 2025

Module documentation[ view · edit · history · purge ]
This documentation is transcluded from Module:Clickable button/doc.

Template:Template rating

Template:Uses templatestyles

Generates wikitext for clickable Codex button. Renders the button component from the Codex design system for Wikimedia. Includes helper functions for URL parsing and cleaning, adding tracking categories. Intended for use in templates and other modules. Implements Template:Clickable button and others. Supports legacy parameters.

  • Options to include an icon or create an icon-only button.
  • Target a URL or a wikilink
  • Set the weight, size and state of the button (enabled or disabled).
  • Dummy button creation can be disabled.

For more information on appropriate usage of UI buttons, see the Codex documentation.

  • Inserts two CSS files. Module:Clickable button/styles.css is required and makes minor tweaks for word-wrapping if the visible label is too long, centering or aligning button left or right, and minimum widths as is needed for icon-only buttons and labels containing two characters or less.
  • The second CSS file, Module:Clickable button/icons.css, is prepended to the button's HTML only if an icon is used.
  • Supports legacy parameters from previous templates.

Usage in wikitext

Some arguments are case-insensitive.

{{#invoke:Clickable button|main
| 1        = <!-- Alias for wikilink -->
| 2        = <!-- Alias for label -->
| label    = <!-- Button visible text label -->
| link     = <!-- Target wikilink -->
| url      = <!-- Target external URL -->
<!-- Inputs action, weight, size, and icon are case-insensitive -->
| action   = <!-- progressive | destructive | default: default. -->
| weight   = <!-- primary | quiet | default: normal. -->
| size     = <!-- small | large | default: medium. Automatically chooses size based on line-height and device. -->
| icon     = <!-- Name of icon, stored in [[Module:Clickable button/icons.css]] e.g., search  -->
| disabled = <!-- `true` or any other true value like `1` or `yes`. -->
| aria-label = <!-- [[w:ARIA]] label for accessibility DOM tree. -->
| nocat      = <!-- `true` to not auto-categorize. -->
<!-- Others -->
| category = <!-- Category name e.g., Category:Name or Name or [[Category:Name]] -->
| class    = <!-- Custom CSS class without quotation marks -->
| style    = <!-- Custom CSS styling without quotation marks -->
<!-- Legacy arguments -->
| color    = <!-- blue | red --> 
}}

Usage in other modules

Ensure you know what to expect from the function you call from another module.

  • function p.main(frame) emits TemplateStyles for the CSS files with the wikitext, and pre-processes the arguments in a frame using Module:Arguments, e.g. ignore blank values'', and trim trailing whitespace.
  • function p._main(arguments) Parses the arguments such as lowercase appropriate arguments, account for use of legacy parameters and decides whether aria-disabled should be true.
  • function p.url(url, [label]) is available, not for button creation, but as an adaption of Module:URL to clean and normalise a URL string and optionally generate a label.
  • The module's other functions, such as makeLinkData() and renderLink(), are localised/local to the module and would need to be made global first to be accessible to other modules.

To call p.main() for example, use:

local createButton = require( 'Module:Clickable button/sandbox' )
buttonWikitext = createButton.main( {
    link = 'South Africa',
    label = 'Go to South Africa',
    action = 'progressive'
    weight = 'default',
    size = 'medium',
    icon = 'link-external',
} ) 
return buttonWikitext

and the value of buttonWikitext would be:

<<templatestyles src="Module:Cdx-button/styles.css" /><templatestyles src="Module:Cdx-button/icons.css" /><span class="cdx-button cdx-button--fake-button cdx-button--action-progressive cdx-button--weight-quiet cdx-button--size-medium" role="button" aria-disabled="false"><span class="cdx-button__icon cdx-demo-css-icon--link-external" aria-hidden="true"></span>Go to South Africa</span>

Function _main would output:

<span class="cdx-button cdx-button--fake-button cdx-button--action-progressive cdx-button--weight-quiet cdx-button--size-medium" role="button" aria-disabled="false"><span class="cdx-button__icon cdx-demo-css-icon--link-external" aria-hidden="true"></span>Go to South Africa</span>

As a result, unless a CSS file is added to give the appropriate class an icon, the icon will not render.

Implementation

Length of visible label

See the Codex button component documentation.


--------------------------------------------------------------------------------
--- @module 'CodexClickableButton'
--- Generates wikitext for clickable Codex button.
--- 
--- Outputs wikitext to render the button component 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 of the button (enabled or disabled).
---- Dummy button creation can be disabled.
---
--- Includes helper functions for URL parsing and cleaning, adding tracking 
--- categories. Intended for use in templates and other modules.
--- Supports legacy parameters. To add icons, see CSS.
---
--- @author [[User:Waddie96]]
--- @license CC-BY-SA-4.0/GFDL
--- @class CodexClickableButton extends ClickableButton
---  Table containing arguments for the button.
--- @class args table
--- @field label? string The button's visible text label.
--- @field link? string|'no' The target wikilink for the button.
--- @field url? string The target external URL for the button.
--- @field icon? string The name of the icon to display found in CSS file.
--- @field color? 'blue'|'green'|'red'|string Legacy color parameter.
--- @field class? string Custom CSS classes for the button.
--- @field weight? 'quiet'|'normal'|'primary' The visual weight of the button.
--- @field size? 'small'|'medium'|'large' The size of the button.
--- @field action? 'progressive'|'destructive'|'default'|string The action type of the button.
--- @field disabled? boolean|'1'|string Whether the button is disabled/greyed out. `disabled` is `true` if: `link` = `'no'` or `false` or `disabled` = `'1'` or `true`.
--- @field style? string Custom inline CSS styles.
--- @field nocat? boolean|string If `true`, suppresses tracking categories.
--- @field category? string An additional category to add.
--- @field aria-label? string The ARIA label for accessibility.
--- @field arialabel? string (alias for aria-label)
--- @field aria_label? string (alias for aria-label)
--- @field [1]? string Positional argument 1 (alias for link/label).
--- @field [2]? string Positional argument 2 (alias for label).
--- @var categories? string Additional categories to add.
--- @var ariaDisabled? boolean Internal flag indicating if the button is functionally disabled for ARIA.
--- @var oldClassMatched string|boolean Internal flag for outdated classes.
--- @var isUrl boolean Whether the link is a URL.
--- @var errorText string|nil Internal string used as both an indicator of an error, and error message text.
--- @var tblClasses table Classes for the button span tag.
--- @var pageTitle mw.title Title of the current page.
--- @todo [[Module:Neturl]] [[Module:Check for unknown parameters]]

-- Dependencies.
require('strict')
local yesno = require('Module:Yesno')
-- [[Module:Yesno]] [[Module:Arguments]] [[Module:Check for unknown parameters]]
-- [[Special:Version]] must include @wikimedia/codex. [[Module:If preview]]

 
local DEFINITIONS = {
	--- Tracking category constants.
	trackingCategories = {
	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',
	unknownParams = 'Category:Pages using Module:Clickable button with unknown parameters',
	errors = 'Category:Errors reported by Module:Clickable button',
	},
	--- Parameters whos inputs are converted to lowercase, and are case-insensitive.
	lowercaseArgs = {'action', 'color', 'weight', 'size', 'icon'},
	--- Valid arguments.
    knownArgs = {
        'class', 'color', 'weight', 'size', 'icon', 'link', 'action',
        'url', 'disabled', 'label', 'aria-label', 'arialabel', 'aria_label',
        'nocat', 'category', '1', '2'
    },
	--- Preview warning text for unknown arguments.
    unknownArgsPreviewText = '<span class="error"><strong>Preview warning:</strong>'
        .. ' Using undocumented parameter(s): "_VALUE_".</span>',
	--- No ARIA-label warning text.
	noAriaLabelWarningText = '<span class="error"><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"><strong>Preview warning:'
		.. '</strong> A button label should ideally be shorter '
		.. 'than 38 characters, see [[en:Template:Clickable button/doc'
		.. '#Button label length|documentation]].'
		.. '</span>',
	baseCSS = 'Template:Clickable button/styles.css',
	iconsCSS = 'Template:Clickable button/icons.css',
	legacyClassSets = { progressive = { ['blue'] = true, ['green'] = true,
				['ui-button-green'] = true, ['ui-button-blue'] = true,
				['mw-ui-constructive'] = true, ['mw-ui-progressive'] = true,
				['progressive'] = true
			},
			destructive = { ['red'] = true, ['ui-button-red'] = true,
				['mw-ui-destructive'] = true, ['destructive'] = true
			}
		}
}
local p = {}
local gsub = mw.ustring.gsub
local lower = mw.ustring.lower

--- Creates [URI object](lua://mw.uri).
--- @see https://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#mw.uri
---	
--- Creates [URI object](lua://mw.uri) from URL.
--- Checks the URI is safe for use as a wikilink in MediaWiki.
---@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()
		return mw.uri.new(s)
	end)
	if success then
		return uri
	else
		return nil
	end
end

--- Extracts a URL from a string.
---@param extract string The full string from which the URL must be obtained.
---@return string|nil url The raw URL.
local function extractUrl(extract)
	local url = extract
	---@type string Extracted URL.
	url = gsub(url, '^([Hh]?[Tt]?[Tt]?[Pp]?[Ss]?:/*)(.+)',
		'https://%2')
	---@type mw.uri|nil
	local uri = safeUri(url);
	if uri and uri.host then
		return url
	end
	return nil
end

--- Parses the `url`. The `url` parameter is required. `text` label is
--- optional and can be generated from the `url`.
---@param url string|nil The URL
---@param text? string|nil The display label of the wikilink
---@return string|nil url The URL
---@return string|nil text The display label of the wikilink
local function _url(url, text)
	---@type string URL with trailing whitespace removed
	url = mw.text.trim(url or '')
	text = mw.text.trim(text or '')

	if url == '' then
		return '', 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')
	----url = gsub(url, '^([Hh]?[Tt]?[Tt]?[Pp]?[Ss]?:/*)(.+)',
	----	'https://%2')

	---@type mw.uri|nil
	local uri = safeUri(url)

	-- Handle URL's without a protocol or 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 == '' then
		if uri then
			if uri.path == '/' then
				uri.path = ''
			end
			local port = ''
			if uri.port then
				port = ':' .. uri.port
			end
			text = lower(uri.host or '') .. port .. (uri.relativePath or '')
			-- 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

--- Cleans and normalises a URL string.
---
---- Encodes `url`.
---- Removes empty query strings and fragement IDs.
---- Fixes the protocol and the double slash that follows, i.e. `https://`
---- Handles URLs that have no protocol or are protocol-relative.
---- Generates label from the URL if one is not given.
---@param url string The raw URL to clean.
---@param text string Optional link display text.
---@return string|nil localUrl Cleaned URL for wikilink.
---@return string|nil text Display label for wikilink.
---[!] @deprecated `mw.uri` class deprecated in MW 1.43 for native browser `URL`.
function p.url(url, text)
	local localUrl = url
	localUrl = localUrl or extractUrl(localUrl) or extractUrl(text) or ''
	-- Strip out HTML tags and [ ] from URL
	localUrl = (localUrl or ''):gsub("<[^>]*>", ""):gsub("[%[%]]", "")
	-- Truncate anything after a space
	localUrl = localUrl:gsub("%%20", " "):gsub(" .*", "")
	return _url(localUrl, text)
end

--[[
local function netUrl(url, text)
	local localUrl = url or ''
	local localText = text or ''
	local parsedUrl = nil
	local moduleNetUrl = require('Module:Neturl')
	if not localUrl and not localUrl ~= '' then return nil, nil end
	if text ~= '' then
		parsedUrl = moduleNetUrl:parse(localUrl)
		localText = tostring(parsedUrl.host) .. tostring(parsedUrl.path)
		localUrl = tostring(parsedUrl:normalize())
	else
		localUrl = tostring(moduleNetUrl:parse(localUrl):normalize())
	end
	return localUrl, localText
end 
]]

--- Helper function for tracking categories.
---- Checks for unknown parameter use.
---- Validates given arguments.
---- Categorizes accordingly.
---@param data args Arguments table.
---@return args data Arguments table.
---@return string categories Category wikitext.
---@return mw.title pageTitle Current page title.
local function renderTrackingCategories(data, oldClassMatched)
	local categories = ''
	local category = data.category or ''
	local class = type(data.class) == 'string' and lower(data.class) or ''
	-- local checkForUnknowns = require("Module:Check for unknown parameters")._check
	local pageTitle = mw.title.getCurrentTitle()

	--- Don't add categories if `nocat=true`, but still add any custom category.
	--- Custom category passed in.
	if category and category ~= '' then
		local s = category
		s = s:gsub('%[', ''):gsub('%]', ''):gsub('[Cc]ategory:', '')
		categories = string.format(' [[Category:%s]]', s)
	end
	if yesno(data.nocat) == true then
		return data, categories, pageTitle
	end

--[=[ 	local unknownText = string.format('[[Category:%s]]',
		DEFINITIONS.trackingCategories.unknownParams, pageTitle.fullText)
 	local unknownParams = checkForUnknowns({
		checkpositional = 'y', unknown = unknownText,
		preview = DEFINITIONS.unknownArgsPreviewText, ignoreblank = 'y',
		'class', 'color', 'weight', 'size', 'icon', 'link', 'action',
		'url', 'disabled', 'label', 'aria-label', 'arialabel', 'aria_label',
		'nocat', 'category', '1', '2'
		}, data
	)

	if unknownParams ~= '' then
		categories = string.format('%s %s', categories, unknownParams)
	end ]=]

	--[=[
	categories = categories .. check_for_unknown_parameters({
		checkpositional = "y",
		ignoreblank = "y",
		regexp1 = "header[%d]+",
		regexp2 = "label[%d]+",
		regexp3 = "data[%d]+[abc]?",
		regexp4 = "class[%d]+[abc]?",
		regexp5 = "rowclass[%d]+",
		regexp6 = "rowstyle[%d]+",
		regexp7 = "rowcellstyle[%d]+",
		unknown = "[[Category:Pages using infobox3cols with undocumented parameters|_VALUE_" .. title.text .. "]]",
		'class', 'color', 'weight', 'size', 'icon', 'link', 'url', 'disabled',
		'label', 'aria-label', 'arialabel', 'aria_label', 'action', 'nocat',
		'category', '1', '2'
		}, data) ]=]

	--- 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 data.link
			or yesno(data.link) == false) -- Checks for falsy or `link == 'no'`
			and not data.url
			and not data.disabled
		then
			categories = string.format('%s [[%s]]', categories,
			DEFINITIONS.trackingCategories.dummyButton)
		end
		---Disabled button is:
		--- - Greyed out (`data.disabled == true`)
		if data.disabled then
			categories = string.format('%s [[%s]]', categories,
			DEFINITIONS.trackingCategories.disabledButton)
		end

		if class and oldClassMatched then
			categories = string.format('%s [[%s]]', categories,
			DEFINITIONS.trackingCategories.outdatedClasses)
		end
		if data.url then
			categories = string.format('%s [[%s]]', categories,
			DEFINITIONS.trackingCategories.externalLinks)
		end
	end
	return data, categories, pageTitle
end

--- Renders the wikitext span tags for the button.
--- @param data args table Arguments table.
--- @param iconSpan mw.html 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 Error message for the button, if needed.
--- @return string link Wikitext span tags for the button.
local function renderLink(data, iconSpan, isUrl, ariaDisabled, categories, errorText, tblClasses)
	---@class mw.html: table MediaWiki DOM document content model based on HTML and RDFa.
	---@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')

	for _, aClass in ipairs(tblClasses or {}) do
			displaySpan:addClass(aClass)
	end
	--- ARIA role and label attributes for button.
	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 then
		-- ARIA disabled attribute for disabled buttons
		displaySpan:attr('aria-disabled', 'true')
		link = string.format('%s %s', tostring(displaySpan), categories)
	elseif ariaDisabled then
		-- ARIA disabled attribute for no-link/dummy buttons
		displaySpan:attr('aria-disabled', 'true')
		link = string.format('%s %s', tostring(displaySpan), categories)
	else
		displaySpan:attr('aria-disabled', 'false')
		if isUrl then
			link = string.format('<span class="plainlinks">[%s %s]</span> %s',
				data.url, tostring(displaySpan), categories)
		elseif isUrl == false then
			link = string.format('[[%s|%s]] %s', data.link, tostring(displaySpan),
				categories)
		else-- `isUrl` should be `nil` to get here.
			-- Dummy/disabled button
			link = string.format('%s %s', tostring(displaySpan), categories)
		end
	end

	if errorText then
		--- Generate error message when viewed in preview mode of an edit.
		--- Categorise into [[Category:Errors reported by Module:Clickable button]]
		---@class ifPreview
		---@field main function
		---@type ifPreview Module checks if previewing an edit.
		local ifPreview = require('Module:If preview')
		if yesno(data.nocat) == false then -- Don't add category if `nocat=true`
			link = string.format('%s [[%s]]', link, DEFINITIONS.trackingCategories.errors)
		end -- Add error message to the link if viewing in preview mode.
		return ifPreview.main({ errorText .. link, link })
	end

	return link
end

--- Parses arguments from old template parameters. For backward compatibility.
---@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|boolean 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, false
    end

    -- Resolve action, check against set constants.
    for actionName, set in pairs(DEFINITIONS.legacyClassSets) do
        if set[color] and not DEFINITIONS.legacyClassSets[actionName][actionValue] then
            return class, actionName, actionValue  	-- Found `color`.
        end
        if set[class] and not DEFINITIONS.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, '', false
end

--- Parses the module's arguments for backward compatibility.
--- 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)
	local ariaDisabled = false
	--- 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` > `2` > `1`
	rawArgs.label = rawArgs.label or rawArgs[2] or rawArgs[1]

	---@todo Should `link == 'no'` disable dummy buttons?
	rawArgs.disabled = yesno(rawArgs.disabled) or (yesno(rawArgs.link) == false)
	--- `link` value priority: `link` > `1`
	rawArgs.link = rawArgs.link or rawArgs[1]
	if rawArgs.disabled then
		-- If `link` was `'no'`, i.e. `true`, then must
		-- not generate a link either. Clear positional `1`
		-- after assigning.
		rawArgs.link = nil
		rawArgs.url = nil
	end

	-- Remove positional rawArgs after assigning
	rawArgs[1] = nil
	rawArgs[2] = nil

	if 	(rawArgs.link and yesno(rawArgs.link) ~= false)
		or rawArgs.url then
		ariaDisabled = false
		--[=[ -- Remove `[[` and `]]` if present in wikilink and label.
		if 	rawArgs.link
			and yesno(rawArgs.link) ~= false then
			rawArgs.link = rawArgs.link:gsub('^%[%[', ''):gsub('%]%]$', '')
			rawArgs.label = rawArgs.label:gsub('^%[%[', ''):gsub('%]%]', '')
		end ]=]
	else
		--- `aria-disabled = true` if no link whatsoever, always.
		--- Make dummy button. But for accessibility,
		--- ARIA must know it won't do anything.
		--- _OPTION_ to forcefully disable dummy buttons by setting:
		--- rawArgs.disabled = true
		ariaDisabled = true
	end
	-- 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

--- Constructs the attributes for the wikitext/HTML elements.
---@param parsedArgs args Parsed arguments.
---@return args data Data, such as attributes, ready to be assembled.
---@return mw.html iconSpan
---@return boolean isUrl
---@return boolean ariaDisabled
---@return boolean hasIcon
---@return string|boolean oldClassMatched
---@return string|nil errorText
---@return table tblClasses
local function makeLinkData(parsedArgs, ariaDisabled)
	local data = {}
	local iconSpan
	local isUrl = false

	-- Decide link vs. url vs. none
	-- URL has priority over link if both provided.
	-- Make pretty URL and label based on URL if no label.
	if parsedArgs.url then
		isUrl = true
		local label
		data.url, label = p.url(parsedArgs.url, parsedArgs.label) -- netUrl(parsedArgs.url, parsedArgs.label)
		data.label = parsedArgs.label or label
	elseif parsedArgs.link then
		isUrl = false
		data.link = parsedArgs.link
		data.label = parsedArgs.label
	elseif not parsedArgs.url and not parsedArgs.link then
		data.label = parsedArgs.label -- Dummy button as has no link or url
	end

	local errorText = nil
	local hasNoLabel = not data.label and not parsedArgs.aria_label
	local isVisuallyActive = not parsedArgs.disabled and not ariaDisabled
	if hasNoLabel and isVisuallyActive then --- Error if no aria-label and no visible label
		errorText = DEFINITIONS.noAriaLabelWarningText
	end

	local tblClasses = {}
	tblClasses = { 'cdx-button', 'cdx-button--fake-button' }
	local class, action, oldClassMatched
		= checkColorAndClass(parsedArgs.color, parsedArgs.class, parsedArgs.action)
	local weight = type(parsedArgs.weight) == 'string' and parsedArgs.weight or 'normal'
	local size   = type(parsedArgs.size) == 'string' and parsedArgs.size or 'medium'
	table.insert(tblClasses, 'cdx-button--action-' .. action)
	table.insert(tblClasses, 'cdx-button--weight-' .. weight)
	table.insert(tblClasses, 'cdx-button--size-' .. size)
	if (class and class ~= '') then
		table.insert(tblClasses, class) -- Custom class.
		data.class = class
	end

	---@todo Check if current page is the target link, if so, make button darker.
	local isSamePage = false
	if not isUrl then
		if data.link == data.fullText then
			isSamePage = true
		end
	end

	data.disabled = parsedArgs.disabled
	local labelLength = (type(data.label) == 'string' and mw.ustring.len(data.label)) or 0
	if data.disabled then
		table.insert(tblClasses, 'cdx-button--fake-button--disabled')
	else
		table.insert(tblClasses, 'cdx-button--fake-button--enabled')
	end
	if data.label and labelLength > 38 then
		table.insert(tblClasses, 'cdx-button--word-wrap')
	end
	---@todo Must still actually use this in the CSS file.
	if isSamePage then
        table.insert(tblClasses, 'cdx-button--same-page')
    end

	local hasIcon = false
	if type(parsedArgs.icon) == 'string' and parsedArgs.icon then
		---@type string Name of icon for button.
		local icon = parsedArgs.icon
		hasIcon = true -- Assign to carry to final return for CSS output.
		iconSpan = mw.html.create('span')
		iconSpan:addClass('cdx-button__icon cdx-demo-css-icon--' .. icon)
		iconSpan:attr('aria-hidden', 'true')
		if not data.label then
			-- Icon-only button, add extra class for styling.
			table.insert(tblClasses, 'cdx-button--icon-only')
		end
	end

	-- Label length checks.
	if data.label then
		if labelLength > 38 then
			errorText = errorText
				and string.format('%s %s', errorText, DEFINITIONS.labelLengthWarningText)
				or DEFINITIONS.labelLengthWarningText
		elseif labelLength < 3 then
			table.insert(tblClasses, 'cdx-button--short-label')
		end
	end

	data.aria_label = parsedArgs.aria_label

	return data, iconSpan, isUrl, ariaDisabled, hasIcon,
		oldClassMatched, errorText, tblClasses
end

--- Interface for other Lua modules.
--- Function can be called by other Lua modules to generate wikitext.
--- Does not render CSS file or pre-process arguments.
--- 
---@param rawArgs args Module's arguments.
---@return string data Wikitext that renders button, without CSS file.
---@return boolean hasIcon Whether the button has an icon.
function p._main(rawArgs)
	---@type args Parsed arguments.
	local parsedArgs, ariaDisabled = parseParameters(rawArgs)

	---@type args HTML attributes with values, and contents.
	local data, iconSpan, isUrl, hasIcon, oldClassMatched, errorText, tblClasses
	data, iconSpan, isUrl, ariaDisabled, hasIcon, oldClassMatched, errorText, tblClasses
		= makeLinkData(parsedArgs, ariaDisabled)

	local categories
	data, categories = renderTrackingCategories(data, oldClassMatched)

	return renderLink(data, iconSpan, isUrl, ariaDisabled,
		categories, errorText, tblClasses), hasIcon
end

--- Interface for templates.
--- Called by the `{{#invoke: Clickable button | main }}` parser function.
---	Pre-processes arguments, inserts CSS file, and renders the button.
---
---@param frame frame Module's arguments from template invocation.
---@return string wikitextOutput Wikitext for insertion on a wiki page.
function p.main(frame)
    ---@type table<string, string> Parsed arguments.
    -- If called from wrapper, don't look for parentFrame().
    local rawArgs = require('Module:Arguments').getArgs(frame, {
            wrappers = {
                'Template:Clickable button',
                'Template:Clickable button/sandbox',
                'Template:Cdx-button', 'Template:Cdx-button/sandbox'
            }
        })

    -- Make arguments lowercase where appropriate.
    -- Except `class` as _HTML class names_ are case-sensitive.
    for _, key in ipairs(DEFINITIONS.lowercaseArgs) do
        if rawArgs[key] then
            rawArgs[key] = lower(rawArgs[key])
        end
    end

    -- Return empty string 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
			return ''
		end
	end

	local output, hasIcon = p._main(rawArgs)
	-- Insert CSS file into the output.
    -- Note: This is not the most efficient way to include CSS,
    -- but it's simple and avoids potential issues with caching.
	local outputCSS = frame:extensionTag(
		'templatestyles', '',
		{ src = DEFINITIONS.baseCSS }
	)
	if hasIcon then
		output = string.format('%s%s%s', outputCSS,
			frame:extensionTag(
				'templatestyles', '',
				{ src = DEFINITIONS.iconsCSS }
			),
		output
		)
	else
		output = string.format('%s%s', outputCSS, output)
	end

    return output
end

return p
Cookies help us deliver our services. By using our services, you agree to our use of cookies.