Module:Quote
Documentation for this module may be created at Module:Quote/documentation
--[=[
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 dialect_tags_module = "Module:dialect tags"
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:parse utilities"
local qualifier_module = "Module:qualifier"
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
local u = mw.ustring.char
-- Use HTML entities here to avoid parsing issues (esp. with brackets)
local SEMICOLON_SPACE = "; "
local SPACE_LBRAC = " ["
local RBRAC = "]"
local TEMP_LT = u(0xFFF1)
local TEMP_GT = u(0xFFF2)
local TEMP_LBRAC = u(0xFFF3)
local TEMP_RBRAC = u(0xFFF4)
local TEMP_SEMICOLON = u(0xFFF5)
local L2R = u(0x200E)
local R2L = u(0x200F)
html_entity_to_replacement = {
{entity="<", repl=TEMP_LT},
{entity=">", repl=TEMP_GT},
{entity="[", entity_pat="�*91;", repl=TEMP_LBRAC},
{entity="]", entity_pat="�*93;", repl=TEMP_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 ""
end
local function isbn(text)
return "[[Special:BookSources/" .. text .. "|→ISBN]]" ..
require(check_isxn_module).check_isbn(text, " [[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, " [[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
local function split_on_comma(term)
if term:find(",%s") then
return require(parse_utilities_module).split_on_comma(term)
else
return rsplit(term, ",")
end
end
local function yesno(val, default)
if not val then
return default
end
return require(yesno_module)(val, default)
end
-- Convert a raw tag= param (or nil) to a list of formatted dialect tags; unrecognized tags are passed through
-- unchanged. Return nil if nil passed in.
local function tags_to_dialects(lang, tags)
if not tags then
return nil
end
return require(dialect_tags_module).make_dialects(split_on_comma(tags), lang)
end
-- Convert a comma-separated list of language codes to a comma-separated list of language names. `fullname` is the
-- name of the parameter from which the list of language codes was fetched.
local function format_langs(langs, fullname)
langs = rsplit(langs, ",")
for i, langcode in ipairs(langs) do
local lang = require(languages_module).getByCode(langcode, fullname, "allow etym")
langs[i] = lang:getCanonicalName()
end
if #langs == 1 then
return langs[1]
else
return require(table_module).serialCommaJoin(langs)
end
end
local function get_first_lang(langs, fullname, get_non_etym, required)
local langcode = langs and rsplit(langs, ",")[1] or not required and "und" or nil
local lang = require(languages_module).getByCode(langcode, fullname, "allow etym")
if get_non_etym then
lang = lang:getNonEtymological()
end
return lang
end
--[=[
Normally we parse off inline modifiers and language code prefixes in various places, e.g. he:מריםame("urls")
if url and urls then
error(("Supply only one of |%s= and |%s="):format(url_fullname, urls_fullname))
end
local function verify_title_supplied(url_name)
if not title then
-- There are too many cases of this to throw an error at this time.
-- error(("If |%s= is given, |%s= must also be supplied"):format(url_name, title_fullname))
end
end
if archiveurl or url then
verify_title_supplied(archiveurl and archiveurl_fullname or url_fullname)
sep = nil
add("‎<sup>[" .. (archiveurl or url) .. "]</sup>")
elseif urls then
verify_title_supplied(urls_fullname)
sep = nil
add("‎<sup>" .. urls .. "</sup>")
end
if need_comma then
sep = ", "
end
local edition, edition_fullname = parse_and_format_annotated_text_with_name("edition")
local edition_plain, edition_plain_fullname = parse_and_format_annotated_text_with_name("edition_plain")
if edition and edition_plain then
error(("Supply only one of |%s= and |%s="):format(edition_fullname, edition_plain_fullname))
end
if edition then
add_with_sep(edition .. " edition")
end
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_fullname = a_with_name(paramname)
local sgobj = parse_annotated_text(sgval, paramname)
local plparamname = paramname .. "s"
local plval, pl_fullname = a_with_name(plparamname)
local plobj = parse_annotated_text(plval, plparamname)
local plainval, plain_fullname = parse_and_format_annotated_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_fullname)
insparam(pl_fullname)
insparam(plain_fullname)
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
if obj.link:find("^!") then
obj.link = obj.link:gsub("^!", "")
end
val = format_annotated_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
local annotations = {}
local genre = a("genre")
if genre then
table.insert(annotations, genre)
end
local format = a("format")
if format then
table.insert(annotations, format)
end
local medium = a("medium")
if medium then
table.insert(annotations, medium)
end
-- Now handle the display of language annotations like "(in French)" or
-- "(quotation in Nauruan; overall work in German)".
local quotelang = args.lang or args[1]
local quotelang_fullname = 1
if not quotelang then
if ind == "" then
-- This can only happen for certain non-mainspace pages, e.g. Talk pages; otherwise an error is thrown
-- above.
table.insert(annotations, maintenance_line("Please specify the language of the quote using |1="))
else
-- do nothing in newversion= portion
end
elseif ind == "" then
local worklang, worklang_fullname = a_with_name("worklang")
local termlang, termlang_fullname = a_with_name("termlang")
worklang = worklang or quotelang
termlang = termlang or quotelang
if worklang == quotelang then
if worklang == termlang then
-- do nothing
else
table.insert(annotations, "in " .. format_langs(quotelang, quotelang_fullname))
end
else
if quotelang ~= termlang then
table.insert(annotations, "quotation in " .. format_langs(quotelang, quotelang_fullname))
end
table.insert(annotations, "overall work in " .. format_langs(worklang, worklang_fullname))
end
else
local lang2, lang2_fullname = a_with_name("lang")
if lang2 then
table.insert(annotations, "in " .. format_langs(lang2, lang2_fullname))
end
end
if #annotations > 0 then
sep = nil
add_with_sep(" (" .. table.concat(annotations, SEMICOLON_SPACE) .. ")")
end
local artist = parse_and_format_multivalued_annotated_text("artist", "and")
if artist then
add_with_sep("performed by " .. artist)
end
local feat = parse_and_format_multivalued_annotated_text("feat", "and")
if feat then
sep = " "
add_with_sep("ft. " .. feat)
end
local role = parse_and_format_multivalued_annotated_text("role", "and")
local actor_val, actor_fullname = a_with_name("actor")
local actor_objs = parse_multivalued_annotated_text(actor_val, actor_fullname)
local actor = format_multivalued_annotated_text(actor_objs, "and")
if role then
add_with_sep("spoken by " .. role)
if actor then
sep = nil
add_with_sep(" (" .. actor .. ")")
end
elseif actor then
add_with_sep(actor .. " (" .. (#actor_objs > 1 and "actors" or "actor") .. ")")
end
local others = parse_and_format_annotated_text("others")
if others then
add_with_sep(others)
end
local quoted_in = parse_and_format_annotated_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_multivalued_annotated_text("location")
local publisher = parse_and_format_multivalued_annotated_text("publisher", "; ")
if publisher then
if location then
add_with_sep(location) -- colon
sep = ": " -- colon
end
add_with_sep(publisher)
elseif location then
add_with_sep(location)
end
local source = parse_and_format_multivalued_annotated_text("source", "and")
if source then
add_with_sep("sourced from " .. source)
end
local original = parse_and_format_annotated_text("original", tag_with_cite, tag_with_cite)
local by = parse_and_format_multivalued_annotated_text("by", "and")
local origtype = a("deriv") or "translation"
if original or by then
add_with_sep(origtype .. " of " .. (original or "original") .. (by and " by " .. by or ""))
end
-- Handle origlang=, origworklang=. How we handle them depends on whether the original title or author are explicitly
-- given.
local origlang, origlang_fullname = a_with_name("origlang")
local origworklang, origworklang_fullname = a_with_name("origworklang")
local origlangtext, origworklangtext
if origlang then
origlangtext = "in " .. format_langs(origlang, origlang_fullname)
end
if origworklang then
origworklangtext = "overall work in " .. format_langs(origworklang, origworklang_fullname)
end
if origlang or origworklang then
if original or by then
local orig_annotations = {}
if origlangtext then
table.insert(orig_annotations, origlangtext)
end
if origworklangtext then
table.insert(orig_annotations, origworklangtext)
end
sep = nil
add_with_sep(" (" .. table.concat(orig_annotations, SEMICOLON_SPACE) .. ")")
else
add_with_sep(origtype .. " of original" .. (origlangtext and " " .. origlangtext or ""))
if origworklangtext then
sep = nil
add_with_sep(" (" .. origworklangtext .. ")")
end
end
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_multivalued_annotated_text("platform", "and")
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
if formatted_new_date then
add_with_sep(formatted_new_date)
end
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.
sep = nil
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", "<span class=\"neverexpand\">[https://doi.org/", " →DOI]</span>")
add_identifier("isbn", "", "", isbn)
add_identifier("issn", "", "", issn)
add_identifier("jstor", "[https://www.jstor.org/stable/", " →JSTOR]")
add_identifier("lccn", "", "", lccn)
add_identifier("oclc", "[https://www.worldcat.org/title/", " →OCLC]")
add_identifier("ol", "[https://openlibrary.org/works/OL", "/ →OL]")
add_identifier("pmid", "[https://www.ncbi.nlm.nih.gov/pubmed/", " →PMID]")
add_identifier("pmcid", "[https://www.ncbi.nlm.nih.gov/pmc/articles/", "/ →PMCID]")
add_identifier("ssrn", "[https://ssrn.com/abstract=", " →SSRN]")
local id = a("id")
if id then
small(id)
end
local archiveurl, archiveurl_fullname = aurl_with_name("archiveurl")
if archiveurl then
add(", archived from ")
local url, url_fullname = 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_fullname,
url_fullname))
end
end
add("[" .. url .. " the original] on ")
local archivedate, archivedate_fullname = 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_fullname, archivedate_fullname))
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
-- video game stuff
local system = parse_and_format_annotated_text("system")
if system then
add(", " .. system)
end
local scene = parse_and_format_annotated_text("scene")
if scene then
add(", scene: " .. scene)
end
local level = parse_and_format_annotated_text("level")
if level then
add(", level/area: " .. level)
end
local note = parse_and_format_annotated_text("note")
if note then
add(", " .. note)
end
local note_plain = parse_and_format_annotated_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("page", a("page_prefix") or "page", ", ")
handle_numeric_param("column", a("column_prefix") or "column", ", ")
handle_numeric_param("line", a("line_prefix") or "line", ", ")
-- FIXME: Does this make sense? What is other=?
local other = parse_and_format_annotated_text("other")
if other then
add(", " .. other)
end
end
-- Display all the text that comes after the author, for the main portion.
postauthor("", num_authors)
author_outputted = false
-- If there's a "newversion" section, add the new-version text.
if has_newversion() then
sep = nil
--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 newversion author(s).
if args["2ndauthor"] or args["2ndlast"] then
num_authors = add_author(args["2ndauthor"], "2ndauthor", nil, nil, args["2ndauthorlink"], "2ndauthorlink", nil,
nil, args["2ndfirst"], "2ndfirst", nil, nil, args["2ndlast"], "2ndlast", nil, nil)
sep = ", "
else
for _, cant_have in ipairs { "2ndauthorlink", "2ndfirst" } do
if args[cant_have] then
error(("Can't have |%s= without |2ndauthor= or |2ndlast="):format(cant_have))
end
end
end
-- Display all the text that comes after the author, for the "newversion" section.
postauthor(2, num_authors)
if not args.nocolon then
sep = nil
add(":")
end
-- 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=. Added to
-- mainspace, Reconstruction: and Appendix: pages as well as Citations: pages if the corresponding mainspace
-- page exists. Not added if nocat= is given. Note that [[Module:usex]] adds the same category using the same
-- logic, but we do it here too because we may not have a quotation to format. (We add in those circumstances
-- because typically when there's no quotation to format, it's because it's formatted manually underneath the
-- citation, or using {{ja-x}}, {{th-x}} or similar.)
-- 2. [[Category:Requests for date in LANG entries]], based on the first language code in 1=. Added to mainspace,
-- Reconstruction:, Appendix: and Citations: pages unless nocat= is given.
-- 3. [[Category:Quotations using nocat parameter]], if nocat= is given. Added to mainspace, Reconstruction:,
-- Appendix: and Citations: pages.
local categories = {}
local termlang = get_first_lang(args.termlang or argslang, true)
if args.nocat then
table.insert(tracking_categories, "Quotations using nocat parameter")
else
local title
if args.pagename then -- for testing, doc pages, etc.
title = mw.title.new(args.pagename)
if not title then
error(("Bad value for `args.pagename`: '%s'"):format(args.pagename))
end
else
title = mw.title.getCurrentTitle()
end
-- Only add [[Citations:foo]] to [[:Category:LANG terms with quotations]] if [[foo]] exists.
local ok_to_add_cat
if title.nsText ~= "Citations" then
ok_to_add_cat = true
else
local mainspace_title = mw.title.new(title.text)
if mainspace_title and mainspace_title.exists then
ok_to_add_cat = true
end
end
if ok_to_add_cat then
table.insert(categories, termlang:getNonEtymologicalName() .. " terms with quotations")
end
if need_date then
local argslangobj = get_first_lang(argslang, 1)
table.insert(categories, "Requests for date in " .. argslangobj:getCanonicalName() .. " entries")
end
end
local FULLPAGENAME = mw.title.getCurrentTitle().fullText
return output_text .. (not lang and "" or
(#categories > 0 and require(utilities_module).format_categories(categories, lang, args.sort) or "") ..
(#tracking_categories > 0 and require(utilities_module).format_categories(tracking_categories, lang, args.sort,
nil, not require(usex_templates_module).page_should_be_ignored(FULLPAGENAME)) or ""))
end
-- Alias specs for type= and type2=. Each spec is `{canon, aliases, with_newversion}` where `canon` is the canonical
-- parameter (with "2" added if type2= is being handled), `aliases` is a comma-separated string of aliases (with "2"
-- added if type2= is being handled, except for numeric params), and `with_newversion` indicates whether we should
-- process this spec if type2= is being handled.
local type_alias_specs = {
book = {
{"author", "3"},
{"chapter", "entry", true},
{"chapterurl", "entryurl", true},
{"trans-chapter", "trans-entry", true},
{"chapter_series", "entry_series", true},
{"chapter_seriesvolume", "entry_seriesvolume", true},
{"chapter_number", "entry_number", true},
{"chapter_plain", "entry_plain", true},
{"title", "4"},
{"url", "5"},
{"year", "2"},
{"page", "6"},
{"text", "7"},
{"t", "8"},
},
journal = {
{"year", "2"},
{"author", "3"},
{"chapter", "title,article,4", true},
{"chapterurl", "titleurl,articleurl", true},
{"trans-chapter", "trans-title,trans-article", true},
{"chapter_tlr", "article_tlr", true},
{"chapter_series", "article_series", true},
{"chapter_seriesvolume", "article_seriesvolume", true},
{"chapter_number", "article_number", true},
{"chapter_plain", "title_plain,article_plain", true},
{"title", "journal,magazine,newspaper,work,5", true},
{"trans-title", "trans-journal,trans-magazine,trans-newspaper,trans-work", true},
{"url", "6"},
{"page", "7"},
{"source", "newsagency", true},
{"text", "8"},
{"t", "9"},
},
}
-- Process interally-handled aliases related to type= or type2=. `args` is a table of arguments; `typ` is the value of
-- type= or type2=; newversion=true if we're dealing with type2=; alias_map is used to keep track of alias mappings
-- seen.
local function process_type_aliases(args, typ, newversion, alias_map)
local ind = newversion and "2" or ""
local deprecated = ine(args.lang)
if not type_alias_specs[typ] then
local possible_values = {}
for possible, _ in pairs(type_alias_specs) do
table.insert(possible_values, possible)
end
table.sort(possible_values)
error(("Unrecognized value '%s' for type%s=; possible values are %s"):format(
typ, ind, table.concat(possible_values, ",")))
end
for _, alias_spec in ipairs(type_alias_specs[typ]) do
local canon, aliases, with_newversion = unpack(alias_spec)
if with_newversion or not newversion then
canon = canon .. ind
aliases = rsplit(aliases, ",")
local saw_alias = nil
for _, alias in ipairs(aliases) do
if rfind(alias, "^[0-9]+$") then
alias = tonumber(alias)
if deprecated then
alias = alias - 1
end
else
alias = alias .. ind
end
if args[alias] then
if saw_alias == nil then
saw_alias = alias
else
error(("|%s= and |%s= are aliases; cannot specify a value for both"):format(saw_alias, alias))
end
end
end
if saw_alias and (not newversion or type(saw_alias) == "string") then
if args[canon] then
error(("|%s= is an alias of |%s=; cannot specify a value for both"):format(saw_alias, canon))
end
args[canon] = args[saw_alias]
-- 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 due to unhandled
-- parameter checking.
args[saw_alias] = nil
alias_map[canon] = saw_alias
end
end
end
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/Template: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
local alias_map = {}
-- Process internally-specified aliases using type= or type2=.
local typ = args.type or direct_args.type
if typ then
process_type_aliases(args, typ, false, alias_map)
end
local typ2 = args.type2 or direct_args.type2
if typ2 then
process_type_aliases(args, typ2, true, alias_map)
end
-- Process externally-specified 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. For 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).
--
-- FIXME: Delete this once we've converted all alias processing to internal.
local aliases = ine(direct_args.alias)
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*")
local saw_source = nil
for _, source in ipairs(sources) do
if rfind(source, "^[0-9]+$") then
source = tonumber(source)
end
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 due to 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
-- ignore control params
if pname ~= "ignore" and pname ~= "alias" and pname ~= "error_if" then
args[pname] = ine(param)
end
end
return 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)
-- FIXME: We are processing arguments twice, once in clone_args() and then again in [[Module:parameters]]. This is
-- wasteful of memory.
local parent_args = frame:getParent().args
local cloned_args, alias_map = clone_args(frame.args, parent_args)
local deprecated = ine(parent_args.lang)
-- First, the "single" params that don't have FOO2 or FOOn versions.
local params = {
[deprecated and "lang" or 1] = {required = true, default = "und"},
newversion = {},
["2ndauthor"] = {},
["2ndauthorlink"] = {},
["2ndfirst"] = {},
["2ndlast"] = {},
nocat = {type = "boolean"},
nocolon = {type = "boolean"},
lang2 = {},
-- quote params
text = {},
passage = {alias_of = "text"},
tr = {},
transliteration = {alias_of = "tr"},
ts = {},
transcription = {alias_of = "ts"},
norm = {},
normalization = {alias_of = "norm"},
sc = {},
normsc = {},
sort = {},
subst = {},
footer = {},
lit = {},
t = {},
translation = {alias_of = "t"},
gloss = {alias_of = "t"},
tag = {},
brackets = {type = "boolean"},
-- original quote params
origtext = {},
origtr = {},
origts = {},
orignorm = {},
origsc = {},
orignormsc = {},
origsubst = {},
origtag = {},
}
-- Then the list params (which have FOOn versions).
local list_spec = {list = true, allow_holes = true}
for _, list_param in ipairs {
"author", "last", "first", "authorlink", "trans-author", "trans-last", "trans-first", "trans-authorlink"
} do
params[list_param] = list_spec
end
-- Then the newversion params (which have FOO2 versions).
for _, param12 in ipairs {
-- author-like params; author params themselves are either list params (author=, last=, etc.) or single params
-- (2ndauthor=, 2ndlast=, etc.)
"coauthors", "quotee", "tlr", "editor", "editors", "mainauthor", "compiler", "compilers", "director", "directors",
"lyricist", "lyrics-translator", "composer", "role", "actor", "artist", "feat",
-- author control params
"default-authorlabel",
"authorlabel",
-- title
"title", "trans-title", "series", "seriesvolume", "notitle",
-- chapter
"chapter", "chapterurl", "chapter_number", "chapter_plain", "chapter_series", "chapter_seriesvolume",
"trans-chapter", "chapter_tlr",
-- section
"section", "sectionurl", "section_number", "section_plain", "section_series", "section_seriesvolume",
"trans-section",
-- other video-game params
"system", "scene", "level",
-- URL
"url", "urls", "archiveurl",
-- edition
"edition", "edition_plain",
-- language params
"worklang", "termlang", "origlang", "origworklang",
-- ID params
"bibcode", "doi", "isbn", "issn", "jstor", "lccn", "oclc", "ol", "pmid", "pmcid", "ssrn", "id",
-- misc date params; most date params handled below
"archivedate", "accessdate", "nodate",
-- numeric params handled below
-- other params
"type", "genre", "format", "medium", "others", "quoted_in", "location", "publisher",
"original", "by", "deriv",
"note", "note_plain",
"other", "source", "platform",
} do
params[param12] = {}
params[param12 .. "2"] = {}
end
-- Then the aliases of newversion params (which have FOO2 versions).
for _, param12_aliased in ipairs {
{"role", "roles"},
{"role", "speaker"},
{"tlr", "translator"},
{"tlr", "translators"},
{"doi", "DOI"},
{"isbn", "ISBN"},
{"issn", "ISSN"},
{"jstor", "JSTOR"},
{"lccn", "LCCN"},
{"oclc", "OCLC"},
{"ol", "OL"},
{"pmid", "PMID"},
{"pmcid", "PMCID"},
{"ssrn", "SSRN"},
} do
local canon, alias = unpack(param12_aliased)
params[alias] = {alias_of = canon}
params[alias .. "2"] = {alias_of = canon .. "2"}
end
-- Then the date params.
for _, datelike in ipairs { {"", ""}, {"orig", ""}, {"", "_published"} } do
local pref, suf = unpack(datelike)
for _, arg in ipairs { "date", "year", "month", "start_date", "start_year", "start_month" } do
params[pref .. arg .. suf] = {}
params[pref .. arg .. suf .. "2"] = {}
end
end
-- Then the numeric params.
for _, numeric in ipairs { "volume", "issue", "number", "line", "page", "column" } do
for _, suf in ipairs { "", "s", "_plain", "url", "_prefix" } do
params[numeric .. suf] = {}
params[numeric .. suf .. "2"] = {}
end
end
local args = require(parameters_module).process(cloned_args, params, nil, "quote", "quote_t")
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>")
local text = args.text
local gloss = args.t
local tr = args.tr
local ts = args.ts
local norm = args.norm
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
-- Fetch original-text parameters.
local origtext, origtextlang, origsc, orignormsc
if args.origtext then
-- Wiktionary language codes have at least two lowercase letters followed possibly by lowercase letters and/or
-- hyphens (there are more restrictions but this is close enough). Also check for nonstandard Latin etymology
-- language codes (e.g. VL. or LL.). (There used to be more nonstandard codes but they have all been
-- eliminated.)
origtextlang, origtext = args.origtext:match("^([a-z][a-z][a-z-]*):([^ ].*)$")
if not origtextlang then
-- Special hack for Latin variants, which can have nonstandard etym codes, e.g. VL., LL.
origtextlang, origtext = args.origtext:match("^([A-Z]L%.):([^ ].*)$")
end
if not origtextlang then
error("origtext= should begin with a language code prefix")
end
origtextlang = require("Module:languages").getByCode(origtextlang, "origtext", "allow etym")
origsc = args.origsc and require(scripts_module).getByCode(args.origsc, "origsc") or nil
orignormsc = args.orignormsc == "auto" and args.orignormsc or
args.orignormsc and require(scripts_module).getByCode(args.orignormsc, "orignormsc") or nil
else
for _, noparam in ipairs { "origtr", "origts", "origsc", "orignorm", "orignormsc", "origsubst", "origtag" } do
if args[noparam] then
error(("Cannot specify %s= without origtext="):format(noparam))
end
end
end
-- If any quote-related args are present, display the actual quote; otherwise, display nothing.
if text or gloss or tr or ts or norm or args.origtext then
-- 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.
local lang = get_first_lang(args[1] or args.lang, 1)
local termlang = args.termlang and get_first_lang(args.termlang, "termlang") or lang
local usex_data = {
lang = lang,
termlang = termlang,
usex = text,
sc = sc,
translation = gloss,
normalization = norm,
normsc = normsc,
transliteration = tr,
transcription = ts,
brackets = args.brackets,
subst = args.subst,
lit = args.lit,
footer = args.footer,
qq = tags_to_dialects(lang, args.tag),
quote = "quote-meta",
orig = origtext,
origlang = origtextlang,
origsc = origsc,
orignorm = args.orignorm,
orignormsc = orignormsc,
origtr = args.origtr,
origts = args.origts,
origsubst = args.origsubst,
origqq = tags_to_dialects(lang, args.origtag),
}
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 other_direct_args.author and not other_direct_args.author:find("[%[<]") and not other_direct_args.author:match("w:") 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 = '{{pa--ra|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