FANDOM


--------------------------------------------------------------------------------
--                              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('Dev:Arguments')
local yesno = require('Dev:Yesno')
local mTableTools = require('Module:TableTools')
 
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 .. '|help]])'
    else
        helpText = ''
    end
    -- Make the category text.
    local category
    if not title.isTalkPage and yesno(addTrackingCategory) ~= false then
        category = 'Hatnote templates with errors'
        category = string.format(
            '[[%s:%s]]',
            mw.site.namespaces[14].name,
            category
        )
    else
        category = ''
    end
    return string.format(
        '<strong class="error">Error: %s%s.</strong>%s',
        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 'disambiguation'
    return string.format('%s (%s)', 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 hatnote 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(
            'no link specified',
            'Template:Format hatnote link#Errors',
            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 .. ' §&nbsp;' .. 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(
            'no text specified',
            '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 = {'hatnote', 'navigation-not-searchable', 'notice', 'dablink'}
    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}}, {{redirect}}, and their 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 = "and",
    separator = ",",
    altSeparator = ";",
    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 conjunction = s .. options.conjunction .. s
    if #list == 2 and searchDisp(list[1], "§") or #list > 2 then
        conjunction = separator .. 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("and", ...) end
function hatnote.orList (...) return conjList("or", ...) end
 
--------------------------------------------------------------------------------
-- For see
--
-- Makes a "For X, see [[Y]]." list from raw parameters. Intended for the
-- {{about}} and {{redirect}} templates and their variants.
--------------------------------------------------------------------------------
 
--default options table used across the forSee family of functions
local forSeeDefaultOptions = {
    andKeyword = 'and',
    title = mw.title.getCurrentTitle().text,
    otherText = 'other uses',
    forSeeForm = 'For %s, see %s.'
}
 
--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:Labelled list hatnote|labelledList|Singular label|Plural label}}
-- The resulting template takes pagename & label parameters normally.
--------------------------------------------------------------------------------
-- Defaults global to this module
local LPLHdefaults = {
    label = 'See also', --Final fallback for label argument
    labelForm = '%s: %s',
    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(
            'no page names specified',
            (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 = 'This %s is about %s. ',
        defaultPageType = 'page',
        namespace = mw.title.getCurrentTitle().namespace,
        otherText = nil, --included for complete list
        pageTypesByNamespace = {
            [0] = 'article',
            [14] = 'category'
        },
        sectionString = '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 = '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}
    args = mTableTools.compressSparseArray(args)
    if #args == 0 then
        return hatnote.makeWikitextError(
            'no page name specified',
            'Template:Details#Errors',
            category
        )
    end
    return hatnote._details(args, topic, options)
end
 
function hatnote._details (list, topic, options)
    list = hatnote.andList(list, true)
    topic = topic or 'this topic'
    local text = string.format('For more details on %s, see %s.', 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 == 'other uses') and
        (not args.category or yesno(args.category)) then
        category = '[[Category:Hatnote templates using 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(
            'no page names specified',
            'Template:Further#Errors',
            args.category
        )
    end
    local options = {
        selfref = args.selfref
    }
    return hatnote._further(pages, options)
end
 
function hatnote._further(pages, options)
    local text = 'Further information: ' .. 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(
            'no page names specified',
            'Template:Main#Errors',
            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], '|.*$', '')
    -- Find the pagetype.
    local pageType = hatnote.findNamespaceId(firstPage) == 0 and 'article' or 'page'
    -- Make the formatted link text
    list = hatnote.andList(args, true)
    -- Build the text.
    local isPlural = #args > 1
    local mainForm
    local curNs = currentTitle.namespace
    if (curNs == 14) or (curNs == 15) then --category/talk namespaces
        mainForm = isPlural and
            'The main %ss for this category are %s'
            or
            'The main %s for this category is %s'
    else
        mainForm = isPlural and 'Main %ss: %s' or 'Main %s: %s'
    end
    local text = string.format(mainForm, pageType, list)
    options = options or {}
    local hnOptions = {
        selfref = options.selfref
    }
    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(
            'no page names specified',
            'Template:See also#Errors',
            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('See also: %s', list)
    -- Pass options through.
    local hnOptions = {
        selfref = options.selfref
    }
    return hatnote._hatnote(text, hnOptions)
end
 
return hatnote