ဝီႇၶီႇၽီးတီးယႃး
shnwiki
https://shn.wikipedia.org/wiki/%E1%81%BC%E1%82%83%E1%82%88%E1%82%81%E1%80%B0%E1%80%9D%E1%80%BA%E1%82%81%E1%82%85%E1%81%B5%E1%80%BA%E1%82%88
MediaWiki 1.46.0-wmf.26
first-letter
သိုဝ်ႇၶၢဝ်ႇ
ၶိုၵ်ႉတွၼ်း
ဢုပ်ႇၵုမ်
ၽူႈၸႂ်ႉတိုဝ်း
ဢုပ်ႇၵုမ် ၽူႈၸႂ်ႉတိုဝ်း
ဝီႇၶီႇၽီးတီးယႃး
ဢုပ်ႇၵုမ် ဝီႇၶီႇၽီးတီးယႃး
ၾၢႆႇ
ဢုပ်ႇၵုမ် ၾၢႆႇ
မီႇတီႇယႃႇဝီႇၶီႇ
ဢုပ်ႇၵုမ် မီႇတီႇယႃႇဝီႇၶီႇ
ထႅမ်းပလဵတ်ႉ
ဢုပ်ႇၵုမ် ထႅမ်းပလဵတ်ႉ
လွင်ႈၸွႆႈထႅမ်
ဢုပ်ႇၵုမ် လွင်ႈၸွႆႈထႅမ်
ပိူင်ထၢၼ်ႈ
ဢုပ်ႇၵုမ် ပိူင်ထၢၼ်ႈ
ၵိူၼ်ႇတူ
တွၼ်ႈဢုပ်ႇ ၵိူၼ်ႇတူ
ပိူင်လိူင်း
ဢုပ်ႇၵုမ် ပိူင်လိူင်း
TimedText
TimedText talk
မေႃႇၵျူး
ဢုပ်ႇၵုမ် မေႃႇၵျူး
Event
Event talk
မေႃႇၵျူး:Convert
828
1021
125515
115030
2026-05-04T23:22:00Z
Saimawnkham
5
ဢၢပ်ႉတဵတ်ႉၶေႃႈမုၼ်း
125515
Scribunto
text/plain
-- Convert a value from one unit of measurement to another.
-- Example: {{convert|123|lb|kg}} --> 123 pounds (56 kg)
-- See [[:en:Template:Convert/Transwiki guide]] if copying to another wiki.
local MINUS = '−' -- Unicode U+2212 MINUS SIGN (UTF-8: e2 88 92)
local abs = math.abs
local floor = math.floor
local format = string.format
local log10 = math.log10
local ustring = mw.ustring
local ulen = ustring.len
local usub = ustring.sub
-- Configuration options to keep magic values in one location.
-- Conversion data and message text are defined in separate modules.
local config, maxsigfig
local numdot -- must be '.' or ',' or a character which works in a regex
local numsep, numsep_remove, numsep_remove2
local data_code, all_units
local text_code
local varname -- can be a code to use variable names that depend on value
local from_en_table -- to translate an output string of en digits to local language
local to_en_table -- to translate an input string of digits in local language to en
-- Use translation_table in convert/text to change the following.
local en_default -- true uses lang=en unless convert has lang=local or local digits
local group_method = 3 -- code for how many digits are in a group
local per_word = 'per' -- for units like "liters per kilometer"
local plural_suffix = 's' -- only other useful value is probably '' to disable plural unit names
local omitsep -- true to omit separator before local symbol/name
-- All units should be defined in the data module. However, to cater for quick changes
-- and experiments, any unknown unit is looked up in an extra data module, if it exists.
-- That module would be transcluded in only a small number of pages, so there should be
-- little server overhead from making changes, and changes should propagate quickly.
local extra_module -- name of module with extra units
local extra_units -- nil or table of extra units from extra_module
-- Some options in the invoking template can set variables used later in the module.
local currency_text -- for a user-defined currency symbol: {{convert|12|$/ha|$=€}} (euro replaces dollar)
local function from_en(text)
-- Input is a string representing a number in en digits with '.' decimal mark,
-- without digit grouping (which is done just after calling this).
-- Return the translation of the string with numdot and digits in local language.
if numdot ~= '.' then
text = text:gsub('%.', numdot)
end
if from_en_table then
text = text:gsub('%d', from_en_table)
end
return text
end
local function to_en(text)
-- Input is a string representing a number in the local language with
-- an optional numdot decimal mark and numsep digit grouping.
-- Return the translation of the string with '.' mark and en digits,
-- and no separators (they have to be removed here to handle cases like
-- numsep = '.' and numdot = ',' with input "1.234.567,8").
if to_en_table then
text = ustring.gsub(text, '%d', to_en_table)
end
if numsep_remove then
text = text:gsub(numsep_remove, '')
end
if numsep_remove2 then
text = text:gsub(numsep_remove2, '')
end
if numdot ~= '.' then
text = text:gsub(numdot, '.')
end
return text
end
local function decimal_mark(text)
-- Return ',' if text probably is using comma for decimal mark, or has no decimal mark.
-- Return '.' if text probably is using dot for decimal mark.
-- Otherwise return nothing (decimal mark not known).
if not text:find('[.,]') then return ',' end
text = text:gsub('^%-', ''):gsub('%+%d+/%d+$', ''):gsub('[Ee]%-?%d+$', '')
local decimal =
text:match('^0?([.,])%d+$') or
text:match('%d([.,])%d?%d?$') or
text:match('%d([.,])%d%d%d%d+$')
if decimal then return decimal end
if text:match('%.%d+%.') then return ',' end
if text:match('%,%d+,') then return '.' end
end
local add_warning, with_separator -- forward declarations
local function to_en_with_check(text, parms)
-- Version of to_en() for a wiki using numdot = ',' and numsep = '.' to check
-- text (an input number as a string) which might have been copied from enwiki.
-- For example, in '1.234' the '.' could be a decimal mark or a group separator.
-- From viwiki.
if to_en_table then
text = ustring.gsub(text, '%d', to_en_table)
end
if decimal_mark(text) == '.' then
local original = text
text = text:gsub(',', '') -- for example, interpret "1,234.5" as an enwiki value
if parms then
add_warning(parms, 0, 'cvt_enwiki_num', original, with_separator({}, text))
end
else
if numsep_remove then
text = text:gsub(numsep_remove, '')
end
if numsep_remove2 then
text = text:gsub(numsep_remove2, '')
end
if numdot ~= '.' then
text = text:gsub(numdot, '.')
end
end
return text
end
local function omit_separator(id)
-- Return true if there should be no separator before id (a unit symbol or name).
-- For zhwiki, there should be no separator if id uses local characters.
-- The following kludge should be a sufficient test.
if omitsep then
if id:sub(1, 2) == '-{' then -- for "-{...}-" content language variant
return true
end
if id:byte() > 127 then
local first = usub(id, 1, 1)
if first ~= 'Å' and first ~= '°' and first ~= 'µ' then
return true
end
end
end
return id:sub(1, 1) == '/' -- no separator before units like "/ha"
end
local spell_module -- name of module that can spell numbers
local speller -- function from that module to handle spelling (set if needed)
local wikidata_module, wikidata_data_module -- names of Wikidata modules
local wikidata_code, wikidata_data -- exported tables from those modules (set if needed)
local function set_config(args)
-- Set configuration options from template #invoke or defaults.
config = args
maxsigfig = config.maxsigfig or 14 -- maximum number of significant figures
local data_module, text_module
local sandbox = config.sandbox and ('/' .. config.sandbox) or ''
data_module = "Module:Convert/data" .. sandbox
text_module = "Module:Convert/text" .. sandbox
extra_module = "Module:Convert/extra" .. sandbox
wikidata_module = "Module:Convert/wikidata" .. sandbox
wikidata_data_module = "Module:Convert/wikidata/data" .. sandbox
spell_module = "Module:ConvertNumeric"
data_code = mw.loadData(data_module)
text_code = mw.loadData(text_module)
all_units = data_code.all_units
local translation = text_code.translation_table
if translation then
numdot = translation.numdot
numsep = translation.numsep
if numdot == ',' and numsep == '.' then
if text_code.all_messages.cvt_enwiki_num then
to_en = to_en_with_check
end
end
if translation.group then
group_method = translation.group
end
if translation.per_word then
per_word = translation.per_word
end
if translation.plural_suffix then
plural_suffix = translation.plural_suffix
end
varname = translation.varname
from_en_table = translation.from_en
local use_workaround = true
if use_workaround then
-- 2013-07-05 workaround bug by making a copy of the required table.
-- mw.ustring.gsub fails with a table (to_en_table) as the replacement,
-- if the table is accessed via mw.loadData.
local source = translation.to_en
if source then
to_en_table = {}
for k, v in pairs(source) do
to_en_table[k] = v
end
end
else
to_en_table = translation.to_en
end
if translation.lang == 'en default' then
en_default = true -- for hiwiki
end
omitsep = translation.omitsep -- for zhwiki
end
numdot = config.numdot or numdot or '.' -- decimal mark before fractional digits
numsep = config.numsep or numsep or ',' -- group separator for numbers
-- numsep should be ',' or '.' or '' or ' ' or a Unicode character.
-- numsep_remove must work in a regex to identify separators to be removed.
if numsep ~= '' then
numsep_remove = (numsep == '.') and '%.' or numsep
end
if numsep ~= ',' and numdot ~= ',' then
numsep_remove2 = ',' -- so numbers copied from enwiki will work
end
end
local function collection()
-- Return a table to hold items.
return {
n = 0,
add = function (self, item)
self.n = self.n + 1
self[self.n] = item
end,
}
end
local function divide(numerator, denominator)
-- Return integers quotient, remainder resulting from dividing the two
-- given numbers, which should be unsigned integers.
local quotient, remainder = floor(numerator / denominator), numerator % denominator
if not (0 <= remainder and remainder < denominator) then
-- Floating point limits may need this, as in {{convert|160.02|Ym|ydftin}}.
remainder = 0
end
return quotient, remainder
end
local function split(text, delimiter)
-- Return a numbered table with fields from splitting text.
-- The delimiter is used in a regex without escaping (for example, '.' would fail).
-- Each field has any leading/trailing whitespace removed.
local t = {}
text = text .. delimiter -- to get last item
for item in text:gmatch('%s*(.-)%s*' .. delimiter) do
table.insert(t, item)
end
return t
end
local function strip(text)
-- If text is a string, return its content with no leading/trailing
-- whitespace. Otherwise return nil (a nil argument gives a nil result).
if type(text) == 'string' then
return text:match("^%s*(.-)%s*$")
end
end
local function table_len(t)
-- Return length (<100) of a numbered table to replace #t which is
-- documented to not work if t is accessed via mw.loadData().
for i = 1, 100 do
if t[i] == nil then
return i - 1
end
end
end
local function wanted_category(catkey, catsort, want_warning)
-- Return message category if it is wanted in current namespace,
-- otherwise return ''.
local cat
local title = mw.title.getCurrentTitle()
if title then
local nsdefault = '0' -- default namespace: '0' = article; '0,10' = article and template
local namespace = title.namespace
for _, v in ipairs(split(config.nscat or nsdefault, ',')) do
if namespace == tonumber(v) then
cat = text_code.all_categories[want_warning and 'warning' or catkey]
if catsort and catsort ~= '' and cat:sub(-2) == ']]' then
cat = cat:sub(1, -3) .. '|' .. mw.text.nowiki(usub(catsort, 1, 20)) .. ']]'
end
break
end
end
end
return cat or ''
end
local function message(parms, mcode, is_warning)
-- Return wikitext for an error message, including category if specified
-- for the message type.
-- mcode = numbered table specifying the message:
-- mcode[1] = 'cvt_xxx' (string used as a key to get message info)
-- mcode[2] = 'parm1' (string to replace '$1' if any in message)
-- mcode[3] = 'parm2' (string to replace '$2' if any in message)
-- mcode[4] = 'parm3' (string to replace '$3' if any in message)
local msg
if type(mcode) == 'table' then
if mcode[1] == 'cvt_no_output' then
-- Some errors should cause convert to output an empty string,
-- for example, for an optional field in an infobox.
return ''
end
msg = text_code.all_messages[mcode[1]]
end
parms.have_problem = true
local function subparm(fmt, ...)
local rep = {}
for i, v in ipairs({...}) do
rep['$' .. i] = v
end
return (fmt:gsub('$%d+', rep))
end
if msg then
local parts = {}
local regex, replace = msg.regex, msg.replace
for i = 1, 3 do
local limit = 40
local s = mcode[i + 1]
if s then
if regex and replace then
s = s:gsub(regex, replace)
limit = nil -- allow long "should be" messages
end
-- Escape user input so it does not break the message.
-- To avoid tags (like {{convert|1<math>23</math>|m}}) breaking
-- the mouseover title, any strip marker starting with char(127) is
-- replaced with '...' (text not needing i18n).
local append
local pos = s:find(string.char(127), 1, true)
if pos then
append = '...'
s = s:sub(1, pos - 1)
end
if limit and ulen(s) > limit then
s = usub(s, 1, limit)
append = '...'
end
s = mw.text.nowiki(s) .. (append or '')
else
s = '?'
end
parts['$' .. i] = s
end
local function ispreview()
-- Return true if a prominent message should be shown.
if parms.test == 'preview' or parms.test == 'nopreview' then
-- For testing, can preview a real message or simulate a preview
-- when running automated tests.
return parms.test == 'preview'
end
local success, revid = pcall(function ()
return (parms.frame):preprocess('{{REVISIONID}}') end)
return success and (revid == '')
end
local want_warning = is_warning and
not config.warnings and -- show unobtrusive warnings if config.warnings not configured
not msg.nowarn -- but use msg settings, not standard warning, if specified
local title = string.gsub(msg[1] or 'Missing message', '$%d+', parts)
local text = want_warning and '*' or msg[2] or 'Missing message'
local cat = wanted_category(msg[3], mcode[2], want_warning)
local anchor = msg[4] or ''
local fmtkey = ispreview() and 'cvt_format_preview' or
(want_warning and 'cvt_format2' or msg.format or 'cvt_format')
local fmt = text_code.all_messages[fmtkey] or 'convert: bug'
return subparm(fmt, title:gsub('"', '"'), text, cat, anchor)
end
return 'Convert internal error: unknown message'
end
function add_warning(parms, level, key, text1, text2) -- for forward declaration above
-- If enabled, add a warning that will be displayed after the convert result.
-- A higher level is more verbose: more kinds of warnings are displayed.
-- To reduce output noise, only the first warning is displayed.
if level <= (tonumber(config.warnings) or 1) then
if parms.warnings == nil then
parms.warnings = message(parms, { key, text1, text2 }, true)
end
end
end
local function spell_number(parms, inout, number, numerator, denominator)
-- Return result of spelling (number, numerator, denominator), or
-- return nil if spelling is not available or not supported for given text.
-- Examples (each value must be a string or nil):
-- number numerator denominator output
-- ------ --------- ----------- -------------------
-- "1.23" nil nil one point two three
-- "1" "2" "3" one and two thirds
-- nil "2" "3" two thirds
if not speller then
local function get_speller(module)
return require(module).spell_number
end
local success
success, speller = pcall(get_speller, spell_module)
if not success or type(speller) ~= 'function' then
add_warning(parms, 1, 'cvt_no_spell', 'spell')
return nil
end
end
local case
if parms.spell_upper == inout then
case = true
parms.spell_upper = nil -- only uppercase first word in a multiple unit
end
local sp = not parms.opt_sp_us
local adj = parms.opt_adjectival
return speller(number, numerator, denominator, case, sp, adj)
end
------------------------------------------------------------------------
-- BEGIN: Code required only for built-in units.
-- LATER: If need much more code, move to another module to simplify this module.
local function speed_of_sound(altitude)
-- This is for the Mach built-in unit of speed.
-- Return speed of sound in metres per second at given altitude in feet.
-- If no altitude given, use default (zero altitude = sea level).
-- Table gives speed of sound in miles per hour at various altitudes:
-- altitude = -17,499 to 402,499 feet
-- mach_table[a + 4] = s where
-- a = (altitude / 5000) rounded to nearest integer (-3 to 80)
-- s = speed of sound (mph) at that altitude
-- From: http://www.aerospaceweb.org/question/atmosphere/q0112.shtml
local mach_table = { -- a =
799.5, 787.0, 774.2, 761.207051, -- -3 to 0
748.0, 734.6, 721.0, 707.0, 692.8, 678.3, 663.5, 660.1, 660.1, 660.1, -- 1 to 10
660.1, 660.1, 660.1, 662.0, 664.3, 666.5, 668.9, 671.1, 673.4, 675.6, -- 11 to 20
677.9, 683.7, 689.9, 696.0, 702.1, 708.1, 714.0, 719.9, 725.8, 731.6, -- 21 to 30
737.3, 737.7, 737.7, 736.2, 730.5, 724.6, 718.8, 712.9, 707.0, 701.0, -- 31 to 40
695.0, 688.9, 682.8, 676.6, 670.4, 664.1, 657.8, 652.9, 648.3, 643.7, -- 41 to 50
639.1, 634.4, 629.6, 624.8, 620.0, 615.2, 613.2, 613.2, 613.2, 613.5, -- 51 to 60
614.4, 615.3, 616.7, 619.8, 623.4, 629.7, 635.0, 641.1, 650.6, 660.0, -- 61 to 70
672.5, 674.3, 676.1, 677.9, 679.7, 681.5, 683.3, 685.1, 686.8, 688.6, -- 71 to 80
}
local function lerp(t, v0, v1)
return t * v1 + (1 - t) * v0
end
altitude = (altitude or 0) / 5000
if altitude < -3 then
altitude = -3
elseif altitude > 80 then
altitude = 80
end
local mach_mph
local a = math.floor(altitude)
if a == altitude then
mach_mph = mach_table[a + 4]
else
mach_mph = lerp(altitude - a, mach_table[a + 4], mach_table[a + 5])
end
return mach_mph * 0.44704 -- mph converted to m/s
end
-- END: Code required only for built-in units.
------------------------------------------------------------------------
local function add_style(parms, class)
-- Add selected template style to parms if not already present.
parms.templatestyles = parms.templatestyles or {}
if not parms.templatestyles[class] then
parms.templatestyles[class] = parms.frame:extensionTag({
name = 'templatestyles', args = { src = text_code.titles[class] }
})
end
end
local function get_styles(parms)
-- Return string of required template styles, empty if none.
if parms.templatestyles then
local t = {}
for _, v in pairs(parms.templatestyles) do
table.insert(t, v)
end
return table.concat(t)
end
return ''
end
local function get_range(word)
-- Return a range (string or table) corresponding to word (like "to"),
-- or return nil if not a range word.
local ranges = text_code.ranges
return ranges.types[word] or ranges.types[ranges.aliases[word]]
end
local function check_mismatch(unit1, unit2)
-- If unit1 cannot be converted to unit2, return an error message table.
-- This allows conversion between units of the same type, and between
-- Nm (normally torque) and ftlb (energy), as in gun-related articles.
-- This works because Nm is the base unit (scale = 1) for both the
-- primary type (torque), and the alternate type (energy, where Nm = J).
-- A match occurs if the primary types are the same, or if unit1 matches
-- the alternate type of unit2, and vice versa. That provides a whitelist
-- of which conversions are permitted between normally incompatible types.
if unit1.utype == unit2.utype or
(unit1.utype == unit2.alttype and unit1.alttype == unit2.utype) then
return nil
end
return { 'cvt_mismatch', unit1.utype, unit2.utype }
end
local function override_from(out_table, in_table, fields)
-- Copy the specified fields from in_table to out_table, but do not
-- copy nil fields (keep any corresponding field in out_table).
for _, field in ipairs(fields) do
if in_table[field] then
out_table[field] = in_table[field]
end
end
end
local function shallow_copy(t)
-- Return a shallow copy of table t.
-- Do not need the features and overhead of the Scribunto mw.clone().
local result = {}
for k, v in pairs(t) do
result[k] = v
end
return result
end
local unit_mt = {
-- Metatable to get missing values for a unit that does not accept SI prefixes.
-- Warning: The boolean value 'false' is returned for any missing field
-- so __index is not called twice for the same field in a given unit.
__index = function (self, key)
local value
if key == 'name1' or key == 'sym_us' then
value = self.symbol
elseif key == 'name2' then
value = self.name1 .. plural_suffix
elseif key == 'name1_us' then
value = self.name1
if not rawget(self, 'name2_us') then
-- If name1_us is 'foot', do not make name2_us by appending plural_suffix.
self.name2_us = self.name2
end
elseif key == 'name2_us' then
local raw1_us = rawget(self, 'name1_us')
if raw1_us then
value = raw1_us .. plural_suffix
else
value = self.name2
end
elseif key == 'link' then
value = self.name1
else
value = false
end
rawset(self, key, value)
return value
end
}
local function prefixed_name(unit, name, index)
-- Return unit name with SI prefix inserted at correct position.
-- index = 1 (name1), 2 (name2), 3 (name1_us), 4 (name2_us).
-- The position is a byte (not character) index, so use Lua's sub().
local pos = rawget(unit, 'prefix_position')
if type(pos) == 'string' then
pos = tonumber(split(pos, ',')[index])
end
if pos then
return name:sub(1, pos - 1) .. unit.si_name .. name:sub(pos)
end
return unit.si_name .. name
end
local unit_prefixed_mt = {
-- Metatable to get missing values for a unit that accepts SI prefixes.
-- Before use, fields si_name, si_prefix must be defined.
-- The unit must define _symbol, _name1 and
-- may define _sym_us, _name1_us, _name2_us
-- (_sym_us, _name2_us may be defined for a language using sp=us
-- to refer to a variant unrelated to U.S. units).
__index = function (self, key)
local value
if key == 'symbol' then
value = self.si_prefix .. self._symbol
if value == 'l' then value = 'L' end
elseif key == 'sym_us' then
value = rawget(self, '_sym_us')
if value then
value = self.si_prefix .. value
else
value = self.symbol
end
elseif key == 'name1' then
value = prefixed_name(self, self._name1, 1)
elseif key == 'name2' then
value = rawget(self, '_name2')
if value then
value = prefixed_name(self, value, 2)
else
value = self.name1 .. plural_suffix
end
elseif key == 'name1_us' then
value = rawget(self, '_name1_us')
if value then
value = prefixed_name(self, value, 3)
else
value = self.name1
end
elseif key == 'name2_us' then
value = rawget(self, '_name2_us')
if value then
value = prefixed_name(self, value, 4)
elseif rawget(self, '_name1_us') then
value = self.name1_us .. plural_suffix
else
value = self.name2
end
elseif key == 'link' then
value = self.name1
else
value = false
end
rawset(self, key, value)
return value
end
}
local unit_per_mt = {
-- Metatable to get values for a per unit of form "x/y".
-- This is never called to determine a unit name or link because per units
-- are handled as a special case.
-- Similarly, the default output is handled elsewhere, and for a symbol
-- this is only called from get_default() for default_exceptions.
__index = function (self, key)
local value
if key == 'symbol' then
local per = self.per
local unit1, unit2 = per[1], per[2]
if unit1 then
value = unit1[key] .. '/' .. unit2[key]
else
value = '/' .. unit2[key]
end
elseif key == 'sym_us' then
value = self.symbol
elseif key == 'scale' then
local per = self.per
local unit1, unit2 = per[1], per[2]
value = (unit1 and unit1.scale or 1) * self.scalemultiplier / unit2.scale
else
value = false
end
rawset(self, key, value)
return value
end
}
local function make_per(unitcode, unit_table, ulookup)
-- Return true, t where t is a per unit with unit codes expanded to unit tables,
-- or return false, t where t is an error message table.
local result = {
unitcode = unitcode,
utype = unit_table.utype,
per = {}
}
override_from(result, unit_table, { 'invert', 'iscomplex', 'default', 'link', 'symbol', 'symlink' })
result.symbol_raw = (result.symbol or false) -- to distinguish between a defined exception and a metatable calculation
local prefix
for i, v in ipairs(unit_table.per) do
if i == 1 and v == '' then
-- First unit symbol can be empty; that gives a nil first unit table.
elseif i == 1 and text_code.currency[v] then
prefix = currency_text or v
else
local success, t = ulookup(v)
if not success then return false, t end
result.per[i] = t
end
end
local multiplier = unit_table.multiplier
if not result.utype then
-- Creating an automatic per unit.
local unit1 = result.per[1]
local utype = (unit1 and unit1.utype or prefix or '') .. '/' .. result.per[2].utype
local t = data_code.per_unit_fixups[utype]
if t then
if type(t) == 'table' then
utype = t.utype or utype
result.link = result.link or t.link
multiplier = multiplier or t.multiplier
else
utype = t
end
end
result.utype = utype
end
result.scalemultiplier = multiplier or 1
result.vprefix = prefix or false -- set to non-nil to avoid calling __index
return true, setmetatable(result, unit_per_mt)
end
local function lookup(parms, unitcode, what, utable, fails, depth)
-- Return true, t where t is a copy of the unit's converter table,
-- or return false, t where t is an error message table.
-- Parameter 'what' determines whether combination units are accepted:
-- 'no_combination' : single unit only
-- 'any_combination' : single unit or combination or output multiple
-- 'only_multiple' : single unit or output multiple only
-- Parameter unitcode is a symbol (like 'g'), with an optional SI prefix (like 'kg').
-- If, for example, 'kg' is in this table, that entry is used;
-- otherwise the prefix ('k') is applied to the base unit ('g').
-- If unitcode is a known combination code (and if allowed by what),
-- a table of output multiple unit tables is included in the result.
-- For compatibility with the old template, an underscore in a unitcode is
-- replaced with a space so usage like {{convert|350|board_feet}} works.
-- Wikignomes may also put two spaces or " " in combinations, so
-- replace underscore, " ", and multiple spaces with a single space.
utable = utable or parms.unittable or all_units
fails = fails or {}
depth = depth and depth + 1 or 1
if depth > 9 then
-- There are ways to mistakenly define units which result in infinite
-- recursion when lookup() is called. That gives a long delay and very
-- confusing error messages, so the depth parameter is used as a guard.
return false, { 'cvt_lookup', unitcode }
end
if unitcode == nil or unitcode == '' then
return false, { 'cvt_no_unit' }
end
unitcode = unitcode:gsub('_', ' '):gsub(' ', ' '):gsub(' +', ' ')
local function call_make_per(t)
return make_per(unitcode, t,
function (ucode) return lookup(parms, ucode, 'no_combination', utable, fails, depth) end
)
end
local t = utable[unitcode]
if t then
if t.shouldbe then
return false, { 'cvt_should_be', t.shouldbe }
end
if t.sp_us then
parms.opt_sp_us = true
end
local target = t.target -- nil, or unitcode is an alias for this target
if target then
local success, result = lookup(parms, target, what, utable, fails, depth)
if not success then return false, result end
override_from(result, t, { 'customary', 'default', 'link', 'symbol', 'symlink', 'usename' })
if t.default then
result.defkey = unitcode -- so default_exceptions uses the alias code, not the target symbol
end
local multiplier = t.multiplier
if multiplier then
result.multiplier = tostring(multiplier)
result.scale = result.scale * multiplier
end
return true, result
end
if t.per then
return call_make_per(t)
end
local combo = t.combination -- nil or a table of unitcodes
if combo then
local multiple = t.multiple
if what == 'no_combination' or (what == 'only_multiple' and not multiple) then
return false, { 'cvt_bad_unit', unitcode }
end
-- Recursively create a combination table containing the
-- converter table of each unitcode.
local result = { utype = t.utype, multiple = multiple, combination = {} }
local cvt = result.combination
for i, v in ipairs(combo) do
local success, t = lookup(parms, v, multiple and 'no_combination' or 'only_multiple', utable, fails, depth)
if not success then return false, t end
cvt[i] = t
end
return true, result
end
local result = shallow_copy(t)
result.unitcode = unitcode
if result.prefixes then
result.si_name = ''
result.si_prefix = ''
return true, setmetatable(result, unit_prefixed_mt)
end
return true, setmetatable(result, unit_mt)
end
local SIprefixes = text_code.SIprefixes
for plen = SIprefixes[1] or 2, 1, -1 do
-- Look for an SI prefix; should never occur with an alias.
-- Check for longer prefix first ('dam' is decametre).
-- SIprefixes[1] = prefix maximum #characters (as seen by mw.ustring.sub).
local prefix = usub(unitcode, 1, plen)
local si = SIprefixes[prefix]
if si then
local t = utable[usub(unitcode, plen+1)]
if t and t.prefixes then
local result = shallow_copy(t)
result.unitcode = unitcode
result.si_name = parms.opt_sp_us and si.name_us or si.name
result.si_prefix = si.prefix or prefix
result.scale = t.scale * 10 ^ (si.exponent * t.prefixes)
return true, setmetatable(result, unit_prefixed_mt)
end
end
end
-- Accept user-defined combinations like "acre+m2+ha" or "acre m2 ha" for output.
-- If '+' is used, each unit code can include a space, and any error is fatal.
-- If ' ' is used and if each space-separated word is a unit code, it is a combo,
-- but errors are not fatal so the unit code can be looked up as an extra unit.
local err_is_fatal
local combo = collection()
if unitcode:find('+', 1, true) then
err_is_fatal = true
for item in (unitcode .. '+'):gmatch('%s*(.-)%s*%+') do
if item ~= '' then
combo:add(item)
end
end
elseif unitcode:find('%s') then
for item in unitcode:gmatch('%S+') do
combo:add(item)
end
end
if combo.n > 1 then
local function lookup_combo()
if what == 'no_combination' or what == 'only_multiple' then
return false, { 'cvt_bad_unit', unitcode }
end
local result = { combination = {} }
local cvt = result.combination
for i, v in ipairs(combo) do
local success, t = lookup(parms, v, 'only_multiple', utable, fails, depth)
if not success then return false, t end
if i == 1 then
result.utype = t.utype
else
local mismatch = check_mismatch(result, t)
if mismatch then
return false, mismatch
end
end
cvt[i] = t
end
return true, result
end
local success, result = lookup_combo()
if success or err_is_fatal then
return success, result
end
end
-- Accept any unit with an engineering notation prefix like "e6cuft"
-- (million cubic feet), but not chained prefixes like "e3e6cuft",
-- and not if the unit is a combination or multiple,
-- and not if the unit has an offset or is a built-in.
-- Only en digits are accepted.
local e, exponent, baseunit = unitcode:match('^([Ee])(%d+)(.*)')
if exponent then
local engscale = text_code.eng_scales[exponent]
if engscale then
local success, result = lookup(parms, baseunit, 'no_combination', utable, fails, depth)
if success and not (result.offset or result.builtin or result.engscale) then
if e == 'E' then
result.this_number_word = true
unitcode = 'e' .. unitcode:sub(2)
end
result.unitcode = unitcode -- 'e6cuft' not 'cuft'
result.defkey = unitcode -- key to lookup default exception
result.engscale = engscale
result.scale = result.scale * 10 ^ tonumber(exponent)
return true, result
end
end
end
-- Look for x/y; split on right-most slash to get scale correct (x/y/z is x/y per z).
local top, bottom = unitcode:match('^(.-)/([^/]+)$')
if top and not unitcode:find('e%d') then
-- If valid, create an automatic per unit for an "x/y" unit code.
-- The unitcode must not include extraneous spaces.
-- Engineering notation (apart from at start and which has been stripped before here),
-- is not supported so do not make a per unit if find text like 'e3' in unitcode.
local success, result = call_make_per({ per = {top, bottom} })
if success then
return true, result
end
end
if not parms.opt_no_extra and not get_range(unitcode) then
-- Want the "what links here" list for the extra_module to show only cases
-- where an extra unit is used, so do not require it when not needed
-- or if looking up a range word which cannot be a unit.
if not extra_units then
local success, extra = pcall(function () return require(extra_module).extra_units end)
if success and type(extra) == 'table' then
extra_units = extra
end
end
if extra_units then
-- A unit in one data table might refer to a unit in the other table, so
-- switch between them, relying on fails or depth to terminate loops.
if not fails[unitcode] then
fails[unitcode] = true
local other = (utable == all_units) and extra_units or all_units
local success, result = lookup(parms, unitcode, what, other, fails, depth)
if success then
return true, result
end
end
end
end
if to_en_table then
-- At fawiki it is common to translate all digits so a unit like "km2" becomes "km۲".
local en_code = ustring.gsub(unitcode, '%d', to_en_table)
if en_code ~= unitcode then
return lookup(parms, en_code, what, utable, fails, depth)
end
end
return false, { 'cvt_unknown', unitcode }
end
local function valid_number(num)
-- Return true if num is a valid number.
-- In Scribunto (different from some standard Lua), when expressed as a string,
-- overflow or other problems are indicated with text like "inf" or "nan"
-- which are regarded as invalid here (each contains "n").
if type(num) == 'number' and tostring(num):find('n', 1, true) == nil then
return true
end
end
local function hyphenated(name, parts)
-- Return a hyphenated form of given name (for adjectival usage).
-- The name may be linked and the target of the link must not be changed.
-- Hypothetical examples:
-- [[long ton|ton]] → [[long ton|ton]] (no change)
-- [[tonne|long ton]] → [[tonne|long-ton]]
-- [[metric ton|long ton]] → [[metric ton|long-ton]]
-- [[long ton]] → [[long ton|long-ton]]
-- Input can also have multiple links in a single name like:
-- [[United States customary units|U.S.]] [[US gallon|gallon]]
-- [[mile]]s per [[United States customary units|U.S.]] [[quart]]
-- [[long ton]]s per [[short ton]]
-- Assume that links cannot be nested (never like "[[abc[[def]]ghi]]").
-- This uses a simple and efficient procedure that works for most cases.
-- Some units (if used) would require more, and can later think about
-- adding a method to handle exceptions.
-- The procedure is to replace each space with a hyphen, but
-- not a space after ')' [for "(pre-1954 US) nautical mile"], and
-- not spaces immediately before '(' or in '(...)' [for cases like
-- "British thermal unit (ISO)" and "Calorie (International Steam Table)"].
if name:find(' ', 1, true) then
if parts then
local pos
if name:sub(1, 1) == '(' then
pos = name:find(')', 1, true)
if pos then
return name:sub(1, pos+1) .. name:sub(pos+2):gsub(' ', '-')
end
elseif name:sub(-1) == ')' then
pos = name:find('(', 1, true)
if pos then
return name:sub(1, pos-2):gsub(' ', '-') .. name:sub(pos-1)
end
end
return name:gsub(' ', '-')
end
parts = collection()
for before, item, after in name:gmatch('([^[]*)(%[%[[^[]*%]%])([^[]*)') do
if item:find(' ', 1, true) then
local prefix
local plen = item:find('|', 1, true)
if plen then
prefix = item:sub(1, plen)
item = item:sub(plen + 1, -3)
else
prefix = item:sub(1, -3) .. '|'
item = item:sub(3, -3)
end
item = prefix .. hyphenated(item, parts) .. ']]'
end
parts:add(before:gsub(' ', '-') .. item .. after:gsub(' ', '-'))
end
if parts.n == 0 then
-- No link like "[[...]]" was found in the original name.
parts:add(hyphenated(name, parts))
end
return table.concat(parts)
end
return name
end
local function hyphenated_maybe(parms, want_name, sep, id, inout)
-- Return s, f where
-- s = id, possibly modified
-- f = true if hyphenated
-- Possible modifications: hyphenate; prepend '-'; append mid text.
if id == nil or id == '' then
return ''
end
local mid = (inout == (parms.opt_flip and 'out' or 'in')) and parms.mid or ''
if want_name then
if parms.opt_adjectival then
return '-' .. hyphenated(id) .. mid, true
end
if parms.opt_add_s and id:sub(-1) ~= 's' then
id = id .. 's' -- for nowiki
end
end
return sep .. id .. mid
end
local function use_minus(text)
-- Return text with Unicode minus instead of '-', if present.
if text:sub(1, 1) == '-' then
return MINUS .. text:sub(2)
end
return text
end
local function digit_groups(parms, text, method)
-- Return a numbered table of groups of digits (left-to-right, in local language).
-- Parameter method is a number or nil:
-- 3 for 3-digit grouping (default), or
-- 2 for 3-then-2 grouping (only for digits before decimal mark).
local len_right
local len_left = text:find('.', 1, true)
if len_left then
len_right = #text - len_left
len_left = len_left - 1
else
len_left = #text
end
local twos = method == 2 and len_left > 5
local groups = collection()
local run = len_left
local n
if run < 4 or (run == 4 and parms.opt_comma5) then
if parms.opt_gaps then
n = run
else
n = #text
end
elseif twos then
n = run % 2 == 0 and 1 or 2
else
n = run % 3 == 0 and 3 or run % 3
end
while run > 0 do
groups:add(n)
run = run - n
n = (twos and run > 3) and 2 or 3
end
if len_right then
if groups.n == 0 then
groups:add(0)
end
if parms.opt_gaps and len_right > 3 then
local want4 = not parms.opt_gaps3 -- true gives no gap before trailing single digit
local isfirst = true
run = len_right
while run > 0 do
n = (want4 and run == 4) and 4 or (run > 3 and 3 or run)
if isfirst then
isfirst = false
groups[groups.n] = groups[groups.n] + 1 + n
else
groups:add(n)
end
run = run - n
end
else
groups[groups.n] = groups[groups.n] + 1 + len_right
end
end
local pos = 1
for i, length in ipairs(groups) do
groups[i] = from_en(text:sub(pos, pos + length - 1))
pos = pos + length
end
return groups
end
function with_separator(parms, text) -- for forward declaration above
-- Input text is a number in en digits with optional '.' decimal mark.
-- Return an equivalent, formatted for display:
-- with a custom decimal mark instead of '.', if wanted
-- with thousand separators inserted, if wanted
-- digits in local language
-- The given text is like '123' or '123.' or '12345.6789'.
-- The text has no sign (caller inserts that later, if necessary).
-- When using gaps, they are inserted before and after the decimal mark.
-- Separators are inserted only before the decimal mark.
-- A trailing dot (as in '123.') is removed because their use appears to
-- be accidental, and such a number should be shown as '123' or '123.0'.
-- It is useful for convert to suppress the dot so, for example, '4000.'
-- is a simple way of indicating that all the digits are significant.
if text:sub(-1) == '.' then
text = text:sub(1, -2)
end
if #text < 4 or parms.opt_nocomma or numsep == '' then
return from_en(text)
end
local groups = digit_groups(parms, text, group_method)
if parms.opt_gaps then
if groups.n <= 1 then
return groups[1] or ''
end
local nowrap = '<span style="white-space: nowrap">'
local gap = '<span style="margin-left: 0.25em">'
local close = '</span>'
return nowrap .. groups[1] .. gap .. table.concat(groups, close .. gap, 2, groups.n) .. close .. close
end
return table.concat(groups, numsep)
end
-- An input value like 1.23e12 is displayed using scientific notation (1.23×10¹²).
-- That also makes the output use scientific notation, except for small values.
-- In addition, very small or very large output values use scientific notation.
-- Use format(fmtpower, significand, '10', exponent) where each argument is a string.
local fmtpower = '%s<span style="margin:0 .15em 0 .25em">×</span>%s<sup>%s</sup>'
local function with_exponent(parms, show, exponent)
-- Return wikitext to display the implied value in scientific notation.
-- Input uses en digits; output uses digits in local language.
return format(fmtpower, with_separator(parms, show), from_en('10'), use_minus(from_en(tostring(exponent))))
end
local function make_sigfig(value, sigfig)
-- Return show, exponent that are equivalent to the result of
-- converting the number 'value' (where value >= 0) to a string,
-- rounded to 'sigfig' significant figures.
-- The returned items are:
-- show: a string of digits; no sign and no dot;
-- there is an implied dot before show.
-- exponent: a number (an integer) to shift the implied dot.
-- Resulting value = tonumber('.' .. show) * 10^exponent.
-- Examples:
-- make_sigfig(23.456, 3) returns '235', 2 (.235 * 10^2).
-- make_sigfig(0.0023456, 3) returns '235', -2 (.235 * 10^-2).
-- make_sigfig(0, 3) returns '000', 1 (.000 * 10^1).
if sigfig <= 0 then
sigfig = 1
elseif sigfig > maxsigfig then
sigfig = maxsigfig
end
if value == 0 then
return string.rep('0', sigfig), 1
end
local exp, fracpart = math.modf(log10(value))
if fracpart >= 0 then
fracpart = fracpart - 1
exp = exp + 1
end
local digits = format('%.0f', 10^(fracpart + sigfig))
if #digits > sigfig then
-- Overflow (for sigfig=3: like 0.9999 rounding to "1000"; need "100").
digits = digits:sub(1, sigfig)
exp = exp + 1
end
assert(#digits == sigfig, 'Bug: rounded number has wrong length')
return digits, exp
end
-- Fraction output format.
local fracfmt = {
{ -- Like {{frac}} (fraction slash).
'<span class="frac">{SIGN}<span class="num">{NUM}</span>⁄<span class="den">{DEN}</span></span>', -- 1/2
'<span class="frac">{SIGN}{WHOLE}<span class="sr-only">+</span><span class="num">{NUM}</span>⁄<span class="den">{DEN}</span></span>', -- 1+2/3
style = 'frac',
},
{ -- Like {{sfrac}} (stacked fraction, that is, horizontal bar).
'<span class="sfrac tion">{SIGN}<span class="num">{NUM}</span><span class="sr-only">/</span><span class="den">{DEN}</span></span>', -- 1//2
'<span class="sfrac">{SIGN}{WHOLE}<span class="sr-only">+</span><span class="tion"><span class="num">{NUM}</span><span class="sr-only">/</span><span class="den">{DEN}</span></span></span>', -- 1+2//3
style = 'sfrac',
},
}
local function format_fraction(parms, inout, negative, wholestr, numstr, denstr, do_spell, style)
-- Return wikitext for a fraction, possibly spelled.
-- Inputs use en digits and have no sign; output uses digits in local language.
local wikitext
if not style then
style = parms.opt_fraction_horizontal and 2 or 1
end
if wholestr == '' then
wholestr = nil
end
local substitute = {
SIGN = negative and MINUS or '',
WHOLE = wholestr and with_separator(parms, wholestr),
NUM = from_en(numstr),
DEN = from_en(denstr),
}
wikitext = fracfmt[style][wholestr and 2 or 1]:gsub('{(%u+)}', substitute)
if do_spell then
if negative then
if wholestr then
wholestr = '-' .. wholestr
else
numstr = '-' .. numstr
end
end
local s = spell_number(parms, inout, wholestr, numstr, denstr)
if s then
return s
end
end
add_style(parms, fracfmt[style].style)
return wikitext
end
local function format_number(parms, show, exponent, isnegative)
-- Parameter show is a string or a table containing strings.
-- Each string is a formatted number in en digits and optional '.' decimal mark.
-- A table represents a fraction: integer, numerator, denominator;
-- if a table is given, exponent must be nil.
-- Return t where t is a table with fields:
-- show = wikitext formatted to display implied value
-- (digits in local language)
-- is_scientific = true if show uses scientific notation
-- clean = unformatted show (possibly adjusted and with inserted '.')
-- (en digits)
-- sign = '' or MINUS
-- exponent = exponent (possibly adjusted)
-- The clean and exponent fields can be used to calculate the
-- rounded absolute value, if needed.
--
-- The value implied by the arguments is found from:
-- exponent is nil; and
-- show is a string of digits (no sign), with an optional dot;
-- show = '123.4' is value 123.4, '1234' is value 1234.0;
-- or:
-- exponent is an integer indicating where dot should be;
-- show is a string of digits (no sign and no dot);
-- there is an implied dot before show;
-- show does not start with '0';
-- show = '1234', exponent = 3 is value 0.1234*10^3 = 123.4.
--
-- The formatted result:
-- * Is for an output value and is spelled if wanted and possible.
-- * Includes a Unicode minus if isnegative and not spelled.
-- * Uses a custom decimal mark, if wanted.
-- * Has digits grouped where necessary, if wanted.
-- * Uses scientific notation if requested, or for very small or large values
-- (which forces result to not be spelled).
-- * Has no more than maxsigfig significant digits
-- (same as old template and {{#expr}}).
local xhi, xlo -- these control when scientific notation (exponent) is used
if parms.opt_scientific then
xhi, xlo = 4, 2 -- default for output if input uses e-notation
elseif parms.opt_scientific_always then
xhi, xlo = 0, 0 -- always use scientific notation (experimental)
else
xhi, xlo = 10, 4 -- default
end
local sign = isnegative and MINUS or ''
local maxlen = maxsigfig
local tfrac
if type(show) == 'table' then
tfrac = show
show = tfrac.wholestr
assert(exponent == nil, 'Bug: exponent given with fraction')
end
if not tfrac and not exponent then
local integer, dot, decimals = show:match('^(%d*)(%.?)(.*)')
if integer == '0' or integer == '' then
local zeros, figs = decimals:match('^(0*)([^0]?.*)')
if #figs == 0 then
if #zeros > maxlen then
show = '0.' .. zeros:sub(1, maxlen)
end
elseif #zeros >= xlo then
show = figs
exponent = -#zeros
elseif #figs > maxlen then
show = '0.' .. zeros .. figs:sub(1, maxlen)
end
elseif #integer >= xhi then
show = integer .. decimals
exponent = #integer
else
maxlen = maxlen + #dot
if #show > maxlen then
show = show:sub(1, maxlen)
end
end
end
if exponent then
local function zeros(n)
return string.rep('0', n)
end
if #show > maxlen then
show = show:sub(1, maxlen)
end
if exponent > xhi or exponent <= -xlo or (exponent == xhi and show ~= '1' .. zeros(xhi - 1)) then
-- When xhi, xlo = 10, 4 (the default), scientific notation is used if the
-- rounded value satisfies: value >= 1e9 or value < 1e-4 (1e9 = 0.1e10),
-- except if show is '1000000000' (1e9), for example:
-- {{convert|1000000000|m|m|sigfig=10}} → 1,000,000,000 metres (1,000,000,000 m)
local significand
if #show > 1 then
significand = show:sub(1, 1) .. '.' .. show:sub(2)
else
significand = show
end
return {
clean = '.' .. show,
exponent = exponent,
sign = sign,
show = sign .. with_exponent(parms, significand, exponent-1),
is_scientific = true,
}
end
if exponent >= #show then
show = show .. zeros(exponent - #show) -- result has no dot
elseif exponent <= 0 then
show = '0.' .. zeros(-exponent) .. show
else
show = show:sub(1, exponent) .. '.' .. show:sub(exponent+1)
end
end
local formatted_show
if tfrac then
show = tostring(tfrac.value) -- to set clean in returned table
formatted_show = format_fraction(parms, 'out', isnegative, tfrac.wholestr, tfrac.numstr, tfrac.denstr, parms.opt_spell_out)
else
if isnegative and show:match('^0.?0*$') then
sign = '' -- don't show minus if result is negative but rounds to zero
end
formatted_show = sign .. with_separator(parms, show)
if parms.opt_spell_out then
formatted_show = spell_number(parms, 'out', sign .. show) or formatted_show
end
end
return {
clean = show,
sign = sign,
show = formatted_show,
is_scientific = false, -- to avoid calling __index
}
end
local function extract_fraction(parms, text, negative)
-- If text represents a fraction, return
-- value, altvalue, show, denominator
-- where
-- value is a number (value of the fraction in argument text)
-- altvalue is an alternate interpretation of any fraction for the hands
-- unit where "12.1+3/4" means 12 hands 1.75 inches
-- show is a string (formatted text for display of an input value,
-- and is spelled if wanted and possible)
-- denominator is value of the denominator in the fraction
-- Otherwise, return nil.
-- Input uses en digits and '.' decimal mark (input has been translated).
-- Output uses digits in local language and local decimal mark, if any.
------------------------------------------------------------------------
-- Originally this function accepted x+y/z where x, y, z were any valid
-- numbers, possibly with a sign. For example '1.23e+2+1.2/2.4' = 123.5,
-- and '2-3/8' = 1.625. However, such usages were found to be errors or
-- misunderstandings, so since August 2014 the following restrictions apply:
-- x (if present) is an integer or has a single digit after decimal mark
-- y and z are unsigned integers
-- e-notation is not accepted
-- The overall number can start with '+' or '-' (so '12+3/4' and '+12+3/4'
-- and '-12-3/4' are valid).
-- Any leading negative sign is removed by the caller, so only inputs
-- like the following are accepted here (may have whitespace):
-- negative = false false true (there was a leading '-')
-- text = '2/3' '+2/3' '2/3'
-- text = '1+2/3' '+1+2/3' '1-2/3'
-- text = '12.3+1/2' '+12.3+1/2' '12.3-1/2'
-- Values like '12.3+1/2' are accepted, but are intended only for use
-- with the hands unit (not worth adding code to enforce that).
------------------------------------------------------------------------
local leading_plus, prefix, numstr, slashes, denstr =
text:match('^%s*(%+?)%s*(.-)%s*(%d+)%s*(/+)%s*(%d+)%s*$')
if not leading_plus then
-- Accept a single U+2044 fraction slash because that may be pasted.
leading_plus, prefix, numstr, denstr =
text:match('^%s*(%+?)%s*(.-)%s*(%d+)%s*⁄%s*(%d+)%s*$')
slashes = '/'
end
local numerator = tonumber(numstr)
local denominator = tonumber(denstr)
if numerator == nil or denominator == nil or (negative and leading_plus ~= '') then
return nil
end
local whole, wholestr
if prefix == '' then
wholestr = ''
whole = 0
else
-- Any prefix must be like '12+' or '12-' (whole number and fraction sign);
-- '12.3+' and '12.3-' are also accepted (single digit after decimal point)
-- because '12.3+1/2 hands' is valid (12 hands 3½ inches).
local num1, num2, frac_sign = prefix:match('^(%d+)(%.?%d?)%s*([+%-])$')
if num1 == nil then return nil end
if num2 == '' then -- num2 must be '' or like '.1' but not '.' or '.12'
wholestr = num1
else
if #num2 ~= 2 then return nil end
wholestr = num1 .. num2
end
if frac_sign ~= (negative and '-' or '+') then return nil end
whole = tonumber(wholestr)
if whole == nil then return nil end
end
local value = whole + numerator / denominator
if not valid_number(value) then return nil end
local altvalue = whole + numerator / (denominator * 10)
local style = #slashes -- kludge: 1 or 2 slashes can be used to select style
if style > 2 then style = 2 end
local wikitext = format_fraction(parms, 'in', negative, leading_plus .. wholestr, numstr, denstr, parms.opt_spell_in, style)
return value, altvalue, wikitext, denominator
end
local function extract_number(parms, text, another, no_fraction)
-- Return true, info if can extract a number from text,
-- where info is a table with the result,
-- or return false, t where t is an error message table.
-- Input can use en digits or digits in local language and can
-- have references at the end. Accepting references is intended
-- for use in infoboxes with a field for a value passed to convert.
-- Parameter another = true if the expected value is not the first.
-- Before processing, the input text is cleaned:
-- * Any thousand separators (valid or not) are removed.
-- * Any sign is replaced with '-' (if negative) or '' (otherwise).
-- That replaces Unicode minus with '-'.
-- If successful, the returned info table contains named fields:
-- value = a valid number
-- altvalue = a valid number, usually same as value but different
-- if fraction used (for hands unit)
-- singular = true if value is 1 or -1 (to use singular form of units)
-- clean = cleaned text with any separators and sign removed
-- (en digits and '.' decimal mark)
-- show = text formatted for output, possibly with ref strip markers
-- (digits in local language and custom decimal mark)
-- The resulting show:
-- * Is for an input value and is spelled if wanted and possible.
-- * Has a rounded value, if wanted.
-- * Has digits grouped where necessary, if wanted.
-- * If negative, a Unicode minus is used; otherwise the sign is
-- '+' (if the input text used '+'), or is '' (if no sign in input).
text = strip(text or '')
local reference
local pos = text:find('\127', 1, true)
if pos then
local before = text:sub(1, pos - 1)
local remainder = text:sub(pos)
local refs = {}
while #remainder > 0 do
local ref, spaces
ref, spaces, remainder = remainder:match('^(\127[^\127]*UNIQ[^\127]*%-ref[^\127]*\127)(%s*)(.*)')
if ref then
table.insert(refs, ref)
else
refs = {}
break
end
end
if #refs > 0 then
text = strip(before)
reference = table.concat(refs)
end
end
local clean = to_en(text, parms)
if clean == '' then
return false, { another and 'cvt_no_num2' or 'cvt_no_num' }
end
local isnegative, propersign = false, '' -- most common case
local singular, show, denominator
local value = tonumber(clean)
local altvalue
if value then
local sign = clean:sub(1, 1)
if sign == '+' or sign == '-' then
propersign = (sign == '+') and '+' or MINUS
clean = clean:sub(2)
end
if value < 0 then
isnegative = true
value = -value
end
else
local valstr
for _, prefix in ipairs({ '-', MINUS, '−' }) do
-- Including '-' sets isnegative in case input is a fraction like '-2-3/4'.
local plen = #prefix
if clean:sub(1, plen) == prefix then
valstr = clean:sub(plen + 1)
if valstr:match('^%s') then -- "- 1" is invalid but "-1 - 1/2" is ok
return false, { 'cvt_bad_num', text }
end
break
end
end
if valstr then
isnegative = true
propersign = MINUS
clean = valstr
value = tonumber(clean)
end
if value == nil then
if not no_fraction then
value, altvalue, show, denominator = extract_fraction(parms, clean, isnegative)
end
if value == nil then
return false, { 'cvt_bad_num', text }
end
if value <= 1 then
singular = true -- for example, "½ mile" or "one half mile" (singular unit)
end
end
end
if not valid_number(value) then -- for example, "1e310" may overflow
return false, { 'cvt_invalid_num' }
end
if show == nil then
-- clean is a non-empty string with no spaces, and does not represent a fraction,
-- and value = tonumber(clean) is a number >= 0.
-- If the input uses e-notation, show will be displayed using a power of ten, but
-- we use the number as given so it might not be normalized scientific notation.
-- The input value is spelled if specified so any e-notation is ignored;
-- that allows input like 2e6 to be spelled as "two million" which works
-- because the spell module converts '2e6' to '2000000' before spelling.
local function rounded(value, default, exponent)
local precision = parms.opt_ri
if precision then
local fmt = '%.' .. format('%d', precision) .. 'f'
local result = fmt:format(tonumber(value) + 2e-14) -- fudge for some common cases of bad rounding
if not exponent then
singular = (tonumber(result) == 1)
end
return result
end
return default
end
singular = (value == 1)
local scientific
local significand, exponent = clean:match('^([%d.]+)[Ee]([+%-]?%d+)')
if significand then
show = with_exponent(parms, rounded(significand, significand, exponent), exponent)
scientific = true
else
show = with_separator(parms, rounded(value, clean))
end
show = propersign .. show
if parms.opt_spell_in then
show = spell_number(parms, 'in', propersign .. rounded(value, clean)) or show
scientific = false
end
if scientific then
parms.opt_scientific = true
end
end
if isnegative and (value ~= 0) then
value = -value
altvalue = -(altvalue or value)
end
return true, {
value = value,
altvalue = altvalue or value,
singular = singular,
clean = clean,
show = show .. (reference or ''),
denominator = denominator,
}
end
local function get_number(text)
-- Return v, f where:
-- v = nil (text is not a number)
-- or
-- v = value of text (text is a number)
-- f = true if value is an integer
-- Input can use en digits or digits in local language or separators,
-- but no Unicode minus, and no fraction.
if text then
local number = tonumber(to_en(text))
if number then
local _, fracpart = math.modf(number)
return number, (fracpart == 0)
end
end
end
local function gcd(a, b)
-- Return the greatest common denominator for the given values,
-- which are known to be positive integers.
if a > b then
a, b = b, a
end
if a <= 0 then
return b
end
local r = b % a
if r <= 0 then
return a
end
if r == 1 then
return 1
end
return gcd(r, a)
end
local function fraction_table(value, denominator)
-- Return value as a string or a table:
-- * If result is a string, there is no fraction, and the result
-- is value formatted as a string of en digits.
-- * If result is a table, it represents a fraction with named fields:
-- wholestr, numstr, denstr (strings of en digits for integer, numerator, denominator).
-- The result is rounded to the nearest multiple of (1/denominator).
-- If the multiple is zero, no fraction is included.
-- No fraction is included if value is very large as the fraction would
-- be unhelpful, particularly if scientific notation is required.
-- Input value is a non-negative number.
-- Input denominator is a positive integer for the desired fraction.
if value <= 0 then
return '0'
end
if denominator <= 0 or value > 1e8 then
return format('%.2f', value)
end
local integer, decimals = math.modf(value)
local numerator = floor((decimals * denominator) +
0.5 + 2e-14) -- add fudge for some common cases of bad rounding
if numerator >= denominator then
integer = integer + 1
numerator = 0
end
local wholestr = tostring(integer)
if numerator > 0 then
local div = gcd(numerator, denominator)
if div > 1 then
numerator = numerator / div
denominator = denominator / div
end
return {
wholestr = (integer > 0) and wholestr or '',
numstr = tostring(numerator),
denstr = tostring(denominator),
value = value,
}
end
return wholestr
end
local function preunits(count, preunit1, preunit2)
-- If count is 1:
-- ignore preunit2
-- return p1
-- else:
-- preunit1 is used for preunit2 if the latter is empty
-- return p1, p2
-- where:
-- p1 is text to insert before the input unit
-- p2 is text to insert before the output unit
-- p1 or p2 may be nil to mean "no preunit"
-- Using '+' gives output like "5+ feet" (no space before, but space after).
local function withspace(text, wantboth)
-- Return text with space before and, if wantboth, after.
-- However, no space is added if there is a space or ' ' or '-'
-- at that position ('-' is for adjectival text).
-- There is also no space if text starts with '&'
-- (e.g. '°' would display a degree symbol with no preceding space).
local char = text:sub(1, 1)
if char == '&' then
return text -- an html entity can be used to specify the exact display
end
if not (char == ' ' or char == '-' or char == '+') then
text = ' ' .. text
end
if wantboth then
char = text:sub(-1, -1)
if not (char == ' ' or char == '-' or text:sub(-6, -1) == ' ') then
text = text .. ' '
end
end
return text
end
local PLUS = '+ '
preunit1 = preunit1 or ''
local trim1 = strip(preunit1)
if count == 1 then
if trim1 == '' then
return nil
end
if trim1 == '+' then
return PLUS
end
return withspace(preunit1, true)
end
preunit1 = withspace(preunit1)
preunit2 = preunit2 or ''
local trim2 = strip(preunit2)
if trim1 == '+' then
if trim2 == '' or trim2 == '+' then
return PLUS, PLUS
end
preunit1 = PLUS
end
if trim2 == '' then
if trim1 == '' then
return nil, nil
end
preunit2 = preunit1
elseif trim2 == '+' then
preunit2 = PLUS
elseif trim2 == ' ' then -- trick to make preunit2 empty
preunit2 = nil
else
preunit2 = withspace(preunit2)
end
return preunit1, preunit2
end
local function range_text(range, want_name, parms, before, after, inout, options)
-- Return before .. rtext .. after
-- where rtext is the text that separates two values in a range.
local rtext, adj_text, exception
options = options or {}
if type(range) == 'table' then
-- Table must specify range text for ('off' and 'on') or ('input' and 'output'),
-- and may specify range text for 'adj=on',
-- and may specify exception = true.
rtext = range[want_name and 'off' or 'on'] or
range[((inout == 'in') == (parms.opt_flip == true)) and 'output' or 'input']
adj_text = range['adj']
exception = range['exception']
else
rtext = range
end
if parms.opt_adjectival then
if want_name or (exception and parms.abbr_org == 'on') then
rtext = adj_text or rtext:gsub(' ', '-'):gsub(' ', '-')
end
end
if rtext == '–' and (options.spaced or after:sub(1, #MINUS) == MINUS) then
rtext = ' – '
end
return before .. rtext .. after
end
local function get_composite(parms, iparm, in_unit_table)
-- Look for a composite input unit. For example, {{convert|1|yd|2|ft|3|in}}
-- would result in a call to this function with
-- iparm = 3 (parms[iparm] = "2", just after the first unit)
-- in_unit_table = (unit table for "yd"; contains value 1 for number of yards)
-- Return true, iparm, unit where
-- iparm = index just after the composite units (7 in above example)
-- unit = composite unit table holding all input units,
-- or return true if no composite unit is present in parms,
-- or return false, t where t is an error message table.
local default, subinfo
local composite_units, count = { in_unit_table }, 1
local fixups = {}
local total = in_unit_table.valinfo[1].value
local subunit = in_unit_table
while subunit.subdivs do -- subdivs is nil or a table of allowed subdivisions
local subcode = strip(parms[iparm+1])
local subdiv = subunit.subdivs[subcode] or subunit.subdivs[(all_units[subcode] or {}).target]
if not subdiv then
break
end
local success
success, subunit = lookup(parms, subcode, 'no_combination')
if not success then return false, subunit end -- should never occur
success, subinfo = extract_number(parms, parms[iparm])
if not success then return false, subinfo end
iparm = iparm + 2
subunit.inout = 'in'
subunit.valinfo = { subinfo }
-- Recalculate total as a number of subdivisions.
-- subdiv[1] = number of subdivisions per previous unit (integer > 1).
total = total * subdiv[1] + subinfo.value
if not default then -- set by the first subdiv with a default defined
default = subdiv.default
end
count = count + 1
composite_units[count] = subunit
if subdiv.unit or subdiv.name then
fixups[count] = { unit = subdiv.unit, name = subdiv.name, valinfo = subunit.valinfo }
end
end
if count == 1 then
return true -- no error and no composite unit
end
for i, fixup in pairs(fixups) do
local unit = fixup.unit
local name = fixup.name
if not unit or (count > 2 and name) then
composite_units[i].fixed_name = name
else
local success, alternate = lookup(parms, unit, 'no_combination')
if not success then return false, alternate end -- should never occur
alternate.inout = 'in'
alternate.valinfo = fixup.valinfo
composite_units[i] = alternate
end
end
return true, iparm, {
utype = in_unit_table.utype,
scale = subunit.scale, -- scale of last (least significant) unit
valinfo = { { value = total, clean = subinfo.clean, denominator = subinfo.denominator } },
composite = composite_units,
default = default or in_unit_table.default
}
end
local function translate_parms(parms, kv_pairs)
-- Update fields in parms by translating each key:value in kv_pairs to terms
-- used by this module (may involve translating from local language to English).
-- Also, checks are performed which may display warnings, if enabled.
-- Return true if successful or return false, t where t is an error message table.
currency_text = nil -- local testing can hold module in memory; must clear globals
if kv_pairs.adj and kv_pairs.sing then
-- For enwiki (before translation), warn if attempt to use adj and sing
-- as the latter is a deprecated alias for the former.
if kv_pairs.adj ~= kv_pairs.sing and kv_pairs.sing ~= '' then
add_warning(parms, 1, 'cvt_unknown_option', 'sing=' .. kv_pairs.sing)
end
kv_pairs.sing = nil
end
kv_pairs.comma = kv_pairs.comma or config.comma -- for plwiki who want default comma=5
for loc_name, loc_value in pairs(kv_pairs) do
local en_name = text_code.en_option_name[loc_name]
if en_name then
local en_value = text_code.en_option_value[en_name]
if en_value == 'INTEGER' then -- altitude_ft, altitude_m, frac, sigfig
en_value = nil
if loc_value == '' then
add_warning(parms, 2, 'cvt_empty_option', loc_name)
else
local minimum
local number, is_integer = get_number(loc_value)
if en_name == 'sigfig' then
minimum = 1
elseif en_name == 'frac' then
minimum = 2
if number and number < 0 then
parms.opt_fraction_horizontal = true
number = -number
end
else
minimum = -1e6
end
if number and is_integer and number >= minimum then
en_value = number
else
local m
if en_name == 'frac' then
m = 'cvt_bad_frac'
elseif en_name == 'sigfig' then
m = 'cvt_bad_sigfig'
else
m = 'cvt_bad_altitude'
end
add_warning(parms, 1, m, loc_name .. '=' .. loc_value)
end
end
elseif en_value == 'TEXT' then -- $, input, qid, qual, stylein, styleout, tracking
en_value = loc_value ~= '' and loc_value or nil -- accept non-empty user text with no validation
if not en_value and (en_name == '$' or en_name == 'qid' or en_name == 'qual') then
add_warning(parms, 2, 'cvt_empty_option', loc_name)
elseif en_name == '$' then
-- Value should be a single character like "€" for the euro currency symbol, but anything is accepted.
currency_text = (loc_value == 'euro') and '€' or loc_value
elseif en_name == 'error' then
-- May have something like {{convert|...|error=x}} which will return the
-- conversion result if successful, or x if not (no error return).
parms.error_text = loc_value -- keep value because parms.error is nil if loc_value == ''
parms.opt_no_extra = true
elseif en_name == 'input' then
-- May have something like {{convert|input=}} (empty input) if source is an infobox
-- with optional fields. In that case, want to output nothing rather than an error.
parms.input_text = loc_value -- keep value because parms.input is nil if loc_value == ''
end
else
en_value = en_value[loc_value]
if en_value and en_value:sub(-1) == '?' then
en_value = en_value:sub(1, -2)
add_warning(parms, -1, 'cvt_deprecated', loc_name .. '=' .. loc_value)
end
if en_value == nil then
if loc_value == '' then
add_warning(parms, 2, 'cvt_empty_option', loc_name)
else
add_warning(parms, 1, 'cvt_unknown_option', loc_name .. '=' .. loc_value)
end
elseif en_value == '' then
en_value = nil -- an ignored option like adj=off
elseif type(en_value) == 'string' and en_value:sub(1, 4) == 'opt_' then
for _, v in ipairs(split(en_value, ',')) do
local lhs, rhs = v:match('^(.-)=(.+)$')
if rhs then
parms[lhs] = tonumber(rhs) or rhs
else
parms[v] = true
end
end
en_value = nil
end
end
parms[en_name] = en_value
else
add_warning(parms, 1, 'cvt_unknown_option', loc_name .. '=' .. loc_value)
end
end
local abbr_entered = parms.abbr
local cfg_abbr = config.abbr
if cfg_abbr then
-- Don't warn if invalid because every convert would show that warning.
if cfg_abbr == 'on always' then
parms.abbr = 'on'
elseif cfg_abbr == 'off always' then
parms.abbr = 'off'
elseif parms.abbr == nil then
if cfg_abbr == 'on default' then
parms.abbr = 'on'
elseif cfg_abbr == 'off default' then
parms.abbr = 'off'
end
end
end
if parms.abbr then
if parms.abbr == 'unit' then
parms.abbr = 'on'
parms.number_word = true
end
parms.abbr_org = parms.abbr -- original abbr, before any flip
elseif parms.opt_hand_hh then
parms.abbr_org = 'on'
parms.abbr = 'on'
else
parms.abbr = 'out' -- default is to abbreviate output only (use symbol, not name)
end
if parms.opt_order_out then
-- Disable options that do not work in a useful way with order=out.
parms.opt_flip = nil -- override adj=flip
parms.opt_spell_in = nil
parms.opt_spell_out = nil
parms.opt_spell_upper = nil
end
if parms.opt_spell_out and not abbr_entered then
parms.abbr = 'off' -- should show unit name when spelling the output value
end
if parms.opt_flip then
local function swap_in_out(option)
local value = parms[option]
if value == 'in' then
parms[option] = 'out'
elseif value == 'out' then
parms[option] = 'in'
end
end
swap_in_out('abbr')
swap_in_out('lk')
if parms.opt_spell_in and not parms.opt_spell_out then
-- For simplicity, and because it does not appear to be needed,
-- user cannot set an option to spell the output only.
parms.opt_spell_in = nil
parms.opt_spell_out = true
end
end
if parms.opt_spell_upper then
parms.spell_upper = parms.opt_flip and 'out' or 'in'
end
if parms.opt_table or parms.opt_tablecen then
if abbr_entered == nil and parms.lk == nil then
parms.opt_values = true
end
parms.table_align = parms.opt_table and 'right' or 'center'
end
if parms.table_align or parms.opt_sortable_on then
parms.need_table_or_sort = true
end
local disp_joins = text_code.disp_joins
local default_joins = disp_joins['b']
parms.join_between = default_joins[3] or '; '
local disp = parms.disp
if disp == nil then -- special case for the most common setting
parms.joins = default_joins
elseif disp == 'x' then
-- Later, parms.joins is set from the input parameters.
else
-- Old template does this.
local abbr = parms.abbr
if disp == 'slash' then
if abbr_entered == nil then
disp = 'slash-nbsp'
elseif abbr == 'in' or abbr == 'out' then
disp = 'slash-sp'
else
disp = 'slash-nosp'
end
elseif disp == 'sqbr' then
if abbr == 'on' then
disp = 'sqbr-nbsp'
else
disp = 'sqbr-sp'
end
end
parms.joins = disp_joins[disp] or default_joins
parms.join_between = parms.joins[3] or parms.join_between
parms.wantname = parms.joins.wantname
end
if (en_default and not parms.opt_lang_local and (parms[1] or ''):find('%d')) or parms.opt_lang_en then
from_en_table = nil
end
if en_default and from_en_table then
-- For hiwiki: localized symbol/name is defined with the US symbol/name field,
-- and is used if output uses localized numbers.
parms.opt_sp_us = true
end
return true
end
local function get_values(parms)
-- If successful, update parms and return true, v, i where
-- v = table of input values
-- i = index to next entry in parms after those processed here
-- or return false, t where t is an error message table.
local valinfo = collection() -- numbered table of input values
local range = collection() -- numbered table of range items (having, for example, 2 range items requires 3 input values)
local had_nocomma -- true if removed "nocomma" kludge from second parameter (like "tonocomma")
local parm2 = strip(parms[2])
if parm2 and parm2:sub(-7, -1) == 'nocomma' then
parms[2] = strip(parm2:sub(1, -8))
parms.opt_nocomma = true
had_nocomma = true
end
local function extractor(i)
-- If the parameter is not a value, try unpacking it as a range ("1-23" for "1 to 23").
-- However, "-1-2/3" is a negative fraction (-1⅔), so it must be extracted first.
-- Do not unpack a parameter if it is like "3-1/2" which is sometimes incorrectly
-- used instead of "3+1/2" (and which should not be interpreted as "3 to ½").
-- Unpacked items are inserted into the parms table.
-- The tail recursion allows combinations like "1x2 to 3x4".
local valstr = strip(parms[i]) -- trim so any '-' as a negative sign will be at start
local success, result = extract_number(parms, valstr, i > 1)
if not success and valstr and i < 20 then -- check i to limit abuse
local lhs, sep, rhs = valstr:match('^(%S+)%s+(%S+)%s+(%S.*)')
if lhs and not (sep == '-' and rhs:match('/')) then
if sep:find('%d') then
return success, result -- to reject {{convert|1 234 567|m}} with a decent message (en only)
end
parms[i] = rhs
table.insert(parms, i, sep)
table.insert(parms, i, lhs)
return extractor(i)
end
if not valstr:match('%-.*/') then
for _, sep in ipairs(text_code.ranges.words) do
local start, stop = valstr:find(sep, 2, true) -- start at 2 to skip any negative sign for range '-'
if start then
parms[i] = valstr:sub(stop + 1)
table.insert(parms, i, sep)
table.insert(parms, i, valstr:sub(1, start - 1))
return extractor(i)
end
end
end
end
return success, result
end
local i = 1
local is_change
while true do
local success, info = extractor(i) -- need to set parms.opt_nocomma before calling this
if not success then return false, info end
i = i + 1
if is_change then
info.is_change = true -- value is after "±" and so is a change (significant for range like {{convert|5|±|5|°C}})
is_change = nil
end
valinfo:add(info)
local range_item = get_range(strip(parms[i]))
if not range_item then
break
end
i = i + 1
range:add(range_item)
if type(range_item) == 'table' then
-- For range "x", if append unit to some values, append it to all.
parms.in_range_x = parms.in_range_x or range_item.in_range_x
parms.out_range_x = parms.out_range_x or range_item.out_range_x
parms.abbr_range_x = parms.abbr_range_x or range_item.abbr_range_x
is_change = range_item.is_range_change
end
end
if range.n > 0 then
if range.n > 30 then -- limit abuse, although 4 is a more likely upper limit
return false, { 'cvt_invalid_num' } -- misleading message but it will do
end
parms.range = range
elseif had_nocomma then
return false, { 'cvt_unknown', parm2 }
end
return true, valinfo, i
end
local function simple_get_values(parms)
-- If input is like "{{convert|valid_value|valid_unit|...}}",
-- return true, i, in_unit, in_unit_table
-- i = index in parms of what follows valid_unit, if anything.
-- The valid_value is not negative and does not use a fraction, and
-- no options requiring further processing of the input are used.
-- Otherwise, return nothing or return false, parm1 for caller to interpret.
-- Testing shows this function is successful for 96% of converts in articles,
-- and that on average it speeds up converts by 8%.
local clean = to_en(strip(parms[1] or ''), parms)
if parms.opt_ri or parms.opt_spell_in or #clean > 10 or not clean:match('^[0-9.]+$') then
return false, clean
end
local value = tonumber(clean)
if not value then return end
local info = {
value = value,
altvalue = value,
singular = (value == 1),
clean = clean,
show = with_separator(parms, clean),
}
local in_unit = strip(parms[2])
local success, in_unit_table = lookup(parms, in_unit, 'no_combination')
if not success then return end
in_unit_table.valinfo = { info }
return true, 3, in_unit, in_unit_table
end
local function wikidata_call(parms, operation, ...)
-- Return true, s where s is the result of a Wikidata operation,
-- or return false, t where t is an error message table.
local function worker(...)
wikidata_code = wikidata_code or require(wikidata_module)
wikidata_data = wikidata_data or mw.loadData(wikidata_data_module)
return wikidata_code[operation](wikidata_data, ...)
end
local success, status, result = pcall(worker, ...)
if success then
return status, result
end
if parms.opt_sortable_debug then
-- Use debug=yes to crash if an error while accessing Wikidata.
error('Error accessing Wikidata: ' .. status, 0)
end
return false, { 'cvt_wd_fail' }
end
local function get_parms(parms, args)
-- If successful, update parms and return true, unit where
-- parms is a table of all arguments passed to the template
-- converted to named arguments, and
-- unit is the input unit table;
-- or return false, t where t is an error message table.
-- For special processing (not a convert), can also return
-- true, wikitext where wikitext is the final result.
-- The returned input unit table may be for a fake unit using the specified
-- unit code as the symbol and name, and with bad_mcode = message code table.
-- MediaWiki removes leading and trailing whitespace from the values of
-- named arguments. However, the values of numbered arguments include any
-- whitespace entered in the template, and whitespace is used by some
-- parameters (example: the numbered parameters associated with "disp=x").
local kv_pairs = {} -- table of input key:value pairs where key is a name; needed because cannot iterate parms and add new fields to it
for k, v in pairs(args) do
if type(k) == 'number' or k == 'test' then -- parameter "test" is reserved for testing and is not translated
parms[k] = v
else
kv_pairs[k] = v
end
end
if parms.test == 'wikidata' then
local ulookup = function (ucode)
-- Use empty table for parms so it does not accumulate results when used repeatedly.
return lookup({}, ucode, 'no_combination')
end
return wikidata_call(parms, '_listunits', ulookup)
end
local success, msg = translate_parms(parms, kv_pairs)
if not success then return false, msg end
if parms.input then
success, msg = wikidata_call(parms, '_adjustparameters', parms, 1)
if not success then return false, msg end
end
local success, i, in_unit, in_unit_table = simple_get_values(parms)
if not success then
if type(i) == 'string' and i:match('^NNN+$') then
-- Some infoboxes have examples like {{convert|NNN|m}} (3 or more "N").
-- Output an empty string for these.
return false, { 'cvt_no_output' }
end
local valinfo
success, valinfo, i = get_values(parms)
if not success then return false, valinfo end
in_unit = strip(parms[i])
i = i + 1
success, in_unit_table = lookup(parms, in_unit, 'no_combination')
if not success then
in_unit = in_unit or ''
if parms.opt_ignore_error then -- display given unit code with no error (for use with {{val}})
in_unit_table = '' -- suppress error message and prevent processing of output unit
end
in_unit_table = setmetatable({
symbol = in_unit, name2 = in_unit, utype = in_unit,
scale = 1, default = '', defkey = '', linkey = '',
bad_mcode = in_unit_table }, unit_mt)
end
in_unit_table.valinfo = valinfo
end
if parms.test == 'msg' then
-- Am testing the messages produced when no output unit is specified, and
-- the input unit has a missing or invalid default.
-- Set two units for testing that.
-- LATER: Remove this code.
if in_unit == 'chain' then
in_unit_table.default = nil -- no default
elseif in_unit == 'rd' then
in_unit_table.default = "ft!X!m" -- an invalid expression
end
end
in_unit_table.inout = 'in' -- this is an input unit
if not parms.range then
local success, inext, composite_unit = get_composite(parms, i, in_unit_table)
if not success then return false, inext end
if composite_unit then
in_unit_table = composite_unit
i = inext
end
end
if in_unit_table.builtin == 'mach' then
-- As with old template, a number following Mach as the input unit is the altitude.
-- That is deprecated: should use altitude_ft=NUMBER or altitude_m=NUMBER.
local success, info
success = tonumber(parms[i]) -- this will often work and will give correct result for values like 2e4 without forcing output scientific notation
if success then
info = { value = success }
else
success, info = extract_number(parms, parms[i], false, true)
end
if success then
i = i + 1
in_unit_table.altitude = info.value
end
end
local word = strip(parms[i])
i = i + 1
local precision, is_bad_precision
local function set_precision(text)
local number, is_integer = get_number(text)
if number then
if is_integer then
precision = number
else
precision = text
is_bad_precision = true
end
return true -- text was used for precision, good or bad
end
end
if word and not set_precision(word) then
parms.out_unit = parms.out_unit or word
if set_precision(strip(parms[i])) then
i = i + 1
end
end
if parms.opt_adj_mid then
word = parms[i]
i = i + 1
if word then -- mid-text words
if word:sub(1, 1) == '-' then
parms.mid = word
else
parms.mid = ' ' .. word
end
end
end
if parms.opt_one_preunit then
parms[parms.opt_flip and 'preunit2' or 'preunit1'] = preunits(1, parms[i])
i = i + 1
end
if parms.disp == 'x' then
-- Following is reasonably compatible with the old template.
local first = parms[i] or ''
local second = parms[i+1] or ''
i = i + 2
if strip(first) == '' then -- user can enter ' ' rather than ' ' to avoid the default
first = ' [ ' .. first
second = ' ]' .. second
end
parms.joins = { first, second }
elseif parms.opt_two_preunits then
local p1, p2 = preunits(2, parms[i], parms[i+1])
i = i + 2
if parms.preunit1 then
-- To simplify documentation, allow unlikely use of adj=pre with disp=preunit
-- (however, an output unit must be specified with adj=pre and with disp=preunit).
parms.preunit1 = parms.preunit1 .. p1
parms.preunit2 = p2
else
parms.preunit1, parms.preunit2 = p1, p2
end
end
if precision == nil then
if set_precision(strip(parms[i])) then
i = i + 1
end
end
if is_bad_precision then
add_warning(parms, 1, 'cvt_bad_prec', precision)
else
parms.precision = precision
end
for j = i, i + 3 do
local parm = parms[j] -- warn if find a non-empty extraneous parameter
if parm and parm:match('%S') then
add_warning(parms, 1, 'cvt_unknown_option', parm)
break
end
end
return true, in_unit_table
end
local function record_default_precision(parms, out_current, precision)
-- If necessary, adjust parameters and return a possibly adjusted precision.
-- When converting a range of values where a default precision is required,
-- that default is calculated for each value because the result sometimes
-- depends on the precise input and output values. This function may cause
-- the entire convert process to be repeated in order to ensure that the
-- same default precision is used for each individual convert.
-- If that were not done, a range like 1000 to 1000.4 may give poor results
-- because the first output could be heavily rounded, while the second is not.
-- For range 1000.4 to 1000, this function can give the second convert the
-- same default precision that was used for the first.
if not parms.opt_round_each then
local maxdef = out_current.max_default_precision
if maxdef then
if maxdef < precision then
parms.do_convert_again = true
out_current.max_default_precision = precision
else
precision = out_current.max_default_precision
end
else
out_current.max_default_precision = precision
end
end
return precision
end
local function default_precision(parms, invalue, inclean, denominator, outvalue, in_current, out_current, extra)
-- Return a default value for precision (an integer like 2, 0, -2).
-- If denominator is not nil, it is the value of the denominator in inclean.
-- Code follows procedures used in old template.
local fudge = 1e-14 -- {{Order of magnitude}} adds this, so we do too
local prec, minprec, adjust
local subunit_ignore_trailing_zero
local subunit_more_precision -- kludge for "in" used in input like "|2|ft|6|in"
local composite = in_current.composite
if composite then
subunit_ignore_trailing_zero = true -- input "|2|st|10|lb" has precision 0, not -1
if composite[#composite].exception == 'subunit_more_precision' then
subunit_more_precision = true -- do not use standard precision with input like "|2|ft|6|in"
end
end
if denominator and denominator > 0 then
prec = math.max(log10(denominator), 1)
else
-- Count digits after decimal mark, handling cases like '12.345e6'.
local exponent
local integer, dot, decimals, expstr = inclean:match('^(%d*)(%.?)(%d*)(.*)')
local e = expstr:sub(1, 1)
if e == 'e' or e == 'E' then
exponent = tonumber(expstr:sub(2))
end
if dot == '' then
prec = subunit_ignore_trailing_zero and 0 or -integer:match('0*$'):len()
else
prec = #decimals
end
if exponent then
-- So '1230' and '1.23e3' both give prec = -1, and '0.00123' and '1.23e-3' give 5.
prec = prec - exponent
end
end
if in_current.istemperature and out_current.istemperature then
-- Converting between common temperatures (°C, °F, °R, K); not keVT.
-- Kelvin value can be almost zero, or small but negative due to precision problems.
-- Also, an input value like -300 C (below absolute zero) gives negative kelvins.
-- Calculate minimum precision from absolute value.
adjust = 0
local kelvin = abs((invalue - in_current.offset) * in_current.scale)
if kelvin < 1e-8 then -- assume nonzero due to input or calculation precision problem
minprec = 2
else
minprec = 2 - floor(log10(kelvin) + fudge) -- 3 sigfigs in kelvin
end
else
if invalue == 0 or outvalue <= 0 then
-- We are never called with a negative outvalue, but it might be zero.
-- This is special-cased to avoid calculation exceptions.
return record_default_precision(parms, out_current, 0)
end
if out_current.exception == 'integer_more_precision' and floor(invalue) == invalue then
-- With certain output units that sometimes give poor results
-- with default rounding, use more precision when the input
-- value is equal to an integer. An example of a poor result
-- is when input 50 gives a smaller output than input 49.5.
-- Experiment shows this helps, but it does not eliminate all
-- surprises because it is not clear whether "50" should be
-- interpreted as "from 45 to 55" or "from 49.5 to 50.5".
adjust = -log10(in_current.scale)
elseif subunit_more_precision then
-- Conversion like "{{convert|6|ft|1|in|cm}}" (where subunit is "in")
-- has a non-standard adjust value, to give more output precision.
adjust = log10(out_current.scale) + 2
else
adjust = log10(abs(invalue / outvalue))
end
adjust = adjust + log10(2)
-- Ensure that the output has at least two significant figures.
minprec = 1 - floor(log10(outvalue) + fudge)
end
if extra then
adjust = extra.adjust or adjust
minprec = extra.minprec or minprec
end
return record_default_precision(parms, out_current, math.max(floor(prec + adjust), minprec))
end
local function convert(parms, invalue, info, in_current, out_current)
-- Convert given input value from one unit to another.
-- Return output_value (a number) if a simple convert, or
-- return f, t where
-- f = true, t = table of information with results, or
-- f = false, t = error message table.
local inscale = in_current.scale
local outscale = out_current.scale
if not in_current.iscomplex and not out_current.iscomplex then
return invalue * (inscale / outscale) -- minimize overhead for most common case
end
if in_current.invert or out_current.invert then
-- Inverted units, such as inverse length, inverse time, or
-- fuel efficiency. Built-in units do not have invert set.
if (in_current.invert or 1) * (out_current.invert or 1) < 0 then
return 1 / (invalue * inscale * outscale)
end
return invalue * (inscale / outscale)
elseif in_current.offset then
-- Temperature (there are no built-ins for this type of unit).
if info.is_change then
return invalue * (inscale / outscale)
end
return (invalue - in_current.offset) * (inscale / outscale) + out_current.offset
else
-- Built-in unit.
local in_builtin = in_current.builtin
local out_builtin = out_current.builtin
if in_builtin and out_builtin then
if in_builtin == out_builtin then
return invalue
end
-- There are no cases (yet) where need to convert from one
-- built-in unit to another, so this should never occur.
return false, { 'cvt_bug_convert' }
end
if in_builtin == 'mach' or out_builtin == 'mach' then
-- Should check that only one altitude is given but am planning to remove
-- in_current.altitude (which can only occur when Mach is the input unit),
-- and out_current.altitude cannot occur.
local alt = parms.altitude_ft or in_current.altitude
if not alt and parms.altitude_m then
alt = parms.altitude_m / 0.3048 -- 1 ft = 0.3048 m
end
local spd = speed_of_sound(alt)
if in_builtin == 'mach' then
inscale = spd
return invalue * (inscale / outscale)
end
outscale = spd
local adjust = 0.1 / inscale
return true, {
outvalue = invalue * (inscale / outscale),
adjust = log10(adjust) + log10(2),
}
elseif in_builtin == 'hand' then
-- 1 hand = 4 inches; 1.2 hands = 6 inches.
-- Decimals of a hand are only defined for the first digit, and
-- the first fractional digit should be a number of inches (1, 2 or 3).
-- However, this code interprets the entire fractional part as the number
-- of inches / 10 (so 1.75 inches would be 0.175 hands).
-- A value like 12.3 hands is exactly 12*4 + 3 inches; base default precision on that.
local integer, fracpart = math.modf(invalue)
local inch_value = 4 * integer + 10 * fracpart -- equivalent number of inches
local factor = inscale / outscale
if factor == 4 then
-- Am converting to inches: show exact result, and use "inches" not "in" by default.
if parms.abbr_org == nil then
out_current.usename = true
end
local show = format('%g', abs(inch_value)) -- show and clean are unsigned
if not show:find('e', 1, true) then
return true, {
invalue = inch_value,
outvalue = inch_value,
clean = show,
show = show,
}
end
end
local outvalue = (integer + 2.5 * fracpart) * factor
local fracstr = info.clean:match('%.(.*)') or ''
local fmt
if fracstr == '' then
fmt = '%.0f'
else
fmt = '%.' .. format('%d', #fracstr - 1) .. 'f'
end
return true, {
invalue = inch_value,
clean = format(fmt, inch_value),
outvalue = outvalue,
minprec = 0,
}
end
end
return false, { 'cvt_bug_convert' } -- should never occur
end
local function user_style(parms, i)
-- Return text for a user-specified style for a table cell, or '' if none,
-- given i = 1 (input style) or 2 (output style).
local style = parms[(i == 1) and 'stylein' or 'styleout']
if style then
style = style:gsub('"', '')
if style ~= '' then
if style:sub(-1) ~= ';' then
style = style .. ';'
end
return style
end
end
return ''
end
local function make_table_or_sort(parms, invalue, info, in_current, scaled_top)
-- Set options to handle output for a table or a sort key, or both.
-- The text sort key is based on the value resulting from converting
-- the input to a fake base unit with scale = 1, and other properties
-- required for a conversion derived from the input unit.
-- For other modules, return the sort key in a hidden span element, and
-- the scaled value used to generate the sort key.
-- If scaled_top is set, it is the scaled value of the numerator of a per unit
-- to be combined with this unit (the denominator) to make the sort key.
-- Scaling only works with units that convert with a factor (not temperature).
local sortkey, scaled_value
if parms.opt_sortable_on then
local base = { -- a fake unit with enough fields for a valid convert
scale = 1,
invert = in_current.invert and 1,
iscomplex = in_current.iscomplex,
offset = in_current.offset and 0,
}
local outvalue, extra = convert(parms, invalue, info, in_current, base)
if extra then
outvalue = extra.outvalue
end
if in_current.istemperature then
-- Have converted to kelvin; assume numbers close to zero have a
-- rounding error and should be zero.
if abs(outvalue) < 1e-12 then
outvalue = 0
end
end
if scaled_top and outvalue ~= 0 then
outvalue = scaled_top / outvalue
end
scaled_value = outvalue
if not valid_number(outvalue) then
if outvalue < 0 then
sortkey = '1000000000000000000'
else
sortkey = '9000000000000000000'
end
elseif outvalue == 0 then
sortkey = '5000000000000000000'
else
local mag = floor(log10(abs(outvalue)) + 1e-14)
local prefix
if outvalue > 0 then
prefix = 7000 + mag
else
prefix = 2999 - mag
outvalue = outvalue + 10^(mag+1)
end
sortkey = format('%d', prefix) .. format('%015.0f', floor(outvalue * 10^(14-mag)))
end
end
local sortspan
if sortkey and not parms.table_align then
sortspan = parms.opt_sortable_debug and
'<span data-sort-value="' .. sortkey .. '♠"><span style="border:1px solid">' .. sortkey .. '♠</span></span>' or
'<span data-sort-value="' .. sortkey .. '♠"></span>'
parms.join_before = sortspan
end
if parms.table_align then
local sort
if sortkey then
sort = ' data-sort-value="' .. sortkey .. '"'
if parms.opt_sortable_debug then
parms.join_before = '<span style="border:1px solid">' .. sortkey .. '</span>'
end
else
sort = ''
end
local style = 'style="text-align:' .. parms.table_align .. ';'
local joins = {}
for i = 1, 2 do
joins[i] = (i == 1 and '' or '\n|') .. style .. user_style(parms, i) .. '"' .. sort .. '|'
end
parms.table_joins = joins
end
return sortspan, scaled_value
end
local cvt_to_hand
local function cvtround(parms, info, in_current, out_current)
-- Return true, t where t is a table with the conversion results; fields:
-- show = rounded, formatted string with the result of converting value in info,
-- using the rounding specified in parms.
-- singular = true if result (after rounding and ignoring any negative sign)
-- is "1", or like "1.00", or is a fraction with value < 1;
-- (and more fields shown below, and a calculated 'absvalue' field).
-- or return false, t where t is an error message table.
-- Input info.clean uses en digits (it has been translated, if necessary).
-- Output show uses en or non-en digits as appropriate, or can be spelled.
if out_current.builtin == 'hand' then
return cvt_to_hand(parms, info, in_current, out_current)
end
local invalue = in_current.builtin == 'hand' and info.altvalue or info.value
local outvalue, extra = convert(parms, invalue, info, in_current, out_current)
if parms.need_table_or_sort then
parms.need_table_or_sort = nil -- process using first input value only
make_table_or_sort(parms, invalue, info, in_current)
end
if extra then
if not outvalue then return false, extra end
invalue = extra.invalue or invalue
outvalue = extra.outvalue
end
if not valid_number(outvalue) then
return false, { 'cvt_invalid_num' }
end
local isnegative
if outvalue < 0 then
isnegative = true
outvalue = -outvalue
end
local precision, show, exponent
local denominator = out_current.frac
if denominator then
show = fraction_table(outvalue, denominator)
else
precision = parms.precision
if not precision then
if parms.sigfig then
show, exponent = make_sigfig(outvalue, parms.sigfig)
elseif parms.opt_round then
local n = parms.opt_round
if n == 0.5 then
local integer, fracpart = math.modf(floor(2 * outvalue + 0.5) / 2)
if fracpart == 0 then
show = format('%.0f', integer)
else
show = format('%.1f', integer + fracpart)
end
else
show = format('%.0f', floor((outvalue / n) + 0.5) * n)
end
elseif in_current.builtin == 'mach' then
local sigfig = info.clean:gsub('^[0.]+', ''):gsub('%.', ''):len() + 1
show, exponent = make_sigfig(outvalue, sigfig)
else
local inclean = info.clean
if extra then
inclean = extra.clean or inclean
show = extra.show
end
if not show then
precision = default_precision(parms, invalue, inclean, info.denominator, outvalue, in_current, out_current, extra)
end
end
end
end
if precision then
if precision >= 0 then
local fudge
if precision <= 8 then
-- Add a fudge to handle common cases of bad rounding due to inability
-- to precisely represent some values. This makes the following work:
-- {{convert|-100.1|C|K}} and {{convert|5555000|um|m|2}}.
-- Old template uses #expr round, which invokes PHP round().
-- LATER: Investigate how PHP round() works.
fudge = 2e-14
else
fudge = 0
end
local fmt = '%.' .. format('%d', precision) .. 'f'
local success
success, show = pcall(format, fmt, outvalue + fudge)
if not success then
return false, { 'cvt_big_prec', tostring(precision) }
end
else
precision = -precision -- #digits to zero (in addition to any digits after dot)
local shift = 10 ^ precision
show = format('%.0f', outvalue/shift)
if show ~= '0' then
exponent = #show + precision
end
end
end
local t = format_number(parms, show, exponent, isnegative)
if type(show) == 'string' then
-- Set singular using match because on some systems 0.99999999999999999 is 1.0.
if exponent then
t.singular = (exponent == 1 and show:match('^10*$'))
else
t.singular = (show == '1' or show:match('^1%.0*$'))
end
else
t.fraction_table = show
t.singular = (outvalue <= 1) -- cannot have 'fraction == 1', but if it were possible it would be singular
end
t.raw_absvalue = outvalue -- absolute value before rounding
return true, setmetatable(t, {
__index = function (self, key)
if key == 'absvalue' then
-- Calculate absolute value after rounding, if needed.
local clean, exponent = rawget(self, 'clean'), rawget(self, 'exponent')
local value = tonumber(clean) -- absolute value (any negative sign has been ignored)
if exponent then
value = value * 10^exponent
end
rawset(self, key, value)
return value
end
end })
end
function cvt_to_hand(parms, info, in_current, out_current)
-- Convert input to hands, inches.
-- Return true, t where t is a table with the conversion results;
-- or return false, t where t is an error message table.
if parms.abbr_org == nil then
out_current.usename = true -- default is to show name not symbol
end
local precision = parms.precision
local frac = out_current.frac
if not frac and precision and precision > 1 then
frac = (precision == 2) and 2 or 4
end
local out_next = out_current.out_next
if out_next then
-- Use magic knowledge to determine whether the next unit is inches without requiring i18n.
-- The following ensures that when the output combination "hand in" is used, the inches
-- value is rounded to match the hands value. Also, displaying say "61½" instead of 61.5
-- is better as 61.5 implies the value is not 61.4.
if out_next.exception == 'subunit_more_precision' then
out_next.frac = frac
end
end
-- Convert to inches; calculate hands from that.
local dummy_unit_table = { scale = out_current.scale / 4, frac = frac }
local success, outinfo = cvtround(parms, info, in_current, dummy_unit_table)
if not success then return false, outinfo end
local tfrac = outinfo.fraction_table
local inches = outinfo.raw_absvalue
if tfrac then
inches = floor(inches) -- integer part only; fraction added later
else
inches = floor(inches + 0.5) -- a hands measurement never shows decimals of an inch
end
local hands, inches = divide(inches, 4)
outinfo.absvalue = hands + inches/4 -- supposed to be the absolute rounded value, but this is close enough
local inchstr = tostring(inches) -- '0', '1', '2' or '3'
if precision and precision <= 0 then -- using negative or 0 for precision rounds to nearest hand
hands = floor(outinfo.raw_absvalue/4 + 0.5)
inchstr = ''
elseif tfrac then
-- Always show an integer before fraction (like "15.0½") because "15½" means 15-and-a-half hands.
inchstr = numdot .. format_fraction(parms, 'out', false, inchstr, tfrac.numstr, tfrac.denstr)
else
inchstr = numdot .. from_en(inchstr)
end
outinfo.show = outinfo.sign .. with_separator(parms, format('%.0f', hands)) .. inchstr
return true, outinfo
end
local function evaluate_condition(value, condition)
-- Return true or false from applying a conditional expression to value,
-- or throw an error if invalid.
-- A very limited set of expressions is supported:
-- v < 9
-- v * 9 < 9
-- where
-- 'v' is replaced with value
-- 9 is any number (as defined by Lua tonumber)
-- only en digits are accepted
-- '<' can also be '<=' or '>' or '>='
-- In addition, the following form is supported:
-- LHS and RHS
-- where
-- LHS, RHS = any of above expressions.
local function compare(value, text)
local arithop, factor, compop, limit = text:match('^%s*v%s*([*]?)(.-)([<>]=?)(.*)$')
if arithop == nil then
error('Invalid default expression', 0)
elseif arithop == '*' then
factor = tonumber(factor)
if factor == nil then
error('Invalid default expression', 0)
end
value = value * factor
end
limit = tonumber(limit)
if limit == nil then
error('Invalid default expression', 0)
end
if compop == '<' then
return value < limit
elseif compop == '<=' then
return value <= limit
elseif compop == '>' then
return value > limit
elseif compop == '>=' then
return value >= limit
end
error('Invalid default expression', 0) -- should not occur
end
local lhs, rhs = condition:match('^(.-%W)and(%W.*)')
if lhs == nil then
return compare(value, condition)
end
return compare(value, lhs) and compare(value, rhs)
end
local function get_default(value, unit_table)
-- Return true, s where s = name of unit's default output unit,
-- or return false, t where t is an error message table.
-- Some units have a default that depends on the input value
-- (the first value if a range of values is used).
-- If '!' is in the default, the first bang-delimited field is an
-- expression that uses 'v' to represent the input value.
-- Example: 'v < 120 ! small ! big ! suffix' (suffix is optional)
-- evaluates 'v < 120' as a boolean with result
-- 'smallsuffix' if (value < 120), or 'bigsuffix' otherwise.
-- Input must use en digits and '.' decimal mark.
local default = data_code.default_exceptions[unit_table.defkey or unit_table.symbol] or unit_table.default
if not default then
local per = unit_table.per
if per then
local function a_default(v, u)
local success, ucode = get_default(v, u)
if not success then
return '?' -- an unlikely error has occurred; will cause lookup of default to fail
end
-- Attempt to use only the first unit if a combination or output multiple.
-- This is not bulletproof but should work for most cases.
-- Where it does not work, the convert will need to specify the wanted output unit.
local t = all_units[ucode]
if t then
local combo = t.combination
if combo then
-- For a multiple like ftin, the "first" unit (ft) is last in the combination.
local i = t.multiple and table_len(combo) or 1
ucode = combo[i]
end
else
-- Try for an automatically generated combination.
local item = ucode:match('^(.-)%+') or ucode:match('^(%S+)%s')
if all_units[item] then
return item
end
end
return ucode
end
local unit1, unit2 = per[1], per[2]
local def1 = (unit1 and a_default(value, unit1) or unit_table.vprefix or '')
local def2 = a_default(1, unit2) -- 1 because per unit of denominator
return true, def1 .. '/' .. def2
end
return false, { 'cvt_no_default', unit_table.symbol }
end
if default:find('!', 1, true) == nil then
return true, default
end
local t = split(default, '!')
if #t == 3 or #t == 4 then
local success, result = pcall(evaluate_condition, value, t[1])
if success then
default = result and t[2] or t[3]
if #t == 4 then
default = default .. t[4]
end
return true, default
end
end
return false, { 'cvt_bad_default', unit_table.symbol }
end
local linked_pages -- to record linked pages so will not link to the same page more than once
local function unlink(unit_table)
-- Forget that the given unit has previously been linked (if it has).
-- That is needed when processing a range of inputs or outputs when an id
-- for the first range value may have been evaluated, but only an id for
-- the last value is displayed, and that id may need to be linked.
linked_pages[unit_table.unitcode or unit_table] = nil
end
local function make_link(link, id, unit_table)
-- Return wikilink "[[link|id]]", possibly abbreviated as in examples:
-- [[Mile|mile]] --> [[mile]]
-- [[Mile|miles]] --> [[mile]]s
-- However, just id is returned if:
-- * no link given (so caller does not need to check if a link was defined); or
-- * link has previously been used during the current convert (to avoid overlinking).
local link_key
if unit_table then
link_key = unit_table.unitcode or unit_table
else
link_key = link
end
if not link or link == '' or linked_pages[link_key] then
return id
end
linked_pages[link_key] = true
-- Following only works for language en, but it should be safe on other wikis,
-- and overhead of doing it generally does not seem worthwhile.
local l = link:sub(1, 1):lower() .. link:sub(2)
if link == id or l == id then
return '[[' .. id .. ']]'
elseif link .. 's' == id or l .. 's' == id then
return '[[' .. id:sub(1, -2) .. ']]s'
else
return '[[' .. link .. '|' .. id .. ']]'
end
end
local function variable_name(clean, unit_table, exp_multiplier, key_id)
-- A unit name may depend on the value in some languages.
-- Parameter clean is the unsigned value in en digits, as a string.
-- It may represent a number ("1.0") or a fraction ("1+2/3").
-- In varname, fields are separated with "!" and are not empty.
-- A field for a unit using an SI prefix has the prefix name inserted,
-- replacing '#' if found, or before the field otherwise.
if clean:match('[./]') or clean:find('⁄', 1, true) then -- float or fraction
if exp_multiplier then
clean = exp_multiplier -- force selection of name for a large integer
else
clean = 34.5 -- force selection of name for a float value
end
else
clean = tonumber(clean) * (exp_multiplier or 1)
end
local name1, vname
if key_id == 'pername' and unit_table.pername then
vname = unit_table.pername
elseif unit_table.varname then
local splitvname = split(unit_table.varname, '!')
name1 = unit_table.name1
vname = mw.language.getContentLanguage():convertPlural(clean, name1, unpack(splitvname))
else
return clean == 1 and unit_table.name1 or unit_table.name2
end
if vname == name1 then
-- SI prefix (if any) has been inserted by unit_prefixed_mt.
else
local si_name = rawget(unit_table, 'si_name') or ''
local pos = vname:find('#', 1, true)
if pos then
vname = vname:sub(1, pos - 1) .. si_name .. vname:sub(pos + 1)
else
vname = si_name .. vname
end
end
return vname
end
local function linked_id(parms, unit_table, key_id, want_link, clean, exp_multiplier)
-- Return final unit id (symbol or name), optionally with a wikilink,
-- and update unit_table.sep if required.
-- key_id is one of: 'symbol', 'sym_us', 'name1', 'name1_us', 'name2', 'name2_us', 'pername'.
local abbr_on = (key_id == 'symbol' or key_id == 'sym_us')
if abbr_on and want_link then
local symlink = rawget(unit_table, 'symlink')
if symlink then
return symlink -- for exceptions that have the linked symbol built-in
end
end
local multiplier = rawget(unit_table, 'multiplier')
local per = unit_table.per
if per then
local paren1, paren2 = '', '' -- possible parentheses around bottom unit
local unit1 = per[1] -- top unit_table, or nil
local unit2 = per[2] -- bottom unit_table
if abbr_on then
if not unit1 then
unit_table.sep = '' -- no separator in "$2/acre"
end
if not want_link then
local symbol = unit_table.symbol_raw
if symbol then
return symbol -- for exceptions that have the symbol built-in
end
end
if (unit2.symbol):find('⋅', 1, true) then
paren1, paren2 = '(', ')'
end
end
local key_id2 -- unit2 is always singular
if key_id == 'name2' then
key_id2 = 'name1'
elseif key_id == 'name2_us' then
key_id2 = 'name1_us'
else
key_id2 = key_id
end
if key_id2 == 'name1' or key_id2 == 'name1_us' then
key_id2 = unit2.pername and 'pername' or key_id2 -- ukwiki has some units with a different name in "per unitname"
end
local result
if abbr_on then
result = '/'
elseif omitsep then
result = per_word
elseif unit1 then
result = ' ' .. per_word .. ' '
else
result = per_word .. ' '
end
if want_link and unit_table.link then
if varname and not abbr_on then
result = (unit1 and variable_name(clean, unit1, exp_multiplier) or '') .. result .. variable_name('1', unit2, exp_multiplier, key_id2)
else
result = (unit1 and linked_id(parms, unit1, key_id, false, clean) or '') .. result .. linked_id(parms, unit2, key_id2, false, '1')
end
if omit_separator(result) then
unit_table.sep = ''
end
return make_link(unit_table.link, result, unit_table)
end
if unit1 then
result = linked_id(parms, unit1, key_id, want_link, clean) .. result
if unit1.sep then
unit_table.sep = unit1.sep
end
elseif omitsep then
unit_table.sep = ''
end
return result .. paren1 .. linked_id(parms, unit2, key_id2, want_link, '1') .. paren2
end
if multiplier then
-- A multiplier (like "100" in "100km") forces the unit to be plural.
multiplier = from_en(multiplier)
if not omitsep then
multiplier = multiplier .. (abbr_on and ' ' or ' ')
end
if not abbr_on then
if key_id == 'name1' then
key_id = 'name2'
elseif key_id == 'name1_us' then
key_id = 'name2_us'
end
end
else
multiplier = ''
end
local id = unit_table.fixed_name or ((varname and not abbr_on) and variable_name(clean, unit_table, exp_multiplier, key_id) or unit_table[key_id])
if omit_separator(id) then
unit_table.sep = ''
end
if want_link then
local link = data_code.link_exceptions[unit_table.linkey or unit_table.symbol] or unit_table.link
if link then
local before = ''
local i = unit_table.customary
if i == 1 and parms.opt_sp_us then
i = 2 -- show "U.S." not "US"
end
if i == 3 and abbr_on then
i = 4 -- abbreviate "imperial" to "imp"
end
local customary = text_code.customary_units[i]
if customary then
-- LATER: This works for language en only, but it's esoteric so ignore for now.
local pertext
if id:sub(1, 1) == '/' then
-- Want unit "/USgal" to display as "/U.S. gal", not "U.S. /gal".
pertext = '/'
id = id:sub(2)
elseif id:sub(1, 4) == 'per ' then
-- Similarly want "per U.S. gallon", not "U.S. per gallon" (but in practice this is unlikely to be used).
pertext = 'per '
id = id:sub(5)
else
pertext = ''
end
-- Omit any "US"/"U.S."/"imp"/"imperial" from start of id since that will be inserted.
local removes = (i < 3) and { 'US ', 'US ', 'U.S. ', 'U.S. ' } or { 'imp ', 'imp ', 'imperial ' }
for _, prefix in ipairs(removes) do
local plen = #prefix
if id:sub(1, plen) == prefix then
id = id:sub(plen + 1)
break
end
end
before = pertext .. make_link(customary.link, customary[1]) .. ' '
end
id = before .. make_link(link, id, unit_table)
end
end
return multiplier .. id
end
local function make_id(parms, which, unit_table)
-- Return id, f where
-- id = unit name or symbol, possibly modified
-- f = true if id is a name, or false if id is a symbol
-- using the value for index 'which', and for 'in' or 'out' (unit_table.inout).
-- Result is '' if no symbol/name is to be used.
-- In addition, set unit_table.sep = ' ' or ' ' or ''
-- (the separator that caller will normally insert before the id).
if parms.opt_values then
unit_table.sep = ''
return ''
end
local inout = unit_table.inout
local info = unit_table.valinfo[which]
local lk = parms.lk
local want_link = (lk == 'on' or lk == inout)
local singular = info.singular
local want_name
local exp_multiplier
if unit_table.usename then
want_name = true
else
if parms.abbr_org == nil then
if parms.wantname then
want_name = true
end
if unit_table.usesymbol then
want_name = false
end
end
if want_name == nil then
local abbr = parms.abbr
if abbr == 'on' or abbr == inout or (abbr == 'mos' and inout == 'out') then
want_name = false
else
want_name = true
end
end
end
local key
if want_name then
if lk == nil and unit_table.builtin == 'hand' then
want_link = true
end
if parms.opt_use_nbsp then
unit_table.sep = ' '
else
unit_table.sep = ' '
end
if parms.opt_singular then
local value
if inout == 'in' then
value = info.value
else
value = info.absvalue
end
if value then -- some unusual units do not always set value field
value = abs(value)
singular = (0 < value and value < 1.0001)
end
end
if unit_table.engscale then
-- engscale: so "|1|e3kg" gives "1 thousand kilograms" (plural)
singular = false
exp_multiplier = 10^unit_table.engscale.exponent -- '1 gram' and '1 thousand grams', for example, may use different names for the unit in some languages
end
key = (parms.opt_adjectival or singular) and 'name1' or 'name2'
if parms.opt_sp_us then
key = key .. '_us'
end
else
if unit_table.builtin == 'hand' then
if parms.opt_hand_hh then
unit_table.symbol = 'hh' -- LATER: might want i18n applied to this
end
end
unit_table.sep = ' '
key = parms.opt_sp_us and 'sym_us' or 'symbol'
end
return linked_id(parms, unit_table, key, want_link, info.clean, exp_multiplier), want_name
end
local function decorate_value(parms, unit_table, which, enable_number_word)
-- If needed, update unit_table so values will be shown with extra information.
-- For consistency with the old template (but different from fmtpower),
-- the style to display powers of 10 includes "display:none" to allow some
-- browsers to copy, for example, "10³" as "10^3", rather than as "103".
-- The engscale table may have entries such as either of the following:
-- ["3"] = { "thousand", exponent = 3 },
-- ["3"] = { name1 = "A", varname = "B!C!D", exponent = 3 },
-- The first option always uses "thousand" as the exponent name.
-- The second option uses one of A, B, C, D as the exponent name, depending on the value.
local info
local engscale = unit_table.engscale
local prefix = unit_table.vprefix
if engscale or prefix then
info = unit_table.valinfo[which]
if info.decorated then
return -- do not redecorate if repeating convert
end
info.decorated = true
if engscale then
-- Range |10|-|20|e3km| gives '10×10³–20×10³' or '10–20 thousand'.
local inout = unit_table.inout
local abbr = parms.abbr
if (abbr == 'on' or abbr == inout) and not (unit_table.this_number_word or parms.number_word) then
info.show = info.show ..
'<span style="margin-left:0.2em">×<span style="margin-left:0.1em">' ..
from_en('10') ..
'</span></span><s style="display:none">^</s><sup>' ..
from_en(tostring(engscale.exponent)) .. '</sup>'
elseif enable_number_word then
local number_id
local name = engscale.varname and variable_name(info.clean, engscale) or engscale[1]
if parms.lk == 'on' or parms.lk == inout then
number_id = make_link(engscale.link, name)
else
number_id = name
end
-- WP:NUMERAL recommends " " in values like "12 million".
info.show = info.show .. (parms.opt_adjectival and '-' or ' ') .. number_id
end
end
if prefix then
info.show = prefix .. info.show
end
end
end
local function process_input(parms, in_current)
-- Processing required once per conversion.
-- Return block of text to represent input (value/unit).
if parms.opt_output_only or parms.opt_output_number_only or parms.opt_output_unit_only then
parms.joins = { '', '' }
return ''
end
local first_unit
local composite = in_current.composite -- nil or table of units
if composite then
first_unit = composite[1]
else
first_unit = in_current
end
local id1, want_name = make_id(parms, 1, first_unit)
local sep = first_unit.sep -- separator between value and unit, set by make_id
local preunit = parms.preunit1
if preunit then
sep = '' -- any separator is included in preunit
else
preunit = ''
end
if parms.opt_input_unit_only then
parms.joins = { '', '' }
if composite then
local parts = { id1 }
for i, unit in ipairs(composite) do
if i > 1 then
table.insert(parts, (make_id(parms, 1, unit)))
end
end
id1 = table.concat(parts, ' ')
end
if want_name and parms.opt_adjectival then
return preunit .. hyphenated(id1)
end
return preunit .. id1
end
if parms.opt_also_symbol and not composite and not parms.opt_flip then
local join1 = parms.joins[1]
if join1 == ' (' or join1 == ' [' then
parms.joins = { ' [' .. first_unit[parms.opt_sp_us and 'sym_us' or 'symbol'] .. ']' .. join1 , parms.joins[2] }
end
end
if in_current.builtin == 'mach' and first_unit.sep ~= '' then -- '' means omitsep with non-enwiki name
local prefix = id1 .. ' '
local range = parms.range
local valinfo = first_unit.valinfo
local result = prefix .. valinfo[1].show
if range then
-- For simplicity and because more not needed, handle one range item only.
local prefix2 = make_id(parms, 2, first_unit) .. ' '
result = range_text(range[1], want_name, parms, result, prefix2 .. valinfo[2].show, 'in', {spaced=true})
end
return preunit .. result
end
if composite then
-- Simplify: assume there is no range, and no decoration.
local mid = (not parms.opt_flip) and parms.mid or ''
local sep1 = ' '
local sep2 = ' '
if parms.opt_adjectival and want_name then
sep1 = '-'
sep2 = '-'
end
if omitsep and sep == '' then
-- Testing the id of the most significant unit should be sufficient.
sep1 = ''
sep2 = ''
end
local parts = { first_unit.valinfo[1].show .. sep1 .. id1 }
for i, unit in ipairs(composite) do
if i > 1 then
table.insert(parts, unit.valinfo[1].show .. sep1 .. (make_id(parms, 1, unit)))
end
end
return table.concat(parts, sep2) .. mid
end
local add_unit = (parms.abbr == 'mos') or
parms[parms.opt_flip and 'out_range_x' or 'in_range_x'] or
(not want_name and parms.abbr_range_x)
local range = parms.range
if range and not add_unit then
unlink(first_unit)
end
local id = range and make_id(parms, range.n + 1, first_unit) or id1
local extra, was_hyphenated = hyphenated_maybe(parms, want_name, sep, id, 'in')
if was_hyphenated then
add_unit = false
end
local result
local valinfo = first_unit.valinfo
if range then
for i = 0, range.n do
local enable_number_word
if i == range.n then
add_unit = false
enable_number_word = true
end
decorate_value(parms, first_unit, i+1, enable_number_word)
local show = valinfo[i+1].show
if add_unit then
show = show .. first_unit.sep .. (i == 0 and id1 or make_id(parms, i+1, first_unit))
end
if i == 0 then
result = show
else
result = range_text(range[i], want_name, parms, result, show, 'in')
end
end
else
decorate_value(parms, first_unit, 1, true)
result = valinfo[1].show
end
return result .. preunit .. extra
end
local function process_one_output(parms, out_current)
-- Processing required for each output unit.
-- Return block of text to represent output (value/unit).
local inout = out_current.inout -- normally 'out' but can be 'in' for order=out
local id1, want_name = make_id(parms, 1, out_current)
local sep = out_current.sep -- set by make_id
local preunit = parms.preunit2
if preunit then
sep = '' -- any separator is included in preunit
else
preunit = ''
end
if parms.opt_output_unit_only then
if want_name and parms.opt_adjectival then
return preunit .. hyphenated(id1)
end
return preunit .. id1
end
if out_current.builtin == 'mach' and out_current.sep ~= '' then -- '' means omitsep with non-enwiki name
local prefix = id1 .. ' '
local range = parms.range
local valinfo = out_current.valinfo
local result = prefix .. valinfo[1].show
if range then
-- For simplicity and because more not needed, handle one range item only.
result = range_text(range[1], want_name, parms, result, prefix .. valinfo[2].show, inout, {spaced=true})
end
return preunit .. result
end
local add_unit = (parms[parms.opt_flip and 'in_range_x' or 'out_range_x'] or
(not want_name and parms.abbr_range_x)) and
not parms.opt_output_number_only
local range = parms.range
if range and not add_unit then
unlink(out_current)
end
local id = range and make_id(parms, range.n + 1, out_current) or id1
local extra, was_hyphenated = hyphenated_maybe(parms, want_name, sep, id, inout)
if was_hyphenated then
add_unit = false
end
local result
local valinfo = out_current.valinfo
if range then
for i = 0, range.n do
local enable_number_word
if i == range.n then
add_unit = false
enable_number_word = true
end
decorate_value(parms, out_current, i+1, enable_number_word)
local show = valinfo[i+1].show
if add_unit then
show = show .. out_current.sep .. (i == 0 and id1 or make_id(parms, i+1, out_current))
end
if i == 0 then
result = show
else
result = range_text(range[i], want_name, parms, result, show, inout)
end
end
else
decorate_value(parms, out_current, 1, true)
result = valinfo[1].show
end
if parms.opt_output_number_only then
return result
end
return result .. preunit .. extra
end
local function make_output_single(parms, in_unit_table, out_unit_table)
-- Return true, item where item = wikitext of the conversion result
-- for a single output (which is not a combination or a multiple);
-- or return false, t where t is an error message table.
if parms.opt_order_out and in_unit_table.unitcode == out_unit_table.unitcode then
out_unit_table.valinfo = in_unit_table.valinfo
else
out_unit_table.valinfo = collection()
for _, v in ipairs(in_unit_table.valinfo) do
local success, info = cvtround(parms, v, in_unit_table, out_unit_table)
if not success then return false, info end
out_unit_table.valinfo:add(info)
end
end
return true, process_one_output(parms, out_unit_table)
end
local function make_output_multiple(parms, in_unit_table, out_unit_table)
-- Return true, item where item = wikitext of the conversion result
-- for an output which is a multiple (like 'ftin');
-- or return false, t where t is an error message table.
local inout = out_unit_table.inout -- normally 'out' but can be 'in' for order=out
local multiple = out_unit_table.multiple -- table of scaling factors (will not be nil)
local combos = out_unit_table.combination -- table of unit tables (will not be nil)
local abbr = parms.abbr
local abbr_org = parms.abbr_org
local disp = parms.disp
local want_name = (abbr_org == nil and (disp == 'or' or disp == 'slash')) or
not (abbr == 'on' or abbr == inout or abbr == 'mos')
local want_link = (parms.lk == 'on' or parms.lk == inout)
local mid = parms.opt_flip and parms.mid or ''
local sep1 = ' '
local sep2 = ' '
if parms.opt_adjectival and want_name then
sep1 = '-'
sep2 = '-'
end
local do_spell = parms.opt_spell_out
parms.opt_spell_out = nil -- so the call to cvtround does not spell the value
local function make_result(info, isfirst)
local fmt, outvalue, sign
local results = {}
for i = 1, #combos do
local tfrac, thisvalue, strforce
local out_current = combos[i]
out_current.inout = inout
local scale = multiple[i]
if i == 1 then -- least significant unit ('in' from 'ftin')
local decimals
out_current.frac = out_unit_table.frac
local success, outinfo = cvtround(parms, info, in_unit_table, out_current)
if not success then return false, outinfo end
if isfirst then
out_unit_table.valinfo = { outinfo } -- in case output value of first least significant unit is needed
end
sign = outinfo.sign
tfrac = outinfo.fraction_table
if outinfo.is_scientific then
strforce = outinfo.show
decimals = ''
elseif tfrac then
decimals = ''
else
local show = outinfo.show -- number as a string in local language
local p1, p2 = show:find(numdot, 1, true)
decimals = p1 and show:sub(p2 + 1) or '' -- text after numdot, if any
end
fmt = '%.' .. ulen(decimals) .. 'f' -- to reproduce precision
if decimals == '' then
if tfrac then
outvalue = floor(outinfo.raw_absvalue) -- integer part only; fraction added later
else
outvalue = floor(outinfo.raw_absvalue + 0.5) -- keep all integer digits of least significant unit
end
else
outvalue = outinfo.absvalue
end
end
if scale then
outvalue, thisvalue = divide(outvalue, scale)
else
thisvalue = outvalue
end
local id
if want_name then
if varname then
local clean
if strforce or tfrac then
clean = '.1' -- dummy value to force name for floating point
else
clean = format(fmt, thisvalue)
end
id = variable_name(clean, out_current)
else
local key = 'name2'
if parms.opt_adjectival then
key = 'name1'
elseif tfrac then
if thisvalue == 0 then
key = 'name1'
end
elseif parms.opt_singular then
if 0 < thisvalue and thisvalue < 1.0001 then
key = 'name1'
end
else
if thisvalue == 1 then
key = 'name1'
end
end
id = out_current[key]
end
else
id = out_current['symbol']
end
if i == 1 and omit_separator(id) then
-- Testing the id of the least significant unit should be sufficient.
sep1 = ''
sep2 = ''
end
if want_link then
local link = out_current.link
if link then
id = make_link(link, id, out_current)
end
end
local strval
local spell_inout = (i == #combos or outvalue == 0) and inout or '' -- trick so the last value processed (first displayed) has uppercase, if requested
if strforce and outvalue == 0 then
sign = '' -- any sign is in strforce
strval = strforce -- show small values in scientific notation; will only use least significant unit
elseif tfrac then
local wholestr = (thisvalue > 0) and tostring(thisvalue) or nil
strval = format_fraction(parms, spell_inout, false, wholestr, tfrac.numstr, tfrac.denstr, do_spell)
else
strval = (thisvalue == 0) and from_en('0') or with_separator(parms, format(fmt, thisvalue))
if do_spell then
strval = spell_number(parms, spell_inout, strval) or strval
end
end
table.insert(results, strval .. sep1 .. id)
if outvalue == 0 then
break
end
fmt = '%.0f' -- only least significant unit can have a non-integral value
end
local reversed, count = {}, #results
for i = 1, count do
reversed[i] = results[count + 1 - i]
end
return true, sign .. table.concat(reversed, sep2)
end
local valinfo = in_unit_table.valinfo
local success, result = make_result(valinfo[1], true)
if not success then return false, result end
local range = parms.range
if range then
for i = 1, range.n do
local success, result2 = make_result(valinfo[i+1])
if not success then return false, result2 end
result = range_text(range[i], want_name, parms, result, result2, inout, {spaced=true})
end
end
return true, result .. mid
end
local function process(parms, in_unit_table, out_unit_table)
-- Return true, s, outunit where s = final wikitext result,
-- or return false, t where t is an error message table.
linked_pages = {}
local success, bad_output
local bad_input_mcode = in_unit_table.bad_mcode -- nil if input unit is a valid convert unit
local out_unit = parms.out_unit
if out_unit == nil or out_unit == '' or type(out_unit) == 'function' then
-- out_unit can be set to a function by adjustparameters in Module:Convert/wikidata.
if bad_input_mcode or parms.opt_input_unit_only then
bad_output = ''
else
local getdef = type(out_unit) == 'function' and out_unit or get_default
success, out_unit = getdef(in_unit_table.valinfo[1].value, in_unit_table)
parms.out_unit = out_unit
if not success then
bad_output = out_unit
end
end
end
if not bad_output and not out_unit_table then
success, out_unit_table = lookup(parms, out_unit, 'any_combination')
if success then
local mismatch = check_mismatch(in_unit_table, out_unit_table)
if mismatch then
bad_output = mismatch
end
else
bad_output = out_unit_table
end
end
local lhs, rhs
local flipped = parms.opt_flip and not bad_input_mcode
if bad_output then
rhs = (bad_output == '') and '' or message(parms, bad_output)
elseif parms.opt_input_unit_only then
rhs = ''
else
local combos -- nil (for 'ft' or 'ftin'), or table of unit tables (for 'm ft')
if not out_unit_table.multiple then -- nil/false ('ft' or 'm ft'), or table of factors ('ftin')
combos = out_unit_table.combination
end
local frac = parms.frac -- nil or denominator of fraction for output values
if frac then
-- Apply fraction to the unit (if only one), or to non-SI units (if a combination),
-- except that if a precision is also specified, the fraction only applies to
-- the hand unit; that allows the following result:
-- {{convert|156|cm|in hand|1|frac=2}} → 156 centimetres (61.4 in; 15.1½ hands)
-- However, the following is handled elsewhere as a special case:
-- {{convert|156|cm|hand in|1|frac=2}} → 156 centimetres (15.1½ hands; 61½ in)
if combos then
local precision = parms.precision
for _, unit in ipairs(combos) do
if unit.builtin == 'hand' or (not precision and not unit.prefixes) then
unit.frac = frac
end
end
else
out_unit_table.frac = frac
end
end
local outputs = {}
local imax = combos and #combos or 1 -- 1 (single unit) or number of unit tables
if imax == 1 then
parms.opt_order_out = nil -- only useful with an output combination
end
if not flipped and not parms.opt_order_out then
-- Process left side first so any duplicate links (from lk=on) are suppressed
-- on right. Example: {{convert|28|e9pc|e9ly|abbr=off|lk=on}}
lhs = process_input(parms, in_unit_table)
end
for i = 1, imax do
local success, item
local out_current = combos and combos[i] or out_unit_table
out_current.inout = 'out'
if i == 1 then
if imax > 1 and out_current.builtin == 'hand' then
out_current.out_next = combos[2] -- built-in hand can influence next unit in a combination
end
if parms.opt_order_out then
out_current.inout = 'in'
end
end
if out_current.multiple then
success, item = make_output_multiple(parms, in_unit_table, out_current)
else
success, item = make_output_single(parms, in_unit_table, out_current)
end
if not success then return false, item end
outputs[i] = item
end
if parms.opt_order_out then
lhs = outputs[1]
table.remove(outputs, 1)
end
local sep = parms.table_joins and parms.table_joins[2] or parms.join_between
rhs = table.concat(outputs, sep)
end
if flipped or not lhs then
local input = process_input(parms, in_unit_table)
if flipped then
lhs = rhs
rhs = input
else
lhs = input
end
end
if parms.join_before then
lhs = parms.join_before .. lhs
end
local wikitext
if bad_input_mcode then
if bad_input_mcode == '' then
wikitext = lhs
else
wikitext = lhs .. message(parms, bad_input_mcode)
end
elseif parms.table_joins then
wikitext = parms.table_joins[1] .. lhs .. parms.table_joins[2] .. rhs
else
wikitext = lhs .. parms.joins[1] .. rhs .. parms.joins[2]
end
if parms.warnings and not bad_input_mcode then
wikitext = wikitext .. parms.warnings
end
return true, get_styles(parms) .. wikitext, out_unit_table
end
local function _main_convert(confArgs, parmsArgs, frame)
-- Do convert, and if needed, do it again with higher default precision.
local parms = { frame = frame or mw.getCurrentFrame() } -- will hold template arguments, after translation
set_config(confArgs)
local success, result = get_parms(parms, parmsArgs)
if success then
if type(result) ~= 'table' then
return tostring(result)
end
local in_unit_table = result
local out_unit_table
for _ = 1, 2 do -- use counter so cannot get stuck repeating convert
success, result, out_unit_table = process(parms, in_unit_table, out_unit_table)
if success and parms.do_convert_again then
parms.do_convert_again = false
else
break
end
end
end
-- If input=x gives a problem, the result should be just the user input
-- (if x is a property like P123 it has been replaced with '').
-- An unknown input unit would display the input and an error message
-- with success == true at this point.
-- Also, can have success == false with a message that outputs an empty string.
if parms.input_text then
if success and not parms.have_problem then
return result
end
local cat
if parms.tracking then
-- Add a tracking category using the given text as the category sort key.
-- There is currently only one type of tracking, but in principle multiple
-- items could be tracked, using different sort keys for convenience.
cat = wanted_category('tracking', parms.tracking)
end
return parms.input_text .. (cat or '')
end
if parms.error_text then
if success and not parms.have_problem then
return result
end
return parms.error_text
end
return success and result or message(parms, result)
end
local function main_convert(frame)
return _main_convert(frame.args, frame:getParent().args, frame)
end
local function _unit(unitcode, options)
-- Helper function for Module:Val to look up a unit.
-- Parameter unitcode must be a string to identify the wanted unit.
-- Parameter options must be nil or a table with optional fields:
-- value = number (for sort key; default value is 1)
-- scaled_top = nil for a normal unit, or a number for a unit which is
-- the denominator of a per unit (for sort key)
-- si = { 'symbol', 'link' }
-- (a table with two strings) to make an SI unit
-- that will be used for the look up
-- link = true if result should be [[linked]]
-- sort = 'on' or 'debug' if result should include a sort key in a
-- span element ('debug' makes the key visible)
-- name = true for the name of the unit instead of the symbol
-- us = true for the US spelling of the unit, if any
-- Return nil if unitcode is not a non-empty string.
-- Otherwise return a table with fields:
-- text = requested symbol or name of unit, optionally linked
-- scaled_value = input value adjusted by unit scale; used for sort key
-- sortspan = span element with sort key like that provided by {{ntsh}},
-- calculated from the result of converting value
-- to a base unit with scale 1.
-- unknown = true if the unitcode was not known
unitcode = strip(unitcode)
if unitcode == nil or unitcode == '' then
return nil
end
set_config({})
linked_pages = {}
options = options or {}
local parms = {
abbr = options.name and 'off' or 'on',
lk = options.link and 'on' or nil,
opt_sp_us = options.us and true or nil,
opt_ignore_error = true,
opt_no_extra = true, -- do not add pages using this function to 'what links here' for Module:Convert/extra
opt_sortable_on = options.sort == 'on' or options.sort == 'debug',
opt_sortable_debug = options.sort == 'debug',
}
if options.si then
-- Make a dummy table of units (just one unit) for lookup to use.
-- This makes lookup recognize any SI prefix in the unitcode.
local symbol = options.si[1] or '?'
parms.unittable = { [symbol] = {
_name1 = symbol,
_name2 = symbol,
_symbol = symbol,
utype = symbol,
scale = symbol == 'g' and 0.001 or 1,
prefixes = 1,
default = symbol,
link = options.si[2],
}}
end
local success, unit_table = lookup(parms, unitcode, 'no_combination')
if not success then
unit_table = setmetatable({
symbol = unitcode, name2 = unitcode, utype = unitcode,
scale = 1, default = '', defkey = '', linkey = '' }, unit_mt)
end
local value = tonumber(options.value) or 1
local clean = tostring(abs(value))
local info = {
value = value,
altvalue = value,
singular = (clean == '1'),
clean = clean,
show = clean,
}
unit_table.inout = 'in'
unit_table.valinfo = { info }
local sortspan, scaled_value
if options.sort then
sortspan, scaled_value = make_table_or_sort(parms, value, info, unit_table, options.scaled_top)
end
return {
text = make_id(parms, 1, unit_table),
sortspan = sortspan,
scaled_value = scaled_value,
unknown = not success and true or nil,
}
end
return { convert = main_convert, _convert = _main_convert, _unit = _unit }
r4djqnuzbxaut805hk89ncew56hajfu
ထႅမ်းပလဵတ်ႉ:Short description
10
18007
125516
125235
2026-05-04T23:22:57Z
Saimawnkham
5
125516
wikitext
text/x-wiki
{{#ifeq:{{lc:{{{1|}}}}}|none|{{SHORTDESC:|{{{2|}}}}}<nowiki/><!--Prevents whitespace issues when used with adjacent newlines-->|<div class="shortdescription nomobile noexcerpt noprint searchaux" style="display:none">{{{1|}}}{{SHORTDESC:{{{1|}}}|{{{2|}}}}}</div>}}<includeonly>{{#ifeq:{{{pagetype}}}|Disambiguation pages||{{#ifeq:{{pagetype |defaultns = all |user=exclude}}|exclude||{{#ifeq:{{#switch: {{NAMESPACENUMBER}} | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 100 | 101 | 118 | 119 | 828 | 829 | = exclude|#default=}}|exclude||[[Category:{{{pagetype|{{pagetype |defaultns = extended |plural=n}}}}} ဢၼ်ပႃး ၶေႃးသပ်းလႅင်းပွတ်း]]}}}}}}</includeonly><!-- Start tracking
-->{{#invoke:Check for unknown parameters|check|unknown={{Main other|[[Category:Pages using short description with unknown parameters|_VALUE_{{PAGENAME}}]]}}|preview=Page using [[Template:Short description]] with unknown parameter "_VALUE_"|ignoreblank=y| 1 | 2 | pagetype | bot |plural }}<!--
-->{{#ifexpr: {{#invoke:String|len|{{{1|}}}}}>100 | [[Category:{{{pagetype|{{pagetype |defaultns = extended |plural=y}}}}} with long short description]]}}<!--
--><includeonly>{{#if:{{{1|}}}||[[Category:Pages with empty short description]]}}</includeonly><!--
-->{{Short description/lowercasecheck|{{{1|}}}}}<!--
-->{{Main other |{{SDcat |sd={{{1|}}} }} }}<noinclude>
{{Documentation}}
</noinclude>
jbgbm49h1xvbb5o8si56k2u15ry7cc7
မီႇတီႇယႃႇဝီႇၶီႇ:Gadget-Shortdesc-helper.css
8
27702
125511
55526
2026-05-04T23:17:47Z
Saimawnkham
5
ဢၢပ်ႉတဵတ်ႉၶေႃႈမုၼ်း
125511
css
text/css
/* _____________________________________________________________________________
* | |
* | === WARNING: GLOBAL GADGET FILE === |
* | Changes to this page affect many users. |
* | Please discuss changes on the talk page or on [[WT:Gadget]] before editing. |
* |_____________________________________________________________________________|
*
*/
/* v3.5.3 */
/* Stylings for missing description and links to wikidata */
.sdh-missing-description {
color: DarkRed; /* Using DarkRed rather than previous Red to meet WCAG AA */
}
.sdh-wikidata-description a {
color: MediumVioletRed; /* Using MediumVioletRed rather than previous HotPink to meet WCAG AA */
}
.sdh-ve-hidden {
display: none;
}
/**
* Interestingly, Vector's stylesheet makes <a> elements without href have a pointer automatically
* This is for other skins.
*/
.sdh-clicky a {
cursor: pointer;
}
.sdh-clicky-disabled a {
pointer-events: none;
}
/* Brackets and seperators for clickies */
.sdh-clicky + .sdh-clicky::before {
content: ' · ';
}
.sdh-clickies::before {
content: ' (';
}
.sdh-clickies::after {
content: ')';
}
/* Used to hide display of description when switching to editing box */
.sdh-showdescrip-hidden {
display: none;
}
/* Styling for editing bar */
#sdh-editbox {
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
}
/* Make things flex to allow description input box size to be set exactly, and everything else to look good. */
#sdh .oo-ui-fieldLayout-field {
display: flex;
align-items: center;
}
#sdh-descriptionbox {
margin-right: 0;
}
/* Styling for processing animation */
@keyframes wave {
0% { transform: translateY( 0 ); }
20% { transform: translateY( -0.25em ); }
40% { transform: translateY( 0 ); }
100% { transform: translateY( 0 ); }
}
.sdh-processing {
display: inline-block;
margin-left: 0.5em;
}
.sdh-processing-dot {
display: inline-block;
animation: wave 1s ease-out infinite;
}
.sdh-processing-dot-1 {
animation-delay: 200ms;
}
.sdh-processing-dot-2 {
animation-delay: 400ms;
}
.sdh-processing-dot-3 {
animation-delay: 600ms;
}
/* Styling for character counter based on length */
.sdh-too-long {
color: #d43900;
}
.sdh-very-long {
color: #e00000;
font-weight: bold;
}
0ilizwthafp6m4ckaxozzgmb812vgw6
မီႇတီႇယႃႇဝီႇၶီႇ:Gadget-Shortdesc-helper.js
8
27778
125510
55607
2026-05-04T23:17:02Z
Saimawnkham
5
125510
javascript
text/javascript
// <nowiki>
/* _____________________________________________________________________________
* | |
* | === WARNING: GLOBAL GADGET FILE === |
* | Changes to this page affect many users. |
* | Please discuss changes on the talk page or on [[WT:Gadget]] before editing. |
* |_____________________________________________________________________________|
*
*/
/**
* Shortdesc helper: v3.5.3
* Documentation at en.wikipedia.org/wiki/Wikipedia:Shortdesc_helper
* The documentation includes instructions for using this gadget on other wikis.
* Shows short descriptions, and allows importing wikidata descriptions, adding descriptions,
* and easier editing of them by giving buttons and inputbox for doing so.
* Forked from [[MediaWiki:Gadget-Page descriptions.js]] written by the TheDJ.
*/
'use strict';
window.sdh = window.sdh || {};
/**
* Set messages using mw.message.
* window.sdh.messages can be used to override these messages (for e.g translations).
*/
window.sdh.initMessages = function () {
/* These messages are used on all wikis and so need translation. */
var messages = {
/** Uncomment the following to change messages used in settings dialog */
/*
"libSettings-settings-title": "Settings",
"libSettings-save-label": "Save settings",
"libSettings-cancel-label": "Cancel",
"libSettings-showDefaults-label": "Show defaults",
"libSettings-showCurrentSettings-label": "Show current settings",
"libSettings-save-success-message": "Settings for $1 successfully saved.",
"libSettings-save-fail-message": "Could not save settings for $1.",
*/
/* Settings messages */
'sdh-settingsDialog-title': 'Settings for Shortdesc helper',
'sdh-header-general': 'General',
'sdh-header-appearance': 'Appearance',
'sdh-AddToRedirect-label': 'Allow additions of short descriptions to redirects',
'sdh-AddToRedirect-help': 'When checked, redirects will have an "add" button to add a short description. (default off)',
'sdh-InputWidth-label': 'Width of editing input in characters (default 40)',
'sdh-FontSize-label': 'Font size, as a percentage (default 100%)',
/* Initial view messages */
'sdh-missing-description': 'Missing <a href="/wiki/Wikipedia:Short description">$1 description</a>',
'sdh-article-label': 'article',
'sdh-redirect-label': 'redirect',
/* Initial view buttons */
'sdh-add-label': 'Add',
'sdh-add-title': 'Add short description',
'sdh-edit-label': 'Edit',
'sdh-edit-title': 'Edit short description',
/* Editing messages */
'sdh-description-placeholder': 'Short description',
'sdh-summary-placeholder': 'Custom edit summary',
'sdh-save-label': 'Save',
'sdh-save-title': 'Save description',
'sdh-cancel-label': 'Cancel',
'sdh-cancel-title': 'Cancel editing',
'sdh-summary-label': 'Summary',
'sdh-summary-title': 'Show edit summary box',
'sdh-settings-title': 'Settings',
/* Wikidata summary messages */
'sdh-wd-summary': '([[w:en:Wikipedia:Shortdesc helper|Shortdesc helper]])',
'sdh-wd-edit-failed': 'Saving the edit to Wikidata failed.',
'sdh-wd-edit-failed-prefix': '\n\nThe info given by Wikidata is that:\n\n'
};
/**
* These messages don't need translation as they are only used on enwiki
* because enwiki has the {{SHORTDESC:}} magic word.
*/
var enwikiMessages = {
/* Settings messages */
'sdh-MarkAsMinor-label': 'Mark edits as minor',
'sdh-header-Wikidata': 'Wikidata',
'sdh-SaveWikidata-label': 'Save changes to Wikidata',
'sdh-SaveWikidata-help': 'Whether to update the Wikidata description when using the script.',
'sdh-SaveWikidata-add-label': 'Only when no Wikidata description exists (default)',
'sdh-SaveWikidata-all-label': 'On all edits',
'sdh-SaveWikidata-never-label': 'Never',
'sdh-ShowWikidataOption-label': 'Show the Wikidata description',
'sdh-ShowWikidataOption-always-label': 'Always',
'sdh-ShowWikidataOption-nolocal-label': 'Only when no local description exists (default)',
'sdh-ShowWikidataOption-never-label': 'Never',
'sdh-ExportButton-label': 'Add a button, "export", to update the Wikidata description to match the local description.',
/* Initial view messages */
'sdh-wikidata-link-label': 'Wikidata',
'sdh-no-description': 'This page intentionally has no description.',
/* Initial view buttons */
'sdh-infoClicky-label': '?',
'sdh-infoClicky-title': 'Click for info',
'sdh-override-label': 'Override',
'sdh-override-title': 'Override current short description',
'sdh-import-label': 'Import',
'sdh-import-title': 'Import description from Wikidata',
'sdh-editimport-label': 'Edit and import',
'sdh-editimport-title': 'Edit and import description from Wikidata',
'sdh-export-label': 'Export',
'sdh-export-title': 'Export local short description to Wikidata',
/* Popup text */
'sdh-no-description-popup': 'This description is intentionally empty. See <a href = "/wiki/Wikipedia:Short_description#SDNONE">our policy page on (none) short descriptions</a> for more info.',
'sdh-override-popup': 'While this description can be overridden with another local short description, it cannot be directly edited. This is most likely because it is automatically generated by the article\'s infobox or some other template. See <a href = "/wiki/Wikipedia:WikiProject_Short_descriptions#Auto-generated_and_bot-generated_descriptions"> our policy on auto-generated and bot-generated descriptions</a> for more info.',
'sdh-disambig-popup': 'This short description should not be edited because it is automatically generated by the disambiguation template and does not need to be changed.',
'sdh-useless-popup': 'Importing of this description has been disabled as it is too generic to be useful.',
/* Summary messages */
'sdh-summary-changing': 'Changing [[Wikipedia:Short description|short description]] from $1 to $2',
'sdh-summary-adding-custom': 'Adding [[Wikipedia:Short description|short description]]: $2, overriding automatically generated description',
'sdh-summary-importing-wikidata': 'Importing Wikidata [[Wikipedia:Short description|short description]]: $2',
'sdh-summary-adding-local': 'Adding local [[Wikipedia:Short description|short description]]: $2, overriding Wikidata description $1',
'sdh-summary-adding': 'Adding [[Wikipedia:Short description|short description]]: $2',
/* Summary none messages */
'sdh-summary-changing-none': 'Changing [[Wikipedia:Short description|short description]] from $1 to one that is [[WP:SDNONE|intentionally blank]]',
'sdh-summary-adding-custom-none': 'Adding [[WP:SDNONE|intentionally blank]] description, instead of automatically generated description',
'sdh-summary-adding-local-none': 'Adding [[WP:SDNONE|intentionally blank]] description, overriding Wikidata description $1',
'sdh-summary-adding-none': 'Adding [[WP:SDNONE|intentionally blank]] description',
/* Failure message */
'sdh-edit-failed': 'Saving the addition of or edit to the short description failed.',
'sdh-edit-failed-no-template': 'Edit failed, as no short description template was found in the page wikitext. This is probably due to an edit conflict.'
};
/**
* Setting window.sdh.messages last means it overrides previous messages
* Thus allowing translations to override previous messages.
*/
mw.messages.set( messages );
mw.messages.set( enwikiMessages );
mw.messages.set( window.sdh.messages );
};
window.sdh.main = function () {
/**
* What section the short description is in, to be determined later
* by searching the DOM. Used so that if the short description is in the lead
* only the wikitext of section 0 needs to be downloaded.
*
* @type {number}
*/
var section;
// Consts
/**
* Selector to find the short description in the DOM.
*
* @type {string}
*/
var SDELEMENT = '.shortdescription';
/**
* Selector to find disambiguation template.
*
* @type {string}
*/
var DISAMBIGELEMENT = '#disambigbox';
/**
* Search pattern for finding short description in wikitext.
* Group 1 in the regex is the short description.
*
* @type {RegExp}
*/
var PATTERN = /\{\{\s*[Ss]hort description\s*\|(.*?)\}\}/;
/**
* List of Wikidata descriptions that are not useful enough to be directly imported.
*
* @type {Array}
*/
var USELESS_DESCRIPTIONS = [
'Wikimedia project page'
];
/**
* Pattern for date spans, to replace hyphen with en dash.
*
* @type {RegExp}
*/
var DATEPATTERN = /(\d\d+)-(\d\d+)/;
/**
* Replace for date spans.
*
* @type {string}
*/
var DATEREPLACEMENT = '$1–$2';
// Config variables
var title = mw.config.get( 'wgPageName' );
var namespace = mw.config.get( 'wgNamespaceNumber' );
var wgQid = mw.config.get( 'wgWikibaseItemId' );
var language = mw.config.get( 'wgContentLanguage' );
var canEdit = mw.config.get( 'wgIsProbablyEditable' );
var isRedirect = mw.config.get( 'wgIsRedirect' );
var DBName = mw.config.get( 'wgDBname' );
/**
* onlyEditWikidata is a site-wide flag.
* If it is true, then the only descriptions for the wiki are assumed to be on Wikidata.
* If it is false, then that means descriptions can also be added through {{SHORTDESC:}}
* (currently, this is only the case on enwiki).
* This flag modifies the behaviour of various methods to display the appropriate buttons and
* settings, and make the description saved to the right place.
*
* @type {boolean}
*/
var onlyEditWikidata = ( DBName !== 'enwiki' );
/**
* Check if the user can edit the page,
* and disallow editing of templates and categories to prevent accidental addition.
*
* @type {boolean}
*/
var allowEditing = (
(
canEdit &&
[ 10, 14, 710, 828, 2300, 2302 ].indexOf( namespace ) === -1
)
);
// Define user agent when accessing the API
var APIoptions = {
ajax: {
headers: {
'Api-User-Agent': 'Short description editer/viewer gadget (w:en:Wikipedia:Shortdesc helper)'
}
}
};
var API = new mw.Api( APIoptions );
var wikidataAPI = new mw.ForeignApi( 'https://www.wikidata.org/w/api.php', APIoptions );
/**
* Get the wikitext of the page.
*
* @return {Promise}
*/
var getText = function () {
return API.get( {
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
rvsection: section,
rvslots: 'main',
formatversion: 2
} );
};
/**
* Download wikitext. Whether to download the whole wikitext,
* or only the lead section wikitext is determined.
*
* @type {Promise}
*/
var callPromiseText = ( function () {
var elements;
if ( onlyEditWikidata ) {
return;
}
/**
* Find whether the short description is in the first section, to determine
* if we need to download the wikitext of the entire page.
* Do this by searching elements above the first heading for ".shortdescription"
*/
// eslint-disable-next-line no-jquery/no-global-selector, no-jquery/variable-pattern
elements = $( '.mw-parser-output > h2' ).first().prevAll();
/**
* Need to check sibling elements with filter and their children
* with find to find short description. If length > 0 then found
* short description before the first heading, so get wikitext of section 0.
*/
if ( elements.filter( SDELEMENT ).add( elements.find( SDELEMENT ) ).length > 0 ) {
section = 0;
}
// Get the wikitext
return getText();
}() );
/**
* Get the local short description
*
* @type {Promise}
*/
var callPromiseDescription = API.get( {
action: 'query',
titles: title,
prop: 'description',
formatversion: 2
} );
/**
* Load settings using libSettings if it exists
* Otherwise gracefully fallback to defaults.
*/
var usinglibSettings = !!mw.libs.libSettings;
var ls, optionsConfig, settings, options;
if ( usinglibSettings ) {
ls = mw.libs.libSettings;
optionsConfig = new ls.OptionsConfig( [
new ls.Page( {
title: mw.msg( 'sdh-header-general' ),
preferences: [
new ls.CheckboxOption( {
name: 'MarkAsMinor',
label: mw.msg( 'sdh-MarkAsMinor-label' ),
defaultValue: false,
hide: onlyEditWikidata
} ),
new ls.CheckboxOption( {
name: 'AddToRedirect',
label: mw.msg( 'sdh-AddToRedirect-label' ),
help: mw.msg( 'sdh-AddToRedirect-help' ),
defaultValue: false
} ),
new ls.CheckboxOption( {
name: 'ExportButton',
label: mw.msg( 'sdh-ExportButton-label' ),
defaultValue: false,
hide: onlyEditWikidata
} ),
new ls.DropdownOption( {
name: 'ShowWikidata',
label: mw.msg( 'sdh-ShowWikidataOption-label' ),
defaultValue: 'nolocal',
values: [
{ data: 'always', label: mw.msg( 'sdh-ShowWikidataOption-always-label' ) },
{ data: 'nolocal', label: mw.msg( 'sdh-ShowWikidataOption-nolocal-label' ) },
{ data: 'never', label: mw.msg( 'sdh-ShowWikidataOption-never-label' ) }
],
hide: onlyEditWikidata
} ),
// Option for all disabled due to issues with people not using it properly
new ls.DropdownOption( {
name: 'SaveWikidata',
label: mw.msg( 'sdh-SaveWikidata-label' ),
help: mw.msg( 'sdh-SaveWikidata-help' ),
defaultValue: 'add',
values: [
{ data: 'add', label: mw.msg( 'sdh-SaveWikidata-add-label' ) },
// { data: 'all', label: mw.msg( 'sdh-SaveWikidata-all-label' ) },
{ data: 'never', label: mw.msg( 'sdh-SaveWikidata-never-label' ) }
],
hide: onlyEditWikidata
} )
]
} ),
new ls.Page( {
title: mw.msg( 'sdh-header-appearance' ),
preferences: [
new ls.NumberOption( {
name: 'InputWidth',
label: mw.msg( 'sdh-InputWidth-label' ),
defaultValue: 40,
UIconfig: {
min: 10,
max: 999
}
} ),
new ls.NumberOption( {
name: 'FontSize',
label: mw.msg( 'sdh-FontSize-label' ),
defaultValue: 100,
UIconfig: {
min: 10,
max: 500
}
} )
]
} )
] );
settings = new mw.libs.libSettings.Settings( {
title: mw.msg( 'sdh-settingsDialog-title' ),
scriptName: 'Shortdesc-helper',
helpInline: true,
size: 'large',
height: 350,
optionsConfig: optionsConfig
} );
options = settings.get();
} else {
// Use defaults
options = {
MarkAsMinor: false,
AddToRedirect: false,
InputWidth: 40,
FontSize: 100,
ExportButton: false,
SaveWikidata: 'add',
ShowWikidata: 'nolocal'
};
}
/**
* Get the Wikidata short description
*
* @type {Promise}
*/
var callPromiseWDDescription = ( options.ShowWikidata === 'never' || wgQid === null ) ? null : wikidataAPI.get( {
action: 'wbgetentities',
ids: wgQid,
props: 'descriptions',
formatversion: 2,
languages: language
} );
// Dynamic CSS based on options
mw.util.addCSS(
'#sdh { font-size:' + options.FontSize + '%}' +
'#sdh-descriptionbox { width:' + ( options.InputWidth + 3 ) + 'ch };' // Do +3 since width includes char counter
);
/* Main code to be run once both the local and Wikidata short description is gotten */
var onResponses = function ( response, responseWD ) {
/**
* These two variables are UI elements that need to be closed and reopened,
* and so need to be accessed outside the scope of the functions
* that define them.
*/
/**
* Used in InfoClickyPopup
*
* @type {OO.ui.PopupWidget}
*/
var infoPopup;
/**
* Used in textInput
*
* @type {OO.ui.ActionFieldLayout}
*/
var actionField;
/**
* Used in textInput
*
* @type {OO.ui.TextInputWidget}
*/
var descriptionInput;
/**
* These three variables are defined by the button being clicked
*/
/**
* The message to be used for the summary
*
* @type {string}
*/
var summaryMsg;
/**
* Is the action a change to an existing local description
* or an addition, importation etc.
*
* @type {boolean}
*/
var change;
/**
* True when there is no description anywhere, and so
* description should be added to Wikidata when options.SaveWikidata is 'add'.
*
* @type {boolean}
*/
var addWikidata;
/**
* Whether there should be text initially in the input box.
*
* @type {boolean}
*/
var emptyPreload = false;
/**
* Various HTML elements
*/
var $sdh = $( '<div>' ).prop( 'id', 'sdh' );
var $description = $( '<div>' ).addClass( 'sdh-showdescrip' );
var $clickies = $( '<span>' ).addClass( 'sdh-clickies' );
var pages = response[ 0 ].query.pages[ 0 ];
/**
* Is the description from Wikidata (non local) or the {{SHORTDESC:}} magic word?
*
* @type {boolean}
*/
var isLocal = ( pages.descriptionsource === 'local' );
/**
* The Wikidata descriptions.
*/
var wikidataDescriptions = responseWD ? responseWD[ 0 ].entities[ wgQid ].descriptions : {};
/**
* The Wikidata description, if it exists.
*/
var wikidataDescription = Object.keys( wikidataDescriptions ).length !== 0 ? wikidataDescriptions[ language ].value : '';
/**
* The page short description.
*
* @type {string}
*/
var pageDescription = ( isLocal ? pages.description : wikidataDescription ).trim();
/**
* Whether this is a disambiguation/set index page or not, determined by searching the DOM.
* If it is, then the option to override the short description will be disabled.
*
* @type {boolean}
*/
var disambigPage = $( DISAMBIGELEMENT ).length > 0;
/**
* Whether a Wikidata description is too generic to be useful.
*
* @type {boolean}
*/
var uselessDescription = !isLocal && USELESS_DESCRIPTIONS.indexOf( pageDescription ) !== -1;
/**
* Whether to append the Wikidata description
*
* @type {boolean}
*/
var appendWDDescription = options.ShowWikidata === 'always' && isLocal && wikidataDescription;
/**
* Creates "clickies", simple link buttons.
* Things are made nice per https://stackoverflow.com/a/10510353
* Links are wrapped in spans to allow separators to be added using css
* without becoming part of the link.
*
* @param {string} msgName
* @param {Function} func
* @return {Object}
*/
var Clicky = function ( msgName, func ) {
return $( '<span>' )
.addClass( 'sdh-clicky' )
.append( $( '<a>' )
.attr( {
title: mw.msg( msgName + '-title' ),
role: 'button',
tabindex: '0'
} )
.text( mw.msg( msgName + '-label' ) )
.on( 'click', func )
.on( 'keydown', function ( e ) {
if ( [ 13, 32 ].indexOf( event.which ) !== -1 ) { // Space and enter
e.preventDefault();
return func();
}
} )
);
};
/**
* Create a Clicky that opens a OOui PopupWidget.
*
* @param {string} text
* @return {Clicky}
*/
var InfoClickyPopup = function ( text ) {
var self = this;
self.text = text;
self.infoClicky = new Clicky(
'sdh-infoClicky',
function () {
if ( !infoPopup ) {
mw.loader.using( [ 'oojs-ui-core', 'oojs-ui-widgets' ] ).then( function () {
infoPopup = new OO.ui.PopupWidget( {
$content: $( '<span>' ).append( self.text ),
$autoCloseIgnore: self.infoClicky,
padded: true,
autoClose: true,
width: 300,
position: 'after'
} );
$clickies.append( infoPopup.$element );
infoPopup.toggle( true );
} );
} else {
infoPopup.toggle( false );
}
}
);
return self.infoClicky;
};
/**
* Creates OOui buttons, which are used for save and cancel.
*
* @param {string} msgName
* @param {Function} func
* @param {Array<string>} flags
* @param {string} icon
* @return {OO.ui.ButtonWidget}
*/
var OOuiClicky = function ( msgName, func, flags, icon ) {
return new OO.ui.ButtonWidget( {
label: mw.msg( msgName + '-label' ),
icon: icon,
title: mw.msg( msgName + '-title' ),
flags: flags,
classes: [ 'sdh-ooui-clicky' ]
} ).on( 'click', func );
};
/**
* Function to check if the short description is in the wikitext.
* If it is, return the wikitext and short description as defined in the text
*
* @param {Object} wikitextResult
* @return {Array}
*/
var shortdescInText = function ( wikitextResult ) {
var wikitext = wikitextResult.query.pages[ 0 ].revisions[ 0 ].slots.main.content;
var match = wikitext && wikitext.match( PATTERN );
if ( match ) {
return [ wikitext, match[ 1 ] ];
} else {
return [ wikitext, false ];
}
};
/**
* Notify the user that the edit failed and log any debug info.
*
* @param {string} msgName
* @param {*} debug
* @param {string} extraMsg
*/
var editFailed = function ( msgName, debug, extraMsg ) {
var message = mw.msg( msgName ) + extraMsg;
mw.notify(
message,
{
autoHide: false
}
);
if ( debug ) {
mw.log.warn( debug );
}
};
/**
* Set the Wikidata description using the API.
*
* @param {string} newDescription
* @param {string} summary Custom edit summary.
* @return {Promise}
*/
var setWikidataDescription = function ( newDescription, summary ) {
return mw.loader.using( 'mediawiki.ForeignApi' ).then( function () {
return wikidataAPI.postWithToken( 'csrf', {
action: 'wbsetdescription',
id: wgQid,
language: language,
summary: ( summary || '' ) + mw.message( 'sdh-wd-summary', language ).plain(),
value: newDescription
} );
} );
};
/**
* This function edits Wikidata descriptions and is used on wikis that aren't enwiki.
* Beyond what setWikidataDescription does, it reloads the page on success
* and gives an informative error notification.
*
* @param {string} newDescription
* @param {string} summary Custom edit summary.
*/
var editWikidataDescription = function ( newDescription, summary ) {
setWikidataDescription( newDescription, summary ).then(
function () {
window.location.reload();
},
function () {
editFailed(
'sdh-wd-edit-failed',
arguments,
arguments[ 1 ].error.info ? (
mw.msg( 'sdh-wd-edit-failed-prefix' ) +
arguments[ 1 ].error.info
) : ''
);
}
);
};
/**
* This function adds or replaces short descriptions.
*
* @param {string} newDescription
* @param {string} summary Custom edit summary.
*/
var editDescription = function ( newDescription, summary ) {
var replacement, prependText, appendText, text, prependDescription;
/**
* Helper function to add quotes around text,
* used when generating the summary.
*
* @param {string} textToQuote
* @return {string}
*/
var quotify = function ( textToQuote ) {
return '"' + textToQuote + '"';
};
/**
* Appends, prepends, or replaces the wikitext.
* depending on which of text, prependText, and appendText exists.
*/
var makeEdit = function () {
API.postWithEditToken( {
action: 'edit',
section: section,
text: text,
title: title,
prependtext: prependText,
appendtext: appendText,
summary: summary,
nocreate: true,
minor: options.MarkAsMinor,
tags: 'shortdesc helper'
} ).then(
function () {
// Reload the page
window.location.reload();
},
function () {
editFailed( 'sdh-edit-failed', arguments );
}
);
};
/**
* Replaces the current local short description with the new one.
* If the short description doesn't exist in the text, return false.
*
* @param {string} wikitextResult Result of getText()
* @return {boolean} Whether there was a description in the wikitext
* and so whether makeEdit could be called.
*/
var replaceAndEdit = function ( wikitextResult ) {
var output = shortdescInText( wikitextResult );
var oldtext = output[ 0 ];
var descriptionFromText = output[ 1 ];
if ( descriptionFromText ) {
text = oldtext.replace( PATTERN, replacement );
makeEdit();
return true;
} else {
return false;
}
};
// Fix manual entry of "none"
if ( newDescription.toLowerCase() === 'none' ) {
newDescription = '';
}
// Replace hyphens in dates with en dashes
newDescription = newDescription.replace( DATEPATTERN, DATEREPLACEMENT );
// Make edits to Wikidata as appropiate
if (
wgQid &&
( options.SaveWikidata === 'add' && addWikidata ) && // options.SaveWikidata === 'all'
newDescription !== ''
) {
setWikidataDescription( newDescription );
}
// Capitalize first letter by default unless editing local description
if ( !isLocal ) {
newDescription = (
newDescription.charAt( 0 ).toUpperCase() +
newDescription.slice( 1 )
);
}
if ( newDescription === '' ) {
newDescription = 'none';
}
// Use 1= if the description has an '='
if ( newDescription.indexOf( '=' ) !== -1 ) {
prependDescription = '1=';
} else {
prependDescription = '';
}
replacement = '{{Short description|' + prependDescription + newDescription + '}}';
// Link guideline if changing to none description
summary = summary || mw.message(
summaryMsg + ( newDescription === 'none' ? '-none' : '' ),
quotify( pageDescription ),
quotify( newDescription )
).plain();
/**
* change = true means there was a previous short description in the wikitext
* that needs to be replaced.
*/
if ( change ) {
/**
* Get the wikitext again right before making the edit
* to avoid issues with edit conflicts, and make the edit.
*/
getText().then( function ( result ) {
if ( !replaceAndEdit( result ) ) {
editFailed( 'sdh-edit-failed-no-template' );
}
} );
} else {
if ( isRedirect ) {
appendText = '\n' + replacement;
} else {
prependText = replacement + '\n';
}
makeEdit();
}
};
/**
* Creates input box with save and cancel buttons.
* If input box was created before, show it again.
* Otherwise, create the input box using OOui.
*/
var textInput = function () {
if ( actionField ) {
$description.addClass( 'sdh-showdescrip-hidden' );
actionField.toggle( true );
descriptionInput.focus();
} else {
mw.loader.using( [ 'oojs-ui-core', 'oojs-ui-widgets', 'oojs-ui.styles.icons-interactions' ] ).then( function () {
var length, saveInput, buttons;
// Define the input box, summary box, and buttons.
descriptionInput = new OO.ui.TextInputWidget( {
autocomplete: false,
autofocus: true,
id: [ 'sdh-descriptionbox' ],
label: '0',
value: emptyPreload ? '' : pageDescription,
placeholder: mw.msg( 'sdh-description-placeholder' )
} );
var summaryInput = new OO.ui.TextInputWidget( {
autocomplete: false,
autofocus: true,
id: [ 'sdh-summarybox' ],
placeholder: mw.msg( 'sdh-summary-placeholder' )
} );
var saveButton = new OOuiClicky(
'sdh-save',
function () {
saveInput();
},
[ 'primary', 'progressive' ]
);
var cancelButton = new OOuiClicky(
'sdh-cancel',
function () {
actionField.toggle( false );
$description.removeClass( 'sdh-showdescrip-hidden' );
},
[ 'safe', 'destructive' ]
);
var summaryButton = new OOuiClicky(
'sdh-summary',
function () {
summaryInput.toggle();
},
[ 'safe' ]
);
var settingsButton = new OO.ui.ButtonWidget( {
icon: 'settings',
framed: false,
title: mw.msg( 'sdh-settings-title' ),
flags: [ 'safe' ],
classes: [ 'sdh-ooui-clicky' ]
} ).on( 'click', function () {
settings.display();
} );
/**
* On change, update character count label.
* If local description has not been modified, prevent
*/
var updateOnChange = function () {
var description = descriptionInput.getValue().trim();
length = descriptionInput.getInputLength();
var classes = [ '' ];
if ( length > 40 ) {
if ( length > 60 ) {
classes = [ 'sdh-very-long' ];
} else {
classes = [ 'sdh-too-long' ];
}
}
descriptionInput.setLabel(
$( '<span>' )
.addClass( classes )
.text( String( length ) )
);
if ( isLocal && description === pageDescription ) {
saveButton.setDisabled( true );
} else {
saveButton.setDisabled( false );
}
};
var items = [ saveButton, summaryButton, cancelButton ];
if ( usinglibSettings ) {
items.push( settingsButton );
}
buttons = new OO.ui.ButtonGroupWidget( {
items: items
} );
/**
* This is bound to the save button.
* Disables all the elements and calls the relevant function
* responsible for saving the the entered short description.
*/
saveInput = function () {
var description = descriptionInput.getValue().trim();
var summary = summaryInput.getValue().trim();
descriptionInput
.setDisabled( true )
.pushPending( true );
summaryInput
.setDisabled( true )
.pushPending( true );
items.forEach( function ( item ) {
item.setDisabled( true );
} );
if ( onlyEditWikidata ) {
editWikidataDescription( description, summary );
} else {
editDescription( description, summary );
}
};
actionField = new OO.ui.ActionFieldLayout(
descriptionInput,
buttons,
{
align: 'top',
id: 'sdh-editbox'
}
);
summaryInput.toggle( false );
// Initial character count
updateOnChange();
descriptionInput.on( 'change', updateOnChange );
descriptionInput.on( 'enter', saveInput );
summaryInput.on( 'enter', saveInput );
summaryInput.on( 'toggle', function ( visible ) {
if ( visible ) {
summaryInput.focus();
} else {
descriptionInput.focus();
}
} );
actionField.$element.append( summaryInput.$element );
// Hide previous displayed clickies and add to DOM
$description.addClass( 'sdh-showdescrip-hidden' );
$sdh.append( actionField.$element );
descriptionInput.focus();
} );
}
};
/**
* Create the html and append it to the DOM
*
* @param {Object} textElement
* @param {Array<Clicky>} clickyElements
* @param {InfoClickyPopup} popupElement
*/
var updateSDH = function ( textElement, clickyElements, popupElement ) {
if ( popupElement ) {
clickyElements.push( popupElement );
}
$description.append( textElement );
if ( clickyElements.length > 0 ) {
$clickies.append( clickyElements );
$description.append( $clickies );
}
$sdh.append( $description );
$sdh.addClass( 'noprint' );
var hideSDH = function () {
$sdh.addClass( 'sdh-ve-hidden' );
};
var showSDH = function () {
$sdh.removeClass( 'sdh-ve-hidden' );
};
$.ready.then( function () {
// Undo padding used to fix content jump
mw.util.addCSS( '.skin-vector.ns-0 #mw-content-subtitle::after {content: none;}' );
// Add the main div to the subtitle
mw.util.addSubtitle( $sdh[0] );
mw.hook( 've.activationComplete' ).add( function () {
hideSDH();
} );
mw.hook( 've.deactivationComplete' ).add( function () {
showSDH();
} );
} );
};
/**
* Disable all buttons and create processing (...) animation
* Used by export and import buttons.
*/
var setProcessing = function () {
var x;
var $processing = $( '<div>' )
.addClass( 'sdh-processing' );
// Disable all clicky buttons
$clickies
.children( '.sdh-clicky' )
.addClass( 'sdh-clicky-disabled' )
.children( 'a' )
.off();
// Add processing ... animation
$description.append( $processing );
for ( x = 0; x < 3; x++ ) {
$processing.append(
$( '<div>' )
.addClass( [
'sdh-processing-dot',
'sdh-processing-dot-' + x
] )
.text( '.' )
);
}
};
/**
* Texts, clickies, and popups contain
* elements that could make up the initial display.
*/
var texts = {
noDescription: $( '<span>' )
.addClass( 'sdh-no-description' )
.text( mw.msg( 'sdh-no-description' ) ),
missingDescription: $( '<span>' )
.addClass( 'sdh-missing-description' )
.html( mw.msg( 'sdh-missing-description', mw.msg( 'sdh-' + (isRedirect ? 'redirect' : 'article') + '-label' ) ) ),
pageDescription: $( '<span>' )
.addClass( 'mw-page-description ' )
.text( pageDescription + ( appendWDDescription ?
( ' (Wikidata: ' + wikidataDescription + ')' ) : '' ) )
};
var clickies = {
add: new Clicky(
'sdh-add',
function () {
summaryMsg = 'sdh-summary-adding';
addWikidata = true; // Description should be added to wikidata in this case
textInput();
}
),
addNone: new Clicky(
'sdh-add',
function () {
summaryMsg = 'sdh-summary-changing';
change = true;
emptyPreload = true;
textInput();
}
),
addUseless: new Clicky(
'sdh-add',
function () {
summaryMsg = 'sdh-summary-adding-local';
emptyPreload = true;
textInput();
}
),
edit: new Clicky(
'sdh-edit',
function () {
summaryMsg = 'sdh-summary-changing';
change = true;
textInput();
}
),
editimport: new Clicky(
'sdh-editimport',
function () {
summaryMsg = 'sdh-summary-adding-local';
textInput();
}
),
export: new Clicky(
'sdh-export',
function () {
setProcessing();
editWikidataDescription( pageDescription );
}
),
import: new Clicky(
'sdh-import',
function () {
setProcessing();
summaryMsg = 'sdh-summary-importing-wikidata';
editDescription( pageDescription );
}
),
override: new Clicky(
'sdh-override',
function () {
summaryMsg = 'sdh-summary-adding-custom';
textInput();
}
),
wikidataLink: $( '<span>' )
.addClass( [
'sdh-clicky',
'sdh-wikidata-description'
] )
.append( $( '<a>' )
.attr( 'href', 'https://www.wikidata.org/wiki/Special:SetLabelDescriptionAliases/' + wgQid + '/' + language )
.text( mw.msg( 'sdh-wikidata-link-label' ) )
)
};
var popups = {
disambig: new InfoClickyPopup(
mw.message( 'sdh-disambig-popup' ).plain()
),
noDescription: new InfoClickyPopup(
mw.message( 'sdh-no-description-popup' ).plain()
),
override: new InfoClickyPopup(
mw.message( 'sdh-override-popup' ).plain()
),
useless: new InfoClickyPopup(
mw.message( 'sdh-useless-popup' ).plain()
)
};
/**
* Depending on various factors, such as
* whether the description exists,
* whether the description is on wikidata or not,
* and whether the page is in mainspace,
* this code determines what elements should make up the initial display.
* updateSDH() is then called to generate the html
* and add that to the DOM.
*
* @param {Object} wikitextResult
*/
var determineElements = function ( wikitextResult ) {
/**
* The description as determined from the wikitext.
*
* @type {string}
*/
var descriptionFromText;
/**
* The short description or a message saying no description exists etc.
*
* @type {Object}
*/
var textElement;
/**
* What the relevant buttons ("clickies") are.
*
* @type {Array<Clicky>}
*/
var clickyElements = [];
/**
* What clickable popup explanation is there if any
*
* @type {InfoClickyPopup}
*/
var popupElement;
/**
* Whether the description is none
*
* @type {boolean}
*/
var isNone;
// Whether to show "Missing article description" if applicable
var showMissing = (
( namespace === 0 || namespace === 118 ) &&
( !isRedirect || options.AddToRedirect )
);
// If not enwiki, complete logic for non-enwiki case and exit.
if ( onlyEditWikidata ) {
if ( pageDescription ) {
textElement = pageDescription;
clickyElements.push( clickies.edit );
} else if ( showMissing ) {
textElement = texts.missingDescription;
clickyElements.push( clickies.add );
}
updateSDH( textElement, clickyElements, popupElement );
return;
}
/**
* Determine if the short description is in the wikitext
* or if it is generated by an infobox.
*/
descriptionFromText = shortdescInText( wikitextResult )[ 1 ];
/**
* Determine if the description is none.
*/
isNone = descriptionFromText && descriptionFromText.toLowerCase() === 'none';
// Show wikidata link at beginning if displaying non-local description.
if ( pageDescription && !isLocal ) {
clickyElements.push( clickies.wikidataLink );
}
if ( isNone ) {
// Handle {{Short description|none}}
isLocal = true;
textElement = texts.noDescription;
clickyElements.push( clickies.addNone );
popupElement = popups.noDescription;
} else {
// Handle remaining cases
if ( pageDescription ) {
textElement = texts.pageDescription;
if ( isLocal ) {
if ( descriptionFromText ) {
clickyElements.push( clickies.edit );
} else {
if ( disambigPage ) {
popupElement = popups.disambig;
} else {
clickyElements.push( clickies.override );
popupElement = popups.override;
}
}
} else {
if ( uselessDescription ) {
popupElement = popups.useless;
clickyElements.push(
clickies.addUseless
);
} else {
clickyElements.push(
clickies.import,
clickies.editimport
);
}
}
} else if ( showMissing ) {
textElement = texts.missingDescription;
clickyElements.push( clickies.add );
}
}
// Don't show clickies for editing if not allowing editing
if ( !allowEditing ) {
clickyElements = [];
}
if ( isLocal && !isNone && options.ExportButton ) {
clickyElements.push( clickies.export );
}
updateSDH( textElement, clickyElements, popupElement );
};
if ( callPromiseText ) {
callPromiseText.then( function ( wikitextResult ) {
determineElements( wikitextResult );
} );
} else {
determineElements();
}
};
$.when( callPromiseDescription, callPromiseWDDescription ).then( onResponses );
};
/* Load if viewing a page normally (not in diff view) */
if (
mw.config.get( 'wgIsArticle' ) &&
!mw.config.get( 'wgDiffOldId' ) &&
mw.config.get( 'wgArticleId' ) !== 0
) {
/**
* Commented out due to issues reported at
* [[Special:PermaLink/925885151#Doubled_short_descriptions_from_Template:Infobox_settlement]].
* Fire on postEdit hook to load after Visual Editor saves,
* as VE does not actually reload the page.
* Unfortunately, postEdit fires both after regular edits and VE edits,
* so duplicate instances will be caused after a regular edit if run always
* on postEdit.
* window.sdh.hasRun is set to true below, and will be undefined after a proper reload,
* but not after a dynamic VE reload.
* FIXME: Post edit hook fires too early, meaning if an editor adds a short description using VE
* it won't show the right description.
*
* mw.hook( 'postEdit' ).add( function () {
* if ( window.sdh.hasRun ) {
* window.sdh.main();
* }
* } );
*
* if ( !window.sdh.hasRun ) { // Don't run twice
* window.sdh.hasRun = true;
* window.sdh.initMessages();
* window.sdh.main();
* }
*/
window.sdh.initMessages();
window.sdh.main();
}
// </nowiki>
sl3btwptzcckzpblowt5t979kp7qskt
မီႇတီႇယႃႇဝီႇၶီႇ:AFC-submit-wizard.js
8
27890
125513
55786
2026-05-04T23:18:57Z
Saimawnkham
5
ဢၢပ်ႉတဵတ်ႉၶေႃႈမုၼ်း
125513
javascript
text/javascript
/**
* MediaWiki:AfC-submit-wizard.js
*
* JavaScript used for submitting drafts to AfC.
* Used on [[Wikipedia:Articles for creation/Submitting]].
* Loaded via [[mw:Snippets/Load JS and CSS by URL]].
*
* Edits can be proposed via GitHub (https://github.com/wikimedia-gadgets/afc-submit-wizard)
* or a talk page request.
*
* Author: [[User:SD0001]]
* Licence: MIT
*/
/* globals mw, $, OO */
/* <nowiki> */
(function () {
$.when(
$.ready,
mw.loader.using([
'mediawiki.util', 'mediawiki.api', 'mediawiki.Title',
'mediawiki.widgets', 'oojs-ui-core', 'oojs-ui-widgets'
])
).then(function () {
if (mw.config.get('wgPageName') !== 'Wikipedia:Articles_for_creation/Submitting' ||
mw.config.get('wgAction') !== 'view') {
return;
}
init();
});
var afc = {}, ui = {};
window.afc = afc;
afc.ui = ui;
var config = {
allowedNamespaces: [2, 118, 5], // User, Draft, WT
debounceDelay: 500,
redirectionDelay: 1000,
defaultAfcTopic: 'other'
};
// TODO: move to a separate JSON subpage, would be feasible once [[phab:T198758]] is resolved
var messages = {
"document-title": "Submitting your draft ...",
"page-title": "Submitting your draft ...",
"fieldset-label": "Submit your draft for review at Articles for Creation (AfC)",
"title-label": "Draft title",
"title-placeholder": "Enter the draft page name, usually begins with \"Draft:\"",
"title-helptip": "This should be pre-filled if you clicked the link while on the draft page",
"rawclass-label": "Choose the most appropriate category",
"rawclass-helptip": "For biographies about scholars, choose one of the two biography categories rather than one associated to their field",
"shortdesc-placeholder": "Briefly describe the subject in 2–5 words (eg. \"British astronomer\", \"Cricket stadium in India\")",
"shortdesc-label": "Short description",
"shortdesc-helptip": "Try not to exceed 40 characters",
"talktags-placeholder": "Start typing to search for tags ...",
"talktags-label": "WikiProject classification tags",
"talktags-helptip": "Adding the 1–4 most applicable WikiProjects is plenty. For example, if you add the Physics tag, you do not need to also add the Science tag.",
"orestopic-placeholder": "Start typing to search for topics ...",
"orestopic-label": "Topic classifiers",
"orestopic-helptip": "Pick the topic areas that are relevant",
"submit-label": "Submit",
"footer-text": "<small>If you are not sure about what to enter in a field, you can skip it. If you need help, you can ask at the <b>[[WP:AFCHD|AfC help desk]]</b> or get live help via <b>[[WP:IRCHELP|IRC]]</b> or <b>[[WP:DISCORD|Discord]]</b>.<br>Facing some issues in using this form? <b>[/w/index.php?title=Wikipedia_talk:WikiProject_Articles_for_creation/Submission_wizard&action=edit§ion=new&preloadtitle=Issue%20with%20submission%20form&editintro=Wikipedia_talk:WikiProject_Articles_for_creation/Submission_wizard/editintro Report it]</b>.</small>",
"submitting-as": "Submitting as User:$1",
"validation-notitle": "Please enter the draft page name",
"validation-invalidtitle": "Please check draft title. This title is invalid.",
"validation-missingtitle": "Please check draft title. No such draft exists.",
"validation-wrongns": "Please check draft title – it should begin with \"Draft:\" or \"User:\"",
"warning-norefs": "This draft doesn't appear to contain any references. Please add references, without which it is likely to be declined. See [[Help:Introduction to referencing with Wiki Markup/2|help on adding references]].",
"status-processing": "Processing ...",
"status-saving": "Saving draft page ...",
"editsummary-main": "Submitting using [[WP:AFCSW|AfC-submit-wizard]]",
"status-redirecting": "Submission succeeded. Redirecting you to the draft page ...",
"captcha-label": "Please enter the letters appearing in the box below",
"captcha-placeholder": "Enter the letters here",
"captcha-helptip": "CAPTCHA security check. Click \"Submit\" again when done.",
"error-saving-main": "An error occurred ($1). Please try again or refer to the help desk.",
"status-saving-talk": "Saving draft talk page ...",
"editsummary-talk": "Adding WikiProject tags using [[WP:AFCSW|AfC-submit-wizard]]",
"status-talk-success": "Successfully added WikiProject tags to talk page",
"error-saving-talk": "An error occurred in editing the talk page ($1).",
"error-main": "An error occurred ($1). Please try again or refer to the help desk."
};
function init() {
for (var key in messages) {
mw.messages.set('afcsw-' + key, messages[key]);
}
document.title = msg('document-title');
$('#firstHeading').text(msg('page-title'));
mw.util.addCSS(
// CSS adjustments for vector-2022: hide prominent page controls which are
// irrelevant and confusing while using the wizard
'.vector-page-toolbar { display: none } ' +
'.vector-page-titlebar #p-lang-btn { display: none } ' +
// Hide categories as well, prevents accidental HotCat usage
'#catlinks { display: none } '
);
var apiOptions = {
parameters: {
format: 'json',
formatversion: '2'
},
ajax: {
headers: {
'Api-User-Agent': 'w:en:MediaWiki:AFC-submit-wizard.js'
}
}
};
// Two different API objects so that aborts on the lookupApi don't stop the final
// evaluate process
afc.api = new mw.Api(apiOptions);
afc.lookupApi = new mw.Api(apiOptions);
constructUI();
}
function constructUI() {
ui.fieldset = new OO.ui.FieldsetLayout({
label: msg('fieldset-label'),
classes: [ 'container' ],
items: [
ui.titleLayout = new OO.ui.FieldLayout(ui.titleInput = new mw.widgets.TitleInputWidget({
value: (mw.util.getParamValue('page') || '').replace(/_/g, ' '),
placeholder: msg('title-placeholder'),
}), {
label: msg('title-label'),
align: 'top',
help: msg('title-helptip'),
helpInline: true
}),
ui.afcTopicLayout = new OO.ui.FieldLayout(ui.afcTopicInput = new OO.ui.RadioSelectInputWidget(), {
label: msg('rawclass-label'),
help: msg('rawclass-helptip'),
align: 'inline',
}),
ui.shortdescLayout = new OO.ui.FieldLayout(ui.shortdescInput = new OO.ui.TextInputWidget({
placeholder: msg('shortdesc-placeholder'),
maxLength: 100
}), {
label: msg('shortdesc-label'),
align: 'top',
help: msg('shortdesc-helptip'),
helpInline: true,
}),
ui.talkTagsLayout = new OO.ui.FieldLayout(ui.talkTagsInput = new OO.ui.MenuTagMultiselectWidget({
placeholder: msg('talktags-placeholder'),
tagLimit: 10,
autocomplete: false,
$overlay: $('<div>').addClass('projectTagOverlay').css({
'position': 'absolute',
'z-index': '110'
}).appendTo('body')
}), {
label: msg('talktags-label'),
align: 'top',
help: msg('talktags-helptip'),
helpInline: true,
}),
// This is shown only if the ORES topic lookup fails, or is inconclusive
ui.oresTopicLayout = new OO.ui.FieldLayout(ui.oresTopicInput = new OO.ui.MenuTagMultiselectWidget({
placeholder: msg('orestopic-placeholder'),
tagLimit: 10,
autocomplete: false, // XXX: doesn't seem to work
options: [ "biography", "women", "food-and-drink", "internet-culture", "linguistics", "literature", "books", "entertainment", "films", "media", "music", "radio", "software", "television", "video-games", "performing-arts", "philosophy-and-religion", "sports", "architecture", "comics-and-anime", "fashion", "visual-arts", "geographical", "africa", "central-africa", "eastern-africa", "northern-africa", "southern-africa", "western-africa", "central-america", "north-america", "south-america", "asia", "central-asia", "east-asia", "north-asia", "south-asia", "southeast-asia", "west-asia", "eastern-europe", "europe", "northern-europe", "southern-europe", "western-europe", "oceania", "business-and-economics", "education", "history", "military-and-warfare", "politics-and-government", "society", "transportation", "biology", "chemistry", "computing", "earth-and-environment", "engineering", "libraries-and-information", "mathematics", "medicine-and-health", "physics", "stem", "space", "technology" ].map(function (e) {
return {
data: e,
label: e
};
})
}), {
label: msg('orestopic-label'),
align: 'top',
help: msg('orestopic-helptip'),
helpInline: true
}),
ui.submitLayout = new OO.ui.FieldLayout(ui.submitButton = new OO.ui.ButtonWidget({
label: msg('submit-label'),
flags: [ 'progressive', 'primary' ],
}))
]
});
ui.footerLayout = new OO.ui.FieldLayout(new OO.ui.LabelWidget({
label: $('<div>')
.append(linkify(msg('footer-text')))
}), {
align: 'top'
});
afc.topicOptionsLoaded = getJSONPage('Wikipedia:WikiProject Articles for creation/AfC topic map.json').then(function (optionsJson) {
var options = [];
$.each(optionsJson, function (code, info) {
options.push({
label: info.label,
data: code
});
});
ui.afcTopicInput.setOptions(options);
ui.afcTopicInput.setValue(config.defaultAfcTopic);
// resolve promise with allowed option codes:
return options.map(function (op) {
return op.data;
});
});
ui.oresTopicLayout.toggle(false);
var asUser = mw.util.getParamValue('username');
if (asUser && asUser !== mw.config.get('wgUserName')) {
ui.fieldset.addItems([
new OO.ui.FieldLayout(new OO.ui.MessageWidget({
type: 'notice',
inline: true,
label: msg('submitting-as', asUser)
}))
], /* position */ 5); // just before submit button
}
// Attach
$('#afc-submit-wizard-container').empty().append(ui.fieldset.$element, ui.footerLayout.$element);
mw.track('stats.mediawiki_gadget_AFCSW_total', 1, { action: 'opened' });
// Populate talk page tags for multi-select widget
afc.talkTagOptionsLoaded = getJSONPage('Wikipedia:WikiProject Articles for creation/WikiProject templates.json').then(function (data) {
ui.talkTagsInput.addOptions(Object.keys(data).map(function (k) {
return {
data: data[k],
label: k
};
}));
});
ui.clearTalkTags = function () {
afc.talkTagOptionsLoaded.then(function () {
ui.talkTagsInput.setValue([]);
});
};
ui.addTalkTags = function (tags) {
afc.talkTagOptionsLoaded.then(function () {
ui.talkTagsInput.setValue(ui.talkTagsInput.getValue().concat(tags));
});
};
// Get mapping of infoboxes with relevant WikiProjects
afc.ibxmapLoaded = getJSONPage('Wikipedia:WikiProject Articles for creation/Infobox WikiProject map.json');
ui.submitButton.on('click', handleSubmit);
ui.titleInput.on('change', mw.util.debounce(config.debounceDelay, onDraftInputChange));
if (mw.util.getParamValue('page')) {
onDraftInputChange();
}
// The default font size in monobook and modern are too small at 10px
mw.util.addCSS('.skin-modern .projectTagOverlay, .skin-monobook .projectTagOverlay { font-size: 130%; }');
afc.beforeUnload = function (e) {
e.preventDefault();
e.returnValue = '';
return '';
};
$(window).on('beforeunload', afc.beforeUnload);
}
function onDraftInputChange() {
afc.lookupApi.abort(); // abort older API requests
var drafttitle = ui.titleInput.getValue().trim();
if (!drafttitle) { // empty
return;
}
debug('draft input changed: "' + ui.titleInput.getValue() + '"');
// re-initialize
ui.titleLayout.setErrors([]);
ui.titleLayout.setWarnings([]);
afc.oresTopics = null;
afc.talktext = null;
afc.pagetext = null;
ui.clearTalkTags();
afc.lookupApi.get({
"action": "query",
"prop": "revisions|description|info",
"titles": drafttitle,
"rvprop": "content",
"rvslots": "main"
}).then(setPrefillsFromPageData);
var titleObj = mw.Title.newFromText(drafttitle);
if (!titleObj || titleObj.isTalkPage()) {
return;
}
var talkpagename = titleObj.getTalkPage().toText();
afc.lookupApi.get({
"action": "query",
"prop": "revisions",
"titles": talkpagename,
"rvprop": "content",
"rvslots": "main",
}).then(setPrefillsFromTalkPageData);
}
function setPrefillsFromPageData(json) {
debug('page fetch query', json);
var page = json.query.pages[0];
var preNormalizedTitle = json.query.normalized && json.query.normalized[0] &&
json.query.normalized[0].from;
debug('page.title: "' + page.title + '"');
if (ui.titleInput.getValue() !== (preNormalizedTitle || page.title)) {
return; // user must have changed the title already
}
var errors = errorsFromPageData(page);
if (errors.length) {
ui.titleLayout.setErrors(errors);
return;
}
ui.titleLayout.setWarnings(warningsFromPageData(page));
afc.pagetext = page.revisions[0].slots.main.content;
// Set AfC topic category
var topicMatch = afc.pagetext.match(/\{\{AfC topic\|(.*?)\}\}/);
if (topicMatch) {
afc.topicOptionsLoaded.then(function(allowedCodes) {
var topic = topicMatch[1];
debug("Allowed topic codes fetched:", allowedCodes);
debug("AfC topic found:", topic);
// if the code found in the template is an invalid one, keep the default to "other",
// rather than the first item in the list
if (allowedCodes.indexOf(topic) !== -1) {
ui.afcTopicInput.setValue(topic);
} else {
ui.afcTopicInput.setValue(config.defaultAfcTopic);
}
});
} else {
ui.afcTopicInput.setValue(config.defaultAfcTopic);
}
// Set short description in form
ui.shortdescInput.setValue(page.description || '');
// Guess WikiProject tags from infoboxes on the page
afc.ibxmapLoaded.then(function (ibxmap) {
var infoboxRgx = /\{\{([Ii]nfobox [^|}]*)/g,
wikiprojects = [],
match;
while (match = infoboxRgx.exec(afc.pagetext)) {
var ibx = match[1].trim();
ibx = ibx[0].toUpperCase() + ibx.slice(1);
if (ibxmap[ibx]) {
wikiprojects = wikiprojects.concat(ibxmap[ibx]);
}
}
debug('wikiprojects from infobox: ', wikiprojects);
ui.addTalkTags(wikiprojects);
});
// Fill ORES topics
getOresTopics(page.lastrevid).then(function (topics) {
debug('ORES topics: ', topics);
if (!topics || !topics.length) { // unexpected API response or API returns unsorted
ui.oresTopicLayout.toggle(true);
} else {
ui.oresTopicLayout.toggle(false);
afc.oresTopics = topics;
}
}, function () {
ui.oresTopicLayout.toggle(true);
});
}
function setPrefillsFromTalkPageData (json) {
var talkpage = json.query.pages[0];
if (!talkpage || talkpage.missing) {
return;
}
afc.talktext = talkpage.revisions[0].slots.main.content;
debug(afc.talktext);
var existingWikiProjects = extractWikiProjectTagsFromText(afc.talktext);
var existingTags = existingWikiProjects.map(function (e) {
return e.name;
});
debug(existingTags);
ui.addTalkTags(existingTags);
}
/**
* @param {Object} page - from query API response
* @returns {string[]}
*/
function errorsFromPageData(page) {
if (!page || page.invalid) {
return [msg('validation-invalidtitle')];
}
if (page.missing) {
return [msg('validation-missingtitle')];
}
if (config.allowedNamespaces.indexOf(page.ns) === -1) {
return [msg('validation-wrongns')];
}
return [];
}
/**
* @param {Object} page - from query API response
* @returns {string[]}
*/
function warningsFromPageData(page) {
var pagetext = page.revisions[0].slots.main.content;
var warnings = [];
// Show no refs warning
if (!/<ref/i.test(pagetext) && !/\{\{([Ss]fn|[Hh]arv)/.test(pagetext)) {
warnings.push('warning-norefs');
}
// TODO: Show warning for use of deprecated/unreliable sources
// TODO: Show tip for avoiding peacock words or promotional language?
return warnings.map(function (warning) {
return new OO.ui.HtmlSnippet(linkify(msg(warning)));
});
}
/**
* @param {number} revid
* @returns {jQuery.Promise<string[]>}
*/
function getOresTopics(revid) {
return $.get('https://ores.wikimedia.org/v3/scores/enwiki/?models=drafttopic&revids=' + revid).then(function (json) {
// null is returned if at any point something in the API output is unexpected
// ES2020 has optional chaining, but of course on MediaWiki we're still stuck with ES5
return json &&
json.enwiki &&
json.enwiki.scores &&
json.enwiki.scores[revid] &&
json.enwiki.scores[revid].drafttopic &&
json.enwiki.scores[revid].drafttopic.score &&
(json.enwiki.scores[revid].drafttopic.score.prediction instanceof Array) &&
json.enwiki.scores[revid].drafttopic.score.prediction.map(function (topic, idx, topics) {
// Remove Asia.Asia* if Asia.South-Asia is present (example)
if (topic.slice(-1) === '*') {
var metatopic = topic.split('.').slice(0, -1).join('.');
for (var i = 0; i < topics.length; i++) {
if (topics[i] !== topic && topics[i].startsWith(metatopic)) {
return;
}
}
return metatopic.split('.').pop();
}
return topic.split('.').pop();
})
.filter(function (e) {
return e; // filter out undefined from above
})
.map(function (topic) {
// convert topic string to normalised form
return topic
.replace(/[A-Z]/g, function (match) {
return match[0].toLowerCase();
})
.replace(/ /g, '-')
.replace(/&/g, 'and');
});
});
}
/***
* @param {string} text
* @returns {{wikitext: string, name: string}[]}
*/
function extractWikiProjectTagsFromText(text) {
if (!text) {
return [];
}
// this is best-effort, no guaranteed accuracy
var existingTags = [];
var rgx = /\{\{(WikiProject [^|}]*).*?\}\}/g;
var match;
while (match = rgx.exec(text)) { // jshint ignore:line
var tag = match[1].trim();
if (tag === 'WikiProject banner shell') {
continue;
}
existingTags.push({
wikitext: match[0],
name: tag
});
}
return existingTags;
}
/**
* @param {string} type
* @param {string} message
*/
function setMainStatus(type, message) {
if (!ui.mainStatusLayout || !ui.mainStatusLayout.isElementAttached()) {
ui.fieldset.addItems([
ui.mainStatusLayout = new OO.ui.FieldLayout(ui.mainStatusArea = new OO.ui.MessageWidget())
]);
}
ui.mainStatusArea.setType(type);
ui.mainStatusArea.setLabel(message);
}
/**
* @param {string} type
* @param {string} message
*/
function setTalkStatus(type, message) {
if (!ui.talkStatusLayout) {
ui.fieldset.addItems([
ui.talkStatusLayout = new OO.ui.FieldLayout(ui.talkStatusArea = new OO.ui.MessageWidget())
]);
}
ui.talkStatusArea.setType(type);
ui.talkStatusArea.setLabel(message);
}
function handleSubmit() {
setMainStatus('notice', msg('status-processing'));
mw.track('stats.mediawiki_gadget_AFCSW_total', 1, { action: 'submit_attempted' });
ui.submitButton.setDisabled(true);
ui.mainStatusLayout.scrollElementIntoView();
var draft = ui.titleInput.getValue();
if (!draft) {
ui.titleLayout.setErrors([msg('validation-notitle')]);
ui.fieldset.removeItems([ui.mainStatusLayout]);
ui.submitButton.setDisabled(false);
ui.titleLayout.scrollElementIntoView();
return;
}
debug(draft);
afc.api.get({
"action": "query",
"prop": "revisions|description",
"titles": draft,
"rvprop": "content",
"rvslots": "main",
}).then(function (json) {
var apiPage = json.query.pages[0];
var errors = errorsFromPageData(apiPage);
if (errors.length) {
ui.titleLayout.setErrors(errors);
ui.fieldset.removeItems([ui.mainStatusLayout]);
ui.submitButton.setDisabled(false);
ui.titleLayout.scrollElementIntoView();
return;
}
var text = prepareDraftText(apiPage);
setMainStatus('notice', msg('status-saving'));
saveDraftPage(draft, text).then(function () {
setMainStatus('success', msg('status-redirecting'));
mw.track('stats.mediawiki_gadget_AFCSW_total', 1, { action: 'submit_succeeded' });
$(window).off('beforeunload', afc.beforeUnload);
setTimeout(function () {
location.href = mw.util.getUrl(draft);
}, config.redirectionDelay);
}, function (code, err) {
if (code === 'captcha') {
ui.fieldset.removeItems([ui.mainStatusLayout, ui.talkStatusLayout]);
ui.captchaLayout.scrollElementIntoView();
mw.track('stats.mediawiki_gadget_AFCSW_total', 1, { action: 'submit_captcha' });
} else {
setMainStatus('error', msg('error-saving-main', makeErrorMessage(code, err)));
mw.track('stats.mediawiki_gadget_AFCSW_total', 1, { action: 'submit_failed' });
mw.track('stats.mediawiki_gadget_AFCSW_total', 1, { action: 'submit_failed_' + code });
}
ui.submitButton.setDisabled(false);
});
var talktext = prepareTalkText(afc.talktext);
if (!afc.talktext && !talktext) {
// No content earlier, no content now. Stop here to avoid
// creating the talk page as empty.
return;
}
setTalkStatus('notice', msg('status-saving-talk'));
afc.api.postWithEditToken({
"action": "edit",
"title": new mw.Title(draft).getTalkPage().toText(),
"text": talktext,
"summary": msg('editsummary-talk')
}).then(function (data) {
if (data.edit && data.edit.result === 'Success') {
setTalkStatus('success', msg('status-talk-success'));
} else {
return $.Deferred().reject('unexpected result');
}
}).catch(function (code, err) {
setTalkStatus('error', msg('error-saving-talk', makeErrorMessage(code, err)));
});
}).catch(function (code, err) {
setMainStatus('error', msg('error-main', makeErrorMessage(code, err)));
ui.submitButton.setDisabled(false);
mw.track('stats.mediawiki_gadget_AFCSW_total', 1, { action: 'submit_failed' });
mw.track('stats.mediawiki_gadget_AFCSW_total', 1, { action: 'submit_failed_' + code });
});
}
function saveDraftPage(title, text) {
// TODO: handle edit conflict
var editParams = {
"action": "edit",
"title": title,
"text": text,
"summary": msg('editsummary-main')
};
if (ui.captchaLayout && ui.captchaLayout.isElementAttached()) {
editParams.captchaid = afc.captchaid;
editParams.captchaword = ui.captchaInput.getValue();
ui.fieldset.removeItems([ui.captchaLayout]);
}
return afc.api.postWithEditToken(editParams).then(function (data) {
if (!data.edit || data.edit.result !== 'Success') {
if (data.edit && data.edit.captcha) {
// Handle captcha for non-confirmed users
var url = data.edit.captcha.url;
afc.captchaid = data.edit.captcha.id; // abuse of global?
ui.fieldset.addItems([
ui.captchaLayout = new OO.ui.FieldLayout(ui.captchaInput = new OO.ui.TextInputWidget({
placeholder: msg('captcha-placeholder'),
required: true
}), {
warnings: [ new OO.ui.HtmlSnippet('<img src=' + url + '>') ],
label: msg('captcha-label'),
align: 'top',
help: msg('captcha-helptip'),
helpInline: true,
}),
], /* position */ 6); // just after submit button
// TODO: submit when enter key is pressed in captcha field
return $.Deferred().reject('captcha');
} else {
return $.Deferred().reject('unexpected-result');
}
}
});
}
/**
* @param {Object} page - page information from the API
* @returns {string} final draft page text to save
*/
function prepareDraftText(page) {
var text = page.revisions[0].slots.main.content;
var header = '';
// Handle short description
var shortDescTemplateExists = /\{\{[Ss]hort ?desc(ription)?\s*\|/.test(text);
var shortDescExists = !!page.description;
var existingShortDesc = page.description;
if (ui.shortdescInput.getValue()) {
// 1. No shortdesc - insert the one provided by user
if (!shortDescExists) {
header += '{{Short description|' + ui.shortdescInput.getValue() + '}}\n';
// 2. Shortdesc exists from {{short description}} template - replace it
} else if (shortDescExists && shortDescTemplateExists) {
text = text.replace(/\{\{[Ss]hort ?desc(ription)?\s*\|.*?\}\}\n*/g, '');
header += '{{Short description|' + ui.shortdescInput.getValue() + '}}\n';
// 3. Shortdesc exists, but not generated by {{short description}}. If the user
// has changed the value, save the new value
} else if (shortDescExists && existingShortDesc !== ui.shortdescInput.getValue()) {
header += '{{Short description|' + ui.shortdescInput.getValue() + '}}\n';
// 4. Shortdesc exists, but not generated by {{short description}}, and user hasn't changed the value
} else {
// Do nothing
}
} else {
// User emptied the shortdesc field (or didn't exist from before): remove any existing shortdesc.
// This doesn't remove any shortdesc that is generated by other templates
// Race condition (FIXME): if someone else added a shortdesc to the draft after this user opened the wizard,
// that shortdesc gets removed
text = text.replace(/\{\{[Ss]hort ?desc(ription)?\s*\|.*?\}\}\n*/g, '');
}
// Draft topics
debug(ui.oresTopicInput);
if (ui.oresTopicLayout.isVisible()) {
afc.oresTopics = ui.oresTopicInput.getValue();
}
if (afc.oresTopics && afc.oresTopics.length) {
text = text.replace(/\{\{[Dd]raft topics\|.*?\}\}\n*/g, '');
header += '{{Draft topics|' + afc.oresTopics.join('|') + '}}\n';
}
// Add AfC topic
text = text.replace(/\{\{AfC topic\|(.*?)\}\}/g, '');
header += '{{AfC topic|' + ui.afcTopicInput.getValue() + '}}\n';
// put AfC submission template
header += '{{subst:submit|1=' + (mw.util.getParamValue('username') || '{{subst:REVISIONUSER}}') + '}}\n';
// insert everything to the top
text = header + text;
debug(text);
return text;
}
/**
* @param {string} initialText - initial talk page text
* @returns {string} - final talk page text to save
*/
function prepareTalkText(initialText) {
var text = initialText;
// TODO: this can be improved to put tags within {{WikiProject banner shell}} (if already present or otherwise)
var alreadyExistingWikiProjects = extractWikiProjectTagsFromText(text);
var alreadyExistingTags = alreadyExistingWikiProjects.map(function (e) {
return e.name;
});
var tagsToAdd = ui.talkTagsInput.getValue().filter(function (tag) {
return alreadyExistingTags.indexOf(tag) === -1;
});
var tagsToRemove = alreadyExistingTags.filter(function (tag) {
return ui.talkTagsInput.getValue().indexOf(tag) === -1;
});
tagsToRemove.forEach(function (tag) {
text = text.replace(new RegExp('\\{\\{\\s*' + tag + '\\s*(\\|.*?)?\\}\\}\\n?'), '');
});
var tagsToAddText = tagsToAdd.map(function (tag) {
return '{{' + tag + '}}';
}).join('\n') + (tagsToAdd.length ? '\n' : '');
text = tagsToAddText + (text || '');
// remove |class=draft parameter in any WikiProject templates
text = text.replace(/(\{\{wikiproject.*?)\|\s*class\s*=\s*draft\s*/gi, '$1');
return text;
}
/**
* Load a JSON page from the wiki.
* Use API (instead of $.getJSON with action=raw) to take advantage of caching
* @param {string} page
* @returns {jQuery.Promise<Record<string, any>>}
**/
function getJSONPage (page) {
return afc.api.get({
action: 'query',
titles: page,
prop: 'revisions',
rvprop: 'content',
rvlimit: 1,
rvslots: 'main',
uselang: 'content',
maxage: '3600', // 1 hour
smaxage: '3600',
formatversion: 2
}).then(function (json) {
var content = json.query.pages[0].revisions[0].slots.main.content;
return JSON.parse(content);
}).catch(function (code, err) {
console.error(makeErrorMessage(code, err));
});
}
/**
* Expands wikilinks and external links into HTML.
* Used instead of mw.msg(...).parse() because we want links to open in a new tab,
* and we don't want tags to be mangled.
* @param {string} input
* @returns {string}
*/
function linkify(input) {
return input
.replace(
/\[\[:?(?:([^|\]]+?)\|)?([^\]|]+?)\]\]/g,
function(_, target, text) {
if (!target) {
target = text;
}
return '<a target="_blank" href="' + mw.util.getUrl(target) +
'" title="' + target.replace(/"/g, '"') + '">' + text + '</a>';
}
)
// for ext links, display text should be given
.replace(
/\[(\S*?) (.*?)\]/g,
function (_, target, text) {
return '<a target="_blank" href="' + target + '">' + text + '</a>';
}
);
}
function msg(key) {
var messageArgs = Array.prototype.slice.call(arguments, 1);
return mw.msg.apply(mw, ['afcsw-' + key].concat(messageArgs));
}
function makeErrorMessage(code, err) {
if (code === 'http') {
return 'http: there is no internet connectivity';
}
return code + (err && err.error && err.error.info ? ': ' + err.error.info : '');
}
function debug() {
Array.prototype.slice.call(arguments).forEach(function (arg) {
console.log(arg);
});
}
})(); // File-level closure to protect functions from being exposed to the global scope or overwritten
/* </nowiki> */
t04xaqmomht8a6nxxgf873vneakuw71
မီႇတီႇယႃႇဝီႇၶီႇ:Gadget-afchelper.js/submissions.js
8
28033
125514
117325
2026-05-04T23:20:02Z
Saimawnkham
5
ဢၢပ်ႉတဵတ်ႉၶေႃႈမုၼ်း
125514
javascript
text/javascript
// <nowiki>
( function ( AFCH, $, mw ) {
let $afchLaunchLink, $afch, $afchWrapper,
afchPage, afchSubmission, afchViews, afchViewer;
// Die if reviewing a nonexistent page or a userjs/css page
if ( typeof inUnitTestEnvironment === 'undefined' ) {
if ( mw.config.get( 'wgArticleId' ) === 0 ||
mw.config.get( 'wgPageContentModel' ) !== 'wikitext' ) {
return;
}
}
/**
* Represents an AfC submission -- its status as well as comments.
* Call submission.parse() to actually run the parsing process and fill
* the object with useful data.
*
* @param {AFCH.Page} page The submission page
*/
AFCH.Submission = function ( page ) {
// The associated page
this.page = page;
this.shortTitle = this.page.title.getMainText();
if ( [ /* User: */ 2, /* Wikipedia talk: */ 5 ].indexOf( this.page.title.getNamespaceId() ) !== -1 ) {
// We need to strip the first path component (part before first slash) from
// titles in the User or Wikipedia talk namespaces because those always have
// an extra page level - being subpages of the user page or WT:Articles for
// creation, respectively:
// 'User:Example/Foo' => 'Foo'
// 'WT:Articles for creation/Foo' => 'Foo'
// 'User:Example/Intel 8231/8232' => 'Intel 8231/8232'
this.shortTitle = this.shortTitle.replace( /.*?\//, '' );
}
this.resetVariables();
};
/**
* Resets variables and lists related to the submission state
*/
AFCH.Submission.prototype.resetVariables = function () {
// Various submission states, set in parse()
this.isPending = false;
this.isUnderReview = false;
this.isDeclined = false;
this.isDraft = false;
// Set in updateAttributesAfterParse()
this.isCurrentlySubmitted = false;
this.hasAfcTemplate = false;
// All parameters on the page, zipped up into one
// pretty package. The most recent value for any given
// parameter (based on `ts`) takes precedent.
this.params = {};
// Holds all of the {{afc submission}} templates that still
// apply to the page
this.templates = [];
// Holds all comments on the page
this.comments = [];
// Holds all submitters currently displayed on the page
// (indicated by the `u` {{afc submission}} parameter)
this.submitters = [];
};
/**
* Parses a submission, writing its current status and data to various properties
*
* @return {jQuery.Deferred} Resolves with the submission when parsed successfully
*/
AFCH.Submission.prototype.parse = function () {
const sub = this,
deferred = $.Deferred();
this.page.getTemplates().done( ( templates ) => {
sub.loadDataFromTemplates( templates );
sub.sortAndParseInternalData();
deferred.resolve( sub );
} );
return deferred;
};
/**
* @internal
* @param {Array<Object>} templates list of templates to parse
* @return {Array<Object>} [ this.templates, this.comments ]
*/
AFCH.Submission.prototype.loadDataFromTemplates = function ( templates ) {
// Represent each AfC submission template as an object.
const submissionTemplates = [],
commentTemplates = [];
$.each( templates, ( _, template ) => {
const name = template.target.toLowerCase();
if ( name === 'afc submission' || name === 'afc submission/draft' ) {
submissionTemplates.push( {
status: ( AFCH.getAndDelete( template.params, '1' ) || '' ).toLowerCase(),
timestamp: AFCH.getAndDelete( template.params, 'ts' ) || '',
params: template.params
} );
} else if ( name === 'afc comment' ) {
commentTemplates.push( {
// If we can't find a timestamp, set it to unicorns, because everyone
// knows that unicorns always come first.
timestamp: AFCH.parseForTimestamp( template.params[ '1' ], /* mwstyle */ true ) || 'unicorns',
text: template.params[ '1' ]
} );
}
} );
this.templates = submissionTemplates;
this.comments = commentTemplates;
// Return these for unit testing
return [ this.templates, this.comments ];
};
/**
* Sort the internal lists of AFC submission and Afc comment templates
*/
AFCH.Submission.prototype.sortAndParseInternalData = function () {
let sub = this,
submissionTemplates = this.templates,
commentTemplates = this.comments;
function timestampSortHelper( a, b ) {
// If we're passed something that's not a number --
// for example, {{REVISIONTIMESTAMP}} -- just sort it
// first and be done with it.
if ( isNaN( a.timestamp ) ) {
return -1;
} else if ( isNaN( b.timestamp ) ) {
return 1;
}
// Otherwise just sort normally
return +b.timestamp - +a.timestamp;
}
// Sort templates by timestamp; most recent are first
submissionTemplates.sort( timestampSortHelper );
commentTemplates.sort( timestampSortHelper );
// Reset variables related to the submisson state before re-parsing
this.resetVariables();
// Useful list of "what to do" in each situation.
const statusCases = {
// Declined
d: function () {
if ( !sub.isPending && !sub.isDraft && !sub.isUnderReview ) {
sub.isDeclined = true;
}
return true;
},
// Draft
t: function () {
// If it's been submitted or declined, remove draft tag
if ( sub.isPending || sub.isDeclined || sub.isUnderReview ) {
return false;
}
sub.isDraft = true;
return true;
},
// Under review
r: function () {
if ( !sub.isPending && !sub.isDeclined ) {
sub.isUnderReview = true;
}
return true;
},
// Pending
'': function () {
// Remove duplicate pending templates or a redundant
// pending template when the submission has already been
// declined / is already under review
if ( sub.isPending || sub.isDeclined || sub.isUnderReview ) {
return false;
}
sub.isPending = true;
sub.isDraft = false;
sub.isUnderReview = false;
return true;
}
};
// Process the submission templates in order, from the most recent to
// the oldest. In the process, we remove unneeded templates (for example,
// a draft tag when it's already been submitted) and also set various
// "isX" properties of the Submission.
submissionTemplates = $.grep( submissionTemplates, ( template ) => {
let keepTemplate = true;
if ( statusCases[ template.status ] ) {
keepTemplate = statusCases[ template.status ]();
} else {
// Default pending status
keepTemplate = statusCases[ '' ]();
}
// If we're going to be keeping this template on the page,
// save the parameter and submitter data. When saving params,
// don't overwrite parameters that are already set, because
// we're going newest to oldest (i.e. save most recent only).
if ( keepTemplate ) {
// Save parameter data
sub.params = $.extend( {}, template.params, sub.params );
// Save submitter if not already listed
if ( template.params.u && sub.submitters.indexOf( template.params.u ) === -1 ) {
sub.submitters.push( template.params.u );
}
// Will be re-added in makeWikicode() if necessary
delete template.params.small; // small=yes for old declines
}
return keepTemplate;
} );
this.isCurrentlySubmitted = this.isPending || this.isUnderReview;
this.hasAfcTemplate = !!submissionTemplates.length;
this.templates = submissionTemplates;
this.comments = commentTemplates;
};
/**
* Converts all the data to a hunk of wikicode
*
* @return {string}
*/
AFCH.Submission.prototype.makeWikicode = function () {
let output = [],
hasDeclineTemplate = false;
// Submission templates go first
$.each( this.templates, ( _, template ) => {
let tout = '{{AFC submission|' + template.status,
paramKeys = [];
// FIXME: Think about if we really want this elaborate-ish
// positional parameter ouput, or if it would be a better
// idea to just make everything absolute. When we get to a point
// where nobody is using the actual templates and it's 100%
// script-based, "pretty" isn't really that important and we
// can scrap this. Until then, though, we can only dream...
// Make an array of the parameters
$.each( template.params, ( key, value ) => {
// Parameters set to false are ignored
if ( value !== false ) {
paramKeys.push( key );
}
} );
paramKeys.sort( ( a, b ) => {
const aIsNumber = !isNaN( a ),
bIsNumber = !isNaN( b );
// If we're passed two numerical parameters then
// sort them in order (1,2,3)
if ( aIsNumber && bIsNumber ) {
return ( +a ) > ( +b ) ? 1 : -1;
}
// A is a number, it goes first
if ( aIsNumber && !bIsNumber ) {
return -1;
}
// B is a number, it goes first
if ( !aIsNumber && bIsNumber ) {
return 1;
}
// Otherwise just leave the positions as they were
return 0;
} );
$.each( paramKeys, ( index, key ) => {
const value = template.params[ key ];
// If it is a numerical parameter, doesn't include
// `=` in the value, AND is in sequence with the other
// numerical parameters, we can omit the key= part
// (positional parameters, joyous day :/ )
if ( key == +key && +key % 1 === 0 &&
value.indexOf( '=' ) === -1 &&
// Parameter 2 will be the first positional parameter,
// since 1 is always going to be the submission status.
( key === '2' || paramKeys[ index - 1 ] == +key - 1 ) ) {
tout += '|' + value;
} else {
tout += '|' + key + '=' + value;
}
} );
// Collapse old decline template if a newer decline
// template is already displayed on the page
if ( hasDeclineTemplate && template.status === 'd' ) {
tout += '|small=yes';
}
// So that subsequent decline templates will be collapsed
if ( template.status === 'd' ) {
hasDeclineTemplate = true;
}
// Finally, add the timestamp and a warning about removing the template
tout += '|ts=' + template.timestamp + '}} <!-- Do not remove this line! -->';
output.push( tout );
} );
// Then comment templates
$.each( this.comments, ( _, comment ) => {
output.push( '\n{{AFC comment|1=' + comment.text + '}}' );
} );
// If there were comments, add a horizontal rule beneath them
if ( this.comments.length ) {
output.push( '\n----' );
}
return output.join( '\n' );
};
/**
* Checks if submission is G13 eligible
*
* @return {jQuery.Deferred} Resolves to bool if submission is eligible
*/
AFCH.Submission.prototype.isG13Eligible = function () {
const deferred = $.Deferred();
// Submission must not currently be submitted
if ( this.isCurrentlySubmitted ) {
return deferred.resolve( false );
}
// Userspace drafts must have
// one or more AFC submission templates to be eligible
if ( this.page.title.getNamespaceId() == 2 &&
this.templates.length === 0 ) {
return deferred.resolve( false );
}
// And not have been modified in 6 months
// FIXME: Ignore bot edits?
this.page.getLastModifiedDate().done( ( lastEdited ) => {
const timeNow = new Date(),
sixMonthsAgo = new Date();
sixMonthsAgo.setMonth( timeNow.getMonth() - 6 );
deferred.resolve( ( timeNow.getTime() - lastEdited.getTime() ) >
( timeNow.getTime() - sixMonthsAgo.getTime() ) );
} );
return deferred;
};
/**
* Sets the submission status
*
* @param {string} newStatus status to set, 'd'|'t'|'r'|''
* @param {Object} newParams optional; params to add to the template whose status was set
* @return {boolean} success
*/
AFCH.Submission.prototype.setStatus = function ( newStatus, newParams ) {
const relevantTemplate = this.templates[ 0 ];
if ( [ 'd', 't', 'r', '' ].indexOf( newStatus ) === -1 ) {
// Unrecognized status
return false;
}
if ( !newParams ) {
newParams = {};
}
// If there are no templates on the page, just generate a new one
// (addNewTemplate handles the reparsing)
if ( !relevantTemplate ||
// Same for if the top template on the stack is already declined;
// we don't want to overwrite it
relevantTemplate.status === 'd' ) {
this.addNewTemplate( {
status: newStatus,
params: newParams
} );
} else {
// Just modify the template at the top of the stack
relevantTemplate.status = newStatus;
relevantTemplate.params.ns = mw.config.get( 'wgNamespaceNumber' );
// Add new parameters if specified
$.extend( relevantTemplate.params, newParams );
// And finally reparse
this.sortAndParseInternalData();
}
return true;
};
/**
* Add a new template to the beginning of this.templates
*
* @param {Object} data object with properties of template
* - status (default: '')
* - timestamp (default: '{{subst:REVISIONTIMESTAMP}}')
* - params (default: {})
*/
AFCH.Submission.prototype.addNewTemplate = function ( data ) {
this.templates.unshift( $.extend( /* deep */ true, {
status: '',
timestamp: '{{subst:REVISIONTIMESTAMP}}',
params: {
ns: mw.config.get( 'wgNamespaceNumber' )
}
}, data ) );
// Reparse :P
this.sortAndParseInternalData();
};
/**
* Add a new comment to the beginning of this.comments
*
* @param {string} text comment text
* @return {boolean} success
*/
AFCH.Submission.prototype.addNewComment = function ( text ) {
const commentText = addSignature( text );
this.comments.unshift( {
// Unicorns are explained in loadDataFromTemplates()
timestamp: AFCH.parseForTimestamp( commentText, /* mwstyle */ true ) || 'unicorns',
text: commentText
} );
// Reparse :P
this.sortAndParseInternalData();
return true;
};
/**
* Gets the submitter, or, if no specific submitter is available,
* just the page creator
*
* @return {jQuery.Deferred} resolves with user
*/
AFCH.Submission.prototype.getSubmitter = function () {
const deferred = $.Deferred(),
user = this.params.u;
// Recursively detect if the user has been renamed by checking the rename log
if ( user ) {
AFCH.api.get( {
action: 'query',
list: 'logevents',
formatversion: 2,
letype: 'renameuser',
lelimit: 1,
letitle: 'User:' + user
} ).then( ( resp ) => {
const logevents = resp.query.logevents;
if ( logevents.length ) {
const newName = logevents[ 0 ].params.newuser;
this.params.u = newName;
this.getSubmitter().then( ( user ) => {
deferred.resolve( user );
} );
} else {
deferred.resolve( user );
}
} );
} else {
this.page.getCreator().done( ( user ) => {
deferred.resolve( user );
} );
}
return deferred;
};
/**
* Represents text of an AfC submission
*
* @param {string} text
*/
AFCH.Text = function ( text ) {
this.text = text;
};
AFCH.Text.prototype.get = function () {
return this.text;
};
AFCH.Text.prototype.set = function ( string ) {
this.text = string;
return this.text;
};
AFCH.Text.prototype.prepend = function ( string ) {
this.text = string + this.text;
return this.text;
};
AFCH.Text.prototype.append = function ( string ) {
this.text += string;
return this.text;
};
AFCH.Text.prototype.cleanUp = function ( isAccept ) {
let text = this.text;
const commentsToRemove = [
'Please don\'t change anything and press save',
'Carry on from here, and delete this comment.',
'Please leave this line alone!',
'Just press the "Save page" button below without changing anything! Doing so will submit your article submission for review. ' +
'Once you have saved this page you will find a new yellow \'Review waiting\' box at the bottom of your submission page. ' +
'If you have submitted your page previously,(?: either)? the old pink \'Submission declined\' template or the old grey ' +
'\'Draft\' template will still appear at the top of your submission page, but you should ignore (them|it). Again, please ' +
'don\'t change anything in this text box. Just press the "Save page" button below.'
];
if ( isAccept ) {
// Remove {{Draft categories}}
text = text.replace( /\{\{(?:Draft categories|Draftcat)\s*\|((?:\s*\[\[:?Category:[ \S]+?\]\]\s*)*)\s*\}\}/gi, '$1' );
// Remove {{Draft article}} (and {{Draft}}).
// Not removed if the |text= parameter is present, which could contain
// arbitrary wikitext and therefore makes the end of the template harder
// to detect
text = text.replace( /\{\{Draft(?!\|\s*text\s*=)(?: article(?!\|\s*text\s*=)(?:\|(?:subject=)?[^|]+)?|\|(?:subject=)?[^|]+)?\}\}/gi, '' );
// Uncomment cats and templates
text = text.replace( /\[\[:Category:/gi, '[[Category:' );
text = text.replace( /\{\{(tl|tlx|tlg)\|(.*?)\}\}/ig, '{{$2}}' );
const templatesToRemove = [
'AfC postpone G13',
'Draft topics',
'AfC topic',
'Drafts moved from mainspace',
'Promising draft'
];
templatesToRemove.forEach( ( template ) => {
text = text.replace( new RegExp( '\\{\\{' + template + '\\s*\\|?(.*?)\\}\\}\\n?', 'gi' ), '' );
} );
// Add to the list of comments to remove
commentsToRemove.push(
'Enter template purpose and instructions here.',
'Enter the content and\\/or code of the template here.',
'EDIT BELOW THIS LINE',
'Metadata: see \\[\\[Wikipedia:Persondata\\]\\].',
'See http://en.wikipedia.org/wiki/Wikipedia:Footnotes on how to create references using\\<ref\\>\\<\\/ref\\> tags, these references will then appear here automatically',
'(After listing your sources please cite them using inline citations and place them after the information they cite.|Inline citations added to your article will automatically display here.) ' +
'(Please see|See) ((https?://)?en.wikipedia.org/wiki/(Wikipedia|WP):REFB|\\[\\[Wikipedia:REFB\\]\\]) for instructions on how to add citations.',
'Important, do not remove this line before (template|article) has been created.',
'Important, do not remove anything above this line before (template|article) has been created.',
'Do not remove this line!'
);
} else {
// We want to disable categories, except categories inside of the template {{Draft categories}}, by adding a colon to the beginning of the wikilink.
// Replace {{Draft categories}} with a placeholder.
const protectedDraftCategories = [];
text = text.replace( /\{\{(?:Draft categories|Draftcat)\b[\s\S]*?\}\}(?:\r?\n)/gi, ( match ) => {
const placeholder = '__AFCH_DRAFT_CATS_' + protectedDraftCategories.length + '__';
protectedDraftCategories.push( match );
return placeholder;
} );
// Disable categories by adding a colon to the beginning of the wikilink.
text = text.replace( /\[\[Category:/gi, '[[:Category:' );
// Replace the placeholder with the original wikicode.
protectedDraftCategories.forEach( ( draftCategories, index ) => {
text = text.replace( '__AFCH_DRAFT_CATS_' + index + '__', draftCategories );
} );
}
// Remove empty section at the end (caused by "Resubmit" button on "declined" template)
// Section may have categories after it - keep them there
text = AFCH.removeEmptySectionAtEnd( text );
// Assemble a master regexp and remove all now-unneeded comments (commentsToRemove)
const commentRegex = new RegExp( '<!-{2,}\\s*(' + commentsToRemove.join( '|' ) + ')\\s*-{2,}>', 'gi' );
text = text.replace( commentRegex, '' );
// Remove initial request artifact
text = text.replace( /== Request review at \[\[WP:AFC\]\] ==/gi, '' );
// Remove sandbox templates
text = text.replace( /\{\{(userspacedraft|userspace draft|user sandbox|Please leave this line alone \(sandbox heading\))(?:\{\{[^{}]*\}\}|[^}{])*\}\}/ig, '' );
// Remove html comments (<!--) that surround categories
text = text.replace( /<!--\s*((\[\[:{0,1}(Category:.*?)\]\]\s*)+)-->/gi, '$1' );
// Remove spaces/commas between <ref> tags
text = text.replace( /\s*(<\/\s*ref\s*>)\s*[,]*\s*(<\s*ref\s*(name\s*=|group\s*=)*\s*[^/]*>)[ \t]*$/gim, '$1$2' );
// Remove whitespace before <ref> tags
text = text.replace( /[ \t]*(<\s*ref\s*(name\s*=|group\s*=)*\s*.*[^/]+>)[ \t]*$/gim, '$1' );
// Move punctuation before <ref> tags
text = text.replace( /\s*((<\s*ref\s*(name\s*=|group\s*=)*\s*.*[/]{1}>)|(<\s*ref\s*(name\s*=|group\s*=)*\s*[^/]*>(?:<[^<>]*>|[^><])*<\/\s*ref\s*>))[ \t]*([.!?,;:])+$/gim, '$6$1' );
// Replace {{http://example.com/foo}} with "* http://example.com/foo" (common newbie error)
text = text.replace( /\n\{\{(http[s]?|ftp[s]?|irc|gopher|telnet):\/\/(.*?)\}\}/gi, '\n* $1://$3' );
text = this.convertExternalLinksToWikilinks( text );
this.text = text;
this.removeExcessNewlines();
return this.text;
};
/**
* Convert http://-style links to other wikipages to wikicode syntax
*
* @param {string} text
* @return {string}
*/
AFCH.Text.prototype.convertExternalLinksToWikilinks = function ( text ) {
const linkRegex = /\[{1,2}(?:https?:)?\/\/(?:en.wikipedia.org\/wiki|enwp.org)\/([^\s|\][]+)(?:\s|\|)?((?:\[\[[^[\]]*\]\]|[^\][])*)\]{1,2}/ig;
let linkMatch = linkRegex.exec( text ),
title, displayTitle, newLink;
while ( linkMatch ) {
title = decodeURI( linkMatch[ 1 ] ).replace( /_/g, ' ' );
displayTitle = decodeURI( linkMatch[ 2 ] ).replace( /_/g, ' ' );
// Don't include the displayTitle if it is equal to the title
if ( displayTitle && title !== displayTitle ) {
newLink = '[[' + title + '|' + displayTitle + ']]';
} else {
newLink = '[[' + title + ']]';
}
text = text.replace( linkMatch[ 0 ], newLink );
linkMatch = linkRegex.exec( text );
}
return text;
};
AFCH.Text.prototype.removeExcessNewlines = function () {
// Replace 3+ newlines with just two
this.text = this.text.replace( /(?:[\t ]*(?:\r?\n|\r)){3,}/ig, '\n\n' );
// Remove all whitespace at the top of the article
this.text = this.text.replace( /^\s*/, '' );
};
AFCH.Text.prototype.getAfcComments = function () {
return this.text.match( /\{\{\s*afc comment[\s\S]+?\(UTC\)\}\}/gi );
};
AFCH.Text.prototype.removeAfcTemplates = function () {
// FIXME: Awful regex to remove the old submission templates
// This is bad. It works for most cases but has a hellish time
// with some double nested templates or faux nested templates (for
// example "{{hi|{ foo}}" -- note the extra bracket). Ideally Parsoid
// would just return the raw template text as well (currently
// working on a patch for that, actually).
this.text = this.text.replace( new RegExp( '\\{\\{\\s*afc submission\\s*(?:\\||[^{{}}]*|{{.*?}})*?\\}\\}' +
// Also remove the AFCH-generated warning message, since if necessary the script will add it again
'( <!-- Do not remove this line! -->)?', 'gi' ), '' );
// Nastiest hack of all time. As above, Parsoid would be great. Gotta wire it up asynchronously first, though.
this.text = this.text.replace( /\{\{\s*afc comment[\s\S]+?\(UTC\)\}\}/gi, '' );
// Remove horizontal rules that were added by AFCH after the comments
this.text = this.text.replace( /^----+$/gm, '' );
// Remove excess newlines created by AFC templates
this.removeExcessNewlines();
return this.text;
};
/**
* Removes old submission templates/comments and then adds new ones
* specified by `new`
*
* @param {string} newCode
* @return {string}
*/
AFCH.Text.prototype.updateAfcTemplates = function ( newCode ) {
this.removeAfcTemplates();
return this.prepend( newCode + '\n\n' );
};
AFCH.Text.prototype.updateCategories = function ( categories ) {
// There's no "g" flag in categoryRegex, because we use it
// to delete its matches in a loop. If it were global, then
// it would internally keep track of lsatIndex - then given
// two adjacent categories, only the first would get deleted
let catIndex, match,
text = this.text,
categoryRegex = /\[\[:?Category:.*?\s*\]\]/i,
newCategoryCode = '\n';
// Create the wikicode block
$.each( categories, ( _, category ) => {
const trimmed = $.trim( category );
if ( trimmed ) {
newCategoryCode += '\n[[Category:' + trimmed + ']]';
}
} );
match = categoryRegex.exec( text );
// If there are no categories currently on the page,
// just add the categories at the bottom
if ( !match ) {
text += newCategoryCode;
// If there are categories on the page, remove them all, and
// then add the new categories where the last category used to be
} else {
while ( match ) {
catIndex = match.index;
text = text.replace( match[ 0 ], '' );
match = categoryRegex.exec( text );
}
text = text.substring( 0, catIndex ) + newCategoryCode + text.substring( catIndex );
}
this.text = text;
return this.text;
};
AFCH.Text.prototype.updateShortDescription = function ( existingShortDescription, newShortDescription ) {
const shortDescTemplateExists = /\{\{[Ss]hort ?desc(ription)?\s*\|/.test( this.text );
const shortDescExists = !!existingShortDescription;
if ( newShortDescription ) {
// 1. No shortdesc - insert the one provided by user
if ( !shortDescExists ) {
this.prepend( '{{Short description|' + newShortDescription + '}}\n' );
// 2. Shortdesc exists from {{short description}} template - replace it
} else if ( shortDescExists && shortDescTemplateExists ) {
this.text = this.text.replace( /\{\{[Ss]hort ?desc(ription)?\s*\|.*?\}\}\n*/g, '' );
this.prepend( '{{Short description|' + newShortDescription + '}}\n' );
// 3. Shortdesc exists, but not generated by {{short description}}. If the user
// has changed the value, save the new value
} else if ( shortDescExists && existingShortDescription !== newShortDescription ) {
this.prepend( '{{Short description|' + newShortDescription + '}}\n' );
// 4. Shortdesc exists, but not generated by {{short description}}, and user hasn't changed the value
} else {
// Do nothing
}
} else {
// User emptied the shortdesc field (or didn't exist from before): remove any existing shortdesc.
// This doesn't remove any shortdesc that is generated by other templates
this.text = this.text.replace( /\{\{[Ss]hort ?desc(ription)?\s*\|.*?\}\}\n*/g, '' );
}
};
if ( typeof inUnitTestEnvironment === 'undefined' ) {
// Add the launch link
$afchLaunchLink = $( mw.util.addPortletLink( AFCH.prefs.launchLinkPosition, '#', 'Review (AFCH)',
'afch-launch', 'Review submission using afc-helper', '1' ) );
if ( AFCH.prefs.autoOpen &&
// Don't autoload in userspace -- too many false positives
AFCH.consts.pagename.indexOf( 'User:' ) !== 0 &&
// Only autoload if viewing or editing the page
[ 'view', 'edit', null ].indexOf( AFCH.getParam( 'action' ) ) !== -1 &&
!AFCH.getParam( 'diff' ) && !AFCH.getParam( 'oldid' ) ) {
// Launch the script immediately if preference set
createAFCHInstance();
} else {
// Otherwise, wait for a click (`one` to prevent spawning multiple panels)
$afchLaunchLink.one( 'click', createAFCHInstance );
}
// Mark launch link for the old helper script as "old" if present on page
$( '#p-cactions #ca-afcHelper > a' ).append( ' (old)' );
}
// If AFCH is destroyed via AFCH.destroy(),
// remove the $afch window and the launch link
AFCH.addDestroyFunction( () => {
$afchLaunchLink.remove();
// The $afch window might not exist yet; make
// sure it does before trying to remove it :)
if ( $afch && $afch.jquery ) {
$afch.remove();
}
} );
function createAFCHInstance() {
/**
* global; wraps ALL afch-y things
*/
$afch = $( '<div>' )
.addClass( 'afch' )
.insertBefore( '#mw-content-text' )
.append(
$( '<div>' )
.addClass( 'top-bar' )
.append(
// Back link appears on the left based on context
$( '<div>' )
.addClass( 'back-link' )
.html( '◀ back to options' ) // back arrow
.attr( 'title', 'Go back' )
.addClass( 'hidden' )
.on( 'click', () => {
// Reload the review panel
spinnerAndRun( setupReviewPanel );
} ),
// On the right, a close button
$( '<div>' )
.addClass( 'close-link' )
.html( '×' )
.on( 'click', () => {
// DIE DIE DIE (...then allow clicks on the launch link again)
$afch.remove();
$afchLaunchLink
.off( 'click' ) // Get rid of old handler
.one( 'click', createAFCHInstance );
} )
)
);
/**
* global; wrapper for specific afch panels
*/
$afchWrapper = $( '<div>' )
.addClass( 'panel-wrapper' )
.appendTo( $afch )
.append(
// Build splash panel in JavaScript rather than via
// a template so we don't have to wait for the
// HTTP request to go through
$( '<div>' )
.addClass( 'review-panel' )
.addClass( 'splash' )
.append(
$( '<div>' )
.addClass( 'initial-header' )
.text( 'Loading AFCH ...' )
)
);
// Now set up the review panel and replace it with actual content, not just a splash screen
setupReviewPanel();
// If the "Review" link is clicked again, just reload the main view
$afchLaunchLink.on( 'click', () => {
spinnerAndRun( setupReviewPanel );
} );
}
function setupReviewPanel() {
// Store this to a variable so we can wait for its success
const loadViews = $.ajax( {
type: 'GET',
url: AFCH.consts.baseurl + '/tpl-submissions.js',
dataType: 'text'
} ).done( ( data ) => {
afchViews = new AFCH.Views( data );
afchViewer = new AFCH.Viewer( afchViews, $afchWrapper );
} );
afchPage = new AFCH.Page( AFCH.consts.pagename );
afchSubmission = new AFCH.Submission( afchPage );
// Set up messages for later
setMessages();
// Parse the page and load the view templates. When done,
// continue with everything else.
$.when(
afchSubmission.parse(),
loadViews
).then( ( submission ) => {
let extrasRevealed = false;
// Render the base buttons view
loadView( 'main', {
title: submission.shortTitle,
accept: submission.isCurrentlySubmitted,
decline: submission.isCurrentlySubmitted,
comment: true, // Comments are always okay!
submit: !submission.isCurrentlySubmitted,
alreadyUnderReview: submission.isUnderReview
} );
// Set up the extra options slide-out panel, which appears
// when the user click on the chevron
$afch.find( '#afchExtra .open' ).on( 'click', () => {
const $extra = $afch.find( '#afchExtra' );
if ( extrasRevealed ) {
$extra.find( 'a' ).hide();
$extra.stop().animate( { width: '20px' }, 100, 'swing', () => {
extrasRevealed = false;
} );
} else {
$extra.stop().animate( { width: '210px' }, 150, 'swing', () => {
$extra.find( 'a' ).css( 'display', 'block' );
extrasRevealed = true;
} );
}
} );
// Add preferences link
AFCH.preferences.initLink( $afch.find( 'span.preferences-wrapper' ), 'preferences' );
// Set up click handlers
$afch.find( '#afchAccept' ).on( 'click', () => {
spinnerAndRun( showAcceptOptions );
} );
$afch.find( '#afchDecline' ).on( 'click', () => {
spinnerAndRun( showDeclineOptions );
} );
$afch.find( '#afchComment' ).on( 'click', () => {
spinnerAndRun( showCommentOptions );
} );
$afch.find( '#afchSubmit' ).on( 'click', () => {
spinnerAndRun( showSubmitOptions );
} );
$afch.find( '#afchClean' ).on( 'click', () => {
handleCleanup();
} );
$afch.find( '#afchMark' ).on( 'click', () => {
handleMark( /* unmark */ submission.isUnderReview );
} );
// Load warnings about the page, then slide them in
getSubmissionWarnings().done( ( warnings ) => {
if ( warnings.length ) {
// FIXME: CSS-based slide-in animation instead to avoid having
// to use stupid hide() + removeClass() workaround?
$afch.find( '.warnings' )
.append( warnings )
.hide().removeClass( 'hidden' )
.slideDown();
}
} );
// Get G13 eligibility and when known, display relevant buttons...
// but don't hold up the rest of the loading to do so
submission.isG13Eligible().done( ( eligible ) => {
$afch.find( '.g13-related' ).toggleClass( 'hidden', !eligible );
$afch.find( '#afchG13' ).on( 'click', () => {
handleG13();
} );
$afch.find( '#afchPostponeG13' ).on( 'click', () => {
spinnerAndRun( showPostponeG13Options );
} );
} );
} );
}
/**
* Loads warnings about the submission
*
* @return {jQuery}
*/
function getSubmissionWarnings() {
const deferred = $.Deferred(),
warnings = [];
/**
* Adds a warning
*
* @param {string} message
* @param {string|boolean} actionMessage set to false to hide action link
* @param {Function|string} onAction function to call on success, or URL to browse to
*/
function addWarning( message, actionMessage, onAction ) {
let $action,
$warning = $( '<div>' )
.addClass( 'afch-warning' )
.text( message );
if ( actionMessage !== false ) {
$action = $( '<a>' )
.addClass( 'link' )
.text( '(' + ( actionMessage || 'Edit page' ) + ')' )
.appendTo( $warning );
if ( typeof onAction === 'function' ) {
$action.on( 'click', onAction );
} else {
$action
.attr( 'target', '_blank' )
.attr( 'href', onAction || mw.util.getUrl( AFCH.consts.pagename, { action: 'edit' } ) );
}
}
warnings.push( $warning );
}
function checkReferences() {
const deferred = $.Deferred();
afchPage.getText( false ).done( ( text ) => {
const refBeginRe = /<\s*ref.*?\s*>/ig,
// If the ref is closed already, we don't want it
// (returning true keeps the item, false removes it)
refBeginMatches = $.grep( text.match( refBeginRe ) || [], ( ref ) => ref.indexOf( '/>', ref.length - 2 ) === -1 ),
refEndRe = /<\/\s*ref\s*>/ig,
refEndMatches = text.match( refEndRe ) || [],
reflistRe = /({{(ref(erence)?(\s|-)?list|listaref|refs|footnote|reference|referencias)(?:{{[^{}]*}}|[^}{])*}})|(<\s*references\s*\/?>)/ig,
hasReflist = reflistRe.test( text ),
// This isn't as good as a tokenizer, and believes that <ref> foo </b> is
// completely correct... but it's a good intermediate level solution.
malformedRefs = text.match( /<\s*ref\s*[^/]*>?<\s*[^/]*\s*ref\s*>/ig ) || [];
// Uneven (/unclosed) <ref> and </ref> tags
if ( refBeginMatches.length !== refEndMatches.length ) {
addWarning( 'The submission contains ' +
( refBeginMatches.length > refEndMatches.length ? 'unclosed' : 'unbalanced' ) + ' <ref> tags.' );
}
// <ref>1<ref> instead of <ref>1</ref> detection
if ( malformedRefs.length ) {
addWarning( 'The submission contains malformed <ref> tags.', 'View details', function () {
const $warningDiv = $( this ).parent();
const $malformedRefWrapper = $( '<div>' )
.addClass( 'malformed-refs' )
.appendTo( $warningDiv );
// Show the relevant code snippets
$.each( malformedRefs, ( _, ref ) => {
$( '<div>' )
.addClass( 'code-wrapper' )
.append( $( '<pre>' ).text( ref ) )
.appendTo( $malformedRefWrapper );
} );
// Now change the "View details" link to behave as a normal toggle for .malformed-refs
AFCH.makeToggle( '.malformed-refs-toggle', '.malformed-refs', 'View details', 'Hide details' );
return false;
} );
}
// <ref> after {{reflist}}
if ( hasReflist ) {
if ( refBeginRe.test( text.substring( reflistRe.lastIndex ) ) ) {
addWarning( 'Not all of the <ref> tags are before the references list. You may not see all references.' );
}
}
// <ref> without {{reflist}}
if ( refBeginMatches.length && !hasReflist ) {
addWarning( 'The submission contains <ref> tags, but has no references list! You may not see all references.' );
}
deferred.resolve();
} );
return deferred;
}
function checkDeletionLog() {
const deferred = $.Deferred();
// Don't show deletion notices for "sandbox" to avoid useless
// information when reviewing user sandboxes and the like
if ( afchSubmission.shortTitle.toLowerCase() === 'sandbox' ) {
deferred.resolve();
return deferred;
}
AFCH.api.get( {
action: 'query',
list: 'logevents',
leprop: 'user|timestamp|comment',
leaction: 'delete/delete',
letype: 'delete',
lelimit: 10,
letitle: afchSubmission.shortTitle
} ).done( ( data ) => {
const rawDeletions = data.query.logevents;
if ( !rawDeletions.length ) {
deferred.resolve();
return;
}
addWarning( 'The page "' + afchSubmission.shortTitle + '" has been deleted ' + rawDeletions.length + ( rawDeletions.length === 10 ? '+' : '' ) +
' time' + ( rawDeletions.length > 1 ? 's' : '' ) + '.', 'View deletion log', function () {
const $toggleLink = $( this ).addClass( 'deletion-log-toggle' ),
$warningDiv = $toggleLink.parent(),
deletions = [];
$.each( rawDeletions, ( _, deletion ) => {
deletions.push( {
timestamp: deletion.timestamp,
relativeTimestamp: AFCH.relativeTimeSince( deletion.timestamp ),
deletor: deletion.user,
deletorLink: mw.util.getUrl( 'User:' + deletion.user ),
reason: AFCH.convertWikilinksToHTML( deletion.comment )
} );
} );
$( afchViews.renderView( 'warning-deletions-table', { deletions: deletions } ) )
.addClass( 'deletion-log' )
.appendTo( $warningDiv );
// ...and now convert the link into a toggle which simply hides/shows the div
AFCH.makeToggle( '.deletion-log-toggle', '.deletion-log', '(View deletion log)', '(Hide deletion log)' );
return false;
} );
deferred.resolve();
} );
return deferred;
}
function checkReviewState() {
let reviewer, isOwnReview;
if ( afchSubmission.isUnderReview ) {
isOwnReview = afchSubmission.params.reviewer === AFCH.consts.user;
if ( isOwnReview ) {
reviewer = 'You';
} else {
reviewer = afchSubmission.params.reviewer || 'Someone';
}
addWarning( reviewer + ( afchSubmission.params.reviewts ?
' began reviewing this submission ' + AFCH.relativeTimeSince( afchSubmission.params.reviewts ) :
' already began reviewing this submission' ) + '.',
isOwnReview ? 'Unmark as under review' : 'View page history',
isOwnReview ? () => {
handleMark( /* unmark */ true );
} : mw.util.getUrl( AFCH.consts.pagename, { action: 'history' } ) );
}
}
function checkLongComments() {
const deferred = $.Deferred();
afchPage.getText( false ).done( ( rawText ) => {
const
// Simulate cleanUp first so that we don't warn about HTML
// comments that the script will remove anyway in the future
text = ( new AFCH.Text( rawText ) ).cleanUp( true ),
longCommentRegex = /(?:<![ \r\n\t]*--)([^-]|[\r\n]|-[^-]){30,}(?:--[ \r\n\t]*>)?/g,
longCommentMatches = text.match( longCommentRegex ) || [],
numberOfComments = longCommentMatches.length,
oneComment = numberOfComments === 1;
if ( numberOfComments ) {
addWarning( 'The page contains ' + ( oneComment ? 'an' : '' ) + ' HTML comment' + ( oneComment ? '' : 's' ) +
' longer than 30 characters.', 'View comment' + ( oneComment ? '' : 's' ), function () {
const $toggleLink = $( this ).addClass( 'long-comment-toggle' ),
$warningDiv = $toggleLink.parent(),
$commentsWrapper = $( '<div>' )
.addClass( 'long-comments' )
.appendTo( $warningDiv );
// Show the relevant code snippets
$.each( longCommentMatches, ( _, comment ) => {
$( '<div>' )
.addClass( 'code-wrapper' )
.append( $( '<pre>' ).text( $.trim( comment ) ) )
.appendTo( $commentsWrapper );
} );
// Now change the "View comment" link to behave as a normal toggle for .long-comments
AFCH.makeToggle( '.long-comment-toggle', '.long-comments',
'(View comment' + ( oneComment ? '' : 's' ) + ')', '(Hide comment' + ( oneComment ? '' : 's' ) + ')' );
return false;
} );
}
deferred.resolve();
} );
return deferred;
}
function checkForCopyvio() {
return AFCH.api.get( {
action: 'pagetriagelist',
page_id: mw.config.get( 'wgArticleId' )
} ).then( ( json ) => {
const triageInfo = json.pagetriagelist.pages[ 0 ];
if ( triageInfo && Number( triageInfo.copyvio ) === mw.config.get( 'wgCurRevisionId' ) ) {
addWarning(
'This submission may contain copyright violations',
'CopyPatrol',
'https://copypatrol.wmcloud.org/en?filter=all&searchCriteria=page_exact&searchText=' + encodeURIComponent( afchPage.rawTitle ) + '&drafts=1&revision=' + mw.config.get( 'wgCurRevisionId' ), '_blank'
);
}
} );
}
function checkForBlocks() {
return afchSubmission.getSubmitter().then( ( creator ) => checkIfUserIsBlocked( creator ).then( ( blockData ) => {
if ( blockData !== null ) {
let date = 'infinity';
if ( blockData.expiry !== 'infinity' ) {
const data = new Date( blockData.expiry );
const monthNames = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ];
date = data.getUTCDate() + ' ' + monthNames[ data.getUTCMonth() ] + ' ' + data.getUTCFullYear() + ' ' + data.getUTCHours() + ':' + data.getUTCMinutes() + ' UTC';
}
const warning = 'Submitter ' + creator + ' was blocked by ' + blockData.by + ' with an expiry time of ' + date + '. Reason: ' + blockData.reason;
addWarning( warning );
}
} ) );
}
$.when(
checkReferences(),
checkDeletionLog(),
checkReviewState(),
checkLongComments(),
checkForCopyvio(),
checkForBlocks()
).then( () => {
deferred.resolve( warnings );
} );
return deferred;
}
/**
* Stores useful strings to AFCH.msg
*/
function setMessages() {
const headerBegin = '== Your submission at [[Wikipedia:Articles for creation|Articles for creation]]: ';
AFCH.msg.set( {
// $1 = article name
// $2 = article class or '' if not available
'accepted-submission': headerBegin +
'[[$1]] has been accepted ==\n{{subst:Afc talk|$1|class=$2|sig=~~~~}}',
// $1 = full submission title
// $2 = short title
// $3 = copyright violation ('yes'/'no')
// $4 = decline reason code
// $5 = decline reason additional parameter
// $6 = second decline reason code
// $7 = additional parameter for second decline reason
// $8 = additional comment
'declined-submission': headerBegin +
'[[$1|$2]] ({{subst:CURRENTMONTHNAME}} {{subst:CURRENTDAY}}) ==\n{{subst:Afc decline|full=$1|cv=$3|reason=$4|details=$5|reason2=$6|details2=$7|comment=$8|sig=yes}}',
// $1 = full submission title
// $2 = short title
// $3 = reject reason code ('e' or 'n')
// $4 = reject reason details (blank for now)
// $5 = second reject reason code
// $6 = second reject reason details
// $7 = comment by reviewer
'rejected-submission': headerBegin +
'[[$1|$2]] ({{subst:CURRENTMONTHNAME}} {{subst:CURRENTDAY}}) ==\n{{subst:Afc reject|full=$1|reason=$3|details=$4|reason2=$5|details2=$6|comment=$7|sig=yes}}',
// $1 = article name
'comment-on-submission': '{{subst:AFC notification|comment|article=$1}}',
// $1 = article name
'g13-submission': '{{subst:Db-afc-notice|$1}} ~~~~',
'teahouse-invite': '{{subst:Wikipedia:Teahouse/AFC invitation|sign=~~~~}}'
} );
}
/**
* Clear the viewer, set up the status log, and
* then update the button text
*
* @param {string} actionTitle optional, if there is no content available and the
* script has to load a new view, this will be its title
* @param {string} actionClass optional, if there is no content available and the
* script has to load a new view, this will be the class
* applied to it
*/
function prepareForProcessing( actionTitle, actionClass ) {
let $content = $afch.find( '#afchContent' ),
$submitBtn = $content.find( '#afchSubmitForm' );
// If we can't find a submit button or a content area, load
// a new temporary "processing" stage instead
if ( !( $submitBtn.length || $content.length ) ) {
loadView( 'quick-action-processing', {
actionTitle: actionTitle || 'Processing',
actionClass: actionClass || 'other-action'
} );
// Now update the variables
$content = $afch.find( '#afchContent' );
$submitBtn = $content.find( '#afchSubmitForm' );
}
// Empty the content area except for the button...
$content.contents().not( $submitBtn ).remove();
// ...and set up the status log in its place
AFCH.status.init( '#afchContent' );
// Update the button show the `running` text
$submitBtn
.text( $submitBtn.data( 'running' ) )
.addClass( 'disabled' )
.off( 'click' );
// Handler will run after the main AJAX requests complete
setupAjaxStopHandler();
}
/**
* Sets up the `ajaxStop` handler which runs after all ajax
* requests are complete and changes the text of the button
* to "Done", shows a link to the next submission and
* auto-reloads the page.
*/
function setupAjaxStopHandler() {
$( document ).on( 'ajaxStop', () => {
$afch.find( '#afchSubmitForm' )
.text( 'Done' )
.append(
' ',
$( '<a>' )
.attr( 'id', 'reloadLink' )
.addClass( 'text-smaller' )
.attr( 'href', mw.util.getUrl() )
.text( '(reloading...)' )
);
// Show a link to the next random submissions
// We need "new" here because Element uses "this." and needs the right context.
// eslint-disable-next-line no-new
new AFCH.status.Element( 'Continue to next $1 or $2 »', {
$1: AFCH.makeLinkElementToCategory( 'Pending AfC submissions', 'random submission' ),
$2: AFCH.makeLinkElementToCategory( 'AfC pending submissions by age/0 days ago', 'zero-day-old submission' )
} );
// Also, automagically reload the page in place
$( '#mw-content-text' ).load( AFCH.consts.pagelink + ' #mw-content-text', () => {
$afch.find( '#reloadLink' ).text( '(reload)' );
// Fire the hook for new page content
mw.hook( 'wikipage.content' ).fire( $( '#mw-content-text' ) );
} );
// Stop listening to ajaxStop events; otherwise these can stack up if
// the user goes back to perform another action, for example
$( document ).off( 'ajaxStop' );
} );
}
/**
* Adds handler for when the accept/decline/etc form is submitted
* that calls a given function and passes an object to the function
* containing data from all .afch-input elements in the dom.
*
* Also sets up the viewer for the "processing" stage.
*
* @param {Function} fn function to call with data
* @param {Object} extraData more data to pass; will be inserted
* into the data passed to `fn`
*/
function addFormSubmitHandler( fn, extraData ) {
$afch.find( '#afchSubmitForm' ).on( 'click', () => {
const data = {};
// Provide page text; use cache created after afchSubmission.parse()
afchPage.getText( false ).done( ( text ) => {
data.afchText = new AFCH.Text( text );
// Also provide the values for each afch-input element
$.extend( data, AFCH.getFormValues( $afch.find( '.afch-input' ) ) );
// Also provide extra data
$.extend( data, extraData );
checkForEditConflict().then( ( editConflict ) => {
if ( editConflict ) {
showEditConflictMessage();
return;
}
// Hide the HTML form. Show #afchStatus messages
prepareForProcessing();
// Now finally call the applicable handler
fn( data );
} );
} );
} );
}
/**
* Displays a spinner in the main content area and then
* calls the passed function
*
* @param {Function} fn function to call when spinner has been displayed
*/
function spinnerAndRun( fn ) {
let $spinner, $container = $afch.find( '#afchContent' );
// Add a new spinner if one doesn't already exist
if ( !$container.find( '.mw-spinner' ).length ) {
$spinner = $.createSpinner( {
size: 'large',
type: 'block'
} )
// Set the spinner's dimensions equal to the viewers's dimensions so that
// the current scroll position is not lost when emptied
.css( {
height: $container.height(),
width: $container.width()
} );
$container.empty().append( $spinner );
}
if ( typeof fn === 'function' ) {
fn();
}
}
/**
* Loads a new view
*
* @param {string} name view to be loaded
* @param {Object} data data to populate the view with
* @param {Function} callback function to call when view is loaded
*/
function loadView( name, data, callback ) {
// Show the back button if we're not loading the main view
$afch.find( '.back-link' ).toggleClass( 'hidden', name === 'main' );
afchViewer.loadView( name, data );
if ( callback ) {
callback();
}
}
// These functions show the options before doing something
// to a submission.
function showAcceptOptions() {
/**
* If possible, use the session storage to get the WikiProject list.
* If it hasn't been cached already, load it manually and then cache
*
* @return {jQuery.Deferred}
*/
function loadWikiProjectList() {
let deferred = $.Deferred(),
// Left over from when a new version of AFCH would invalidate the WikiProject cache. The lsKey doesn't change nowadays though.
lsKey = 'mw-afch-wikiprojects-2',
wikiProjects = mw.storage.getObject( lsKey );
if ( wikiProjects ) {
deferred.resolve( wikiProjects );
} else {
wikiProjects = [];
$.ajax( {
url: mw.config.get( 'wgServer' ) + '/w/index.php?title=Wikipedia:WikiProject_Articles_for_creation/WikiProject_templates.json&action=raw&ctype=text/json',
dataType: 'json'
} ).done( ( projectData ) => {
$.each( projectData, ( display, template ) => {
wikiProjects.push( {
displayName: display,
templateName: template
} );
} );
// If possible, cache the WikiProject data!
if ( !mw.storage.setObject( lsKey, wikiProjects, ( 7 * 24 * 60 * 60 ) ) ) {
AFCH.log( 'Unable to cache WikiProject list.' );
}
deferred.resolve( wikiProjects );
} ).fail( ( jqxhr, textStatus, errorThrown ) => {
console.error( 'Could not parse WikiProject list: ', textStatus, errorThrown );
} );
}
return deferred;
}
const existingWikiProjectsPromise = $.when(
loadWikiProjectList(),
new AFCH.Page( 'Draft talk:' + afchSubmission.shortTitle ).getTemplates()
).then( ( wikiProjects, templates ) => {
let templateNames = templates.map( ( template ) => template.target.trim().toLowerCase() );
// Turn the WikiProject list into an Object to make lookups faster
let wikiProjectMap = {};
for ( let projIdx = 0; projIdx < wikiProjects.length; projIdx++ ) {
wikiProjectMap[ wikiProjects[ projIdx ].templateName.toLowerCase() ] = {
displayName: wikiProjects[ projIdx ].displayName,
templateName: wikiProjects[ projIdx ].templateName,
alreadyOnPage: false
};
}
let alreadyHasWPBio = false;
if ( templates.length === 0 ) {
return {
alreadyHasWPBio: alreadyHasWPBio,
wikiProjectMap: wikiProjectMap
};
}
let otherTemplates = [];
for ( let tplIdx = 0; tplIdx < templateNames.length; tplIdx++ ) {
if ( wikiProjectMap.hasOwnProperty( templateNames[ tplIdx ] ) ) {
wikiProjectMap[ templateNames[ tplIdx ] ].alreadyOnPage = true;
} else if ( templateNames[ tplIdx ] === 'wikiproject biography' ) {
alreadyHasWPBio = true;
} else {
otherTemplates.push( templateNames[ tplIdx ] );
}
}
// If any templates weren't in the WikiProject map, check if they were redirects
if ( otherTemplates.length > 0 ) {
let titles = otherTemplates.map( ( n ) => 'Template:' + n );
titles = titles.slice( 0, 50 ); // prevent API error by capping max # of titles at 50
titles = titles.join( '|' );
return AFCH.api.post( {
action: 'query',
titles: titles,
redirects: 'true'
} ).then( ( data ) => {
let existingWPBioTemplateName = null;
if ( data.query && data.query.redirects && data.query.redirects.length > 0 ) {
let redirs = data.query.redirects;
for ( let redirIdx = 0; redirIdx < redirs.length; redirIdx++ ) {
let redir = redirs[ redirIdx ].to.slice( 'Template:'.length ).toLowerCase();
let originalName = redirs[ redirIdx ].from.slice( 'Template:'.length );
if ( wikiProjectMap.hasOwnProperty( redir ) ) {
wikiProjectMap[ redir ].alreadyOnPage = true;
wikiProjectMap[ redir ].realTemplateName = originalName;
} else if ( redir === 'wikiproject biography' ) {
alreadyHasWPBio = true;
existingWPBioTemplateName = originalName;
}
}
}
return {
alreadyHasWPBio: alreadyHasWPBio,
wikiProjectMap: wikiProjectMap,
existingWPBioTemplateName: existingWPBioTemplateName
};
} );
} else {
return {
alreadyHasWPBio: alreadyHasWPBio,
wikiProjectMap: wikiProjectMap
};
}
} );
$.when(
afchPage.getText( false ),
existingWikiProjectsPromise,
afchPage.getCategories( /* useApi */ false, /* includeCategoryLinks */ true ),
afchPage.getShortDescription()
).then( ( pageText, existingWikiProjectsResult, categories, shortDescription ) => {
const alreadyHasWPBio = existingWikiProjectsResult.alreadyHasWPBio,
wikiProjectMap = existingWikiProjectsResult.wikiProjectMap,
existingWPBioTemplateName = existingWikiProjectsResult.existingWPBioTemplateName;
const existingWikiProjects = []; // already on draft's talk page
$.each( wikiProjectMap, ( lowercaseTemplateName, obj ) => {
if ( obj.alreadyOnPage ) {
existingWikiProjects.push( obj );
}
} );
const hasWikiProjects = Object.keys( wikiProjectMap ).length > 0;
if ( !hasWikiProjects ) {
mw.notify( 'Could not load WikiProject list!' );
}
const wikiProjectObjs = Object.keys( wikiProjectMap ).map( ( key ) => wikiProjectMap[ key ] );
loadView( 'accept', {
newTitle: afchSubmission.shortTitle,
hasWikiProjects: hasWikiProjects,
wikiProjects: wikiProjectObjs,
categories: categories,
shortDescription: shortDescription,
// Only offer to patrol the page if not already patrolled (in other words, if
// the "Mark as patrolled" link can be found in the DOM)
showPatrolOption: !!$afch.find( '.patrollink' ).length
}, () => {
$afch.find( '#newAssessment' ).chosen( {
allow_single_deselect: true,
disable_search: true,
width: '140px',
placeholder_text_single: 'Click to select'
} );
// If draft is assessed as stub, show stub sorting
// interface using User:SD0001/StubSorter.js
$afch.find( '#newAssessment' ).on( 'change', function () {
const isClassStub = $( this ).val() === 'Stub';
$afch.find( '#stubSorterWrapper' ).toggleClass( 'hidden', !isClassStub );
if ( isClassStub ) {
if ( mw.config.get( 'wgDBname' ) !== 'enwiki' && mw.config.get( 'wgDBname' ) !== 'testwiki' ) {
console.warn( 'no stub sorting script available for this language wiki' );
return;
}
if ( $afch.find( '#stubSorterContainer' ).html() === '' ) {
mw.hook( 'StubSorter_activate' ).fire( $afch.find( '#stubSorterContainer' ) );
let promise = $.when();
const wasStubSorterActivated = $afch.find( '#stubSorterContainer' ).html() !== '';
if ( !wasStubSorterActivated ) {
promise = mw.loader.getScript( 'https://en.wikipedia.org/w/index.php?title=User:SD0001/StubSorter.js&action=raw&ctype=text/javascript' );
}
promise.then( () => {
if ( !wasStubSorterActivated ) {
mw.hook( 'StubSorter_activate' ).fire( $afch.find( '#stubSorterContainer' ) );
}
$( '#stub_sorter_select_chosen' ).css( 'width', '' );
$( '#stub-sorter-select' ).addClass( 'afch-input' );
if ( /\{\{[^{ ]*[sS]tub(\|.*?)?\}\}\s*/.test( pageText ) ) {
$afch.find( '#newAssessment' ).val( 'Stub' ).trigger( 'chosen:updated' ).trigger( 'change' );
}
} );
}
}
} );
$afch.find( '#newWikiProjects' ).chosen( {
placeholder_text_multiple: 'Start typing to filter WikiProjects...',
no_results_text: 'Whoops, no WikiProjects matched in database!',
width: '350px'
} );
// Extend the chosen menu for new WikiProjects. We hackily show a
// "Click to manually add {{PROJECT}}" link -- sadly, jquery.chosen
// doesn't support this natively.
$afch.find( '#newWikiProjects_chzn input' ).on( 'keyup', function () {
const $chzn = $afch.find( '#newWikiProjects_chzn' ),
$input = $( this ),
newProject = $input.val(),
$noResults = $chzn.find( 'li.no-results' );
// Only show "Add {{PROJECT}}" link if there are no results
if ( $noResults.length ) {
$( '<div>' )
.appendTo( $noResults.empty() )
.text( 'Whoops, no WikiProjects matched in database! ' )
.append(
$( '<a>' )
.text( 'Click to manually add {{' + newProject + '}} to the page\'s WikiProject list.' )
.on( 'click', () => {
const $wikiprojects = $afch.find( '#newWikiProjects' );
$( '<option>' )
.attr( 'value', newProject )
.attr( 'selected', true )
.text( newProject )
.appendTo( $wikiprojects );
$wikiprojects.trigger( 'liszt:updated' );
$input.val( '' );
} )
);
}
} );
$afch.find( '#newCategories' ).chosen( {
placeholder_text_multiple: 'Start typing to add categories...',
width: '350px'
} );
// Offer dynamic category suggestions!
// Since jquery.chosen doesn't natively support dynamic results,
// we sneakily inject some dynamic suggestions instead. Consider
// switching to something like Select2 to avoid this hackery...
$afch.find( '#newCategories_chosen input' ).on( 'keyup', function ( e ) {
const $input = $( this ),
prefix = $input.val(),
$categories = $afch.find( '#newCategories' );
// Ignore up/down keys to allow users to navigate through the suggestions,
// and don't show results when an empty string is provided
if ( [ 38, 40 ].indexOf( e.which ) !== -1 || !prefix ) {
return;
}
// The worst hack. Because Chosen keeps messing with the
// width of the text box, keep on resetting it to 100%
$input.css( 'width', '100%' );
$input.parent().css( 'width', '100%' );
AFCH.api.getCategoriesByPrefix( prefix ).done( ( categories ) => {
// Reset the text box width again
$input.css( 'width', '100%' );
$input.parent().css( 'width', '100%' );
// If the input has changed since we started searching,
// don't show outdated results
if ( $input.val() !== prefix ) {
return;
}
// Clear existing suggestions
$categories.children().not( ':selected' ).remove();
// Now, add the new suggestions
$.each( categories, ( _, category ) => {
$( '<option>' )
.attr( 'value', category )
.text( category )
.appendTo( $categories );
} );
// We've changed the <select>, now tell Chosen to
// rebuild the visible list
$categories.trigger( 'liszt:updated' );
$categories.trigger( 'chosen:updated' );
$input.val( prefix );
$input.css( 'width', '100%' );
$input.parent().css( 'width', '100%' );
} );
} );
// Show bio options if Biography option checked
$afch.find( '#isBiography' ).on( 'change', function () {
$afch.find( '#bioOptionsWrapper' ).toggleClass( 'hidden', !this.checked );
} );
if ( alreadyHasWPBio ) {
$afch.find( '#isBiography' ).prop( 'checked', true ).trigger( 'change' );
}
function prefillBiographyDetails() {
let titleParts;
// Prefill `LastName, FirstName` for Biography if the page title is two words
// after removing any trailing parentheticals (likely disambiguation), and
// therefore probably safe to asssume in a `FirstName LastName` format.
const title = afchSubmission.shortTitle.replace( / \([\s\S]*?\)$/g, '' );
titleParts = title.split( ' ' );
if ( titleParts.length === 2 ) {
$afch.find( '#subjectName' ).val( titleParts[ 1 ] + ', ' + titleParts[ 0 ] );
}
}
prefillBiographyDetails();
// If subject is dead, show options for death details
$afch.find( '#lifeStatus' ).on( 'change', function () {
$afch.find( '#deathWrapper' ).toggleClass( 'hidden', $( this ).val() !== 'dead' );
} );
// Show an error if the page title already exists in the mainspace,
// or if the title is create-protected and user is not an admin
$afch.find( '#newTitle' ).on( 'keyup', function () {
let page,
linkToPage,
$field = $( this ),
$status = $afch.find( '#titleStatus' ),
$submitButton = $afch.find( '#afchSubmitForm' ),
value = $field.val();
// Reset to a pure state
$field.removeClass( 'bad-input' );
$status.text( '' );
$submitButton
.removeClass( 'disabled' )
.css( 'pointer-events', 'auto' )
.text( 'Accept & publish' );
// If there is no value, die now, because otherwise mw.Title
// will throw an exception due to an invalid title
if ( !value ) {
return;
}
page = new AFCH.Page( value );
linkToPage = AFCH.jQueryToHtml( AFCH.makeLinkElementToPage( page.rawTitle ) );
AFCH.api.get( {
action: 'query',
titles: 'Talk:' + page.rawTitle
} ).done( ( data ) => {
if ( !data.query.pages.hasOwnProperty( '-1' ) ) {
$status.html( 'The talk page for "' + linkToPage + '" exists.' );
}
} );
$.when(
AFCH.api.get( {
action: 'titleblacklist',
tbtitle: page.rawTitle,
tbaction: 'create',
tbnooverride: true
} ),
AFCH.api.get( {
action: 'query',
prop: 'info',
inprop: 'protection',
titles: page.rawTitle
} )
).then( ( rawBlacklist, rawInfo ) => {
let errorHtml, buttonText;
// Get just the result, not the Promise object
let blacklistResult = rawBlacklist[ 0 ],
infoResult = rawInfo[ 0 ];
const pageAlreadyExists = !infoResult.query.pages.hasOwnProperty( '-1' );
const pages = infoResult && infoResult.query && infoResult.query.pages && infoResult.query.pages;
const firstPageInObject = Object.values( pages )[ 0 ];
const pageIsRedirect = firstPageInObject && ( 'redirect' in firstPageInObject );
if ( pageAlreadyExists && pageIsRedirect ) {
const linkToRedirect = AFCH.jQueryToHtml( AFCH.makeLinkElementToPage( page.rawTitle, null, null, true ) );
errorHtml = '<br />Whoops, the page "' + linkToRedirect + '" already exists and is a redirect. <span id="afch-redirect-notification">Do you want to tag it for speedy deletion so you can accept this draft later? <a id="afch-redirect-tag-speedy">Yes</a> / <a id="afch-redirect-abort">No</a></span>';
buttonText = 'The proposed title already exists';
} else if ( pageAlreadyExists ) {
errorHtml = 'Whoops, the page "' + linkToPage + '" already exists.';
buttonText = 'The proposed title already exists';
} else {
// If the page doesn't exist but IS create-protected and the
// current reviewer is not an admin, also display an error
// FIXME: offer one-click request unprotection?
$.each( infoResult.query.pages[ '-1' ].protection, ( _, entry ) => {
if ( entry.type === 'create' && entry.level === 'sysop' && $.inArray( 'sysop', mw.config.get( 'wgUserGroups' ) ) === -1 ) {
const linkToRfup = AFCH.jQueryToHtml( AFCH.makeLinkElementToPage( 'WP:RFUP' ) );
errorHtml = 'Darn it, "' + linkToPage + '" is create-protected. You will need to request unprotection from the protecting admin on their talk page or at ' + linkToRfup + '.';
buttonText = 'The proposed title is create-protected';
}
} );
}
// Now check the blacklist result, but if another error already exists,
// don't bother showing this one too
blacklistResult = blacklistResult.titleblacklist;
if ( !errorHtml && blacklistResult.result === 'blacklisted' ) {
errorHtml = 'Shoot! ' + blacklistResult.reason.replace( /\s+/g, ' ' );
buttonText = 'The proposed title is blacklisted';
}
if ( !errorHtml ) {
return;
}
// Add a red border around the input field
$field.addClass( 'bad-input' );
// Show the error message
$status.html( errorHtml );
// Add listener for the "Do you want to tag it for speedy deletion so you can accept this draft later?" "yes" link.
$( '#afch-redirect-tag-speedy' ).on( 'click', () => {
handleAcceptOverRedirect( page.rawTitle );
} );
// Add listener for the "Do you want to tag it for speedy deletion so you can accept this draft later?" "no" link.
$( '#afch-redirect-abort' ).on( 'click', () => {
$( '#afch-redirect-notification' ).hide();
} );
// Disable the submit button and show an error in its place
$submitButton
.addClass( 'disabled' )
.css( 'pointer-events', 'none' )
.text( buttonText );
} );
} );
// Update titleStatus
$afch.find( '#newTitle' ).trigger( 'keyup' );
} );
addFormSubmitHandler( handleAccept, {
existingWikiProjects: existingWikiProjects,
alreadyHasWPBio: alreadyHasWPBio,
existingWPBioTemplateName: existingWPBioTemplateName,
existingShortDescription: shortDescription
} );
} );
}
function showDeclineOptions() {
loadView( 'decline', {}, () => {
let $reasons, $commonSection, declineCounts,
pristineState = $afch.find( '#declineInputWrapper' ).html();
// pos is either 1 or 2, based on whether the chosen reason that
// is triggering this update is first or second in the multi-select
// control
function updateTextfield( newPrompt, newPlaceholder, newValue, pos ) {
const $wrapper = $afch.find( '#textfieldWrapper' + ( pos === 2 ? '2' : '' ) );
// Update label and placeholder
$wrapper.find( 'label' ).text( newPrompt );
$wrapper.find( 'input' ).attr( 'placeholder', newPlaceholder );
// Update default textfield value (perhaps)
if ( typeof newValue !== 'undefined' ) {
$wrapper.find( 'input' ).val( newValue );
}
// And finally show the textfield
$wrapper.removeClass( 'hidden' );
}
// Copy most-used options to top of decline dropdown
declineCounts = AFCH.userData.get( 'decline-counts', false );
if ( declineCounts ) {
const declineList = $.map( declineCounts, ( _, key ) => key );
// Sort list in descending order (most-used at beginning)
declineList.sort( ( a, b ) => declineCounts[ b ] - declineCounts[ a ] );
$reasons = $afch.find( '#declineReason' );
$commonSection = $( '<optgroup>' )
.attr( 'label', 'Frequently used' )
.insertBefore( $reasons.find( 'optgroup' ).first() );
// Show the 5 most used options
$.each( declineList.splice( 0, 5 ), ( _, rationale ) => {
const $relevant = $reasons.find( 'option[value="' + rationale + '"]' );
$relevant.clone( true ).appendTo( $commonSection );
} );
}
// Set up jquery.chosen for the decline reason
$afch.find( '#declineReason' ).chosen( {
placeholder_text_multiple: 'Select one or more decline reasons...',
no_results_text: 'Whoops, no reasons matched your search. Type "custom" to add a custom rationale instead.',
search_contains: true,
inherit_select_classes: true,
max_selected_options: 2
} );
// Set up jquery.chosen for the reject reason
$afch.find( '#rejectReason' ).chosen( {
placeholder_text_multiple: 'Select one or more reject reasons...',
search_contains: true,
inherit_select_classes: true,
max_selected_options: 2
} );
// rejectReason starts off hidden by default, which makes the _chosen div
// display at 0px wide for some reason. We must manually fix this.
$afch.find( '#rejectReason_chosen' ).css( 'width', '350px' );
// And now add the handlers for when a specific decline reason is selected
$afch.find( '#declineReason' ).on( 'change', () => {
const reason = $afch.find( '#declineReason' ).val(),
candidateDupeName = ( afchSubmission.shortTitle !== 'sandbox' ) ? afchSubmission.shortTitle : '',
prevDeclineComment = $afch.find( '#declineTextarea' ).val(),
declineHandlers = {
cv: function () {
$afch.find( '#cvUrlWrapper' ).removeClass( 'hidden' );
$afch.add( '#csdWrapper' ).removeClass( 'hidden' );
$afch.find( '#cvUrlTextarea' ).on( 'keyup', function () {
let text = $( this ).val(),
numUrls = text ? text.split( '\n' ).length : 0,
$submitButton = $afch.find( '#afchSubmitForm' );
if ( numUrls >= 1 && numUrls <= 3 ) {
$( this ).removeClass( 'bad-input' );
$submitButton
.removeClass( 'disabled' )
.css( 'pointer-events', 'auto' )
.text( 'Decline submission' );
} else {
$( this ).addClass( 'bad-input' );
$submitButton
.addClass( 'disabled' )
.css( 'pointer-events', 'none' )
.text( 'Please enter between one and three URLs!' );
}
} );
// Check if there's an OTRS notice
new AFCH.Page( 'Draft talk:' + afchSubmission.shortTitle ).getText( /* usecache */ false ).done( ( text ) => {
if ( /ConfirmationOTRS/.test( text ) ) {
$afch.find( '#declineInputWrapper' ).append(
$( '<div>' )
.addClass( 'warnings' )
.css( {
'max-width': '50%',
margin: '0px auto'
} )
.text( 'This draft has an OTRS template on the talk page. Verify that the copyright violation isn\'t covered by the template before marking this draft as a copyright violation.' ) );
}
} );
},
dup: function ( pos ) {
updateTextfield( 'Title of duplicate submission (no namespace)', 'Articles for creation/Fudge', candidateDupeName, pos );
},
mergeto: function ( pos ) {
updateTextfield( 'Article which submission should be merged into', 'Milkshake', candidateDupeName, pos );
},
lang: function ( pos ) {
updateTextfield( 'Language of the submission if known', 'German', '', pos );
},
exists: function ( pos ) {
updateTextfield( 'Title of existing article', 'Chocolate chip cookie', candidateDupeName, pos );
},
plot: function ( pos ) {
updateTextfield( 'Title of existing related article, if one exists', 'Charlie and the Chocolate Factory', candidateDupeName, pos );
},
// Custom decline rationale
reason: function () {
$afch.find( '#declineTextarea' )
.attr( 'placeholder', 'Enter your decline reason here using wikicode syntax.' );
}
};
// Reset to a pristine state :)
$afch.find( '#declineInputWrapper' ).html( pristineState );
// If there are special options to be displayed for each
// particular decline reason, load them now
if ( declineHandlers[ reason[ 0 ] ] ) {
declineHandlers[ reason[ 0 ] ]( 1 );
}
if ( declineHandlers[ reason[ 1 ] ] ) {
declineHandlers[ reason[ 1 ] ]( 2 );
}
// Preserve the custom comment text
$afch.find( '#declineTextarea' ).val( prevDeclineComment );
// If the user wants a preview, show it
if ( $( '#previewTrigger' ).text() == '(hide preview)' ) {
$( '#previewContainer' )
.empty()
.append( $.createSpinner( {
size: 'large',
type: 'block'
} ).css( 'padding', '20px' ) );
AFCH.getReason( reason ).done( ( html ) => {
$( '#previewContainer' ).html( html );
} );
}
// If a reason has been specified, show the textarea, notify
// option, and the submit form button
$afch.find( '#declineTextarea' ).add( '#declineNotifyWrapper' ).add( '#afchSubmitForm' )
.toggleClass( 'hidden', !reason || !reason.length )
.on( 'keyup', mw.util.debounce( 500, () => {
previewComment( $( '#declineTextarea' ), $( '#declineInputPreview' ) );
} ) );
} ); // End change handler for the decline reason select box
// And the the handlers for when a specific REJECT reason is selected
$afch.find( '#rejectReason' ).on( 'change', () => {
const reason = $afch.find( '#rejectReason' ).val();
// If a reason has been specified, show the textarea, notify
// option, and the submit form button
$afch.find( '#rejectTextarea' ).add( '#rejectNotifyWrapper' ).add( '#afchSubmitForm' )
.toggleClass( 'hidden', !reason || !reason.length )
.on( 'keyup', mw.util.debounce( 500, () => {
previewComment( $( '#rejectTextarea' ), $( '#rejectInputPreview' ) );
} ) );
} ); // End change handler for the reject reason select box
// Attach the preview event listener
$afch.find( '#previewTrigger' ).on( 'click', function () {
const reason = $afch.find( '#declineReason' ).val();
if ( this.textContent == '(preview)' && reason ) {
$( '#previewContainer' )
.empty()
.append( $.createSpinner( {
size: 'large',
type: 'block'
} ).css( 'padding', '20px' ) );
const reasonDeferreds = reason.map( AFCH.getReason );
$.when.apply( $, reasonDeferreds ).then( function () {
$( '#previewContainer' )
.html( Array.prototype.slice.call( arguments )
.join( '<hr />' ) );
} );
this.textContent = '(hide preview)';
} else {
$( '#previewContainer' ).empty();
this.textContent = '(preview)';
}
} );
// Attach the decline vs reject radio button listener
$afch.find( 'input[type=radio][name=declineReject]' ).on( 'click', () => {
const declineOrReject = $afch.find( 'input[name=declineReject]:checked' ).val();
$afch.find( '#declineReasonWrapper' ).toggleClass( 'hidden', declineOrReject === 'reject' );
$afch.find( '#rejectReasonWrapper' ).toggleClass( 'hidden', declineOrReject === 'decline' );
$afch.find( '#declineInputWrapper' ).toggleClass( 'hidden', declineOrReject === 'reject' );
$afch.find( '#rejectInputWrapper' ).toggleClass( 'hidden', declineOrReject === 'decline' );
$afch.find( '#declineNotifyWrapper' ).toggleClass( 'hidden2', declineOrReject === 'reject' );
$afch.find( '#rejectNotifyWrapper' ).toggleClass( 'hidden2', declineOrReject === 'decline' );
} );
} ); // End loadView callback
addFormSubmitHandler( handleDecline );
}
function addSignature( text ) {
text = text.trim();
if ( text.indexOf( '~~~~' ) === -1 ) {
text += ' ~~~~';
}
return text;
}
function previewComment( $textarea, $previewArea ) {
const commentText = $textarea.val();
if ( commentText ) {
AFCH.api.parse( '{{AfC comment|1=' + addSignature( commentText ) + '}}', {
pst: true,
title: mw.config.get( 'wgPageName' )
} ).then( ( html ) => {
$previewArea.html( html );
} );
} else {
$previewArea.html( '' );
}
}
function checkIfUserIsBlocked( userName ) {
return AFCH.api.get( {
action: 'query',
list: 'blocks',
bkusers: userName
} ).then( ( data ) => {
const blocks = data.query.blocks;
let blockData = null;
const currentTime = new Date().toISOString();
for ( let i = 0; i < blocks.length; i++ ) {
if ( blocks[ i ].expiry === 'infinity' || blocks[ i ].expiry > currentTime ) {
blockData = blocks[ i ];
break;
}
}
return blockData;
} ).catch( ( err ) => {
console.log( 'abort ' + err );
return null;
} );
}
function showCommentOptions() {
loadView( 'comment', {} );
const $submitButton = $( '#afchSubmitForm' );
$submitButton.hide();
$( '#commentText' ).on( 'keyup', mw.util.debounce( 500, () => {
previewComment( $( '#commentText' ), $( '#commentPreview' ) );
// Hide the submit button if there is no comment typed in
const comment = $( '#commentText' ).val();
if ( comment.length > 0 ) {
$submitButton.show();
} else {
$submitButton.hide();
}
} ) );
addFormSubmitHandler( handleComment );
}
function showSubmitOptions() {
const customSubmitters = [];
// Iterate over the submitters and add them to the custom submitters list,
// displayed in the "submit as" dropdown.
$.each( afchSubmission.submitters, ( index, submitter ) => {
customSubmitters.push( {
name: submitter,
description: submitter + ( index === 0 ? ' (most recent submitter)' : ' (past submitter)' ),
selected: index === 0
} );
} );
loadView( 'submit', {
customSubmitters: customSubmitters
}, () => {
// Reset the status indicators for the username & errors
function resetStatus() {
$afch.find( '#submitterName' ).removeClass( 'bad-input' );
$afch.find( '#submitterNameStatus' ).text( '' );
$afch.find( '#afchSubmitForm' )
.removeClass( 'disabled' )
.css( 'pointer-events', 'auto' )
.text( 'Submit' );
}
// Show the other textbox when `other` is selected in the menu
$afch.find( '#submitType' ).on( 'change', () => {
const isOtherSelected = $afch.find( '#submitType' ).val() === 'other';
if ( isOtherSelected ) {
$afch.find( '#submitterNameWrapper' ).removeClass( 'hidden' );
$afch.find( '#submitterName' ).trigger( 'keyup' );
} else {
$afch.find( '#submitterNameWrapper' ).addClass( 'hidden' );
}
resetStatus();
// Show an error if there's no such user
$afch.find( '#submitterName' ).on( 'keyup', function () {
const $field = $( this ),
$status = $( '#submitterNameStatus' ),
$submitButton = $afch.find( '#afchSubmitForm' ),
submitter = $field.val();
// Reset form
resetStatus();
// If there's no value, don't even try
if ( !submitter || !isOtherSelected ) {
return;
}
// Check if the user string starts with "User:", because Template:AFC submission dies horribly if it does
if ( submitter.lastIndexOf( 'User:', 0 ) === 0 ) {
$field.addClass( 'bad-input' );
$status.text( 'Remove "User:" from the beginning.' );
$submitButton
.addClass( 'disabled' )
.css( 'pointer-events', 'none' )
.text( 'Invalid user name' );
return;
}
// Check if there is such a user
AFCH.api.get( {
action: 'query',
list: 'users',
ususers: submitter
} ).done( ( data ) => {
if ( data.query.users[ 0 ].missing !== undefined ) {
$field.addClass( 'bad-input' );
$status.text( 'No user named "' + submitter + '".' );
$submitButton
.addClass( 'disabled' )
.css( 'pointer-events', 'none' )
.text( 'No such user' );
}
} );
} );
} );
} );
addFormSubmitHandler( handleSubmit );
}
function showPostponeG13Options() {
loadView( 'postpone-g13', {} );
addFormSubmitHandler( handlePostponeG13 );
}
// These functions perform a given action using data passed in the `data` parameter.
function handleAcceptOverRedirect( destinationPageTitle ) {
// get rid of the accept form. replace it with the status div.
prepareForProcessing();
// Add {{Db-afc-move}} speedy deletion tag to redirect, and add to watchlist
( new AFCH.Page( destinationPageTitle ) ).edit( {
contents: '{{Db-afc-move|' + afchPage.rawTitle + '}}\n\n',
mode: 'prependtext',
summary: 'Requesting speedy deletion ([[Wikipedia:CSD#G6|CSD G6]]).',
statusText: 'Tagging',
watchlist: 'watch'
} );
// Mark the draft as under review.
afchPage.getText( false ).then( ( rawText ) => {
const text = new AFCH.Text( rawText );
afchSubmission.setStatus( 'r', {
reviewer: AFCH.consts.user,
reviewts: '{{subst:REVISIONTIMESTAMP}}'
} );
text.updateAfcTemplates( afchSubmission.makeWikicode() );
text.cleanUp();
afchPage.edit( {
contents: text.get(),
summary: 'Marking submission as under review',
statusText: 'Marking as under review'
} );
} );
}
function handleAccept( data ) {
let newText = data.afchText;
AFCH.actions.movePage( afchPage.rawTitle, data.newTitle,
'Publishing accepted [[Wikipedia:Articles for creation|Articles for creation]] submission',
{ movetalk: true } ) // Also move associated talk page if exists (e.g. `Draft_talk:`)
.done( ( moveData ) => {
let $patrolLink,
newPage = new AFCH.Page( moveData.to ),
talkPage = newPage.getTalkPage(),
recentPage = new AFCH.Page( 'Wikipedia:Articles for creation/recent' );
// ARTICLE
// -------
// get comments left by reviewers to put on talk page
let comments = [];
if ( data.copyComments ) {
comments = newText.getAfcComments();
}
newText.removeAfcTemplates();
newText.updateCategories( data.newCategories );
newText.updateShortDescription( data.existingShortDescription, data.shortDescription );
// Clean the page
newText.cleanUp( /* isAccept */ true );
// Add biography details
if ( data.isBiography ) {
let deathYear = 'LIVING';
if ( data.lifeStatus === 'dead' ) {
deathYear = data.deathYear || 'MISSING';
} else if ( data.lifeStatus === 'unknown' ) {
deathYear = 'UNKNOWN';
}
// {{subst:L}}, which generates DEFAULTSORT as well as
// adds the appropriate birth/death year categories
newText.append( '\n{{subst:L' +
'|1=' + data.birthYear +
'|2=' + deathYear +
'|3=' + data.subjectName + '}}'
);
}
// Stub sorting
newText = newText.get();
if ( typeof window.StubSorter_create_edit === 'function' ) {
newText = window.StubSorter_create_edit( newText, data[ 'stub-sorter-select' ] || [] ).text;
}
newPage.edit( {
contents: newText,
summary: 'Cleaning up accepted [[Wikipedia:Articles for creation|Articles for creation]] submission'
} );
// Patrol the new page if desired
if ( data.patrolPage ) {
$patrolLink = $afch.find( '.patrollink' );
if ( $patrolLink.length ) {
AFCH.actions.patrolRcid(
mw.util.getParamValue( 'rcid', $patrolLink.find( 'a' ).attr( 'href' ) ),
newPage.rawTitle // Include the title for a prettier log message
);
}
}
// TALK PAGE
// ---------
talkPage.getText().done( ( talkText ) => {
talkText = AFCH.addTalkPageBanners(
talkText,
data.newAssessment,
afchPage.additionalData.revId,
data.isBiography,
data.newWikiProjects,
data.lifeStatus,
data.subjectName
);
const summary = 'Placing WikiProject banners';
if ( comments && comments.length > 0 ) {
talkText = talkText.trim() + '\n\n== Comments left by AfC reviewers ==\n' + comments.join( '\n\n' );
}
talkPage.edit( {
contents: talkText,
summary: summary
} );
} );
// NOTIFY SUBMITTER
// ----------------
if ( data.notifyUser ) {
afchSubmission.getSubmitter().done( ( submitter ) => {
AFCH.actions.notifyUser( submitter, {
message: AFCH.msg.get( 'accepted-submission',
{ $1: newPage, $2: data.newAssessment } ),
summary: 'Notification: Your [[' + AFCH.consts.pagename + '|Articles for Creation submission]] has been accepted'
} );
} );
}
// AFC/RECENT
// ----------
const reviewer = AFCH.consts.user;
$.when( recentPage.getText(), afchSubmission.getSubmitter() )
.then( ( text, submitter ) => {
let newRecentText = text,
matches = text.match( /{{afc contrib.*?}}\s*/gi ),
newTemplate = '{{afc contrib|' + data.newAssessment + '|' + newPage + '|' + submitter + '|' + reviewer + '}}\n';
// Remove the older entries (at bottom of the page) if necessary
// to ensure we keep only 10 entries at any given point in time
while ( matches.length >= 10 ) {
newRecentText = newRecentText.replace( matches.pop(), '' );
}
newRecentText = newTemplate + newRecentText;
recentPage.edit( {
contents: newRecentText,
summary: 'Adding [[' + newPage + ']] to list of recent AfC creations',
watchlist: 'nochange'
} );
} );
// LOG TO USERSPACE
// ----------
afchSubmission.getSubmitter().done( ( submitter ) => {
AFCH.actions.logAfc( {
title: afchPage.rawTitle,
actionType: 'accept',
submitter: submitter
} );
} );
} );
}
function handleDecline( data ) {
let declineCounts,
isDecline = data.declineRejectWrapper === 'decline', // true=decline, false=reject
text = data.afchText,
declineReason = data.declineReason[ 0 ],
declineReason2 = data.declineReason.length > 1 ? data.declineReason[ 1 ] : null,
newParams = {
decliner: AFCH.consts.user,
declinets: '{{subst:REVISIONTIMESTAMP}}'
};
if ( isDecline ) {
newParams[ '2' ] = declineReason;
// If there's a second reason, add it to the params
if ( declineReason2 ) {
newParams.reason2 = declineReason2;
}
} else {
newParams[ '2' ] = data.rejectReason[ 0 ];
if ( data.rejectReason[ 1 ] ) {
newParams.reason2 = data.rejectReason[ 1 ];
}
}
// Update decline counts
declineCounts = AFCH.userData.get( 'decline-counts', {} );
declineCounts[ declineReason ] = ( declineCounts[ declineReason ] || 1 ) + 1;
if ( declineReason2 ) {
declineCounts[ declineReason2 ] = ( declineCounts[ declineReason2 ] || 1 ) + 1;
}
AFCH.userData.set( 'decline-counts', declineCounts );
// If the first reason is a custom decline, we include the declineTextarea in the {{AFC submission}} template
if ( declineReason === 'reason' ) {
newParams[ '3' ] = data.declineTextarea;
} else if ( declineReason2 === 'reason' ) {
newParams.details2 = data.declineTextarea;
} else if ( isDecline && data.declineTextarea ) {
// But otherwise if addtional text has been entered we just add it as a new comment
afchSubmission.addNewComment( data.declineTextarea );
}
// If a user has entered something in the declineTextfield (for example, a URL or an
// associated page), pass that as the third parameter...
if ( data.declineTextfield ) {
newParams[ '3' ] = data.declineTextfield;
}
// ...and do the same with the second decline text field
if ( data.declineTextfield2 ) {
newParams.details2 = data.declineTextfield2;
}
// If we're rejecting, any text in the text area is a comment
if ( !isDecline && data.rejectTextarea ) {
afchSubmission.addNewComment( data.rejectTextarea );
}
// Copyright violations get {{db-g12}}'d as well
if ( declineReason === 'cv' || declineReason2 === 'cv' ) {
let cvUrls = data.cvUrlTextarea.split( '\n' ).slice( 0, 3 ),
urlParam = '';
if ( data.csdSubmission ) {
// Build url param for db-g12 template
urlParam = cvUrls[ 0 ];
if ( cvUrls.length > 1 ) {
urlParam += '|url2=' + cvUrls[ 1 ];
if ( cvUrls.length > 2 ) {
urlParam += '|url3=' + cvUrls[ 2 ];
}
}
text.prepend( '{{db-g12|url=' + urlParam + ( afchPage.additionalData.revId ? '|oldid=' + afchPage.additionalData.revId : '' ) + '}}\n' );
}
// Include the URLs in the decline template
if ( declineReason === 'cv' ) {
newParams[ '3' ] = cvUrls.join( ', ' );
} else {
newParams.details2 = cvUrls.join( ', ' );
}
}
if ( !isDecline ) {
newParams.reject = 'yes';
}
// Now update the submission status
afchSubmission.setStatus( 'd', newParams );
text.updateAfcTemplates( afchSubmission.makeWikicode() );
text.cleanUp();
// Build edit summary
let editSummary = ( isDecline ? 'Declining' : 'Rejecting' ) + ' submission: ',
lengthLimit = declineReason2 ? 120 : 180;
if ( declineReason === 'reason' ) {
// If this is a custom decline, use the text in the edit summary
editSummary += data.declineTextarea.substring( 0, lengthLimit );
// If we had to trunucate, indicate that
if ( data.declineTextarea.length > lengthLimit ) {
editSummary += '...';
}
} else {
editSummary += isDecline ? data.declineReasonTexts[ 0 ] : data.rejectReasonTexts[ 0 ];
}
if ( declineReason2 ) {
editSummary += ' and ';
if ( declineReason2 === 'reason' ) {
editSummary += data.declineTextarea.substring( 0, lengthLimit );
if ( data.declineTextarea.length > lengthLimit ) {
editSummary += '...';
}
} else {
editSummary += data.declineReasonTexts[ 1 ];
}
}
afchPage.edit( {
contents: text.get(),
summary: editSummary
} );
if ( data.notifyUser ) {
afchSubmission.getSubmitter().done( ( submitter ) => {
const userTalk = new AFCH.Page( ( new mw.Title( submitter, 3 ) ).getPrefixedText() ),
shouldTeahouse = data.inviteToTeahouse && isDecline ? $.Deferred() : false;
// Check categories on the page to ensure that if the user has already been
// invited to the Teahouse, we don't invite them again.
if ( data.inviteToTeahouse ) {
userTalk.getCategories( /* useApi */ true ).done( ( categories ) => {
let hasTeahouseCat = false,
teahouseCategories = [
'Category:Wikipedians who have received a Teahouse invitation',
'Category:Wikipedians who have received a Teahouse invitation through AfC'
];
$.each( categories, ( _, cat ) => {
if ( teahouseCategories.indexOf( cat ) !== -1 ) {
hasTeahouseCat = true;
return false;
}
} );
shouldTeahouse.resolve( !hasTeahouseCat );
} );
}
$.when( shouldTeahouse ).then( ( teahouse ) => {
let message;
if ( isDecline ) {
message = AFCH.msg.get( 'declined-submission', {
$1: AFCH.consts.pagename,
$2: afchSubmission.shortTitle,
$3: ( declineReason === 'cv' || declineReason2 === 'cv' ) ?
'yes' : 'no',
$4: declineReason,
$5: newParams[ '3' ] || '',
$6: declineReason2 || '',
$7: newParams.details2 || '',
$8: ( declineReason === 'reason' || declineReason2 === 'reason' ) ?
'' : data.declineTextarea
} );
} else {
message = AFCH.msg.get( 'rejected-submission', {
$1: AFCH.consts.pagename,
$2: afchSubmission.shortTitle,
$3: data.rejectReason[ 0 ],
$4: '',
$5: data.rejectReason[ 1 ] || '',
$6: '',
$7: data.rejectTextarea
} );
}
if ( teahouse ) {
message += '\n\n' + AFCH.msg.get( 'teahouse-invite' );
}
AFCH.actions.notifyUser( submitter, {
message: message,
summary: 'Notification: Your [[' + AFCH.consts.pagename + '|Articles for Creation submission]] has been ' + ( isDecline ? 'declined' : 'rejected' )
} );
} );
} );
}
// Log AfC if enabled and CSD if necessary
afchSubmission.getSubmitter().done( ( submitter ) => {
AFCH.actions.logAfc( {
title: afchPage.rawTitle,
actionType: isDecline ? 'decline' : 'reject',
declineReason: declineReason,
declineReason2: declineReason2,
submitter: submitter
} );
if ( data.csdSubmission ) {
AFCH.actions.logCSD( {
title: afchPage.rawTitle,
reason: declineReason === 'cv' ? '[[WP:G12]] ({{tl|db-copyvio}})' :
'{{tl|db-reason}} ([[WP:AFC|Articles for creation]])',
usersNotified: data.notifyUser ? [ submitter ] : []
} );
}
} );
}
function checkForEditConflict() {
// Get timestamp of the revision currently loaded in the browser
return AFCH.api.get( {
action: 'query',
format: 'json',
prop: 'revisions',
revids: mw.config.get( 'wgCurRevisionId' ),
formatversion: 2
} ).then( ( data ) => {
// convert timestamp format from 2024-05-03T09:40:20Z to 1714729221
const currentRevisionTimestampTZ = data.query.pages[ 0 ].revisions[ 0 ].timestamp;
let currentRevisionSeconds = ( new Date( currentRevisionTimestampTZ ).getTime() ) / 1000;
// add one second. we don't want the current revision to be in our list of revisions
currentRevisionSeconds++;
// Then get all revisions since that timestamp
return AFCH.api.get( {
action: 'query',
format: 'json',
prop: 'revisions',
titles: [ mw.config.get( 'wgPageName' ) ],
formatversion: 2,
rvstart: currentRevisionSeconds,
rvdir: 'newer'
} ).then( ( data ) => {
const revisionsSinceTimestamp = data.query.pages[ 0 ].revisions;
if ( revisionsSinceTimestamp && revisionsSinceTimestamp.length > 0 ) {
return true;
}
return false;
} );
} );
}
function showEditConflictMessage() {
$( '#afchSubmitForm' ).hide();
// Putting this here instead of in tpl-submissions.html to reduce code duplication
const editConflictHtml = 'Edit conflict! Your changes were not saved. Please check the <a id="afchHistoryLink" href="">page history</a>. To avoid overwriting the other person\'s edits, please refresh this page and start again.';
$( '#afchEditConflict' ).html( editConflictHtml );
const historyLink = new mw.Uri( mw.util.getUrl( mw.config.get( 'wgPageName' ), { action: 'history' } ) );
$( '#afchHistoryLink' ).prop( 'href', historyLink );
$( '#afchEditConflict' ).show();
}
function handleComment( data ) {
const text = data.afchText;
afchSubmission.addNewComment( data.commentText );
text.updateAfcTemplates( afchSubmission.makeWikicode() );
text.cleanUp();
afchPage.edit( {
contents: text.get(),
summary: 'Commenting on submission'
} );
if ( data.notifyUser ) {
afchSubmission.getSubmitter().done( ( submitter ) => {
AFCH.actions.notifyUser( submitter, {
message: AFCH.msg.get( 'comment-on-submission',
{ $1: AFCH.consts.pagename } ),
summary: 'Notification: I\'ve commented on [[' + AFCH.consts.pagename + '|your Articles for Creation submission]]'
} );
} );
}
}
function handleSubmit( data ) {
const text = data.afchText,
submitter = $.Deferred(),
submitType = data.submitType;
if ( submitType === 'other' ) {
submitter.resolve( data.submitterName );
} else if ( submitType === 'self' ) {
submitter.resolve( AFCH.consts.user );
} else if ( submitType === 'creator' ) {
afchPage.getCreator().done( ( user ) => {
submitter.resolve( user );
} );
} else {
// Custom selected submitter
submitter.resolve( data.submitType );
}
submitter.done( ( submitter ) => {
afchSubmission.setStatus( '', { u: submitter } );
text.updateAfcTemplates( afchSubmission.makeWikicode() );
text.cleanUp();
afchPage.edit( {
contents: text.get(),
summary: 'Submitting'
} );
} );
}
function handleCleanup() {
prepareForProcessing( 'Cleaning' );
afchPage.getText( false ).done( ( rawText ) => {
const text = new AFCH.Text( rawText );
// Even though we didn't modify them, still update the templates,
// because the order may have changed/been corrected
text.updateAfcTemplates( afchSubmission.makeWikicode() );
text.cleanUp();
afchPage.edit( {
contents: text.get(),
minor: true,
summary: 'Cleaning up submission'
} );
} );
}
function handleMark( unmark ) {
const actionText = ( unmark ? 'Unmarking' : 'Marking' );
prepareForProcessing( actionText, 'mark' );
afchPage.getText( false ).done( ( rawText ) => {
const text = new AFCH.Text( rawText );
if ( unmark ) {
afchSubmission.setStatus( '', { reviewer: false, reviewts: false } );
} else {
afchSubmission.setStatus( 'r', {
reviewer: AFCH.consts.user,
reviewts: '{{subst:REVISIONTIMESTAMP}}'
} );
}
text.updateAfcTemplates( afchSubmission.makeWikicode() );
text.cleanUp();
afchPage.edit( {
contents: text.get(),
summary: actionText + ' submission as under review'
} );
} );
}
function handleG13() {
// We start getting the creator now (for notification later) because ajax is
// radical and handles simultaneous requests, but we don't let it delay tagging
const gotCreator = afchPage.getCreator();
// Update the display
prepareForProcessing( 'Requesting', 'g13' );
// Get the page text and the last modified date (cached!) and tag the page
$.when(
afchPage.getText( false ),
afchPage.getLastModifiedDate()
).then( ( rawText, lastModified ) => {
const text = new AFCH.Text( rawText );
// Add the deletion tag and clean up for good measure
text.prepend( '{{db-g13|ts=' + AFCH.dateToMwTimestamp( lastModified ) + '}}\n' );
text.cleanUp();
afchPage.edit( {
contents: text.get(),
summary: 'Tagging abandoned [[Wikipedia:Articles for creation|Articles for creation]] draft ' +
'for speedy deletion under [[WP:G13|G13]]'
} );
// Now notify the page creator as well as any and all previous submitters
$.when( gotCreator ).then( ( creator ) => {
const usersToNotify = [ creator ];
$.each( afchSubmission.submitters, ( _, submitter ) => {
// Don't notify the same user multiple times
if ( usersToNotify.indexOf( submitter ) === -1 ) {
usersToNotify.push( submitter );
}
} );
$.each( usersToNotify, ( _, user ) => {
AFCH.actions.notifyUser( user, {
message: AFCH.msg.get( 'g13-submission',
{ $1: AFCH.consts.pagename } ),
summary: 'Notification: [[WP:G13|G13]] speedy deletion nomination of [[' + AFCH.consts.pagename + ']]'
} );
} );
// And finally log the CSD nomination once all users have been notified
AFCH.actions.logCSD( {
title: afchPage.rawTitle,
reason: '[[WP:G13]] ({{tl|db-afc}})',
usersNotified: usersToNotify
} );
} );
} );
}
function handlePostponeG13( data ) {
let postponeCode,
text = data.afchText,
rawText = text.get(),
postponeRegex = /\{\{AfC postpone G13\s*(?:\|\s*(\d*)\s*)?\}\}/ig;
const match = postponeRegex.exec( rawText );
// First add the postpone template
if ( match ) {
if ( match[ 1 ] !== undefined ) {
postponeCode = '{{AfC postpone G13|' + ( parseInt( match[ 1 ] ) + 1 ) + '}}';
} else {
postponeCode = '{{AfC postpone G13|2}}';
}
rawText = rawText.replace( match[ 0 ], postponeCode );
} else {
rawText += '\n{{AfC postpone G13|1}}';
}
text.set( rawText );
// Then add the comment if entered
if ( data.commentText ) {
afchSubmission.addNewComment( data.commentText );
text.updateAfcTemplates( afchSubmission.makeWikicode() );
}
text.cleanUp();
afchPage.edit( {
contents: text.get(),
summary: 'Postponing [[WP:G13|G13]] speedy deletion'
} );
}
}( AFCH, jQuery, mediaWiki ) );
// </nowiki>
6c72twsiah8slexynuiid9wbp77h48g
မီႇတီႇယႃႇဝီႇၶီႇ:Gadget-Shortdesc-helper-pagestyles-vector.css
8
28760
125512
57262
2026-05-04T23:18:24Z
Saimawnkham
5
125512
css
text/css
/* This is its own file/gadget because a CSS-only gadget like this is page blocking, but CSS in a JavaScript gadget is loaded later. Doing it this way avoids a flash of unstyled content where the short description jumps around. */
/* Fix content jump */
.skin-vector.ns-0 #mw-content-subtitle::after {
content: '\200B';
}
.skin-vector.ns-0 .noarticletext {
margin-top: -1.008em; /* 0.84 * 1.2em */
}
/* Fix layout for deleted mainspace pages */
.skin-vector.ns-0:not(.skin-vector-2022) #contentSub2 {
margin-top: -1.2em;
}
.skin-vector.ns-0 .mw-warning-with-logexcerpt {
margin-bottom: 1.2em;
}
a0ny5pgcmp01m9qmqh63b6pin8d7t6o
မိူင်းၵဵင်းလိူၼ်
0
35701
125517
125508
2026-05-04T23:24:07Z
Saimawnkham
5
added [[Category:ဝၢၼ်ႈၸိူဝ်းမီးတီႈ မိူင်းတႆး]] using [[Help:Gadget-HotCat|HotCat]]
125517
wikitext
text/x-wiki
{{Infobox settlement
|name = မိူင်းၵဵင်းလိူၼ်
|settlement_type = [[ဝဵင်းၼႂ်းမိူင်းမျၢၼ်ႇမႃႇ|ဝဵင်းၼႂ်းမိူင်းတႆး]]
|pushpin_label_position = bottom
|pushpin_map = မိူင်းမၢၼ်ႈ
|pushpin_map_caption = ဢွင်ႈတီႈ ၼႂ်းမိူင်းမျၢၼ်ႇမႃႇ
|image_skyline =
|imagesize =
|image_caption =
|image_map =
|map_caption =
|subdivision_type = မိူင်း
|subdivision_name = {{ၸွမ်ပိဝ်|မိူင်းမျၢၼ်ႇမႃႇ}}
|subdivision_type1 = [[ၼႃႈလိၼ်ၽွင်းငမ်းဢုပ်ႉပိူင်ႇ မိူင်းမျၢၼ်ႇမႃႇ|ၸႄႈမိူင်း]]
|subdivision_name1 = {{ၸွမ်ပိဝ်|မိူင်းတႆး}}
|subdivision_type2 = [[ၸႄႈတွၼ်ႈၸိူဝ်းမီးၼႂ်း မိူင်းမျၢၼ်ႇမႃႇ|ၸႄႈတွၼ်ႈ]]
|subdivision_name2 = [[ၸႄႈတွၼ်ႈမိူင်းသူႈ]]
|subdivision_type3 = [[ၸႄႈဝဵင်းၸိူဝ်းမီးၼႂ်း မိူင်းမျၢၼ်ႇမႃႇ|ၸႄႈဝဵင်း]]
|subdivision_name3 = [[ၸႄႈဝဵင်းၵေးသီး]]
|unit_pref = Imperial
|area_total_km2 =
|population =
|population_total =
|population_urban =
|population_rural =
|population_as_of =
|population_footnotes =
|population_blank1_title = Ethnicities
|population_blank2 = [[ထေရဝႃတ|ပုတ်ႉထၽႃႇသႃႇ]]
|population_blank2_title = သႃႇသၼႃႇၵိူဝ်းယမ်
|population_density_km2 = auto
|coordinates = {{Coord|21.98928|98.088040|region:MM|format=dms|display=inline, title}}
|leader_title =
|elevation_ft =
|elevation_m = 800
|timezone = [[လၵ်းၸဵင်ၶၢဝ်းယၢမ်းမျၢၼ်ႇမႃႇ|MST]]
|utc_offset = +06:30
|website =
|postal_code_type = မၢႆသူင်ႇလိၵ်ႈ
|postal_code =
}}
==မိူင်းၵဵင်းလိူၼ်==
မိူင်းၵဵင်းလိူၼ် (1857 – 1926)
မိူင်းၵဵင်းလိူၼ်ၼႆႉ ဢင်းၵိတ်းႁွင်ႉဝႃႈ [Kenglon] လိူဝ်ၼၼ်ႉ Kyainglun ၼႆၵေႃႉတႅမ်ႈသေ (ၽၢႆႇမၢၼ်ႈတေႉ မိူဝ်ႈဢွၼ် တႅမ်ႈ ကျိုင်းလွန်း ၼႆ မိူဝ်ႈလဵဝ်တေႉ တႅမ်ႈ ကျိုင်းလင်ရွာ ) ယၢမ်ႈပဵၼ်ဝဵင်းဢွၼ်ႇဝဵင်းၼိုင်ႈ မီးလွင်ႈဢုပ်ႉပိူင်ႇႁင်းၶေႃ ၼႂ်းမိူင်းတႆး။
မိူင်းၵဵင်းလိူၼ်ၼႆႉ ယၢမ်ႈလႆႈယူႇတႂ်ႈၽွင်းငမ်း မိူင်းသႅၼ်ဝီၸၢၼ်း (မိူင်းယႆ)၊ လႆႈလွင်ႈၵွၼ်းၶေႃ တႂ်ႈသႅၼ်ဝီၸၢၼ်း မိူဝ်ႈ 1857။ လႆႈၶၢမ်ႇတၢင်း ၶုၼ်မၢၼ်ႈၶဝ် ၶဝ်ႈၵဝ်ၼဝ်း ၵဵပ်းၶွၼ်ႇမႃး တေႃႇထိုင် 1887ၼီႈ မိူဝ်ႈၶုၼ်ၵူင်းပွင်ႇၶဝ်သုမ်းသိုၵ်းၸူး ဢင်းၵိတ်းၼၼ်ႉသေ မိူင်းတႆးၵေႃႈ လႆႈတူၵ်းလူင်းတႂ်ႈ ၵုမ်းၵမ်းဢင်းၵိတ်းၵႂႃႇ။
မိူဝ်ႈ 1926 ၼၼ်ႉ မိူင်းၵဵင်းလိူၼ် လႆႈၶၢမ်ႇ ဢဝ်ၵႂႃႇၶဝ်ႈႁူမ်ႈ တႂ်ႈဢုပ်ႉပိူင်ႇ မိူင်းၵေးသီးဝၢၼ်ႈၸၢမ်ၵႂႃႇယူႇယဝ်ႉ။
မိူင်းၵဵင်းလိူၼ်ၼႆႉ မီးၵွင်းမူးၵိုၵ်းပိုၼ်း [[ၵွင်းမူးၵဵင်းလိူၼ်]] ၵိုၵ်းဝႆႉလႄႈ ၸိုဝ်ႈၵွင်းမူးလႄႈ ၸိုဝ်ႈမိူင်းၵဵင်းလိူၼ်ၵေႃႈ ၵိုၵ်းၵၼ်ဝႆႉတေႃႇႁၢၼ်ႉၼႆႉ။
မိူဝ်ႈလဵဝ် မိူင်းၵဵင်းလိူၼ်ၼႆႉ ၵိုတ်းပဵၼ်ဝၢၼ်ႈဢွၼ်ႇၵူၺ်းယဝ်ႉသေ မီးဝႆႉၽၢႆႇဢွၵ်ႇၵေးသီးဝၢၼ်ႈၸၢမ် ၽၢႆႇႁွင်ႇဝၢၼ်ႈႁႆး ၼိူဝ်သဵၼ်ႈတၢင်းၵႃး မိူင်းသူႈ လႃႈသဵဝ်ႈၼၼ်ႉယဝ်ႉ။
==မိူင်းၵဵင်းလိူၼ်==
*ၸိုဝ်ႈဝဵင်း - မိူင်းၵဵင်းလိူၼ်
*တင်းၵႂၢင်ႈ 1901- 111.3 ၵီႇလူဝ်ႇမီႇတႃႇ (43.0 လၵ်းပၼ်ႇမူၼ်း)
*ႁူဝ်ၼပ်ႉၵူၼ်း (1901) – 4259
*လႆႈၵွၼ်းၶေႃတီႈသႅၼ်ဝီၸၢၼ်း - 1857
*ႁူမ်ႈပွင်းလေႃးၵေးသီးဝၢၼ်ႈၸၢမ် - 1925
==ၶုၼ်ဢၼ်ဢုပ်ႉပိူင်ႇ မိူင်းၵဵင်းလိူၼ်==
ၶုၼ်ၸိူဝ်းဢုပ်ႉပိူင်ႇ မိူင်းၵဵင်းလိူၼ်ၼႆႉ ၸႂ်ႉၸိုဝ်ႈမဵဝ်ႉၸႃးၼႆႉယဝ်ႉ။
*1857 -1873 ၶုၼ်မွင်ႇပႂၢင်ႉ(ၶုၼ်ပႂၢင်ႉ)
*1873 -1874 ၶုၼ်ၼေႃႇၶမ်းဢူး
*1874 – 1885 ၶုၼ်တႃႇဝ
*1885 – 1888 ၶုၼ်ထွၼ်း
*1888 – 1926 ၶုၼ်မွင်ႇ
==ၽိုၼ်ဢိင်==
#Imperial Gazetteer of India, v. 15, p.200.
#WHKMLA: History of th Shan States” 18 May 2010. Retrieved 21 December 2010.
#Ben Cahoon (2000). “World Statesmen.org: Shan and Karenni States of Burma”. Retrieved 21 December 2010
[[ပိူင်ထၢၼ်ႈ:ဝၢၼ်ႈၸိူဝ်းမီးတီႈ မိူင်းတႆး]]
fr6gbpoq8dylojsb3n53cm9xui4pqpp
မေႃႇၵျူး:Shortdesc helper JS
828
36920
125509
2026-05-04T23:14:29Z
Saimawnkham
5
ၵေႃႇသၢင်ႈၼႃႈလိၵ်ႈဝႆႉ တင်း "local p = {} local prepend = [[ window.sdh = {}; /* These messages can be changed to localize Shortdesc helper for your wiki. ** If that is not necessary, then this can be removed, and English-language messages will be used. */ ]] local append = [[ mw.loader.getScript( 'https://shn.wikipedia.org/w/load.php?modules=ext.gadget.libSettings' ).then( function() { mw.loader.load( 'https://shn.wikipedia.org/w/load.php?..."
125509
Scribunto
text/plain
local p = {}
local prepend = [[
window.sdh = {};
/* These messages can be changed to localize Shortdesc helper for your wiki.
** If that is not necessary, then this can be removed, and English-language messages will be used. */
]]
local append = [[
mw.loader.getScript( 'https://shn.wikipedia.org/w/load.php?modules=ext.gadget.libSettings' ).then( function() {
mw.loader.load( 'https://shn.wikipedia.org/w/load.php?modules=ext.gadget.Shortdesc-helper' );
})]]
function p.main(frame)
local text = mw.title.new('MediaWiki:Gadget-Shortdesc-helper.js'):getContent()
local content = text
:match('var messages.-;')
:gsub('var messages', 'window.sdh.messages')
:gsub('\t\t', '\t')
:gsub('\t};', '};')
content = prepend..content..'\n\n'..append
return frame:extensionTag ('syntaxhighlight', content, {lang = 'js', copy="copy"})
end
function p.append(frame)
return frame:extensionTag ('syntaxhighlight', append, {lang = 'js', copy="copy"})
end
return p
n20yp9mhi6pxqc6qzeq794v7wuaqk2k