User:Enterprisey/section-watchlist.js

// vim: ts=4 sw=4 et $.when( mw.loader.using( [ "mediawiki.api" ] ), $.ready ).then( function {    var api = new mw.Api;    var PARSOID_ENDPOINT = "https:" + mw.config.get( "wgServer" ) + "/api/rest_v1/page/html/";    var HEADER_REGEX = /^\s*=(=*)\s*(.+?)\s*\1=\s*$/gm;    var BACKEND_URL = "https://section-watchlist.toolforge.org";    var TOKEN_OPTION_NAME = "userjs-section-watchlist-token";    var LOCAL_STORAGE_PREFIX = "wikipedia-section-watchlist-";    var LOCAL_STORAGE_PAGE_LIST_KEY = LOCAL_STORAGE_PREFIX + "page-list";    var LOCAL_STORAGE_EXPIRY_KEY = LOCAL_STORAGE_PREFIX + "expiry";    var PAGE_LIST_EXPIRY_MILLIS = 7 * 24 * 60 * 60 * 1000; // a week

var ENTERPRISEY_ENWP_TALK_PAGE_LINK = 'User talk:Enterprisey/section-watchlist'; var CORS_ERROR_MESSAGE = 'Error contacting the server. It might be down, in which case ' + ENTERPRISEY_ENWP_TALK_PAGE_LINK + ' (en.wiki) will have updates.';

/////////////////////////////////////////////////////////////////   //    // Utilities

// Polyfill from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes if( !String.prototype.includes ) { String.prototype.includes = function( search, start ) { if( search instanceof RegExp ) { throw TypeError('first argument must not be a RegExp'); }           if( start === undefined ) { start = 0; }           return this.indexOf( search, start ) !== -1; };   }

// Polyfill from https://github.com/jonathantneal/array-flat-polyfill, which is CC0-licensed if( !Array.prototype.flat ) { Object.defineProperty( Array.prototype, 'flat', {           configurable: true,            value: function flat  {                var depth = isNaN( arguments[0] ) ? 1 : Number( arguments[0] );

return depth ? Array.prototype.reduce.call( this, function ( acc, cur ) {                   if( Array.isArray( cur ) ) {                        acc.push.apply( acc, flat.call( cur, depth - 1 ) );                    } else {                        acc.push( cur );                    }

return acc; }, [] ) : Array.prototype.slice.call( this );           },            writable: true        } ); }

// https://stackoverflow.com/a/9229821/1757964 function removeDuplicates( array ) { var seen = {}; return array.filter( function( item ) {           return seen.hasOwnProperty( item ) ? false : ( seen[ item ] = true );        } ); }

function lastInArray( array ) { return array[ array.length - 1 ]; }

function pageNameOfHeader( header ) { var editLinks = Array.prototype.slice.call( header.querySelectorAll( "a" ) ) .filter( function ( e ) { return e.textContent.indexOf( "edit" ) === 0; } ); if( editLinks.length ) { var encoded = editLinks[0] .getAttribute( "href" ) .match( /title=(.+?)(?:$|&)/ ) [1];           return decodeURIComponent( encoded ).replace( /_/g, " " ); } else { return null; }   }

