Module:Hatnote: Difference between revisions
Jump to navigation
Jump to search
No edit summary |
No edit summary |
||
(One intermediate revision by the same user not shown) | |||
Line 1: | Line 1: | ||
-- This Module is | -- <nowiki> | ||
-- | -------------------------------------------------------------------------------- | ||
-- | -- Module:Hatnote -- | ||
-- | -- -- | ||
local | -- This module produces hatnote links and links to related articles. It -- | ||
-- | -- implements the {{hatnote}} and {{format link}} meta-templates and includes -- | ||
-- helper functions for other Lua hatnote modules. -- | |||
-- | -------------------------------------------------------------------------------- | ||
return | |||
local libraryUtil = require('libraryUtil') | |||
local checkType = libraryUtil.checkType | |||
local mArguments = require('Module:Arguments') | |||
local yesno = require('Module:Yesno') | |||
local mTableTools = require('Module:TableTools') | |||
local i18n = require('Module:I18n').loadMessages('Hatnote') | |||
local hatnote = {} | |||
-------------------------------------------------------------------------------- | |||
-- Helper functions | |||
-------------------------------------------------------------------------------- | |||
local function getArgs(frame) | |||
-- Fetches the arguments from the parent frame. Whitespace is trimmed and | |||
-- blanks are removed. | |||
return mArguments.getArgs(frame, {parentOnly = true}) | |||
end | |||
local function removeInitialColon(s) | |||
-- Removes the initial colon from a string, if present. | |||
return s:match('^:?(.*)') | |||
end | |||
function hatnote.findNamespaceId(link, removeColon) | |||
-- Finds the namespace id (namespace number) of a link or a pagename. This | |||
-- function will not work if the link is enclosed in double brackets. Colons | |||
-- are trimmed from the start of the link by default. To skip colon | |||
-- trimming, set the removeColon parameter to false. | |||
checkType('findNamespaceId', 1, link, 'string') | |||
checkType('findNamespaceId', 2, removeColon, 'boolean', true) | |||
if removeColon ~= false then | |||
link = removeInitialColon(link) | |||
end | |||
local namespace = link:match('^(.-):') | |||
if namespace then | |||
local nsTable = mw.site.namespaces[namespace] | |||
if nsTable then | |||
return nsTable.id | |||
end | |||
end | |||
return 0 | |||
end | |||
function hatnote.formatPages(...) | |||
-- Formats a list of pages using formatLink and returns it as an array. Nil | |||
-- values are not allowed. | |||
local pages = {...} | |||
local ret = {} | |||
for i, page in ipairs(pages) do | |||
ret[i] = hatnote._formatLink(page) | |||
end | |||
return ret | |||
end | |||
function hatnote.formatPageTables(...) | |||
-- Takes a list of page/display tables and returns it as a list of | |||
-- formatted links. Nil values are not allowed. | |||
local pages = {...} | |||
local links = {} | |||
for i, t in ipairs(pages) do | |||
checkType('formatPageTables', i, t, 'table') | |||
local link = t[1] | |||
local display = t[2] | |||
links[i] = hatnote._formatLink(link, display) | |||
end | |||
return links | |||
end | |||
function hatnote.makeWikitextError(msg, helpLink, addTrackingCategory, title) | |||
-- Formats an error message to be returned to wikitext. If | |||
-- addTrackingCategory is not false after being returned from | |||
-- [[Module:Yesno]], and if we are not on a talk page, a tracking category | |||
-- is added. | |||
checkType('makeWikitextError', 1, msg, 'string') | |||
checkType('makeWikitextError', 2, helpLink, 'string', true) | |||
title = title or mw.title.getCurrentTitle() | |||
-- Make the help link text. | |||
local helpText | |||
if helpLink then | |||
helpText = ' ([[' .. helpLink .. '|' .. i18n:msg('help') .. ']])' | |||
else | |||
helpText = '' | |||
end | |||
-- Make the category text. | |||
local category | |||
if not title.isTalkPage and yesno(addTrackingCategory) ~= false then | |||
category = i18n:msg('cat-errors') | |||
category = string.format( | |||
'[[%s:%s]]', | |||
mw.site.namespaces[14].name, | |||
category | |||
) | |||
else | |||
category = '' | |||
end | |||
return string.format( | |||
i18n:msg('error'), | |||
msg, | |||
helpText, | |||
category | |||
) | |||
end | |||
function hatnote.disambiguate(page, disambiguator) | |||
-- Formats a page title with a disambiguation parenthetical, | |||
-- i.e. "Example" → "Example (disambiguation)". | |||
checkType('disambiguate', 1, page, 'string') | |||
checkType('disambiguate', 2, disambiguator, 'string', true) | |||
disambiguator = disambiguator or i18n:msg('disambiguation') | |||
return string.format(i18n:msg('brackets'), page, disambiguator) | |||
end | |||
-------------------------------------------------------------------------------- | |||
-- Format link | |||
-- | |||
-- Makes a wikilink from the given link and display values. Links are escaped | |||
-- with colons if necessary, and links to sections are detected and displayed | |||
-- with " § " as a separator rather than the standard MediaWiki "#". Used in | |||
-- the {{format link}} template. | |||
-------------------------------------------------------------------------------- | |||
function hatnote.formatLink(frame) | |||
local args = getArgs(frame) | |||
local link = args[1] | |||
local display = args[2] | |||
if not link then | |||
return hatnote.makeWikitextError( | |||
i18n:msg('error-link'), | |||
'w:c:Module:Template:Format link#Errors',-- there is no actual docs for this. not even on wikipedia | |||
args.category | |||
) | |||
end | |||
return hatnote._formatLink(link, display) | |||
end | |||
function hatnote._formatLink(link, display) | |||
checkType('_formatLink', 1, link, 'string') | |||
checkType('_formatLink', 2, display, 'string', true) | |||
-- Remove the initial colon for links where it was specified manually. | |||
link = removeInitialColon(link) | |||
-- Find whether a faux display value has been added with the {{!}} magic | |||
-- word. | |||
if not display then | |||
local prePipe, postPipe = link:match('^(.-)|(.*)$') | |||
link = prePipe or link | |||
display = postPipe | |||
end | |||
-- Find the display value. | |||
if not display then | |||
local page, section = link:match('^(.-)#(.*)$') | |||
if page then | |||
display = page .. ' § ' .. section | |||
end | |||
end | |||
-- Assemble the link. | |||
if display then | |||
return string.format( | |||
'[[:%s|%s]]', | |||
string.gsub(link, '|(.*)$', ''), --display overwrites manual piping | |||
display | |||
) | |||
else | |||
return string.format('[[:%s]]', link) | |||
end | |||
end | |||
-------------------------------------------------------------------------------- | |||
-- Hatnote | |||
-- | |||
-- Produces standard hatnote text. Implements the {{hatnote}} template. | |||
-------------------------------------------------------------------------------- | |||
function hatnote.hatnote(frame) | |||
local args = getArgs(frame) | |||
local s = args[1] | |||
local options = {} | |||
if not s then | |||
return hatnote.makeWikitextError( | |||
i18n:msg('error-text'), | |||
'w:c:Module:Template:Hatnote#Errors', | |||
args.category | |||
) | |||
end | |||
options.extraclasses = args.extraclasses | |||
options.selfref = args.selfref | |||
return hatnote._hatnote(s, options) | |||
end | |||
function hatnote._hatnote(s, options) | |||
checkType('_hatnote', 1, s, 'string') | |||
checkType('_hatnote', 2, options, 'table', true) | |||
options = options or {} | |||
local classes = {'notice', 'hatnote'} | |||
local extraclasses = options.extraclasses | |||
local selfref = options.selfref | |||
if type(extraclasses) == 'string' then | |||
classes[#classes + 1] = extraclasses | |||
end | |||
if selfref then | |||
classes[#classes + 1] = 'selfref' | |||
end | |||
return string.format( | |||
'<div role="note" class="%s">%s</div>', | |||
table.concat(classes, ' '), | |||
s | |||
) | |||
end | |||
-------------------------------------------------------------------------------- | |||
-- Module:Hatnote list -- | |||
-- -- | |||
-- This module produces and formats lists for use in hatnotes. In particular, -- | |||
-- it implements the for-see list, i.e. lists of "For X, see Y" statements, -- | |||
-- as used in {{about}}, and its variants. Also introduced are andList & -- | |||
-- orList helpers for formatting lists with those conjunctions. -- | |||
-------------------------------------------------------------------------------- | |||
-------------------------------------------------------------------------------- | |||
-- List stringification helper functions | |||
-- | |||
-- These functions are used for stringifying lists, usually page lists inside | |||
-- the "Y" portion of "For X, see Y" for-see items. | |||
-------------------------------------------------------------------------------- | |||
--default options table used across the list stringification functions | |||
local stringifyListDefaultOptions = { | |||
conjunction = i18n:msg('conjunction'), | |||
separator = i18n:msg('separator'), | |||
altSeparator = i18n:msg('altSeparator'), | |||
space = i18n:msg('space'), | |||
formatted = false | |||
} | |||
-- Stringifies a list generically; probably shouldn't be used directly | |||
function stringifyList(list, options) | |||
-- Type-checks, defaults, and a shortcut | |||
checkType("stringifyList", 1, list, "table") | |||
if #list == 0 then return nil end | |||
checkType("stringifyList", 2, options, "table", true) | |||
options = options or {} | |||
for k, v in pairs(stringifyListDefaultOptions) do | |||
if options[k] == nil then options[k] = v end | |||
end | |||
local s = options.space | |||
-- Format the list if requested | |||
if options.formatted then list = hatnote.formatPages(unpack(list)) end | |||
-- Set the separator; if any item contains it, use the alternate separator | |||
local separator = options.separator | |||
--searches display text only | |||
function searchDisp(t, f) | |||
return string.find(string.sub(t, (string.find(t, '|') or 0) + 1), f) | |||
end | |||
for k, v in pairs(list) do | |||
if searchDisp(v, separator) then | |||
separator = options.altSeparator | |||
break | |||
end | |||
end | |||
-- Set the conjunction, apply Oxford comma, and force a comma if #1 has "§" | |||
local oxfordLangs = { | |||
-- list of languages that does respect oxford commas | |||
['en'] = true, | |||
['en-us'] = true, | |||
['en-gb'] = true, | |||
} | |||
local conjunction = s .. options.conjunction .. s | |||
if #list == 2 and searchDisp(list[1], "§") or #list > 2 then | |||
conjunction = (oxfordLangs[i18n.defaultLang] and separator or '') .. conjunction | |||
end | |||
-- Return the formatted string | |||
return mw.text.listToText(list, separator .. s, conjunction) | |||
end | |||
--DRY function | |||
function conjList (conj, list, fmt) | |||
return stringifyList(list, {conjunction = conj, formatted = fmt}) | |||
end | |||
-- Stringifies lists with "and" or "or" | |||
function hatnote.andList (...) return conjList(i18n:msg('conj-and'), ...) end | |||
function hatnote.orList (...) return conjList(i18n:msg('conj-or'), ...) end | |||
-------------------------------------------------------------------------------- | |||
-- For see | |||
-- | |||
-- Makes a "For X, see [[Y]]." list from raw parameters. Intended for the | |||
-- {{about}} templates and their variants. | |||
-------------------------------------------------------------------------------- | |||
--default options table used across the forSee family of functions | |||
local forSeeDefaultOptions = { | |||
andKeyword = i18n:msg('conj-and'), | |||
title = mw.title.getCurrentTitle().text, | |||
otherText = i18n:msg('other-uses'), | |||
forSeeForm = i18n:msg('for') | |||
} | |||
--Collapses duplicate punctuation | |||
function punctuationCollapse (text) | |||
local replacements = { | |||
["%.%.$"] = ".", | |||
["%?%.$"] = "?", | |||
["%!%.$"] = "!", | |||
["%.%]%]%.$"] = ".]]", | |||
["%?%]%]%.$"] = "?]]", | |||
["%!%]%]%.$"] = "!]]" | |||
} | |||
for k, v in pairs(replacements) do text = string.gsub(text, k, v) end | |||
return text | |||
end | |||
-- Structures arguments into a table for stringification, & options | |||
function hatnote.forSeeArgsToTable (args, from, options) | |||
-- Type-checks and defaults | |||
checkType("forSeeArgsToTable", 1, args, 'table') | |||
checkType("forSeeArgsToTable", 2, from, 'number', true) | |||
from = from or 1 | |||
checkType("forSeeArgsToTable", 3, options, 'table', true) | |||
options = options or {} | |||
for k, v in pairs(forSeeDefaultOptions) do | |||
if options[k] == nil then options[k] = v end | |||
end | |||
-- maxArg's gotten manually because getArgs() and table.maxn aren't friends | |||
local maxArg = 0 | |||
for k, v in pairs(args) do | |||
if type(k) == 'number' and k > maxArg then maxArg = k end | |||
end | |||
-- Structure the data out from the parameter list: | |||
-- * forTable is the wrapper table, with forRow rows | |||
-- * Rows are tables of a "use" string & a "pages" table of pagename strings | |||
-- * Blanks are left empty for defaulting elsewhere, but can terminate list | |||
local forTable = {} | |||
local i = from | |||
local terminated = false | |||
-- Loop to generate rows | |||
repeat | |||
-- New empty row | |||
local forRow = {} | |||
-- On blank use, assume list's ended & break at end of this loop | |||
forRow.use = args[i] | |||
if not args[i] then terminated = true end | |||
-- New empty list of pages | |||
forRow.pages = {} | |||
-- Insert first pages item if present | |||
table.insert(forRow.pages, args[i + 1]) | |||
-- If the param after next is "and", do inner loop to collect params | |||
-- until the "and"'s stop. Blanks are ignored: "1|and||and|3" → {1, 3} | |||
while args[i + 2] == options.andKeyword do | |||
if args[i + 3] then | |||
table.insert(forRow.pages, args[i + 3]) | |||
end | |||
-- Increment to next "and" | |||
i = i + 2 | |||
end | |||
-- Increment to next use | |||
i = i + 2 | |||
-- Append the row | |||
table.insert(forTable, forRow) | |||
until terminated or i > maxArg | |||
return forTable | |||
end | |||
-- Stringifies a table as formatted by forSeeArgsToTable | |||
function hatnote.forSeeTableToString (forSeeTable, options) | |||
-- Type-checks and defaults | |||
checkType("forSeeTableToString", 1, forSeeTable, "table") | |||
checkType("forSeeTableToString", 2, options, "table", true) | |||
options = options or {} | |||
for k, v in pairs(forSeeDefaultOptions) do | |||
if options[k] == nil then options[k] = v end | |||
end | |||
-- Stringify each for-see item into a list | |||
local strList = {} | |||
for k, v in pairs(forSeeTable) do | |||
local useStr = v.use or options.otherText | |||
local pagesStr = hatnote.andList(v.pages, true) or | |||
hatnote._formatLink(hatnote.disambiguate(options.title)) | |||
local forSeeStr = string.format(options.forSeeForm, useStr, pagesStr) | |||
forSeeStr = punctuationCollapse(forSeeStr) | |||
table.insert(strList, forSeeStr) | |||
end | |||
-- Return the concatenated list | |||
return table.concat(strList, ' ') | |||
end | |||
-- Produces a "For X, see [[Y]]" string from arguments. Expects index gaps | |||
-- but not blank/whitespace values. Ignores named args and args < "from". | |||
function hatnote._forSee (args, from, options) | |||
local forSeeTable = hatnote.forSeeArgsToTable(args, from, options) | |||
return hatnote.forSeeTableToString(forSeeTable, options) | |||
end | |||
-- As _forSee, but uses the frame. | |||
function hatnote.forSee (frame, from, options) | |||
return hatnote._forSee(mArguments.getArgs(frame), from, options) | |||
end | |||
-------------------------------------------------------------------------------- | |||
-- Produces a labelled pages-list hatnote. | |||
-- The main frame (template definition) takes 1 or 2 arguments, for a singular | |||
-- and (optionally) plural label respectively: | |||
-- * {{#invoke:Hatnote|labelledList|Singular label|Plural label}} | |||
-- The resulting template takes pagename & label parameters normally. | |||
-------------------------------------------------------------------------------- | |||
-- Defaults global to this module | |||
local LPLHdefaults = { | |||
label = i18n:msg('see-also'), --Final fallback for label argument | |||
labelForm = i18n:msg('colon'), | |||
prefixes = {'label', 'label ', 'l'}, | |||
template = 'Module:Hatnote' | |||
} | |||
-- Helper function that pre-combines display parameters into page arguments. | |||
-- Also compresses sparse arrays, as a desirable side-effect. | |||
function hatnote.preprocessDisplays (args, prefixes) | |||
-- Prefixes specify which parameters, in order, to check for display options | |||
-- They each have numbers auto-appended, e.g. 'label1', 'label 1', & 'l1' | |||
prefixes = prefixes or LPLHdefaults.prefixes | |||
local pages = {} | |||
for k, v in pairs(args) do | |||
if type(k) == 'number' then | |||
local display | |||
for i = 1, #prefixes do | |||
display = args[prefixes[i] .. k] | |||
if display then break end | |||
end | |||
local page = display and | |||
string.format('%s|%s', string.gsub(v, '|.*$', ''), display) or v | |||
pages[#pages + 1] = page | |||
end | |||
end | |||
return pages | |||
end | |||
function hatnote.labelledList (frame) | |||
local labels = {frame.args[1] or LPLHdefaults.label} | |||
labels[2] = frame.args[2] or labels[1] | |||
local template = frame:getParent():getTitle() | |||
local args = mArguments.getArgs(frame, {parentOnly = true}) | |||
local pages = hatnote.preprocessDisplays(args) | |||
local options = { | |||
extraclasses = frame.args.extraclasses, | |||
category = args.category, | |||
selfref = frame.args.selfref or args.selfref, | |||
template = template | |||
} | |||
return hatnote._labelledList(pages, labels, options) | |||
end | |||
function hatnote._labelledList (pages, labels, options) | |||
labels = labels or {} | |||
if #pages == 0 then | |||
return hatnote.makeWikitextError( | |||
i18n:msg('error-pagename', 2), | |||
(options.template or LPLHdefaults.template) .. '#Errors', | |||
options.category | |||
) | |||
end | |||
label = (#pages == 1 and labels[1] or labels[2]) or LPLHdefaults.label | |||
local text = string.format( | |||
options.labelForm or LPLHdefaults.labelForm, | |||
label, | |||
hatnote.andList(pages, true) | |||
) | |||
local hnOptions = { | |||
extraclasses = options.extraclasses, | |||
selfref = options.selfref | |||
} | |||
return hatnote._hatnote(text, hnOptions) | |||
end | |||
-------------------------------------------------------------------------------- | |||
-- About | |||
-- | |||
-- These functions implement the {{about}} hatnote template. | |||
-------------------------------------------------------------------------------- | |||
function hatnote.about (frame) | |||
-- A passthrough that gets args from the frame and all | |||
args = mArguments.getArgs(frame) | |||
return hatnote._about(args) | |||
end | |||
function hatnote._about (args, options) | |||
-- Produces "about" hatnote. | |||
-- Type checks and defaults | |||
checkType('_about', 1, args, 'table', true) | |||
args = args or {} | |||
checkType('_about', 2, options, 'table', true) | |||
options = options or {} | |||
local defaultOptions = { | |||
aboutForm = i18n:msg('about', mw.title.getCurrentTitle().namespace), | |||
defaultPageType = i18n:msg('page'), | |||
namespace = mw.title.getCurrentTitle().namespace, | |||
otherText = nil, --included for complete list | |||
pageTypesByNamespace = { | |||
[0] = i18n:msg('pagetype-0'), | |||
[14] = i18n:msg('pagetype-14') | |||
}, | |||
sectionString = i18n:msg('section') | |||
} | |||
for k, v in pairs(defaultOptions) do | |||
if options[k] == nil then options[k] = v end | |||
end | |||
-- Set initial "about" string | |||
local pageType = (args.section and options.sectionString) or | |||
options.pageTypesByNamespace[options.namespace] or | |||
options.defaultPageType | |||
local about = '' | |||
if args[1] then | |||
about = string.format(options.aboutForm, pageType, args[1]) | |||
end | |||
--Allow passing through certain options | |||
local fsOptions = { | |||
otherText = options.otherText | |||
} | |||
-- Set for-see list | |||
local forSee = " " .. hatnote._forSee(args, 2, fsOptions) | |||
-- Concatenate and return | |||
return hatnote._hatnote(about .. forSee, {extraclasses = 'context-link about dablink'}) | |||
end | |||
-------------------------------------------------------------------------------- | |||
-- Details | |||
-- | |||
-- These functions implement the {{details}} hatnote template. | |||
-------------------------------------------------------------------------------- | |||
function hatnote.details (frame) | |||
local args = mArguments.getArgs(frame, {parentOnly = true}) | |||
local topic, category = args.topic, args.category | |||
local options = { | |||
selfref = args.selfref, | |||
extraclasses = 'context-link details dablink' | |||
} | |||
args = mTableTools.compressSparseArray(args) | |||
if #args == 0 then | |||
return hatnote.makeWikitextError( | |||
i18n:msg('error-pagename'), | |||
'w:c:Module:Template:Details#Errors',-- another undocumented thing | |||
category | |||
) | |||
end | |||
return hatnote._details(args, topic, options) | |||
end | |||
function hatnote._details (list, topic, options) | |||
list = hatnote.andList(list, true) | |||
topic = topic or i18n:msg('topic') | |||
local text = string.format(i18n:msg('details'), topic, list) | |||
return hatnote._hatnote(text, options) | |||
end | |||
-------------------------------------------------------------------------------- | |||
-- For | |||
-- | |||
-- These functions implement the {{for}} hatnote template. | |||
-------------------------------------------------------------------------------- | |||
function hatnote.For (frame) | |||
return hatnote._For(mArguments.getArgs(frame)) | |||
end | |||
--Implements {{For}} but takes a manual arguments table | |||
function hatnote._For (args) | |||
local use = args[1] | |||
local category = '' | |||
if (not use or use == i18n:msg('other-uses')) and | |||
(not args.category or yesno(args.category)) then | |||
category = '[[Category:' .. i18n:msg('cat-unusual-parameters') .. ']]' | |||
end | |||
local pages = {} | |||
function two (a, b) return a, b, 1 end --lets us run ipairs from 2 | |||
for k, v in two(ipairs(args)) do table.insert(pages, v) end | |||
return hatnote._hatnote( | |||
hatnote.forSeeTableToString({{use = use, pages = pages}}), | |||
{selfref = args.selfref} | |||
) .. category | |||
end | |||
-------------------------------------------------------------------------------- | |||
-- Further | |||
-- | |||
-- These functions implement the {{further}} hatnote template. | |||
-------------------------------------------------------------------------------- | |||
function hatnote.further(frame) | |||
local args = mArguments.getArgs(frame, {parentOnly = true}) | |||
local pages = mTableTools.compressSparseArray(args) | |||
if #pages < 1 then | |||
return hatnote.makeWikitextError( | |||
i18n:msg('error-pagename', 2), | |||
'w:c:Module:Template:Further#Errors',-- undocumented thing #3 | |||
args.category | |||
) | |||
end | |||
local options = { | |||
selfref = args.selfref, | |||
extraclasses = 'context-link further dablink' | |||
} | |||
return hatnote._further(pages, options) | |||
end | |||
function hatnote._further(pages, options) | |||
local text = i18n:msg('further2') .. hatnote.andList(pages, true) | |||
return hatnote._hatnote(text, options) | |||
end | |||
-------------------------------------------------------------------------------- | |||
-- Main | |||
-- | |||
-- These functions implement the {{main}} hatnote template. | |||
-------------------------------------------------------------------------------- | |||
function hatnote.main(frame) | |||
local args = mArguments.getArgs(frame, {parentOnly = true}) | |||
local pages = {} | |||
for k, v in pairs(args) do | |||
if type(k) == 'number' then | |||
local display = args['label ' .. k] or args['l' .. k] | |||
local page = display and | |||
string.format('%s|%s', string.gsub(v, '|.*$', ''), display) or v | |||
pages[#pages + 1] = page | |||
end | |||
end | |||
if #pages == 0 and mw.title.getCurrentTitle().namespace == 0 then | |||
return hatnote.makeWikitextError( | |||
i18n:msg('error-pagename', 2), | |||
'w:c:Module:Template:Main#Errors',-- undocumented thing #4 | |||
args.category | |||
) | |||
end | |||
local options = { | |||
selfref = args.selfref | |||
} | |||
return hatnote._main(pages, options) | |||
end | |||
function hatnote._main(args, options) | |||
-- Get the list of pages. If no first page was specified we use the current | |||
-- page name. | |||
local currentTitle = mw.title.getCurrentTitle() | |||
if #args == 0 then args = {currentTitle.text} end | |||
local firstPage = string.gsub(args[1], '|.*$', '') | |||
-- Make the formatted link text | |||
list = hatnote.andList(args, true) | |||
-- Build the text. | |||
local isPlural = #args > 1 | |||
-- Find the pagetype. | |||
local pageType = hatnote.findNamespaceId(firstPage) == 0 and i18n:msg('article', isPlural and 2 or 1) or i18n:msg('page', isPlural and 2 or 1) | |||
local mainForm | |||
local curNs = currentTitle.namespace | |||
if (curNs == 14) or (curNs == 15) then --category/talk namespaces | |||
mainForm = isPlural and i18n:msg('main-category', 2) | |||
or | |||
i18n:msg('main-category', 1) | |||
else | |||
mainForm = isPlural and i18n:msg('main', 2) or i18n:msg('main', 1) | |||
end | |||
local text = string.format(mainForm, pageType, list) | |||
options = options or {} | |||
local hnOptions = { | |||
selfref = options.selfref, | |||
extraclasses = 'context-link main dablink' | |||
} | |||
return hatnote._hatnote(text, hnOptions) | |||
end | |||
-------------------------------------------------------------------------------- | |||
-- See also | |||
-- | |||
-- These functions implement the {{see also}} hatnote template. | |||
-------------------------------------------------------------------------------- | |||
function hatnote.seeAlso(frame) | |||
local args = mArguments.getArgs(frame, {parentOnly = true}) | |||
local pages = {} | |||
for k, v in pairs(args) do | |||
if type(k) == 'number' then | |||
local display = args['label ' .. k] or args['l' .. k] | |||
local page = display and | |||
string.format('%s|%s', string.gsub(v, '|.*$', ''), display) or v | |||
pages[#pages + 1] = page | |||
end | |||
end | |||
if not pages[1] then | |||
return hatnote.makeWikitextError( | |||
i18n:msg('error-pagename', 2), | |||
'w:c:Module:Template:See also#Errors',-- undocumented thing #5 | |||
args.category | |||
) | |||
end | |||
local options = { | |||
selfref = args.selfref | |||
} | |||
return hatnote._seeAlso(pages, options) | |||
end | |||
function hatnote._seeAlso(args, options) | |||
checkType('_seeAlso', 1, args, 'table') | |||
checkType('_seeAlso', 2, options, 'table', true) | |||
options = options or {} | |||
local list = hatnote.andList(args, true) | |||
local text = string.format(i18n:msg('see-also2'), list) | |||
-- Pass options through. | |||
local hnOptions = { | |||
selfref = options.selfref, | |||
extraclasses = 'context-link seealso dablink' | |||
} | |||
return hatnote._hatnote(text, hnOptions) | |||
end | |||
hatnote['for'] = hatnote.For | |||
return hatnote |
Latest revision as of 03:11, 24 May 2021
This module is invoked by the {{Hatnote}}
template, which is used by a number of Notice templates.
-- <nowiki> -------------------------------------------------------------------------------- -- Module:Hatnote -- -- -- -- This module produces hatnote links and links to related articles. It -- -- implements the {{hatnote}} and {{format link}} meta-templates and includes -- -- helper functions for other Lua hatnote modules. -- -------------------------------------------------------------------------------- local libraryUtil = require('libraryUtil') local checkType = libraryUtil.checkType local mArguments = require('Module:Arguments') local yesno = require('Module:Yesno') local mTableTools = require('Module:TableTools') local i18n = require('Module:I18n').loadMessages('Hatnote') local hatnote = {} -------------------------------------------------------------------------------- -- Helper functions -------------------------------------------------------------------------------- local function getArgs(frame) -- Fetches the arguments from the parent frame. Whitespace is trimmed and -- blanks are removed. return mArguments.getArgs(frame, {parentOnly = true}) end local function removeInitialColon(s) -- Removes the initial colon from a string, if present. return s:match('^:?(.*)') end function hatnote.findNamespaceId(link, removeColon) -- Finds the namespace id (namespace number) of a link or a pagename. This -- function will not work if the link is enclosed in double brackets. Colons -- are trimmed from the start of the link by default. To skip colon -- trimming, set the removeColon parameter to false. checkType('findNamespaceId', 1, link, 'string') checkType('findNamespaceId', 2, removeColon, 'boolean', true) if removeColon ~= false then link = removeInitialColon(link) end local namespace = link:match('^(.-):') if namespace then local nsTable = mw.site.namespaces[namespace] if nsTable then return nsTable.id end end return 0 end function hatnote.formatPages(...) -- Formats a list of pages using formatLink and returns it as an array. Nil -- values are not allowed. local pages = {...} local ret = {} for i, page in ipairs(pages) do ret[i] = hatnote._formatLink(page) end return ret end function hatnote.formatPageTables(...) -- Takes a list of page/display tables and returns it as a list of -- formatted links. Nil values are not allowed. local pages = {...} local links = {} for i, t in ipairs(pages) do checkType('formatPageTables', i, t, 'table') local link = t[1] local display = t[2] links[i] = hatnote._formatLink(link, display) end return links end function hatnote.makeWikitextError(msg, helpLink, addTrackingCategory, title) -- Formats an error message to be returned to wikitext. If -- addTrackingCategory is not false after being returned from -- [[Module:Yesno]], and if we are not on a talk page, a tracking category -- is added. checkType('makeWikitextError', 1, msg, 'string') checkType('makeWikitextError', 2, helpLink, 'string', true) title = title or mw.title.getCurrentTitle() -- Make the help link text. local helpText if helpLink then helpText = ' ([[' .. helpLink .. '|' .. i18n:msg('help') .. ']])' else helpText = '' end -- Make the category text. local category if not title.isTalkPage and yesno(addTrackingCategory) ~= false then category = i18n:msg('cat-errors') category = string.format( '[[%s:%s]]', mw.site.namespaces[14].name, category ) else category = '' end return string.format( i18n:msg('error'), msg, helpText, category ) end function hatnote.disambiguate(page, disambiguator) -- Formats a page title with a disambiguation parenthetical, -- i.e. "Example" → "Example (disambiguation)". checkType('disambiguate', 1, page, 'string') checkType('disambiguate', 2, disambiguator, 'string', true) disambiguator = disambiguator or i18n:msg('disambiguation') return string.format(i18n:msg('brackets'), page, disambiguator) end -------------------------------------------------------------------------------- -- Format link -- -- Makes a wikilink from the given link and display values. Links are escaped -- with colons if necessary, and links to sections are detected and displayed -- with " § " as a separator rather than the standard MediaWiki "#". Used in -- the {{format link}} template. -------------------------------------------------------------------------------- function hatnote.formatLink(frame) local args = getArgs(frame) local link = args[1] local display = args[2] if not link then return hatnote.makeWikitextError( i18n:msg('error-link'), 'w:c:Module:Template:Format link#Errors',-- there is no actual docs for this. not even on wikipedia args.category ) end return hatnote._formatLink(link, display) end function hatnote._formatLink(link, display) checkType('_formatLink', 1, link, 'string') checkType('_formatLink', 2, display, 'string', true) -- Remove the initial colon for links where it was specified manually. link = removeInitialColon(link) -- Find whether a faux display value has been added with the {{!}} magic -- word. if not display then local prePipe, postPipe = link:match('^(.-)|(.*)$') link = prePipe or link display = postPipe end -- Find the display value. if not display then local page, section = link:match('^(.-)#(.*)$') if page then display = page .. ' § ' .. section end end -- Assemble the link. if display then return string.format( '[[:%s|%s]]', string.gsub(link, '|(.*)$', ''), --display overwrites manual piping display ) else return string.format('[[:%s]]', link) end end -------------------------------------------------------------------------------- -- Hatnote -- -- Produces standard hatnote text. Implements the {{hatnote}} template. -------------------------------------------------------------------------------- function hatnote.hatnote(frame) local args = getArgs(frame) local s = args[1] local options = {} if not s then return hatnote.makeWikitextError( i18n:msg('error-text'), 'w:c:Module:Template:Hatnote#Errors', args.category ) end options.extraclasses = args.extraclasses options.selfref = args.selfref return hatnote._hatnote(s, options) end function hatnote._hatnote(s, options) checkType('_hatnote', 1, s, 'string') checkType('_hatnote', 2, options, 'table', true) options = options or {} local classes = {'notice', 'hatnote'} local extraclasses = options.extraclasses local selfref = options.selfref if type(extraclasses) == 'string' then classes[#classes + 1] = extraclasses end if selfref then classes[#classes + 1] = 'selfref' end return string.format( '<div role="note" class="%s">%s</div>', table.concat(classes, ' '), s ) end -------------------------------------------------------------------------------- -- Module:Hatnote list -- -- -- -- This module produces and formats lists for use in hatnotes. In particular, -- -- it implements the for-see list, i.e. lists of "For X, see Y" statements, -- -- as used in {{about}}, and its variants. Also introduced are andList & -- -- orList helpers for formatting lists with those conjunctions. -- -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -- List stringification helper functions -- -- These functions are used for stringifying lists, usually page lists inside -- the "Y" portion of "For X, see Y" for-see items. -------------------------------------------------------------------------------- --default options table used across the list stringification functions local stringifyListDefaultOptions = { conjunction = i18n:msg('conjunction'), separator = i18n:msg('separator'), altSeparator = i18n:msg('altSeparator'), space = i18n:msg('space'), formatted = false } -- Stringifies a list generically; probably shouldn't be used directly function stringifyList(list, options) -- Type-checks, defaults, and a shortcut checkType("stringifyList", 1, list, "table") if #list == 0 then return nil end checkType("stringifyList", 2, options, "table", true) options = options or {} for k, v in pairs(stringifyListDefaultOptions) do if options[k] == nil then options[k] = v end end local s = options.space -- Format the list if requested if options.formatted then list = hatnote.formatPages(unpack(list)) end -- Set the separator; if any item contains it, use the alternate separator local separator = options.separator --searches display text only function searchDisp(t, f) return string.find(string.sub(t, (string.find(t, '|') or 0) + 1), f) end for k, v in pairs(list) do if searchDisp(v, separator) then separator = options.altSeparator break end end -- Set the conjunction, apply Oxford comma, and force a comma if #1 has "§" local oxfordLangs = { -- list of languages that does respect oxford commas ['en'] = true, ['en-us'] = true, ['en-gb'] = true, } local conjunction = s .. options.conjunction .. s if #list == 2 and searchDisp(list[1], "§") or #list > 2 then conjunction = (oxfordLangs[i18n.defaultLang] and separator or '') .. conjunction end -- Return the formatted string return mw.text.listToText(list, separator .. s, conjunction) end --DRY function function conjList (conj, list, fmt) return stringifyList(list, {conjunction = conj, formatted = fmt}) end -- Stringifies lists with "and" or "or" function hatnote.andList (...) return conjList(i18n:msg('conj-and'), ...) end function hatnote.orList (...) return conjList(i18n:msg('conj-or'), ...) end -------------------------------------------------------------------------------- -- For see -- -- Makes a "For X, see [[Y]]." list from raw parameters. Intended for the -- {{about}} templates and their variants. -------------------------------------------------------------------------------- --default options table used across the forSee family of functions local forSeeDefaultOptions = { andKeyword = i18n:msg('conj-and'), title = mw.title.getCurrentTitle().text, otherText = i18n:msg('other-uses'), forSeeForm = i18n:msg('for') } --Collapses duplicate punctuation function punctuationCollapse (text) local replacements = { ["%.%.$"] = ".", ["%?%.$"] = "?", ["%!%.$"] = "!", ["%.%]%]%.$"] = ".]]", ["%?%]%]%.$"] = "?]]", ["%!%]%]%.$"] = "!]]" } for k, v in pairs(replacements) do text = string.gsub(text, k, v) end return text end -- Structures arguments into a table for stringification, & options function hatnote.forSeeArgsToTable (args, from, options) -- Type-checks and defaults checkType("forSeeArgsToTable", 1, args, 'table') checkType("forSeeArgsToTable", 2, from, 'number', true) from = from or 1 checkType("forSeeArgsToTable", 3, options, 'table', true) options = options or {} for k, v in pairs(forSeeDefaultOptions) do if options[k] == nil then options[k] = v end end -- maxArg's gotten manually because getArgs() and table.maxn aren't friends local maxArg = 0 for k, v in pairs(args) do if type(k) == 'number' and k > maxArg then maxArg = k end end -- Structure the data out from the parameter list: -- * forTable is the wrapper table, with forRow rows -- * Rows are tables of a "use" string & a "pages" table of pagename strings -- * Blanks are left empty for defaulting elsewhere, but can terminate list local forTable = {} local i = from local terminated = false -- Loop to generate rows repeat -- New empty row local forRow = {} -- On blank use, assume list's ended & break at end of this loop forRow.use = args[i] if not args[i] then terminated = true end -- New empty list of pages forRow.pages = {} -- Insert first pages item if present table.insert(forRow.pages, args[i + 1]) -- If the param after next is "and", do inner loop to collect params -- until the "and"'s stop. Blanks are ignored: "1|and||and|3" → {1, 3} while args[i + 2] == options.andKeyword do if args[i + 3] then table.insert(forRow.pages, args[i + 3]) end -- Increment to next "and" i = i + 2 end -- Increment to next use i = i + 2 -- Append the row table.insert(forTable, forRow) until terminated or i > maxArg return forTable end -- Stringifies a table as formatted by forSeeArgsToTable function hatnote.forSeeTableToString (forSeeTable, options) -- Type-checks and defaults checkType("forSeeTableToString", 1, forSeeTable, "table") checkType("forSeeTableToString", 2, options, "table", true) options = options or {} for k, v in pairs(forSeeDefaultOptions) do if options[k] == nil then options[k] = v end end -- Stringify each for-see item into a list local strList = {} for k, v in pairs(forSeeTable) do local useStr = v.use or options.otherText local pagesStr = hatnote.andList(v.pages, true) or hatnote._formatLink(hatnote.disambiguate(options.title)) local forSeeStr = string.format(options.forSeeForm, useStr, pagesStr) forSeeStr = punctuationCollapse(forSeeStr) table.insert(strList, forSeeStr) end -- Return the concatenated list return table.concat(strList, ' ') end -- Produces a "For X, see [[Y]]" string from arguments. Expects index gaps -- but not blank/whitespace values. Ignores named args and args < "from". function hatnote._forSee (args, from, options) local forSeeTable = hatnote.forSeeArgsToTable(args, from, options) return hatnote.forSeeTableToString(forSeeTable, options) end -- As _forSee, but uses the frame. function hatnote.forSee (frame, from, options) return hatnote._forSee(mArguments.getArgs(frame), from, options) end -------------------------------------------------------------------------------- -- Produces a labelled pages-list hatnote. -- The main frame (template definition) takes 1 or 2 arguments, for a singular -- and (optionally) plural label respectively: -- * {{#invoke:Hatnote|labelledList|Singular label|Plural label}} -- The resulting template takes pagename & label parameters normally. -------------------------------------------------------------------------------- -- Defaults global to this module local LPLHdefaults = { label = i18n:msg('see-also'), --Final fallback for label argument labelForm = i18n:msg('colon'), prefixes = {'label', 'label ', 'l'}, template = 'Module:Hatnote' } -- Helper function that pre-combines display parameters into page arguments. -- Also compresses sparse arrays, as a desirable side-effect. function hatnote.preprocessDisplays (args, prefixes) -- Prefixes specify which parameters, in order, to check for display options -- They each have numbers auto-appended, e.g. 'label1', 'label 1', & 'l1' prefixes = prefixes or LPLHdefaults.prefixes local pages = {} for k, v in pairs(args) do if type(k) == 'number' then local display for i = 1, #prefixes do display = args[prefixes[i] .. k] if display then break end end local page = display and string.format('%s|%s', string.gsub(v, '|.*$', ''), display) or v pages[#pages + 1] = page end end return pages end function hatnote.labelledList (frame) local labels = {frame.args[1] or LPLHdefaults.label} labels[2] = frame.args[2] or labels[1] local template = frame:getParent():getTitle() local args = mArguments.getArgs(frame, {parentOnly = true}) local pages = hatnote.preprocessDisplays(args) local options = { extraclasses = frame.args.extraclasses, category = args.category, selfref = frame.args.selfref or args.selfref, template = template } return hatnote._labelledList(pages, labels, options) end function hatnote._labelledList (pages, labels, options) labels = labels or {} if #pages == 0 then return hatnote.makeWikitextError( i18n:msg('error-pagename', 2), (options.template or LPLHdefaults.template) .. '#Errors', options.category ) end label = (#pages == 1 and labels[1] or labels[2]) or LPLHdefaults.label local text = string.format( options.labelForm or LPLHdefaults.labelForm, label, hatnote.andList(pages, true) ) local hnOptions = { extraclasses = options.extraclasses, selfref = options.selfref } return hatnote._hatnote(text, hnOptions) end -------------------------------------------------------------------------------- -- About -- -- These functions implement the {{about}} hatnote template. -------------------------------------------------------------------------------- function hatnote.about (frame) -- A passthrough that gets args from the frame and all args = mArguments.getArgs(frame) return hatnote._about(args) end function hatnote._about (args, options) -- Produces "about" hatnote. -- Type checks and defaults checkType('_about', 1, args, 'table', true) args = args or {} checkType('_about', 2, options, 'table', true) options = options or {} local defaultOptions = { aboutForm = i18n:msg('about', mw.title.getCurrentTitle().namespace), defaultPageType = i18n:msg('page'), namespace = mw.title.getCurrentTitle().namespace, otherText = nil, --included for complete list pageTypesByNamespace = { [0] = i18n:msg('pagetype-0'), [14] = i18n:msg('pagetype-14') }, sectionString = i18n:msg('section') } for k, v in pairs(defaultOptions) do if options[k] == nil then options[k] = v end end -- Set initial "about" string local pageType = (args.section and options.sectionString) or options.pageTypesByNamespace[options.namespace] or options.defaultPageType local about = '' if args[1] then about = string.format(options.aboutForm, pageType, args[1]) end --Allow passing through certain options local fsOptions = { otherText = options.otherText } -- Set for-see list local forSee = " " .. hatnote._forSee(args, 2, fsOptions) -- Concatenate and return return hatnote._hatnote(about .. forSee, {extraclasses = 'context-link about dablink'}) end -------------------------------------------------------------------------------- -- Details -- -- These functions implement the {{details}} hatnote template. -------------------------------------------------------------------------------- function hatnote.details (frame) local args = mArguments.getArgs(frame, {parentOnly = true}) local topic, category = args.topic, args.category local options = { selfref = args.selfref, extraclasses = 'context-link details dablink' } args = mTableTools.compressSparseArray(args) if #args == 0 then return hatnote.makeWikitextError( i18n:msg('error-pagename'), 'w:c:Module:Template:Details#Errors',-- another undocumented thing category ) end return hatnote._details(args, topic, options) end function hatnote._details (list, topic, options) list = hatnote.andList(list, true) topic = topic or i18n:msg('topic') local text = string.format(i18n:msg('details'), topic, list) return hatnote._hatnote(text, options) end -------------------------------------------------------------------------------- -- For -- -- These functions implement the {{for}} hatnote template. -------------------------------------------------------------------------------- function hatnote.For (frame) return hatnote._For(mArguments.getArgs(frame)) end --Implements {{For}} but takes a manual arguments table function hatnote._For (args) local use = args[1] local category = '' if (not use or use == i18n:msg('other-uses')) and (not args.category or yesno(args.category)) then category = '[[Category:' .. i18n:msg('cat-unusual-parameters') .. ']]' end local pages = {} function two (a, b) return a, b, 1 end --lets us run ipairs from 2 for k, v in two(ipairs(args)) do table.insert(pages, v) end return hatnote._hatnote( hatnote.forSeeTableToString({{use = use, pages = pages}}), {selfref = args.selfref} ) .. category end -------------------------------------------------------------------------------- -- Further -- -- These functions implement the {{further}} hatnote template. -------------------------------------------------------------------------------- function hatnote.further(frame) local args = mArguments.getArgs(frame, {parentOnly = true}) local pages = mTableTools.compressSparseArray(args) if #pages < 1 then return hatnote.makeWikitextError( i18n:msg('error-pagename', 2), 'w:c:Module:Template:Further#Errors',-- undocumented thing #3 args.category ) end local options = { selfref = args.selfref, extraclasses = 'context-link further dablink' } return hatnote._further(pages, options) end function hatnote._further(pages, options) local text = i18n:msg('further2') .. hatnote.andList(pages, true) return hatnote._hatnote(text, options) end -------------------------------------------------------------------------------- -- Main -- -- These functions implement the {{main}} hatnote template. -------------------------------------------------------------------------------- function hatnote.main(frame) local args = mArguments.getArgs(frame, {parentOnly = true}) local pages = {} for k, v in pairs(args) do if type(k) == 'number' then local display = args['label ' .. k] or args['l' .. k] local page = display and string.format('%s|%s', string.gsub(v, '|.*$', ''), display) or v pages[#pages + 1] = page end end if #pages == 0 and mw.title.getCurrentTitle().namespace == 0 then return hatnote.makeWikitextError( i18n:msg('error-pagename', 2), 'w:c:Module:Template:Main#Errors',-- undocumented thing #4 args.category ) end local options = { selfref = args.selfref } return hatnote._main(pages, options) end function hatnote._main(args, options) -- Get the list of pages. If no first page was specified we use the current -- page name. local currentTitle = mw.title.getCurrentTitle() if #args == 0 then args = {currentTitle.text} end local firstPage = string.gsub(args[1], '|.*$', '') -- Make the formatted link text list = hatnote.andList(args, true) -- Build the text. local isPlural = #args > 1 -- Find the pagetype. local pageType = hatnote.findNamespaceId(firstPage) == 0 and i18n:msg('article', isPlural and 2 or 1) or i18n:msg('page', isPlural and 2 or 1) local mainForm local curNs = currentTitle.namespace if (curNs == 14) or (curNs == 15) then --category/talk namespaces mainForm = isPlural and i18n:msg('main-category', 2) or i18n:msg('main-category', 1) else mainForm = isPlural and i18n:msg('main', 2) or i18n:msg('main', 1) end local text = string.format(mainForm, pageType, list) options = options or {} local hnOptions = { selfref = options.selfref, extraclasses = 'context-link main dablink' } return hatnote._hatnote(text, hnOptions) end -------------------------------------------------------------------------------- -- See also -- -- These functions implement the {{see also}} hatnote template. -------------------------------------------------------------------------------- function hatnote.seeAlso(frame) local args = mArguments.getArgs(frame, {parentOnly = true}) local pages = {} for k, v in pairs(args) do if type(k) == 'number' then local display = args['label ' .. k] or args['l' .. k] local page = display and string.format('%s|%s', string.gsub(v, '|.*$', ''), display) or v pages[#pages + 1] = page end end if not pages[1] then return hatnote.makeWikitextError( i18n:msg('error-pagename', 2), 'w:c:Module:Template:See also#Errors',-- undocumented thing #5 args.category ) end local options = { selfref = args.selfref } return hatnote._seeAlso(pages, options) end function hatnote._seeAlso(args, options) checkType('_seeAlso', 1, args, 'table') checkType('_seeAlso', 2, options, 'table', true) options = options or {} local list = hatnote.andList(args, true) local text = string.format(i18n:msg('see-also2'), list) -- Pass options through. local hnOptions = { selfref = options.selfref, extraclasses = 'context-link seealso dablink' } return hatnote._hatnote(text, hnOptions) end hatnote['for'] = hatnote.For return hatnote