User:Matma Rex/signaturedetector.js

// Copied from: // https://gerrit.wikimedia.org/r/c/mediawiki/core/+/539305/3/signaturedetector.js

'use strict';

var SP_CONTRIBS_NAME, LOCAL_TIMEZONE, LOCAL_TIMEZONE_OFFSET;

// getformats.php var DATE_FORMATS = { 'ab': 'H:i, j xg Y', 'abs': 'j F Y H.i', 'ace': 'j F Y H.i', 'ady-cyrl': 'H:i, j F Y', 'aeb-arab': 'H:i، j xg Y', 'aeb-latn': 'H:i, j F Y', 'af': 'H:i, j F Y', 'ais': 'H:i, j F Y', 'ak': 'H:i, j F Y', 'aln': 'j F Y H:i', 'ami': 'H:i, j F Y', 'am': 'H:i, j F Y', 'ang': 'H:i, j F Y', 'an': 'H:i j M Y', 'anp': 'H:i, j F Y', 'arc': 'H:i, j F Y', 'ar': 'H:i، j xg Y', 'arn': 'H:i j M Y', 'arq': 'H:i، j xg Y', 'ary': 'H:i, j F Y', 'arz': 'H:i، j xg Y', 'ase': 'H:i, j F Y', 'as': 'H:i, j F Y', 'ast': 'H:i j M Y', 'atj': 'j F Y à H:i', 'av': 'H:i, j xg Y', 'avk': 'H:i, j F Y', 'awa': 'H:i, j F Y', 'ay': 'H:i j M Y', 'azb': 'j xg Y، ساعت H:i', 'az': 'H:i, j F Y', 'ba': 'H:i, j xg Y', 'ban': 'j F Y H.i', 'bar': 'H:i, j. M Y', 'bbc-latn': 'j F Y H.i', 'bcc': 'j xg Y، ساعت H:i', 'bcl': 'H:i, j F Y', 'be': 'H:i, j xg Y', 'be-tarask': 'H:i, j xg Y', 'bg': 'H:i, j F Y', 'bgn': 'j xg Y، ساعت H:i', 'bho': 'H:i, j F Y', 'bi': 'H:i, j F Y', 'bjn': 'j F Y H.i', 'bm': 'j F Y à H:i', 'bn': 'H:i, j F Y', 'bo': 'H:i, j F Y', 'bpy': 'H:i, j F Y', 'bqi': 'j xg Y، ساعت H:i', 'brh': 'H:i, j F Y', 'br': 'j M Y "da" H:i', 'bs': 'H:i, j F Y', 'btm': 'j F Y H.i', 'bto': 'H:i, j F Y', 'bug': 'j F Y H.i', 'bxr': 'H:i, j xg Y', 'ca': 'H:i, j M Y', 'cbk-zam': 'H:i j M Y', 'cdo': 'Y "nièng" n "nguŏk" j "hô̤" (D) H:i', 'ceb': 'H:i, j F Y', 'ce': 'Y, j F, H:i', 'ch': 'H:i, j F Y', 'chr': 'H:i, j F Y', 'chy': 'H:i, j F Y', 'ckb': 'H:i، jی xg Y', 'co': 'H:i, j M Y', 'cps': 'H:i, j F Y', 'crh-cyrl': 'H:i, Y "с." xg j', 'crh-latn': 'H:i, Y "s." xg j', 'cr': 'H:i, j F Y', 'csb': 'H:i, j M Y', 'cs': 'j. n. Y, H:i', 'cu': 'H:i, xg j числа, Y', 'cv': 'H:i, j xg Y', 'cy': 'H:i, j F Y', 'da': 'j. M Y, H:i', 'de-at': 'H:i, j. M Y', 'de-ch': 'H:i, j. M Y', 'de-formal': 'H:i, j. M Y', 'de': 'H:i, j. M Y', 'din': 'H:i, j F Y', 'diq': 'H:i, j F Y', 'dsb': 'j. xg Y, H:i', 'dtp': 'H:i, j F Y', 'dty': 'H:i, j F Y', 'dv': 'H:i, j F Y', 'dz': 'H:i, j F Y', 'ee': 'H:i, j F Y', 'egl': 'H:i, j M Y', 'el': 'H:i, j xg Y', 'eml': 'H:i, j M Y', 'en-ca': 'H:i, j F Y', 'en-gb': 'H:i, j F Y', 'en': 'H:i, j F Y', 'eo': 'H:i, j M. Y', 'es-formal': 'H:i j M Y', 'es': 'H:i j M Y', 'et': 'j. F Y, "kell" H:i', 'eu': 'H:i, j F Y', 'exif': 'H:i, j F Y', 'ext': 'H:i j M Y', 'fa': 'j xg Y، ساعت H:i', 'ff': 'j F Y à H:i', 'fi': 'j. F"ta" Y "kello" H.i', 'fit': 'j. F"ta" Y "kello" H.i', 'fj': 'H:i, j F Y', 'fo': 'j. M Y "kl." H:i', 'frc': 'j F Y à H:i', 'fr': 'j F Y à H:i', 'frp': 'j F Y "a" H:i', 'frr': 'H:i, j. M Y', 'fur': 'j "di" M Y "a lis" H:i', 'fy': 'j M Y, H.i', 'gag': 'H.i, j F Y', 'ga': 'H:i, j F Y', 'gan-hans': 'Y年n月j日 (D) H:i', 'gan-hant': 'Y年n月j日 (D) H:i', 'gan': 'Y年n月j日 (D) H:i', 'gcr': 'j F Y à H:i', 'gd': 'H:i, j F Y', 'gl': 'j \\d\\e F \\d\\e Y "ás" H:i', 'glk': 'j xg Y، ساعت H:i', 'gn': 'H:i j M Y', 'gom-deva': 'H:i, j F Y', 'gom-latn': 'H:i, j F Y', 'gor': 'j F Y H.i', 'got': 'H:i, j F Y', 'grc': 'H:i, j xg Y', 'gsw': 'H:i, j. M Y', 'gu': 'H:i, j F Y', 'gv': 'H:i, j F Y', 'ha': 'H:i, j F Y', 'hak': 'H:i, j F Y', 'haw': 'H:i, j F Y', 'he': 'H:i, j xg Y', 'hif-latn': 'H:i, j F Y', 'hi': 'H:i, j F Y', 'hil': 'H:i, j F Y', 'hr': 'H:i, j. F Y.', 'hrx': 'H:i, j. M Y', 'hsb': 'j. xg Y, H:i', 'ht': 'j F Y à H:i', 'hu-formal': 'Y. F j., H:i', 'hu': 'Y. F j., H:i', 'hy': 'H:i, j xg Y', 'hyw': 'H:i, j xg Y', 'ia': 'H:i, j F Y', 'id': 'j F Y H.i', 'ie': 'H:i, j F Y', 'ig': 'H:i, j F Y', 'ii': 'Y年n月j日 (D) H:i', 'ike-cans': 'H:i, j F Y', 'ike-latn': 'H:i, j F Y', 'ik': 'H:i, j F Y', 'ilo': 'H:i, j F Y', 'inh': 'H:i, j xg Y', 'io': 'H:i, j M. Y', 'is': 'j. F Y "kl." H:i', 'it': 'H:i, j M Y', 'ja': 'Y年n月j日 (D) H:i', 'jam': 'H:i, j F Y', 'jbo': 'H:i, j F Y', 'jut': 'j. M Y, H:i', 'jv': 'j F Y H.i', 'kaa': 'H:i, Y "j." xg j', 'kab': 'H:i, j F Y', 'ka': 'H:i, j F Y', 'kbd-cyrl': 'H:i, j F Y', 'kbp': 'j F Y à H:i', 'kg': 'H:i, j F Y', 'khw': 'H:i، j xg Yء', 'ki': 'H:i, j F Y', 'kiu': 'H.i, j F Y', 'kjp': ' H:i"၊" j F Y', 'kk-arab': 'H:i، Y "ج." xg j', 'kk-cyrl': 'H:i, Y "ж." xg j', 'kk': 'H:i, Y "ж." xg j', 'kk-latn': 'H:i, Y "j." xg j', 'kl': 'j. M Y, H:i', 'km': 'មោងH:i l ទd F ឆ្នាY', 'kn': 'H:i, j F Y', 'krc': 'H:i, j xg Y', 'kri': 'H:i, j F Y', 'krj': 'H:i, j F Y', 'krl': 'j. F"ta" Y "kello" H.i', 'ks-arab': 'H:i, j F Y', 'ks-deva': 'H:i, j F Y', 'ksh': 'H:i, j. M Y', 'ks': 'H:i, j F Y', 'ku-arab': 'H:i، jی xg Y', 'ku-latn': 'H:i, j F Y', 'kum': 'H:i, j xg Y', 'kv': 'H:i, j xg Y', 'kw': 'H:i, j F Y', 'ky': 'H:i, j F Y', 'lad': 'H:i j M Y', 'la': 'H:i, j xg Y', 'lbe': 'H:i, j xg Y', 'lb': 'H:i, j. M Y', 'lez': 'H:i, j xg Y', 'lfn': 'H:i, j F Y', 'lg': 'H:i, j F Y', 'lij': 'H:i, j M Y', 'li': 'j M Y H:i', 'liv': 'j. F Y, "kell" H:i', 'lki': 'j xg Y، ساعت H:i', 'lmo': 'H:i, j M Y', 'ln': 'j F Y à H:i', 'lo': 'H:i, j F Y', 'loz': 'H:i, j F Y', 'lrc': 'j xg Y، ساعت H:i', 'ltg': 'Y". gada" j. F", plkst." H.i', 'lt': 'H:i, j F Y', 'lus': 'H:i, j F Y', 'luz': 'j xg Y، ساعت H:i', 'lv': 'Y". gada" j. F", plkst." H.i', 'lzh': 'Y年n月j日 （D） H時i分', 'lzz': 'H.i, j F Y', 'mai': 'H:i, j F Y', 'map-bms': 'j F Y H.i', 'mdf': 'H:i, j xg Y', 'mg': 'j F Y à H:i', 'mhr': 'H:i, j xg Y', 'mi': 'H:i, j F Y', 'min': 'j F Y H.i', 'mk': 'H:i, j F Y', 'ml': 'H:i, j F Y', 'mni': 'H:i, j F Y', 'mn': 'H:i, j F Y', 'mnw': ' H:i"၊" j F Y', 'mo': 'j F Y H:i', 'mrj': 'H:i, j xg Y', 'mr': 'H:i, j F Y', 'ms': 'H:i, j F Y', 'mt': 'H:i, j F Y', 'mwl': 'H\\hi\\m\\i\\n \\d\\e j \\d\\e F \\d\\e Y', 'my': ' H:i"၊" j F Y', 'myv': 'H:i, j xg Y', 'mzn': 'j xg Y، ساعت H:i', 'nah': 'H:i j M Y', 'na': 'H:i, j F Y', 'nan': 'Y-"nî" n-"goe̍h" j-"ji̍t" (D) H:i', 'nap': 'H:i, j M Y', 'nb': 'j. M Y "kl." H:i', 'nds': 'H:i, j. M Y', 'nds-nl': 'H:i, j M Y', 'ne': 'H:i, j F Y', 'new': 'H:i, j F Y', 'niu': 'H:i, j F Y', 'nl-informal': 'j M Y H:i', 'nl': 'j M Y H:i', 'nn': 'j. F Y "kl." H:i', 'nov': 'H:i, j F Y', 'nqo': 'H:i, j F Y', 'nrm': 'j F Y à H:i', 'nso': 'H:i, j F Y', 'nv': 'H:i, j F Y', 'ny': 'H:i, j F Y', 'nys': 'H:i, j F Y', 'oc': 'j F "de" Y "a" H.i', 'olo': 'j. F"ta" Y "kello" H.i', 'om': 'H:i, j F Y', 'or': 'H:i, j F Y', 'os': 'H:i, j xg Y', 'pag': 'H:i, j F Y', 'pa': 'H:i, j F Y', 'pam': 'H:i, j F Y', 'pap': 'H:i, j F Y', 'pcd': 'j F Y à H:i', 'pdc': 'H:i, j. M Y', 'pdt': 'H:i, j. M Y', 'pfl': 'H:i, j. M Y', 'pih': 'H:i, j F Y', 'pi': 'H:i, j F Y', 'pl': 'H:i, j M Y', 'pms': 'H:i, j M Y', 'pnb': 'H:i, j F Y', 'pnt': 'H:i, j xg Y', 'prg': 'H:i, j F Y', 'ps': 'H:i, j F Y', 'pt-br': 'H"h"i"min" "de" j "de" F "de" Y', 'pt': 'H\\hi\\m\\i\\n \\d\\e j \\d\\e F \\d\\e Y', 'qqq': 'H:i, j F Y', 'qug': 'H:i j M Y', 'qu': 'H:i j M Y', 'rgn': 'H:i, j M Y', 'rif': 'H:i, j F Y', 'rm': 'H:i, j F Y', 'rmy': 'j F Y H:i', 'roa-tara': 'H:i, j M Y', 'ro': 'j F Y H:i', 'rue': 'H:i, j xg Y', 'ru': 'H:i, j xg Y', 'rup': 'j F Y H:i', 'ruq-cyrl': 'H:i, j F Y', 'ruq-latn': 'j F Y H:i', 'rw': 'H:i, j F Y', 'sah': 'H:i, j xg Y', 'sa': 'H:i, j F Y', 'sat': 'H:i, j F Y', 'sc': 'H:i, j M Y', 'scn': 'H:i, j M Y', 'sco': 'H:i, j F Y', 'sdc': 'H:i, j F Y', 'sdh': 'j xg Y، ساعت H:i', 'sd': 'H:i, j F Y', 'sei': 'H:i, j F Y', 'se': 'xg j "b." Y "dii." G.i', 'ses': 'j F Y à H:i', 'sg': 'j F Y à H:i', 'sgs': 'H:i, j F Y', 'shi': 'H:i, j F Y', 'sh': 'H:i, j F Y', 'shn': 'H:i, j F Y', 'shy-latn': 'H:i, j F Y', 'si': 'H:i, j F Y', 'sk': 'H:i, j. F Y', 'skr-arab': 'H:i، j xg Yء', 'sli': 'H:i, j. M Y', 'sl': 'H:i, j. F Y', 'sma': 'H:i, j F Y', 'sm': 'H:i, j F Y', 'sn': 'H:i, j F Y', 'so': 'H:i, j F Y', 'sq': 'j F Y H:i', 'sr-ec': 'H:i, j. F Y.', 'sr-el': 'H:i, j. F Y.', 'srn': 'j M Y H:i', 'ss': 'H:i, j F Y', 'st': 'H:i, j F Y', 'stq': 'H:i, j. M Y', 'sty': 'H:i, j xg Y', 'su': 'j F Y H.i', 'sv': 'j F Y "kl." H.i', 'sw': 'H:i, j F Y', 'szl': 'H:i, j M Y', 'ta': 'H:i, j F Y', 'tay': 'H:i, j F Y', 'tcy': 'H:i, j F Y', 'te': 'H:i, j F Y', 'tet': 'H\\hi\\m\\i\\n \\d\\e j \\d\\e F \\d\\e Y', 'tg-cyrl': 'H:i, j xg Y', 'tg-latn': 'H:i, j F Y', 'th': 'H:i, j F xkY', 'ti': 'H:i, j F Y', 'tk': 'H:i, j F Y', 'tl': 'H:i, j F Y', 'tly': 'H:i, j F Y', 'tn': 'H:i, j F Y', 'to': 'H:i, j F Y', 'tpi': 'H:i, j F Y', 'tr': 'H.i, j F Y', 'tru': 'H:i, j F Y', 'trv': 'H:i, j F Y', 'ts': 'H:i, j F Y', 'tt-cyrl': 'j M Y, H:i', 'tt-latn': 'j M Y, H:i', 'tw': 'H:i, j F Y', 'ty': 'j F Y à H:i', 'tyv': 'H:i, j xg Y', 'tzm': 'H:i, j F Y', 'udm': 'H:i, j xg Y', 'ug-arab': 'H:i, j F Y', 'ug-latn': 'H:i, j F Y', 'uk': 'H:i, j xg Y', 'ur': 'H:i، j xg Yء', 'uz': 'H:i, j-F Y', 'vec': 'H:i, j M Y', 've': 'H:i, j F Y', 'vep': 'j. F Y, "kell" H:i', 'vi': 'H:i, "ngày" j "tháng" n "năm" Y', 'vls': 'j M Y H:i', 'vmf': 'H:i, j. M Y', 'vo': 'H:i, Y F j"id"', 'vot': 'j. F"ta" Y "kello" H.i', 'vro': 'j. F Y, "kell" H:i', 'wa': 'j F Y à H:i', 'war': 'H:i, j F Y', 'wo': 'j F Y à H:i', 'wuu': 'Y年n月j号 (D) H:i', 'xal': 'H:i, j xg Y', 'xh': 'H:i, j F Y', 'xmf': 'H:i, j F Y', 'xsy': 'H:i, j F Y', 'yi': 'H:i, j xg Y', 'yo': 'H:i, j F Y', 'yue': 'Y年n月j號 (D) H:i', 'za': 'Y年n月j日 (D) H:i', 'zea': 'j M Y H:i', 'zgh': 'H:i, j F Y', 'zh-hans': 'Y年n月j日 (D) H:i', 'zh-hant': 'Y年n月j日 (D) H:i', 'zh-hk': 'Y年n月j日 (D) H:i', 'zh': 'Y年n月j日 (D) H:i', 'zh-sg': 'Y年n月j日 (D) H:i', 'zh-tw': 'Y年n月j日 (D) H:i', 'zu': 'H:i, j F Y' };