var getAllTranscludedTitlesCache = null; function getAllTranscludedTitles { if( !getAllTranscludedTitlesCache ) { var allHeadersArray = Array.prototype.slice.call(               document.querySelector( "#mw-content-text" ).querySelectorAll( "h1,h2,h3,h4,h5,h6" ) ); getAllTranscludedTitlesCache = removeDuplicates( allHeadersArray               .filter( function ( header ) { // The word "Contents" at the top of the table of contents is a heading return header.getAttribute( "id" ) !== "mw-toc-heading" } )               .map( pageNameOfHeader )                .filter( Boolean ) ); }       return getAllTranscludedTitlesCache; }

/////////////////////////////////////////////////////////////////   //    // User interface for normal pages

function loadPagesWatched { try { var expiryStr = window.localStorage.getItem(LOCAL_STORAGE_EXPIRY_KEY); if( expiryStr ) { var expiry = parseInt( expiryStr ); if( expiry && ( ( new Date.getTime - expiry ) < PAGE_LIST_EXPIRY_MILLIS ) ) { var list = window.localStorage.getItem(LOCAL_STORAGE_PAGE_LIST_KEY); return $.when( { status: "success", data: list.split( "," ) } ); }           }

var url = BACKEND_URL + "/subbed_pages?user_id=" + mw.config.get( "wgUserId" ) + "&token=" + mw.user.options.get( TOKEN_OPTION_NAME ); return $.getJSON( url ).then( function ( data ) {               if( data.status === "success" ) {                    try {                        window.localStorage.setItem(LOCAL_STORAGE_EXPIRY_KEY, new Date.getTime);                        window.localStorage.setItem(LOCAL_STORAGE_PAGE_LIST_KEY, data.data.join( "," ));                    } catch ( e ) {                        console.error( e );                    }                }                return data;            } ); } catch ( e ) { console.error( e ); }   }

function loadSectionsWatched( allTranscludedIds ) { var promises = allTranscludedIds.map( function ( id ) {           return $.getJSON( BACKEND_URL + "/subbed_sections?page_id=" + id + "&user_id=" + mw.config.get( "wgUserId" ) + "&token=" + mw.user.options.get( TOKEN_OPTION_NAME ) );       } ); return $.when.apply( $, promises ).then( function {            var obj = {};            if( allTranscludedIds.length === 1 ) {                if( arguments[0].status === "success" ) {                    obj[allTranscludedIds[0]] = arguments[0].data;                    return { status: "success", data: obj };                } else {                    return arguments[0];                }            } else {                var groupStatus = "";                var errorMessage = null;                for( var i = 0; i < arguments.length; i++ ) {                    if( arguments[i][0].status !== "success" ) {                        allSuccess = false;                        errorMessage = arguments[i][0].data;                    } else {                        obj[allTranscludedIds[i]] = arguments[i][0].data;                    }                    if( groupStatus === "success" ) { groupStatus = arguments[i][0].status; }               }                return { status: groupStatus, data: ( groupStatus === "success" ) ? obj : errorMessage };           }        } );    }

function initializeFakeLinks( messageHtml ) { mw.loader.using( [ "mediawiki.util", "oojs-ui-core", "oojs-ui-widgets" ] ); $( "#mw-content-text" ).find( "h1,h2,h3,h4,h5,h6" ).each( function ( idx, header ) {           var popup = null;            $( header ).find( ".mw-editsection *" ).last.before( " | ",               $( " " ).append(                    $( "" )                        .attr( "href", "#" )                        .text( "watch" )                        .click( function  { if( popup === null ) { mw.loader.using( [ "mediawiki.util", "oojs-ui-core", "oojs-ui-widgets" ] ).then( function {                                    popup = new OO.ui.PopupWidget( { $content: $( ' ', { style: 'padding-top: 0.5em' } ).html( messageHtml ), padded: true, width: 400, align: 'forwards', hideCloseButton: false, } );                                   $( this ).parent.append( popup.$element );                                    popup.toggle( true );                                }.bind( this ) ); } else { popup.toggle; }                           return false; } ) ) );       } );    }

function attachLink( header, pageId, pageName, wikitextName, dupIdx, isAlreadyWatched ) { $( header ).find( ".mw-editsection *" ).last.before(           " | ",            $( "" )                .attr( "href", "#" )                .text( isAlreadyWatched ? "unwatch" : "watch" )               .click( function  { var link = $( this ); if( !mw.user.options.get( TOKEN_OPTION_NAME ) ) { alert( "You must register first by visiting Special:BlankPage/section-watchlist." ); return false; }                   var data = { page_id: pageId, page_title: pageName, section_name: wikitextName, section_dup_idx: dupIdx, user_id: mw.config.get( "wgUserId" ), token: mw.user.options.get( TOKEN_OPTION_NAME ) };                   if( this.textContent === "watch" ) { $.post( BACKEND_URL + "/sub", data ).then( function ( data2 ) {                           if( data2.status === "success" ) {                                link.text( "unwatch" );                                try {                                    var list = window.localStorage.getItem( LOCAL_STORAGE_PAGE_LIST_KEY ) || "";                                    if( !list.includes( pageId ) ) {                                        window.localStorage.setItem( LOCAL_STORAGE_PAGE_LIST_KEY, list + "," + pageId );                                    }                                } catch ( e ) {                                    console.error( e );                                }                            } else {                                console.error( data2 );                            }                        }, function ( request ) {                            if( request.responseJSON && request.responseJSON.status ) { console.error( request.responseJSON ); }                           console.error( request ); } );                   } else {                        $.post( BACKEND_URL + "/unsub", data ).then( function ( data2 ) { if( data2.status === "success" ) { link.text( "watch" ); } else { console.error( data2 ); }                       }, function ( request ) { if( request.responseJSON && request.responseJSON.status ) { console.error( request.responseJSON ); }                           console.error( request ); } );                   }                    return false;                } ) );    }

function initializeLinks( transcludedTitlesAndIds, allWatchedSections ) {

var allHeadersArray = Array.prototype.slice.call(           document.querySelector( "#mw-content-text" ).querySelectorAll( "h1,h2,h3,h4,h5,h6" ) ); var allHeaders = allHeadersArray .filter( function ( header ) {               // The word "Contents" at the top of the table of contents is a heading                return header.getAttribute( "id" ) !== "mw-toc-heading"            } ) .map( function ( header ) {               return [ header, pageNameOfHeader( header ) ];            } ) .filter( function ( headerAndPage ) {               return headerAndPage[1] !== null            } ); var allTranscludedTitles = removeDuplicates( allHeaders.map( function ( header ) { return header[1]; } ) );

return api.get( {           action: "query",            prop: "revisions",            titles: allTranscludedTitles.join("|"),            rvprop: "content",            rvslots: "main",            formatversion: 2        } ).then( function( revData ) {            for( var pageIdx = 0; pageIdx < revData.query.pages.length; pageIdx++ ) {                var targetTitle = revData.query.pages[pageIdx].title;                var targetPageId = revData.query.pages[pageIdx].pageid;                var targetWikitext = revData.query.pages[pageIdx].revisions[0].slots.main.content;                var watchedSections = allWatchedSections ? allWatchedSections[targetPageId] : {};

var allHeadersFromTarget = allHeaders.filter( function ( header ) { return header[1] === targetTitle; } );

// Find all the headers in the wikitext

// (Nowiki exclusion code copied straight from reply-link) // Save all nowiki spans var nowikiSpanStarts = []; // list of ignored span beginnings var nowikiSpanLengths = []; // list of ignored span lengths var NOWIKI_RE = /<(nowiki|pre)>[\s\S]*?<\/\1>/g; var spanMatch; do { spanMatch = NOWIKI_RE.exec( targetWikitext ); if( spanMatch ) { nowikiSpanStarts.push( spanMatch.index ); nowikiSpanLengths.push( spanMatch[0].length ); }               } while( spanMatch );

// So that we don't check every ignore span every time var nowikiSpanStartIdx = 0;

var headerMatches = []; var headerMatch; matchLoop: do { headerMatch = HEADER_REGEX.exec( targetWikitext ); if( headerMatch ) {

// Check that we're not inside a nowiki for( var nwIdx = nowikiSpanStartIdx; nwIdx <                           nowikiSpanStarts.length; nwIdx++ ) { if( headerMatch.index > nowikiSpanStarts[nwIdx] ) { if ( headerMatch.index + headerMatch[0].length <=                                   nowikiSpanStarts[nwIdx] + nowikiSpanLengths[nwIdx] ) {

// Invalid sig continue matchLoop; } else {

// We'll never encounter this span again, since // headers only get later and later in the wikitext nowikiSpanStartIdx = nwIdx; }                           }                        }                        headerMatches.push( headerMatch ); }               } while( headerMatch );

// We'll use this dictionary to calculate the duplicate index var headersByText = {}; for( var i = 0; i < headerMatches.length; i++ ) {

// Group 2 of HEADER_REGEX is the header text var text = headerMatches[i][2]; headersByText[text] = ( headersByText[text] || [] ).concat( i ); }

// allHeadersFromTarget should contain every header we found in the wikitext // (and more, if targetPageName was transcluded multiple times) if( allHeadersFromTarget.length % headerMatches.length !== 0 ) { console.error(allHeadersFromTarget); console.error(headerMatches); throw new Error( "non-divisble header list lengths" ); }

for( var headerIdx = 0; headerIdx < allHeadersFromTarget.length; headerIdx++ ) { var trueHeaderIdx = headerIdx % headerMatches.length; var headerText = headerMatches[trueHeaderIdx][2];

// NOTE! The duplicate index is calculated relative to the // *wikitext* header matches (because that's how the backend                   // does it)! That is, if we have a page that includes two // headers, both called "a", and we transclude that page // twice, the result will be four headers called "a". But we                   // want to assign those four headers, respectively, the // duplicate indices of 0, 1, 0, 1. That's why we use // trueHeaderIdx here, not headerIdx. var dupIdx = headersByText[headerText].indexOf( trueHeaderIdx );

var headerEl = allHeadersFromTarget[headerIdx]; var headerId = headerEl[0].querySelector( "span.mw-headline" ).id;

var isAlreadyWatched = ( watchedSections[headerText] || [] ).indexOf( dupIdx ) >= 0;

attachLink( headerEl, targetPageId, targetTitle, headerText, dupIdx, isAlreadyWatched ); }           }        }, function  { console.error( arguments ); } );   }

/////////////////////////////////////////////////////////////////   //    // The watchlist page

function parseSimpleAddition( diffHtml ) { var CONTEXT_ROW = / \n &#160;<\/td>\n  (?: ([^<]*)<\/div>)?<\/td>\n  &#160;<\/td>\n  .*?<\/td>\n<\/tr>\n/g; var ADDED_ROW = / \n &#160;<\/td>\n  \+<\/td>\n  (?: ([^<]*)<\/div>)?<\/td>\n<\/tr>\n/g;

function consecutiveMatches( regex, text ) { var prevMatchEndIdx = null; var match = null; var rows = []; while( ( match = regex.exec( text ) ) !== null ) { if( ( prevMatchEndIdx !== null ) && ( prevMatchEndIdx !== match.index ) ) { // this match wasn't immediately after the previous one break; }               rows.push( match[1] || "" ); prevMatchEndIdx = match.index + match[0].length; }           return { text: rows.join( "\n" ), endIdx: prevMatchEndIdx };       }

var prevContext = consecutiveMatches( CONTEXT_ROW, diffHtml ); var added = consecutiveMatches( ADDED_ROW, diffHtml.substring( prevContext.endIdx ) );

function fix( text ) { var INS_DEL = /|<\/ins>||<\/del>/g; var ENTITIES = /&(lt|gt|amp);/g; return text.replace( INS_DEL, "" ).replace( ENTITIES, function ( _match, group1 ) {               switch( group1 ) {                    case "lt": return "<";                    case "gt": return ">";                    case "amp": return "&";                }            } ); }

return { prevContext: fix( prevContext.text ), added: fix( added.text ) };   }

function handleViewNewText( listElement, streamEvent, sectionEvent ) { api.get( {           action: "compare",            fromrev: streamEvent.data.revision["new"],            torelative: "prev",            formatversion: "2",            prop: "diff"        } ).then( function ( compareResponse ) {            var diffHtml = compareResponse.compare.body;            var parsedDiff = parseSimpleAddition( diffHtml );            var addedHtmlPromise = $.post( { url: "https:" + mw.config.get( "wgServer" ) + "/w/api.php", data: { action: "parse", format: "json", formatversion: "2", title: streamEvent.title, text: parsedDiff.added, prop: "text", // just wikitext, please pst: "1" // do the pre-save transform }           } );

var listElementAddedPromise = addedHtmlPromise.then( function ( newHtmlResponse ) {               listElement.append( newHtmlResponse.parse.text );                var newContent = listElement.find( ".mw-parser-output" );                mw.hook( "wikipage.content" ).fire( $( newContent ) );            } );

var revObjPromise = api.get( {               action: "query",                prop: "revisions",                rvprop: "timestamp|content|ids",                rvslots: "main",                rvlimit: 1,                titles: streamEvent.title,                formatversion: 2,            } ).then( function ( data ) {                if( data.query.pages[0].revisions ) {                    var rev = data.query.pages[0].revisions[0];                    return { revId: rev.revid, timestamp: rev.timestamp, content: rev.slots.main.content };                } else {                    console.error( data );                    throw new Error( "[getWikitext] bad response: " + data );                }            } );

$.when(               addedHtmlPromise,                revObjPromise,                listElementAddedPromise            ).then( function ( newHtmlResponse, revObj, _ ) {

// Walmart reply-link var namespace = streamEvent.namespace; var ttdykPage = streamEvent.title.indexOf( "Template:Did_you_know_nominations" ) === 0; if( ( namespace % 2 ) === 1 || namespace === 4 || ttdykPage ) { // Ideally this is kept in sync with the one defined // near the top of reply-link; if they differ, I imagine // the reply-link one is correct var REPLY_LINK_TIMESTAMP_REGEX = /\(UTC(?:(?:−|\+)\d+?(?:\.\d+)?)?\)\S*?\s*$/m; var newContent = listElement.find( ".mw-parser-output" ).get( 0 ); if( REPLY_LINK_TIMESTAMP_REGEX.test( newContent.textContent ) ) { var nodeToAttachAfter = newContent.children[0]; do { nodeToAttachAfter = lastInArray( nodeToAttachAfter.childNodes ); } while( lastInArray( nodeToAttachAfter.childNodes ).nodeType !== 3 /* Text */ ); nodeToAttachAfter = lastInArray( nodeToAttachAfter.childNodes ); var parentCmtIndentation = /^[:*#]*/.exec( parsedDiff.added )[0]; var sectionName = sectionEvent.target[0].replace( /_/g, " " ); var headerRegex = new RegExp( "^=(=*)\\s*" + mw.util.escapeRegExp( sectionName ) + "\\s*\\1=\\s*$", "gm" ); var sectionDupIdx = sectionEvent.target[1]; for( var i = 0; i < sectionDupIdx; i++ ) { // Advance the regex past all the previous duplicate matches headerRegex.exec( revObj.content ); }                       var headerMatch = headerRegex.exec( revObj.content ); var REPLY_LINK_HEADER_REGEX = /^\s*=(=*)\s*(.+?)\s*\1=\s*$/gm; var endOfThatHeaderIdx = headerMatch.index + headerMatch[0].length; var nextHeaderMatch = REPLY_LINK_HEADER_REGEX.exec( revObj.content.substring( endOfThatHeaderIdx ) ); var nextHeaderIdx = endOfThatHeaderIdx + ( nextHeaderMatch ? nextHeaderMatch.index : revObj.content.length ); var parentCmtEndStrIdx = revObj.content.indexOf( parsedDiff.prevContext ) + parsedDiff.prevContext.length + parsedDiff.added.length - headerMatch.index; mw.hook( "replylink.attachlinkafter" ).fire(                           nodeToAttachAfter,                            /* preferredId */ "",                            /* parentCmtObj */ {                                indentation: parentCmtIndentation,                                sigIdx: null,                                endStrIdx: parentCmtEndStrIdx                            },                            /* sectionObj */ {                                title: sectionName,                                dupIdx: sectionDupIdx,                                startIdx: headerMatch.index,                                endIdx: nextHeaderIdx,                                idxInDomHeaders: null,                                pageTitle: streamEvent.title.replace( /_/g, " " ),                                revObj: revObj,                                headerEl: null }                       );                    } else {                        console.warn( "text content didn't match timestamp regex" );                    }                } else {                    console.warn( "bad namespace " + namespace );                }            } ); } );   }

function renderLengthDiff( beforeLength, afterLength ) { var delta = afterLength - beforeLength; var el = ( Math.abs( delta ) > 500 ) ? "strong" : "span"; var elClass = "mw-plusminus-" + ( ( delta > 0 ) ? "pos" : ( ( delta < 0 ) ? "neg" : "null" ) ); return $( " ", { "class": "mw-changeslist-line-inner-characterDiff" } ).append(           $( "<" + el + ">", { "class": elClass + " mw-diff-bytes", "dir": "ltr", "title": afterLength + " byte" + ( ( afterLength === 1 ) ? "" : "s" ) + " after change of this size" } ).text( ( ( delta > 0 ) ? "+" : "" ) + mw.language.convertNumber( delta ) ) ); }

function renderItem( streamEvent, sectionEvent ) { var url = mw.util.getUrl( streamEvent.title ) + "#" + sectionEvent.target[0]; var els = [ streamEvent.timestamp.substring( 8, 10 ) + ":" + streamEvent.timestamp.substring( 10, 12 ), $( " ", { "class": "mw-changeslist-line-inner-articleLink" } ).append(               $( " ", { "class": "mw-title" } ).append( $( "", { "class": "mw-changeslist-title", "href": url, "title": streamEvent.title } ) .text( streamEvent.title + " § " + sectionEvent.target[0].replace( /_/g, " " ) ) ) ), // TODO pending support for "vague sections" //sectionEvent.target[2] //   ? $( " " ).append( "(under ", $( "", { "href": secondaryUrl } ).text( streamEvent.target[2][0] ) )            //    : "",            streamEvent.data.revision["new"]                ? $( " ", { "class": "mw-changeslist-line-inner-historyLink" } ).append( $( " ", { "class": "mw-changeslist-links" } ).append(                       $( " " ).append(

// The URL parameters must be in this order, or Navigation Popups will not work for this link. (UGH.) $( "", {                               "class": "mw-changeslist-diff",                                "href": mw.util.getUrl( "", { "title": streamEvent.title, "diff": "prev", "oldid": streamEvent.data.revision["new"] } )                           } ).text( "diff" ) ),                        $( " " ).append( $( "<a>", { "class": "mw-changeslist-history", "href": mw.util.getUrl( streamEvent.title, { "action": "history" } ) } ) .text( "hist" ) ),                       ) )                : "",            $( " ", { "class": "mw-changeslist-line-inner-separatorAfterLinks" } ).append( $( " ", { "class": "mw-changeslist-separator" } ) ),           renderLengthDiff( streamEvent.data.length.old, streamEvent.data.length["new"] ),            $( " ", { "class": "mw-changeslist-line-inner-separatorAftercharacterDiff" } ).append( $( " ", { "class": "mw-changeslist-separator" } ) ),           $( " ", { "class": "mw-changeslist-line-inner-userLink" } ).append( $( "<a>", { "class": "mw-userlink", "href": mw.util.getUrl( "User:" + streamEvent.user ), "title": "User:" + streamEvent.user } ).append(                   $( " " ).text( streamEvent.user ) ) ),            $( " ", { "class": "mw-changeslist-line-inner-userTalkLink" } ).append( $( " ", { "class": "mw-usertoollinks mw-changeslist-links" } ).append(                   $( " " ).append( $( "<a>", { "class": "mw-usertoollinks-talk", "href": mw.util.getUrl( "User talk:" + streamEvent.user ), "title": "User talk:" + streamEvent.user } ) .text( "talk" ) ),                   $( " " ).append( $( "<a>", { "class": "mw-usertoollinks-contribs", "href": mw.util.getUrl( "Special:Contributions/" + streamEvent.user ), "title": "Special:Contributions/" + streamEvent.user } ) .text( "contribs" ) ) ) ),           streamEvent.data.minor                ? $( " ", { "class": "minoredit", "title": "This is a minor edit" } ).text( "m" )                : "",            $( " ", { "class": "mw-changeslist-line-inner-comment" } ).append( $( " ", { "class": "comment comment--without-parentheses" } ).append(                   $( " ", { "dir": "auto" } ).append( streamEvent.parsedcomment ) ) )        ];        if( streamEvent.data.is_simple_addition ) {            els.push( $( " " ).append( "(", $( "<a>", { "class": "section-watchlist-view-new-text", "href": "#" } ).text( "view new text" ), ")" ) );        }        for( var i = els.length - 1; i >= 0; i-- ) {            els.splice( i, 0, " " );        }        return els;    }

function renderInbox( inbox ) { var days = []; var currDateString; // for example, the string "20200701", meaning "1 July 2020" var currItems = []; // the inbox entries for the current day, sorted from latest to earliest for( var i = 0; i < inbox.length; i++ ) { var streamEventAndSectionEvent = inbox[i]; var streamEvent = streamEventAndSectionEvent.stream; var sectionEvent = streamEventAndSectionEvent.section; if( streamEvent.timestamp.substring( 0, 8 ) !== currDateString ) { if( currItems.length ) { days.push( [ currDateString, currItems ] ); }               currItems = []; currDateString = streamEvent.timestamp.substring( 0, 8 ); }           if( sectionEvent.type === "Edit" ) { var sectionName = sectionEvent.target[0]; var listEl = $( "<li>" ).append( renderItem( streamEvent, sectionEvent ) ); if( streamEvent.data.is_simple_addition ) { ( function {                        var currStreamEvent = streamEvent;                        var currSectionEvent = sectionEvent;                        listEl.find( ".section-watchlist-view-new-text" ).click( function ( evt ) { var parserOutput = this.parentNode.parentNode.querySelector( ".mw-parser-output" ); if( parserOutput ) { $( parserOutput ).toggle; } else { handleViewNewText( $( this ).parent.parent, currStreamEvent, currSectionEvent ); }                           if( this.textContent === "view new text" ) { this.textContent = "hide new text"; } else { this.textContent = "view new text"; }                           evt.preventDefault; return false; } );                   } );                }                currItems.push( listEl ); } else { currItems.push( $( "<li>" ).text( JSON.stringify( streamEvent ) + " | " + JSON.stringify( sectionEvent ) ) ); }       }        if( currItems.length ) { days.push( [ currDateString, currItems ] ); }       return days; }

// "20200701" -> "July 1" (in the user's interface language... approximately) // TODO there really has to be a better way to do this var englishMonths = [ 'january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december' ];   function renderIsoDate( isoDate ) { return mw.msg( englishMonths[ parseInt( isoDate.substring( 4, 6 ) ) - 1 ] ) + " " + parseInt( isoDate.substring( 6, 8 ) ); }

// i.e. generate a message in the case that we have no token. function generateNoTokenMessage( registerUrl ) { return $.ajax( {           type: "HEAD",            "async": true,            url: BACKEND_URL        } ).then( function  {            return 'You must register first by visiting <a href="' + registerUrl +                '" title="The section-watchlist registration page">the registration page</a>.';        }, function  {            return 'The server is down. Check ' + ENTERPRISEY_ENWP_TALK_PAGE_LINK + ' for updates.';        } ); }

// i.e. generate a message in the case that the backend gave us an error. function generateBackendErrorMessage( backendResponse, registerUrl ) { if( backendResponse.status === "bad_request" ) { switch( backendResponse.data ) { case "no_stored_token": return "The system doesn't have a stored registration for your username. Please authenticate by visiting <a href='" + registerUrl + "' title='The section-watchlist registration page'>the registration page</a>."; case "bad_token": return "Authentication failed. Please re-authenticate by visiting <a href='" + registerUrl + "'>the registration page</a>."; }       }        return "Request failed (error: " + backendResponse.status + "/" + backendResponse.data +            "). Re-authenticating by visiting <a href='" + registerUrl + "'>the registration page</a> may help."; }

function makeBackendQuery( query_path, callback ) { var swtoken = mw.user.options.get( TOKEN_OPTION_NAME ); var registerUrl = BACKEND_URL + "/oauth-register?user_id=" + mw.config.get( "wgUserId" ); if( swtoken ) { $.getJSON( BACKEND_URL + query_path + "&token=" + swtoken ).then( function ( response ) {               if( response.status === "success" ) {                    callback( response.data );                    $( "#mw-content-text" )                        .append( " ( <ul id='section-watchlist-links'><li><a href='" + registerUrl + "'>re-register with backend</a></li></ul> ) " );                } else {                    $( "#mw-content-text" ).html( generateBackendErrorMessage( response, registerUrl ) );                }            }, function  {                $( "#mw-content-text" ).html( CORS_ERROR_MESSAGE );            } ); } else { generateNoTokenMessage( registerUrl ).then( function ( msg ) {               $( "#mw-content-text" ).html( msg );            } ); }   }

function showTabBackToWatchlist { // This tab doesn't get an access key because "L" already goes to the watchlist var pageName = "Special:Watchlist"; var link = $( "<a>" ) .text( "Regular watchlist" ) .attr( "title", pageName ) .attr( "href", mw.util.getUrl( pageName ) ); $( "#p-namespaces ul" ).append(           $( "<li>" ).append( $( " " ).append( link ) )                .attr( "id", "ca-nstab-regular-watchlist" ) ); }

mw.loader.using( [       "mediawiki.api",        "mediawiki.language",        "mediawiki.util",        "mediawiki.special.changeslist",        "mediawiki.special.changeslist.enhanced",        "mediawiki.interface.helpers.styles"    ] ).then( function  {        var pageId = mw.config.get( "wgArticleId" );        var registerUrl = BACKEND_URL + "/oauth-register?user_id=" + mw.config.get( "wgUserId" );

if( mw.config.get( "wgPageName" ) === "Special:BlankPage/section-watchlist" ) { var months = ( new mw.Api ).loadMessages( englishMonths ); $( "#firstHeading" ).text( "Section watchlist" ); document.title = "Section watchlist - Wikipedia"; $( "#mw-content-text" ).empty; makeBackendQuery( "/inbox?user_id=" + mw.config.get( "wgUserId" ), function ( data ) {               if( data.length ) {                    var rendered = renderInbox( data );                    $.when( months ).then( function  { var renderedDays = rendered.map( function ( dayAndItems ) {                           dayAndItems[1].reverse;                            return [                                $( " " ).text( renderIsoDate( dayAndItems[0] ) ),                                $( "<ul>" ).append( dayAndItems[1] )                            ];                        } ); renderedDays.reverse; var elements = renderedDays.flat; $( "#section-watchlist-links" ).prepend(                           $( "<li>" ).append( $( "<a>", { "href": mw.util.getUrl( "Special:BlankPage/section-watchlist/edit" ) } ).text( "view list of watched sections" ) ) ); $( "#mw-content-text" ).append( elements ); mw.hook( "wikipage.content" ).fire( $( "#mw-content-text" ) ); } );               } else {                    $( "#mw-content-text" ).text( "No edits yet!" );               }            } );            showTabBackToWatchlist; } else if( mw.config.get( "wgPageName" ) === "Special:BlankPage/section-watchlist/edit" ) { $( "#firstHeading" ).text( "Edit section watchlist" ); document.title = "Edit section watchlist - Wikipedia"; $( "#mw-content-text" ) .empty .append( $( " " ).append( $( "<a>", { "href": mw.util.getUrl( "Special:BlankPage/section-watchlist" ) } ).text( "< Back to section watchlist" ) ) ); makeBackendQuery( "/all_subbed_sections?user_id=" + mw.config.get( "wgUserId" ), function ( data ) {               if( Object.keys( data ).length ) {                    var list = $( "<ul>" ).appendTo( "#mw-content-text" );                    Object.keys( data ).forEach( function ( pageId ) { var pageData = data[pageId]; var listEl = $( "<li>" ).append( $( "<a>", { "href": mw.util.getUrl( pageData.title ) } ).text( pageData.title ) ); var sectionsList = $( "<ul>" ).appendTo( listEl ); pageData.sections.forEach( function ( section ) {                           sectionsList.append( $( "<li>" ).append( $( "<a>", { "href": mw.util.getUrl( pageData.title ) + "#" + ( section[2] || section[0] ) } ).text( pageData.title + " § " + section[0].replace( /_/g, " " ) ) ) );                        } ); list.append( listEl ); } );                   //$( "#mw-content-text" ).append( elements );                    //mw.hook( "wikipage.content" ).fire( $( "#mw-content-text" ) );                } else {                    $( "#mw-content-text" ).text( "No subscribed sections yet!" );               }            } );            showTabBackToWatchlist; } else if( mw.config.get( "wgAction" ) === "view" &&               pageId !== 0 &&                !window.location.search.includes( "oldid" ) ) { registerUrl += "&return_page=" + encodeURIComponent( mw.config.get( "wgPageName" ) + window.location.hash ); if( mw.user.options.get( TOKEN_OPTION_NAME ) ) { var allTranscludedTitles = getAllTranscludedTitles; if( allTranscludedTitles.length ) { $.when(                       loadPagesWatched,                        api.get( { action: "query", prop: "info", titles: allTranscludedTitles.join("|"), inprop: "", formatversion: 2 } )                   ).then( function ( pagesWatchedResult, infoQueryResult ) {                        if( pagesWatchedResult.status === "success" ) {                            var watchedPages = pagesWatchedResult.data;                            var allTranscludedIds = infoQueryResult[0].query.pages.map( function ( page ) { return page.pageid; } );                           var doesPageHaveWatchedSection = allTranscludedIds.some( function ( id ) { return watchedPages.indexOf( String( id ) ) >= 0; } );                           var transcludedTitlesAndIds = infoQueryResult[0].query.pages.map( function ( page ) { return { "title": page.title, "id": page.pageid }; } );                           loadSectionsWatched( allTranscludedIds ).then( function ( sectionsWatchedResult ) { if( sectionsWatchedResult.status === "success" ) { initializeLinks( transcludedTitlesAndIds, sectionsWatchedResult.data ); } else { console.error( "sectionsWatchedResult = ", sectionsWatchedResult ); initializeFakeLinks( generateBackendErrorMessage( sectionsWatchedResult, registerUrl ) ); }                           }, function  { console.error( "loadSectionsWatched failed, arguments = ", arguments ); initializeFakeLinks( CORS_ERROR_MESSAGE ); } );                       } else {                            console.error( "loadPagesWatched failed, pagesWatchedResult = ", pagesWatchedResult );                            initializeFakeLinks( generateBackendErrorMessage( pagesWatchedResult, registerUrl ) );                        }                    }, function  {                        initializeFakeLinks( CORS_ERROR_MESSAGE );                    } ); }           } else {

// No stored token generateNoTokenMessage( registerUrl ).then( function ( msg ) {                   initializeFakeLinks( msg );                } ); }       } else if( mw.config.get( "wgPageName" ) === "Special:Watchlist" ) { var pageName = "Special:BlankPage/section-watchlist"; var link = $( "<a>" ) .text( "Section watchlist" ) .attr( "accesskey", "s" ) .attr( "title", pageName ) .attr( "href", mw.util.getUrl( pageName ) ); link.updateTooltipAccessKeys; $( "#p-namespaces ul" ).append(               $( "<li>" ).append( $( " " ).append( link ) )                    .attr( "id", "ca-nstab-section-watchlist" ) ); }   } ); } );