मोड्यूल:Clickable button/sandbox
स्वरूप
इस मॉड्यूल हेतु प्रलेख मोड्यूल:Clickable button/sandbox/doc पर बनाया जा सकता है
--- @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