// getdigits.php // We can't use mw.language.convertNumber because that uses user language // and we need to use content language. var DIGITS = { 'aeb-arab': '٠١٢٣٤٥٦٧٨٩', 'anp': '०१२३४५६७८९', 'ar': '٠١٢٣٤٥٦٧٨٩', 'as': '০১২৩৪৫৬৭৮৯', 'azb': '۰۱۲۳۴۵۶۷۸۹', 'bcc': '۰۱۲۳۴۵۶۷۸۹', 'bgn': '۰۱۲۳۴۵۶۷۸۹', 'bho': '०१२३४५६७८९', 'bn': '০১২৩৪৫৬৭৮৯', 'bo': '༠༡༢༣༤༥༦༧༨༩', 'bpy': '০১২৩৪৫৬৭৮৯', 'bqi': '۰۱۲۳۴۵۶۷۸۹', 'ckb': '٠١٢٣٤٥٦٧٨٩', 'dty': '०१२३४५६७८९', 'dz': '༠༡༢༣༤༥༦༧༨༩', 'fa': '۰۱۲۳۴۵۶۷۸۹', 'glk': '۰۱۲۳۴۵۶۷۸۹', 'gom-deva': '०१२३४५६७८९', 'gu': '૦૧૨૩૪૫૬૭૮૯', 'hi': '०१२३४५६७८९', 'kjp': '၀၁၂၃၄၅၆၇၈၉', 'kk-arab': '۰۱۲۳۴۵۶۷۸۹', 'km': '០១២៣៤៥៦៧៨៩', 'kn': '೦೧೨೩೪೫೬೭೮೯', 'ks-arab': '٠١٢٣٤٥٦٧٨٩', 'ks-deva': '०१२३४५६७८९', 'ks': '٠١٢٣٤٥٦٧٨٩', 'ku-arab': '٠١٢٣٤٥٦٧٨٩', 'lki': '۰۱۲۳۴۵۶۷۸۹', 'lo': '໐໑໒໓໔໕໖໗໘໙', 'lrc': '۰۱۲۳۴۵۶۷۸۹', 'luz': '۰۱۲۳۴۵۶۷۸۹', 'lzh': '〇一二三四五六七八九', 'mai': '०१२३४५६७८९', 'mni': '꯰꯱꯲꯳꯴꯵꯶꯷꯸꯹', 'mnw': '၀၁၂၃၄၅၆၇၈၉', 'mr': '०१२३४५६७८९', 'my': '၀၁၂၃၄၅၆၇၈၉', 'mzn': '۰۱۲۳۴۵۶۷۸۹', 'ne': '०१२३४५६७८९', 'new': '०१२३४५६७८९', 'nqo': '߀߁߂߃߄߅߆߇߈߉', 'or': '୦୧୨୩୪୫୬୭୮୯', 'pi': '०१२३४५६७८९', 'ps': '۰۱۲۳۴۵۶۷۸۹', 'sa': '०१२३४५६७८९', 'sat': '᱐᱑᱒᱓᱔᱕᱖᱗᱘᱙', 'sdh': '۰۱۲۳۴۵۶۷۸۹', 'skr-arab': '٠١٢٣٤٥٦٧٨٩', 'tcy': '೦೧೨೩೪೫೬೭೮೯' };

