const {
	HighlightStyle,
	LanguageSupport,
	StreamLanguage,
	StreamParser,
	StringStream,
	Tag,
	syntaxHighlighting
} = require( 'ext.CodeMirror.v6.lib' );
const mwModeConfig = require( './codemirror.mediawiki.config.js' );
const bidiIsolationExtension = require( './codemirror.mediawiki.bidiIsolation.js' );
const { codeFoldingExtension } = require( './codemirror.mediawiki.codeFolding.js' );
const { autocompleteExtension, completionSource } = require( './codemirror.mediawiki.autocomplete.js' );
const openLinksExtension = require( './codemirror.mediawiki.openLinks.js' );
const mwKeymap = require( './codemirror.mediawiki.keymap.js' );

const copyState = ( state ) => {
	const newState = {};
	for ( const key in state ) {
		const val = state[ key ];
		if ( Array.isArray( val ) ) {
			newState[ key ] = [ ...val ];
		} else if ( key === 'extState' ) {
			newState.extState =
				( state.extName && state.extMode && state.extMode.copyState || copyState )( val );
		} else {
			newState[ key ] = key !== 'data' && val && typeof val === 'object' ? Object.assign( {}, val ) : val;
		}
	}
	return newState;
};

/**
 * MediaWiki language support for CodeMirror 6.
 * Adapted from the original CodeMirror 5 stream parser by Pavel Astakhov.
 *
 * @module CodeMirrorModeMediaWiki
 *
 * @example
 * mw.loader.using( [
 *   'ext.CodeMirror.v6',
 *   'ext.CodeMirror.v6.mode.mediawiki'
 * ] ).then( ( require ) => {
 *   const CodeMirror = require( 'ext.CodeMirror.v6' );
 *   const mediawikiLang = require( 'ext.CodeMirror.v6.mode.mediawiki' );
 *   const cm = new CodeMirror( myTextarea, mediawikiLang() );
 *   cm.initialize();
 * } );
 */
class CodeMirrorModeMediaWiki {
	/**
	 * @param {Object} config MediaWiki configuration as generated by DataScript.php
	 * @internal
	 */
	constructor( config ) {
		this.config = config;
		this.urlProtocols = new RegExp( `^(?:${ config.urlProtocols })(?=[^\\s\u00a0{[\\]<>~).,'])`, 'i' );
		this.tokenTable = mwModeConfig.tokenTable;
		this.registerGroundTokens();

		// Dynamically register any tags that aren't already in CodeMirrorModeMediaWikiConfig
		Object.keys( config.tags ).forEach( ( tag ) => mwModeConfig.addTag( tag ) );

		this.functionSynonyms = [
			...Object.keys( config.functionSynonyms[ 0 ] )
				.map( ( label ) => ( { type: 'function', label } ) ),
			...Object.keys( config.functionSynonyms[ 1 ] )
				.map( ( label ) => ( { type: 'constant', label } ) )
		];
		this.doubleUnderscore = [
			...Object.keys( config.doubleUnderscore[ 0 ] ),
			...Object.keys( config.doubleUnderscore[ 1 ] )
		].map( ( label ) => ( { type: 'constant', label } ) );
		const extTags = Object.keys( config.tags );
		this.extTags = extTags.map( ( label ) => ( { type: 'type', label } ) );
		this.htmlTags = Object.keys( mwModeConfig.permittedHtmlTags )
			.filter( ( tag ) => !extTags.includes( tag ) )
			.map( ( label ) => ( { type: 'type', label } ) );
		this.protocols = config.urlProtocols.split( '|' )
			.map( ( label ) => ( { type: 'namespace', label: label.replace( /\\([:/])/g, '$1' ) } ) );
		this.redirectRegex = new RegExp( `^\\s*(?:${
			config.redirection.join( '|' )
		})(\\s*:)?\\s*(?=\\[\\[)`, 'i' );
		this.nsRegex = new RegExp( `^(${
			Object.keys( mw.config.get( 'wgNamespaceIds' ) ).filter( Boolean ).join( '|' ).replace( /_/g, ' ' )
		})\\s*:\\s*`, 'i' );
	}

	/**
	 * Register the ground tokens. These aren't referenced directly in the StreamParser, nor do
	 * they have a parent Tag, so we don't need them as constants like we do for other tokens.
	 * See this.makeLocalStyle() for how these tokens are used.
	 *
	 * @private
	 */
	registerGroundTokens() {
		[
			'mw-ext-ground',
			'mw-ext-link-ground',
			'mw-ext2-ground',
			'mw-ext2-link-ground',
			'mw-ext3-ground',
			'mw-ext3-link-ground',
			'mw-link-ground',
			'mw-template-ext-ground',
			'mw-template-ext-link-ground',
			'mw-template-ext2-ground',
			'mw-template-ext2-link-ground',
			'mw-template-ext3-ground',
			'mw-template-ext3-link-ground',
			'mw-template-ground',
			'mw-template-link-ground',
			'mw-template2-ext-ground',
			'mw-template2-ext-link-ground',
			'mw-template2-ext2-ground',
			'mw-template2-ext2-link-ground',
			'mw-template2-ext3-ground',
			'mw-template2-ext3-link-ground',
			'mw-template2-ground',
			'mw-template2-link-ground',
			'mw-template3-ext-ground',
			'mw-template3-ext-link-ground',
			'mw-template3-ext2-ground',
			'mw-template3-ext2-link-ground',
			'mw-template3-ext3-ground',
			'mw-template3-ext3-link-ground',
			'mw-template3-ground',
			'mw-template3-link-ground'
		].forEach( ( ground ) => mwModeConfig.addToken( ground ) );
	}

	eatHtmlEntity( stream, style ) {
		let ok;
		if ( stream.eat( '#' ) ) {
			if ( stream.eat( 'x' ) ) {
				ok = stream.eatWhile( /[a-fA-F\d]/ ) && stream.eat( ';' );
			} else {
				ok = stream.eatWhile( /[\d]/ ) && stream.eat( ';' );
			}
		} else {
			ok = stream.eatWhile( /[\w.\-:]/ ) && stream.eat( ';' );
		}
		if ( ok ) {
			return mwModeConfig.tags.htmlEntity;
		}
		return style;
	}

	isNested( state ) {
		return state.nExt > 0 || state.nTemplate > 0 || state.nLink > 0 || state.nExtLink > 0;
	}

	makeFullStyle( style, state ) {
		return ( typeof style === 'string' ?
			style :
			`${ style[ 0 ] } ${ state.bold || state.nDt > 0 ? mwModeConfig.tags.strong : '' } ${ state.italic ? mwModeConfig.tags.em : '' }`
		).replace( /\s{2,}/g, ' ' ).trim() || ' ';
	}

	makeStyle( style, state, endGround ) {
		return [ this.makeLocalStyle( style, state, endGround ) ];
	}

