Module:User:Benwing2/quote

--[=[	This module contains functions to implement quote-* templates.

Author: Benwing2; conversion into Lua of 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 = "&#59; " local SPACE_LBRAC = " &#91;" local RBRAC = "&#93;"

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 "(" .. text .. ") " end

local function isbn(text) return "→ISBN" .. require(check_isxn_module).check_isbn(text, " Invalid ISBN ") end

local function issn(text) return "[https://www.worldcat.org/issn/" .. text .. " →ISSN]" .. require(check_isxn_module).check_issn(text, " Invalid ISSN ") 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 = { --  and  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  or  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:مُؤَلِّف'), 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  modifier, if given; otherwise nil. --  `tr`: The transliteration corresponding to the  modifier, if given; otherwise nil. --  `ts`: The transcription corresponding to the  modifier, if given; otherwise nil. --  `gloss`: The gloss/translation corresponding to the `explicit_gloss` parameter (if given and non-nil), otherwise --           the  or  modifiers if given, otherwise nil. -- -- Note that as a special case, if `val` contains HTML tags at the top level (e.g. ' ... ', as -- might be generated by specifying مُؤَلِّف), 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. מרים. But exclude HTML entry with, , or -- similar in it, caused by wrapping an argument in, 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. , 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 > (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	-- مُؤَلِّف as the author, you'll get an extra big font coming from the fact that 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. -- 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  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 = ' ' .. gloss .. " "			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, 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 , -- 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 ( 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. redefines number= for the episode number, and				--  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 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 = "<abbr title="' .. abbr.full .. '">' ..			abbr.anchor .. "  " -- 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 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. magic word is more forgiving. Fix dates so that, for -- instance, the |date= parameter of (which uses this code) and the |accessdate= parameter (which	-- uses ) accept similar date formats. See: -- * mw:Extension:Scribunto/Lua_reference_manual -- * mw:Help:Extension:ParserFunctions -- * mw:Help:Magic_words 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 " " .. txt .. " " 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 ). 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 ... 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 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 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 "" .. 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("[^<>]*", ""):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("&lrm;[" .. url .. "]") sep = ", " end

local urls = aurl("urls") if urls then add("&lrm;" .. urls .. "") 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("&#8211;", "–") check_val = check_val:gsub("&mdash;", "—") check_val = check_val:gsub("&#8212;", "—") -- 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, 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 .. "&#58;") -- 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(", ") add(txt) add(" ") 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", "" →Bibcode") add_identifier({"DOI", "doi"}, "<span class=\"neverexpand\">" →DOI ") add_identifier({"ISBN", "isbn"}, "", "", isbn) add_identifier({"ISSN", "issn"}, "", "", issn) add_identifier({"JSTOR", "jstor"}, "" →JSTOR") add_identifier({"LCCN", "lccn"}, "", "", lccn) add_identifier({"OCLC", "oclc"}, "" →OCLC") add_identifier({"OL", "ol"}, ""/ →OL") add_identifier({"PMID", "pmid"}, "" →PMID") add_identifier({"PMCID", "pmcid"}, ""/ →PMCID") add_identifier({"SSRN", "ssrn"}, "" →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 "" .. 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., 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., based on the first language code in 1=. Added under the same --   circumstances as above. -- 3., 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 and meant to be the primary -- interface for 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(' ') ins(export.source(args, alias_map)) ins(" <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> ") 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 = '< >, text, or passage', param_no_synonym = 'text or passage', text = [=[ },	page = { param_with_synonym = '< > or page, or pages', param_no_synonym = 'page or pages', text = [=[
 * < > – the passage to be quoted.]=],
 * < > – 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: 10–11.
 * You must also use 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 = [=[
 * < > – 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: 10–11 or iii–iv.
 * You must also use 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 = '< > or chapter', param_no_synonym = 'chapter', text = [=[ },	roman_chapter = { param_with_synonym = {"inherit", "chapter"}, param_no_synonym = {"inherit", "chapter"}, text = [=[ },	arabic_chapter = { param_with_synonym = {"inherit", "chapter"}, param_no_synonym = {"inherit", "chapter"}, text = [=[ },	trailing_params = { text = [=[ } }
 * < > – the name of the chapter quoted from.]=],
 * < > – the chapter number quoted from in uppercase Roman numerals.]=],
 * < > – the chapter number quoted from in Arabic numerals.]=],
 * footer – a comment on the passage quoted.
 * brackets – use on to surround a quotation with brackets. 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 .. "") else subbed_paramtext = paramtext_no_synonym end return frame:preprocess(rsub(text_to_sub, "< >", 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