// array_keys( timezone_abbreviations_list ) var TIMEZONES = [ "acdt", "acst", "act", "acwdt", "acwst", "addt", "adt", "aedt", "aest", "aft", "ahdt", "ahst", "akdt", "akst", "amst", "amt", "ant", "apt", "arst", "art", "ast", "awdt", "awst", "awt", "azomt", "azost", "azot", "bdst", "bdt", "beat", "beaut", "bmt", "bnt", "bortst", "bort", "bost", "bot", "brst", "brt", "bst", "btt", "burt", "cant", "capt", "cast", "cat", "cawt", "cct", "cddt", "cdt", "cemt", "cest", "cet", "cgst", "cgt", "chadt", "chast", "chdt", "chost", "chot", "chut", "ckhst", "ckt", "clst", "clt", "cmt", "cost", "cot", "cpt", "cst", "cut", "cvst", "cvt", "cwt", "cxt", "chst", "dact", "dmt", "easst", "east", "eat", "ect", "eddt", "edt", "eest", "eet", "egst", "egt", "ehdt", "emt", "ept", "est", "ewt", "ffmt", "fjst", "fjt", "fkst", "fkt", "fmt", "fnst", "fnt", "galt", "gamt", "gbgt", "gft", "ghst", "gilt", "gmt", "gst", "gyt", "hdt", "hkst", "hkt", "hmt", "hovst", "hovt", "hst", "ict", "iddt", "idt", "ihst", "imt", "iot", "irdt", "irst", "isst", "ist", "javt", "jcst", "jdt", "jmt", "jst", "jwst", "kart", "kdt", "kmt", "kost", "kst", "kwat", "lhdt", "lhst", "lint", "lkt", "lrt", "lst", "madmt", "madst", "madt", "malst", "malt", "mart", "mddt", "mdst", "mdt", "mest", "met", "mht", "mist", "mmt", "most", "mot", "mpt", "msd", "msk", "mst", "must", "mut", "mvt", "mwt", "myt", "ncst", "nct", "nddt", "ndt", "negt", "nest", "net", "nfst", "nft", "nmt", "npt", "nrt", "nst", "nut", "nwt", "nzdt", "nzmt", "nzst", "pddt", "pdt", "pest", "pet", "pgt", "phot", "phst", "pht", "pkst", "pkt", "plmt", "pmdt", "pmmt", "pmst", "pmt", "pnt", "pont", "ppmt", "ppt", "pst", "pwt", "pyst", "pyt", "qmt", "ret", "rmt", "sast", "sbt", "sct", "sdmt", "sdt", "set", "sgt", "sjmt", "smt", "srt", "sst", "swat", "taht", "tbmt", "tkt", "tlt", "tmt", "tost", "tot", "tvt", "uct", "ulast", "ulat", "utc", "uyhst", "uyst", "uyt", "vet", "vust", "vut", "wakt", "warst", "wart", "wast", "wat", "wemt", "west", "wet", "wft", "wgst", "wgt", "wib", "wita", "wit", "wmt", "wsdt", "wsst", "xjt", "yddt", "ydt", "ypt", "yst", "ywt", "a", "b", "c", "d", "e", "f", "g", "h", "i", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z" ];