	makeLocalStyle( style, state, endGround ) {
		let ground = '';
		switch ( state.nTemplate ) {
			case 0:
				break;
			case 1:
				ground += '-template';
				break;
			case 2:
				ground += '-template2';
				break;
			default:
				ground += '-template3';
				break;
		}
		switch ( state.nExt ) {
			case 0:
				break;
			case 1:
				ground += '-ext';
				break;
			case 2:
				ground += '-ext2';
				break;
			default:
				ground += '-ext3';
				break;
		}
		if ( state.nLink > 0 || state.nExtLink > 0 ) {
			ground += '-link';
		}
		if ( ground !== '' ) {
			style = `mw${ ground }-ground ${ style }`;
		}
		if ( endGround ) {
			state[ endGround ]--;
		}
		return style.trim();
	}

	eatBlock( style, terminator, consumeLast ) {
		return ( stream, state ) => {
			if ( stream.skipTo( terminator ) ) {
				if ( consumeLast !== false ) {
					stream.match( terminator );
				}
				state.tokenize = state.stack.pop();
			} else {
				stream.skipToEnd();
			}
			return this.makeLocalStyle( style, state );
		};
	}

	eatEnd( style ) {
		return ( stream, state ) => {
			stream.skipToEnd();
			state.tokenize = state.stack.pop();
			return this.makeLocalStyle( style, state );
		};
	}

	eatChar( char, style ) {
		return ( stream, state ) => {
			state.tokenize = state.stack.pop();
			if ( stream.eat( char ) ) {
				return this.makeLocalStyle( style, state );
			}
			return this.makeLocalStyle( mwModeConfig.tags.error, state );
		};
	}

