User:Derugon/contentFilter.js

/** * Removes information from pages according to a filter, which can be * enabled/disabled from the toolbar. */ var contentFilter = { /**	 * The version number of the content filter. */	version: '1.2',

/**	 * The parser output. * @type {Element} */	parserOutput: null, /**	 * The table of contents from the parser output. * @type {Element} */	toc: null, /**	 * The filter form items. * @type {HTMLLIElement[]} */	items: [],

/**	 * A MediaWiki API to the current wiki. * @type {mw.Api} */	api: null, /**	 * The current URI. * Used to set links to the current page with a filter on or off. * @type {mw.Uri} */	uri: null,

/**	 * The page global filter. */	pageFilter: 0, /**	 * The index of the currently selected filter form item. */	selectedIndex: -1, /**	 * The currently selected filter. */	selectedFilter: 0,

/**	 * @type {[(element:Element)=>void,Element][]} */	postponed: [],

/**	 * Initializes the content filter on a page. */	init: function { console.log( 'Content Filter v' + contentFilter.version );

try { contentFilterConfig; } catch ( _ ) { mw.log.error(				'Content Filter: The configuration object is undefined. ' +				'Please define a contentFilterConfig object this script ' +				'would have access to.'			); return; }		var isUtilDefined = true; try { contentFilterUtil.selectedFilter = 0; contentFilterUtil.getFilterMax  = contentFilter.getFilterMax; contentFilterUtil.getFilter     = contentFilter.getFilter; contentFilterUtil.applyFilter   = function  {}; } catch ( _ ) { isUtilDefined = false; }

if ( !contentFilter.isFilteringAvailable ) { if ( isUtilDefined ) { contentFilterUtil.loaded = true; }			return; }

var contentText = document.getElementById( 'mw-content-text' ); contentFilter.parserOutput = contentText ? contentText.getElementsByClassName( 'mw-parser-output' )[ 0 ] : null; contentFilter.toc         = document.getElementById( 'toc' ); contentFilter.api         = new mw.Api; contentFilter.uri         = new mw.Uri( document.location.href ); contentFilter.pageFilter  = contentFilter.getPageFilter;

contentFilter.generateFilterItems; contentFilter.insertFilterElement;

if ( !contentFilter.updateSelectedIndex ) { if ( isUtilDefined ) { contentFilterUtil.loaded = true; }			return; }

contentFilter.selectedFilter = Math.pow( 2, contentFilter.selectedIndex ); if ( isUtilDefined ) { contentFilterUtil.selectedFilter = contentFilter.selectedFilter; contentFilterUtil.applyFilter = contentFilter.applyFilter; }

contentFilter.updateSelectedFilterItem; contentFilter.applyFilter( contentFilter.parserOutput ); contentFilter.updateAnchorsFilter;

if ( isUtilDefined ) { contentFilterUtil.loaded = true; }	},

/**	 * Indicates whether the filters can be used on the current page. * @returns True if the filters can be used, false otherwise. */	isFilteringAvailable: function { if (			contentFilterConfig.filterEnableClass &&			document.getElementsByClassName( contentFilterConfig.filterEnableClass ).length		) { return true; }		var namespace = contentFilter.findClassStartingWith( document.body, 'ns-' ); return contentFilterConfig.filteredNamespaces.includes( +namespace ); },

/**	 * Checks if the entire page is limited to some versions then sets the page * global filter accordingly. */	getPageFilter: function { if (			!contentFilterConfig.contextFilterClass ||			!contentFilter.parserOutput		) { return contentFilter.getFilterMax; }		var contextBoxes = contentFilter.parserOutput .getElementsByClassName( contentFilterConfig.contextFilterClass ); if (			!contextBoxes.length ||			contentFilter.getPreviousHeading( contextBoxes[ 0 ] )		) { return contentFilter.getFilterMax; }		if ( contentFilterConfig.blockFilterClass ) { var blockElement = contextBoxes[ 0 ].getElementsByClassName(				contentFilterConfig.blockFilterClass			)[ 0 ]; if ( blockElement ) { return contentFilter.getFilter( blockElement ); }		}		if ( contentFilterConfig.wrapperFilterClass ) { var wrapperElement = contextBoxes[ 0 ].getElementsByClassName(				contentFilterConfig.wrapperFilterClass			)[ 0 ]; if ( wrapperElement ) { return contentFilter.getFilter( wrapperElement ); }		}		if ( contentFilterConfig.inlineFilterClass ) { var inlineElement = contextBoxes[ 0 ].getElementsByClassName(				contentFilterConfig.inlineFilterClass			)[ 0 ]; if ( inlineElement ) { return contentFilter.getFilter( inlineElement ); }		}		return 0; },

/**	 * Gets the last heading element used before an element. * @param {Element} element The element. * @returns The previous heading element if there is one, null otherwise. */	getPreviousHeading: function ( element ) { element = element.previousElementSibling; while ( element && !( element instanceof HTMLHeadingElement ) ) { element = element.previousElementSibling; }		return element; },

/**	 * Gets the numeric filter preventing content from being removed with any * filter. * @returns The maximum allowed numeric filter. */	getFilterMax: function { return Math.pow( 2, contentFilterConfig.filters.length ) - 1; },

/**	 * Gets the numeric filter of an element. * @param {Element} element The element. * @returns The numeric filter of the given element, 0 otherwise. */	getFilter: function ( element ) { var filterClass = contentFilter.findClassStartingWith(			element,			contentFilterConfig.filterClassIntro		); return filterClass ? +filterClass : 0; },

/**	 * Gets the first class of an element beginning with a specific string. * @param {Element} element The element. * @param {string} intro   The beginning of the class name. * @returns The first corresponding class name, null otherwise. */	findClassStartingWith: function ( element, intro ) { var classList = element.classList; for ( var i = 0; i < classList.length; ++i ) { if ( classList[ i ].startsWith( intro ) ) { return classList[ i ].substr( intro.length ); }		}		return null; },

/**	 * Generates the filter form items. */	generateFilterItems: function { var itemBase = document.createElement( 'li' ); itemBase.classList.add( 'content-filter-item' ); itemBase.appendChild( document.createElement( 'a' ) ); for (			var i = 0, pow = 1;			i < contentFilterConfig.filters.length;			++i, pow *= 2		) { var item = itemBase.cloneNode( true ); item.id = 'content-filter-item-' + i;			contentFilter.items.push( item ); if ( ( pow & contentFilter.pageFilter ) === 0 ) { item.classList.add( 'content-filter-item-deactivated' ); continue; }			item.title = contentFilterConfig.filters[ i ]; /** @type */ var obj = {}; obj[ contentFilterConfig.urlParam ] = i;			contentFilter.uri.extend( obj ); /** @type {HTMLAnchorElement} */ ( item.firstChild ).href = contentFilter.uri.toString; }	},

/**	 * Generates the filter form and puts it on the page. */	insertFilterElement: function { var ul = document.createElement( 'ul' ); ul.id = 'content-filter'; for ( var i = 0; i < contentFilter.items.length; ++i ) { ul.appendChild( contentFilter.items[ i ] ); }		var info = contentFilterConfig.filtersInfoId && document.getElementById( contentFilterConfig.filtersInfoId ); if ( !info ) { var wrapper = document .getElementsByClassName( 'page-header__actions' ) .item( 0 ); wrapper.prepend( ul ); return; }		contentFilter.getMessage( 'info', function ( pageContent ) {			info.append( pageContent || document.createTextNode(					'Use one of the following filters to hide the wiki ' +					'content unrelated to your game version:'				), document.createElement( 'br' ), ul );		} );	},

/**	 * Gets the value of a localized message. * @param {string}             name     The message name. * @param {(e:ChildNode)=>void} callback The function to call when the *                                      message has been retrieved. */	getMessage: function ( name, callback ) { var messagePage         = contentFilterConfig.messagesLocation + name, localizedMessagePage = messagePage + '/' + contentFilter.getPageLanguage;

contentFilter.pageExists( messagePage ).then( messagePageExists );

function messagePageExists( /** @type {boolean} */ pageExists ) { if ( !pageExists ) { callback( null ); return; }			if ( !contentFilterConfig.languageCodes.length ) { contentFilter.getPageContent( messagePage ).then( callback ); return; }			contentFilter .pageExists( localizedMessagePage ) .then( localizedMessagePageExists ); }

function localizedMessagePageExists( /** @type {boolean} */ pageExists ) { if ( !pageExists ) { contentFilter.getPageContent( messagePage ).then( callback ); return; }			contentFilter.getPageContent( localizedMessagePage ).then( callback ); }	},

/**	 * Indicates whether a page exists. * @param {string} pageName The name of the page. * @returns The boolean promise. */	pageExists: function ( pageName ) { return contentFilter.api .get( { action: 'query', titles: pageName } ) .then( function ( ret ) {				return !ret.query.pages[ '-1' ];			} ); },

/**	 * Gets the HTML content of a page. * @param {string} pageName The name of the page. * @returns The HTML content promise. */	getPageContent: function ( pageName ) { return contentFilter.api .parse( '' ) .then( function ( parserOutput ) {				return contentFilter.stringToElements( parserOutput ).firstChild;			} ); },

/**	 * Generates DOM elements from a string. * @param {string} str The DOM string. * @returns The generated DOM elements. */	stringToElements: function ( str ) { var template = document.createElement( 'template' ); template.innerHTML = str; return template.content.firstChild; },

/**	 * Gets the language used on the page. * @returns The language code used on the page. */	getPageLanguage: function { var pageName = mw.config.get( 'wgPageName' ), lastPartIndex = pageName.lastIndexOf( '/' ); if ( lastPartIndex === -1 ) { return 'en'; }		var lastPart = pageName.substr( lastPartIndex + 1 ); if ( !contentFilterConfig.languageCodes.includes( lastPart ) ) { return 'en'; }		return lastPart; },

/**	 * Updates the index of the currently selected filter form item from the URL * parameters. * @returns True if a valid filter should be applied, false otherwise. */	updateSelectedIndex: function { if ( contentFilter.selectedIndex !== -1 ) { return true; }		var urlParam = mw.util.getParamValue( contentFilterConfig.urlParam ); if ( !urlParam ) { return false; }		contentFilter.selectedIndex = parseInt( urlParam, 10 ); if (			contentFilter.isIndex( contentFilter.selectedIndex, contentFilter.items )		) {			return true; }		contentFilter.selectedIndex = -1; mw.log.error(			'Content Filter: The selected numeric filter (' + urlParam + ') ' +			'is unavailable, please use an integer x so 0 ≤ x ≤ ' +			( contentFilter.items.length - 1 ) + '. No filtering will be ' +			'performed.'		); return false; },

/**	 * Indicates if a number is a valid index of an array. * @param {number} number The number. * @param {any[]} array  The array. * @returns True if "array[ number ]" exists, false otherwise. */	isIndex: function ( number, array ) { return !isNaN( number ) && number >= 0 && number < array.length; },

/**	 * Removes elements with a filter from a container. * @param {Element} container The container to remove elements from. */	applyFilter: function ( container ) { contentFilterConfig.preprocess( container ); if ( contentFilterConfig.blockFilterClass ) { contentFilter.forEachLiveElement(				container.getElementsByClassName( contentFilterConfig.blockFilterClass ),				contentFilter.processBlockFilter			); }		if ( contentFilterConfig.wrapperFilterClass ) { contentFilter.forEachLiveElement(				container.getElementsByClassName( contentFilterConfig.wrapperFilterClass ),				contentFilter.processWrapperFilter			); }		if ( contentFilterConfig.inlineFilterClass ) { contentFilter.forEachLiveElement(				container.getElementsByClassName( contentFilterConfig.inlineFilterClass ),				contentFilter.processInlineFilter			); }		while ( contentFilter.postponed.length ) { var todo = contentFilter.postponed; contentFilter.postponed = []; for ( var i = 0; i < todo.length; ++i ) { todo[ i ][ 0 ]( todo[ i ][ 1 ] ); }		}		contentFilterConfig.postprocess( container ); },

/**	 * Performs the specified action for each element of a live list. * @template E The element type. * @param {HTMLCollectionOf} liveElementList The live element list. * @param {(element:E)=>void}  callback        A function called for each *                                             element. */	forEachLiveElement: function ( liveElementList, callback ) { var previousLength = liveElementList.length; for ( var i = 0; i < liveElementList.length; ) { callback( liveElementList[ i ] ); if ( previousLength > liveElementList.length ) { previousLength = liveElementList.length; } else { ++i; }		}	},

/**	 * Removes an element with a block filter if its filter does not match the * selected one. * @param {Element} element The element. */	processBlockFilter: function ( element ) { var elementFilter = contentFilter.getFilter( element ); if ( ( elementFilter & contentFilter.selectedFilter ) > 0 ) { element.classList.remove(				/** @type {string} */				( contentFilterConfig.blockFilterClass )			); } else if ( !contentFilter.handleBlockFilter( element ) ) { element.classList.remove(				/** @type {string} */				( contentFilterConfig.blockFilterClass )			); mw.log.warn( 'unmatched block filter' ); }	},

/**	 * Removes an element with a block filter if its filter does not match the * selected one. * @param {Element} element The element. */	processWrapperFilter: function ( element ) { var elementFilter = contentFilter.getFilter( element ); if ( ( elementFilter & contentFilter.selectedFilter ) > 0 ) { element.classList.remove(				/** @type {string} */				( contentFilterConfig.wrapperFilterClass )			); } else if ( !contentFilter.handleWrapperFilter( element ) ) { contentFilter.unwrap( element ); mw.log.warn( 'unmatched wrapper filter' ); }	},

/**	 * Removes an element with an inline filter. Also removes its related * content if the element filter does not match the selected one. * @param {Element} element The element. */	processInlineFilter: function ( element ) { var elementFilter = contentFilter.getFilter( element ); if ( ( elementFilter & contentFilter.selectedFilter ) > 0 ) { contentFilter.removeElementWithoutContext( element ); } else if ( !contentFilter.handleInlineFilter( element ) ) { element.classList.remove(				/** @type {string} */				( contentFilterConfig.inlineFilterClass )			); mw.log.warn( 'unmatched inline filter' ); }	},

/**	 * Removes an element and its empty parents. * @param {Element} element The element to remove. */	removeElementWithoutContext: function ( element ) { var parent = element.parentElement; while (			parent !== contentFilter.parserOutput &&			!contentFilter.hasSibling( element ) &&			parent.tagName !== 'TD'		) { element = parent; parent = parent.parentElement; }		parent.removeChild( element ); },

/**	 * Removes an element with a block filter. * @param {Element} element The element to remove. * @returns True if the removal has been handled properly, false otherwise. */	handleBlockFilter: function ( element ) { if (			contentFilterConfig.blockFilterCustomHandler &&			contentFilterConfig.blockFilterCustomHandler( element )		) { return true; }

contentFilter.removeElement( element ); return true; },

/**	 * Removes an element with a wrapper filter. * @param {Element} element The element to remove. * @returns True if the removal has been handled properly, false otherwise. */	handleWrapperFilter: function ( element ) { if (			contentFilterConfig.wrapperFilterCustomHandler &&			contentFilterConfig.wrapperFilterCustomHandler( element )		) { return true; }

contentFilter.removeElement( element ); return true; },

/**	 * Removes an element with an inline filter and its related content. * @param {Element} element The element to remove. * @returns True if the removal has been handled properly, false otherwise. */	handleInlineFilter: function ( element ) { if (			contentFilterConfig.inlineFilterCustomHandler &&			contentFilterConfig.inlineFilterCustomHandler( element )		) { return true; }

var parent = element.parentElement;

if (			contentFilterConfig.contextFilterClass &&			parent.classList.contains( contentFilterConfig.contextFilterClass )		) {			var heading = contentFilter.getPreviousHeading( parent ); contentFilter.removeElement( heading || parent ); return true; }

if (			parent.tagName === 'LI' &&			!contentFilter.hasPreviousSibling( element )		) { contentFilter.removeElement( parent ); return true; }

contentFilter.removeGhostSiblings( element ); if ( !contentFilter.getNextText( element ) ) { var nextElement = element.nextElementSibling; if ( !nextElement ) { contentFilter.removeElement( element.parentElement ); return true; }			if ( nextElement.tagName === 'BR' ) { contentFilter.removePreviousNodesUntilName( nextElement, 'BR' ); nextElement.remove; return true; }		}

var previousElement = element.previousElementSibling, previousText   = contentFilter.getPreviousText( element ); if (			previousText ?				!previousText.endsWith( '.' ) :				previousElement && previousElement.tagName !== 'BR'		) { return false; }

/** @type {ChildNode} */ var node       = element, nextNode   = node, textContent = ''; do { textContent = node.textContent.trimEnd; nextNode   = node.nextSibling; node.remove; node = nextNode; if ( !node ) { if ( !previousElement && !previousText ) { contentFilter.removeElement( parent ); }				return true; }			if ( node.nodeName === 'BR' ) { node.remove; return true; }			if (				textContent.endsWith( '.' ) &&				node instanceof HTMLElement &&				node.classList.contains( /** @type {string} */ ( contentFilterConfig.inlineFilterClass ) )			) {				return true; }		} while ( true ); },

/**	 * Removes an element. Also removes its containers and previous headings if * they are empty after the element being removed. * @param {Element} element The element to remove. */	removeElement: function ( element ) { if ( element.classList.contains( 'gallerytext' ) ) { while ( element.classList.contains( 'gallerybox' ) ) { element = element.parentElement; }		}		contentFilter.removeGhostSiblings( element ); switch ( element.tagName ) { case 'H2': case 'H3': case 'H4': case 'H5': case 'H6': contentFilter.removeHeadingElement( element ); return; case 'LI': contentFilter.removeListItem( element ); return; case 'TBODY': contentFilter.removeElement( element.parentElement ); return; case 'TR': if ( !contentFilter.hasSibling( element ) ) { contentFilter.removeElement( element.parentElement ); } else { element.remove; }			return; case 'TH': case 'TD': contentFilter.removeTableCell( element ); return; }		contentFilter.removeDefaultElement( element ); },

/**	 * Handles the removal of a heading element. * @param {Element} element The  element. */	removeHeadingElement: function ( element ) { var headingLevel = contentFilter.getHeadingLevel( element ), sibling     = element.nextElementSibling; while ( !contentFilter.isOutOfSection( sibling, headingLevel ) ) { var toRemove = sibling; sibling = sibling.nextElementSibling; toRemove.remove; }		contentFilter.removeTocElement(			element.getElementsByClassName( 'mw-headline' )[ 0 ].id		); },

/**	 * Handles the removal of a list item. * @param {Element} item The  element. */	removeListItem: function ( item ) { var list = item.parentElement; if ( list.childNodes.length > 1 ) { item.remove; return; }		contentFilter.removeElement( list ); },

/**	 * Handles the removal of a table cell, from clearing it to removing the * entire table depending to the situation. * @param {Element} cell The  element. */	removeTableCell: function ( cell ) { var row   = cell.parentElement, tbody = row.parentElement, table = tbody.parentElement, column = 0; for (			var sibling = cell.previousElementSibling;			sibling;			sibling = sibling.previousElementSibling		) { ++column; }

if ( tbody.tagName === 'THEAD' && cell.tagName === 'TH' ) { // TODO: Fix with mw-collapsible & sortable. var isLastColumn = !cell.nextElementSibling; row.removeChild( cell ); if ( !tbody.nextElementSibling ) { return; }			var nextRow = tbody.nextElementSibling.firstElementChild; while ( nextRow ) { nextRow.removeChild( nextRow.children[ column ] ); nextRow = nextRow.nextElementSibling; }			if ( isLastColumn ) { table.classList.remove(					'mw-collapsible',					'mw-made-collapsible'				); $( table ).makeCollapsible; }		}

var mainColumn = contentFilterConfig.mainColumnClassIntro && contentFilter.findClassStartingWith(				table,				contentFilterConfig.mainColumnClassIntro			) || 1; if ( +mainColumn === column + 1 ) { contentFilter.removeElement( row ); return; }

if (			contentFilterConfig.listTableClass &&			table.classList.contains( contentFilterConfig.listTableClass )		) { row.removeChild( cell ); return; }		while ( cell.firstChild ) { cell.removeChild( cell.firstChild ); }	},

/**	 * Handles the removal of any element. * @param {Element} element The element. */	removeDefaultElement: function ( element ) { if ( element.classList.contains( 'mw-headline' ) ) { contentFilter.removeElement( element.parentElement ); return; }		var parent = element.parentElement, sibling = element.previousElementSibling; element.remove; contentFilter.ensureNonEmptySection( sibling ); if ( !parent.childNodes.length ) { contentFilter.removeElement( parent ); }	},

/**	 * Recursively removes an element if it is a heading and its associated * section is empty. Also updates the table of contents. * @param {Element} element The element. */	ensureNonEmptySection: function ( element ) { if ( !element ) { return; }		while ( !( element instanceof HTMLHeadingElement ) ) { if (				!contentFilterConfig.inContentAddClass ||				!element.classList.contains( contentFilterConfig.inContentAddClass )			) {				return; }			element = element.previousElementSibling; }		if (			!contentFilter.isOutOfSection( element.nextElementSibling, contentFilter.getHeadingLevel( element ) )		) {			return; }		var previousElement = element.previousElementSibling; contentFilter.removeTocElement(			element.getElementsByClassName( 'mw-headline' )[ 0 ].id		); element.parentNode.removeChild( element ); contentFilter.ensureNonEmptySection( previousElement ); },

/**	 * Removes a row (associated to a removed heading element) from the * table of contents, then updates the numbering of the following rows. * @param {string} id The ID of the removed heading element. * @returns True if a row has been removed from the table of contents, false *         if the table of contents has not been defined or if there is no *         associated row. */	removeTocElement: function ( id ) { if ( !contentFilter.toc ) { return false; }		var element = contentFilter.toc.querySelector( '[href="#' + id + '"]' ); if ( !element ) { return false; }		var parent    = element.parentElement, number    = element .getElementsByClassName( 'tocnumber' )[ 0 ].textContent, lastDotPos = number.lastIndexOf( '.', 1 ) + 1, lastNumber = +number.substring( lastDotPos ), nextParent = parent.nextElementSibling; while ( nextParent ) { var nextNumbers = nextParent .getElementsByClassName( 'tocnumber' ); for ( var i = 0; i < nextNumbers.length; ++i ) { var textContent = nextNumbers[ i ].textContent; nextNumbers[ i ].textContent = textContent.substring( 0, lastDotPos ) + lastNumber + textContent.substring( number.length ); }			++lastNumber; nextParent = nextParent.nextElementSibling; }		parent.parentNode.removeChild( parent ); return true; },

/**	 * Gets the level of a heading element. * @param {Element} heading The heading element. * @returns The level of the heading element. */	getHeadingLevel: function ( heading ) { return +heading.tagName.substr( 1 ); },

/**	 * Indicates whether an element would be the first below a section defined * with a previous heading element. * @param {Element} element     The element. * @param {number} headingLevel The level of the last heading element. * @returns True if the element is missing, defines a new section with a	 * 	       higher or same level, or is the end of the page content. */	isOutOfSection: function ( element, headingLevel ) { return !element || element instanceof HTMLHeadingElement && headingLevel >= contentFilter.getHeadingLevel( element ) || contentFilterConfig.contentEndClass && element.classList.contains(			      contentFilterConfig.contentEndClass		       ); },

/**	 * Indicates whether an element or all its children have a class. * @param {Element} element  The element. * @param {string} className The class name. * @returns {boolean} True if the element or all its children have the *                   given class, false otherwise. */	hasClass: function ( element, className ) { if ( !element ) { return false; }		if ( element.classList.contains( className ) ) { return true; }		var children = element.children; if ( !children.length ) { return false; }		for ( var i = 0; i < children.length; ++i ) { if ( !contentFilter.hasClass( children[ i ], className ) ) { return false; }		}		return true; },

/**	 * Indicates whether an element has a sibling. Ignores comments and * "invisible" strings. * @param {Element} element The element. * @returns True if the element has no sibling other than a comment or an *         "invisible" string. */	hasSibling: function ( element ) { return contentFilter.hasPreviousSibling( element ) || contentFilter.hasNextSibling( element ); },

/**	 * Indicates whether an element has a previous sibling. Ignores comments and * "invisible" strings. * @param {Element} element The element. * @returns True if the element has a previous sibling other than a comment *         or an "invisible" string. */	hasPreviousSibling: function ( element ) { var sibling = element.previousSibling; if ( !sibling ) { return false; }		while ( contentFilter.isGhostNode( sibling ) ) { sibling = sibling.previousSibling; if ( !sibling ) { return false; }		}		return true; },

/**	 * Indicates whether an element has a next sibling. Ignores comments and * "invisible" strings. * @param {Element} element The element. * @returns True if the element has a next sibling other than a comment or *         an "invisible" string. */	hasNextSibling: function ( element ) { var sibling = element.nextSibling; if ( !sibling ) { return false; }		while ( contentFilter.isGhostNode( sibling ) ) { sibling = sibling.nextSibling; if ( !sibling ) { return false; }		}		return true; },

/**	 * Indicates whether a node should be considered as an additional * non-essential node. * @param {Node} node The node. * @returns True if the node is non-essential, false otherwise. */	isGhostNode: function ( node ) { if ( !node ) { return false; }		switch ( node.nodeType ) { case Node.COMMENT_NODE: return true; case Node.TEXT_NODE: return !node.textContent.trim; case Node.ELEMENT_NODE: /** @type {Element} */ var element = ( node ); return element.classList.contains( 'mw-collapsible-toggle' ) || contentFilterConfig.skipClass && element.classList.contains( contentFilterConfig.skipClass ); }		return false; },

/**	 * Removes the non-essential nodes around a node. * @param {Node} node The node. */	removeGhostSiblings: function ( node ) { var sibling = node.previousSibling; while ( contentFilter.isGhostNode( sibling ) ) { sibling.remove; sibling = node.previousSibling; }		sibling = node.nextSibling; while ( contentFilter.isGhostNode( sibling ) ) { sibling.remove; sibling = node.nextSibling; }	},

/**	 * Removes nodes before a node while they do not have the given node name. * @param {ChildNode} node        The node. * @param {string}   nodeName     The node name. * @param {boolean}  [removeLast] If the last node (with the given name) *                                should also be removed. */	removePreviousNodesUntilName: function ( node, nodeName, removeLast ) { var sibling = node.previousSibling; while ( sibling && ( sibling.nodeName !== nodeName ) ) { sibling.remove; sibling = node.previousSibling; }		if ( removeLast ) { sibling.remove; }	},

/**	 * Removes nodes after a node while they do not have the given node name. * @param {ChildNode} node        The node. * @param {string}   nodeName     The node name. * @param {boolean}  [removeLast] If the last node (with the given name) *                                should also be removed. */	removeNextNodesUntilName: function ( node, nodeName, removeLast ) { var sibling = node.nextSibling; while ( sibling && ( sibling.nodeName !== nodeName ) ) { sibling.remove; sibling = node.nextSibling; }		if ( removeLast ) { sibling.remove; }	},

/**	 * Removes nodes before a node while they do not contain the given text. * @param {ChildNode} node        The node. * @param {string}   text 	       The searched text. * @param {boolean}  [removeText] If the searched text should also be *                                removed from the last node. */	removePreviousNodeUntilText: function ( node, text, removeText ) { var sibling = node.previousSibling; while (			sibling && ( sibling.nodeType !== Node.TEXT_NODE || sibling.textContent.indexOf( text ) === -1 )		) {			sibling.remove; sibling = node.previousSibling; }		if ( !sibling ) { return; }		if ( !removeText ) { sibling.textContent = sibling.textContent.substr(				0,				sibling.textContent.lastIndexOf( text ) + text.length			); return; }		sibling.textContent = sibling.textContent .substr( 0, sibling.textContent.lastIndexOf( text ) ) .trimEnd; if ( !sibling.textContent ) { sibling.remove; }	},	/**	 * Removes nodes after a node while they do not contain the given text. * @param {ChildNode} node        The node. * @param {string}   text 	       The searched text. * @param {boolean}  [removeText] If the searched text should also be *                                removed from the last node. */	removeNextNodeUntilText: function ( node, text, removeText ) { var sibling = node.nextSibling; while (			sibling && ( sibling.nodeType !== Node.TEXT_NODE || sibling.textContent.indexOf( text ) === -1 )		) {			sibling.remove; sibling = node.nextSibling; }		if ( !sibling ) { return; }		if ( !removeText ) { sibling.textContent = sibling.textContent .substr( sibling.textContent.indexOf( text ) ); return; }		sibling.textContent = sibling.textContent .substr( sibling.textContent.indexOf( text ) + text.length ) .trimStart; if ( !sibling.textContent ) { sibling.remove; }	},

/**	 * Gets the text from the text node before a DOM element. * @param {Element} element The element. */	getPreviousText: function ( element ) { var previousNode = element.previousSibling; return previousNode instanceof Text && previousNode.textContent ? previousNode.textContent.trim : '';	},

/**	 * Gets the text from the text node after a DOM element. * @param {Element} element The element. */	getNextText: function ( element ) { var nextNode = element.nextSibling; return nextNode instanceof Text && nextNode.textContent ? nextNode.textContent.trim : '';	},

/**	 * Removes an element, leaving its content in place. * @param {Element}  element  The element to remove. * @param {ChildNode} [target] The node which should be directly after the *                            initial element contents, defaults to the *                            initial element. */	unwrap: function ( element, target ) { if ( !target ) { target = element; }		var parent = target.parentElement; if ( !parent ) { return; }		var childNode = element.firstChild; if ( !childNode ) { element.remove; return; }		var sibling = target.previousSibling; if (			sibling &&			childNode.nodeType === Node.TEXT_NODE &&			sibling.nodeType === Node.TEXT_NODE		) { sibling.textContent += childNode.textContent; childNode.remove; }		childNode = element.lastChild; if ( !childNode ) { element.remove; return; }		sibling = target.nextSibling; if (			sibling &&			childNode.nodeType === Node.TEXT_NODE &&			sibling.nodeType === Node.TEXT_NODE		) { sibling.textContent = childNode.textContent + sibling.textContent; childNode.remove; }		childNode = element.firstChild; while ( childNode ) { parent.insertBefore( childNode, target ); childNode = element.firstChild; }		element.remove; },

/**	 * Updates the selected filter form item. */	updateSelectedFilterItem: function { delete contentFilter.uri.query[ contentFilterConfig.urlParam ]; var item = contentFilter.items[ contentFilter.selectedIndex ]; item.classList.add( 'content-filter-item-active' ); item.firstElementChild.setAttribute(			'href',			contentFilter.uri.toString		); },

/**	 * Adds a corresponding filter URL parameter to anchors where none is * used. */	updateAnchorsFilter: function { var anchors = document.getElementsByTagName( 'a' ); for ( var i = 0; i < anchors.length; ++i ) { var anchor = anchors[ i ]; if ( !anchor.href ) { continue; }			if (				anchor.parentElement.classList.contains( 'content-filter-item' )			) { continue; }			var uri = new mw.Uri( anchor.href ); if ( uri.query[ contentFilterConfig.urlParam ] ) { continue; }			var match = uri.path.match(				mw.RegExp					.escape( mw.config.get( 'wgArticlePath' ) )					.replace( '\\$1', '(.*)' )			); if ( !match ) { continue; }			var title = new mw.Title(				mw.Uri.decode( match[ 1 ] ) ||				mw.config.get( 'wgMainPageTitle' )			); if (				!contentFilterConfig.filteredNamespaces.includes( title.getNamespaceId ) &&				!contentFilterConfig.filteredSpecialTitles.includes( title.getPrefixedText )			) {				continue; }			/** @type */ var obj = {}; obj[ contentFilterConfig.urlParam ] = contentFilter.selectedIndex; uri.extend( obj ); anchor.href = uri.toString; }	} };

$( contentFilter.init );