var rtl = $( 'html' ).attr( 'dir' ) === 'rtl'; var api = new mw.Api; var loading = $.when(	api.loadMessages( [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat',

'sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday',

'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec',

'january', 'february', 'march', 'april', 'may_long', 'june', 'july', 'august', 'september', 'october', 'november', 'december',

'january-gen', 'february-gen', 'march-gen', 'april-gen', 'may-gen', 'june-gen', 'july-gen', 'august-gen', 'september-gen', 'october-gen', 'november-gen', 'december-gen', ], {		amlang: mw.config.get( 'wgContentLanguage' ) } ),	api.loadMessages( undefined, { amlang: mw.config.get( 'wgContentLanguage' ), "amincludelocal": 1, "amfilter": "timezone-" } ),	api.get( { "action": "query", "meta": "siteinfo", "siprop": [ "specialpagealiases", "general" ] } ).then( function ( resp ) { for ( var i = 0; i < resp.query.specialpagealiases.length; i++ ) { if ( resp.query.specialpagealiases[ i ].realname === 'Contributions' ) { SP_CONTRIBS_NAME = resp.query.specialpagealiases[ i ].aliases[ 0 ]; break; }		}		// TODO: Implement DST offsets LOCAL_TIMEZONE = resp.query.general.timezone; LOCAL_TIMEZONE_OFFSET = resp.query.general.timeoffset; } ) );

function getMessages( msg ) { return msg.map( mw.msg ); }

// Language::sprintfDate // This only supports format characters that are used by the default date format in any of // MediaWiki's languages, namely: D, d, F, G, H, i, j, l, M, n, Y, xg, xkY (and escape characters), // and only dates when MediaWiki existed, let's say 2000 onwards (Thai dates before 1941 are complicated). function getTimestampRegexp( format, digits ) { function regexpGroup( regexp ) { return '(' + regexp + ')'; }

function regexpAlternateGroup( array ) { return '(' + array.map( mw.util.escapeRegExp ).join( '|' ) + ')'; }

var s, p, num, code, endQuote;

s = '';

for ( p = 0; p < format.length; p++ ) { num = false; code = format[p]; if ( code === 'x' && p < format.length - 1 ) { code += format[++p]; }		if ( code === 'xk' && p < format.length - 1 ) { code += format[++p]; }

switch ( code ) { case 'xx': s += 'x'; break; case 'xg': s += regexpAlternateGroup( getMessages( [ 'january-gen', 'february-gen', 'march-gen', 'april-gen', 'may-gen', 'june-gen', 'july-gen', 'august-gen', 'september-gen', 'october-gen', 'november-gen', 'december-gen' ] ) );				break; case 'd': num = '2'; break; case 'D': s += regexpAlternateGroup( getMessages( [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ] ) );				break; case 'j': num = '1,2'; break; case 'l': s += regexpAlternateGroup( getMessages( [ 'sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday' ] ) );				break; case 'F': s += regexpAlternateGroup( getMessages( [ 'january', 'february', 'march', 'april', 'may_long', 'june', 'july', 'august', 'september', 'october', 'november', 'december' ] ) );				break; case 'M': s += regexpAlternateGroup( getMessages( [ 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec' ] ) );				break; case 'n': num = '1,2'; break; case 'Y': num = '4'; break; case 'xkY': num = '4'; break; case 'G': num = '1,2'; break; case 'H': num = '2'; break; case 'i': num = '2'; break; case '\\': // Backslash escaping if ( p < format.length - 1 ) { s += format[++p]; } else { s += '\\'; }				break; case '"':				// Quoted literal				if ( p < format.length - 1 ) {					endQuote = format.indexOf( '"', p + 1 ) if ( endQuote === -1 ) { // No terminating quote, assume literal "						s += '"'; } else { s += format.substr( p + 1, endQuote - p - 1 ); p = endQuote; }				} else { // Quote at end of string, assume literal "					s += '"'; }				break; default: s += format[p]; }		if ( num !== false ) { s += regexpGroup( digits + '{' + num + '}' ); }	}

return s; }

function getTimestampParser( format, digits, timezoneOffset ) { var p, code, matchingGroups = []; for ( p = 0; p < format.length; p++ ) { code = format[p]; if ( code === 'x' && p < format.length - 1 ) { code += format[++p]; }		if ( code === 'xk' && p < format.length - 1 ) { code += format[++p]; }

switch ( code ) { case 'xx': s += 'x'; break; case 'xg': case 'd': case 'j': case 'D': case 'l': case 'F': case 'M': case 'n': case 'Y': case 'xkY': case 'G': case 'H': case 'i': matchingGroups.push( code ); break; case '\\': // Backslash escaping if ( p < format.length - 1 ) { ++p; }				break; case '"':				// Quoted literal				if ( p < format.length - 1 ) {					endQuote = format.indexOf( '"', p + 1 ) if ( endQuote !== -1 ) { p = endQuote; }				}				break; default: break; }	}

function untransformDigits( text ) { if ( !digits ) { return text; }		return text.replace(			new RegExp( '[' + digits + ']', 'g' ),			function ( m ) {				return digits.indexOf( m );			}		); }

return function ( match ) { var year = 0, monthIdx = 0, day = 0, hour = 0, minute = 0;

var i, code, text; for ( i = 0; i < matchingGroups.length; i++ ) { code = matchingGroups[ i ]; text = match[ i + 1 ];

switch ( code ) { case 'xg': monthIdx = getMessages( [						'january-gen', 'february-gen', 'march-gen', 'april-gen', 'may-gen', 'june-gen',						'july-gen', 'august-gen', 'september-gen', 'october-gen', 'november-gen',						'december-gen'					] ).indexOf( text ); break; case 'd': case 'j': day = Number( untransformDigits( text ) ); break; case 'D': case 'l': // Day of the week - unused break; case 'F': monthIdx = getMessages( [						'january', 'february', 'march', 'april', 'may_long', 'june',						'july', 'august', 'september', 'october', 'november',						'december'					] ).indexOf( text ); break; case 'M': monthIdx = getMessages( [						'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug',						'sep', 'oct', 'nov', 'dec'					] ).indexOf( text ); break; case 'n': monthIdx = Number( untransformDigits( text ) ) - 1; break; case 'Y': year = Number( untransformDigits( text ) ); break; case 'xkY': // Thai year year = Number( untransformDigits( text ) ) - 543; break; case 'G': case 'H': hour = Number( untransformDigits( text ) ); break; case 'i': minute = Number( untransformDigits( text ) ); break; default: throw "Not implemented"; }		}

return new Date( Date.UTC( year, monthIdx, day, hour, minute ) - timezoneOffset * 60 * 1000 ); }; }

// Parser::pstPass2 function getLocalTimestampRegexp { var langcode = mw.config.get( 'wgContentLanguage' ); var df = DATE_FORMATS[ langcode ]; var digits = mw.config.get( 'wgTranslateNumerals' ) ? DIGITS[ langcode ] : null; var dfRegexp = getTimestampRegexp( df, digits ? '[' + digits + ']' : '\\d' ); // MWTimestamp::getTimezoneMessage var localizedTimezones = TIMEZONES.map( function ( abbrev ) {		var message = mw.message( 'timezone-' + abbrev.toLowerCase );		return message.exists ? message.text : abbrev;	} ); // TODO: Timezone abbreviations are not unique so we can't do anything useful with this. var tzRegexp = '(?:' + localizedTimezones.map( mw.util.escapeRegExp ).join( '|' ).toUpperCase + ')'; var regexp = dfRegexp + ' \\(' + tzRegexp + '\\)'; return regexp; } function getLocalTimestampParser { var langcode = mw.config.get( 'wgContentLanguage' ); var df = DATE_FORMATS[ langcode ]; var digits = mw.config.get( 'wgTranslateNumerals' ) ? DIGITS[ langcode ] : null; // TODO: Implement DST offsets // TODO: Implement timezone validation var parseFunction = getTimestampParser( df, digits, LOCAL_TIMEZONE_OFFSET ); return parseFunction; }

function findTimestamps( node ) { var node, match; var nodes = []; var xpathResult = document.evaluate( './/text', node ); var dateRegexp = getLocalTimestampRegexp; while ( ( node = xpathResult.iterateNext ) ) { // TODO Multiple matches per node? if ( match = node.nodeValue.match( dateRegexp ) ) { nodes.push( [ node, match ] ); }	}	return nodes; }

function getPageTitleFromHref( href ) { var uri = new mw.Uri( href );

var articlePathRegexp = new RegExp(		mw.util.escapeRegExp( mw.config.get( 'wgArticlePath' ) )			.replace( mw.util.escapeRegExp( '$1' ), '(.*)' )	);

var match; if ( match = uri.path.match( articlePathRegexp ) ) { return decodeURIComponent( match[ 1 ] ); }	if ( uri.query.title ) { return uri.query.title; }	return null; }

function findSignature( timestampNode ) { var node = timestampNode; var sigNodes = [ node ]; var sigUsername = null; var length = 0; var lastLinkNode = timestampNode; while ( ( node = node.previousSibling ) && length < 100 ) { sigNodes.push( node ); length += ( node.textContent || '' ).length; if ( !node.tagName ) { continue; }		var links = []; if ( node.tagName.toLowerCase === 'a' ) { links.push( node ); } else { // Handle links nested in formatting elements. // Helpful accidental feature: users whose signature is not detected in full (due to text			// formatting) can just wrap it in a to fix that. // "Ten Pound Hammer • (What did I screw up now?)" // "« Saper // dyskusja »" var xpathResult = document.evaluate( './/a', node ); var link; while ( ( link = xpathResult.iterateNext ) ) { links.push( link ); }		}		if ( !links.length ) { continue; }		// Use .some rather than .every to permit vanity links // "TonyTheTiger (T / C / WP:FOUR / WP:CHICAGO / WP:WAWARD)" if ( links.some( function ( link ) { var username; var title = getPageTitleFromHref( link.href ); if ( !title ) { return false; }			var mwTitle = mw.Title.newFromText( title ); if (				mwTitle.getNamespaceId === mw.config.get( 'wgNamespaceIds' ).user ||				mwTitle.getNamespaceId === mw.config.get( 'wgNamespaceIds' ).user_talk			) { username = mwTitle.getMainText; } else if (				mwTitle.getNamespaceId === mw.config.get( 'wgNamespaceIds' ).special &&				mwTitle.getMainText.split( '/' )[ 0 ] === SP_CONTRIBS_NAME			) { username = mwTitle.getMainText.split( '/' )[ 1 ]; }			if ( !username ) { return false; }			if ( mw.util.isIPv6Address( username ) ) { // Canonicalize links // Bot-generated links "Preceding unsigned comment added by" are wrong username = username.toUpperCase; }

// Check that every link points to the same user if ( !sigUsername ) { sigUsername = username; }			return username === sigUsername; } ) ) {			lastLinkNode = node; }		// Keep looking if a node with links wasn't a link to a user page // "Doc James (talk · contribs · email)" }	// Pop excess text nodes while ( sigNodes[ sigNodes.length - 1 ] !== lastLinkNode ) { sigNodes.pop; }	return [ sigNodes, sigUsername ]; }

function markTimestamp( node, match, dfParser ) { var newNode = node.splitText( match.index ); newNode.splitText( match[ 0 ].length );

var wrapper = document.createElement( 'span' ); wrapper.className = 'detected-timestamp' // Note that this is not supported by all browsers, we should use Moment or something. // Moment also requires a plugin to support timezones. //	// We might need to actually port all the date formatting code from MediaWiki's PHP code // if we want to support displaying dates in all the formats available in user preferences // (which include formats in several non-Gregorian calendars). var date = dfParser( match ); wrapper.title = date.toLocaleString( "en-GB", { timeZone: LOCAL_TIMEZONE } ); wrapper.appendChild( newNode ); node.parentNode.insertBefore( wrapper, node.nextSibling ); }

function markSignature( sigNodes ) { var where = sigNodes[ 0 ]; var wrapper = document.createElement( 'span' ); wrapper.className = 'detected-signature' where.parentNode.insertBefore( wrapper, where ); while ( sigNodes.length ) { wrapper.appendChild( sigNodes.pop ); } }

function getIndentLevel( node, rootNode ) { var indent = 0; while ( node = node.parentNode ) { if ( node === rootNode ) { break; }		if ( node.tagName.toLowerCase === 'li' || node.tagName.toLowerCase === 'dl' ) { indent++; }	}	return indent; }

function nextInterestingLeafNode( node, rootNode ) { var treeWalker = document.createTreeWalker(		rootNode,		NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT,		function ( node ) {			if ( node.nodeType === Node.TEXT_NODE && node.textContent.trim !== '' ) {				return NodeFilter.FILTER_ACCEPT;			}			if ( node.nodeType === Node.ELEMENT_NODE && !node.firstChild ) {				return NodeFilter.FILTER_ACCEPT;			}		}	); treeWalker.currentNode = node; // Skip over this node's descendants treeWalker.nextSibling || treeWalker.nextNode; // while ( treeWalker.currentNode.firstChild ) { // 	treeWalker.nextNode; // }	return treeWalker.currentNode; }

function getComments( rootNode, timestamps, dfParser ) { var comments = []; var curComment;

var treeWalker = document.createTreeWalker(		rootNode,		NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT	); var node, range, startNode, match, startLevel, endLevel;

var nextTimestamp = 0; while ( node = treeWalker.nextNode ) { if ( node.tagName && node.tagName.match( /h[1-6]/i ) ) { range = new Range; range.selectNodeContents( node ); curComment = { range: range, level: 0 };			comments.push( curComment ); } else if ( timestamps[ nextTimestamp ] && node === timestamps[ nextTimestamp ][ 0 ] ) { // Everything from last comment up to here is the next comment startNode = nextInterestingLeafNode( curComment.range.endContainer, rootNode ); range = new Range; range.setStartBefore( startNode ); match = timestamps[ nextTimestamp ][ 1 ]; range.setEnd( node, match.index + match[ 0 ].length );

startLevel = getIndentLevel( startNode, rootNode ) + 1; endLevel = getIndentLevel( node, rootNode ) + 1; if ( startLevel !== endLevel ) { console.log( 'Comment starts and ends with different indentation', startNode, node ); }

curComment = { timestamp: dfParser( match ), author: findSignature( node )[ 1 ], range: range, // Should this use the indent level of `startNode` or `node`? level: Math.min( startLevel, endLevel ) };			comments.push( curComment ); nextTimestamp++; }	}

// return threads; return comments; }

function groupThreads( comments ) { var threads = []; var replies = [];

for ( var i = 0; i < comments.length; i++ ) { var comment = comments[ i ]; // clone comment = { timestamp: comment.timestamp, author: comment.author, range: comment.range, level: comment.level, // add these properties: replies: [], parent: null };

if ( replies.length < comment.level ) { // Someone skipped an indentation level (or several). Pretend that the previous reply // covers multiple indentation levels, so that the following comments get connected to it. console.log( 'Comment skips indentation level', comment.range ); var previousReplyIndex = replies.length - 1; replies.length = comment.level; replies.fill( replies[ previousReplyIndex ], previousReplyIndex + 1, comment.level ); }

if ( comment.level === 0 ) { // new root (thread) threads.push( comment ); } else if ( replies[ comment.level - 1 ] ) { // add as a reply to closest less nested comment replies[ comment.level - 1 ].replies.push( comment ); comment.parent = replies[ comment.level - 1 ]; } else { console.log( 'Comment could not be connected to a thread', comment.range ); }

replies[ comment.level ] = comment; // cut off more deeply nested replies replies.length = comment.level + 1; }

return threads; }

function markComment( comment ) { var rect = comment.range.getBoundingClientRect;

var marker = document.createElement( 'div' ); marker.className = 'detected-comment'; var scrollTop = document.documentElement.scrollTop || document.body.scrollTop; var scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft; marker.style.top = (rect.top + scrollTop) + 'px'; marker.style.height = (rect.height) + 'px'; marker.style.left = (rect.left + scrollLeft) + 'px'; marker.style.width = (rect.width) + 'px'; document.body.appendChild( marker );

if ( comment.parent ) { var parentRect = comment.parent.range.getBoundingClientRect; if ( comment.parent.level === 0 ) { // Twiddle so that it looks nice parentRect = JSON.parse( JSON.stringify( parentRect ) ); parentRect.height -= 10; if ( rtl ) { parentRect.width += 20; } else { parentRect.left -= 20; }		}

var marker2 = document.createElement( 'div' ); marker2.className = 'detected-comment-relationship';

marker2.style.top = (parentRect.top + parentRect.height + scrollTop) + 'px'; marker2.style.height = (rect.top - (parentRect.top + parentRect.height) + 10) + 'px'; if ( rtl ) { marker2.style.left = (rect.left + rect.width + scrollLeft) + 'px'; marker2.style.width = (10) + 'px'; } else { marker2.style.left = (parentRect.left + 10 + scrollLeft) + 'px'; marker2.style.width = (rect.left - (parentRect.left + 10)) + 'px'; }		document.body.appendChild( marker2 ); }

for ( var i = 0; i < comment.replies.length; i++ ) { marker = markComment( comment.replies[ i ] ); }

return marker; }

function markThreads( threads ) { var marker; for ( var i = 0; i < threads.length; i++ ) { marker = markComment( threads[ i ] ); } }

function getAuthors( comment ) { var authors = comment.author ? [ comment.author ] : []; authors = authors.concat( comment.replies.flatMap( getAuthors ) ); authors = Array.from( new Set( authors ) ); // unique authors.sort; return authors; }

loading.then( function {	var start = Date.now;

var timestamps = findTimestamps( document.getElementById( 'mw-content-text' ) ); var comments = getComments( document.getElementById( 'mw-content-text' ), timestamps, getLocalTimestampParser ); var threads = groupThreads( comments );

window.timestamps = timestamps; window.comments = comments; window.threads = threads;

// List authors per-thread for autocompletion or something for ( var i = 0; i < threads.length; i++ ) { threads[ i ].authors = getAuthors( threads[ i ] ); }

markThreads( threads ); // Reverse order so that box-shadows look right $( 'body' ).append( $( '.detected-comment-relationship' ).get.reverse );

for ( var i = 0; i < timestamps.length; i++ ) { var [ node, match ] = timestamps[ i ]; var [ signature, sigUsername ] = findSignature( node ); var emptySignature = signature.length === 1 && signature[ 0 ] === node; // Note that additional content may follow the timestamp (e.g. in some voting formats), but we // don't care about it. The code below doesn't mark that due to now the text nodes are sliced, // but we might need to take care to use the matched range of node in other cases. markTimestamp( node, match, getLocalTimestampParser ); if ( emptySignature ) { console.log( 'Timestamp without signature: ' + match[ 0 ] ); } else { markSignature( signature ); }	}

var end = Date.now; // In case you pay attention to the numbers reported here, consider that apparently code runs much // slower when pasted into the browser console than normally. console.log( 'Signature detection took ' + ( end - start ) + 'ms and found ' + timestamps.length + ' signatures.' );

function rtlSwap( css ) { if ( rtl ) { css = css.replace( /\b(left|right)\b/g, function ( m ) {				return m === 'left' ? 'right' : 'left';			} ); css = css.replace( /box-shadow: (-?[\d.]+)/g, function ( m, value ) {				return 'box-shadow: ' + ( -Number( value ) );			} ); }		return css; }	mw.util.addCSS( rtlSwap( ` .detected-timestamp { border: 2px solid forestgreen; padding: 1px; border-radius: 6px; margin: -3px; }		.detected-signature { border: 3px solid blue; border-bottom-left-radius: 6px; border-top-left-radius: 6px; border-right: 0; margin: -3px; margin-right: 0; }		.detected-signature + .detected-timestamp { border-left: 0; padding-left: 0; margin-left: 0; border-bottom-left-radius: 0; border-top-left-radius: 0; }		.detected-timestamp:hover:after { content: " " attr(title); }		.detected-comment { position: absolute; opacity: 0.2; pointer-events: none; }		.detected-comment:nth-child( 2n ) { background: lightpink; }		.detected-comment:nth-child( 2n + 1 ) { background: skyblue; }		.detected-comment-relationship { position: absolute; box-sizing: border-box; border-left: 3px solid red; border-bottom: 3px solid red; box-shadow: -5px 10px 6px 0 white; border-bottom-left-radius: 8px; }	` ) ); } );