Module:User:Benwing2/quote
Appearance
- This module sandbox lacks a documentation subpage. Please create it.
- Useful links: root page • root page’s subpages • links • transclusions • testcases • sandbox
--[=[
This module contains functions to implement quote-* templates.
Author: Benwing2; conversion into Lua of {{quote-meta/source}} template,
written by Sgconlaw with some help from Erutuon and Benwing2.
The main interface is quote_t(). Note that the source display is handled by source(), which reads both the
arguments passed to it *and* the arguments passed to the parent template, with the former overriding the latter.
]=]
local export = {}
-- Named constants for all modules used, to make it easier to swap out sandbox versions.
local check_isxn_module = "Module:check isxn"
local debug_track_module = "Module:debug/track"
local italics_module = "Module:italics"
local languages_module = "Module:languages"
local links_module = "Module:links"
local number_utilities_module = "Module:number-utilities"
local parameters_module = "Module:parameters"
local parse_utilities_module = "Module:User:Benwing2/parse utilities"
local roman_numerals_module = "Module:roman numerals"
local script_utilities_module = "Module:script utilities"
local scripts_module = "Module:scripts"
local string_utilities_module = "Module:string utilities"
local table_module = "Module:table"
local usex_module = "Module:usex"
local usex_templates_module = "Module:usex/templates"
local utilities_module = "Module:utilities"
local yesno_module = "Module:yesno"
local rsubn = mw.ustring.gsub
local rmatch = mw.ustring.match
local rfind = mw.ustring.find
local rsplit = mw.text.split
local rgsplit = mw.text.gsplit
local ulen = mw.ustring.len
local usub = mw.ustring.sub
-- Use HTML entities here to avoid parsing issues (esp. with brackets)
local SEMICOLON_SPACE = "; "
local SPACE_LBRAC = " ["
local RBRAC = "]"
local function track(page)
require(debug_track_module)("quote/" .. page)
return true
end
-- version of rsubn() that discards all but the first return value
local function rsub(term, foo, bar)
local retval = rsubn(term, foo, bar)
return retval
end
local function maintenance_line(text)
return "<span class=\"maintenance-line\" style=\"color: #777777;\">(" .. text .. ")</span>"
end
local function isbn(text)
return "[[Special:BookSources/" .. text .. "|→ISBN]]" ..
require(check_isxn_module).check_isbn(text, " <span class=\"error\" style=\"font-size:88%\">Invalid ISBN</span>[[Category:Pages with ISBN errors]]")
end
local function issn(text)
return "[https://www.worldcat.org/issn/" .. text .. " →ISSN]" ..
require(check_isxn_module).check_issn(text, " <span class=\"error\" style=\"font-size:88%\">Invalid ISSN</span>[[Category:Pages with ISSN errors]]")
end
local function lccn(text)
local origtext = text
text = rsub(text, " ", "")
if rfind(text, "%-") then
-- old-style LCCN; reformat per request by [[User:The Editor's Apprentice]]
local prefix, part1, part2 = rmatch(text, "^(.-)([0-9]+)%-([0-9]+)$")
if prefix then
if ulen(part2) < 6 then
part2 = ("0"):rep(6 - ulen(part2)) .. part2
end
text = prefix .. part1 .. part2
end
end
return "[https://lccn.loc.gov/" .. mw.uri.encode(text) .. " →LCCN]"
end
local function format_date(text)
return mw.getCurrentFrame():callParserFunction{name="#formatdate", args=text}
end
local function tag_nowiki(text)
return mw.getCurrentFrame():callParserFunction{name="#tag", args={"nowiki", text}}
end
-- Convert a comma-separated list of language codes to a comma-separated list of language names. `paramname` is the
-- name of the parameter from which the list of language codes was fetched.
local function format_langs(langs, paramname)
langs = rsplit(langs, ",")
for i, langcode in ipairs(langs) do
local lang = require(languages_module).getByCode(langcode, paramname)
langs[i] = lang:getCanonicalName()
end
if #langs == 1 then
return langs[1]
else
return require(table_module).serialCommaJoin(langs)
end
end
local param_mods = {
t = {
-- <t:...> and <gloss:...> are aliases.
item_dest = "gloss",
},
gloss = {},
tr = {},
ts = {},
sc = {
convert = function(arg, parse_err)
return require(scripts_module).getByCode(arg, parse_err)
end,
}
}
-- Parse a text property that may be in a foreign language or script. `val` is the value of the parameter and
-- `paramname` is the name of the parameter from which the value was retrieved. `explicit_gloss`, if specified and
-- non-nil, overrides any gloss specified using the <t:...> or <gloss:...> inline modifier.
--
-- If `val` is nil, the return value of this function is nil. Otherwise it is parsed for a language prefix (e.g.
-- 'ar:مُؤَلِّف') and inline modifiers (e.g. 'ar:مُؤَلِّف<t:Author>'), and the return value is an object with the following
-- fields:
-- `text`: The text after stripping off any language prefix and inline modifiers.
-- `lang`: The language object corresponding to the language prefix, if specified, or nil if no language prefix is
-- given.
-- `sc`: The script object corresponding to the <sc:...> modifier, if given; otherwise nil.
-- `tr`: The transliteration corresponding to the <tr:...> modifier, if given; otherwise nil.
-- `ts`: The transcription corresponding to the <ts:...> modifier, if given; otherwise nil.
-- `gloss`: The gloss/translation corresponding to the `explicit_gloss` parameter (if given and non-nil), otherwise
-- the <t:...> or <gloss:...> modifiers if given, otherwise nil.
--
-- Note that as a special case, if `val` contains HTML tags at the top level (e.g. '<span class="Arab">...</span>', as
-- might be generated by specifying {{lang|ar|مُؤَلِّف}}), no language prefix or inline modifiers are parsed, and the return
-- value has the `noscript` field set to true, which tells format_text() not to try to identify the script of the text
-- and CSS-tag the text accordingly, but to leave the text untagged.
--
-- This object can be passed to format_text() to format a string displaying the text (appropriately script-tagged,
-- unless `noscript` is set, as described above) and modifiers.
local function parse_text_with_lang(val, paramname, explicit_gloss)
if not val then
return nil
end
-- Check for inline modifier, e.g. מרים<tr:Miryem>. But exclude HTML entry with <span ...>, <i ...>, <br/> or
-- similar in it, caused by wrapping an argument in {{l|...}}, {{lang|...}} or similar. Basically, all tags of the
-- sort we parse here should consist of a less-than sign, plus letters, plus a colon, e.g. <tr:...>, so if we see a
-- tag on the outer level that isn't in this format, we don't try to parse it. The restriction to the outer level is
-- to allow generated HTML inside of e.g. qualifier modifiers, such as foo<q:similar to {{m|fr|bar}}> (if we end up
-- supporting such modifiers). If we find a parameter value with top-level HTML in it, add 'noscript = true' to
-- indicate that we should not try to do script inference and tagging. (Otherwise, e.g. if you specify
-- {{lang|ar|مُؤَلِّف}} as the author, you'll get an extra big font coming from the fact that {{lang|...}} wraps the
-- Arabic text in CSS that increases the size from the default, and then we do script detection and again wrap the
-- text in the same CSS, which increases the size even more.)
--
-- Also exclude things that look like URL's from being parsed as having language code prefixes.
if val:find("^[^<]*<[a-z]*[^a-z:]") or val:find("^[a-z]+://") then
return {text = val, noscript = true}
end
local function generate_obj(text, parse_err_or_paramname)
local obj = {}
if text:find(":[^ ]") then
local actual_text, textlang = require(parse_utilities_module).parse_term_with_lang(text, parse_err_or_paramname)
obj.text = actual_text
obj.lang = textlang
else
obj.text = text
end
return obj
end
local obj
if val:find("<") then
-- Check for inline modifier.
obj = require(parse_utilities_module).parse_inline_modifiers(val, {
paramname = paramname,
param_mods = param_mods,
generate_obj = generate_obj,
})
else
obj = generate_obj(val, paramname)
end
if explicit_gloss then
obj.gloss = explicit_gloss
end
return obj
end
-- Format a text property that may be in a foreign language or script, along with annotations. This is conceptually
-- similar to the full_link() function in [[Module:links]], but displays the annotations in a different format that is
-- more appropriate for bibliographic entries. The output looks like this:
--
-- TEXT [TRANSLIT /TRANSCRIPTION/, GLOSS]
--
-- `textobj` is as returned by `parse_text_with_lang`. `tag_text`, if supplied, is a function of one argument to further
-- wrap the text after it has been processed and CSS-tagged appropriately, directly before insertion. `tag_gloss` is a
-- similar function for the gloss.
local function format_text(textobj, tag_text, tag_gloss)
if not textobj then
return nil
end
local text = textobj.text
local tr, ts, gloss = textobj.tr, textobj.ts, textobj.gloss
-- See above for `noscript`, meaning HTML was found in the text value, probably generated using {{lang|...}}.
-- {{lang}} already script-tags the text and processes embedded language links, so we don't want to do it again (in
-- fact, the code below within the if-clause is similar to what {{lang}} does). In such a case, an explicit language
-- won't be available and findBestScriptWithoutLang() may not be accurate, so we can't do automatic transliteration.
if not textobj.noscript then
local lang = textobj.lang
-- As an optimization, don't do script detection on an argument that contains only ASCII.
local sc = textobj.sc or lang and lang:findBestScript(text) or not text:find("^[ -~]$") and
require(scripts_module).findBestScriptWithoutLang(text) or nil
-- As an optimization, don't do any of the following if there's no language, script, translit or transcription,
-- as will be the case with simple ASCII values.
if lang or sc or tr or ts then
lang = lang or require(languages_module).getByCode("und", true)
if tr == "-" then
tr = nil
elseif not tr and sc and not sc:getCode():find("Latn") then -- Latn, Latnx or a lang-specific variant
-- might return nil
tr = (lang:transliterate(require(links_module).remove_links(text), sc))
end
text = require(links_module).embedded_language_links(
{
term = text,
lang = lang,
sc = sc
},
false
)
if lang:getCode() ~= "und" or sc:getCode() ~= "Latn" then
text = require(script_utilities_module).tag_text(text, lang, sc)
end
if tr then
-- Should we link to the transliteration of languages with lang:link_tr()? Probably not because `text` is not
-- likely to be a term that has an entry.
tr = require(script_utilities_module).tag_translit(tr, lang, "usex")
end
if ts then
ts = require(script_utilities_module).tag_transcription(ts, lang, "usex")
end
end
end
text = require(italics_module).unitalicize_brackets(text)
if tag_text then
text = tag_text(text)
end
local parts = {}
local function ins(txt)
table.insert(parts, txt)
end
ins(text)
if tr or ts or gloss then
ins(SPACE_LBRAC)
if tr then
ins(tr)
end
if ts then
if tr then
ins(" ")
end
ins("/" .. ts .. "/")
end
if gloss then
if tr or ts then
ins(", ")
end
gloss = '<span class="e-translation">' .. gloss .. "</span>"
gloss = require(italics_module).unitalicize_brackets(gloss)
if tag_gloss then
gloss = tag_gloss(gloss)
end
ins(gloss)
end
ins(RBRAC)
end
return table.concat(parts)
end
-- Fancy version of ine() (if-not-empty). Converts empty string to nil, but also strips leading/trailing space.
local function ine(arg)
if not arg then return nil end
arg = mw.text.trim(arg)
if arg == "" then return nil end
return arg
end
-- Clone and combine frame's and parent's args while also assigning nil to empty strings. Handle aliases and ignores.
local function clone_args(direct_args, parent_args)
local args = {}
-- Processing parent args must come first so that direct args override parent args. Note that if a direct arg is
-- specified but is blank, it will still override the parent arg (with nil).
for pname, param in pairs(parent_args) do
-- [[Special:WhatLinksHere/Wiktionary:Tracking/quote/param/PARAM]]
track("param/" .. pname)
args[pname] = ine(param)
end
-- Process ignores. The value of `ignore` is a comma-separated list of parameter names to ignore (erase). We need to
-- do this before aliases due to {{quote-song}}, which sets chapter= to the value of title= in the direct params and
-- sets title= to the value of album= using an alias. If we do the ignores after aliases, we get an error during alias
-- processing, saying that title= and its alias album= are both present.
local ignores = ine(direct_args.ignore)
if ignores then
for ignore in rgsplit(ignores, "%s*,%s*") do
args[ignore] = nil
end
end
-- Process aliases. The value of `alias` is a list of semicolon-separated specs, each of which is of the form
-- DEST:SOURCE,SOURCE,... where DEST is the canonical name of a parameter and SOURCE refers to an alias. Whitespace
-- is allowed between all delimiters. The order of aliases may be important. Ffor example, for {{quote-journal}},
-- title= contains the article name and is an alias of underlying chapter=, while journal= or work= contains the
-- journal name and is an alias of underlying title=. As a result, the title -> chapter alias must be specified
-- before the journal/work -> title alias.
--
-- Whenever we copy a value from argument SOURCE to argument DEST, we record an entry for the pair in alias_map, so
-- that when we would display an error message about DEST, we display SOURCE instead.
--
-- Do alias processing (and ignore and error_if processing) before processing direct_args so that e.g. we can set up
-- an alias of title -> chapter and then set title= to something else in the direct args ({{quote-hansard}} does this).
local aliases = ine(direct_args.alias)
local alias_map = {}
if aliases then
-- Allow and discard a trailing semicolon, to make managing multiple aliases easier.
aliases = rsub(aliases, "%s*;$", "")
for alias_spec in rgsplit(aliases, "%s*;%s*") do
local alias_spec_parts = rsplit(alias_spec, "%s*:%s*")
if #alias_spec_parts ~= 2 then
error(("Alias spec '%s' should have one colon in it"):format(alias_spec))
end
local dest, sources = unpack(alias_spec_parts)
sources = rsplit(sources, "%s*,%s*")
saw_source = nil
for _, source in ipairs(sources) do
if args[source] then
if saw_source == nil then
saw_source = source
else
error(("|%s= and |%s= are aliases; cannot specify a value for both"):format(saw_source, source))
end
end
end
if saw_source then
if args[dest] then
error(("|%s= is an alias of |%s=; cannot specify a value for both"):format(saw_source, dest))
end
args[dest] = args[saw_source]
-- Wipe out the original after copying. This important in case of a param that has general significance
-- but has been redefined (e.g. {{quote-av}} redefines number= for the episode number, and
-- {{quote-journal}} redefines title= for the chapter= (article). It's also important once we implement
-- unhandled parameter checking.
args[saw_source] = nil
alias_map[dest] = saw_source
end
end
end
-- Process error_if. The value of `error_if` is a comma-separated list of parameter names to throw an error if seen
-- in parent_args (they are params we overwrite in the direct args).
local error_ifs = ine(direct_args.error_if)
if error_ifs then
for error_if in rgsplit(error_ifs, "%s*,%s*") do
if ine(parent_args[error_if]) then
error(("Cannot specify a value |%s=%s as it would be overwritten or ignored"):format(error_if, ine(parent_args[error_if])))
end
end
end
for pname, param in pairs(direct_args) do
args[pname] = ine(param)
end
return args, alias_map
end
local function format_date_with_code(code, timestamp)
local language = mw.getContentLanguage()
local ok, date = pcall(language.formatDate, language, code, timestamp)
if ok then
return date
else
-- All the formats used in format_date_args() are fine, so the timestamp must be at fault.
error("Timestamp '" .. tostring(timestamp) .. "' could not be parsed; "
.. "see the [[mw:Help:Extension:ParserFunctions##time|documentation for the #time parser function]]).")
end
end
-- Try to figure out if the given timestamp has the day of the month explicitly given. We use the following algorithm:
-- 1. Format as year-month-day; if the day is not 1, the day was explicitly given, since if only the year/month are
-- given, the day shows up as 1.
-- 2. If the day shows up as 1 and there isn't a 1 or 01 in the timestamp, the day wasn't explicitly given.
-- 3. Otherwise, if there are three separate numbers (e.g. 2022-07-01), or two separate numbers plus a capitalized
-- letter (taken as an English month, e.g. 2022 July 1), the day was explicitly given, otherwise not.
local function date_has_day_specified(timestamp)
local day = format_date_with_code("j", timestamp)
if day ~= "1" then
return true
end
local english_month = timestamp:find("[A-Z]")
local canon_timestamp = mw.text.trim((timestamp:gsub("[^0-9]+", " ")))
local seen_nums = rsplit(canon_timestamp, " ")
local saw_one = false
for _, num in ipairs(seen_nums) do
if num == "1" or num == "01" then
saw_one = true
break
end
end
if not saw_one then
return false
end
return #seen_nums >= 3 or english_month and #seen_nums >= 2
end
local abbrs = {
["a."] = { anchor = "a.", full = "ante", },
["c."] = { anchor = "c.", full = "circa", },
["p."] = { anchor = "p.", full = "post", },
}
local function process_ante_circa_post(date)
local prefix = usub(date, 1, 2)
local abbr = abbrs[prefix]
local abbr_prefix = ""
if abbr then
abbr_prefix = "''[[Appendix:Glossary#" .. abbr.anchor .. '|<abbr title="' .. abbr.full .. '">' ..
abbr.anchor .. "</abbr>]]'' "
-- Remove lowercase letter, period, and space from beginning of date parameter.
date = rsub(date, "^%l%.%s*", "")
end
return abbr_prefix, date
end
-- Format the arguments that specify the date of the quotation. These include the following:
-- |date=: The date. If |start_date= is given, this is the end date.
-- |year=, |month=: Year and month of quotation date or end of range, if |date= isn't given.
-- |start_date=: The start date, to specify a range.
-- |start_year=, |start_month=: Year and month of start of range, if |start_date= isn't given.
-- |accessdate=: Date a website was accessed; processed if no other date was given.
-- |nodate=: Indicate that no date is present; otherwise a maintenance line will be displayed if there is no date.
--
-- If `parampref` and/or `paramsuf` are given, this modifies all the date arguments accordingly. For example, if
-- `parampref` == "orig" and `paramsuf` is omitted, the date is specified using |origdate= or |origyear=/|origmonth=,
-- and the start of the range is |origstart_date=, etc. Similarly, if `parampref` is omitted and `paramsuf` is
-- "_published", the date is specified using |date_published= or |year_published=/|month_published=, and the start of
-- the range is |start_date_published=, etc.
--
-- `a` and `get_full_paramname` are functions with the same interpretation as the local functions of the same name in
-- source(). These are used to fetch parameters and get their full names. Note that this may cause all arguments to
-- have an index added to them (|date2=, |year2=, |month2=, etc.).
--
-- `alias_map` is as in source() and is used to map canonical arguments to their aliases when aliases were used.
--
-- If `bold_year` is given, displayed years are boldfaced unless boldface is present in the parameter value.
--
-- If `maintenance_line_no_date` is specified, it should be a string that will be returned if no date is found (i.e.
-- neither |date= nor |year=, or their appropriate equivalents per `parampref` and `paramsuf`, are specified, and
-- neither |nodate= is given to indicate that there is no date, or |accessdate= is given).
--
-- Returns two values: the formatted date and a boolean indicating whether to add a maintenance category
-- [[:Category:Requests for date in LANG entries]]. The return value can be nil if `maintenance_line_no_date` is not
-- given (in which case the scond return value will always be nil).
local function format_date_args(a, get_full_paramname, alias_map, parampref, paramsuf, bold_year,
maintenance_line_no_date)
local output = {}
parampref = parampref or ""
paramsuf = paramsuf or ""
local function getp(param)
return a(parampref .. param .. paramsuf)
end
local function pname(param)
local fullname = get_full_paramname(parampref .. param .. paramsuf)
return alias_map[fullname] or fullname
end
local function ins(text)
table.insert(output, text)
end
local function format_bold_date(date, explicit_day)
local month_day_code = explicit_day and "F j" or "F"
if bold_year then
-- This formats like "'''2023''' August 3" (or "'''2023''' August" if day not explicitly given).
return format_date_with_code("'''Y''' " .. month_day_code, date)
else
-- This formats like "2023 August 3" (or "2023 August" if day not explicitly given).
return format_date_with_code("Y " .. month_day_code, date)
end
end
local function boldface_if_not_already(year)
if not bold_year or year:find("'''") then
return year
else
return "'''" .. year .. "'''"
end
end
-- The formatDate method of the mw.language object behaves like the {{#time:}} parser function, which doesn't
-- accept the formats "monthday monthname, year" or "year monthname monthday", but outputs garbage when it receives
-- them, behavior inherited from PHP. {{#formatdate:}} magic word is more forgiving. Fix dates so that, for
-- instance, the |date= parameter of {{quote-journal}} (which uses this code) and the |accessdate= parameter (which
-- uses {{#formatdate:}}) accept similar date formats. See:
-- * [[mw:Extension:Scribunto/Lua_reference_manual#mw.language:formatDate]]
-- * [[mw:Help:Extension:ParserFunctions##time]]
-- * [[mw:Help:Magic_words#Formatting]]
local function fix_date(date)
if tonumber(date) ~= nil then
error(("|%s= should contain a full date (year, month, day of month); use |%s= for year"):
format(pname("date"), pname("year")))
elseif date and date:find "%s*%a+,%s*%d+%s*$" then
error(("|%s= should contain a full date (year, month, day of month); use |%s=, |%s= for month and year"):
format(pname("date"), pname("month"), pname("year")))
end
if date then
date = rsub(date, "(%d+ %a+),", "%1")
date = rsub(date, "^(%d%d%d%d) (%a+ %d%d?)$", "%2 %1")
return date
end
end
local start_date, date = fix_date(getp("start_date")), fix_date(getp("date"))
local year = getp("year")
local month = getp("month")
local start_year = getp("start_year")
local start_month = getp("start_month")
if date and year then
error(("Only one of |%s= or |%s= should be specified"):format(pname("date"), pname("year")))
end
if date and month then
error(("|%s= should only be specified in conjunction with |%s=, not with |%s="):
format(pname("month"), pname("year"), pname("date")))
end
if start_date and start_year then
error(("Only one of |%s= or |%s= should be specified"):format(pname("start_date"), pname("start_year")))
end
if start_date and start_month then
error(("|%s= should only be specified in conjunction with |%s=, not with |%="):
format(pname("start_month"), pname("start_year"), pname("start_date")))
end
if (start_date or start_year) and not (date or year) then
error(("|%s= or |%s=/|%s= cannot be specified without specifying |%s= or |%s=/|%s="):
format(pname("start_date"), pname("start_year"), pname("start_month"),
pname("date"), pname("year"), pname("month")))
end
local dash = " – "
local day_explicitly_given = date and date_has_day_specified(date)
local start_day_explicitly_given = start_date and date_has_day_specified(start_date)
local function format_date_or_year_month(date, year, month, explicit_day)
if date then
return format_bold_date(date, explicit_day)
else
return boldface_if_not_already(year) .. (month and " " .. month or "")
end
end
if year then
local abbr_prefix
abbr_prefix, year = process_ante_circa_post(year)
ins(abbr_prefix)
end
if start_date or start_year then
ins(format_date_or_year_month(start_date, start_year, start_month, start_day_explicitly_given))
local cur_year = year or format_date_with_code("Y", date)
local cur_month = month or date and format_date_with_code("F", date) or nil
local cur_day = date and day_explicitly_given and format_date_with_code("j", date) or nil
local beg_year = start_year or format_date_with_code("Y", start_date)
local beg_month = start_month or start_date and format_date_with_code("F", start_date) or nil
local beg_day = start_date and start_day_explicitly_given and format_date_with_code("j", start_date) or nil
if cur_year ~= beg_year then
-- Different years; insert current date in full.
ins(dash)
ins(format_date_or_year_month(date, year, month))
elseif cur_month and cur_month ~= beg_month then
-- Same year but different months; insert current month and (if available) current day.
ins(dash)
ins(cur_month)
if cur_day then
ins(" " .. cur_day)
end
elseif cur_day and cur_day ~= beg_day then
-- Same year and month but different days; insert current day.
ins(dash)
ins(" " .. cur_day)
else
-- Same year, month and day; or same year and month, and day not available; or same year, and month and
-- day not available. Do nothing. FIXME: Should we throw an error?
end
elseif date or year then
ins(format_date_or_year_month(date, year, month, day_explicitly_given))
elseif not maintenance_line_no_date then
-- Not main quote date. Return nil, caller will handle.
return nil, nil
elseif not getp("nodate") then
local accessdate = getp("accessdate")
if accessdate then
local explicit_day = date_has_day_specified(accessdate)
ins(format_bold_date(accessdate, explicit_day) .. " (last accessed)")
else
if mw.title.getCurrentTitle().nsText ~= "Template" then
return maintenance_line(maintenance_line_no_date), true
else
return "", nil
end
end
end
return table.concat(output), nil
end
local function tag_with_cite(txt)
return "<cite>" .. txt .. "</cite>"
end
local function pluralize(txt)
-- Try to shortcut loading [[Module:string utilities]].
if txt:find("[sxzhy%]]$") then
return require(string_utilities_module).pluralize(txt)
else
return txt .. "s"
end
end
-- Display the source line of the quote, above the actual quote text. This contains the majority of the logic of this
-- module (formerly contained in {{quote-meta/source}}).
function export.source(args, alias_map)
local tracking_categories = {}
local argslang = args.lang or args[1]
if not argslang then
-- For the moment, only trigger an error on mainspace pages and
-- other pages that are not user pages or pages containing discussions.
-- These are the same pages that appear in the appropriate tracking
-- categories. User and discussion pages have not generally been
-- fixed up to include a language code and so it's more helpful
-- to use a maintenance line than signal an error.
local FULLPAGENAME = mw.title.getCurrentTitle().fullText
local NAMESPACE = mw.title.getCurrentTitle().nsText
if NAMESPACE ~= "Template" and not require(usex_templates_module).page_should_be_ignored(FULLPAGENAME) then
require(languages_module).err(nil, 1)
end
end
-- Given a canonical param, convert it to the original parameter specified by the user (which may have been an
-- alias).
local function alias(param)
return alias_map[param] or param
end
local output = {}
-- Add text to the output. The text goes into a list, and we concatenate
-- all the list components together at the end.
local function add(text)
table.insert(output, text)
end
if args.brackets then
add("[")
end
-- Return a function that generates the actual parameter name associated with a base param (e.g. "author", "last").
-- The actual parameter name may have an index added (an empty string for the first set of params, e.g. author=,
-- last=, or a numeric index for further sets of params, e.g. author2=, last2=, etc.).
local function make_get_full_paramname(ind)
return function(param)
return param .. ind
end
end
-- Function to fetch the actual parameter name associated with a base param (see make_get_full_paramname() above).
-- Assigned at various times below by calling make_get_full_paramname(). We do it this way so that we can have
-- wrapper functions that access params and define them only oncec.
local get_full_paramname
-- Return two values: the value of a parameter given the base param name (which may have a numeric index added),
-- and the parameter name from which the value was fetched (which may be an alias, i.e. you can't necessarily fetch
-- the parameter value from args[] given this name). The base parameter can be a list of such base params, which
-- are checked in turn.
local function a_with_name(param)
if type(param) == "table" then
for _, par in ipairs(param) do
local val, name = a_with_name(par)
if val then
return val, name
end
end
return nil
end
local fullname = get_full_paramname(param)
return args[fullname], alias(fullname)
end
-- Fetch the value of a parameter given the base param name (which may have a numeric index added). The base
-- parameter can be a list of such base params, which are checked in turn.
local function a(param)
return (a_with_name(param))
end
-- Identical to a_with_name(param) except that it verifies that no space is present. Should be used for URL's.
local function aurl_with_name(param)
local value, fullname = a_with_name(param)
if value and value:find(" ") and not value:find("%[") then
error(("URL not allowed to contain a space, but saw |%s=%s"):format(fullname, value))
end
return value, fullname
end
-- Identical to a(param) except that it verifies that no space is present. Should be used for URL's.
local function aurl(param)
return (aurl_with_name(param))
end
-- Convenience function to fetch a parameter that may be in a foreign language or text (and may consequently have
-- a language prefix and/or inline modifiers), parse the modifiers and convert the result into a formatted string.
-- This is the same as parse_and_format_text() below but also returns the param name as the second return value
-- and the raw param value (as directly specified by the user, after stripping off inline modifiers and language
-- prefix) as the third return value.
local function parse_and_format_text_with_name(param, tag_text, tag_gloss)
local paramval, paramname = a_with_name(param)
local obj = parse_text_with_lang(paramval, paramname)
return format_text(obj, tag_text, tag_gloss), paramname, obj and obj.text or nil
end
-- Convenience function to fetch a parameter that may be in a foreign language or text (and may consequently have
-- a language prefix and/or inline modifiers), parse the modifiers and convert the result into a formatted string.
-- This is a wrapper around parse_text_with_lang() and format_text(). `param` is the base parameter name (see
-- a_with_name()), `tag_text` is an optional function to tag the parameter text after all other processing (e.g.
-- wrap in <cite>...</cite> tags), and `tag_gloss` is a similar function for the parameter translation/gloss.
local function parse_and_format_text(param, tag_text, tag_gloss)
return (parse_and_format_text_with_name(param, tag_text, tag_gloss))
end
-- Set this now so a() works just below.
get_full_paramname = make_get_full_paramname("")
local formatted_date, need_date = format_date_args(a, get_full_paramname, alias_map, nil, nil, "bold year",
"Can we [[:Category:Requests for date|date]] this quote?")
add(formatted_date)
-- Fetch origdate=/origyear=/origmonth= and format appropriately.
local formatted_origdate = format_date_args(a, get_full_paramname, alias_map, "orig")
if formatted_origdate then
add(SPACE_LBRAC .. formatted_origdate .. RBRAC)
end
if args.author or args.last or args.quotee then
-- Find maximum indexed author or last name.
local maxind = 0
for arg, _ in pairs(args) do
local argbase, argind = rmatch(arg, "^([a-z]+)([0-9]*)$")
if argbase == "author" or argbase == "last" then
argind = argind == "" and 1 or tonumber(argind)
if argind > maxind then
maxind = argind
end
end
end
for i = 1, maxind do
local ind = i == 1 and "" or i
get_full_paramname = make_get_full_paramname(ind)
local author, author_param = a_with_name("author")
local last = a("last")
local first = a("first")
if author or last then
-- If first author, output a comma unless {{{nodate}}} used.
if i == 1 and not args.nodate then
add(", ")
end
-- If not first author, output a semicolon to separate from preceding authors.
add(i == 1 and " " or SEMICOLON_SPACE)
local function make_author_with_url(txt, authorlink)
if authorlink then
return "[[w:" .. authorlink .. "|" .. txt .. "]]"
else
return txt
end
end
local authorlink = a("authorlink")
local authorlink_gloss = a("trans-authorlink")
if author then
local authorobj = parse_text_with_lang(author, author_param, a("trans-author"))
authorobj.text = make_author_with_url(authorobj.text, authorlink)
if authorobj.gloss and authorlink_gloss then
authorobj.gloss = make_author_with_url(authorobj.gloss, authorlink_gloss)
end
add(format_text(authorobj))
else
-- Author separated into first name + last name. We don't currently support non-Latin-script
-- authors separated this way and probably never will.
local first = a("first")
if first then
author = first .. " " .. last
else
author = last
end
author = make_author_with_url(author, authorlink)
local last_gloss = a("trans-last")
local author_gloss
if last_gloss then
local first_gloss = a("trans-first")
if first_gloss then
author_gloss = first_gloss .. " " .. last_gloss
else
author_gloss = last_gloss
end
author_gloss = make_author_with_url(author_gloss, authorlink_gloss)
end
add(author)
if author_gloss then
add(SPACE_LBRAC)
add(author_gloss)
add(RBRAC)
end
end
end
end
if args.coauthors or args.quotee then
-- Need to set this. It's accessed (indirectly) by parse_and_format_text(), and will have the wrong value
-- as a result of the 1, maxind loop above.
get_full_paramname = make_get_full_paramname("")
if args.coauthors then
add(SEMICOLON_SPACE .. parse_and_format_text("coauthors"))
end
if args.quotee then
add(", quoting " .. parse_and_format_text("quotee"))
end
end
add(",")
elseif args.year or args.date or args.start_year or args.start_date then
--If no author stated but date provided, add a comma.
add(",")
end
add(" ")
local function has_new_title_or_ancillary_author()
return args.chapter2 or args.title2 or
args.tlr2 or args.translator2 or args.translators2 or
args.mainauthor2 or args.editor2 or args.editors2
end
local function has_new_title_or_author()
return args["2ndauthor"] or args["2ndlast"] or has_new_title_or_ancillary_author()
end
local function has_newversion()
return args.newversion or args.location2 or has_new_title_or_author()
end
-- Handle chapter=, section=, etc. `param` is the base name of the parameter in question, e.g. "chapter" or
-- "section". If numeric (either Arabic or Roman), add `numeric_prefix`; otherwise, parse as textual (allowing for
-- language prefixes, inline modifiers, etc.), prefix with `textual_prefix` (if given) and suffix with
-- `textual_suffix` (if given). Also checks for and handles the following (assuming param == "chapter"):
-- * chapterurl=: URL of the chapter.
-- * trans-chapter=: Chapter translation (can be given using an inline modifier <t:...>).
-- * chapter_number=: Chapter number, when chapter= is also given (otherwise put the chapter number in chapter=).
-- * chapter_plain=: Plain version of the chapter number; the "chapter " prefix isn't added.
-- * chapter_series=: Series that the chapter is within (used e.g. for journal articles part of a series).
-- * chapter_seriesvolume=: Volume of the series (compare seriesvolume=).
--
-- Returns nil if no value specified for the main parameter, otherwise the formatted value.
local function format_chapterlike(param, numeric_prefix, textual_prefix, textual_suffix)
local chap, chap_param = a_with_name(param)
local chap_num, chap_num_param = a_with_name(param .. "_number")
local chap_plain, chap_plain_param = parse_and_format_text_with_name(param .. "_plain")
if chap_num and chap_plain then
error(("Specify only one of |%s= or %s="):format(chap_num_param, chap_plain_param))
end
local chap_series, chap_series_param =
parse_and_format_text_with_name(param .. "_series", tag_with_cite, tag_with_cite)
local chap_seriesvolume, chap_seriesvolume_param = parse_and_format_text_with_name(param .. "_seriesvolume")
if chap_series then
chap_series = ", " .. chap_series
end
if chap_seriesvolume then
if not chap_series then
error(("Cannot specify |%s= without %s="):format(chap_series_param, chap_seriesvolume_param))
end
chap_series = chap_series .. " (" .. chap_seriesvolume .. ")"
end
if not chap then
if chap_num then
error(("Cannot specify |%s= without |%s=; put the numeric value in |%s= directly"):
format(chap_num_param, chap_param, chap_param))
end
if chap_plain then
return chap_plain .. (chap_series or "")
end
return nil
end
local cleaned_chap = chap:gsub("<sup>[^<>]*</sup>", ""):gsub("[*+#]", "")
local chapterurl = aurl(param .. "url")
local function make_chapter_with_url(chap)
if chapterurl then
return "[" .. chapterurl .. " " .. chap .. "]"
else
return chap
end
end
local formatted
if require(number_utilities_module).get_number(cleaned_chap) then
-- Arabic chapter number
formatted = numeric_prefix .. make_chapter_with_url(chap)
elseif rfind(cleaned_chap, "^[mdclxviMDCLXVI]+$") and require(roman_numerals_module).roman_to_arabic(cleaned_chap, true) then
-- Roman chapter number
formatted = numeric_prefix .. make_chapter_with_url(mw.ustring.upper(chap))
else
-- Must be a chapter name
local chapterobj = parse_text_with_lang(chap, chap_param, a("trans-" .. param))
chapterobj.text = make_chapter_with_url(chapterobj.text)
formatted = (textual_prefix or "") .. format_text(chapterobj) .. (textual_suffix or "")
end
if chap_num or chap_plain then
-- NOTE: Up above we throw an error if both chap_num and chap_plain are specified.
formatted = formatted .. " (" .. (chap_plain or numeric_prefix .. chap_num) .. ")"
end
if chap_series then
formatted = formatted .. chap_series
end
return formatted
end
-- This handles everything after displaying the author, starting with the chapter and ending with page, column and
-- then other=. It is currently called twice: Once to handle the main portion of the citation, and once to handle a
-- "newversion" citation. `ind` is either "" for the main portion or a number (currently only 2) for a "newversion"
-- citation. In a few places we conditionalize on `ind` to take actions depending on its value. `sep` is the
-- separator to display before the first item we add; see add_with_sep() below.
local function postauthor(ind, sep)
get_full_paramname = make_get_full_paramname(ind)
local chapter_tlr = parse_and_format_text("chapter_tlr")
if chapter_tlr then
add(chapter_tlr .. ", transl., ")
end
local formatted_chapter = format_chapterlike("chapter", "chapter ", "“", "”")
if formatted_chapter then
add(" ")
add(formatted_chapter)
if not a("notitle") then
add(", in ")
end
end
local tlr = parse_and_format_text({"tlr", "translator", "translators"})
local editor, editor_param = parse_and_format_text_with_name("editor")
local editors, editors_param = parse_and_format_text_with_name("editors")
if editor and editors then
error(("Can't specify both |%s= and |%s="):format(editor_param, editors_param))
end
if a("mainauthor") then
add(parse_and_format_text("mainauthor") .. ((tlr or editor or editors) and SEMICOLON_SPACE or ","))
end
if tlr then
add(tlr .. ", transl." .. ((editor or editors) and SEMICOLON_SPACE or ","))
end
if editor then
add(editor .. ", editor,")
elseif editors then
add(editors .. ", editors,")
end
-- If we're in the "newversion" code (ind ~= ""), and there's no title and no URL, then the first time we add
-- anything after the title, we don't want to add a separating comma because the preceding text will say
-- "republished " or "republished as " or "translated as " or similar. In all other cases, we do want to add a
-- separating comma. We handle this using a `sep` variable whose value will generally either be "" or ", ". The
-- add_with_sep(text) function adds the `sep` variable and then `text`, and then resets `sep` to ", " so the
-- next time around we do add a comma to separate `text` from the preceding piece of text.
local function add_with_sep(text)
add(sep .. text)
sep = ", "
end
local title, title_param = a_with_name("title")
if title then
local titleobj = parse_text_with_lang(title, title_param, a("trans-title"))
add(" ")
add(format_text(titleobj, tag_with_cite, tag_with_cite))
local series = parse_and_format_text("series")
if series then
add(" (" .. series)
local seriesvolume = parse_and_format_text("seriesvolume")
if seriesvolume then
add(SEMICOLON_SPACE .. seriesvolume)
end
add(")")
end
sep = ", "
elseif ind == "" then
sep = ", "
if not a("notitle") then
add(maintenance_line("Please provide the book title or journal name"))
end
end
local url = aurl("archiveurl") or aurl("url")
if url then
add("‎<sup>[" .. url .. "]</sup>")
sep = ", "
end
local urls = aurl("urls")
if urls then
add("‎<sup>" .. urls .. "</sup>")
sep = ", "
end
local edition = parse_and_format_text("edition")
if edition then
add_with_sep(edition .. " edition")
end
local edition_plain = parse_and_format_text("edition_plain")
if edition_plain then
add_with_sep(" " .. edition_plain)
end
-- Display a numeric param such as page=, volume=, column=. For each `paramname`, four params are actually
-- recognized, e.g. for paramname == "page", the params page=, pages=, page_plain= and pageurl= are recognized
-- and checked (or the same with an index, e.g. page2=, pages2=, page_plain2= and pageurl2= respectively if
-- ind == "2"). Only one of the first three can be specified; an error results if more than one are given.
-- If none are given, the return value is nil; otherwise it is a string. The numeric spec is taken directly
-- from e.g. page_plain= if given; otherwise if e.g. pages= is given, or if page= is given and looks like a
-- combination of numbers (i.e. it has a hyphen or dash in it, a comma, or the word " and "), it is prefixed
-- by `singular_desc` + "s" (e.g. "pages "), otherwise it is prefixed by just `singular_desc` (e.g. "page ").
-- (As a special case, if either e.g. page=unnumbered or pages=unnumbered is given, the numeric spec is
-- "unnumbered page".) The resulting spec is returned directly unless e.g. pageurl= is given, in which case
-- it is linked to the specified URL. Note that any of the specs can be foreign text, e.g. foreign numbers
-- (including with optional inline modifiers), and such text is handled appropriately.
local function format_numeric_param(paramname, singular_desc)
local sgval, sg_param = a_with_name(paramname)
local sgobj = parse_text_with_lang(sgval, paramname)
local plparamname = paramname .. "s"
local plval, pl_param = a_with_name(plparamname)
local plobj = parse_text_with_lang(plval, plparamname)
local plainval, plain_param = parse_and_format_text_with_name(paramname .. "_plain")
local howmany = (sgval and 1 or 0) + (plval and 1 or 0) + (plainval and 1 or 0)
if howmany > 1 then
local params_specified = {}
local function insparam(param)
if param then
table.insert(params_specified, ("|%s="):format(param))
end
end
insparam(sg_param)
insparam(pl_param)
insparam(plain_param)
error(("Can't specify more than one of %s"):format(
require(table_module).serialCommaJoin(params_specified, {dontTag = true})))
end
if howmany == 0 then
return nil
end
-- Merge page= and pages= and treat alike because people often mix them up in both directions.
local numspec
if plainval then
numspec = plainval
else
local val = sgobj and sgobj.text or plobj.text
if val == "unnumbered" then
numspec = "unnumbered " .. singular_desc
else
local function get_plural_desc()
-- Only call when needed to potentially avoid a module load.
return pluralize(singular_desc)
end
local desc
if val:find("^!") then
val = val:gsub("^!", "")
desc = sgval and singular_desc or get_plural_desc()
else
local check_val = val
if check_val:find("%[") then
check_val = require(links_module).remove_links(check_val)
-- convert URL's of the form [URL DISPLAY] to the displayed value
check_val = check_val:gsub("%[[^ %[%]]* ([^%[%]]*)%]", "%1")
end
-- in case of negative page numbers (do they exist?), don't treat as multiple pages
check_val = check_val:gsub("^%-", "")
-- replace HTML entity en-dashes and em-dashes with their literal codes
check_val = check_val:gsub("–", "–")
check_val = check_val:gsub("–", "–")
check_val = check_val:gsub("—", "—")
check_val = check_val:gsub("—", "—")
-- Check for en-dash or em-dash, or two numbers (possibly with stuff after like 12a-15b)
-- separated by a hyphen or by comma a followed by a space (to avoid firing on thousands separators).
if rfind(check_val, "[–—]") or check_val:find(" and ") or rfind(check_val, "[0-9]+[^ ]* *%- *[0-9]+")
or rfind(check_val, "[0-9]+[^ ]* *, +[0-9]+") then
desc = get_plural_desc()
else
desc = singular_desc
end
end
local obj = sgobj or plobj
obj.text = val
val = format_text(obj)
numspec = desc .. " " .. val
end
end
local url = a(paramname .. "url")
if url then
return "[" .. url .. " " .. numspec .. "]"
else
return numspec
end
end
local volume = format_numeric_param("volume", a("volume_prefix") or "volume")
if volume then
add_with_sep(volume)
end
local issue = format_numeric_param("issue", a("issue_prefix") or "number")
if issue then
add_with_sep(issue)
end
-- number= is an alias for issue= (except in {{quote-av}}, where it is the episode number)
local number = format_numeric_param("number", a("number_prefix") or "number")
if number then
add_with_sep(number)
end
-- This function handles the display of annotations like "(in French)" or "(in German; quote in Nauruan)". It
-- takes two params PRETEXT and POSTTEXT to display before and after the annotation, respectively. These are
-- used to insert the surrounding parens, commas, etc. They are necessary because we don't always display the
-- annotation (in fact it's usually not displayed), and when there's no annotation, no pre-text or post-text is
-- displayed.
local function langhandler(pretext, posttext)
local argslang, argslang_param
if ind == "" then
argslang = args.lang or args[1]
argslang_param = 1
else
argslang, argslang_param = a_with_name("lang")
end
local worklang, worklang_param = a_with_name("worklang")
if worklang then
return pretext .. "in " .. format_langs(worklang, worklang_param) .. posttext
elseif argslang and a("termlang") and argslang ~= a("termlang") then
return pretext .. "in " .. format_langs(argslang, argslang_param) .. posttext
elseif not argslang and ind == "" then
return pretext .. maintenance_line("Please specify the language of the quote") .. posttext
end
return ""
end
if a("genre") then
add(" (" .. a("genre") .. (a("format") and ", " .. a("format") or "") .. langhandler(", ", "") .. ")")
sep = ", "
elseif a("format") then
add(" (" .. a("format") .. langhandler(", ", "") .. ")")
sep = ", "
else
local to_insert = langhandler(" (", ")")
if to_insert ~= "" then
sep = ", "
add(to_insert)
end
end
if a("others") then
add_with_sep(parse_and_format_text("others"))
end
local quoted_in = parse_and_format_text("quoted_in", tag_with_cite, tag_with_cite)
if quoted_in then
add_with_sep("quoted in " .. quoted_in)
table.insert(tracking_categories, "Quotations using quoted-in parameter")
end
local location = parse_and_format_text("location")
if a("publisher") then
if location then
add_with_sep(location .. ":") -- colon
sep = " "
end
add_with_sep(parse_and_format_text("publisher"))
elseif location then
add_with_sep(location)
end
if a("source") then
add_with_sep("sourced from " .. parse_and_format_text("source"))
end
local original = parse_and_format_text("original", tag_with_cite, tag_with_cite)
local by = parse_and_format_text("by")
if original or by then
add_with_sep((a("type") or "translation") .. " of " .. (original or "original") .. (by and " by " .. by or ""))
end
-- Fetch date_published=/year_published=/month_published= and format appropriately.
local formatted_date_published = format_date_args(a, get_full_paramname, alias_map, "", "_published")
local platform = parse_and_format_text("platform")
if formatted_date_published then
add_with_sep("published " .. formatted_date_published .. (platform and " via " .. platform or ""))
elseif platform then
add_with_sep("via " .. platform)
end
if ind ~= "" and has_newversion() then
local formatted_new_date, this_need_date = format_date_args(a, get_full_paramname, alias_map, "", "", nil,
"Please provide a date or year")
need_date = need_date or this_need_date
add_with_sep(formatted_new_date)
end
-- From here on out, there should always be a preceding item, so we
-- can dispense with add_with_sep() and always insert the comma.
local function small(txt)
add(", <small>")
add(txt)
add("</small>")
end
-- Add an identifier to a book or article database such as DOI, ISBN, JSTOR, etc. `param_or_params`
-- is a string identifying the base param, or a list of such strings to check in turn. If found, the value
-- of the parameter is processed using `process` (a function of one argument, defaulting to mw.uri.encode()),
-- and then the actual URL to insert is generated by preceding with `pretext`, following with `posttext`,
-- and running the resulting string through small(), which first adds a comma and then the URL in small font.
local function add_identifier(param_or_params, pretext, posttext, process)
local val = a(param_or_params)
if val then
val = (process or mw.uri.encode)(val)
small(pretext .. val .. posttext)
end
end
add_identifier("bibcode", "[https://adsabs.harvard.edu/abs/", " →Bibcode]")
add_identifier({"DOI", "doi"}, "<span class=\"neverexpand\">[https://doi.org/", " →DOI]</span>")
add_identifier({"ISBN", "isbn"}, "", "", isbn)
add_identifier({"ISSN", "issn"}, "", "", issn)
add_identifier({"JSTOR", "jstor"}, "[https://www.jstor.org/stable/", " →JSTOR]")
add_identifier({"LCCN", "lccn"}, "", "", lccn)
add_identifier({"OCLC", "oclc"}, "[https://www.worldcat.org/title/", " →OCLC]")
add_identifier({"OL", "ol"}, "[https://openlibrary.org/works/OL", "/ →OL]")
add_identifier({"PMID", "pmid"}, "[https://www.ncbi.nlm.nih.gov/pubmed/", " →PMID]")
add_identifier({"PMCID", "pmcid"}, "[https://www.ncbi.nlm.nih.gov/pmc/articles/", "/ →PMCID]")
add_identifier({"SSRN", "ssrn"}, "[https://ssrn.com/abstract=", " →SSRN]")
local id = a("id")
if id then
small(id)
end
local archiveurl, archiveurl_param = aurl_with_name("archiveurl")
if archiveurl then
add(", archived from ")
local url, url_param = aurl_with_name("url")
if not url then
-- attempt to infer original URL from archive URL; this works at
-- least for Wayback Machine (web.archive.org) URL's
url = rmatch(archiveurl, "/(https?:.*)$")
if not url then
error(("When |%s= is specified, |%s= must also be included"):format(archiveurl_param, url_param))
end
end
add("[" .. url .. " the original] on ")
local archivedate, archivedate_param = a_with_name("archivedate")
if archivedate then
add(format_date(archivedate))
elseif (string.sub(archiveurl, 1, 28) == "https://web.archive.org/web/") then
-- If the archive is from the Wayback Machine, then it already contains the date
-- Get the date and format into ISO 8601
local wayback_date = string.sub(archiveurl, 29, 29+7)
wayback_date = string.sub(wayback_date, 1, 4) .. "-" .. string.sub(wayback_date, 5, 6) .. "-" ..
string.sub(wayback_date, 7, 8)
add(format_date(wayback_date))
else
error(("When |%s= is specified, |%s= must also be included"):format(
archiveurl_param, archivedate_param))
end
end
if a("accessdate") then
--Otherwise do not display here, as already used as a fallback for missing date= or year= earlier.
if (a("date") or a("nodate") or a("year")) and not a("archivedate") then
add(", retrieved " .. format_date(a("accessdate")))
end
end
local formatted_section = format_chapterlike("section", "section ")
if formatted_section then
add(", ")
add(formatted_section)
end
local note = parse_and_format_text("note")
if note then
add(", " .. note)
end
local note_plain = parse_and_format_text("note_plain")
if note_plain then
add(" " .. note_plain)
end
-- Wrapper around format_numeric_param that inserts the formatted text with optional preceding text.
local function handle_numeric_param(paramname, singular_desc, pretext)
local numspec = format_numeric_param(paramname, singular_desc)
if numspec then
add((pretext or "") .. numspec)
end
end
handle_numeric_param("line", a("line_prefix") or "line", ", ")
handle_numeric_param("page", a("page_prefix") or "page", ", ")
handle_numeric_param("column", a("column_prefix") or "column", ", ")
-- FIXME: Does this make sense? What is other=?
local other = parse_and_format_text("other")
if other then
add(", " .. other)
end
end
-- Display all the text that comes after the author, for the main portion.
postauthor("", "")
local sep
-- If there's a "newversion" section, add the new-version text.
if has_newversion() then
--Test for new version of work.
add(SEMICOLON_SPACE)
if args.newversion then -- newversion= is intended for English text, e.g. "quoted in" or "republished as".
add(args.newversion)
elseif not args.edition2 then
if has_new_title_or_author() then
add("republished as")
else
add("republished")
end
end
add(" ")
sep = ""
else
sep = ", "
end
-- Add the author(s).
if args["2ndauthor"] or args["2ndlast"] then
add(" ")
local authorlink = args["2ndauthorlink"]
local function make_author_with_url(txt)
if authorlink then
return "[[w:" .. authorlink .. "|" .. txt .. "]]"
else
return txt
end
end
local author = args["2ndauthor"]
if author then
local authorobj = parse_text_with_lang(author, "2ndauthor")
authorobj.text = make_author_with_url(authorobj.text)
add(format_text(authorobj))
else
local last = args["2ndlast"]
local first = args["2ndfirst"]
-- Author separated into first name + last name. We don't currently support non-Latin-script
-- authors separated this way and probably never will.
if first then
author = first .. " " .. last
else
author = last
end
author = make_author_with_url(author)
add(author)
end
-- FIXME, we should use sep = ", " here too and fix up the handling
-- of chapter/mainauthor/etc. in postauthor() to use add_with_sep().
if has_new_title_or_ancillary_author() then
add(", ")
sep = ""
else
sep = ", "
end
end
-- Display all the text that comes after the author, for the "newversion" section.
postauthor(2, sep)
add(":")
-- Concatenate output portions to form output text.
local output_text = table.concat(output)
-- Remainder of code handles adding categories. We add one or more of the following categories:
--
-- 1. [[Category:LANG terms with quotations]], based on the first language code in termlang= or 1=. Not added to
-- non-main-namespace pages except for Reconstruction: and Appendix:. Not added if 1= is missing or nocat= is
-- given.
-- 2. [[Category:Requests for date in LANG entries]], based on the first language code in 1=. Added under the same
-- circumstances as above.
-- 3. [[Category:Quotations using nocat parameter]], if nocat= is given. Added to mainspace, Reconstruction: and
-- Appendix: pages.
local categories = {}
local langcode = args.termlang or argslang or "und"
langcode = rsplit(langcode, ",")[1]
local lang = require(languages_module).getByCode(langcode, true)
if args.nocat then
table.insert(tracking_categories, "Quotations using nocat parameter")
elseif argslang then
if lang then
table.insert(categories, lang:getCanonicalName() .. " terms with quotations")
end
if need_date then
local argslangcode = rsplit(argslang, ",")[1]
local argslangobj = require(languages_module).getByCode(argslangcode, 1)
table.insert(categories, "Requests for date in " .. argslangobj:getCanonicalName() .. " entries")
end
else
-- Only allowable on non-mainspace pages, where we don't add categories.
end
local FULLPAGENAME = mw.title.getCurrentTitle().fullText
return output_text .. (not lang and "" or
(#categories > 0 and require(utilities_module).format_categories(categories, lang) or "") ..
(#tracking_categories > 0 and require(utilities_module).format_categories(tracking_categories, lang, nil, nil,
not require(usex_templates_module).page_should_be_ignored(FULLPAGENAME)) or ""))
end
-- External interface, meant to be called from a template.
-- FIXME: Remove this in favor of using quote_t.
function export.source_t(frame)
local parent_args = frame:getParent().args
local args, alias_map = clone_args(frame.args, parent_args)
return export.source(args, alias_map)
end
-- External interface, meant to be called from a template. Replaces {{quote-meta}} and meant to be the primary
-- interface for {{quote-*}} templates.
function export.quote_t(frame)
local parent_args = frame:getParent().args
local args, alias_map = clone_args(frame.args, parent_args)
local deprecated = args.lang
local function yesno(val)
if not val then
return false
end
return require(yesno_module)(val)
end
args.nocat = yesno(args.nocat)
args.brackets = yesno(args.brackets)
local text = args.text or args.passage
local gloss = args.t or args.gloss or args.translation
local parts = {}
local function ins(text)
table.insert(parts, text)
end
ins('<div class="citation-whole"><span class="cited-source">')
ins(export.source(args, alias_map))
ins("</span><dl><dd>")
-- If any quote-related args are present, display the actual quote; otherwise, display nothing.
local tr = args.tr or args.transliteration
local ts = args.ts or args.transcription
local norm = args.norm or args.normalization
local sc = args.sc and require(scripts_module).getByCode(args.sc, "sc") or nil
local normsc = args.normsc == "auto" and args.normsc or args.normsc and require(scripts_module).getByCode(args.normsc, "normsc") or nil
if text or gloss or tr or ts or norm then
local langcodes = args[1] or args.lang
local langcode = langcodes and rsplit(langcodes, ",")[1] or nil
local usex_data = {
-- Pass "und" here rather than cause an error; there will be an error on mainspace, Citations, etc. pages
-- in any case in source() if the language is omitted.
lang = require(languages_module).getByCode(langcode or "und", 1),
usex = text,
sc = sc,
translation = gloss,
normalization = norm,
normsc = normsc,
transliteration = tr,
transcription = ts,
brackets = args.brackets,
substs = args.subst,
lit = args.lit,
footer = args.footer,
-- pass true here because source() already adds 'LANG terms with quotations'
nocat = true,
quote = "quote-meta",
}
ins(require(usex_module).format_usex(usex_data))
end
ins("</dd></dl></div>")
local retval = table.concat(parts)
return deprecated and frame:expandTemplate{title = "check deprecated lang param usage", args = {retval, lang = args.lang}} or retval
end
-- External interface, meant to be called from a template.
function export.call_quote_template(frame)
local iparams = {
["template"] = {},
["textparam"] = {},
["pageparam"] = {},
["allowparams"] = {list = true},
["propagateparams"] = {list = true},
}
local iargs, other_direct_args = require(parameters_module).process(frame.args, iparams, "return unknown", "quote", "call_quote_template")
local direct_args = {}
for pname, param in pairs(other_direct_args) do
direct_args[pname] = ine(param)
end
local function process_paramref(paramref)
if not paramref then
return {}
end
local params = rsplit(paramref, "%s*,%s*")
for i, param in ipairs(params) do
if rfind(param, "^[0-9]+$") then
param = tonumber(param)
end
params[i] = param
end
return params
end
local function fetch_param(source, params)
for _, param in ipairs(params) do
if source[param] then
return source[param]
end
end
return nil
end
local params = {
["text"] = {},
["passage"] = {},
["footer"] = {},
["brackets"] = {},
}
local textparams = process_paramref(iargs.textparam)
for _, param in ipairs(textparams) do
params[param] = {}
end
local pageparams = process_paramref(iargs.pageparam)
if #pageparams > 0 then
params["page"] = {}
params["pages"] = {}
for _, param in ipairs(pageparams) do
params[param] = {}
end
end
local parent_args = frame:getParent().args
local allow_all = false
for _, allowspec in ipairs(iargs.allowparams) do
for _, allow in ipairs(rsplit(allowspec, "%s*,%s*")) do
local param = rmatch(allow, "^(.*):list$")
if param then
if rfind(param, "^[0-9]+$") then
param = tonumber(param)
end
params[param] = {list = true}
elseif allow == "*" then
allow_all = true
else
if rfind(allow, "^[0-9]+$") then
allow = tonumber(allow)
end
params[allow] = {}
end
end
end
local params_to_propagate = {}
for _, propagate_spec in ipairs(iargs.propagateparams) do
for _, param in ipairs(process_paramref(propagate_spec)) do
table.insert(params_to_propagate, param)
params[param] = {}
end
end
local args = require(parameters_module).process(parent_args, params, allow_all, "quote", "call_quote_template")
parent_args = require(table_module).shallowcopy(parent_args)
if textparams[1] ~= "-" then
other_direct_args.passage = args.text or args.passage or fetch_param(args, textparams)
end
if #pageparams > 0 and pageparams[1] ~= "-" then
other_direct_args.page = fetch_param(args, pageparams) or args.page or nil
other_direct_args.pages = args.pages
end
if args.footer then
other_direct_args.footer = frame:expandTemplate { title = "small", args = {args.footer} }
end
other_direct_args.brackets = args.brackets
if not other_direct_args.authorlink and not other_direct_args.author:find("[%[<]") then
other_direct_args.authorlink = other_direct_args.author
end
for _, param in ipairs(params_to_propagate) do
if args[param] then
other_direct_args[param] = args[param]
end
end
return frame:expandTemplate { title = iargs.template or "quote-book", args = other_direct_args }
end
local paramdoc_param_replacements = {
passage = {
param_with_synonym = '<<synonym>>, {{para|text}}, or {{para|passage}}',
param_no_synonym = '{{para|text}} or {{para|passage}}',
text = [=[
* <<params>> – the passage to be quoted.]=],
},
page = {
param_with_synonym = '<<synonym>> or {{para|page}}, or {{para|pages}}',
param_no_synonym = '{{para|page}} or {{para|pages}}',
text = [=[
* <<params>> – '''mandatory in some cases''': the page number(s) quoted from. When quoting a range of pages, note the following:
** Separate the first and last pages of the range with an [[en dash]], like this: {{para|pages|10–11}}.
** You must also use {{para|pageref}} to indicate the page to be linked to (usually the page on which the Wiktionary entry appears).
: This parameter must be specified to have the template link to the online version of the work.]=]
},
page_with_roman_preface = {
param_with_synonym = {"inherit", "page"},
param_no_synonym = {"inherit", "page"},
text = [=[
* <<params>> – '''mandatory in some cases''': the page number(s) quoted from. If quoting from the preface, specify the page number(s) in lowercase Roman numerals. When quoting a range of pages, note the following:
** Separate the first and last page number of the range with an [[en dash]], like this: {{para|pages|10–11}} or {{para|pages|iii–iv}}.
** You must also use {{para|pageref}} to indicate the page to be linked to (usually the page on which the Wiktionary entry appears).
: This parameter must be specified to have the template link to the online version of the work.]=]
},
chapter = {
param_with_synonym = '<<synonym>> or {{para|chapter}}',
param_no_synonym = '{{para|chapter}}',
text = [=[
* <<params>> – the name of the chapter quoted from.]=],
},
roman_chapter = {
param_with_synonym = {"inherit", "chapter"},
param_no_synonym = {"inherit", "chapter"},
text = [=[
* <<params>> – the chapter number quoted from in uppercase Roman numerals.]=],
},
arabic_chapter = {
param_with_synonym = {"inherit", "chapter"},
param_no_synonym = {"inherit", "chapter"},
text = [=[
* <<params>> – the chapter number quoted from in Arabic numerals.]=],
},
trailing_params = {
text = [=[
* {{para|footer}} – a comment on the passage quoted.
* {{para|brackets}} – use {{para|brackets|on}} to surround a quotation with [[bracket]]s. This indicates that the quotation either contains a mere mention of a term (for example, “some people find the word '''''manoeuvre''''' hard to spell”) rather than an actual use of it (for example, “we need to '''manoeuvre''' carefully to avoid causing upset”), or does not provide an actual instance of a term but provides information about related terms.]=],
}
}
function export.paramdoc(frame)
local params = {
[1] = {},
}
local parargs = frame:getParent().args
local args = require(parameters_module).process(parargs, params, nil, "quote", "paramdoc")
local text = args[1]
local function do_param_with_optional_synonym(param, text_to_sub, paramtext_synonym, paramtext_no_synonym)
local function sub_param(synonym)
local subbed_paramtext
if synonym then
subbed_paramtext = rsub(paramtext_synonym, "<<synonym>>", "{{para|" .. synonym .. "}}")
else
subbed_paramtext = paramtext_no_synonym
end
return frame:preprocess(rsub(text_to_sub, "<<params>>", subbed_paramtext))
end
text = rsub(text, "<<" .. param .. ">>", function() return sub_param() end)
text = rsub(text, "<<" .. param .. ":(.-)>>", sub_param)
end
local function fetch_text(param_to_replace, key)
local spec = paramdoc_param_replacements[param_to_replace]
local val = spec[key]
if type(val) == "string" then
return val
end
if type(val) == "table" and val[1] == "inherit" then
return fetch_text(val[2], key)
end
error("Internal error: Unrecognized value for param '" .. param_to_replace .. "', key '" .. key .. "': "
.. mw.dumpObject(val))
end
for param_to_replace, spec in pairs(paramdoc_param_replacements) do
local function fetch(key)
return fetch_text(param_to_replace, key)
end
if not spec.param_no_synonym then
-- Text to substitute directly.
text = rsub(text, "<<" .. param_to_replace .. ">>", function() return frame:preprocess(fetch("text")) end)
else
do_param_with_optional_synonym(param_to_replace, fetch("text"), fetch("param_with_synonym"),
fetch("param_no_synonym"))
end
end
-- Remove final newline so template code can add a newline after invocation
text = text:gsub("\n$", "")
return text
end
return export