/*!
* Experimental advanced wikitext parser-emitter.
* See: https://www.mediawiki.org/wiki/Extension:UploadWizard/MessageParser for docs
*
* @author neilk@wikimedia.org
* @author mflaschen@wikimedia.org
*/
/**
 * @typedef {string|string[]} module:mediawiki.jqueryMsg~Replacements
 * @ignore
 */
/**
 * @callback {Function} module:mediawiki.jqueryMsg~MessageFormatterFunction
 * @param {Array<module:mediawiki.jqueryMsg~Replacements>} replacements Optional variable replacements (variadically or an array).
 *   This is a mixed array of strings or arrays of string. This is equivalent to Array<string|string[]> but cannot be documented until the
 *   jsdoc theme has been patched (T354716).
 * @return {jQuery} Rendered HTML.
 * @ignore
 */

/**
 * @callback {Function} MessageFormatterFunctionGenerator
 * @return {module:mediawiki.jqueryMsg~MessageFormatterFunction}
 * @ignore
 */

const slice = Array.prototype.slice,
	util = require( 'mediawiki.util' ),
	mwString = require( 'mediawiki.String' ),
	parserDefaults = {
		// Magic words and their expansions. Server-side data is added to this below.
		magic: {
			PAGENAME: mw.config.get( 'wgPageName' ),
			PAGENAMEE: util.wikiUrlencode( mw.config.get( 'wgPageName' ) ),
			SERVERNAME: mw.config.get( 'wgServerName' ),
			CONTENTLANGUAGE: mw.config.get( 'wgContentLanguage' )
		},
		// Whitelist for allowed HTML elements in wikitext.
		// Filled in with server-side data below
		allowedHtmlElements: [],
		// Whitelist for allowed self-closing elements.
		// We're not using server data here because we don't want to allow <meta> or <link>.
		allowedSelfClosingHtmlElements: [
			'br',
			'wbr',
			'hr'
		],
		// Key tag name, value allowed attributes for that tag.
		// See Sanitizer::setupAttributesAllowedInternal
		allowedHtmlCommonAttributes: [
			// HTML
			'id',
			'class',
			// 'style' attribute is not allowed because it is difficult to sanitize (T251032)
			'lang',
			'dir',
			'title',

			// WAI-ARIA
			'role'
		],

		// Attributes allowed for specific elements.
		// Key is element name in lower case
		// Value is array of allowed attributes for that element
		allowedHtmlAttributesByElement: {},
		messages: mw.messages,
		language: mw.language,

		// Same meaning as in mediawiki.js.
		//
		// Only 'text', 'parse', and 'escaped' are supported, and the
		// actual escaping for 'escaped' is done by other code (generally
		// through mediawiki.js).
		//
		// However, note that this default only
		// applies to direct calls to jqueryMsg. The default for mediawiki.js itself
		// is 'text', including when it uses jqueryMsg.
		format: 'parse'
	};

// Add in server-side data (allowedHtmlElements and magic words)
$.extend( true, parserDefaults, require( './parserDefaults.json' ) );

/**
 * Wrapper around jQuery append that converts all non-objects to TextNode so append will not
 * convert what it detects as an htmlString to an element.
 *
 * If our own HtmlEmitter jQuery object is given, its children will be unwrapped and appended to
 * new parent.
 *
 * Object elements of children (jQuery, HTMLElement, TextNode, etc.) will be left as is.
 *
 * @private
 * @param {jQuery} $parent Parent node wrapped by jQuery
 * @param {Object|string|Array} children What to append, with the same possible types as jQuery
 * @return {jQuery} $parent
 */
function appendWithoutParsing( $parent, children ) {
	let i, len;

	if ( !Array.isArray( children ) ) {
		children = [ children ];
	}

	for ( i = 0, len = children.length; i < len; i++ ) {
		if ( typeof children[ i ] !== 'object' ) {
			children[ i ] = document.createTextNode( children[ i ] );
		}
		if ( children[ i ] instanceof $ && children[ i ].hasClass( 'mediaWiki_htmlEmitter' ) ) {
			children[ i ] = children[ i ].contents();
		}
	}

	return $parent.append( children );
}

/**
 * Decodes the main HTML entities, those encoded by mw.html.escape.
 *
 * @private
 * @param {string} encoded Encoded string
 * @return {string} String with those entities decoded
 */
