mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/CodeMirror
synced 2024-11-23 22:03:28 +00:00
7d3482f89e
CodeMirror 6 requires the use of NPM, but we can still bundle all CM packages into one file, and then everything else (i.e. our code) is managed by ResourceLoader as per usual. This makes contribution considerably easier as we no longer need a build step for each change. CM5 files are now under resources/legacy, and the CM6 files are moved to the root of the resources/ directory. Only one file, codemirror.bundle.js, is managed by Rollup, while everything else is RL. The Rollup output for now is put under resources/lib/ alongside the CM5 upstream files. This patch is *mostly* renames of files, along with changing ECMAScript Module (ESM) syntax into the CommonJS style that ResourceLoader prefers. We also remove more modern JS syntax (i.e. private class methods) that we were able to use before because we had a build step with Babel. This patch should effectively make no user-facing changes, or to the ResourceLoader modules we offer in Extension:CodeMirror. Finally, bump version in extension.json to 6, to match the upstream lib, and add Bhsd as an author :-) Bug: T368053 Change-Id: Ie258e49f5df8db23a7344ac3c4c9300aaa991042
1297 lines
39 KiB
JavaScript
1297 lines
39 KiB
JavaScript
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 templateFoldingExtension = require( './codemirror.mediawiki.templateFolding.js' );
|
|
|
|
/**
|
|
* 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 );
|
|
* cm.initialize( [ cm.defaultExtensions, mediawikiLang() ] );
|
|
* } );
|
|
*/
|
|
class CodeMirrorModeMediaWiki {
|
|
/**
|
|
* @param {Object} config MediaWiki configuration as generated by DataScript.php
|
|
* @internal
|
|
*/
|
|
constructor( config ) {
|
|
this.config = config;
|
|
|
|
this.urlProtocols = new RegExp( `^(?:${ this.config.urlProtocols })(?=[^\\s\u00a0{[\\]<>~).,'])`, 'i' );
|
|
this.isBold = false;
|
|
this.wasBold = false;
|
|
this.isItalic = false;
|
|
this.wasItalic = false;
|
|
this.firstSingleLetterWord = null;
|
|
this.firstMultiLetterWord = null;
|
|
this.firstSpace = null;
|
|
this.oldStyle = null;
|
|
this.tokens = [];
|
|
this.oldTokens = [];
|
|
this.tokenTable = mwModeConfig.tokenTable;
|
|
this.registerGroundTokens();
|
|
|
|
// Dynamically register any tags that aren't already in CodeMirrorModeMediaWikiConfig
|
|
Object.keys( this.config.tags ).forEach( ( tag ) => mwModeConfig.addTag( tag ) );
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
makeStyle( style, state, endGround ) {
|
|
if ( this.isBold || state.nDt > 0 ) {
|
|
style += ' ' + mwModeConfig.tags.strong;
|
|
}
|
|
if ( this.isItalic ) {
|
|
style += ' ' + mwModeConfig.tags.em;
|
|
}
|
|
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 ) {
|
|
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.nLink--;
|
|
// @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.nLink--;
|
|
// @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, 'nLink' );
|
|
}
|
|
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.nLink--;
|
|
// @todo error message
|
|
state.tokenize = state.stack.pop();
|
|
return;
|
|
}
|
|
if ( stream.eat( ']' ) ) {
|
|
state.tokenize = state.stack.pop();
|
|
return this.makeLocalStyle( mwModeConfig.tags.extLinkBracket, state, 'nLink' );
|
|
}
|
|
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 ) {
|
|
return ( stream, state ) => {
|
|
|
|
if ( stream.match( /^(?:"[^<">]*"|'[^<'>]*'|[^>/<{&~])+/ ) ) {
|
|
return this.makeLocalStyle( mwModeConfig.tags.htmlTagAttribute, 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 );
|
|
}
|
|
return this.eatWikiText( mwModeConfig.tags.htmlTagAttribute )( stream, state );
|
|
};
|
|
}
|
|
|
|
eatNowiki() {
|
|
return ( stream ) => {
|
|
if ( stream.match( /^[^&]+/ ) ) {
|
|
return '';
|
|
}
|
|
// eat &
|
|
stream.next();
|
|
return this.eatHtmlEntity( stream, '' );
|
|
};
|
|
}
|
|
|
|
eatExtTagAttribute( name ) {
|
|
return ( stream, state ) => {
|
|
|
|
if ( stream.match( /^(?:"[^">]*"|'[^'>]*'|[^>/<{&~])+/ ) ) {
|
|
return this.makeLocalStyle( `${ mwModeConfig.tags.extTagAttribute } mw-ext-${ name }`, 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 );
|
|
}
|
|
return this.eatWikiText( `${ mwModeConfig.tags.extTagAttribute } mw-ext-${ name }` )( stream, 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 ) {
|
|
return ( 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 );
|
|
};
|
|
}
|
|
|
|
eatStartTable( stream, state ) {
|
|
stream.match( '{|' );
|
|
stream.eatSpace();
|
|
state.tokenize = this.inTableDefinition.bind( this );
|
|
return mwModeConfig.tags.tableBracket;
|
|
}
|
|
|
|
inTableDefinition( stream, state ) {
|
|
if ( stream.sol() ) {
|
|
state.tokenize = this.inTable.bind( this );
|
|
return this.inTable( stream, state );
|
|
}
|
|
return this.eatWikiText( mwModeConfig.tags.tableDefinition )( stream, state );
|
|
}
|
|
|
|
inTable( stream, state ) {
|
|
if ( stream.sol() ) {
|
|
stream.eatSpace();
|
|
if ( stream.eat( '|' ) ) {
|
|
if ( stream.eat( '-' ) ) {
|
|
stream.eatSpace();
|
|
state.tokenize = this.inTableDefinition.bind( this );
|
|
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( '!!' ) ) ) {
|
|
this.isBold = false;
|
|
this.isItalic = false;
|
|
state.tokenize = this.eatTableRow( true, isHead, isCaption );
|
|
return this.makeLocalStyle( mwModeConfig.tags.tableDelimiter, state );
|
|
}
|
|
if ( isStart && stream.eat( '|' ) ) {
|
|
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.eol() ) {
|
|
// @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( /^[*#;:]*/u );
|
|
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 ) => {
|
|
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 ) {
|
|
// 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( /^:*{\|/, false ) ) {
|
|
state.stack.push( state.tokenize );
|
|
state.tokenize = this.eatStartTable.bind( this );
|
|
}
|
|
return this.eatList( stream, state );
|
|
case ' ':
|
|
// Leading spaces is valid syntax for tables, bug T108454
|
|
if ( stream.match( /^[\s\u00a0]*:*{\|/, false ) ) {
|
|
stream.eatSpace();
|
|
if ( stream.match( /^:+/ ) ) { // ::{|
|
|
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.bind( this );
|
|
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 ( !( this.firstSingleLetterWord || stream.match( '\'\'', false ) ) ) {
|
|
this.prepareItalicForCorrection( stream );
|
|
}
|
|
this.isBold = !this.isBold;
|
|
return this.makeLocalStyle( mwModeConfig.tags.apostrophesBold, state );
|
|
} else if ( stream.eat( '\'' ) ) { // italic
|
|
this.isItalic = !this.isItalic;
|
|
return this.makeLocalStyle( mwModeConfig.tags.apostrophesItalic, 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.nLink++;
|
|
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( /^([^\s\u00a0}[\]<{'|&:]+)(:|[\s\u00a0]*)(\}\}?)?(.)?/ );
|
|
if ( name ) {
|
|
stream.backUp( name[ 0 ].length );
|
|
if (
|
|
( name[ 2 ] === ':' || name[ 4 ] === undefined || name[ 3 ] === '}}' ) &&
|
|
(
|
|
name[ 1 ].toLowerCase() in this.config.functionSynonyms[ 0 ] ||
|
|
name[ 1 ] in this.config.functionSynonyms[ 1 ]
|
|
)
|
|
) {
|
|
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 '<':
|
|
isCloseTag = !!stream.eat( '/' );
|
|
tagname = stream.match( /^[^>/\s\u00a0.*,[\]{}$^+?|/\\'`~<=!@#%&()-]+/ );
|
|
if ( stream.match( '!--' ) ) { // comment
|
|
return chain( this.eatBlock( mwModeConfig.tags.comment, '-->' ) );
|
|
}
|
|
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
|
|
* @private
|
|
*/
|
|
prepareItalicForCorrection( stream ) {
|
|
// See Parser::doQuotes() in MediaWiki Core, it works similarly.
|
|
// this.firstSingleLetterWord has maximum priority
|
|
// this.firstMultiLetterWord has medium priority
|
|
// this.firstSpace has low priority
|
|
const end = stream.pos,
|
|
str = stream.string.slice( 0, end - 3 ),
|
|
x1 = str.slice( -1 ),
|
|
x2 = str.slice( -2, -1 );
|
|
|
|
// this.firstSingleLetterWord always is undefined here
|
|
if ( x1 === ' ' ) {
|
|
if ( this.firstMultiLetterWord || this.firstSpace ) {
|
|
return;
|
|
}
|
|
this.firstSpace = end;
|
|
} else if ( x2 === ' ' ) {
|
|
this.firstSingleLetterWord = end;
|
|
} else if ( this.firstMultiLetterWord ) {
|
|
return;
|
|
} else {
|
|
this.firstMultiLetterWord = end;
|
|
}
|
|
// remember bold and italic state for later restoration
|
|
this.wasBold = this.isBold;
|
|
this.wasItalic = this.isItalic;
|
|
}
|
|
|
|
/**
|
|
* @see https://codemirror.net/docs/ref/#language.StreamParser
|
|
* @return {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,
|
|
nExt: 0,
|
|
nDt: 0
|
|
} ),
|
|
|
|
/**
|
|
* Copies the given state.
|
|
*
|
|
* @param {Object} state
|
|
* @return {Object}
|
|
* @private
|
|
*/
|
|
copyState: ( state ) => ( {
|
|
tokenize: state.tokenize,
|
|
stack: state.stack.concat( [] ),
|
|
inHtmlTag: state.inHtmlTag.concat( [] ),
|
|
extName: state.extName,
|
|
extMode: state.extMode,
|
|
extState: state.extMode !== false && state.extMode.copyState( state.extState ),
|
|
nTemplate: state.nTemplate,
|
|
nLink: state.nLink,
|
|
nExt: state.nExt,
|
|
nDt: state.nDt
|
|
} ),
|
|
|
|
/**
|
|
* 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 ) => {
|
|
let style, p, t, f,
|
|
readyTokens = [],
|
|
tmpTokens = [];
|
|
|
|
if ( this.oldTokens.length > 0 ) {
|
|
// just send saved tokens till they exists
|
|
t = this.oldTokens.shift();
|
|
stream.pos = t.pos;
|
|
state = t.state;
|
|
return t.style;
|
|
}
|
|
|
|
if ( stream.sol() ) {
|
|
// reset bold and italic status in every new line
|
|
state.nDt = 0;
|
|
this.isBold = false;
|
|
this.isItalic = false;
|
|
this.firstSingleLetterWord = null;
|
|
this.firstMultiLetterWord = null;
|
|
this.firstSpace = null;
|
|
}
|
|
|
|
do {
|
|
// get token style
|
|
style = state.tokenize( stream, state );
|
|
f = this.firstSingleLetterWord || this.firstMultiLetterWord || this.firstSpace;
|
|
if ( f ) {
|
|
// rollback point exists
|
|
if ( f !== p ) {
|
|
// new rollback point
|
|
p = f;
|
|
// it's not first rollback point
|
|
if ( tmpTokens.length > 0 ) {
|
|
// save tokens
|
|
readyTokens = readyTokens.concat( tmpTokens );
|
|
tmpTokens = [];
|
|
}
|
|
}
|
|
// save token
|
|
tmpTokens.push( {
|
|
pos: stream.pos,
|
|
style,
|
|
state: ( state.extMode || this.mediawiki ).copyState( state )
|
|
} );
|
|
} else {
|
|
// rollback point does not exist
|
|
// remember style before possible rollback point
|
|
this.oldStyle = style;
|
|
// just return token style
|
|
return style;
|
|
}
|
|
} while ( !stream.eol() );
|
|
|
|
if ( this.isBold && this.isItalic ) {
|
|
// needs to rollback
|
|
// restore status
|
|
this.isItalic = this.wasItalic;
|
|
this.isBold = this.wasBold;
|
|
this.firstSingleLetterWord = null;
|
|
this.firstMultiLetterWord = null;
|
|
this.firstSpace = null;
|
|
if ( readyTokens.length > 0 ) {
|
|
// it contains tickets before the point of rollback
|
|
// add one apostrophe, next token will be italic (two apostrophes)
|
|
readyTokens[ readyTokens.length - 1 ].pos++;
|
|
// for sending tokens till the point of rollback
|
|
this.oldTokens = readyTokens;
|
|
} else {
|
|
// there are no tickets before the point of rollback
|
|
stream.pos = tmpTokens[ 0 ].pos - 2; // eat( '\'')
|
|
// send saved Style
|
|
return this.oldStyle;
|
|
}
|
|
} else {
|
|
// do not need to rollback
|
|
// send all saved tokens
|
|
this.oldTokens = readyTokens.concat( tmpTokens );
|
|
}
|
|
// return first saved token
|
|
t = this.oldTokens.shift();
|
|
stream.pos = t.pos;
|
|
state = t.state;
|
|
return t.style;
|
|
},
|
|
|
|
/**
|
|
* @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
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 {Object|null} [mwConfig] Ignore; used only by unit tests.
|
|
* @return {LanguageSupport}
|
|
* @stable to call
|
|
*/
|
|
const mediaWikiLang = ( config = { bidiIsolation: false }, mwConfig = null ) => {
|
|
mwConfig = mwConfig || mw.config.get( 'extCodeMirrorConfig' );
|
|
const mode = new CodeMirrorModeMediaWiki( mwConfig );
|
|
const parser = mode.mediawiki;
|
|
const lang = StreamLanguage.define( parser );
|
|
const langExtension = [ syntaxHighlighting(
|
|
HighlightStyle.define(
|
|
mwModeConfig.getTagStyles( parser )
|
|
)
|
|
) ];
|
|
|
|
// Add template folding if in supported namespace.
|
|
const templateFoldingNs = mwConfig.templateFoldingNamespaces;
|
|
// Set to [] to disable everywhere, or null to enable everywhere.
|
|
if ( !templateFoldingNs || templateFoldingNs.includes( mw.config.get( 'wgNamespaceNumber' ) ) ) {
|
|
langExtension.push( templateFoldingExtension );
|
|
}
|
|
|
|
// Bundle the bidi isolation extension, as it's coded specifically for MediaWiki.
|
|
// This is behind a config option for performance reasons (we only use it on RTL pages).
|
|
if ( config.bidiIsolation ) {
|
|
langExtension.push( bidiIsolationExtension );
|
|
}
|
|
|
|
return new LanguageSupport( lang, langExtension );
|
|
};
|
|
|
|
module.exports = mediaWikiLang;
|