Module:Common
Module:Common
Shared stateless helper functions, which can be loaded in other Lua modules via:
local common = require("Module:Common")
Usage
This module also exposes a few permanent utility entry points.
Replace tildes with another character
Tildes are good delimiters in the wiki context, as the alternatives are commas, which are common in prose, and pipes, which are special wikitext characters.
By default, tildes ~ will be replaced by pipes |:
{{#invoke:Common|replaceTilde|Item 1~Item 2~Item 3}}
Optionally, a character can be specified to be used as the replacement. For example, comma:
{{#invoke:Common|replaceTilde|Item 1~Item 2~Item 3|,}}
Render a tilde-delimited string as a bullet list
Converts a tilde-separated string into a bullet list. The list items are left-aligned (by default), while the list as a whole remains centered.
{{#invoke:Common|bulletList|Item 1~Item 2~Item 3}}
Optionally, set link=true to wrap each item as a wikilink:
{{#invoke:Common|bulletList|Item 1~Item 2~Item 3|link=true}}
Optionally, specify prefix to prepend a certain string to every item's link target:
{{#invoke:Common|bulletList|Item 1~Item 2~Item 3|link=true|prefix=ParentPage/}}
Optionally, set leftAlign=no to omit the left-alignment, allowing invoking elements to control text alignment instead:
{{#invoke:Common|bulletList|Item 1~Item 2~Item 3|leftAlign=no}}
Get URL-safe version of a given string
MessageBundle keys are built from a slugified (URL-safe) version of a string. Use this to generate the exact slug from a given input.
If no value is specified, it defaults to the current page's title:
{{#invoke:Common|toSlug}}
A specific string can also be provided instead:
{{#invoke:Common|toSlug|Terrarium of Muad'Dib}}
Get an auto-localised string from a MessageBundle
Retrieves a string for a given key from a specified MessageBundle. It auto-switches to the language that the viewer's page is currently on, and falls back to the English source if no translation exists for the current language.
{{#invoke:Common|localise|Vehicles|sandbike-name}}
Optionally, set link=true to wrap the result as a link to the page matching the key's English value (translated subpage if one exists for the viewer's language), e.g. [[Wildlife of Arrakis|Fauna von Arrakis]]:
{{#invoke:Common|localise|DLC|wildlife-of-arrakis-name|link=true}}
If the link target differs from the localised string itself (e.g. a label whose page title doesn't match the label text), specify the target page title directly instead of true. This also resolves to a translated subpage where available:
{{#invoke:Common|localise|Labels|vehicle_backup|link=Vehicle Backup Tool}}
Parameters
replaceTilde
- 1
- string
- The string containing tildes
~to replace.
- 2 (optional)
- string
- The character to replace the tildes with.
- Defaults to pipe
|.
bulletList
- 1
- string
- The tilde-separated string of items to convert into a bullet list.
- link (optional)
- boolean
- If specified with a truthy value, each item will be wrapped in double square brackets, rendering it as a wikilink.
- Valid values:
trueyes1
- prefix (optional)
- string
- Only functions when
linkis truthy. - If specified, prepends the string to every item's link target.
- leftAlign (optional)
- boolean
- If set to a falsy value, the inline text-align:left style is omitted from the list wrapper, so the caller's own CSS can control alignment instead.
- Defaults to true.
toSlug
- 1 (optional)
- string
- The string to generate a slug for.
- If omitted, defaults to the current page's title.
localise
- 1
- string
- The name of the MessageBundle to look up (without the "MessageBundle:" prefix).
- 2
- string
- The key to retrieve from the MessageBundle.
- link (optional)
- boolean or string
- If
true, wraps the result as a link to the page matching the key's English value, e.g.[[English|Translation]]. - If set to a page title instead, links to that page rather than the key's English value. Use this when the link target differs from the localised string itself.
- In both cases, the link resolves to a translated subpage of the target (e.g.
Vehicle Backup Tool/de) matching the language of the current page, falling back to English if no translated subpage exists. - Defaults to false.
local p = {}
local tmb = require("mw.ext.translate.messageBundle")
-- ---------- Processing ----------
-- trims any whitespace on the edges
function p.trim(s)
return mw.text.trim(s or "")
end
-- converts string to lowercase and trims any whitespace on the edges
function p.lowerTrim(s)
return mw.ustring.lower(p.trim(s))
end
-- checks whether a string represents a truthy value; case-insensitive
function p.isTruthy(s)
s = p.lowerTrim(s)
return s == "true" or s == "yes" or s == "1"
end
-- checks whether a string represents a falsy value; case-insensitive
function p.isFalsy(s)
s = p.lowerTrim(s)
return s == "false" or s == "no" or s == "0"
end
-- checks whether boolean input was specified and returns corresponding value, else nil
function p.resolveBooleanInput(s)
if p.isTruthy(s) then return true
elseif p.isFalsy(s) then return false
else return nil end
end
-- replaces all occurrences of one character within a string with another character
function p.replaceChar(s, a, b)
s = s:gsub(a, b)
return s
end
-- splits a tilde-delimited string into trimmed, non-empty items
function p.splitTilde(s)
local items = {}
for item in mw.text.gsplit(s or "", "~") do
item = p.trim(item)
if item ~= "" then items[#items + 1] = item end
end
return items
end
-- checks whether a list of strings contains a specified token; case-insensitive
function p.listContains(list, token)
token = p.lowerTrim(token)
for _, item in ipairs(list) do
if p.lowerTrim(item) == token then return true end
end
return false
end
-- returns a URL-friendly version of a specified string
function p.slugify(name)
local s = mw.uri.decode(name, "PATH") -- decode percent-encoding
s = mw.text.decode(s) -- decode html entities
s = mw.ustring.lower(s) -- lowercase all letters
s = mw.ustring.gsub(s, "[^%w%s_]", "") -- remove all characters except alphanumerics, underscores and whitespace
s = mw.ustring.gsub(s, "%s+", "_") -- convert each group of whitespace into a single underscore
s = mw.ustring.gsub(s, "_+", "_") -- combine consecutive underscores into one
s = mw.ustring.gsub(s, "^_+", ""):gsub("_+$", "") -- trim any underscores at the edges
return s
end
-- splits a "Bundle:slug" source token into its parts
function p.parseSourceToken(token)
token = p.trim(token)
if token == "" then return nil end
return token:match("^([^:]+):(.+)$")
end
-- checks whether a source field (either a single token or a table of tokens) contains a given slug, ignoring the bundle prefix
function p.sourceMatches(source, filterSlug)
if type(source) ~= "table" then
local _, slug = p.parseSourceToken(source)
return slug == filterSlug
end
-- recurse if table
for _, token in ipairs(source) do
if p.sourceMatches(token, filterSlug) then return true end
end
return false
end
-- splits any trailing number `_<number>` off a slug/ID
-- returns base_slug, number if matched, else nil
function p.splitTrailingNumber(str)
local base, num = str:match("^(.-)_(%d+)$")
if not base then
return nil
end
return base, tonumber(num)
end
-- returns a display-ready error message, standardised for input validation checks across all modules
-- any number of extra args can be specified to be encoded and substituted into msg
function p.inputError(msg, ...)
if select("#", ...) > 0 then
local args = {...}
for i = 1, #args do args[i] = mw.text.encode(tostring(args[i])) end
msg = string.format(msg, unpack(args))
end
return '<strong class="error">⚠ ' .. msg .. '</strong>'
end
-- ---------- Localisation ----------
local pageLang
local bundleCache = {}
local subpageCache = {}
local function getBundle(bundlePage, lang)
local cacheKey = bundlePage .. "|" .. tostring(lang)
if bundleCache[cacheKey] == nil then
-- if `lang` is nil, tmb.new() automatically attempts to grab a string of the invoking page's language
-- if a translation does not exist, it falls back to the English source
local ok, mb = pcall(tmb.new, bundlePage, lang)
bundleCache[cacheKey] = ok and mb or false
end
return bundleCache[cacheKey]
end
-- returns current page's language
function p.getPageLanguage()
if not pageLang then
local ok, code = pcall(mw.getCurrentFrame().preprocess, mw.getCurrentFrame(), "{{PAGELANGUAGE}}")
pageLang = mw.language.new(ok and code or mw.language.getContentLanguage():getCode())
end
return pageLang
end
-- formats a number according to the current page's language
function p.formatNum(number)
return p.getPageLanguage():formatNum(number)
end
-- checks if a translation exists for a target page
-- returns the language subpage path (<target>/<lang>) if it exists, else just <target>
function p.resolveLangSubpage(target, lang)
lang = lang or p.getPageLanguage():getCode()
if lang == "en" then return target end
local subpagePath = target .. "/" .. lang
local cached = subpageCache[subpagePath]
if cached ~= nil then return cached end
local subpage = mw.title.new(subpagePath)
local resolved = (subpage and subpage.exists) and subpagePath or target
subpageCache[subpagePath] = resolved
return resolved
end
-- retrieves a string value associated with a specified key from a specified MessageBundle
-- optionally returns result as a wikilink if `link` is true
function p.translate(bundleName, key, lang, link, silent)
local bundlePage = "MessageBundle:" .. bundleName
local mb = getBundle(bundlePage, lang)
if not mb then
if silent then return nil end
return p.inputError("Unknown MessageBundle:%s", bundleName)
end
local msg = mb:t(key)
if not msg then
if silent then return nil end
error(string.format("Missing key '%s' in MessageBundle:%s", key, bundleName))
end
local translated = msg:plain()
if not link then return translated end
-- link is now either literally true, or a string
local target
-- page title string
if link ~= true then
target = p.resolveLangSubpage(link, lang)
-- true
else
local mbEn = getBundle(bundlePage, "en")
local msgEn = mbEn and mbEn:t(key)
if not msgEn then
if silent then return nil end
error(string.format("Missing English value for key '%s' in MessageBundle:%s", key, bundleName))
end
target = p.resolveLangSubpage(msgEn:plain(), lang)
end
return string.format("[[%s|%s]]", target, translated)
end
-- ---------- Rendering ----------
-- keys are MessageBundles where the strings should be anchors instead of titles
-- values are the pages the anchors will be found on
local BUNDLE_REDIRECT_PAGE = {
Rewards = "Rewards",
}
-- creates a collapsible section: a heading followed by an inner content div
-- an item count is shown above the collapse toggle, and is always visible regardless of collapse state
-- mw-collapsed sections can be auto-expanded with wikilinks that target the heading ID
-- subtitle, if specified, renders above the item count
function p.newCollapsibleSection(title, count, headingLevel, expandedRaw, subtitle, anchorId)
local expanded = p.resolveBooleanInput(expandedRaw)
local outer = mw.html.create("div")
:addClass("mw-collapsible")
-- collapse the section if it contains more than 6 items (2 rows of 3 infoboxes)
-- or if expanded is specified as false
if (count > 6 and expanded == nil) or expanded == false then
outer:addClass("mw-collapsed")
end
local level = tonumber(headingLevel)
if not level or level < 0 or level > 6 then level = 3 end
-- omit heading if headingLevel was specified as 0
if level ~= 0 then
local heading = outer:tag("h" .. level):wikitext(title)
if anchorId then heading:attr("id", anchorId) end
end
if subtitle and subtitle ~= "" then
outer:tag("div")
:addClass("collapsible-subtitle")
:addClass("scalable-text")
:wikitext(subtitle)
end
outer:tag("div")
:addClass("collapsible-count")
:addClass("scalable-text")
:wikitext(string.format("Total: '''%d'''", count))
local content = outer:tag("div")
:addClass("mw-collapsible-content")
return outer, content
end
-- creates a standard sortable wikitable, ready for header/rows to be added
function p.newSortableTable()
local table_ = mw.html.create("table")
:addClass("wikitable sortable")
:css("text-align", "center")
return table_
end
-- creates a left-aligned bullet list, one <li> per item
-- if omitIfSingle is true and there is only 1 item in the list, then a plain span will be returned
-- if leftAlign is false, text-align:left style is omitted
function p.renderBulletList(items, omitIfSingle, leftAlign)
if omitIfSingle and #items == 1 then
return mw.html.create("span"):wikitext(items[1])
end
local wrapper = mw.html.create("div")
:css("display", "inline-block")
if leftAlign ~= false then
wrapper:css("text-align", "left")
end
local ul = wrapper:tag("ul")
for _, item in ipairs(items) do
ul:tag("li"):wikitext(item)
end
return wrapper
end
-- resolves one or more source tokens into a display string
-- token may be a single "Bundle:slug" string, or a table of such strings (for items with multiple sources)
-- single source renders as a plain line; multiple sources render as a bullet list
function p.renderSource(token)
local function keyFor(part)
return part .. "-name"
end
if type(token) == "table" then
if #token == 0 then return p.inputError("No source specified") end
if #token == 1 then return p.renderSource(token[1]) end
local rendered = {}
for i, t in ipairs(token) do
rendered[i] = p.renderSource(t)
end
return tostring(p.renderBulletList(rendered))
end
token = p.trim(token)
if token == "" then return p.inputError("No source specified") end
local bundleName, slug = p.parseSourceToken(token)
if not bundleName then return p.inputError("Malformed source token: %s", token) end
if not slug:find("/", 1, true) then
-- prevent Sources:Default label from becoming a wikilink
if bundleName == "Sources" and slug == "default" then
return p.translate(bundleName, keyFor(slug), nil, false)
end
-- process strings from certain MessageBundles as page anchors instead of page titles
local redirectPage = BUNDLE_REDIRECT_PAGE[bundleName]
if redirectPage then
local translated = p.translate(bundleName, keyFor(slug), nil, false)
local anchorText = p.translate(bundleName, keyFor(slug), "en", false, true)
return string.format("[[%s#%s|%s]]", redirectPage, anchorText, translated)
end
return p.translate(bundleName, keyFor(slug), nil, true)
end
-- handle contract pages (i.e. [[ContractChain/Contract|Contract]])
local parts = mw.text.split(slug, "/", true)
local targetParts = {}
for i, part in ipairs(parts) do
local resolved = p.translate(bundleName, keyFor(part), "en", false, true)
if not resolved then
return p.inputError("Missing key '%s' in MessageBundle:%s", keyFor(part), bundleName)
end
targetParts[i] = resolved
end
local displayText = p.translate(bundleName, keyFor(parts[#parts]))
return string.format("[[%s|%s]]", table.concat(targetParts, "/"), displayText)
end
-- renders a version string as a wikilink to its corresponding Version page
function p.renderVersion(version)
version = p.trim(version)
if version == "" then return p.inputError("No version specified") end
return "[[" .. version .. "]]"
end
-- ---------- Interface ----------
-- wrapper for p.replaceChar(), exposed as an entry point to replace all tildes in a string with another character (defaults to pipe)
function p.replaceTilde(frame)
local args = frame.args
local input = p.trim(args[1])
if input == "" then return "" end
local replacement = p.trim(args[2])
if replacement == "" then replacement = "|" end
return p.replaceChar(input, "~", replacement)
end
-- wrapper for p.renderBulletList(), exposed as an entry point to render a tilde-delimited string as a bullet list
function p.bulletList(frame)
local args = frame.args
local content = args[1]
if not content or content == "" then return "" end
local items = p.splitTilde(content)
if p.isTruthy(args.link) then
local prefix = args.prefix or ""
for i, item in ipairs(items) do
if prefix ~= "" then
items[i] = "[[" .. prefix .. item .. "|" .. item .. "]]"
else
items[i] = "[[" .. item .. "]]"
end
end
end
local leftAlign = not p.isFalsy(args.leftAlign)
return tostring(p.renderBulletList(items, nil, leftAlign))
end
-- wrapper for p.slugify(), exposed as an entry point to check URL-friendly versions of specified strings
-- this is specifically intended to help in cases where it may not be clear what IDs to use in MessageBundles
function p.toSlug(frame)
local input = frame.args[1]
if not input or input == "" then input = mw.title.getCurrentTitle().rootText end
return p.slugify(input)
end
-- wrapper for p.translate(), exposed as an entry point to grab strings from MessageBundles directly
function p.localise(frame)
local args = frame.args
-- process link input; can either be boolean or a string
local linkArg = p.trim(args.link or "")
local link = false
if p.isTruthy(linkArg) then
link = true
elseif linkArg ~= "" then
link = linkArg
end
return p.translate(p.trim(args[1]), p.trim(args[2]), nil, link)
end
return p