	eatSectionHeader( count ) {
		return ( stream, state ) => {
			if ( stream.match( /^[^&<[{~]+/ ) ) {
				if ( stream.eol() ) {
					stream.backUp( count );
					state.tokenize = this.eatEnd( mwModeConfig.tags.sectionHeader );
				} else if ( stream.match( /^<!--(?!.*?-->.*?=)/, false ) ) {
					// T171074: handle trailing comments
					stream.backUp( count );
					state.tokenize = this.eatBlock( mwModeConfig.tags.sectionHeader, '<!--', false );
				}
				return mwModeConfig.tags.section; // style is null
			}
			return this.eatWikiText( mwModeConfig.tags.section )( stream, state );
		};
	}

	inVariable( stream, state ) {
		if ( stream.match( /^[^{}|]+/ ) ) {
			return this.makeLocalStyle( mwModeConfig.tags.templateVariableName, state );
		}
		if ( stream.eat( '|' ) ) {
			state.tokenize = this.inVariableDefault.bind( this );
			return this.makeLocalStyle( mwModeConfig.tags.templateVariableDelimiter, state );
		}
		if ( stream.match( '}}}' ) ) {
			state.tokenize = state.stack.pop();
			return this.makeLocalStyle( mwModeConfig.tags.templateVariableBracket, state );
		}
		if ( stream.match( '{{{' ) ) {
			state.stack.push( state.tokenize );
			return this.makeLocalStyle( mwModeConfig.tags.templateVariableBracket, state );
		}
		stream.next();
		return this.makeLocalStyle( mwModeConfig.tags.templateVariableName, state );
	}

	inVariableDefault( stream, state ) {
		if ( stream.match( /^[^{}[<&~]+/ ) ) {
			return this.makeLocalStyle( mwModeConfig.tags.templateVariable, state );
		}
		if ( stream.match( '}}}' ) ) {
			state.tokenize = state.stack.pop();
			return this.makeLocalStyle( mwModeConfig.tags.templateVariableBracket, state );
		}
		return this.eatWikiText( mwModeConfig.tags.templateVariable )( stream, state );
	}

	inParserFunctionName( stream, state ) {
		// FIXME: {{#name}} and {{uc}} are wrong, must have ':'
		if ( stream.match( /^#?[^:}{~]+/ ) ) {
			return this.makeLocalStyle( mwModeConfig.tags.parserFunctionName, state );
		}
		if ( stream.eat( ':' ) ) {
			state.tokenize = this.inParserFunctionArguments.bind( this );
			return this.makeLocalStyle( mwModeConfig.tags.parserFunctionDelimiter, state );
		}
		if ( stream.match( '}}' ) ) {
			state.tokenize = state.stack.pop();
			return this.makeLocalStyle( mwModeConfig.tags.parserFunctionBracket, state, 'nExt' );
		}
		return this.eatWikiText( mwModeConfig.tags.parserFunction )( stream, state );
	}

	inParserFunctionArguments( stream, state ) {
		if ( stream.match( /^[^|}{[<&~]+/ ) ) {
			return this.makeLocalStyle( mwModeConfig.tags.parserFunction, state );
		} else if ( stream.eat( '|' ) ) {
			return this.makeLocalStyle( mwModeConfig.tags.parserFunctionDelimiter, state );
		} else if ( stream.match( '}}' ) ) {
			state.tokenize = state.stack.pop();
			return this.makeLocalStyle( mwModeConfig.tags.parserFunctionBracket, state, 'nExt' );
		}
		return this.eatWikiText( mwModeConfig.tags.parserFunction )( stream, state );
	}

	eatTemplatePageName( haveAte ) {
		return ( stream, state ) => {
			if ( stream.match( /^[\s\u00a0]*\|[\s\u00a0]*/ ) ) {
				state.tokenize = this.eatTemplateArgument( true );
				return this.makeLocalStyle( mwModeConfig.tags.templateDelimiter, state );
			}
			if ( stream.match( /^[\s\u00a0]*\}\}/ ) ) {
				state.tokenize = state.stack.pop();
				return this.makeLocalStyle( mwModeConfig.tags.templateBracket, state, 'nTemplate' );
			}
			if ( stream.match( /^[\s\u00a0]*<!--.*?-->/ ) ) {
				return this.makeLocalStyle( mwModeConfig.tags.comment, state );
			}
			if ( haveAte && stream.sol() ) {
				// @todo error message
				state.nTemplate--;
				state.tokenize = state.stack.pop();
				return '';
			}
			if ( stream.match( /^[\s\u00a0]*[^\s\u00a0|}<{&~]+/ ) ) {
				state.tokenize = this.eatTemplatePageName( true );
				return this.makeLocalStyle( mwModeConfig.tags.templateName, state );
			} else if ( stream.eatSpace() ) {
				if ( stream.eol() === true ) {
					return this.makeLocalStyle( mwModeConfig.tags.templateName, state );
				}
				return this.makeLocalStyle( mwModeConfig.tags.templateName, state );
			}
			return this.eatWikiText( mwModeConfig.tags.templateName )( stream, state );
		};
	}

	eatTemplateArgument( expectArgName ) {
		return ( stream, state ) => {
			if ( expectArgName && stream.eatWhile( /[^=|}{[<&~]/ ) ) {
				if ( stream.eat( '=' ) ) {
					state.tokenize = this.eatTemplateArgument( false );
					return this.makeLocalStyle( mwModeConfig.tags.templateArgumentName, state );
				}
				return this.makeLocalStyle( mwModeConfig.tags.template, state );
			} else if ( stream.eatWhile( /[^|}{[<&~]/ ) ) {
				return this.makeLocalStyle( mwModeConfig.tags.template, state );
			} else if ( stream.eat( '|' ) ) {
				state.tokenize = this.eatTemplateArgument( true );
				return this.makeLocalStyle( mwModeConfig.tags.templateDelimiter, state );
			} else if ( stream.match( '}}' ) ) {
				state.tokenize = state.stack.pop();
				return this.makeLocalStyle( mwModeConfig.tags.templateBracket, state, 'nTemplate' );
			}
			return this.eatWikiText( mwModeConfig.tags.template )( stream, state );
		};
	}

	eatExternalLinkProtocol( chars ) {
		return ( stream, state ) => {
			while ( chars > 0 ) {
				chars--;
				stream.next();
			}
			if ( stream.eol() ) {
				state.nExtLink--;
				// @todo error message
				state.tokenize = state.stack.pop();
			} else {
				state.tokenize = this.inExternalLink.bind( this );
			}
			return this.makeLocalStyle( mwModeConfig.tags.extLinkProtocol, state );
		};
	}

	inExternalLink( stream, state ) {
		if ( stream.sol() ) {
			state.nExtLink--;
			// @todo error message
			state.tokenize = state.stack.pop();
			return '';
		}
		if ( stream.match( /^[\s\u00a0]*\]/ ) ) {
			state.tokenize = state.stack.pop();
			return this.makeLocalStyle( mwModeConfig.tags.extLinkBracket, state, 'nExtLink' );
		}
		if ( stream.eatSpace() ) {
			state.tokenize = this.inExternalLinkText.bind( this );
			return this.makeStyle( '', state );
		}
		if ( stream.match( /^[^\s\u00a0\]{&~']+/ ) || stream.eatSpace() ) {
			if ( stream.peek() === '\'' ) {
				if ( stream.match( '\'\'', false ) ) {
					state.tokenize = this.inExternalLinkText.bind( this );
				} else {
					stream.next();
				}
			}
			return this.makeStyle( mwModeConfig.tags.extLink, state );
		}
		return this.eatWikiText( mwModeConfig.tags.extLink )( stream, state );
	}

	inExternalLinkText( stream, state ) {
		if ( stream.sol() ) {
			state.nExtLink--;
			// @todo error message
			state.tokenize = state.stack.pop();
			return '';
		}
		if ( stream.eat( ']' ) ) {
			state.tokenize = state.stack.pop();
			return this.makeLocalStyle( mwModeConfig.tags.extLinkBracket, state, 'nExtLink' );
		}
		if ( stream.match( /^[^'\]{&~<]+/ ) ) {
			return this.makeStyle( mwModeConfig.tags.extLinkText, state );
		}
		return this.eatWikiText( mwModeConfig.tags.extLinkText )( stream, state );
	}

	inLink( stream, state ) {
		if ( stream.sol() ) {
			state.nLink--;
			// @todo error message
			state.tokenize = state.stack.pop();
			return '';
		}
		if ( stream.match( /^[\s\u00a0]*#[\s\u00a0]*/ ) ) {
			state.tokenize = this.inLinkToSection.bind( this );
			return this.makeLocalStyle( mwModeConfig.tags.link, state );
		}
		if ( stream.match( /^[\s\u00a0]*\|[\s\u00a0]*/ ) ) {
			state.tokenize = this.eatLinkText();
			return this.makeLocalStyle( mwModeConfig.tags.linkDelimiter, state );
		}
		if ( stream.match( /^[\s\u00a0]*\]\]/ ) ) {
			state.tokenize = state.stack.pop();
			return this.makeLocalStyle( mwModeConfig.tags.linkBracket, state, 'nLink' );
		}
		if ( stream.match( /^[\s\u00a0]*[^\s\u00a0#|\]&~{]+/ ) || stream.eatSpace() ) {
			return this.makeStyle(
				`${ mwModeConfig.tags.linkPageName } ${ mwModeConfig.tags.pageName }`,
				state
			);
		}
		return this.eatWikiText(
			`${ mwModeConfig.tags.linkPageName } ${ mwModeConfig.tags.pageName }`
		)( stream, state );
	}

	inLinkToSection( stream, state ) {
		if ( stream.sol() ) {
			// @todo error message
			state.nLink--;
			state.tokenize = state.stack.pop();
			return '';
		}
		// FIXME '{{' breaks links, example: [[z{{page]]
		if ( stream.match( /^[^|\]&~{}]+/ ) ) {
			return this.makeLocalStyle( mwModeConfig.tags.linkToSection, state );
		}
		if ( stream.eat( '|' ) ) {
			state.tokenize = this.eatLinkText();
			return this.makeLocalStyle( mwModeConfig.tags.linkDelimiter, state );
		}
		if ( stream.match( ']]' ) ) {
			state.tokenize = state.stack.pop();
			return this.makeLocalStyle( mwModeConfig.tags.linkBracket, state, 'nLink' );
		}
		return this.eatWikiText( mwModeConfig.tags.linkToSection )( stream, state );
	}

	eatLinkText() {
		let linkIsBold, linkIsItalic;
		return ( stream, state ) => {
			let tmpstyle;
			if ( stream.match( ']]' ) ) {
				state.tokenize = state.stack.pop();
				return this.makeLocalStyle( mwModeConfig.tags.linkBracket, state, 'nLink' );
			}
			if ( stream.match( '\'\'\'' ) ) {
				linkIsBold = !linkIsBold;
				return this.makeLocalStyle(
					`${ mwModeConfig.tags.linkText } ${ mwModeConfig.tags.apostrophes }`,
					state
				);
			}
			if ( stream.match( '\'\'' ) ) {
				linkIsItalic = !linkIsItalic;
				return this.makeLocalStyle(
					`${ mwModeConfig.tags.linkText } ${ mwModeConfig.tags.apostrophes }`,
					state
				);
			}
			tmpstyle = mwModeConfig.tags.linkText;
			if ( linkIsBold ) {
				tmpstyle += ' ' + mwModeConfig.tags.strong;
			}
			if ( linkIsItalic ) {
				tmpstyle += ' ' + mwModeConfig.tags.em;
			}
			if ( stream.match( /^[^'\]{&~<]+/ ) ) {
				return this.makeStyle( tmpstyle, state );
			}
			return this.eatWikiText( tmpstyle )( stream, state );
		};
	}

	eatTagName( chars, isCloseTag, isHtmlTag ) {
		return ( stream, state ) => {
			let name = '';
			while ( chars > 0 ) {
				chars--;
				name = name + stream.next();
			}
			stream.eatSpace();
			name = name.toLowerCase();

			if ( isHtmlTag ) {
				if ( isCloseTag && !mwModeConfig.implicitlyClosedHtmlTags[ name ] ) {
					state.tokenize = this.eatChar( '>', mwModeConfig.tags.htmlTagBracket );
				} else {
					state.tokenize = this.eatHtmlTagAttribute( name );
				}
				return this.makeLocalStyle( mwModeConfig.tags.htmlTagName, state );
			}
			// it is the extension tag
			if ( isCloseTag ) {
				state.tokenize = this.eatChar(
					'>',
					`${ mwModeConfig.tags.extTagBracket } mw-ext-${ name }`
				);
			} else {
				state.tokenize = this.eatExtTagAttribute( name );
			}
			return this.makeLocalStyle( `${ mwModeConfig.tags.extTagName } mw-ext-${ name }`, state );
		};
	}

	eatHtmlTagAttribute( name, quote ) {
		const style = mwModeConfig.tags[ quote === undefined ? 'htmlTagAttribute' : 'htmlTagAttributeValue' ];
		return ( stream, state ) => {

			if ( stream.eat( '>' ) ) {
				if ( !( name in mwModeConfig.implicitlyClosedHtmlTags ) ) {
					state.inHtmlTag.push( name );
				}
				state.tokenize = state.stack.pop();
				return this.makeLocalStyle( mwModeConfig.tags.htmlTagBracket, state );
			}
			if ( stream.match( '/>' ) ) {
				state.tokenize = state.stack.pop();
				return this.makeLocalStyle( mwModeConfig.tags.htmlTagBracket, state );
			}
			const peek = stream.peek();
			if ( peek === '<' ) {
				const { pos } = stream,
					{ length } = state.stack,
					// eat comment or extension tag
					result = this.eatWikiText( '' )( stream, state );
				if (
					typeof result !== 'string' ||
					!result.includes( mwModeConfig.tags.comment ) &&
					!result.includes( mwModeConfig.tags.extTagBracket )
				) {
					state.stack.length = length;
					state.tokenize = state.stack.pop();
					stream.pos = pos;
					return '';
				}
				return result;
			}
			if ( peek === '&' || peek === '{' ) {
				return this.eatWikiText( style )( stream, state );
			}
			if ( quote ) {
				if ( stream.eat( quote[ 0 ] ) ) {
					state.tokenize = this.eatHtmlTagAttribute( name, quote[ 1 ] );
				} else {
					stream.match( new RegExp( `^(?:[^<>&{/${ quote[ 0 ] }]|/(?!>))+` ) );
				}
				return this.makeLocalStyle( style, state );
			}
			if ( quote === '' ) {
				if ( stream.sol() || /\s/.test( peek ) ) {
					state.tokenize = this.eatHtmlTagAttribute( name );
					return '';
				}
				stream.match( /^(?:[^\s<>&{/]|\/(?!>))+/ );
				return this.makeLocalStyle( style, state );
			}
			if ( stream.match( /^=\s*/ ) ) {
				const next = stream.peek();
				state.tokenize = this.eatHtmlTagAttribute( name, next === '"' || next === "'" ? next.repeat( 2 ) : '' );
				return this.makeLocalStyle( style, state );
			}
			stream.match( /^(?:[^<>&={/]|\/(?!>))+/ );
			return this.makeLocalStyle( style, state );
		};
	}

	eatNowiki() {
		return ( stream ) => {
			if ( stream.match( /^[^&]+/ ) ) {
				return '';
			}
			// eat &
			stream.next();
			return this.eatHtmlEntity( stream, '' );
		};
	}

	eatExtTagAttribute( name, quote ) {
		const style = `${ mwModeConfig.tags.extTagAttribute } mw-ext-${ name }`;
		return ( stream, state ) => {

			if ( stream.eat( '>' ) ) {
				state.extName = name;

				// FIXME: remove nowiki and pre from TagModes in extension.json after CM6 upgrade
				// leverage the tagModes system for <nowiki> and <pre>
				if ( name === 'nowiki' || name === 'pre' ) {
					// There's no actual processing within these tags (apart from HTML entities),
					// so startState and copyState can be no-ops.
					state.extMode = {
						startState: () => {},
						copyState: () => {},
						token: this.eatNowiki()
					};
				} else if ( name in this.config.tagModes ) {
					const mode = this.config.tagModes[ name ];
					if ( mode === 'mediawiki' || mode === 'text/mediawiki' ) {
						state.extMode = this.mediawiki;
						state.extState = state.extMode.startState();
					}
				}

				state.tokenize = this.eatExtTagArea( name );
				return this.makeLocalStyle( `${ mwModeConfig.tags.extTagBracket } mw-ext-${ name }`, state );
			}
			if ( stream.match( '/>' ) ) {
				state.tokenize = state.stack.pop();
				return this.makeLocalStyle( `${ mwModeConfig.tags.extTagBracket } mw-ext-${ name }`, state );
			}
			if ( quote ) {
				if ( stream.eat( quote[ 0 ] ) ) {
					state.tokenize = this.eatExtTagAttribute( name, quote[ 1 ] );
				} else {
					stream.match( new RegExp( `^(?:[^>/${ quote[ 0 ] }]|/(?!>))+` ) );
				}
				return this.makeLocalStyle( mwModeConfig.tags.extTagAttributeValue, state );
			}
			if ( quote === '' ) {
				if ( stream.sol() || /\s/.test( stream.peek() ) ) {
					state.tokenize = this.eatExtTagAttribute( name );
					return '';
				}
				stream.match( /^(?:[^>/\s]|\/(?!>))+/ );
				return this.makeLocalStyle( mwModeConfig.tags.extTagAttributeValue, state );
			}
			if ( stream.match( /^=\s*/ ) ) {
				const next = stream.peek();
				state.tokenize = this.eatExtTagAttribute( name, next === '"' || next === "'" ? next.repeat( 2 ) : '' );
				return this.makeLocalStyle( style, state );
			}
			stream.match( /^(?:[^>/=]|\/(?!>))+/ );
			return this.makeLocalStyle( style, state );
		};
	}

	eatExtTagArea( name ) {
		return ( stream, state ) => {
			const from = stream.pos,

				pattern = new RegExp( `</${ name }\\s*>`, 'i' ),
				m = pattern.exec( from ? stream.string.slice( from ) : stream.string );
			let origString = false,
				to;

			if ( m ) {
				if ( m.index === 0 ) {
					state.tokenize = this.eatExtCloseTag( name );
					state.extName = false;
					if ( state.extMode !== false ) {
						state.extMode = false;
						state.extState = false;
					}
					return state.tokenize( stream, state );
				}
				to = m.index + from;
				origString = stream.string;
				stream.string = origString.slice( 0, to );
			}

			state.stack.push( state.tokenize );
			state.tokenize = this.eatExtTokens( origString );
			return state.tokenize( stream, state );
		};
	}

	eatExtCloseTag( name ) {
		return ( stream, state ) => {
			stream.next(); // eat <
			stream.next(); // eat /
			state.tokenize = this.eatTagName( name.length, true, false );
			return this.makeLocalStyle( `${ mwModeConfig.tags.extTagBracket } mw-ext-${ name }`, state );
		};
	}

	eatExtTokens( origString ) {
		const tokenize = ( stream, state ) => {
			let ret;
			if ( state.extMode === false ) {
				ret = mwModeConfig.tags.extTag;
				stream.skipToEnd();
			} else {
				ret = `mw-tag-${ state.extName } ` +
					state.extMode.token( stream, state.extState, origString === false );
			}
			if ( stream.eol() ) {
				if ( origString !== false ) {
					stream.string = origString;
				}
				state.tokenize = state.stack.pop();
			}
			return this.makeLocalStyle( ret, state );
		};
		Object.defineProperty( tokenize, 'name', { value: 'eatExtTokens' } );
		return tokenize;
	}

	eatStartTable( stream, state ) {
		stream.match( '{|' );
		stream.eatSpace();
		state.tokenize = this.inTableDefinition();
		return mwModeConfig.tags.tableBracket;
	}

	inTableDefinition( quote ) {
		const style = mwModeConfig.tags[ quote === undefined ? 'tableDefinition' : 'tableDefinitionValue' ];
		return ( stream, state ) => {
			if ( stream.sol() ) {
				state.tokenize = this.inTable.bind( this );
				return this.inTable( stream, state );
			} else if ( stream.match( /^[&{<]/, false ) ) {
				return this.eatWikiText( style )( stream, state );
			} else if ( quote ) {
				if ( stream.eat( quote[ 0 ] ) ) {
					state.tokenize = this.inTableDefinition( quote[ 1 ] );
				} else {
					stream.match( new RegExp( `^[^&{<${ quote[ 0 ] }]+` ) );
				}
				return this.makeLocalStyle( style, state );
			} else if ( quote === '' ) {
				if ( /\s/.test( stream.peek() ) ) {
					state.tokenize = this.inTableDefinition();
					return '';
				}
				stream.match( /^[^\s&{<]+/ );
				return this.makeLocalStyle( style, state );
			} else if ( stream.match( /^=\s*/ ) ) {
				const next = stream.peek();
				state.tokenize = this.inTableDefinition( next === '"' || next === "'" ? next.repeat( 2 ) : '' );
				return this.makeLocalStyle( style, state );
			}
			stream.match( /^[^&{<=]+/ );
			return this.makeLocalStyle( style, state );
		};
	}

	inTable( stream, state ) {
		if ( stream.sol() ) {
			stream.eatSpace();
			if ( stream.eat( '|' ) ) {
				if ( stream.eat( '-' ) ) {
					stream.eatSpace();
					state.tokenize = this.inTableDefinition();
					return this.makeLocalStyle( mwModeConfig.tags.tableDelimiter, state );
				}
				if ( stream.eat( '+' ) ) {
					stream.eatSpace();
					state.tokenize = this.eatTableRow( true, false, true );
					return this.makeLocalStyle( mwModeConfig.tags.tableDelimiter, state );
				}
				if ( stream.eat( '}' ) ) {
					state.tokenize = state.stack.pop();
					return this.makeLocalStyle( mwModeConfig.tags.tableBracket, state );
				}
				stream.eatSpace();
				state.tokenize = this.eatTableRow( true, false );
				return this.makeLocalStyle( mwModeConfig.tags.tableDelimiter, state );
			}
			if ( stream.eat( '!' ) ) {
				stream.eatSpace();
				state.tokenize = this.eatTableRow( true, true );
				return this.makeLocalStyle( mwModeConfig.tags.tableDelimiter, state );
			}
		}
		return this.eatWikiText( '' )( stream, state );
	}

	// isStart actually means whether there may be attributes */
	eatTableRow( isStart, isHead, isCaption ) {
		let tag = '';
		if ( isCaption ) {
			tag = mwModeConfig.tags.tableCaption;
		} else if ( isHead ) {
			tag = mwModeConfig.tags.strong;
		}
		return ( stream, state ) => {
			if ( stream.sol() ) {
				if ( stream.match( /^[\s\u00a0]*[|!]/, false ) ) {
					state.tokenize = this.inTable.bind( this );
					return this.inTable( stream, state );
				}
			} else {
				if ( stream.match( /^[^'|{[<&~!]+/ ) ) {
					return this.makeStyle( tag, state );
				}
				if ( stream.match( '||' ) || ( isHead && stream.match( '!!' ) ) ) {
					state.bold = false;
					state.italic = false;
					state.tokenize = this.eatTableRow( true, isHead, isCaption );
					return this.makeLocalStyle( mwModeConfig.tags.tableDelimiter, state );
				}
				if ( isStart && stream.eat( '|' ) ) {
					state.bold = false;
					state.italic = false;
					state.tokenize = this.eatTableRow( false, isHead, isCaption );
					return this.makeLocalStyle( mwModeConfig.tags.tableDelimiter, state );
				}
			}
			return this.eatWikiText( tag )( stream, state );
		};
	}

	eatFreeExternalLinkProtocol( stream, state ) {
		stream.match( this.urlProtocols );
		state.tokenize = this.eatFreeExternalLink.bind( this );
		return this.makeLocalStyle( mwModeConfig.tags.freeExtLinkProtocol, state );
	}

	eatFreeExternalLink( stream, state ) {
		if ( stream.sol() ) {
			// @todo error message
		} else if ( stream.match( /^[^\s\u00a0{[\]<>~).,']*/ ) ) {
			if ( stream.peek() === '~' ) {
				if ( !stream.match( /^~~~+/, false ) ) {
					stream.match( /^~*/ );
					return this.makeLocalStyle( mwModeConfig.tags.freeExtLink, state );
				}
			} else if ( stream.peek() === '{' ) {
				if ( !stream.match( '{{', false ) ) {
					stream.next();
					return this.makeLocalStyle( mwModeConfig.tags.freeExtLink, state );
				}
			} else if ( stream.peek() === '\'' ) {
				if ( !stream.match( '\'\'', false ) ) {
					stream.next();
					return this.makeLocalStyle( mwModeConfig.tags.freeExtLink, state );
				}
			} else if ( stream.match( /^[).,]+(?=[^\s\u00a0{[\]<>~).,])/ ) ) {
				return this.makeLocalStyle( mwModeConfig.tags.freeExtLink, state );
			}
		}
		state.tokenize = state.stack.pop();
		return this.makeLocalStyle( mwModeConfig.tags.freeExtLink, state );
	}

	eatList( stream, state ) {
		// Just consume all nested list and indention syntax when there is more
		const mt = stream.match( /^[*#;:]*/ );
		if ( mt && !this.isNested( state ) && mt[ 0 ].includes( ';' ) ) {
			state.nDt += mt[ 0 ].split( ';' ).length - 1;
		}
		return this.makeLocalStyle( mwModeConfig.tags.list, state );
	}

	/**
	 * @param {string} style
	 * @return {string|Function}
	 * @private
	 */
	eatWikiText( style ) {
		return ( stream, state ) => {
			if ( stream.eol() ) {
				return '';
			}
			let ch, tmp, mt, name, isCloseTag, tagname;
			const sol = stream.sol();

			function chain( parser ) {
				state.stack.push( state.tokenize );
				state.tokenize = parser;
				return parser( stream, state );
			}

			if ( sol ) {
				if ( state.sof ) {
					if ( stream.match( /^\s+$/ ) ) {
						return '';
					}
					state.sof = false;
					const mtRedirect = stream.match( this.redirectRegex );
					if ( mtRedirect ) {
						return mwModeConfig.tags.redirect;
					}
				}
				// highlight free external links, see T108448
				if ( !stream.match( '//', false ) && stream.match( this.urlProtocols ) ) {
					state.stack.push( state.tokenize );
					state.tokenize = this.eatFreeExternalLink.bind( this );
					return this.makeLocalStyle( mwModeConfig.tags.freeExtLinkProtocol, state );
				}
				ch = stream.next();
				switch ( ch ) {
					case '-':
						if ( stream.match( /^---+/ ) ) {
							return mwModeConfig.tags.hr;
						}
						break;
					case '=':

						tmp = stream.match( /^(={0,5})(.+?(=\1\s*)(<!--(?!.*-->.*\S).*?)?)$/ );
						// Title
						if ( tmp ) {
							stream.backUp( tmp[ 2 ].length );
							state.stack.push( state.tokenize );
							state.tokenize = this.eatSectionHeader( tmp[ 3 ].length );
							return mwModeConfig.tags.sectionHeader + ' ' +
								/**
								 * Tokens used here include:
								 * - cm-mw-section-1
								 * - cm-mw-section-2
								 * - cm-mw-section-3
								 * - cm-mw-section-4
								 * - cm-mw-section-5
								 * - cm-mw-section-6
								 */
								mwModeConfig.tags[ `sectionHeader${ tmp[ 1 ].length + 1 }` ];
						}
						break;
					case ';':
						stream.backUp( 1 );
					// fall through
					case '*':
					case '#':
						return this.eatList( stream, state );
					case ':':
						// Highlight indented tables :{|, bug T108454
						if ( stream.match( /^:*[\s\u00a0]*(?={\|)/ ) ) {
							state.stack.push( state.tokenize );
							state.tokenize = this.eatStartTable.bind( this );
							return mwModeConfig.tags.indenting;
						}
						return this.eatList( stream, state );
					case ' ':
						// Leading spaces is valid syntax for tables, bug T108454
						if ( stream.match( /^[\s\u00a0]*(?::+[\s\u00a0]*)?{\|/, false ) ) {
							stream.eatSpace();
							if ( stream.match( /^:+/ ) ) { // ::{|
								stream.eatSpace();
								state.stack.push( state.tokenize );
								state.tokenize = this.eatStartTable.bind( this );
								return mwModeConfig.tags.indenting;
							}
							stream.eat( '{' );
						} else {
							return mwModeConfig.tags.skipFormatting;
						}
					// break is not necessary here
					// falls through
					case '{':
						if ( stream.eat( '|' ) ) {
							stream.eatSpace();
							state.stack.push( state.tokenize );
							state.tokenize = this.inTableDefinition();
							return mwModeConfig.tags.tableBracket;
						}
				}
			} else {
				ch = stream.next();
			}

			switch ( ch ) {
				case '&':
					return this.makeStyle(
						this.eatHtmlEntity( stream, style ),
						state
					);
				case '\'':
					// skip the irrelevant apostrophes ( >5 or =4 )
					if ( stream.match( /^'*(?=''''')/ ) || stream.match( /^'''(?!')/, false ) ) {
						break;
					}
					if ( stream.match( '\'\'' ) ) { // bold
						if ( !( state.data.firstSingleLetterWord || stream.match( '\'\'', false ) ) ) {
							this.prepareItalicForCorrection( stream, state );
						}
						state.bold = !state.bold;
						return this.makeLocalStyle( mwModeConfig.tags.apostrophes, state );
					} else if ( stream.eat( '\'' ) ) { // italic
						state.italic = !state.italic;
						return this.makeLocalStyle( mwModeConfig.tags.apostrophes, state );
					}
					break;
				case '[':
					if ( stream.eat( '[' ) ) { // Link Example: [[ Foo | Bar ]]
						stream.eatSpace();
						if ( /[^\]|[]/.test( stream.peek() ) ) {
							state.nLink++;
							state.stack.push( state.tokenize );
							state.tokenize = this.inLink.bind( this );
							return this.makeLocalStyle( mwModeConfig.tags.linkBracket, state );
						}
					} else {
						mt = stream.match( this.urlProtocols );
						if ( mt ) {
							state.nExtLink++;
							stream.backUp( mt[ 0 ].length );
							state.stack.push( state.tokenize );
							state.tokenize = this.eatExternalLinkProtocol( mt[ 0 ].length );
							return this.makeLocalStyle( mwModeConfig.tags.extLinkBracket, state );
						}
					}
					break;
				case '{':
					// Can't be a variable when it starts with more than 3 brackets (T108450) or
					// a single { followed by a template. E.g. {{{!}} starts a table (T292967).
					if ( stream.match( /^{{(?!{|[^{}]*}}(?!}))/ ) ) {
						stream.eatSpace();
						state.stack.push( state.tokenize );
						state.tokenize = this.inVariable.bind( this );
						return this.makeLocalStyle(
							mwModeConfig.tags.templateVariableBracket,
							state
						);
					} else if ( stream.match( /^{(?!{(?!{))[\s\u00a0]*/ ) ) {
						// Parser function
						if ( stream.peek() === '#' ) {
							state.nExt++;
							state.stack.push( state.tokenize );
							state.tokenize = this.inParserFunctionName.bind( this );
							return this.makeLocalStyle(
								mwModeConfig.tags.parserFunctionBracket,
								state
							);
						}
						// Check for parser function without '#'
						name = stream.match( /^([^}[\]<{|:]+)(.)?/, false );
						if ( name ) {
							const [ , f, delimiter ] = name,
								ff = delimiter === ':' ? f : f.trim(),
								ffLower = ff.toLowerCase(),
								{ config: { functionSynonyms, variableIDs } } = this,
								insensitiveName = Object.prototype.hasOwnProperty.call(
									functionSynonyms[ 0 ], ffLower
								) && functionSynonyms[ 0 ][ ffLower ],
								sensitiveName = Object.prototype.hasOwnProperty.call(
									functionSynonyms[ 1 ], ff
								) && functionSynonyms[ 1 ][ ff ],
								canonicalName = insensitiveName || sensitiveName;
							if (
								( !delimiter || delimiter === ':' || delimiter === '}' ) &&
								canonicalName &&
								( delimiter === ':' || variableIDs.includes( canonicalName ) )
							) {
								state.nExt++;
								state.stack.push( state.tokenize );
								state.tokenize = this.inParserFunctionName.bind( this );
								return this.makeLocalStyle(
									mwModeConfig.tags.parserFunctionBracket,
									state
								);
							}
						}
						// Template
						state.nTemplate++;
						state.stack.push( state.tokenize );
						state.tokenize = this.eatTemplatePageName( false );
						return this.makeLocalStyle( mwModeConfig.tags.templateBracket, state );
					}
					break;
				case '<':
					if ( stream.match( '!--' ) ) { // comment
						return chain( this.eatBlock( mwModeConfig.tags.comment, '-->' ) );
					}
					isCloseTag = !!stream.eat( '/' );
					tagname = stream.match( /^[a-z][^>/\s\u00a0]*/i );
					if ( tagname ) {
						tagname = tagname[ 0 ].toLowerCase();
						if ( tagname in this.config.tags ) {
							// Parser function
							if ( isCloseTag === true ) {
								return mwModeConfig.tags.error;
							}
							stream.backUp( tagname.length );
							state.stack.push( state.tokenize );
							state.tokenize = this.eatTagName( tagname.length, isCloseTag, false );
							return this.makeLocalStyle( `${ mwModeConfig.tags.extTagBracket } mw-ext-${ tagname }`, state );
						}
						if ( tagname in mwModeConfig.permittedHtmlTags ) {
							// Html tag
							if ( isCloseTag === true && tagname !== state.inHtmlTag.pop() ) {
								// Increment position so that the closing '>' gets highlighted red.
								stream.pos++;
								return mwModeConfig.tags.error;
							}
							if (
								isCloseTag === true &&
								tagname in mwModeConfig.implicitlyClosedHtmlTags
							) {
								return mwModeConfig.tags.error;
							}
							stream.backUp( tagname.length );
							state.stack.push( state.tokenize );
							state.tokenize = this.eatTagName(
								tagname.length,
								// Opening void tags should also be treated as the closing tag.
								isCloseTag ||
								( tagname in mwModeConfig.implicitlyClosedHtmlTags ),
								true
							);
							return this.makeLocalStyle( mwModeConfig.tags.htmlTagBracket, state );
						}
						stream.backUp( tagname.length );
					}
					break;
				case '~':
					if ( stream.match( /^~{2,4}/ ) ) {
						return mwModeConfig.tags.signature;
					}
					break;
				// Maybe double underscored Magic Word such as __TOC__
				case '_':
					tmp = 1;
					// Optimize processing of many underscore symbols
					while ( stream.eat( '_' ) ) {
						tmp++;
					}
					// Many underscore symbols
					if ( tmp > 2 ) {
						if ( !stream.eol() ) {
							// Leave last two underscore symbols for processing in next iteration
							stream.backUp( 2 );
						}
						// Optimization: skip regex function for EOL and backup-ed symbols
						return this.makeStyle( style, state );
						// Check on double underscore Magic Word
					} else if ( tmp === 2 ) {
						// The same as the end of function except '_' inside and '__' at the end.
						name = stream.match( /^([^\s\u00a0>}[\]<{'|&:~]+?)__/ );
						if ( name && name[ 0 ] ) {
							if (
								'__' + name[ 0 ].toLowerCase() in this.config.doubleUnderscore[ 0 ] ||
								'__' + name[ 0 ] in this.config.doubleUnderscore[ 1 ]
							) {
								return mwModeConfig.tags.doubleUnderscore;
							}
							if ( !stream.eol() ) {
								// Two underscore symbols at the end can be the
								// beginning of another double underscored Magic Word
								stream.backUp( 2 );
							}
							// Optimization: skip regex for EOL and backup-ed symbols
							return this.makeStyle( style, state );
						}
					}
					break;
				case ':':
					if ( state.nDt > 0 && !this.isNested( state ) ) {
						state.nDt--;
						return mwModeConfig.tags.indenting;
					}
					break;
				default:
					if ( /[\s\u00a0]/.test( ch ) ) {
						stream.eatSpace();
						// highlight free external links, bug T108448
						if ( stream.match( this.urlProtocols, false ) && !stream.match( '//' ) ) {
							state.stack.push( state.tokenize );
							state.tokenize = this.eatFreeExternalLinkProtocol.bind( this );
							return this.makeStyle( style, state );
						}
					}
					break;
			}
			stream.match( /^[^\s\u00a0_>}[\]<{'|&:~=]+/ );
			return this.makeStyle( style, state );
		};
	}

	/**
	 * Remembers position and status for rollbacking.
	 * It is needed for changing from bold to italic with apostrophes before it, if required.
	 *
	 * @see https://phabricator.wikimedia.org/T108455
	 *
	 * @param {StringStream} stream
	 * @param {Object} state
	 * @private
	 */
	prepareItalicForCorrection( stream, state ) {
		// See Parser::doQuotes() in MediaWiki Core, it works similarly.
		// firstSingleLetterWord has maximum priority
		// firstMultiLetterWord has medium priority
		// firstSpace has low priority
		const end = stream.pos,
			str = stream.string.slice( 0, end - 3 ),
			x1 = str.slice( -1 ),
			x2 = str.slice( -2, -1 );

		// firstSingleLetterWord always is undefined here
		if ( x1 === ' ' ) {
			if ( state.data.firstMultiLetterWord || state.data.firstSpace ) {
				return;
			}
			state.data.firstSpace = end;
		} else if ( x2 === ' ' ) {
			state.data.firstSingleLetterWord = end;
		} else if ( state.data.firstMultiLetterWord ) {
			return;
		} else {
			state.data.firstMultiLetterWord = end;
		}
		state.data.mark = end;
	}

	/**
	 * @see https://codemirror.net/docs/ref/#language.StreamParser
	 * @type {StreamParser}
	 * @private
	 */
	get mediawiki() {
		return {
			name: 'mediawiki',

			/**
			 * Initial State for the parser.
			 *
			 * @return {Object}
			 * @private
			 */
			startState: () => ( {
				tokenize: this.eatWikiText( '' ),
				stack: [],
				inHtmlTag: [],
				extName: false,
				extMode: false,
				extState: false,
				nTemplate: 0,
				nLink: 0,
				nExtLink: 0,
				nExt: 0,
				nDt: 0,
				bold: false,
				italic: false,
				sof: true,
				data: {
					firstSingleLetterWord: null,
					firstMultiLetterWord: null,
					firstSpace: null,
					readyTokens: [],
					oldToken: null,
					mark: null
				}
			} ),

			/**
			 * Copies the given state.
			 *
			 * @param {Object} state
			 * @return {Object}
			 * @private
			 */
			copyState,

			/**
			 * Reads one token, advancing the stream past it,
			 * and returning a string indicating the token's style tag.
			 *
			 * @param {StringStream} stream
			 * @param {Object} state
			 * @return {string|null}
			 * @private
			 */
			token: ( stream, state ) => {
				const { data } = state,
					{ readyTokens } = data;
				let { oldToken } = data;
				while ( oldToken && (
					// If the start of PartialParse is after the current position
					stream.pos > oldToken.pos ||
					stream.pos === oldToken.pos && state.tokenize !== oldToken.state.tokenize
				) ) {
					oldToken = readyTokens.shift();
				}
				if ( // check the start
					oldToken &&
					stream.pos === oldToken.pos &&
					stream.string === oldToken.string
				) {
					const { pos, string, state: other, style } = readyTokens[ 0 ];
					delete other.bold;
					delete other.italic;
					Object.assign( state, other );
					if (
						!( state.extName && state.extMode ) &&
						state.nLink === 0 &&
						typeof style === 'string' &&
						style.includes( mwModeConfig.tags.apostrophes )
					) {
						if ( data.mark === pos ) {
							// rollback
							data.mark = null;
							// add one apostrophe, next token will be italic (two apostrophes)
							stream.string = string.slice( 0, pos - 2 );
							const s = state.tokenize( stream, state );
							stream.string = string;
							oldToken.pos++;
							data.oldToken = oldToken;
							return this.makeFullStyle( s, state );
						}
						const length = pos - stream.pos;
						if ( length !== 3 ) {
							state.italic = !state.italic;
						}
						if ( length !== 2 ) {
							state.bold = !state.bold;
						}
					} else if ( typeof style === 'string' && style.includes( mwModeConfig.tags.tableDelimiter ) ) {
						state.bold = false;
						state.italic = false;
					}
					// return first saved token
					data.oldToken = readyTokens.shift();
					stream.pos = pos;
					stream.string = string;
					return this.makeFullStyle( style, state );
				} else if ( stream.sol() ) {
					// reset bold and italic status in every new line
					state.bold = false;
					state.italic = false;
					state.nDt = 0;
					data.firstSingleLetterWord = null;
					data.firstMultiLetterWord = null;
					data.firstSpace = null;
					if ( state.tokenize.name === 'eatExtTokens' ) {
						state.stack.pop(); // dispose eatExtTokens
						state.tokenize = state.stack.pop(); // dispose eatExtTagArea
						state.extName = false;
						state.extMode = false;
						state.extState = false;
					}
				}
				readyTokens.length = 0;
				data.mark = null;
				data.oldToken = { pos: stream.pos, string: stream.string, state: copyState( state ), style: '' };
				const { start } = stream;
				do {
					// get token style
					stream.start = stream.pos;
					const st = state.tokenize( stream, state );
					// save token
					readyTokens.push( {
						pos: stream.pos,
						string: stream.string,
						state: copyState( state ),
						style: st
					} );
				} while ( !stream.eol() );
				if ( !state.bold || !state.italic ) {
					// no need to rollback
					data.mark = null;
				}
				stream.start = start;
				stream.pos = data.oldToken.pos;
				stream.string = data.oldToken.string;
				Object.assign( state, data.oldToken.state );
				return '';
			},

			/**
			 * @param {Object} state
			 * @private
			 */
			blankLine: ( state ) => {
				if ( state.extMode && state.extMode.blankLine ) {
					state.extMode.blankLine( state.extState );
				}
			},

			/**
			 * Extra tokens to use in this parser.
			 *
			 * @see CodeMirrorModeMediaWikiConfig.defaultTokenTable
			 * @return {Object<Tag>}
			 * @private
			 */
			tokenTable: this.tokenTable,

			/**
			 * @see https://codemirror.net/docs/ref/#language.StreamParser.languageData
			 * @return {Object}
			 * @private
			 */
			languageData: {
				autocomplete: completionSource( this )
			}
		};
	}
}

let handler;

/**
 * Gets a LanguageSupport instance for the MediaWiki mode.
 *
 * @member CodeMirrorModeMediaWiki
 * @method
 * @param {Object} [config] Configuration options for the MediaWiki mode.
 * @param {boolean} [config.bidiIsolation=false] Enable bidi isolation around HTML tags.
 *   This should generally always be enabled on RTL pages, but it comes with a performance cost.
 * @param {boolean} [config.codeFolding=true] Enable code folding.
 * @param {boolean} [config.autocomplete=true] Enable autocompletion.
 * @param {boolean} [config.openLinks=true] Enable opening of links.
 * @return {LanguageSupport}
 * @stable to call
 */
const mediaWikiLang = ( config = { bidiIsolation: false } ) => {
	const mode = new CodeMirrorModeMediaWiki( mw.config.get( 'extCodeMirrorConfig' ) );
	const parser = mode.mediawiki;
	const lang = StreamLanguage.define( parser );
	const langExtension = [ syntaxHighlighting(
		HighlightStyle.define(
			mwModeConfig.getTagStyles( parser )
		)
	) ];

	// Register MW-specific Extensions into CodeMirror preferences. Whether they are enabled
	// or not is determined by the user's preferences and wiki configuration.
	if ( handler ) {
		mw.hook( 'ext.CodeMirror.ready' ).remove( handler );
	}

	handler = ( cm ) => {
		// T380840
		if ( !cm.view ) {
			return;
		}

		// Register MW-specific keymaps.
		mwKeymap( cm );

		if ( config.codeFolding !== false ) {
			cm.preferences.registerExtension( 'codeFolding', codeFoldingExtension, cm.view );
		}
		if ( config.autocomplete !== false ) {
			cm.preferences.registerExtension( 'autocomplete', autocompleteExtension, cm.view );
		}
		if ( config.openLinks !== false ) {
			cm.preferences.registerExtension( 'openLinks', openLinksExtension, cm.view );
		}
		if ( config.bidiIsolation ) {
			cm.preferences.registerExtension( 'bidiIsolation', bidiIsolationExtension, cm.view );
		}
	};
	mw.hook( 'ext.CodeMirror.ready' ).add( handler );

	return new LanguageSupport( lang, langExtension );
};

module.exports = mediaWikiLang;