//Factotum[https://en.wikipedia.org/wiki/User:Alexis_Jazz/Factotum] is a userscript to add comments to discussions and edit pages in general with more features and options than usual. Report issues on the talk page. If you see short garbage variable names you are reading the compact version of this script.
//Everything in Factotum is (unless noted otherwise) public domain, irrevocably released as WTFPL Version 2[www.wtfpl.net/about/] by its author, Alexis Jazz. If you don't like that or think it's invalid where you live you may use this under CC BY-SA 3.0, CC BY 2.5 or CC BY 4.0 instead. Your choice.
//Facototum includes lz-string by Pieroxy[https://pieroxy.net/blog/pages/lz-string/index.html][https://github.com/pieroxy/lz-string], originally licensed WTFPL (currently licensed MIT, see FTT.LZString below for details), Version 2, modified by Alexis Jazz. See w:en:User:Alexis_Jazz/lz-string for details.
/*globals $:false,mw:false,OO:false*/
/*jshint eqnull:true, eqeqeq:false */
if ( typeof window.FTT == 'undefined' || typeof window.FTT.nodeName != 'undefined' ) { //window.FTT could also exist if there's a section named "FTT"
window.FTT = {};
var FTT = window.FTT;
FTT.run = function(D1,D2,M1,M2,E1,PCL){
'use strict';
/*jshint eqnull:true, eqeqeq:false */
FTT.translations = ['nl','de','sq','ur']; //array of full translations available in FTT-extra.js
D1 = function(str){
return encodeURIComponent(str);
};
D2 = function(str){
return decodeURIComponent(str);
};
M1 = function(str){
return mw.config.get(str);
};
M2 = function(str){
return mw.util.getParamValue(str);
};
FTT.timestampInit = new Date().getTime();
FTT.semiRandom = FTT.timestampInit.toString().slice(-4); //last 4 digits of current timestamp. Won't change as long as you don't leave the page. Used in regular expressions for temporary placeholders (see FTT.safeText) so you can discuss the actual placeholders without them getting replaced
FTT.openingFormInProgressDelay = 2000; //how many ms it may take to open a form. It's usually only 1 or 2, but if oojs-ui-core isn't available yet and your device is very old, maybe longer. This value is used to throw an error if the form didn't open in time.
FTT.purpleBGJoker = $('.FTTPurpleBG').length; //if some joker decides to post an element with the "FTTPurpleBG" class (which otherwise indicates an ongoing or stuck process), FTT won't break as we subtract those
FTT.SN = 'Factotum'; //script name
FTT.VERSIONDATE = '08:49, 26 September 2024 (UTC)';
FTT.nowikiOpen = '';//not used anywhere but this way the minifier won't remove it. needed because MediaWiki parses JS pages as wikitext, sometimes resulting in maintenance categories
FTT.nowiki = 'nowiki';//to avoid entering nowiki tags in replacements
try {FTT.testEncodableNameSpace = D1(Object.keys(M1('wgNamespaceIds'))[FTT.userNSInt].slice(1));} catch (e) {delete FTT.testEncodableNameSpace;console.log('FTT: namespace fubar');} //gotwiki has a namespace "\udf3d𐌹𐌿𐍄𐌰𐌽𐌳𐍃", no idea what's up with that, don't really care either
if ( FTT.testEncodableNameSpace && [2,3].includes(Object.values(M1('wgNamespaceIds'))[FTT.userNSInt]) && ! ['user','user_talk'].includes(Object.keys(M1('wgNamespaceIds'))[FTT.userNSInt]) ) {
'newSectionCmt':'PLUSSNIPPET'+ FTT.summaryCredit, //"CREATE" will be replaced with "Creating comment", "PLUS" will be replaced with "+comment". Sorry, there is no "new $1", "add(ing) $1", "append(ing) $1" or any similar message available in MediaWiki by default
'cmt':'comment',
'plus':'+',
'rmCmtSummary':'DELETE USER' + FTT.summaryCredit,
'summaryCredit':FTT.summaryCredit,
'quoteOpen':'', //only used if there is no talk quote inline template
'quoteClose':'',
'refOpen':'',
'refClose':'',
},de:{'cmt':'Kommentar'
//'postCmtSummaryPost':'Antwort an USER' + FTT.summaryCreditDE,
//'postCmtSummaryPost':'replying to USER' + FTT.summaryCredit,
},nl:{'cmt':'reactie'
},fr:{'cmt':'commentaire'
},ja:{'cmt':'コメント'
},es:{'cmt':'comentario'
},ru:{'cmt':'сообщения'
},pt:{'cmt':'comentário'
},zh:{'cmt':'留言'
},it:{'cmt':'commento'
},ar:{'cmt':'تعليق'
},pl:{'cmt':'komentarza'
},uk:{'cmt':'коментар'
},sv:{'cmt':'kommentar'
},vi:{'cmt':'bình luận'
},fa:{'cmt':'نظر'
},id:{'cmt':'komentar'
},he:{'cmt':'תגובה'
},tr:{'cmt':'yorum'
},cs:{'cmt':'komentáře'
},sq:{'cmt':'përgjigju'
}
};
PCL = 'wgPageContentLanguage';
if ( typeof window.FTTWikiMsgsObj != 'undefined' && window.FTTWikiMsgsObj[ M1(PCL) ] ) { // FTTwikiMsgsObj could be specified in common.js before FTT is loaded
'mwuibuttonTO':'Take over new section buttons with .mw-ui-button and .oo-ui-buttonElement-button class',
'hideArchived':'Hide reply links within elements with the "archived" or "boilerplate" class',
'hideArchivedAll':'Hide thanks/permalink links within elements with the "archived" or "boilerplate" class',
'editFullPage':'Enable full page editing',
'firstHeadingFull':'Add full page edit icon to toolbar instead of page heading',
'editFullSection':'Enable full section editing',
'editFullSHref':'Add link to section edit icon (allows opening the native editor in a new tab/window with long/right/middle click, slightly increases page load time)',
'editFullSHide':'Hide native section edit link',
'editRefs':'Edit references',
'dateLinksIconSection':'Add link/permalink generator icon for sections',
'dateLinksIconSectExtra':'Also provide non-permalink',
'dateLinksIcon':'Permalink generator for comments',
'dateLinksIconAlt':'When the reply form is open, use comment link icons in the same section to insert anchored links to those comments',
'swapIcons':'Change appearance of permalink/reply icons when opening a form to indicate their function changed',
'thankLink':'',
'scrollTop':'Arrow in section headers to scroll back to the top of the page',
'scrollPrev':'Arrow in section headers to scroll to the previous section',
'scrollNext':'Arrow in section headers to scroll to the next section',
'reverseSectionOrder':'Reverse section order',
'reverseCollapToC':'Automatically collapse table of contents',
'collapsible':'Collapsible sections',
'autoCollapse':'Automatically collapse sections that contain no new comments since your last visit',
'collapArticle':'Collapsible sections on non-talk pages',
'collapArticleDefault':'Collapse all non-talk page sections by default on desktop',
'collapArticleDefaultMF':'Collapse all non-talk page sections by default on mobile',
'collapIcons':'Add "collapse section" icon after every comment',
'autonum':'Add hierarchical outline-style numbering to headers',
'autonumPlain':'Style as plain text',
'autonumCopy':'Copy link on click',
'autonumScroll':'Scroll to table of contents on click',
'floatingToC':'Floating table of contents',
'hideToC':'Hide table of contents',
'discussionActivity':'Show discussion activity',
'discussionActivityTitleOnly':'Add activity information only to title attribute of sections',
'dateLinksLocalTime':'Display signature dates in local time ([customize])',
'dateLinksLocalTimeUserOptTZ':'Use timezone from account preferences',
'dateLinksLocalTimeRelative':'Show relative date (15 days ago)',
'dateLinksLocalTimeAbsolute':'Show actual date (1 May 2022)',
'AWBtyposCustomTitle':'Page title for RegExTypoFix (not needed on WMF wikis)',
'enableCI':'Custom inserts',
'cI':'One insert/replacement per line. Example insert: foo[FTTCRT]barNUM:<> Example replacement:/lorem/ipsum/:<>',
'autoPostAbove':'Put autopost-type custom inserts/replacements (buttons that insert/replace text and submit the form afterwards) above the text input',
'enableCIThatRun':'Custom regular expressions that are automatically applied on preview/publish',
'cIThatRun':'One replacement per line. Example: /[Ff]ooNUM/bar/g:<>',
'enableCIThatRunCmt':'Custom regular expressions that are applied to comments only',
'cIThatRunCmt':'One replacement per line. Example: /[Ff]ooNUM/bar/g:<>',
'runCIAgain':'Run automatic replacements again after processing markup (useful to apply replacements to URLs that need to be rewritten first)',
'monospace':'Use monospace font in editing window by default (switch with ctrl+alt+m)',
'markdown':'Convert Markdown markup language (partial implementation)',
'stalkAutoSub':'Automatically subscribe when replying',
'stalkStoreInPrefs':'Store subscriptions in account preferences. Takes some extra bandwidth. Allows your subscriptions to be shared across multiple devices/browsers.',
'markNewCmts':'Mark new comments since your last visit to the page',
'markNewCmtsSubbed':'Mark new comments in sections you subscribed to since your last visit to the page',
'stalkAddCycleBtn':'Button to scroll to the next unread comment on a page',
'stalkAddCycleBtnSubbed':'Button to scroll to the next unread comment on a page in sections you subscribed to',
'stalkMarkReadScroll':'Mark comments as read when you scroll them into view',
'stalkWatchListCmts':'[unfinished] Show unread comments from subscribed sections on watchlist',
'stalkTackOnEcho':'Notify me about new comments in the general notification area',
'stalkTackOnMail':'Notify me about new comments by mail (mail can only be send while you are browsing)',
'stalkInterval':'Number of minutes between checks for new comments outside the watchlist:',
'UILabelMobile':'Mobile',
'overlayPad':'Add padding to the fullscreen form on narrow screens',
'MFmarkupAbove':'DUPLICATE:markupAbove',
'MFbarRightAbove':'DUPLICATE:barRightAbove',
'MFAdj':'Customize settings to disable/hide on mobile',
'submitShortcut':'Use ctrl+enter to submit form, ctrl+shift+enter to mark edit as minor and submit form',
'HLreply':'Highlight the comment you\'re replying to',
'HLCmtClick':'Highlight comment on click',
'editCmtDblClick':'Edit comment on double click',
'replyDblClick':'Reply to comment on double click (when both are available and enabled "edit" will take precedence over "reply")',
'previewDblClick':'Refresh preview/diff by double clicking it',
'warnCancel':'Warn before discarding changes',
'reparseConfirm':'Warn before reparsing page content when there are unread comments',
'hideDT':'Hide reply links from DiscussionTools',
'hideDTStats':'Hide discussion activity from DiscussionTools',
'hideDTSub':'Hide subscription links from DiscussionTools if Factotum subscription links are enabled',
'hideNewSec':'Hide regular "new section" link from toolbar',
'methodLocator':'Detect comments by Factotum locator',
'methodLegacy':'Detect comments by legacy method (essential to reply to comments that were not posted with Factotum)',
'extendedSigDetect':'Slightly improve detection rate of legacy signatures at the cost of slightly more CPU-time',
'autoDash':'Automatically prepend my signature with an em dash (—)',
'useLocator':'Add invisible comment locator to my comments, makes for a less buggy experience, DO NOT DISABLE',
'preventDoubleHashtag':'Prevent double hashtag indentation (numbered list inside numbered list)',
'filterDirMarks':'Filter left-to-right/right-to-left (LTR/RTL) marks on wikis using the same direction as the mark when no mark in the other direction is present',
'enableOnDiffOldId':'Enable Factotum within page content on diffs/old revisions. Terrible idea. Don\'t do it. (not needed for full page editing of old revisions)',
'editTheUneditable':'Show full page edit icon nearly everywhere, including special pages and protected pages',
'theStranger':'Add edit links to comments by others. Don\'t expect comment editing to always work well on non-Factotum comments. YMMV.',
'ninjaLoader':'Load links within page content on desktop only after pressing a section header or double-clicking page content (ninja mode)',
'killswitch':'If adding reply/edit/etc buttons takes more than 8 seconds stop adding them',
'recombineNowiki':'Recombine adjacent nowiki tags within syntaxhighlight. Neater wikitext, but when editing such a comment you may be greeted by #tag:syntaxhighlight instead of or nowiki tags may fail to be filtered out.',
'dryRun':'Button to perform a dry run (testing/development ONLY, assume your input will be lost, submits form without making any edits) in advanced tools section (requires advanced tools icon)',
// 'debug':'Enable test/debug mode', //FTT.debug
// 'dbgLimit':'Maximum debug messages to log to console:', //FTT.debug
// 'doubleTimeout':'Assume edit failure after 20 seconds instead of 10 (editing long pages can take slightly more than 10 seconds)',//FTT.debug
'afterPost':'After posting:',
'afterPostReload':'Never re-parse the page, just reload instead',
'watchlist':'Watchlist setting:',
'watchlistexpiry':'Watchlist expiration: ',
'watchlistexpirynew':'Watchlist expiration for page creation: ',
'editor':'Default editor for comments and new sections:',
'editorSwitch':'Button to switch between editors',
'editorSwitchSkipSource':'Skip source editor when switching editors for comments and new sections',
'editorSwitchSkip2010':'Skip 2010 wikitext editor when switching editors for comments and new sections',
'editorSwitchSkipvisual':'Skip VisualLight when switching editors for comments and new sections',
'2010wikitextDefault':'Load 2010 wikitext editor by default when editing a full page or section on wikitext pages',
'2010codepageDefault':'Load 2010 wikitext editor with CodeEditor by default when editing JavaScript/CSS/JSON pages',
'2010templateDefault':'Load 2010 wikitext editor with CodeMirror by default when editing templates',
'2010codeMirror2023':'Always try to enable CodeMirror in the 2010 wikitext editor on wikitext pages (not working properly atm)',
'RLmasq':'Masquerade as reply-link (use plain text links instead of icons)',
'RLmasqSect':'Use plain text links instead of icons for section tools',
'grayscale':'Grayscale icons',
'blacklist':'Never load Factotum when page title matches: (one per line, /[Rr]egEx.*/ allowed)',
'blacklistMain':'Never load Factotum in article/main space',
'newHeadingSubj':'Enter a subject to post as a new subsection',
'same':'(same)',
'redo':'redo',
'shortcutsMap':
'
'+
'
ctrl+enter
submit form / load links when using ninja mode
'+
'
alt+shift+s
submit form
'+
'
ctrl+shift+enter
check as minor+submit form
'+
'
ctrl+alt+m
toggle monospace
'+
'
ctrl+alt+p
scroll to previous header (while holding down ctrl+alt, press enter to collapse/uncollapse the highlighted section if collapsible sections are enabled, same for scroll to next section, press j/arrow left and l/arrow right to cycle focus for header icons/links)
'+
'
ctrl+alt+up
'+
'
ctrl+alt+n
scroll to next header
'+
'
ctrl+alt+down
'+
'
ctrl+alt+shift+p
scroll to previous comment (while holding down ctrl+alt, press j/arrow left and l/arrow right to cycle focus for action icons/links)
'+
'
ctrl+alt+shift+up
'+
'
ctrl+alt+shift+n
scroll to next comment
'+
'
ctrl+alt+shift+down
'+
'
ctrl+k
insert link
'+
'
ctrl+b
bold text
'+
'
ctrl+i
italicize text
'+
'
ctrl+shift+5
strike text
'+
'
ctrl+shift+6
code text
'+
'
ctrl+u
underline text
'+
'
ctrl+, (comma)
sub text
'+
'
ctrl+. (period)
super text
'+
'
ctrl+1
insert level 1 header
'+
'
ctrl+2
insert level 2 header
'+
'
ctrl+3
insert level 3 header
'+
'
ctrl+4
insert level 4 header
'+
'
ctrl+5
insert level 5 header
'+
'
ctrl+6
insert level 6 header
'+
'
ctrl+8
insert quote
'+
'
ctrl+alt+f
advanced tools/search and replace
'+
'
alt+shift+v
view diff
'+
'
alt+shift+i
toggle minor edit
'+
'
ctrl+z
undo
'+
'
ctrl+y
redo
'+
'
ctrl+shift+z
'+
'
alt+i
focus input
'+
'
ctrl+shift+/ (slash)
open settings
'+
'
ctrl+/ (slash)
show keyboard shortcuts
',
'reparseConfirmPopup':'You\'ve just performed an action that requires some of the page content to be refreshed to see the result. If you continue, comments that are currently marked as new will no longer be marked. Continue?',
'backToTop':'Back to top',
'backToBottom':'To bottom',
'findPrev':'Find previous',
'scrollPrevHeader':'Scroll to previous header',
'scrollNextHeader':'Scroll to next header',
'exportSettings':'Currently saved settings:',
'importSettings':'Import settings (JSON):',
'code':'Code',
'underline':'Underline',
'struck':'Struck text',
'nochange':'No change',
};
FTT.mulKeys = Object.keys(FTT.msgsObj.mul);
FTT.B1Obj.mul = { //multilingual and default texts, selected using wgUserLanguage. Messages that are required to load the script and open the form
'htmlform-submit':' 📢 ', //regular spaces are stripped, figure spaces are not
'preview':' 👁 ',
'showdiff':'Diff',
'cancel':' 🗑️ ',
'saveprefs':'Save',
'restoreprefs':'Restore all default settings',
'mw-widgets-copytextlayout-copy':'Copy',
'mw-widgets-copytextlayout-copy-success':'Copied to clipboard.',
'mw-widgets-copytextlayout-copy-fail':'Failed to copy to clipboard.',
'tooltip-summary':'Edit summary',
'actionfailed':'Action failed',
'revid':'revision REVID',
'diff-empty':'(no differences)',
'anoneditwarning':'Unless you log in or sign up your IP address will be publicly visible if you post a message!',
'subject':'',
'newsection':'New section',
'bold':'B',
'italic':'I',
'strike':'xyz',
'Contributions':'Special:Contributions',
'Diff':'Special:Diff',
'Permalink':'Special:PermanentLink',
'thankLink':'Icon to thank for comments',
'flow-thanks-confirmation-special':'Do you want to publicly send thanks for this comment?',
'editsectionhint':'Edit section: $1',
'tooltip-ca-addsection':'Start a new section',
'AWBtyposTitle':'',//AWB RETF page title
'newline':'',//name of template that transcludes a pure newline
'smiley':'',//Template:Smiley title
'atops':'',//Archive top substitution template
'abots':'',//Archive bottom substitution template
'tq':'',//Talk quote inline template or its most common redirect
'citemap':'',//local JSON with template map and parameter map for citation templates
'lockicon':'',//is Twemoji2 1f512.svg available on this wiki?
'ipbother':'Other time:',//for non-English the translation includes a note sometimes that the entered value must be in English
'TZ':'UTC',
'copyrightwarning':'By submitting your input you agree to release your work under the license of this project and to comply with all applicable terms and conditions of this site. (if you are reading this the proper disclaimer failed to load)',
'wikieditor-toolbar-tool-replace-invalidregex':'The regular expression you entered is invalid: $1',
'content-model-wikitext':'wikitext',
'watch':'Watch',
'unwatch':'Unwatch',
};
FTT.clearGender = function(text,intG) {
text = (text || ''); //if text is undefined, turn to empty string
for (intG=0;intG<4;intG++) {
text = text.replace(/\{\{GENDER:\$[0-9]\|(([^\{\}]|\{\{[^\}]+\}\})+)\}\}/,'$1');
}
return text;
};
//lz-string by Pieroxy[https://pieroxy.net/blog/pages/lz-string/index.html], originally licensed WTFPL, Version 2, modified by Alexis Jazz. See w:en:User:Alexis_Jazz/lz-string for details
/*
Just to be safe, here's the current license[https://github.com/pieroxy/lz-string/blob/master/LICENSE], though the package I took lz-string from is WTFPL:
MIT License
Copyright (c) 2013 pieroxy
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
A few details here on how this mess came to be. When looking for a compression method, the first good candidate I found was lz-string by Pieroxy.
It's incredibly small (about 1.5K after min+gzip), WTFPL and achieves fair compression. In UTF16 mode, it rivals base64 pako in characters (not bytes).
Sometimes it compresses better, sometimes worse. It's great for localStorage (always UTF16). In base64 it performs significantly worse. (counting chars)
Pako doesn't do UTF16. In theory a base64>UTF16 conversion could increase the compression ratio further for pako.
But the compression ratio isn't even the main issue. While lz-string claims to be fast, pako is faster with compression. Close to irrelevant on desktop.
But on (older) phones? Decompression with lz-string is as fast if not faster than pako though.
And the primary need for compression is with the subscriptions object (easily the biggest) when stored in account preferences.
Because account preferences are limited to 64K. (though I'll probably double it by using two prefs) And prefs only allow base64.
Second: reliability. lz-string is embedded in Factotum in full, so it's always there. The pako library is much bigger (14K after min+gzip),
so only pako's inflate module is embedded (which is still 7.5K) and mw.deflate (which may well be cached as VE uses it too) is used for compression.
mw.deflate also uses pako, at least currently. But it's more risky as there is no official counterpart to mw.deflate. mw.deflate is used to compress
HTML on the client before uploading to Wikimedia. Because unliky downloads (which can be gzipped and automatically unpacked by any current browser),
uploads are NEVER compressed, so the client has to. The counterpart to mw.deflate only exists on the SERVER side.
The risk is obvious: if mw.deflate is deprecated or suddenly starts to output data in another format, Factotum would instantly break.
And thus lz-string must stay for now as a backup. If mw.deflate breaks, user data can still be decompressed as that part is embedded.
The compression automatically switches to lz-string if the mw.deflate integrity check fails.
For the future, if mw.deflate DOES change or vanish, Factotum will need to either include pako's deflate+mw.deflate wrapper or just stick with lz-string.
But if T312720 gets resolved, the pako inflate module could be removed from Factotum so let's hope that happens before mw.deflate breaks.
Af of 30-10-2022, the Pako module has been dropped from Factotum. The base64<>numbered array conversion was unreliable.
Pako 1.x was perfect as it supported base64 input/output, but with pako 2.x this became something I no longer want to deal with. Lz-string wins this round.
if ( typeof data == 'object' && !Array.isArray(data) ) {
data = JSON.stringify(data);
} else if ( typeof data != 'string' ) {
// if(FTT.debug){FTT.debug('can only compress strings!');} //if you try to deflate an array or object using mw.deflate it says "strm.input.subarray is not a function"
FTT.badThing = data;
throw 'FTT: tried to compress some '+typeof data+', it\'s in FTT.badThing now';
}
if ( format == 'UTF16' ) {
return 'lzsdeflU16,'+FTT.LZString.compressToUTF16(data); //UTF16 is more efficient for localStorage but doesn't work for account prefs
//CRC32 checksum generator. Based on https://simplycalc.com/crc32-source.php ("This source code is in the public domain. You may use, share, modify it freely, without any conditions or restrictions."). Compacted a bit.
// if(FTT.debug){FTT.debug('localStorage no longer failing, saved '+key+' to localStorage, removing from sessionStorage if it exists there as well');}
FTT.rmItemLS(key,'session');
}
} else {
// if(FTT.debug){FTT.debug('saved '+key+' to localStorage, removing from sessionStorage if it exists there as well');}
FTT.rmItemLS(key,'session');
}
};
FTT.getItemLS = function(key,quiet) {
try{FTT.itemInSessionStor = window.sessionStorage.getItem(key);} catch (e) {} // sessionStorage is used as a fallback for localStorage so if it exists it's probably more recent
if ( FTT.itemInSessionStor != null ) {
// if(FTT.debug && ! quiet){FTT.debug('found '+key+' in sessionStorage');}
return FTT.itemInSessionStor;
} else {
FTT.itemInLS = mw.storage.get(key);
if ( FTT.itemInLS != null ) {
// if(FTT.debug){FTT.debug('found '+key+' in localStorage');}
return FTT.itemInLS;
} else {
// if(FTT.debug){FTT.debug(key+' not found in sessionStorage either, sorry');}
return null;
}
}
};
FTT.rmItemLS = function(key,type) {
if ( ! type || type == 'local' ) {
// if(FTT.debug){FTT.debug('removing '+key+' from localStorage');}
mw.storage.remove(key);
}
if ( ! type || type == 'session' ) {
// if(FTT.debug){FTT.debug('removing '+key+' from sessionStorage');}
if ( typeof window.FTTBasicmsgsObj != 'undefined' && window.FTTBasicmsgsObj[ FTT.userLang ] ) { // FTTBasicmsgsObj could be specified in common.js before FTT is loaded
if ( ! FTT.B1Array[FTT.basicLangKeysInt].match('⧼') ) { //if it does match ⧼ it's probably a message that doesn't exist on this MediaWiki installation, it should fallback to the message in FTT.B1Obj.mul
//Fair question: is there no easier way to do this? Possibly, but none that I know of. {{#time:M}} would work in some cases (like dewiki), but not on zh-min-nan for example. That would also require ParserFunctions to be available, while true on Wikimedia that may not be the case on every wiki.
FTT.getMonthNames = function(mInt) {
if ( FTT.monthNames ) {
// FTT.debug('already know the months');
return;
}
FTT.monthNames = M1('wgMonthNames'); // months as found in signatures.. well, often anyways
if ( ['ha'].includes(M1('wgContentLanguage')) ) {
for(mInt=1;mInt<13;mInt++){
FTT.monthNames[mInt] = M1('wgMonthNames')[mInt].replace(/,/g,''); //commas on hawiki wtf why
}
}
try {FTT.testmg1 = FTT.B1.mg1;} catch (e) {}
if ( ['be','be-tarask','crh','cu','cv','dsb','el','kaa','koi','kk','kv','ky','la','lbe','mdf','mrj','myv','pnt','os','ru','rue','sah','se','tg','tyv','udm','uk','xal'].includes(M1('wgContentLanguage')) ) {
// FTT.debug('use genitive months as found in MediaWiki:january-gen etc.');
if ( ! FTT.testmg1
FTT.testmg1 == '' ) {
// FTT.debug('basicmsgs not available yet');
delete FTT.monthNames;
return false;
}
FTT.monthNames = [''];
for ( mInt=1;mInt<13;mInt++){
FTT.monthNames.push(FTT.B1['mg'+mInt]);
}
}
if ( ['an','ast','ay','bar','br','ca','cbk-zam','co','csb','da','de','eml','eo','es','ext','fo','frr','fur','fy','gsw','io','it','kl','ksh','lad','lb','li','lid','lij','lld','lmo','nah','nap','nb','nds','nds-nl','nl','no','pdc','pfl','pl','pms','qu','roa-tara','sc','scn','srn','stq','szl','tt','vec','vls','zea'].includes(M1('wgContentLanguage')) ) {
// FTT.debug('use abbreviated months as found in MediaWiki:jan etc.');
if ( ! FTT.testmg1
FTT.testmg1 == '' ) {
// FTT.debug('basicmsgs not available yet');
delete FTT.monthNames;
return false;
}
FTT.monthNames = [''];
for ( mInt=1;mInt<13;mInt++){
FTT.monthNames.push(FTT.B1['ma'+mInt].replace(/\./g,'')); //at least nowiki has periods..
}
}
if ( ['ami','cdo','cs','fi','gn','hak','hy','ii','jbo','lzh','nan','olo','pwn','szy','tay','trv','za'].includes(M1('wgContentLanguage')) ) {
if ( ['sq'].includes(M1('wgContentLanguage')) ) { //sqwiki changed from capitalized to non-capitalized months at some point, this allows replying to old comments. Which DT can't
FTT.signDateRegExp = new RegExp('([0-2]?[0-9]:[0-5][0-9] |[0-3]?[0-9] |(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) |[0-9]{4} |\\([A-Z0-9\+\-]{1,9}\\)){5}'); //English
//Largely universal regex, adjustments are made by signDateCleanerFunc and cleanTimestamp. In some ways, customizing the regex for a language would be easier than adjusting the input.
//But the regexes get complicated real fast, and to actually convert anything a function would still be needed.
\([^a-zA-Z]*\) = jawiki, kowiki (matches e.g. (日) for "sunday", FTT doesn't need the day of week. May match similar stuff as well, anything within brackets that isn't a timezone would invalidate the timestamp
ngày = viwiki (day)
năm = viwiki (year)
kell[o]? = fiwiki (kello=clock, used like "kello 12:00"), et (kell=clock)
x ב x = hewiki (gets appended to months, encapsulated with X because RTL)
ás = glwiki
ж. = kkwiki
gada / plkst. = lvwiki
d[ea] = brwiki
tme = smnwiki
di/lis: furwiki
*/
FTT.cleanTimestampReplaceChar = FTT.B1.timestamp.match(/(، | [aà] | kl\. | d[ae] | \([^a-zA-Z]*\) | ngày | năm | kell[o]? | ב| ás | ж\. | (gada|plkst\.) | d\' | b\. | dii\. | tme |lis | j\. | числа, )/g); //this can only run after basicmsgs have been loaded
return;
};
FTT.pageLoadTimeStamp = new Date().toISOString(); //to detect new comments since loading the page
'cIThatRun':[''], //regexp that get automatically executed
'enableCIThatRunCmt':true,
'cIThatRunCmt':[''], //regexp that get automatically executed but only on comments
'runCIAgain':false,
'monospace':false,
'markdown':false,
'bbcode':false,
'outdent':10,
'sumSnippet':true,
'saveDraft':true,
'pingText':FTT.pingText,
'pingTextInSection':FTT.pingTextInSection,
'neverPing':'',
'rewritun':true,
'rewriteOnBlur':false,
'runRewritunAgain':false,
'rewritunOther':'',
'runCIOther':'',
'AWBtyposOther':'',
'UILabelSubscribe':'TAB',
'stalkAddSubLinks':false,
'stalkMaxSubsSize':64,
'stalkAutoSub':false,
'stalkStoreInPrefs':false,
'markNewCmts':false, //disabled by default again. If you are logged out and your localStorage is broken everything is always new which is a bit annoying
if ( FTT.MD.reminder.subbedMsgDate > FTT.timestampInit ) { //ignore reminders in the past, since we don't keep track of delivered reminders due to storage limits these would get delivered again and again otherwise
if ( FTT.MD.reminder.existingMsgs.indexOf(FTT.MD.reminder.subbedMsgJSONdata.msg) == -1 && FTT.MD.reminder.addedFromSubs < 4 ) { //add no more than 3 messages per page per hourly check to prevent sudden floods
FTT.MD.reminder.newFromSubs = JSON.stringify(FTT.MD.reminder.data); //this is better not compressed. Preferences are gzipped when downloaded and not updated often enough to warrant the reduced upload size. So don't waste CPU time on compression.
// FTT.debug('deliver reminders to userjs-FTTTackOnEchoGlobal and remove delivered reminders from userjs-FTTMemoria (' + FTT.MD.reminder.prefType+')');
FTT.MD.reminder.formUserInputs[num] = new OO.ui.TextInputWidget({value:user,classes:['FTTReminderUserInput'],placeholder:FTT.B1['userlogin-yourname'],title:FTT.B1['userlogin-yourname']});
FTT.MD.reminder.formLinkInputs[num] = new OO.ui.TextInputWidget({value:link,classes:['FTTReminderLinkInput'],placeholder:FTT.B1['pagelang-name'],title:FTT.B1['pagelang-name']});
FTT.MD.reminder.formMsgInputs[num] = new OO.ui.TextInputWidget({value:msg,classes:['FTTReminderMsgInput'],placeholder:FTT.B1.emailmessage.replace(/:/,),title:FTT.B1.emailmessage.replace(/:/,)});
FTT.MD.reminder.formHorizontalLayouts[num] = new OO.ui.HorizontalLayout({items:[FTT.MD.reminder.formDateInputs[num],FTT.MD.reminder.formRepeatInputs[num],FTT.MD.reminder.formIconInputs[num],FTT.MD.reminder.formUserInputs[num],FTT.MD.reminder.formLinkInputs[num],FTT.MD.reminder.formMsgInputs[num]]});
if ( typeof FTT.loadedModules == 'object' && FTT.loadedModules.length>0 ) {
// FTT.debug('applyModules: modules found, looking for type '+a);
FTT.moduleButtons = {};
for(FTT.loadedModulesInt=0;FTT.loadedModulesInt
// if ( a != 'afterDefaultSettings' ) { FTT.debug('applyModules: check module #' + FTT.loadedModulesInt); }//debug not available yet at afterDefaultSettings
if ( FTT.loadedModules[FTT.loadedModulesInt].load.includes(a) ) { //execute some function contained within the module
// FTT.debug('applyModules: found module for type '+a);
// if ( a != 'afterDefaultSettings' ) { FTT.debug('applyModules: module includes "'+a+'" loadtype'); }
if ( typeof FTT.loadedModules[FTT.loadedModulesInt][a+'Func'] == 'function' ) {
// if ( a != 'afterDefaultSettings' ) { FTT.debug('applyModules: run function from module with type '+a); }
//hack for signatures in templates on enwiki, fixing them on the fly
if ( FTT.PRMOpened.pageTitle.match(/:Requested moves/) ) {
text = text.replace(/(\[\^\n\]\*\]\])/g,'$1PIPE_UNIQ_HACK$2').replace(/(\{\{RMassist\/core.*)(\|sig=)([^\}\|]*)(.*\}\})/g,'$1$4 $3').replace(/PIPE_UNIQ_HACK/g,'|')+'\n';
FTT.queueUpdatePref = function(type, key, val, instanceID, int) { //stuffs prefs you want to change in localStorage (so works across tabs), if no further prefs have changed for 1.2s it saves them. If you close a tab before that in theory it should get picked up on the next page load
//just call this function for every change that needs to be made, the changes will automatically be combined within one request (when made within 1.2 seconds)
// FTT.debug('queueUpdatePref: allowing 1.2s to accumulate changes to preferences');
instanceID = Math.random();
FTT.queueUpdatePrefInstanceID = instanceID;
FTT.setPrefLocal = {};
FTT.setPrefLocal[key] = val;
mw.user.options.set(FTT.setPrefLocal); //this changes the value in mw.user.options (non-persistent) so we don't have to reload the page to apply the settings. We update it early so if some other process reads this pref it'll get the latest version so later requests won't be based on old data
{ options:{}, globalpreferences:{}, globalpreferenceoverrides:{} } ); //First try localStorage. If LS isn't working, see if FTT.prefQueue already exists for just this page. If not, init new object
{ options:{}, globalpreferences:{}, globalpreferenceoverrides:{} } ); //First try localStorage. If LS isn't working, see if FTT.prefQueue already exists for just this page. If not, init new object
FTT.prefQueueoptionsArr = [];
for ( int=0;int
if ( Object.values(FTT.prefQueue.options)[int] == '' ) {
if ( mw.user.options.get(Object.keys(FTT.prefQueue.globalpreferences)[int]+'-local-exception') ) {
FTT.prefQueueoptionsArr.push(Object.keys(FTT.prefQueue.globalpreferences)[int]+'-local-exception'); //remove local exception when setting global preference
api.postWithEditToken( {format: 'json', assert:FTT.assert, action: type, change:change } ).then( function ( data ) { //change is an array, e.g.: ['userjs-FTTSubs=','userjs-FTTSubs2=']. Do not use alternative multiple-value separator for change parameter: T306319
if ( navigator.userAgent.match(/Firefox/) && $('html.client-dark-mode')[0]) { //Dark mode gadget on Firefox
FTT.pageXOffset = window.pageXOffset;
FTT.pageYOffset = window.pageYOffset;
// FTT.debug('seems you have dark mode enabled and are using Firefox, scroll to top due to Firefox bug https://bugzilla.mozilla.org/show_bug.cgi?id=1650522');
window.scrollTo(0,0);
} else {
delete FTT.pageXOffset;
delete FTT.pageYOffset;
}
};
FTT.popup = function(msg,removeElement,isEncoded) { //second argument allows an element to be removed after the popup is dismissed so it doesn't linger invisibly
// FTT.debug('you\'re leaving? guess this error won\'t interest you then.');
var DelayedScrew = setInterval(function () { //sometimes errors seem to happen BECAUSE we're leaving the page (which probably causes http requests to get canceled), but just in case the user decides to stay, just delay this error. 5 seconds should be enough to finalize leaving.
FTT.svgFTTPingIconData = FTT.svgFTTIconData.replace('%3C%2Fdefs%3E','%3C%2Fdefs%3E%3Cg%20opacity%3D%220.5%22%3E').replace('%3C%2Fsvg%3E',D1('')); //Speech bubble with arrow to insert mention
FTT.svgFTTIconLinkArrowData = FTT.svgFTTIconLinkData.replace('%3C%2Fdefs%3E','%3C%2Fdefs%3E%3Cg%20opacity%3D%220.5%22%3E').replace('%3C%2Fsvg%3E',D1('')); //Link icon with arrow to insert link to comment
if ( FTT.origBGColor.match(/#[0-9A-Fa-f]{6}/) ) { //convert #ff00ff to rgb(255,0,255). This allows adding transparency like rgba(255,0,255,0.5) later using FTT.origBGColor.replace(/^rgb\((.*)\)$/,'rgba($1,0.5)')
'.FTTSVG>.FTTScreenReaderLabel{color:rgba(0,0,0,0);font-size:0.2px !important;user-select:none;-ms-user-select:none;-webkit-user-select:none}'+ //very tiny text with 0% opacity, selection disabled to prevent copy-pasting, screen readers should pick it up though
'#FTTUITextInput .wikiEditor-ui-text>textarea{max-height:75vh}'+//2010 wikitext editor tends to merge with the summary and other bits when using a stupidly low viewport. Why that textarea can't just be contained within #FTTUITextInput I will never know. At 75vh it triggers less easily.
'.content .collapsible-heading a.edit-page{display:none}'+ //why do I have to fix Minerva?? just changing visibility means it still takes up space, you have to use display.. am I missing something?
'@media screen and (max-width:600px){.skin-minerva.FTTWithHeadingLinks .content .mw-parser-output h1,.skin-minerva.FTTWithHeadingLinks .content .mw-parser-output h2,.skin-minerva.FTTWithHeadingLinks .content .mw-parser-output h3,.skin-minerva.FTTWithHeadingLinks .content .mw-parser-output h4,.skin-minerva.FTTWithHeadingLinks .content .mw-parser-output h5,.skin-minerva.FTTWithHeadingLinks .content .mw-parser-output h6{display:inline-block;width:100%}}'+ //why are headers flex??? okay with infoboxes it can make sense, but at limited width?
'.client-js .ns-0 #FTTPreviewBox ol>li>ul{display: unset !important}'+ //hack to show collapsed quotations on English Wiktionary, which they themselves collapsed with what amounts to a hack. Should have been possible to use mw-collapsible (for personal reference: mw.loader.using('jquery.makeCollapsible').then( function () {$($('.mw-collapsible')[0]).makeCollapsible()}); ) or better yet DON'T collapse quotations but nooooooo
FTT.previewHeightFix = function(A) { //DOM polling is no good, but I see no other way that isn't stupidly complicated
A = true; //forces the adjustment to activate the first time
var heightFix = setInterval(function(){ //sometimes null gets registered in FTT.processElementArray despite the element really existing.. If it was just changed to adjust for the real (serverside) timestamp there should still be an element there, even if it would no longer be attached to the DOM. So I guess maybe sometimes the element just isn't available in the DOM fast enough? In that case, this delay should help
FTT.shiftToOverlay = function(int) { // the overlay is removed by FTT.cancelReply()
if ( window.innerWidth > FTT.settings.overlayThreshold && ( !FTT.isMobile
!FTT.isDiscussionPage ) ) {
// FTT.debug('shiftToOverlay: your viewport exceeds the threshold for overlay mode and you\'re either not on the mobile site or not on a talk page, skip overlay');
FTT.autoCollapseButtons = $(element+'.autocollapse .mw-collapsible-toggle'); //not sure why makeCollapsible doesn't take care of this. Maybe another function
([^]*)<\/p>[\s]*/,'$1').replace(/]*>(]*><\/span>)*<\/span>/,'');//get rid of P tags that would otherwise break up comment and signature when editing an existing comment
if ( FTT.PRMOpened.freshindent && FTT.PRMOpened.type == 'edit' ) {
// FTT.debug('preview starts with a section header. Moving section header outside the previewAfterParseBox so editing the comment right after posting won\'t destroy the header');
// FTT.debug('recorded wrong locator ID for preview: ' + FTT.wrongLocatorID + ', this will be replaced when we get confirmation from the server with the correct timestamp');
$('.FTTPreviewAfterPost .ext-discussiontools-init-replylink-buttons,.FTTPreviewAfterPost .ext-discussiontools-init-section-subscribe').remove();//taking out the trash. not like these could have ever worked like this..
} else if ( FTT.PRMOpened.type == 'edit' ) {
// FTT.debug('editing a comment, not from the preview after posting');
FTT.cancelReply('commentedit');
FTT.locatorInHTMLRegExp = new RegExp('([^]*)()(([^<]|<(?![\/]?span>|))*)*)*<\/span>)');
if ( document.getElementById('FTTLink-' + FTT.escapeHTML(FTT.PRMOpened.id)).parentElement.parentElement.innerHTML.match(FTT.locatorInHTMLRegExp) ) {
var DelayedAssociateElementParse = setInterval(function(){ //sometimes null gets registered in FTT.processElementArray despite the element really existing.. If it was just changed to adjust for the real (serverside) timestamp there should still be an element there, even if it would no longer be attached to the DOM. So I guess maybe sometimes the element just isn't available in the DOM fast enough? In that case, this delay should help
clearInterval(DelayedAssociateElementParse);
$(document).ready(function() {
FTT.processElementArray[FTT.PRMOpened.int] = document.getElementById('FTTLink-' + FTT.escapeHTML(FTT.PRMOpened.id)).parentElement; //the replacement above means the .FTTCmt from processElementArray is no longer attached to the DOM. We update it in case you want to reply to yourself.
});
}, 2000);
} else {
// FTT.debug('locator not found in wikitext, you are probably editing some comment without one. This is incompatible with parse-in-place');
$('#content').addClass('FTTEaseIn FTTPurpleBG'); //can't open new reply forms
FTT.mustReload = true; //but we can't until we have edit confirmation
}
} else if ( document.getElementById('FTTForm-' + FTT.PRMOpened.id) ) {
document.getElementById('PreviewAfterPost').id = 'PreviewAfterPost-' + D2(FTT.newCommentID.slice(1)); //keep IDs unique if a user posts multiple comments without reloading
}
$('.FTTPurpleBG').removeClass('FTTPurpleBG');
var DelayAddRL1 = setInterval(function(){clearInterval(DelayAddRL1);
FTT.addReplyLinkTo(FTT.previewPostedParams); //adds edit link
}, 2500); //must be higher than the delay for DelayedAssociateElementParse
$('.FTTLinks').removeClass('FTTNoDisplay');
}
if ( apiResponse.parse.text.match('mw-collapsible') ) {
FTT.makeCollapsible();
}
} else if (params.action == 'parse' && (mode == 'preview'
mode == 'livepreview'
mode == 'diff') ) {
// FTT.debug('Loading preview');
FTT.smartViewingDiff = 0;
if ( ! $('#FTTPreviewBox')[0] ) {
// FTT.debug('form closed? trash preview');
FTT.doPreviewInProgress = 0;
FTT.runSmartLivePreview = false;
return;
}
FTT.UIPreviewButton.setDisabled(false);
FTT.reappliedSmart = FTT.reApplySmart(apiResponse.parse.text); //re-apply smartLivePreview updates since making the parsed preview request
FTT.newInitDate = new Date(apiResponse.edit.newtimestamp).getTime()+1000; //will only be applied if parse-in-place is used. This is used to prevent false positives due to server and client clocks not being synchronized when when checking for new comments
}
if ( FTT.wipeSectionParams ) {
// FTT.debug('copied a section to another page, wiping section from original page');
FTT.PRM[FTT.previewPostedParams.int] = FTT.previewPostedParams; //registering the new comment in the internal administration
if ( FTT.newCommentID.slice(1) != '' ) {
var DelayedAssociateElementEdit = setInterval(function(){
clearInterval(DelayedAssociateElementEdit);
$(document).ready(function() {
FTT.processElementArray[FTT.previewPostedParams.int] = document.getElementById(D2(FTT.newCommentID.slice(1))); //associating a DOM element with the new comment
document.getElementById('PreviewAfterPost').id = 'PreviewAfterPost-' + FTT.escapeHTML(D2(FTT.newCommentID.slice(1))); //keep IDs unique if a user posts multiple comments without reloading
var DelayAddRL2 = setInterval(function(){clearInterval(DelayAddRL2);
FTT.addReplyLinkTo(FTT.previewPostedParams); //adds edit link
}, 2500);
}
}
if ( $('.FTTForm')[0] ) {
// FTT.debug('the form hadn\'t been removed, possibly due to some "awww shit", possibly triggered by a timeout. Removing the form now, including any attached "awww shit" as apparently the edit came through after all.');
// FTT.debug('encountered either an edit conflict or FTT is performing null edits. (which it never should) So which is it? Can somebody PLEASE look at T299809? ' + FTT.editConflictRetries + ' tries left.');
// if ( FTT.settings.debug ) { window.alert('no API response: '+extra); }//FTT.debug
FTT.addScrewedLink('no_response','Received no response from API. Is your internet plugged in?'+FTT.APIErrorExtra); // may need to disable again if this is still popping up when leaving a page
}
};
E1 = function(text) {
if ( ['string','number','object'].includes(typeof text) ) {
return mw.util.escapeRegExp(text.toString()); //in case the input is a number (happens with timestamps)
// FTT.debug('genPermaLink: no FTTThanks localStorage object found, will initiate one');
FTT.thankedComments = {};
}
if ( Object.keys(FTT.thankedComments).length > 500 ) {//remove the two oldest thanks given, only the last 500 thanked edits are remembered so localStorage can't overflow
} else {// section permalinks are "ugly" (not using Special:Permalink) because the "ugly" variant includes the page title which FTT can use while rewriting.
// FTT.debug('add field with permalink to processElementArray #' + PRM.int + ' with ID FTTLink-' + FTT.escapeHTML(PRM.id).replace(/([\-\.:%\/\(\)\!\?])/g,'\\$1'));
// FTT.debug('initRewritunRegExps: basicmsgs not loaded yet');
return false;
}
urlPre = '([^\[\}\|"\'\/]|^)http[s]?\\:\\/\\/([a-z\-]{0,10})(\.m)?[\\.]?'; //3 groups
projects = '(wikipedia\\.org|wikimedia\\.org|mediawiki\\.org|wikidata\\.org|wikibooks\\.org|wiktionary\\.org|wikinews\\.org|wikiquote\\.org|wikisource\\.org|wikiversity\\.org|wikivoyage\\.org|'+E1(M1('wgServerName'))+')'; //1 group
//if your wgArticlePath would include something *after* the $1 this is going to be a problem
pre = '('+E1(M1('wgScript'))+'|'+E1(M1('wgArticlePath')).replace('\\$1','')+')'; //1 group
pretitle = '('+E1(M1('wgScript'))+'\\?title\=|'+E1(M1('wgArticlePath')).replace('\\$1','')+')'; //1 group
title = '(([^\\n\\.\\,\\?\\|\\& #]|[\\.\\,][^\\n\\& #])*)'; //2 groups
anchor = '(#[^#\\s\\n]*)?'; //1 group
AL = '([\\.\\,]( |\n|$)| |\\n|\\?($| |\\n)|$)'; //3 groups, character(s) after link
FTT.rewritunRegExps = { //these only attempt to rewrite URLs that occur naturally within MediaWiki and may be copy-pasted, **not** every technically valid URL
'diffbare':[new RegExp(urlPre+projects+pre+'\\?diff\=([0-9]+)(\\&oldid\=[0-9]*)?'+anchor+AL,'g'),'$1' + FTT.B1.difflinknamebare.replace('diffID','$6') + '$9'],//https://en.wikipedia.org/w/index.php?diff=1 (you'd get this from your address bar after following a Special:Diff/1 link)
'oldidbare':[new RegExp(urlPre+projects+pre+'\\?oldid\=([0-9]*)'+anchor+AL,'g'),'$1' + FTT.B1.revid.replace('REVID','$6') + '$8'],//https://en.wikipedia.org/w/index.php?oldid=1085167190 (you'd get this from your address bar after following a Special:Permalink/1 link)
'diffprevnextreverse':[new RegExp(urlPre+projects+pre+'\\?(title\='+title+'\\&)?oldid\=(next|prev)\\&diff\=([0-9]*)'+AL,'g'),'$1$7' + FTT.B1.difflinknameprevnext.replace('diffID','$10') + '$11'],//https://en.wikipedia.org/w/index.php?title=Wikipedia_talk:What_Wikipedia_is_not&oldid=prev&diff=1103993080 got there from a notification due to a mention in an edit summary
'nonlocalwikidiff':[new RegExp('(\\[\\[INTERWIKI:[a-z][a-z:\.\-]*(?!Special:Diff)\/)(' + E1(FTT.B1.Diff) + ')','g'),'$1Special:Diff'],//for interwiki links the special page names are rewritten back to the canonical (English) names. Those work everywhere. I could store or request the localized name for diff/permalink/history, but frankly, my dear, I don't give a damn
'localwiki':[new RegExp('\\[\\[INTERWIKI:[\\.]?' + E1(M1('wgServerName')),'g'),'INTERWIKI[['], //the lone optional period after INTERWIKI: is for wikis without a subdomain (commonly $2 above) like EN.wikipedia.org. Does is always the case for wgServerName matches like example.fandom.com as the "example" is already in wgServerName, so there's no unspecified subdomain
//We select the preceding character to ensure we're not replacing links that aren't bare. We select the trailing character to ensure we found the end of the URL. If two links that are caught by the same RegExp (e.g. two history links) have only one character between them (like a space or newline) they can't both match, so we run the RegExp twice.
// FTT.debug('rewritun forms.gle'); //Google forms URL shortener, converts https://forms.gle/t1M6XLkgiHTL4XQW6 to https://docs.google.com/forms/d/e/1FAIpQLSdLK83vxevNXMPxfDKqZbDBb9VHBGGa6B-mwKIBcHalwSnNTw/closedform so can't automatically convert. Wrap in nowiki instead so at least it doesn't trigger spamblacklist
$('body').on('click',function() { FTT.addRewritunToOther(); });//the reason we don't simply do this on load is that input fields that need rewriting may not exist yet, for example if you haven't opened Twinkle yet, so we check on every click.
FTT.RETF(this.value,'AWBtyposOther',this); //we pass the element in question to RETF as RETF will just call itself after downloading the list the first time it runs. It needs to know to which element to apply the stuff when it calls itself
$('body').on('click',function() { FTT.addAWBtyposToOther(); });//the reason we don't simply do this on load is that input fields that need rewriting may not exist yet, for example if you haven't opened Twinkle yet, so we check on every click.
}
FTT.safeText = function(text,mode,skiplinks) {
if ( ! text ) {
return '';
}
// FTT.safeTextStart = new Date().getTime();//FTT.debug
FTT.safedMarker = 'FTTSAFED'+FTT.semiRandom;
if ( mode == 'unsafe' ) {
// FTT.debug('safeText: return text to original form');
//THE ORDER OF THE FOLLOWING MUST BE THE REVERSE OF THE REPLACEMENTS!!
for (FTT.safeTextPlaceholdersInt=0;FTT.safeTextPlaceholdersInt
if ( FTT['RETFEscapeMatched' + FTT.safeTextPlaceholders[FTT.safeTextPlaceholdersInt]] ) {
// FTT.debug('safeText: replace placeholder "'+FTT.safedMarker+FTT.safeTextPlaceholders[FTT.safeTextPlaceholdersInt] + ' with original content');
for ( FTT.RETFEscapeInt=0;FTT.RETFEscapeInt
text = text.replace(new RegExp(FTT.safedMarker + FTT.safeTextPlaceholders[FTT.safeTextPlaceholdersInt]),FTT.escapeReplacement(FTT['RETFEscapeMatched' + FTT.safeTextPlaceholders[FTT.safeTextPlaceholdersInt]][FTT.RETFEscapeInt]));
}
}
}
// FTT.debug('restored text in ' + (new Date().getTime() - FTT.safeTextStart) + 'ms');
return text;
} else if ( ! text.match(new RegExp(FTT.safedMarker)) ) {
// FTT.debug('safeText: replace tags/links etc with placeholders to stop replacements from matching'); //A note on the order here. It's best to safe items that may contain other items first. So safe ref tags first as they may contain templates. Safe templates before links as they may contain links. A template or link that was already safed as part of something bigger doesn't need to be safed individually, which makes the process faster.
FTT.RETFEscapeNew = text;
// FTT.debug('safeText: safing nowiki tags..');
FTT.RETFEscapeMatchedNOWIKIRegExp = new RegExp('<([Nn]o[Ww]iki|NOWIKI)>(([^\^<]|<(?!\/([Nn]o[Ww]iki>|NOWIKI>)))*)<\/([Nn]o[Ww]iki|NOWIKI)>','g');
FTT.RETFEscapeMatchedSTLRegExp = new RegExp('<([Ss]yntax[Hh]igh[Ll]ight[^>]*|SYNTAXHIGHLIGHT[^>]*)>(([^<]|<(?!\/([Ss]yntax[Hh]igh[Ll]ight>|SYNTAXHIGHLIGHT>)))*)<\/([Ss]yntax[Hh]igh[Ll]ight|SYNTAXHIGHLIGHT)>','g');
if ( FTT.settings.rewritun && mode == 'rewritun' ) { //we rewrite URLs here because links in HTML comments etc have already been safed here but links haven't been safed yet.
// FTT.debug('found non-empty regexp to be applied automatically and you\'re not editing a full page or section ' + (RCCINt + 1) + '/'+FTT.settings.enableCIThatRunCmt);
if ( D2(FTT.settings.cIThatRunCmt[RCCINt]).match(FTT.insertIsRegExpRegExp) ) {
// FTT.debug('a new section input or a newline template is not known AND you\'re posting a new (sub)section which is unindented AND your message appears to contains some multiline pre/nowiki/syntaxhighlight. OR you are editing a multiline new section comment. At the cost being unable to edit your whole comment (you could still edit the whole section of course), we\'ll skip the conversion to a single line for this comment.');
return text;
}
// FTT.debug('converting comment to a single line');
FTT.multilineText1 = ' \n \n' + text;
FTT.multilineText2 = '';
FTT.listInt = 0;
FTT.multilineText1 = FTT.multilineText1.replace(/<([\/])?([Ss]yntax[Hh]igh[Ll]ight|SYNTAXHIGHLIGHT)([^>\n]*)>/g,'<$1syntaxhighlight$3>').replace(/(]*)lang=""/,'$1lang=text').replace(/(]*)lang=["]?[Jj][Ss]["]?/,'$1lang=javascript'); //empty lang and "js" are both invalid and result in maintenance cats
FTT.orderedListItemRegExp = new RegExp('[\n]?FTTORDEREDLISTITEM'+FTT.semiRandom+'(.*)','g');
FTT.multilineText2 = FTT.multilineText1;
FTT.multilineText1 = FTT.multilineText1.replace(/(<[n]owiki>)(([^<\n]|<(?!\/[n]owiki>))*)<(?!\/[n]owiki><\/code>)(([^<]|<(?!\/[n]owiki><\/code>))*)(<\/[n]owiki><\/code>)/g,'$1$2<$4$6') //tags within a combination of code-nowiki should pretty much exclusively get escaped. Ideally we'd figure out which tag with nowiki effect is the least nested, but that's a major headache
.replace(/([^\|\=][ ]*)\n([^\|\=])/g,'$1FTTNOTOUCHNEWLINE'+FTT.semiRandom+'$2') //escape newlines that isn't immediately next to a pipe or equal sign, either not part of a template or part of a multiline template parameter, just leave it
.replace(/\{\{(([^\{\}\n]|\{[^\{\n]|\}[^\}\n]|\{\{[^\{\}\n]*\}\})*)\n(((([^\{\}]|\{[^\{]|\}[^\}]|\{\{[^\{\}]*\}\})*)\n)*)([^\}]*)\}\}/g,'{{$1$3$7}}') //This removes one newline from multiline templates. Only one, so it has to be applied as many times as the number of lines a template takes up. In SOME cases the result is possibly not desirable, but in those cases whatever you were trying to do probably wasn't going to work in an indented comment anyway. Note: you can only nest one level. So {{x(newline)|{{y}}}} would work, but {{x(newline)|{{y|{{z}}}}}} wouldn't.
.replace(new RegExp('FTTNOTOUCHNEWLINE'+FTT.semiRandom,'g'),'\n') //put escaped newlines back
.replace(/<([Pp]re|PRE)([^>\n]*)>(([^<\n]|<(?![\/]?[Pp]re))*)\n(([^<]|<(?![\/]?[Pp]re))*)<\/[Pp]re>/g,'$5') //pre with at least one newline in it
if ( FTT.settings.recombineNowiki ) { //to combine (read "nowiki" where it says "lorem?") {{ into {{, can prevent conversion from {{#tag:syntaxhighlight to
( ! FTT.isDiscussionPage && ! $('.FTTCmt,.LegacyCmt')[0] && ['FCL','comment'].indexOf(FTT.PRMOpened.type) == -1 ) ) ) { //if the type is FCL or comment we could be dealing with a transcluded page like in The Signpost
// FTT.debug('found NOSIGN or this is an edit of an existing comment or not a talk page, skipping signature');
FTT.detectedNOSIGN = true;
return text.replace(/[ ]*NOSIGN$/,'');
} else {
// FTT.debug('adding signature');
FTT.signatureAdded = true;
FTT.addSigSeparator = ' ';
if ( text.trim().match(/( |<\/([Pp]re|PRE|[Ss]yntax[Hh]igh[Ll]ight|SYNTAXHIGHLIGHT)>|\}\})$/)
text.match(/\n$/) ) {
FTT.addSigSeparator = '';
}
if ( mode == 'livepreview' ) {
FTT.addSigSeparator = ''+FTT.addSigSeparator+''; //class to easily filter this node from smart live preview for performance reasons
}
if ( FTT.settings.autoDash && ! FTT.flattenWikiText(mw.user.options.get('nickname')
'' ).match(/^[ ]?[\-\—\/]/) ) {
FTT.addSigSeparator = FTT.addSigSeparator+'— ';
// FTT.debug('your username doesn\'t start with a dash yet');
$('#mw-content-text').removeClass('FTTNoDisplay'); //full page editing can hide page content
$('#FTTnSecBottom').removeClass('FTTNoDisplay');
$('#mw-content-text .FTTFirstReply').removeClass('FTTNoDisplay'); //re-enable reply-to-section-starter speech balloons that were hidden when opening the form
// FTT.debug('filtering left-to-right marks from text as this is a left-to-right wiki already and there are no right-to-left marks in the input. The marks probably result from copy-pasting.');
// FTT.debug('filtering right-to-left marks from text as this is a right-to-left wiki already and there are no left-to-right marks in the input. The marks probably result from copy-pasting.');
text = text.replace(/\u200f/g,'');
}
}
text = text.replace(/(<[Ss]yntax[Hh]igh[Ll]ight)(>[^]*<\/[Ss]yntax[Hh]igh[Ll]ight>)/g,'$1 lang=text$2');
if ( !['preview','livepreview','toVisual'].includes(mode) ) {
text = text.trim(); //replace(/[\s]*$/, '');
}
text = FTT.applyModules('processComment',text);
if ( FTT.settings.enableCIThatRun || FTT.settings.enableCIThatRunCmt ) {
// FTT.debug('applying your regular expressions');
text = FTT.safeText(text,'rewritun'); //rewritun is done inside safing as rewritun can't be done after safing (when the URLs are safed) but is preferably done after HTML comments etc have been safed
text = text.replace(/\*\*(([^\*\n]|\*[^\*])+)\*\*/g, 'FTTBOLDTAGOPEN$1FTTBOLDTAGCLOSE').replace(/(^|[^:\[])\/\/(([^\/\:]|\:..|\/[^\/])+)(?![:\[])\/\/($|[^\/])/g, '$1FTTITALICTAGOPEN$2FTTITALICTAGCLOSE$4');//the :.. allows a URL within italicized text
text = text.replace(/FTTBOLDTAGOPEN(([^F]|F(?!TTBOLDTAGCLOSE))+)FTTBOLDTAGCLOSE/g,'$1').replace(/FTTITALICTAGOPEN(([^F]|F(?!TTITALICTAGCLOSE))*)FTTITALICTAGCLOSE/g,'$1');
// FTT.debug('return processed text for full page/section edit');
// FTT.processCommentEnd = new Date().getTime();//FTT.debug
// FTT.debug('processed comment in ' + (FTT.processCommentEnd - FTT.processCommentStart)+'ms');
return text.replace(/[ ]*NOSIGN$/,'');//if there's NOSIGN here the user added it erroneously
} else if ( mode == 'toVisual' ) {
// FTT.debug('return processed text for toVisual');
text = FTT.convertToOneLineCmt(text);
// FTT.processCommentEnd = new Date().getTime();//FTT.debug
// FTT.debug('processed comment in ' + (FTT.processCommentEnd - FTT.processCommentStart)+'ms');
return text;
} else {
FTT.detectedNOSIGN = false;
text = FTT.addSignature(text,mode);
if ( FTT.PRMOpened.type == 'edit' ) {
// FTT.debug('return processed text for comment edit');
text = FTT.convertToOneLineCmt(text).replace(/[\s]*$/, '');
// FTT.processCommentEnd = new Date().getTime();//FTT.debug
// FTT.debug('processed comment in ' + (FTT.processCommentEnd - FTT.processCommentStart)+'ms');
return text;
} else if ( FTT.detectedNOSIGN ) {
// FTT.debug('found NOSIGN, no signature added, will not convert comment to one line');
// FTT.processCommentEnd = new Date().getTime();//FTT.debug
// FTT.debug('processed comment in ' + (FTT.processCommentEnd - FTT.processCommentStart)+'ms');
return text;
} else {
// FTT.debug('return processed and signed text');
text = FTT.convertToOneLineCmt(text);
// FTT.processCommentEnd = new Date().getTime();//FTT.debug
// FTT.debug('processed comment in ' + (FTT.processCommentEnd - FTT.processCommentStart)+'ms');
return text;
}
}
};
FTT.parsePageInPlace = function(newInitTime,storedFormData,force){ // learned a great deal from w:enUser:BrandonXLF/QuickEdit.js for this, thanks BrandonXLF!
if ( $('.FTTNewCmtSubscribed,.FTTNewCmt')[0] && ! force && FTT.settings.reparseConfirm ) {
// FTT.debug('parsePageInPlace: you\'d rather just have the page reloaded than reparsed, so let\'s do that. (or there is no #mw-content-text>.mw-parser-output on this page)');
// FTT.debug('section not found, try preloading text by section number instead'); //this happens on sections like "== {{int:license-header}} ==" as they don't contain the innerText "Licensing"
}, function ( code, data ) { FTT.UIDiffButton.setDisabled(false);FTT.APIError(code, data);
});
};
FTT.hidefloatReturn = function() {
$('#FTTFloatReturn')[0].style.opacity = 0;
var DelayFRToggle = setInterval(function () { //just in case the preview parsing times out or something
clearInterval(DelayFRToggle);
FTT.floatReturnLink.toggle(false);
},520);
};
FTT.unhidefloatReturn = function() {
FTT.floatReturnLink.toggle(true);
var DelayOpacity = setInterval(function () { //just in case the preview parsing times out or something
clearInterval(DelayOpacity);
$('#FTTFloatReturn')[0].style.opacity = 1;
},20);
};
FTT.throttle = 0;
FTT.togglefloatReturn = function(throttle) {
FTT.throttle++;
throttle = FTT.throttle;
var DelayThrottle = setInterval(function () { //just in case the preview parsing times out or something
clearInterval(DelayThrottle);
if ( throttle == FTT.throttle && FTT.UITextInput && FTT.UITextInput.isElementAttached() ) { //if this function was called again within 500ms the number won't match and it'll skip (performance, scroll event fires way too much)
if ( PRM.type == 'newsection' && FTT.UITextInputTitle.getValue() != '' ) { //this and the one below were restricted to mode == 'previewposted', I probably had a good reason for that..? no idea
FTT.insertionPointThrowAway = FTT.getInsertionPointComment(PRM, FTT.pageRevisionCurrentText); //we don't do anything with this, but it triggers an error if the comment has vanished
if ( ! FTT.pageRevisionSinceLastCheckText ) { //could happen with a locator if the page was moved. getInsertionPointSection should probably get the same fallback getInsertionPointComment has but I'm too tired
return;
}
if ( FTT.pageRevisionCurrentText != FTT.pageRevisionSinceLastCheckText ){
// FTT.debug('editing comment or just opening settings, skipping new comment check (unlikely to be interesting for comment edits, might lead to false positives otherwise)');
return;
}
if ( ! PRM.pageTitle ) {
FTT.addPageAndSectionTitleToRPL(PRM);
}
if ( mode != 'postreply' && ( (FTT.lastCheckedForNewComments||0) + 30000 ) > new Date().getTime() && FTT.lastCheckedID == PRM.id ) {
// FTT.debug('checkForNewComments, already checked for new comments in the past 30 seconds, skipping');
} else {
// FTT.debug('checkForNewComments, check for new comments');
FTT.lastCheckedForNewComments = new Date().getTime();
FTT.lastCheckedID = PRM.id;
if ( FTT.pageRevisionSinceLastCheck[PRM.pageTitle] ) {
// FTT.debug('already have a version that was checked against');
FTT.recipientIsAnon = PRM.origReplyTo.match(/(^anon$|^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$|\:)/); //IPv4 or contains a colon as in IPv6 which is disallowed in usernames. Will need adjustment after IP anonimization
} else if ( FTT.activeInput == 'UITextInputTitle' ) {
if ( FTT.pipedLink == '' && FTT.UITextInputTitle.getRange().to != FTT.UITextInputTitle.getRange().from ) { //some text has been selected but we are about to insert an unpiped link
} else if ( FTT.activeInput == 'UITextInputSummary' ) {
if ( FTT.pipedLink == '' && FTT.UITextInputSummary.getRange().to != FTT.UITextInputSummary.getRange().from ) { //some text has been selected but we are about to insert an unpiped link
FTT.syncToSource(); //in visual we insert those generic "FTTINSERTMARKUP" markers, then we sync to source. These markers allow us to find the corresponding offset in source for the anchorNodeMP and focusNodeMP. The generic markers are replaced with the desired content IN SOURCE, the source change triggers a sync to visual.
var DelayedSkipLink = setInterval(function () {//skipFieldChange doesn't work without a delay because the field change happens earlier than the choose event
clearInterval(DelayedSkipLink);
if ( FTT.skipFieldChange ) { //when a user is picked from the dropdown when pinging, don't bother looking this up
if ( mode == 'ping' && FTT.UIinsertLinkLink.getValue() == '' ) { //avoid race condition repeatedly pressing backspace until field is empty. User could get to 1 or 2 characters, trigger API call, continue to backspace during API call and the result would overwrite the menu despite the field now being empty.
// FTT.debug('linkFieldChange: field is empty now, skip menu generation');
if ( ! suggestTitle.blockid ) { //blocked users probably don't want to be pinged, also some vandals create usernames that look similar to existing ones
FTT.UICancelButton.focus(); //force title/main body/summary to get blurred if currently in focus, only needed when transferring to link form from entering brackets or @
if ( (FTT.lastBlurTitle || 0)+200 > new Date().getTime() ) {
FTT.UITextInputTitle.focus();
$('#FTTUICancelButton').removeClass('FTTfixed');
return 'UITextInputTitle';
} else if ( (FTT.lastBlurSummary || 0)+200 > new Date().getTime() ) {
// FTT.debug('mention menu or no citemap available for your wiki!! If no citemap, GO CREATE IT!! https://www.wikidata.org/wiki/Q112131235 en:User:Alexis_Jazz/Factotum/MakeCitemap.js');
// FTT.debug('insertRef: loading template/parameter map for cites');
$.getJSON( mw.util.wikiScript('api'),{format:'json',action:'query',titles:FTT.B1.citemap,prop:'revisions',rvprop:'content'} ).done( function ( data ) {
FTT[FTT.fixField].setValue(FTT[FTT.fixField].getValue().slice(0,FTT[FTT.fixField].getRange().to -2) + FTT[FTT.fixField].getValue().slice(FTT[FTT.fixField].getRange().to)); //remove the brackets you just entered so we don't get too many
var DelayedOpenFormCheck = setInterval(function () {
clearInterval(DelayedOpenFormCheck);
if ( ! FTT.openingFormInProgress || (FTT.openingFormInProgress+100) < new Date().getTime() ) { //no form was opened, or a form was opened more than 100ms ago
// FTT.debug('selected text emptied');
FTT.selectedText = '';
}
},50);
}
});
}
});
}
FTT.lastUsed = function() {
if ( FTT.settings.editor == 'lastused' && ['comment','edit','newsection','newheading'].includes(FTT.PRMOpened.type) ) {