function decodePrimaryHtmlEntities( encoded ) {
	return encoded
		.replace( /&#039;/g, '\'' )
		.replace( /&quot;/g, '"' )
		.replace( /&lt;/g, '<' )
		.replace( /&gt;/g, '>' )
		.replace( /&amp;/g, '&' );
}

/**
 * Turn input into a string.
 *
 * @private
 * @param {string|jQuery} input
 * @return {string} Textual value of input
 */
function textify( input ) {
	if ( input instanceof $ ) {
		input = input.text();
	}
	return String( input );
}

/**
 * Given parser options, return a function that parses a key and replacements, returning jQuery object
 *
 * Try to parse a key and optional replacements, returning a jQuery object that may be a tree of jQuery nodes.
 * If there was an error parsing, return the key and the error message (wrapped in jQuery). This should put the error right into
 * the interface, without causing the page to halt script execution, and it hopefully should be clearer how to fix it.
 *
 * @private
 * @param {Object} options Parser options
 * @return {Function}
 * @return {Array} return.args First element is the key, replacements may be in array in 2nd element, or remaining elements.
 * @return {jQuery} return.return
 */
function getFailableParserFn( options ) {
	return function ( args ) {
		const parser = new Parser( options ),
			key = args[ 0 ],
			argsArray = Array.isArray( args[ 1 ] ) ? args[ 1 ] : slice.call( args, 1 );
		try {
			return parser.parse( key, argsArray );
		} catch ( e ) {
			const fallback = parser.settings.messages.get( key );
			mw.log.warn( 'mediawiki.jqueryMsg: ' + key + ': ' + e.message );
			mw.track( 'mediawiki.jqueryMsg.error', {
				messageKey: key,
				errorMessage: e.message
			} );
			return $( '<span>' ).text( fallback );
		}
	};
}

/**
 * Initialize parser defaults.
 *
 * This is currently used by the QUnit testrunner to change the reference in
 * parserDefaults.messages to the test messages and back.
 *
 * @private
 * @param {Object} data New data to extend parser defaults with
 */
const setParserDefaults = function ( data ) {
	Object.assign( parserDefaults, data );
};

/**
 * Get current parser defaults.
 *
 * Primarily used for the unit test. Returns a copy.
 *
 * @private
 * @return {Object}
 */
const getParserDefaults = function () {
	return Object.assign( {}, parserDefaults );
};

/**
 * Returns a function suitable for static use, to construct strings from a message key (and optional replacements).
 *
 * @ignore
 * @param {Object} options parser options
 * @return {module:mediawiki.jqueryMsg~MessageFormatterFunction}
 */
const defaultMessageFunction = function ( options ) {
	let failableParserFn, format;

	if ( options && options.format !== undefined ) {
		format = options.format;
	} else {
		format = parserDefaults.format;
	}

	return function () {
		if ( !failableParserFn ) {
			failableParserFn = getFailableParserFn( options );
		}
		const $result = failableParserFn( arguments );
		if ( format === 'text' || format === 'escaped' ) {
			return $result.text();
		} else {
			return $result.html();
		}
	};
};
/**
 * @type {MessageFormatterFunctionGenerator}
 * @ignore
 */
let messageFunction = defaultMessageFunction;

/**
 * @ignore
 * @param {Object} options parser options
 * @return {module:mediawiki.jqueryMsg~MessageFormatterFunction} options
 */
const getMessageFunction = function ( options ) {
	return messageFunction( options );
};

/**
 * Allows tests to override the message function.
 *
 * @ignore
 * @param {MessageFormatterFunctionGenerator} msgFunction
 * @return {Function} that allows you to restore the original message function.
 */
const setMessageFunction = function ( msgFunction ) {
	messageFunction = msgFunction;
	return function () {
		messageFunction = defaultMessageFunction;
	};
};

/**
 * Returns a jQuery plugin.
 *
 * @ignore
 * @param {Object} [options] Parser options
 * @return {module:mediawiki.jqueryMsg~MessageFormatterFunction}
 */
const getPlugin = function ( options ) {
	let failableParserFn;

	return function () {
		if ( !failableParserFn ) {
			failableParserFn = getFailableParserFn( options );
		}
		const $result = failableParserFn( arguments );
		return this.empty().append( $result.contents() );
	};
};

/**
 * The parser itself.
 * Describes an object, whose primary duty is to .parse() message keys.
 *
 * @class
 * @private
 * @param {Object} options
 */
function Parser( options ) {
	this.settings = Object.assign( {}, parserDefaults, options );
	this.settings.onlyCurlyBraceTransform = ( this.settings.format === 'text' || this.settings.format === 'escaped' );
	this.astCache = {};

	this.emitter = new HtmlEmitter( this.settings.language, this.settings.magic );
}

Parser.prototype = {
	/**
	 * Where the magic happens.
	 * Parses a message from the key, and swaps in replacements as necessary, wraps in jQuery
	 * If an error is thrown, returns original key, and logs the error
	 *
	 * @param {string} key Message key.
	 * @param {Array} replacements Variable replacements for $1, $2... $n
	 * @return {jQuery}
	 */
	parse: function ( key, replacements ) {
		const ast = this.getAst( key, replacements );
		return this.emitter.emit( ast, replacements );
	},

	/**
	 * Fetch the message string associated with a key, return parsed structure. Memoized.
	 * Note that we pass '⧼' + key + '⧽' back for a missing message here.
	 *
	 * @param {string} key
	 * @param {Array} replacements Variable replacements for $1, $2... $n
	 * @return {string|Array} string of '⧼key⧽' if message missing, simple string if possible, array of arrays if needs parsing
	 */
	getAst: function ( key, replacements ) {
		if ( !Object.prototype.hasOwnProperty.call( this.astCache, key ) ) {
			let wikiText = this.settings.messages.get( key );
			// Keep this synchronised with Message#parser in mediawiki.base.js
			if (
				mw.config.get( 'wgUserLanguage' ) === 'qqx' &&
				( !wikiText || wikiText === '(' + key + ')' )
			) {
				wikiText = '(' + key + '$*)';
			} else if ( typeof wikiText !== 'string' ) {
				wikiText = '⧼' + key + '⧽';
			}
			wikiText = mw.internalDoTransformFormatForQqx( wikiText, replacements );
			this.astCache[ key ] = this.wikiTextToAst( wikiText );
		}
		return this.astCache[ key ];
	},

	/**
	 * Parses the input wikiText into an abstract syntax tree, essentially an s-expression.
	 *
	 * CAVEAT: This does not parse all wikitext. It could be more efficient, but it's pretty good already.
	 * n.b. We want to move this functionality to the server. Nothing here is required to be on the client.
	 *
	 * @param {string} input Message string wikitext
	 * @throws Error
	 * @return {any} abstract syntax tree
	 */
	wikiTextToAst: function ( input ) {
		let nonWhitespaceExpression = null, expression = null, templateContents = null, paramExpression = null, colon = null;
		const settings = this.settings;

		// Indicates current position in input as we parse through it.
		// Shared among all parsing functions below.
		let pos = 0;

		// =========================================================
		// parsing combinators - could be a library on its own
		// =========================================================

		/**
		 * Try parsers until one works, if none work return null
		 *
		 * @private
		 * @param {Function[]} ps
		 * @return {Function} that will return {string|null}
		 */
		function choice( ps ) {
			return function () {
				let i, result;
				for ( i = 0; i < ps.length; i++ ) {
					result = ps[ i ]();
					if ( result !== null ) {
						return result;
					}
				}
				return null;
			};
		}

		/**
		 * Try several ps in a row, all must succeed or return null.
		 * This is the only eager one.
		 *
		 * @private
		 * @param {Function[]} ps Each function should return a string or null
		 * @return {string[]|null}
		 */
		function sequence( ps ) {
			const originalPos = pos,
				result = [];
			for ( let i = 0; i < ps.length; i++ ) {
				const r = ps[ i ]();
				if ( r === null ) {
					pos = originalPos;
					return null;
				}
				result.push( r );
			}
			return result;
		}

		/**
		 * Run the same parser over and over until it fails.
		 * Must succeed a minimum of n times or return null.
		 *
		 * @private
		 * @param {number} n
		 * @param {Function} p Should return a string or null
		 * @return {Function} that will return {string[]|null}
		 */
		function nOrMore( n, p ) {
			return function () {
				const originalPos = pos,
					result = [];
				let parsed = p();
				while ( parsed !== null ) {
					result.push( parsed );
					parsed = p();
				}
				if ( result.length < n ) {
					pos = originalPos;
					return null;
				}
				return result;
			};
		}

		/**
		 * Just make parsers out of simpler JS builtin types
		 *
		 * @private
		 * @param {string} s
		 * @return {Function} that will return {string|null}
		 */
		function makeStringParser( s ) {
			const len = s.length;
			return function () {
				let result = null;
				if ( input.slice( pos, pos + len ) === s ) {
					result = s;
					pos += len;
				}
				return result;
			};
		}

		/**
		 * Makes a regex parser, given a RegExp object.
		 * The regex being passed in should start with a ^ to anchor it to the start
		 * of the string.
		 *
		 * @private
		 * @param {RegExp} regex anchored regex
		 * @return {Function} function to parse input based on the regex
		 */
		function makeRegexParser( regex ) {
			return function () {
				const matches = input.slice( pos ).match( regex );
				if ( matches === null ) {
					return null;
				}
				pos += matches[ 0 ].length;
				return matches[ 0 ];
			};
		}

		// ===================================================================
		// General patterns above this line -- wikitext specific parsers below
		// ===================================================================

		// Parsing functions follow. All parsing functions work like this:
		// They don't accept any arguments.
		// Instead, they just operate non destructively on the string 'input'
		// As they can consume parts of the string, they advance the shared variable pos,
		// and return tokens (or whatever else they want to return).
		// some things are defined as closures and other things as ordinary functions
		// converting everything to a closure makes it a lot harder to debug... errors pop up
		// but some debuggers can't tell you exactly where they come from. Also the mutually
		// recursive functions seem not to work in all browsers then. (Tested IE6-7, Opera, Safari, FF)
		// This may be because, to save code, memoization was removed

		/* eslint-disable no-useless-escape */
		const regularLiteral = makeRegexParser( /^[^{}\[\]$<\\]/ );
		const regularLiteralWithoutBar = makeRegexParser( /^[^{}\[\]$\\|]/ );
		const regularLiteralWithoutSpace = makeRegexParser( /^[^{}\[\]$\s]/ );
		/* eslint-enable no-useless-escape */

		const backslash = makeStringParser( '\\' );
		const anyCharacter = makeRegexParser( /^./ );
		function escapedLiteral() {
			const result = sequence( [
				backslash,
				anyCharacter
			] );
			return result === null ? null : result[ 1 ];
		}
		const escapedOrLiteralWithoutSpace = choice( [
			escapedLiteral,
			regularLiteralWithoutSpace
		] );
		const escapedOrLiteralWithoutBar = choice( [
			escapedLiteral,
			regularLiteralWithoutBar
		] );
		const escapedOrRegularLiteral = choice( [
			escapedLiteral,
			regularLiteral
		] );
		// Used to define "literals" without spaces, in space-delimited situations
		function literalWithoutSpace() {
			const result = nOrMore( 1, escapedOrLiteralWithoutSpace )();
			return result === null ? null : result.join( '' );
		}
		// Used to define "literals" within template parameters. The pipe character is the parameter delimeter, so by default
		// it is not a literal in the parameter
		function literalWithoutBar() {
			const result = nOrMore( 1, escapedOrLiteralWithoutBar )();
			return result === null ? null : result.join( '' );
		}

		function literal() {
			const result = nOrMore( 1, escapedOrRegularLiteral )();
			return result === null ? null : result.join( '' );
		}

		const asciiAlphabetLiteral = makeRegexParser( /^[A-Za-z]+/ );

		const whitespace = makeRegexParser( /^\s+/ );

		const dollar = makeStringParser( '$' );
		const digits = makeRegexParser( /^\d+/ );
		function replacement() {
			const result = sequence( [
				dollar,
				digits
			] );
			if ( result === null ) {
				return null;
			}
			return [ 'REPLACE', parseInt( result[ 1 ], 10 ) - 1 ];
		}
		const openExtlink = makeStringParser( '[' );
		const closeExtlink = makeStringParser( ']' );
		// this extlink MUST have inner contents, e.g. [foo] not allowed; [foo bar] [foo <i>bar</i>], etc. are allowed
		function extlink() {
			const parsedResult = sequence( [
				openExtlink,
				nOrMore( 1, nonWhitespaceExpression ),
				whitespace,
				nOrMore( 1, expression ),
				closeExtlink
			] );
			if ( parsedResult === null ) {
				return null;
			}
			// When the entire link target is a single parameter, we can't use CONCAT, as we allow
			// passing fancy parameters (like a whole jQuery object or a function) to use for the
			// link. Check only if it's a single match, since we can either do CONCAT or not for
			// singles with the same effect.
			const target = parsedResult[ 1 ].length === 1 ?
				parsedResult[ 1 ][ 0 ] :
				[ 'CONCAT', ...parsedResult[ 1 ] ];
			return [
				'EXTLINK',
				target,
				[ 'CONCAT', ...parsedResult[ 3 ] ]
			];
		}
		const pipe = makeStringParser( '|' );

		const openTemplate = makeStringParser( '{{' );
		const closeTemplate = makeStringParser( '}}' );
		function template() {
			const result = sequence( [
				openTemplate,
				templateContents,
				closeTemplate
			] );
			return result === null ? null : result[ 1 ];
		}

		function templateName() {
			// see $wgLegalTitleChars
			// not allowing : due to the need to catch "PLURAL:$1"
			const templateNameRegex = makeRegexParser( /^#?[ !"$&'()*,./0-9;=?@A-Z^_`a-z~\x80-\xFF+-]+/ );
			const result = templateNameRegex();
			return result === null ? null : result.toString();
		}

		function templateParam() {
			const result = sequence( [
				pipe,
				nOrMore( 0, paramExpression )
			] );
			if ( result === null ) {
				return null;
			}
			const expr = result[ 1 ];
			// use a CONCAT operator if there are multiple nodes, otherwise return the first node, raw.
			return expr.length > 1 ? [ 'CONCAT', ...expr ] : expr[ 0 ];
		}

		function templateNameWithParam() {
			const result = sequence( [
				templateName,
				colon,
				nOrMore( 0, paramExpression )
			] );
			if ( result === null ) {
				return null;
			}
			const expr = result[ 2 ];
			// use a CONCAT operator if there are multiple nodes, otherwise return the first node, raw.
			return [ result[ 0 ], expr.length > 1 ? [ 'CONCAT', ...expr ] : expr[ 0 ] ];
		}
		colon = makeStringParser( ':' );
		templateContents = choice( [
			function () {
				const result = sequence( [
					templateNameWithParam,
					nOrMore( 0, templateParam )
				] );
				return result === null ? null : [ ...result[ 0 ], ...result[ 1 ] ];
			},
			function () {
				const result = sequence( [
					templateName,
					nOrMore( 0, templateParam )
				] );
				if ( result === null ) {
					return null;
				}
				return [ result[ 0 ], ...result[ 1 ] ];
			}
		] );

		function pipedWikilink() {
			const result = sequence( [
				nOrMore( 1, paramExpression ),
				pipe,
				nOrMore( 1, expression )
			] );
			return result === null ? null : [
				[ 'CONCAT', ...result[ 0 ] ],
				[ 'CONCAT', ...result[ 2 ] ]
			];
		}

		function unpipedWikilink() {
			const result = sequence( [
				nOrMore( 1, paramExpression )
			] );
			return result === null ? null : [
				[ 'CONCAT', ...result[ 0 ] ]
			];
		}

		const wikilinkContents = choice( [
			pipedWikilink,
			unpipedWikilink
		] );

		const openWikilink = makeStringParser( '[[' );
		const closeWikilink = makeStringParser( ']]' );
		function wikilink() {
			const parsedResult = sequence( [
				openWikilink,
				wikilinkContents,
				closeWikilink
			] );
			return parsedResult === null ? null : [ 'WIKILINK', ...parsedResult[ 1 ] ];
		}

		// TODO: Support data- if appropriate
		const doubleQuote = makeStringParser( '"' );
		const htmlDoubleQuoteAttributeValue = makeRegexParser( /^[^"]*/ );
		function doubleQuotedHtmlAttributeValue() {
			const parsedResult = sequence( [
				doubleQuote,
				htmlDoubleQuoteAttributeValue,
				doubleQuote
			] );
			return parsedResult === null ? null : parsedResult[ 1 ];
		}

		const singleQuote = makeStringParser( '\'' );
		const htmlSingleQuoteAttributeValue = makeRegexParser( /^[^']*/ );
		function singleQuotedHtmlAttributeValue() {
			const parsedResult = sequence( [
				singleQuote,
				htmlSingleQuoteAttributeValue,
				singleQuote
			] );
			return parsedResult === null ? null : parsedResult[ 1 ];
		}

		const htmlAttributeEquals = makeRegexParser( /^\s*=\s*/ );
		function htmlAttribute() {
			const parsedResult = sequence( [
				whitespace,
				asciiAlphabetLiteral,
				htmlAttributeEquals,
				choice( [
					doubleQuotedHtmlAttributeValue,
					singleQuotedHtmlAttributeValue
				] )
			] );
			return parsedResult === null ? null : [ parsedResult[ 1 ], parsedResult[ 3 ] ];
		}

		/**
		 * Checks if HTML is allowed
		 *
		 * @param {string} startTagName HTML start tag name
		 * @param {string|null} endTagName HTML end tag name, or null for self-closing tags
		 * @param {Object} attributes array of consecutive key value pairs,
		 *  with index 2 * n being a name and 2 * n + 1 the associated value
		 * @return {boolean} true if this is HTML is allowed, false otherwise
		 * @ignore
		 */
		function isAllowedHtml( startTagName, endTagName, attributes ) {
			startTagName = startTagName.toLowerCase();
			const isSelfClosing = endTagName === null;
			if (
				( isSelfClosing && !settings.allowedSelfClosingHtmlElements.includes( startTagName ) ) ||
				( !isSelfClosing && ( startTagName !== endTagName.toLowerCase() || !settings.allowedHtmlElements.includes( startTagName ) ) )
			) {
				return false;
			}

			let attributeName;
			for ( let i = 0, len = attributes.length; i < len; i += 2 ) {
				attributeName = attributes[ i ];
				if ( !settings.allowedHtmlCommonAttributes.includes( attributeName ) &&
					!( settings.allowedHtmlAttributesByElement[ startTagName ] || [] ).includes( attributeName ) ) {
					return false;
				}
			}

			return true;
		}

		function htmlAttributes() {
			const parsedResult = nOrMore( 0, htmlAttribute )();
			// Un-nest attributes array due to structure of jQueryMsg operations (see emit).
			return [ 'HTMLATTRIBUTES' ].concat( ...parsedResult );
		}

		const openHtmlStartTag = makeStringParser( '<' );
		const optionalWhitespace = makeRegexParser( /\s*/ );
		const optionalForwardSlash = makeRegexParser( /^\/?/ );
		const openHtmlEndTag = makeStringParser( '</' );
		const closeHtmlTag = makeRegexParser( /^\s*>/ );
		// Subset of allowed HTML markup.
		// Most elements and many attributes allowed on the server are not supported yet.
		function html() {
			// Break into three sequence calls.  That should allow accurate reconstruction of the original HTML, and requiring an exact tag name match.
			// 1. open through closeHtmlTag
			// 2. expression
			// 3. openHtmlEnd through close
			// This will allow recording the positions to reconstruct if HTML is to be treated as text.

			const startOpenTagPos = pos;
			const parsedOpenTagResult = sequence( [
				openHtmlStartTag,
				asciiAlphabetLiteral,
				htmlAttributes,
				optionalWhitespace,
				optionalForwardSlash,
				closeHtmlTag
			] );

			if ( parsedOpenTagResult === null ) {
				return null;
			}

			const endOpenTagPos = pos;
			const startTagName = parsedOpenTagResult[ 1 ];
			const wrappedAttributes = parsedOpenTagResult[ 2 ];
			const attributes = wrappedAttributes.slice( 1 );

			// Handle self-closing elements before parsing any contents
			if ( settings.allowedSelfClosingHtmlElements.includes( startTagName ) ) {
				if ( isAllowedHtml( startTagName, null, attributes ) ) {
					return [ 'HTMLELEMENT', startTagName, wrappedAttributes ];
				}
				return [ 'CONCAT', input.slice( startOpenTagPos, endOpenTagPos ) ];
			}

			const parsedHtmlContents = nOrMore( 0, expression )();

			const startCloseTagPos = pos;
			const parsedCloseTagResult = sequence( [
				openHtmlEndTag,
				asciiAlphabetLiteral,
				closeHtmlTag
			] );

			if ( parsedCloseTagResult === null ) {
				// Closing tag failed.  Return the start tag and contents.
				return [ 'CONCAT', input.slice( startOpenTagPos, endOpenTagPos ),
					...parsedHtmlContents ];
			}

			const endCloseTagPos = pos;
			const endTagName = parsedCloseTagResult[ 1 ];
			if ( isAllowedHtml( startTagName, endTagName, attributes ) ) {
				return [ 'HTMLELEMENT', startTagName, wrappedAttributes,
					...parsedHtmlContents ];
			}
			// HTML is not allowed, so contents will remain how
			// it was, while HTML markup at this level will be
			// treated as text
			// E.g. assuming script tags are not allowed:
			//
			// <script>[[Foo|bar]]</script>
			//
			// results in '&lt;script&gt;' and '&lt;/script&gt;'
			// (not treated as an HTML tag), surrounding a fully
			// parsed HTML link.
			//
			// Concatenate everything from the tag, flattening the contents.
			return [ 'CONCAT', input.slice( startOpenTagPos, endOpenTagPos ),
				...parsedHtmlContents, input.slice( startCloseTagPos, endCloseTagPos ) ];
		}

		// <nowiki>...</nowiki> tag. The tags are stripped and the contents are returned unparsed.
		function nowiki() {
			const parsedResult = sequence( [
				makeStringParser( '<nowiki>' ),
				// We use a greedy non-backtracking parser, so we must ensure here that we don't take too much
				makeRegexParser( /^.*?(?=<\/nowiki>)/ ),
				makeStringParser( '</nowiki>' )
			] );
			return parsedResult === null ? null : [ 'CONCAT', ...parsedResult[ 1 ] ];
		}

		nonWhitespaceExpression = choice( [
			template,
			wikilink,
			extlink,
			replacement,
			literalWithoutSpace
		] );
		paramExpression = choice( [
			template,
			wikilink,
			extlink,
			replacement,
			literalWithoutBar
		] );

		expression = choice( [
			template,
			wikilink,
			extlink,
			replacement,
			nowiki,
			html,
			literal
		] );

		const regularLiteralWithSquareBrackets = makeRegexParser( /^[^{}$\\]/ );
		function curlyBraceTransformExpressionLiteral() {
			const result = nOrMore( 1, regularLiteralWithSquareBrackets )();
			return result === null ? null : result.join( '' );
		}
		// Used when only {{-transformation is wanted, for 'text'
		// or 'escaped' formats
		const curlyBraceTransformExpression = choice( [
			template,
			replacement,
			curlyBraceTransformExpressionLiteral
		] );

		/**
		 * Starts the parse
		 *
		 * @param {Function} rootExpression Root parse function
		 * @return {Array|null}
		 * @ignore
		 */
		function start( rootExpression ) {
			const result = nOrMore( 0, rootExpression )();
			if ( result === null ) {
				return null;
			}
			return [ 'CONCAT', ...result ];
		}
		// everything above this point is supposed to be stateless/static, but
		// I am deferring the work of turning it into prototypes & objects. It's quite fast enough
		// finally let's do some actual work...

		const res = start( this.settings.onlyCurlyBraceTransform ? curlyBraceTransformExpression : expression );

		/*
		 * For success, the p must have gotten to the end of the input
		 * and returned a non-null.
		 * n.b. This is part of language infrastructure, so we do not throw an internationalizable message.
		 */
		if ( res === null || pos !== input.length ) {
			throw new Error( 'Parse error at position ' + pos.toString() + ' in input: ' + input );
		}
		return res;
	}

};

/**
 * Class that primarily exists to emit HTML from parser ASTs.
 *
 * @private
 * @class
 * @param {mw.language} language
 * @param {Object.<string,string>} [magic]
 */
function HtmlEmitter( language, magic ) {
	this.language = language;
	for ( const key in ( magic || {} ) ) {
		const val = magic[ key ];
		this[ key.toLowerCase() ] = function () {
			return val;
		};
	}

	/**
	 * (We put this method definition here, and not in prototype, to make sure it's not overwritten by any magic.)
	 * Walk entire node structure, applying replacements and template functions when appropriate
	 *
	 * @param {any} node Abstract syntax tree (top node or subnode)
	 * @param {Array} replacements for $1, $2, ... $n
	 * @return {any} single-string node or array of nodes suitable for jQuery appending
	 */
	this.emit = ( node, replacements ) => {
		switch ( typeof node ) {
			case 'string':
			case 'number':
				return node;

			// typeof returns object for arrays
			case 'object': {
				// node is an array of nodes
				// eslint-disable-next-line no-jquery/no-map-util
				const subnodes = $.map( node.slice( 1 ), ( n ) => this.emit( n, replacements ) );
				const operation = node[ 0 ].toLowerCase();
				if ( typeof this[ operation ] === 'function' ) {
					return this[ operation ]( subnodes, replacements );
				} else {
					throw new Error( 'Unknown operation "' + operation + '"' );
				}
			}

			case 'undefined':
				// Parsing the empty string (as an entire expression, or as a paramExpression in a template) results in undefined
				// Perhaps a more clever parser can detect this, and return the empty string? Or is that useful information?
				// The logical thing is probably to return the empty string here when we encounter undefined.
				return '';

			default:
				throw new Error( 'Unexpected type in AST: ' + typeof node );
		}
	};
}

// BIDI utility function, copied from jquery.i18n.emitter.bidi.js
//
// Matches the first strong directionality codepoint:
// - in group 1 if it is LTR
// - in group 2 if it is RTL
// Does not match if there is no strong directionality codepoint.
//
// Generated by UnicodeJS (see tools/strongDir) from the UCD; see
// https://gerrit.wikimedia.org/g/unicodejs .
// eslint-disable-next-line no-misleading-character-class
const strongDirRegExp = new RegExp(
	'(?:' +
		'(' +
			'[\u0041-\u005a\u0061-\u007a\u00aa\u00b5\u00ba\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u02b8\u02bb-\u02c1\u02d0\u02d1\u02e0-\u02e4\u02ee\u0370-\u0373\u0376\u0377\u037a-\u037d\u037f\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0482\u048a-\u052f\u0531-\u0556\u0559-\u055f\u0561-\u0587\u0589\u0903-\u0939\u093b\u093d-\u0940\u0949-\u094c\u094e-\u0950\u0958-\u0961\u0964-\u0980\u0982\u0983\u0985-\u098c\u098f\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bd-\u09c0\u09c7\u09c8\u09cb\u09cc\u09ce\u09d7\u09dc\u09dd\u09df-\u09e1\u09e6-\u09f1\u09f4-\u09fa\u0a03\u0a05-\u0a0a\u0a0f\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32\u0a33\u0a35\u0a36\u0a38\u0a39\u0a3e-\u0a40\u0a59-\u0a5c\u0a5e\u0a66-\u0a6f\u0a72-\u0a74\u0a83\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2\u0ab3\u0ab5-\u0ab9\u0abd-\u0ac0\u0ac9\u0acb\u0acc\u0ad0\u0ae0\u0ae1\u0ae6-\u0af0\u0af9\u0b02\u0b03\u0b05-\u0b0c\u0b0f\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32\u0b33\u0b35-\u0b39\u0b3d\u0b3e\u0b40\u0b47\u0b48\u0b4b\u0b4c\u0b57\u0b5c\u0b5d\u0b5f-\u0b61\u0b66-\u0b77\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99\u0b9a\u0b9c\u0b9e\u0b9f\u0ba3\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bbe\u0bbf\u0bc1\u0bc2\u0bc6-\u0bc8\u0bca-\u0bcc\u0bd0\u0bd7\u0be6-\u0bf2\u0c01-\u0c03\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c39\u0c3d\u0c41-\u0c44\u0c58-\u0c5a\u0c60\u0c61\u0c66-\u0c6f\u0c7f\u0c82\u0c83\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbd-\u0cc4\u0cc6-\u0cc8\u0cca\u0ccb\u0cd5\u0cd6\u0cde\u0ce0\u0ce1\u0ce6-\u0cef\u0cf1\u0cf2\u0d02\u0d03\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d3a\u0d3d-\u0d40\u0d46-\u0d48\u0d4a-\u0d4c\u0d4e\u0d57\u0d5f-\u0d61\u0d66-\u0d75\u0d79-\u0d7f\u0d82\u0d83\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0dcf-\u0dd1\u0dd8-\u0ddf\u0de6-\u0def\u0df2-\u0df4\u0e01-\u0e30\u0e32\u0e33\u0e40-\u0e46\u0e4f-\u0e5b\u0e81\u0e82\u0e84\u0e87\u0e88\u0e8a\u0e8d\u0e94-\u0e97\u0e99-\u0e9f\u0ea1-\u0ea3\u0ea5\u0ea7\u0eaa\u0eab\u0ead-\u0eb0\u0eb2\u0eb3\u0ebd\u0ec0-\u0ec4\u0ec6\u0ed0-\u0ed9\u0edc-\u0edf\u0f00-\u0f17\u0f1a-\u0f34\u0f36\u0f38\u0f3e-\u0f47\u0f49-\u0f6c\u0f7f\u0f85\u0f88-\u0f8c\u0fbe-\u0fc5\u0fc7-\u0fcc\u0fce-\u0fda\u1000-\u102c\u1031\u1038\u103b\u103c\u103f-\u1057\u105a-\u105d\u1061-\u1070\u1075-\u1081\u1083\u1084\u1087-\u108c\u108e-\u109c\u109e-\u10c5\u10c7\u10cd\u10d0-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u1360-\u137c\u1380-\u138f\u13a0-\u13f5\u13f8-\u13fd\u1401-\u167f\u1681-\u169a\u16a0-\u16f8\u1700-\u170c\u170e-\u1711\u1720-\u1731\u1735\u1736\u1740-\u1751\u1760-\u176c\u176e-\u1770\u1780-\u17b3\u17b6\u17be-\u17c5\u17c7\u17c8\u17d4-\u17da\u17dc\u17e0-\u17e9\u1810-\u1819\u1820-\u1877\u1880-\u18a8\u18aa\u18b0-\u18f5\u1900-\u191e\u1923-\u1926\u1929-\u192b\u1930\u1931\u1933-\u1938\u1946-\u196d\u1970-\u1974\u1980-\u19ab\u19b0-\u19c9\u19d0-\u19da\u1a00-\u1a16\u1a19\u1a1a\u1a1e-\u1a55\u1a57\u1a61\u1a63\u1a64\u1a6d-\u1a72\u1a80-\u1a89\u1a90-\u1a99\u1aa0-\u1aad\u1b04-\u1b33\u1b35\u1b3b\u1b3d-\u1b41\u1b43-\u1b4b\u1b50-\u1b6a\u1b74-\u1b7c\u1b82-\u1ba1\u1ba6\u1ba7\u1baa\u1bae-\u1be5\u1be7\u1bea-\u1bec\u1bee\u1bf2\u1bf3\u1bfc-\u1c2b\u1c34\u1c35\u1c3b-\u1c49\u1c4d-\u1c7f\u1cc0-\u1cc7\u1cd3\u1ce1\u1ce9-\u1cec\u1cee-\u1cf3\u1cf5\u1cf6\u1d00-\u1dbf\u1e00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u200e\u2071\u207f\u2090-\u209c\u2102\u2107\u210a-\u2113\u2115\u2119-\u211d\u2124\u2126\u2128\u212a-\u212d\u212f-\u2139\u213c-\u213f\u2145-\u2149\u214e\u214f\u2160-\u2188\u2336-\u237a\u2395\u249c-\u24e9\u26ac\u2800-\u28ff\u2c00-\u2c2e\u2c30-\u2c5e\u2c60-\u2ce4\u2ceb-\u2cee\u2cf2\u2cf3\u2d00-\u2d25\u2d27\u2d2d\u2d30-\u2d67\u2d6f\u2d70\u2d80-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u3005-\u3007\u3021-\u3029\u302e\u302f\u3031-\u3035\u3038-\u303c\u3041-\u3096\u309d-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312d\u3131-\u318e\u3190-\u31ba\u31f0-\u321c\u3220-\u324f\u3260-\u327b\u327f-\u32b0\u32c0-\u32cb\u32d0-\u32fe\u3300-\u3376\u337b-\u33dd\u33e0-\u33fe\u3400-\u4db5\u4e00-\u9fd5\ua000-\ua48c\ua4d0-\ua60c\ua610-\ua62b\ua640-\ua66e\ua680-\ua69d\ua6a0-\ua6ef\ua6f2-\ua6f7\ua722-\ua787\ua789-\ua7ad\ua7b0-\ua7b7\ua7f7-\ua801\ua803-\ua805\ua807-\ua80a\ua80c-\ua824\ua827\ua830-\ua837\ua840-\ua873\ua880-\ua8c3\ua8ce-\ua8d9\ua8f2-\ua8fd\ua900-\ua925\ua92e-\ua946\ua952\ua953\ua95f-\ua97c\ua983-\ua9b2\ua9b4\ua9b5\ua9ba\ua9bb\ua9bd-\ua9cd\ua9cf-\ua9d9\ua9de-\ua9e4\ua9e6-\ua9fe\uaa00-\uaa28\uaa2f\uaa30\uaa33\uaa34\uaa40-\uaa42\uaa44-\uaa4b\uaa4d\uaa50-\uaa59\uaa5c-\uaa7b\uaa7d-\uaaaf\uaab1\uaab5\uaab6\uaab9-\uaabd\uaac0\uaac2\uaadb-\uaaeb\uaaee-\uaaf5\uab01-\uab06\uab09-\uab0e\uab11-\uab16\uab20-\uab26\uab28-\uab2e\uab30-\uab65\uab70-\uabe4\uabe6\uabe7\uabe9-\uabec\uabf0-\uabf9\uac00-\ud7a3\ud7b0-\ud7c6\ud7cb-\ud7fb\ue000-\ufa6d\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\uff21-\uff3a\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc]|\ud800[\udc00-\udc0b]|\ud800[\udc0d-\udc26]|\ud800[\udc28-\udc3a]|\ud800\udc3c|\ud800\udc3d|\ud800[\udc3f-\udc4d]|\ud800[\udc50-\udc5d]|\ud800[\udc80-\udcfa]|\ud800\udd00|\ud800\udd02|\ud800[\udd07-\udd33]|\ud800[\udd37-\udd3f]|\ud800[\uddd0-\uddfc]|\ud800[\ude80-\ude9c]|\ud800[\udea0-\uded0]|\ud800[\udf00-\udf23]|\ud800[\udf30-\udf4a]|\ud800[\udf50-\udf75]|\ud800[\udf80-\udf9d]|\ud800[\udf9f-\udfc3]|\ud800[\udfc8-\udfd5]|\ud801[\udc00-\udc9d]|\ud801[\udca0-\udca9]|\ud801[\udd00-\udd27]|\ud801[\udd30-\udd63]|\ud801\udd6f|\ud801[\ude00-\udf36]|\ud801[\udf40-\udf55]|\ud801[\udf60-\udf67]|\ud804\udc00|\ud804[\udc02-\udc37]|\ud804[\udc47-\udc4d]|\ud804[\udc66-\udc6f]|\ud804[\udc82-\udcb2]|\ud804\udcb7|\ud804\udcb8|\ud804[\udcbb-\udcc1]|\ud804[\udcd0-\udce8]|\ud804[\udcf0-\udcf9]|\ud804[\udd03-\udd26]|\ud804\udd2c|\ud804[\udd36-\udd43]|\ud804[\udd50-\udd72]|\ud804[\udd74-\udd76]|\ud804[\udd82-\uddb5]|\ud804[\uddbf-\uddc9]|\ud804\uddcd|\ud804[\uddd0-\udddf]|\ud804[\udde1-\uddf4]|\ud804[\ude00-\ude11]|\ud804[\ude13-\ude2e]|\ud804\ude32|\ud804\ude33|\ud804\ude35|\ud804[\ude38-\ude3d]|\ud804[\ude80-\ude86]|\ud804\ude88|\ud804[\ude8a-\ude8d]|\ud804[\ude8f-\ude9d]|\ud804[\ude9f-\udea9]|\ud804[\udeb0-\udede]|\ud804[\udee0-\udee2]|\ud804[\udef0-\udef9]|\ud804\udf02|\ud804\udf03|\ud804[\udf05-\udf0c]|\ud804\udf0f|\ud804\udf10|\ud804[\udf13-\udf28]|\ud804[\udf2a-\udf30]|\ud804\udf32|\ud804\udf33|\ud804[\udf35-\udf39]|\ud804[\udf3d-\udf3f]|\ud804[\udf41-\udf44]|\ud804\udf47|\ud804\udf48|\ud804[\udf4b-\udf4d]|\ud804\udf50|\ud804\udf57|\ud804[\udf5d-\udf63]|\ud805[\udc80-\udcb2]|\ud805\udcb9|\ud805[\udcbb-\udcbe]|\ud805\udcc1|\ud805[\udcc4-\udcc7]|\ud805[\udcd0-\udcd9]|\ud805[\udd80-\uddb1]|\ud805[\uddb8-\uddbb]|\ud805\uddbe|\ud805[\uddc1-\udddb]|\ud805[\ude00-\ude32]|\ud805\ude3b|\ud805\ude3c|\ud805\ude3e|\ud805[\ude41-\ude44]|\ud805[\ude50-\ude59]|\ud805[\ude80-\udeaa]|\ud805\udeac|\ud805\udeae|\ud805\udeaf|\ud805\udeb6|\ud805[\udec0-\udec9]|\ud805[\udf00-\udf19]|\ud805\udf20|\ud805\udf21|\ud805\udf26|\ud805[\udf30-\udf3f]|\ud806[\udca0-\udcf2]|\ud806\udcff|\ud806[\udec0-\udef8]|\ud808[\udc00-\udf99]|\ud809[\udc00-\udc6e]|\ud809[\udc70-\udc74]|\ud809[\udc80-\udd43]|\ud80c[\udc00-\udfff]|\ud80d[\udc00-\udc2e]|\ud811[\udc00-\ude46]|\ud81a[\udc00-\ude38]|\ud81a[\ude40-\ude5e]|\ud81a[\ude60-\ude69]|\ud81a\ude6e|\ud81a\ude6f|\ud81a[\uded0-\udeed]|\ud81a\udef5|\ud81a[\udf00-\udf2f]|\ud81a[\udf37-\udf45]|\ud81a[\udf50-\udf59]|\ud81a[\udf5b-\udf61]|\ud81a[\udf63-\udf77]|\ud81a[\udf7d-\udf8f]|\ud81b[\udf00-\udf44]|\ud81b[\udf50-\udf7e]|\ud81b[\udf93-\udf9f]|\ud82c\udc00|\ud82c\udc01|\ud82f[\udc00-\udc6a]|\ud82f[\udc70-\udc7c]|\ud82f[\udc80-\udc88]|\ud82f[\udc90-\udc99]|\ud82f\udc9c|\ud82f\udc9f|\ud834[\udc00-\udcf5]|\ud834[\udd00-\udd26]|\ud834[\udd29-\udd66]|\ud834[\udd6a-\udd72]|\ud834\udd83|\ud834\udd84|\ud834[\udd8c-\udda9]|\ud834[\uddae-\udde8]|\ud834[\udf60-\udf71]|\ud835[\udc00-\udc54]|\ud835[\udc56-\udc9c]|\ud835\udc9e|\ud835\udc9f|\ud835\udca2|\ud835\udca5|\ud835\udca6|\ud835[\udca9-\udcac]|\ud835[\udcae-\udcb9]|\ud835\udcbb|\ud835[\udcbd-\udcc3]|\ud835[\udcc5-\udd05]|\ud835[\udd07-\udd0a]|\ud835[\udd0d-\udd14]|\ud835[\udd16-\udd1c]|\ud835[\udd1e-\udd39]|\ud835[\udd3b-\udd3e]|\ud835[\udd40-\udd44]|\ud835\udd46|\ud835[\udd4a-\udd50]|\ud835[\udd52-\udea5]|\ud835[\udea8-\udeda]|\ud835[\udedc-\udf14]|\ud835[\udf16-\udf4e]|\ud835[\udf50-\udf88]|\ud835[\udf8a-\udfc2]|\ud835[\udfc4-\udfcb]|\ud836[\udc00-\uddff]|\ud836[\ude37-\ude3a]|\ud836[\ude6d-\ude74]|\ud836[\ude76-\ude83]|\ud836[\ude85-\ude8b]|\ud83c[\udd10-\udd2e]|\ud83c[\udd30-\udd69]|\ud83c[\udd70-\udd9a]|\ud83c[\udde6-\ude02]|\ud83c[\ude10-\ude3a]|\ud83c[\ude40-\ude48]|\ud83c\ude50|\ud83c\ude51|[\ud840-\ud868][\udc00-\udfff]|\ud869[\udc00-\uded6]|\ud869[\udf00-\udfff]|[\ud86a-\ud86c][\udc00-\udfff]|\ud86d[\udc00-\udf34]|\ud86d[\udf40-\udfff]|\ud86e[\udc00-\udc1d]|\ud86e[\udc20-\udfff]|[\ud86f-\ud872][\udc00-\udfff]|\ud873[\udc00-\udea1]|\ud87e[\udc00-\ude1d]|[\udb80-\udbbe][\udc00-\udfff]|\udbbf[\udc00-\udffd]|[\udbc0-\udbfe][\udc00-\udfff]|\udbff[\udc00-\udffd]' +
		')|(' +
			'[\u0590\u05be\u05c0\u05c3\u05c6\u05c8-\u05ff\u07c0-\u07ea\u07f4\u07f5\u07fa-\u0815\u081a\u0824\u0828\u082e-\u0858\u085c-\u089f\u200f\ufb1d\ufb1f-\ufb28\ufb2a-\ufb4f\u0608\u060b\u060d\u061b-\u064a\u066d-\u066f\u0671-\u06d5\u06e5\u06e6\u06ee\u06ef\u06fa-\u0710\u0712-\u072f\u074b-\u07a5\u07b1-\u07bf\u08a0-\u08e2\ufb50-\ufd3d\ufd40-\ufdcf\ufdf0-\ufdfc\ufdfe\ufdff\ufe70-\ufefe]|\ud802[\udc00-\udd1e]|\ud802[\udd20-\ude00]|\ud802\ude04|\ud802[\ude07-\ude0b]|\ud802[\ude10-\ude37]|\ud802[\ude3b-\ude3e]|\ud802[\ude40-\udee4]|\ud802[\udee7-\udf38]|\ud802[\udf40-\udfff]|\ud803[\udc00-\ude5f]|\ud803[\ude7f-\udfff]|\ud83a[\udc00-\udccf]|\ud83a[\udcd7-\udfff]|\ud83b[\udc00-\uddff]|\ud83b[\udf00-\udfff]|\ud83b[\udf00-\udfff]|\ud83b[\udf00-\udfff]|\ud83b[\udf00-\udfff]|\ud83b[\udf00-\udfff]|\ud83b[\udf00-\udfff]|\ud83b[\udf00-\udfff]|\ud83b[\udf00-\udfff]|\ud83b[\udf00-\udfff]|\ud83b[\udf00-\udfff]|\ud83b[\udf00-\udfff]|\ud83b[\udf00-\udfff]|\ud83b[\udf00-\udfff]|\ud83b[\ude00-\udeef]|\ud83b[\udef2-\udeff]' +
		')' +
	')'
);

/**
 * Gets directionality of the first strongly directional codepoint
 *
 * This is the rule the BIDI algorithm uses to determine the directionality of
 * paragraphs ( http://unicode.org/reports/tr9/#The_Paragraph_Level ) and
 * FSI isolates ( http://unicode.org/reports/tr9/#Explicit_Directional_Isolates ).
 *
 * TODO: Does not handle BIDI control characters inside the text.
 * TODO: Does not handle unallocated characters.
 *
 * @ignore
 * @param {string} text The text from which to extract initial directionality.
 * @return {string|null} Directionality (either 'ltr' or 'rtl')
 */
function strongDirFromContent( text ) {
	const m = text.match( strongDirRegExp );
	if ( !m ) {
		return null;
	}
	if ( m[ 2 ] === undefined ) {
		return 'ltr';
	}
	return 'rtl';
}

// For everything in input that follows double-open-curly braces, there should be an equivalent parser
// function. For instance {{PLURAL ... }} will be processed by 'plural'.
// If you have 'magic words' then configure the parser to have them upon creation.
//
// An emitter method takes the parent node, the array of subnodes and the array of replacements (the values that $1, $2... should translate to).
// Note: all such functions must be pure, with the exception of referring to other pure functions via this.language (convertPlural and so on)
HtmlEmitter.prototype = {
	/**
	 * Parsing has been applied depth-first we can assume that all nodes here are single nodes
	 * Must return a single node to parents -- a jQuery with synthetic span
	 * However, unwrap any other synthetic spans in our children and pass them upwards
	 *
	 * @param {any[]} nodes Some single nodes, some arrays of nodes
	 * @return {jQuery}
	 */
	concat: function ( nodes ) {
		const $span = $( '<span>' ).addClass( 'mediaWiki_htmlEmitter' );
		// Use Array.from since mixed parameter.
		Array.from( nodes ).forEach( ( node ) => {
			// Let jQuery append nodes, arrays of nodes and jQuery objects
			// other things (strings, numbers, ..) are appended as text nodes (not as HTML strings)
			appendWithoutParsing( $span, node );
		} );
		return $span;
	},

	/**
	 * Return escaped replacement of correct index, or string if unavailable.
	 * Note that we expect the parsed parameter to be zero-based. i.e. $1 should have become [ 0 ].
	 * if the specified parameter is not found return the same string
	 * (e.g. "$99" -> parameter 98 -> not found -> return "$99" )
	 *
	 * If the replacement at the index is an object, then a special property
	 * is is added to it (if it does not exist already).
	 * If the special property was already set, then we try to clone (instead of append)
	 * the replacement object. This allows allow using a jQuery or HTMLElement object
	 * multiple times within a single interface message.
	 *
	 * TODO: Throw error if nodes.length > 1 ?
	 *
	 * @param {Array} nodes List of one element, integer, n >= 0
	 * @param {Array} replacements List of at least n strings
	 * @return {string|jQuery} replacement
	 */
	replace: function ( nodes, replacements ) {
		const index = parseInt( nodes[ 0 ], 10 );

		if ( index < replacements.length ) {
			if ( typeof replacements[ index ] === 'object' ) {
				// Only actually clone on second use
				if ( !replacements[ index ].mwJQueryMsgHasAlreadyBeenUsedAsAReplacement ) {
					// Add our special property to the foreign object
					// in the least invasive way
					Object.defineProperty(
						replacements[ index ],
						'mwJQueryMsgHasAlreadyBeenUsedAsAReplacement',
						{
							value: true,
							enumerable: false,
							writable: false
						}
					);
					return replacements[ index ];
				}
				if ( typeof replacements[ index ].clone === 'function' ) {
					// if it is a jQuery object, use jQuery's clone method
					return replacements[ index ].clone( true );
				}
				if ( typeof replacements[ index ].cloneNode === 'function' ) {
					// if it is a Node, then use the native cloning functionality
					return replacements[ index ].cloneNode( true );
				}
				return replacements[ index ];
			}
			return replacements[ index ];
		} else {
			// index not found, fallback to displaying variable
			return '$' + ( index + 1 );
		}
	},

	/**
	 * Transform wiki-link
	 *
	 * TODO:
	 * It only handles basic cases, either no pipe, or a pipe with an explicit
	 * anchor.
	 *
	 * It does not attempt to handle features like the pipe trick.
	 * However, the pipe trick should usually not be present in wikitext retrieved
	 * from the server, since the replacement is done at save time.
	 * It may, though, if the wikitext appears in extension-controlled content.
	 *
	 * @param {string[]} nodes
	 * @return {jQuery}
	 */
	wikilink: function ( nodes ) {
		let page = textify( nodes[ 0 ] );
		// Strip leading ':', which is used to suppress special behavior in wikitext links,
		// e.g. [[:Category:Foo]] or [[:File:Foo.jpg]]
		if ( page.startsWith( ':' ) ) {
			page = page.slice( 1 );
		}
		const title = new mw.Title( page );

		let anchor;
		if ( nodes.length === 1 ) {
			// [[Some Page]] or [[Namespace:Some Page]]
			anchor = page;
		} else {
			// [[Some Page|anchor text]] or [[Namespace:Some Page|anchor]]
			anchor = nodes[ 1 ];
		}

		const $el = $( '<a>' ).attr( {
			title: title.getPrefixedText() || null,
			href: title.getUrl()
		} );
		return appendWithoutParsing( $el, anchor );
	},

	/**
	 * Converts array of HTML element key value pairs to object
	 *
	 * @param {Array} nodes Array of consecutive key value pairs, with index 2 * n being a
	 *  name and 2 * n + 1 the associated value
	 * @return {Object} Object mapping attribute name to attribute value
	 */
	htmlattributes: function ( nodes ) {
		const mapping = {};
		for ( let i = 0, len = nodes.length; i < len; i += 2 ) {
			mapping[ nodes[ i ] ] = decodePrimaryHtmlEntities( nodes[ i + 1 ] );
		}
		return mapping;
	},

	/**
	 * Handles an (already-validated) HTML element.
	 *
	 * @param {Array} nodes Nodes to process when creating element
	 * @return {jQuery}
	 */
	htmlelement: function ( nodes ) {
		const tagName = nodes.shift();
		const attributes = nodes.shift();
		const contents = nodes;
		const $element = $( document.createElement( tagName ) ).attr( attributes );
		return appendWithoutParsing( $element, contents );
	},

	/**
	 * Transform parsed structure into external link.
	 *
	 * The "href" can be:
	 * - a jQuery object, treat it as "enclosing" the link text.
	 * - a function, treat it as the click handler.
	 * - a string, or our HtmlEmitter jQuery object, treat it as a URI after stringifying.
	 *
	 * TODO: throw an error if nodes.length > 2 ?
	 *
	 * @param {Array} nodes List of two elements, {jQuery|Function|String} and {string}
	 * @return {jQuery}
	 */
	extlink: function ( nodes ) {
		const arg = nodes[ 0 ],
			contents = nodes[ 1 ];
		let $el;
		if ( arg instanceof $ && !arg.hasClass( 'mediaWiki_htmlEmitter' ) ) {
			$el = arg;
		} else {
			$el = $( '<a>' );
			if ( typeof arg === 'function' ) {
				$el.attr( {
					role: 'button',
					tabindex: 0
				} ).on( 'click keypress', function ( e ) {
					if (
						e.type === 'click' ||
						e.type === 'keypress' && e.which === 13
					) {
						arg.call( this, e );
					}
				} );
			} else {
				const target = textify( arg );

				if ( target.search( new RegExp( '^(/|' + mw.config.get( 'wgUrlProtocols' ) + ')' ) ) !== -1 ) {
					$el.attr( 'href', target );
					if ( target.search( '^' + mw.config.get( 'wgArticlePath' ).replace( /\$1/g, '.+?' ) + '$' ) === -1 ) {
						$el.addClass( 'external' );
					}
				} else {
					mw.log( 'External link in message had illegal target ' + target );
					return appendWithoutParsing(
						$( '<span>' ),
						[ '[' + target + ' ' ].concat( contents ).concat( ']' )
					).contents();
				}
			}
		}
		return appendWithoutParsing( $el.empty(), contents );
	},

	/**
	 * Transform formal syntax
	 *
	 * @param {string[]} nodes List of nodes
	 * @return {string|jQuery} selected (in)formal form according to the current language
	 */
	'#formal': function ( nodes ) {
		const formalityIndex = this.language.getData(
			mw.config.get( 'wgUserLanguage' ),
			'formalityIndex'
		);

		if ( nodes.length === 0 ) {
			return '';
		} else if ( nodes.length === 1 ) {
			return nodes[ 0 ];
		}

		return nodes[ formalityIndex ];
	},

	/**
	 * Transform parsed structure into pluralization
	 * n.b. The first node may be a non-integer (for instance, a string representing an Arabic number).
	 * So convert it back with the current language's convertNumber.
	 *
	 * @param {Array} nodes List of nodes, [ {string|number}, {string}, {string} ... ]
	 * @return {string|jQuery} selected pluralized form according to current language
	 */
	plural: function ( nodes ) {
		const explicitPluralForms = {};

		const count = parseFloat( this.language.convertNumber( textify( nodes[ 0 ] ), true ) );
		let forms = nodes.slice( 1 );
		for ( let formIndex = 0; formIndex < forms.length; formIndex++ ) {
			const form = forms[ formIndex ];

			if ( form instanceof $ && form.hasClass( 'mediaWiki_htmlEmitter' ) ) {
				// This is a nested node, may be an explicit plural form like 5=[$2 linktext]
				const firstChild = form.contents().get( 0 );
				if ( firstChild && firstChild.nodeType === Node.TEXT_NODE ) {
					const firstChildText = firstChild.textContent;
					if ( /^\d+=/.test( firstChildText ) ) {
						const explicitPluralFormNumber = parseInt( firstChildText.split( /=/ )[ 0 ], 10 );
						// Use the digit part as key and rest of first text node and
						// rest of child nodes as value.
						firstChild.textContent = firstChildText.slice( firstChildText.indexOf( '=' ) + 1 );
						explicitPluralForms[ explicitPluralFormNumber ] = form;
						forms[ formIndex ] = undefined;
					}
				}
			} else if ( /^\d+=/.test( form ) ) {
				// Simple explicit plural forms like 12=a dozen
				const explicitPluralFormNumber = parseInt( form.split( /=/ )[ 0 ], 10 );
				explicitPluralForms[ explicitPluralFormNumber ] = form.slice( form.indexOf( '=' ) + 1 );
				forms[ formIndex ] = undefined;
			}
		}

		// Remove explicit plural forms from the forms. They were set undefined in the above loop.
		// eslint-disable-next-line no-jquery/no-map-util
		forms = $.map( forms, ( f ) => f );

		return this.language.convertPlural( count, forms, explicitPluralForms );
	},

	/**
	 * Transform parsed structure according to gender.
	 *
	 * The first node must be one of:
	 * - the mw.user object (or a compatible one)
	 * - an empty string - indicating the current user, same effect as passing the mw.user object
	 * - a gender string ('male', 'female' or 'unknown')
	 *
	 * @example
	 * {{gender:[ mw.user object | '' | 'male' | 'female' | 'unknown' ] | masculine form | feminine form | neutral form}}
	 *
	 * @param {Array} nodes List of nodes, [ {string|mw.user}, {string}, {string}, {string} ]
	 * @return {string|jQuery} Selected gender form according to current language
	 */
	gender: function ( nodes ) {
		const forms = nodes.slice( 1 );

		let maybeUser = nodes[ 0 ];
		if ( maybeUser === '' ) {
			maybeUser = mw.user;
		}

		let gender;
		// If we are passed a mw.user-like object, check their gender.
		// Otherwise, assume the gender string itself was passed .
		if ( maybeUser && maybeUser.options instanceof mw.Map ) {
			gender = maybeUser.options.get( 'gender' );
		} else {
			gender = textify( maybeUser );
		}

		return this.language.gender( gender, forms );
	},

	/**
	 * Wraps argument with unicode control characters for directionality safety
	 *
	 * Identical to the implementation in jquery.i18n.emitter.bidi.js
	 *
	 * This solves the problem where directionality-neutral characters at the edge of
	 * the argument string get interpreted with the wrong directionality from the
	 * enclosing context, giving renderings that look corrupted like "(Ben_(WMF".
	 *
	 * The wrapping is LRE...PDF or RLE...PDF, depending on the detected
	 * directionality of the argument string, using the BIDI algorithm's own "First
	 * strong directional codepoint" rule. Essentially, this works round the fact that
	 * there is no embedding equivalent of U+2068 FSI (isolation with heuristic
	 * direction inference). The latter is cleaner but still not widely supported.
	 *
	 * @param {string[]} nodes The text nodes from which to take the first item.
	 * @return {string} Wrapped String of content as needed.
	 */
	bidi: function ( nodes ) {
		const dir = strongDirFromContent( nodes[ 0 ] );
		if ( dir === 'ltr' ) {
			// Wrap in LEFT-TO-RIGHT EMBEDDING ... POP DIRECTIONAL FORMATTING
			return '\u202A' + nodes[ 0 ] + '\u202C';
		}
		if ( dir === 'rtl' ) {
			// Wrap in RIGHT-TO-LEFT EMBEDDING ... POP DIRECTIONAL FORMATTING
			return '\u202B' + nodes[ 0 ] + '\u202C';
		}
		// No strong directionality: do not wrap
		return nodes[ 0 ];
	},

	/**
	 * Transform parsed structure into grammar conversion.
	 * Invoked by putting `{{grammar:form|word}}` in a message
	 *
	 * @param {Array} nodes List of nodes [{Grammar case eg: genitive}, {string word}]
	 * @return {string|jQuery} selected grammatical form according to current language
	 */
	grammar: function ( nodes ) {
		const form = nodes[ 0 ],
			word = nodes[ 1 ];
		// These could be jQuery objects (passed as message parameters),
		// in which case we can't transform them (like rawParams() in PHP).
		if ( typeof form === 'string' && typeof word === 'string' ) {
			return this.language.convertGrammar( word, form );
		}
		return word;
	},

	/**
	 * Transform parsed structure into a int: (interface language) message include
	 * Invoked by putting `{{int:othermessage}}` into a message
	 *
	 * TODO Syntax in the included message is not parsed, this seems like a bug?
	 *
	 * @param {Array} nodes List of nodes
	 * @return {string} Other message
	 */
	int: function ( nodes ) {
		const msg = textify( nodes[ 0 ] );
		return getMessageFunction()( mwString.lcFirst( msg ) );
	},

	/**
	 * Get localized namespace name from canonical name or namespace number.
	 * Invoked by putting `{{ns:foo}}` into a message
	 *
	 * @param {Array} nodes List of nodes
	 * @return {string} Localized namespace name
	 */
	ns: function ( nodes ) {
		let ns = textify( nodes[ 0 ] ).trim();
		if ( !/^\d+$/.test( ns ) ) {
			ns = mw.config.get( 'wgNamespaceIds' )[ ns.replace( / /g, '_' ).toLowerCase() ];
		}
		ns = mw.config.get( 'wgFormattedNamespaces' )[ ns ];
		return ns || '';
	},

	/**
	 * Takes an unformatted number (arab, no group separators and . as decimal separator)
	 * and outputs it in the localized digit script and formatted with decimal
	 * separator, according to the current language.
	 *
	 * @param {Array} nodes List of nodes
	 * @return {number|string|jQuery} Formatted number
	 */
	formatnum: function ( nodes ) {
		const isInteger = !!nodes[ 1 ] && nodes[ 1 ] === 'R',
			number = nodes[ 0 ];

		// These could be jQuery objects (passed as message parameters),
		// in which case we can't transform them (like rawParams() in PHP).
		if ( typeof number === 'string' || typeof number === 'number' ) {
			return this.language.convertNumber( number, isInteger );
		}
		return number;
	},

	/**
	 * Takes a pagename and optional URL queries and returns a full URL to that
	 * page (with URL queries).
	 *
	 * @param {Array} nodes List of nodes
	 * @return {string} A URL string
	 */
	fullurl: function ( nodes ) {
		const targetPage = textify( nodes[ 0 ] ),
			queryObject = {};
		let queryStrings = nodes[ 1 ];

		if ( queryStrings ) {
			queryStrings = textify( queryStrings );
			queryStrings = new URLSearchParams( queryStrings );
			for ( const [ key, value ] of queryStrings.entries() ) {
				queryObject[ key ] = value;
			}
		}

		return mw.config.get( 'wgServer' ) + util.getUrl( targetPage, queryObject );
	},

	/**
	 * Lowercase text
	 *
	 * @param {Array} nodes List of nodes
	 * @return {string} The given text, all in lowercase
	 */
	lc: function ( nodes ) {
		return textify( nodes[ 0 ] ).toLowerCase();
	},

	/**
	 * Uppercase text
	 *
	 * @param {Array} nodes List of nodes
	 * @return {string} The given text, all in uppercase
	 */
	uc: function ( nodes ) {
		return textify( nodes[ 0 ] ).toUpperCase();
	},

	/**
	 * Lowercase first letter of input, leaving the rest unchanged
	 *
	 * @param {Array} nodes List of nodes
	 * @return {string} The given text, with the first character in lowercase
	 */
	lcfirst: function ( nodes ) {
		const text = textify( nodes[ 0 ] );
		return mwString.lcFirst( text );
	},

	/**
	 * Uppercase first letter of input, leaving the rest unchanged
	 *
	 * @param {Array} nodes List of nodes
	 * @return {string} The given text, with the first character in uppercase
	 */
	ucfirst: function ( nodes ) {
		const text = textify( nodes[ 0 ] );
		return mwString.ucFirst( text );
	}
};

/**
 * Provides a {@link jQuery} plugin that parses messages.
 *
 * @module mediawiki.jqueryMsg
 */
/**
 * Parses the message in the message key, doing replacements optionally, and appends the nodes to
 * the current selector. Bindings to passed-in jquery elements are preserved. Functions become click handlers for [$1 linktext] links.
 *
 * To use this {@link jQuery} plugin, load the `mediawiki.jqueryMsg` module with {@link mw.loader}.
 *
 * @memberof module:mediawiki.jqueryMsg
 * @param {string} message key
 * @param {...string[]} arguments
 * @example
 * mw.loader.using('mediawiki.jqueryMsg' ).then(() => {
 *        var $userlink = $( '<a>' ).click( function () { alert( "hello!!" ) } );
 *        $( 'p#headline' ).msg( 'hello-user', $userlink );
 * } );
 *
 * // N.B. replacements are variadic arguments or an array in second parameter. In other words:
 * somefunction( a, b, c, d )
 * // is equivalent to
 * somefunction( a, [b, c, d] )
 *
 * // Note: We append to 'this', which in a jQuery plugin context will be the selected elements.
 */
$.fn.msg = getPlugin();

// Replace the default message parser with jqueryMsg
const oldParser = mw.Message.prototype.parser;
mw.Message.prototype.parser = function ( format ) {
	// Fall back to mw.msg's simple parser where possible
	if (
		// Plain text output always uses the simple parser
		format === 'plain' ||
		(
			// jqueryMsg parser is needed for messages containing wikitext
			!/\{\{|[<>[&]/.test( this.map.get( this.key ) ) &&
			// jqueryMsg parser is needed when jQuery objects or DOM nodes are passed in as parameters
			!this.parameters.some( ( param ) => param instanceof $ || ( param && param.nodeType !== undefined ) )
		)
	) {
		return oldParser.call( this, format );
	}

	if ( !Object.prototype.hasOwnProperty.call( this.map, format ) ) {
		this.map[ format ] = getMessageFunction( {
			messages: this.map,
			// For format 'escaped', escaping part is handled by mediawiki.js
			format: format
		} );
	}
	return this.map[ format ]( this.key, this.parameters );
};

/**
 * Parse the message to DOM nodes, rather than HTML string like {@link mw.Message#parse}.
 *
 * This method is only available when jqueryMsg is loaded.
 *
 * @example
 * const msg = mw.message( 'key' );
 * mw.loader.using(`mediawiki.jqueryMsg`).then(() => {
 *   if ( msg.isParseable() ) {
 *     const $node = msg.parseDom();
 *     $node.appendTo('body');
 *   }
 * })
 *
 * @since 1.27
 * @method parseDom
 * @memberof mw.Message.prototype
 * @return {jQuery}
 */
mw.Message.prototype.parseDom = ( function () {
	let failableParserFn;

	return function () {
		if ( !failableParserFn ) {
			failableParserFn = getFailableParserFn();
		}
		const $result = failableParserFn( [ this.key, this.parameters ] );
		return $result.contents();
	};
}() );

/**
 * Check whether the message contains only syntax supported by jqueryMsg.
 *
 * This method is only available when jqueryMsg is loaded.
 *
 * @example
 * const msg = mw.message( 'key' );
 * mw.loader.using(`mediawiki.jqueryMsg`).then(() => {
 *   if ( msg.isParseable() ) {
 *     ...
 *   }
 * })
 *
 * @since 1.41
 * @method isParseable
 * @memberof mw.Message.prototype
 * @return {boolean}
 */
mw.Message.prototype.isParseable = function () {
	const parser = new Parser();
	try {
		parser.parse( this.key, this.parameters );
		return true;
	} catch ( e ) {
		return false;
	}
};

/**
 * Can be deleted when MobileFrontend is updated.
 * https://phabricator.wikimedia.org/T354540
 *
 * @private
 */
mw.jqueryMsg = {
	Parser
};
mw.log.deprecate( mw, 'jqueryMsg', mw.jqueryMsg, 'mw.jqueryMsg is a @private library.' );
// Expose for testing purposes only (not a stable API).
module.exports = {
	test: {
		getMessageFunction,
		setMessageFunction,
		getParserDefaults,
		setParserDefaults,
		Parser
	}
};
