mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/CodeMirror
synced 2024-11-23 22:03:28 +00:00
13c9eae26e
This is toggled by pressing Mod-Shift-, (or Command-Shift-, on MacOS), which then puts focus on the preferences panel. It can be closed with the Escape key, just like other CM panels. The CodeMirror class comes with these extension which can be toggled in preferences: * Bracket matching * Line numbering * Line wrapping * Highlight the active line * Show special characters Only bracket matching, line numbering, and line wrapping are available in the 2017 editor. The bidi isolation and template folding extensions are registered in CodeMirrorModeMediaWiki as they are MW-specific. CodeMirrorPreferences' new registerExtension() method allows any consumer of CodeMirror to add any arbitrary extensions to the preferences panel. This is expected to be called *after* CodeMirror has finished initializing. The 'ext.CodeMirror.ready' hook now passes the CodeMirror instance to accommodate this. The preferences are stored as a single user option in the database, called 'codemirror-preferences'. The defaults can be configured with the $wgCodeMirrorDefaultPreferences configuration setting. The sysadmin-facing values are the familiar boolean, but since CodeMirror is widely used, we make extra efforts to reduce the storage footprint (see T54777). This includes only storing preferences that differ from the defaults, and using binary representation instead of boolean values, since the user option is stored as a string. For now, all preferences are ignored in the 2017 editor. In a future patch, we may add some as toggleable Tools in the VE toolbar. Other changes: * Refactor CSS to use a .darkmode() mixin * Add a method to create a CSS-only fieldset in CodeMirrorPanel * Fix Jest tests now that there are more calls to mw.user.options.get() * Adjust Selenium tests to always use CM6 * Adjust Selenium tests to delete test pages (useful for local dev) * Remove unused code Bug: T359498 Change-Id: I70dcf2f49418cea632c452c1266440effad634f3
1304 lines
39 KiB
JavaScript
1304 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.sol() ) {
|
|
// @todo error message
|
|
} else if ( stream.match( /^[^\s\u00a0{[\]<>~).,']*/ ) ) {
|
|
if ( stream.peek() === '~' ) {
|
|
if ( !stream.match( /^~~~+/, false ) ) {
|
|
stream.match( /^~*/ );
|
|
return this.makeLocalStyle( mwModeConfig.tags.freeExtLink, state );
|
|
}
|
|
} else if ( stream.peek() === '{' ) {
|
|
if ( !stream.match( '{{', false ) ) {
|
|
stream.next();
|
|
return this.makeLocalStyle( mwModeConfig.tags.freeExtLink, state );
|
|
}
|
|
} else if ( stream.peek() === '\'' ) {
|
|
if ( !stream.match( '\'\'', false ) ) {
|
|
stream.next();
|
|
return this.makeLocalStyle( mwModeConfig.tags.freeExtLink, state );
|
|
}
|
|
} else if ( stream.match( /^[).,]+(?=[^\s\u00a0{[\]<>~).,])/ ) ) {
|
|
return this.makeLocalStyle( mwModeConfig.tags.freeExtLink, state );
|
|
}
|
|
}
|
|
state.tokenize = state.stack.pop();
|
|
return this.makeLocalStyle( mwModeConfig.tags.freeExtLink, state );
|
|
}
|
|
|
|
eatList( stream, state ) {
|
|
// Just consume all nested list and indention syntax when there is more
|
|
const mt = stream.match( /^[*#;:]*/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( /^:*[\s\u00a0]*(?={\|)/ ) ) {
|
|
state.stack.push( state.tokenize );
|
|
state.tokenize = this.eatStartTable.bind( this );
|
|
return mwModeConfig.tags.indenting;
|
|
}
|
|
return this.eatList( stream, state );
|
|
case ' ':
|
|
// Leading spaces is valid syntax for tables, bug T108454
|
|
if ( stream.match( /^[\s\u00a0]*(?::+[\s\u00a0]*)?{\|/, false ) ) {
|
|
stream.eatSpace();
|
|
if ( stream.match( /^:+/ ) ) { // ::{|
|
|
stream.eatSpace();
|
|
state.stack.push( state.tokenize );
|
|
state.tokenize = this.eatStartTable.bind( this );
|
|
return mwModeConfig.tags.indenting;
|
|
}
|
|
stream.eat( '{' );
|
|
} else {
|
|
return mwModeConfig.tags.skipFormatting;
|
|
}
|
|
// break is not necessary here
|
|
// falls through
|
|
case '{':
|
|
if ( stream.eat( '|' ) ) {
|
|
stream.eatSpace();
|
|
state.stack.push( state.tokenize );
|
|
state.tokenize = this.inTableDefinition.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( /^([^}[\]<{|:]+)(.)?/, false );
|
|
if ( name ) {
|
|
const [ , f, delimiter ] = name,
|
|
ff = delimiter === ':' ? f : f.trim(),
|
|
ffLower = ff.toLowerCase(),
|
|
{ config: { functionSynonyms } } = this;
|
|
if (
|
|
( !delimiter || delimiter === ':' || delimiter === '}' ) &&
|
|
(
|
|
Object.prototype.hasOwnProperty.call(
|
|
functionSynonyms[ 0 ], ffLower
|
|
) ||
|
|
Object.prototype.hasOwnProperty.call(
|
|
functionSynonyms[ 1 ], ff
|
|
)
|
|
)
|
|
) {
|
|
state.nExt++;
|
|
state.stack.push( state.tokenize );
|
|
state.tokenize = this.inParserFunctionName.bind( this );
|
|
return this.makeLocalStyle(
|
|
mwModeConfig.tags.parserFunctionBracket,
|
|
state
|
|
);
|
|
}
|
|
}
|
|
// Template
|
|
state.nTemplate++;
|
|
state.stack.push( state.tokenize );
|
|
state.tokenize = this.eatTemplatePageName( false );
|
|
return this.makeLocalStyle( mwModeConfig.tags.templateBracket, state );
|
|
}
|
|
break;
|
|
case '<':
|
|
if ( stream.match( '!--' ) ) { // comment
|
|
return chain( this.eatBlock( mwModeConfig.tags.comment, '-->' ) );
|
|
}
|
|
isCloseTag = !!stream.eat( '/' );
|
|
tagname = stream.match( /^[a-z][^>/\s\u00a0]*/i );
|
|
if ( tagname ) {
|
|
tagname = tagname[ 0 ].toLowerCase();
|
|
if ( tagname in this.config.tags ) {
|
|
// Parser function
|
|
if ( isCloseTag === true ) {
|
|
return mwModeConfig.tags.error;
|
|
}
|
|
stream.backUp( tagname.length );
|
|
state.stack.push( state.tokenize );
|
|
state.tokenize = this.eatTagName( tagname.length, isCloseTag, false );
|
|
return this.makeLocalStyle( `${ mwModeConfig.tags.extTagBracket } mw-ext-${ tagname }`, state );
|
|
}
|
|
if ( tagname in mwModeConfig.permittedHtmlTags ) {
|
|
// Html tag
|
|
if ( isCloseTag === true && tagname !== state.inHtmlTag.pop() ) {
|
|
// Increment position so that the closing '>' gets highlighted red.
|
|
stream.pos++;
|
|
return mwModeConfig.tags.error;
|
|
}
|
|
if (
|
|
isCloseTag === true &&
|
|
tagname in mwModeConfig.implicitlyClosedHtmlTags
|
|
) {
|
|
return mwModeConfig.tags.error;
|
|
}
|
|
stream.backUp( tagname.length );
|
|
state.stack.push( state.tokenize );
|
|
state.tokenize = this.eatTagName(
|
|
tagname.length,
|
|
// Opening void tags should also be treated as the closing tag.
|
|
isCloseTag ||
|
|
( tagname in mwModeConfig.implicitlyClosedHtmlTags ),
|
|
true
|
|
);
|
|
return this.makeLocalStyle( mwModeConfig.tags.htmlTagBracket, state );
|
|
}
|
|
stream.backUp( tagname.length );
|
|
}
|
|
break;
|
|
case '~':
|
|
if ( stream.match( /^~{2,4}/ ) ) {
|
|
return mwModeConfig.tags.signature;
|
|
}
|
|
break;
|
|
// Maybe double underscored Magic Word such as __TOC__
|
|
case '_':
|
|
tmp = 1;
|
|
// Optimize processing of many underscore symbols
|
|
while ( stream.eat( '_' ) ) {
|
|
tmp++;
|
|
}
|
|
// Many underscore symbols
|
|
if ( tmp > 2 ) {
|
|
if ( !stream.eol() ) {
|
|
// Leave last two underscore symbols for processing in next iteration
|
|
stream.backUp( 2 );
|
|
}
|
|
// Optimization: skip regex function for EOL and backup-ed symbols
|
|
return this.makeStyle( style, state );
|
|
// Check on double underscore Magic Word
|
|
} else if ( tmp === 2 ) {
|
|
// The same as the end of function except '_' inside and '__' at the end.
|
|
name = stream.match( /^([^\s\u00a0>}[\]<{'|&:~]+?)__/ );
|
|
if ( name && name[ 0 ] ) {
|
|
if (
|
|
'__' + name[ 0 ].toLowerCase() in this.config.doubleUnderscore[ 0 ] ||
|
|
'__' + name[ 0 ] in this.config.doubleUnderscore[ 1 ]
|
|
) {
|
|
return mwModeConfig.tags.doubleUnderscore;
|
|
}
|
|
if ( !stream.eol() ) {
|
|
// Two underscore symbols at the end can be the
|
|
// beginning of another double underscored Magic Word
|
|
stream.backUp( 2 );
|
|
}
|
|
// Optimization: skip regex for EOL and backup-ed symbols
|
|
return this.makeStyle( style, state );
|
|
}
|
|
}
|
|
break;
|
|
case ':':
|
|
if ( state.nDt > 0 && !this.isNested( state ) ) {
|
|
state.nDt--;
|
|
return mwModeConfig.tags.indenting;
|
|
}
|
|
break;
|
|
default:
|
|
if ( /[\s\u00a0]/.test( ch ) ) {
|
|
stream.eatSpace();
|
|
// highlight free external links, bug T108448
|
|
if ( stream.match( this.urlProtocols, false ) && !stream.match( '//' ) ) {
|
|
state.stack.push( state.tokenize );
|
|
state.tokenize = this.eatFreeExternalLinkProtocol.bind( this );
|
|
return this.makeStyle( style, state );
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
stream.match( /^[^\s\u00a0_>}[\]<{'|&:~=]+/ );
|
|
return this.makeStyle( style, state );
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Remembers position and status for rollbacking.
|
|
* It is needed for changing from bold to italic with apostrophes before it, if required.
|
|
*
|
|
* @see https://phabricator.wikimedia.org/T108455
|
|
*
|
|
* @param {StringStream} stream
|
|
* @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 {boolean} [config.templateFolding=true] Enable template folding.
|
|
* @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 )
|
|
)
|
|
) ];
|
|
|
|
// Register MW-specific Extensions into CodeMirror preferences. Whether they are enabled
|
|
// or not is determined by the user's preferences and wiki configuration.
|
|
mw.hook( 'ext.CodeMirror.ready' ).add( ( $textarea, cm ) => {
|
|
if ( config.templateFolding !== false ) {
|
|
cm.preferences.registerExtension( 'templateFolding', templateFoldingExtension, cm.view );
|
|
}
|
|
if ( config.bidiIsolation ) {
|
|
cm.preferences.registerExtension( 'bidiIsolation', bidiIsolationExtension, cm.view );
|
|
}
|
|
} );
|
|
|
|
return new LanguageSupport( lang, langExtension );
|
|
};
|
|
|
|
module.exports = mediaWikiLang;
|