diff --git a/extension.json b/extension.json index 5c73a5f4..907c3172 100644 --- a/extension.json +++ b/extension.json @@ -1,6 +1,6 @@ { "name": "CodeMirror", - "version": "3.1.14", + "version": "3.2", "author": [ "[https://www.mediawiki.org/wiki/User:Pastakhov Pavel Astakhov]", "[https://www.mediawiki.org/wiki/User:Florianschmidtwelzow Florian Schmidt]" diff --git a/resources/mode/mediawiki/mediawiki.css b/resources/mode/mediawiki/mediawiki.css index 197d764d..aae23002 100644 --- a/resources/mode/mediawiki/mediawiki.css +++ b/resources/mode/mediawiki/mediawiki.css @@ -19,7 +19,7 @@ .cm-mw-indenting {color: #08f; font-weight: bold; background-color: #ddd;} .cm-mw-mnemonic {color: #090;} .cm-mw-comment {color: #aaa; font-weight: normal;} -.cm-mw-apostrophes {color: #08f;} +.cm-mw-apostrophes-bold, .cm-mw-apostrophes-italic {color: #08f;} pre.cm-mw-section-1 {font-size: 1.8em;} pre.cm-mw-section-2 {font-size: 1.5em;} diff --git a/resources/mode/mediawiki/mediawiki.js b/resources/mode/mediawiki/mediawiki.js index 0eca7f45..60b65fbc 100644 --- a/resources/mode/mediawiki/mediawiki.js +++ b/resources/mode/mediawiki/mediawiki.js @@ -22,23 +22,24 @@ function eatMnemonic( stream, style, mnemonicStyle ) { CodeMirror.defineMode( 'mediawiki', function( config/*, parserConfig */ ) { - var urlProtocols = new RegExp( config.mwextUrlProtocols, 'i' ); - var permittedHtmlTags = {'b': true, 'bdi': true, 'del': true, 'i': true, 'ins': true, - 'u': true, 'font': true, 'big': true, 'small': true, 'sub': true, 'sup': true, - 'h1': true, 'h2': true, 'h3': true, 'h4': true, 'h5': true, 'h6': true, 'cite': true, - 'code': true, 'em': true, 's': true, 'strike': true, 'strong': true, 'tt': true, - 'var': true, 'div': true, 'center': true, 'blockquote': true, 'ol': true, 'ul': true, - 'dl': true, 'table': true, 'caption': true, 'pre': true, 'ruby': true, 'rb': true, - 'rp': true, 'rt': true, 'rtc': true, 'p': true, 'span': true, 'abbr': true, 'dfn': true, - 'kbd': true, 'samp': true, 'data': true, 'time': true, 'mark': true, 'br': true, - 'wbr': true, 'hr': true, 'li': true, 'dt': true, 'dd': true, 'td': true, 'th': true, - 'tr': true, 'noinclude': true, 'includeonly': true, 'onlyinclude': true}; + var urlProtocols = new RegExp( config.mwextUrlProtocols, 'i' ), + permittedHtmlTags = {'b': true, 'bdi': true, 'del': true, 'i': true, 'ins': true, + 'u': true, 'font': true, 'big': true, 'small': true, 'sub': true, 'sup': true, + 'h1': true, 'h2': true, 'h3': true, 'h4': true, 'h5': true, 'h6': true, 'cite': true, + 'code': true, 'em': true, 's': true, 'strike': true, 'strong': true, 'tt': true, + 'var': true, 'div': true, 'center': true, 'blockquote': true, 'ol': true, 'ul': true, + 'dl': true, 'table': true, 'caption': true, 'pre': true, 'ruby': true, 'rb': true, + 'rp': true, 'rt': true, 'rtc': true, 'p': true, 'span': true, 'abbr': true, 'dfn': true, + 'kbd': true, 'samp': true, 'data': true, 'time': true, 'mark': true, 'br': true, + 'wbr': true, 'hr': true, 'li': true, 'dt': true, 'dd': true, 'td': true, 'th': true, + 'tr': true, 'noinclude': true, 'includeonly': true, 'onlyinclude': true}, + isBold, isItalic, firstsingleletterword, firstmultiletterword, firstspace, mBold, mItalic, mTokens = [], mStyle; function makeStyle( style, state, endGround ) { - if ( state.isBold ) { + if ( isBold ) { style += ' strong'; } - if ( state.isItalic ) { + if ( isItalic ) { style += ' em'; } return makeLocalStyle( style, state, endGround ); @@ -351,25 +352,25 @@ CodeMirror.defineMode( 'mediawiki', function( config/*, parserConfig */ ) { } function eatLinkText() { - var isBold, isItalic; + var linkIsBold, linkIsItalic; return function ( stream, state ) { if ( stream.match( ']]' ) ) { state.tokenize = state.stack.pop(); return makeLocalStyle( 'mw-link-bracket', state, 'nLink' ); } if ( stream.match( '\'\'\'' ) ) { - isBold = (isBold ? false : true); + linkIsBold = (linkIsBold ? false : true); return makeLocalStyle( 'mw-link-text mw-apostrophes', state ); } if ( stream.match( '\'\'' ) ) { - isItalic = (isItalic ? false : true); + linkIsItalic = (linkIsItalic ? false : true); return makeLocalStyle( 'mw-link-text mw-apostrophes', state ); } var tmpstyle = 'mw-link-text'; - if ( isBold ) { + if ( linkIsBold ) { tmpstyle += ' strong'; } - if ( isItalic ) { + if ( linkIsItalic ) { tmpstyle += ' em'; } if ( stream.match( /[^'\]\{\&~]+/ ) ) { @@ -457,9 +458,10 @@ CodeMirror.defineMode( 'mediawiki', function( config/*, parserConfig */ ) { function eatExtTagArea( name ) { return function( stream, state ) { - var origString = false, from = stream.pos, to; - var pattern = new RegExp( '' ); - var m = pattern.exec( from ? stream.string.slice( from ) : stream.string ); + var origString = false, from = stream.pos, to, + pattern = new RegExp( '' ), + m = pattern.exec( from ? stream.string.slice( from ) : stream.string ); + if ( m ) { if ( m.index === 0 ) { state.tokenize = eatExtCloseTag( name ); @@ -474,6 +476,7 @@ CodeMirror.defineMode( 'mediawiki', function( config/*, parserConfig */ ) { origString = stream.string; stream.string = origString.slice( 0, to ); } + state.stack.push( state.tokenize ); state.tokenize = eatExtTokens( origString ); return state.tokenize( stream, state ); @@ -525,13 +528,9 @@ CodeMirror.defineMode( 'mediawiki', function( config/*, parserConfig */ ) { } function inTableCaption( stream, state ) { - if ( stream.sol() ) { - state.isBold = false; - state.isItalic = false; - if ( stream.match( /[\s\u00a0]*[\|!]/, false ) ) { - state.tokenize = inTable; - return inTable( stream, state ); - } + if ( stream.sol() && stream.match( /[\s\u00a0]*[\|!]/, false ) ) { + state.tokenize = inTable; + return inTable( stream, state ); } return eatWikiText( 'mw-table-caption', '' )( stream, state ); } @@ -570,8 +569,6 @@ CodeMirror.defineMode( 'mediawiki', function( config/*, parserConfig */ ) { function eatTableRow( isStart, isHead ) { return function ( stream, state ) { if ( stream.sol() ) { - state.isBold = false; - state.isItalic = false; if ( stream.match( /[\s\u00a0]*[\|!]/, false ) ) { state.tokenize = inTable; return inTable( stream, state ); @@ -581,8 +578,8 @@ CodeMirror.defineMode( 'mediawiki', function( config/*, parserConfig */ ) { return makeStyle( (isHead ? 'strong' : ''), state ); } if ( stream.match( '||' ) || isHead && stream.match( '!!' ) || (isStart && stream.eat( '|' )) ) { - state.isBold = false; - state.isItalic = false; + isBold = false; + isItalic = false; if ( isStart ) { state.tokenize = eatTableRow( false, isHead ); } @@ -636,8 +633,6 @@ CodeMirror.defineMode( 'mediawiki', function( config/*, parserConfig */ ) { var ch, sol = stream.sol(); if ( sol ) { - state.isBold = false; - state.isItalic = false; if ( stream.match( urlProtocols ) ) { // highlight free external links, bug T108448 state.stack.push( state.tokenize ); state.tokenize = eatFreeExternalLink; @@ -704,12 +699,18 @@ CodeMirror.defineMode( 'mediawiki', function( config/*, parserConfig */ ) { case '&': return makeStyle( eatMnemonic( stream, style, mnemonicStyle ), state ); case '\'': - if ( stream.match( '\'\'' ) ) { - state.isBold = state.isBold ? false : true; - return makeLocalStyle( 'mw-apostrophes', state ); - } else if ( stream.eat( '\'' ) ) { - state.isItalic = state.isItalic ? false : true; - return makeLocalStyle( 'mw-apostrophes', state ); + if ( stream.match( /'*(?=''''')/ ) || stream.match( /'''(?!')/, false ) ) { // skip the irrelevant apostrophes ( >5 or =4 ) + break; + } + if ( stream.match( '\'\'' ) ) { // bold\ + if ( !(firstsingleletterword || stream.match( '\'\'', false )) ) { + prepareItalicForCorrection( stream ); + } + isBold = isBold ? false : true; + return makeLocalStyle( 'mw-apostrophes-bold', state ); + } else if ( stream.eat( '\'' ) ) { // italic + isItalic = isItalic ? false : true; + return makeLocalStyle( 'mw-apostrophes-italic', state ); } break; case '[': @@ -815,17 +816,50 @@ CodeMirror.defineMode( 'mediawiki', function( config/*, parserConfig */ ) { }; } + /** + * Remembers position and status for rollbacking. + * It needed for change bold to italic with apostrophe before it if required + * @see https://phabricator.wikimedia.org/T108455 + * @param CodeMirror.StringStream stream + * @returns null + */ + function prepareItalicForCorrection( stream ) { + // see Parser::doQuotes() in MediaWiki core, it works similar + // firstsingleletterword has maximum priority + // firstmultiletterword has medium priority + // firstspace has low priority + var end = stream.pos, + str = stream.string.substr( 0, end - 3 ), + x1 = str.substr( -1, 1 ), + x2 = str.substr( -2, 1 ); + + // firstsingleletterword olways is undefined here + if ( x1 === ' ' ) { + if ( firstmultiletterword || firstspace ) { + return; + } + firstspace = end; + } else if ( x2 === ' ' ) { + firstsingleletterword = end; + } else if ( firstmultiletterword ) { + return; + } else { + firstmultiletterword = end; + } + // remember bold and italic state for restore + mBold = isBold; + mItalic = isItalic; + } + return { startState: function() { - return { tokenize: eatWikiText('', ''), stack: [], InHtmlTag:[], isBold: false, isItalic: false, extName: false, extMode: false, extState: false, nTemplate: 0, nLink: 0, nExt: 0 }; + return { tokenize: eatWikiText('', ''), stack: [], InHtmlTag:[], extName: false, extMode: false, extState: false, nTemplate: 0, nLink: 0, nExt: 0 }; }, copyState: function( state ) { return { tokenize: state.tokenize, stack: state.stack.concat( [] ), InHtmlTag: state.InHtmlTag.concat( [] ), - isBold: state.isBold, - isItalic: state.isItalic, extName: state.extName, extMode: state.extMode, extState: state.extMode !== false && CodeMirror.copyState( state.extMode, state.extState ), @@ -835,7 +869,68 @@ CodeMirror.defineMode( 'mediawiki', function( config/*, parserConfig */ ) { }; }, token: function( stream, state ) { - return state.tokenize( stream, state ); + var style, p, t, f, + readyTokens = [], + tmpTokens = []; + + if ( mTokens.length > 0 ) { // just send saved tokens till they exists + t = mTokens.shift(); + stream.pos = t.pos; + state = t.state; + return t.style; + } + + if ( stream.sol() ) { // reset bold and italic status in every new line + isBold = false; + isItalic = false; + firstsingleletterword = undefined; + firstmultiletterword = undefined; + firstspace = undefined; + } + + do { + style = state.tokenize( stream, state ); // get token style + f = firstsingleletterword || firstmultiletterword || firstspace; + if ( f ) { // rollback point exists + if ( f !== p ) { // new rollbak point + p = f; + if ( tmpTokens.length > 0 ) { // it's not first rollbak point + readyTokens = readyTokens.concat( tmpTokens ); // save tokens + tmpTokens = []; + } + } + tmpTokens.push( { // save token + pos: stream.pos, + style: style, + state: CodeMirror.copyState( state.extMode ? state.extMode : 'mediawiki', state ) + } ); + } else { // rollback point not exists + mStyle = style; // remember style before possible rollback point + return style; // just return token style + } + } while ( !stream.eol() ); + + if ( isBold && isItalic ) { // needs to rollback + isItalic = mItalic; // restore status + isBold = mBold; + firstsingleletterword = undefined; + firstmultiletterword = undefined; + firstspace = undefined; + if ( readyTokens.length > 0 ) { // it contains tickets before the point of rollback + readyTokens[readyTokens.length-1].pos++; // add one apostrophe, next token will be italic (two apostrophes) + mTokens = readyTokens; // for sending tokens till the point of rollback + } else { // there are no tikets before the point of rollback + stream.pos = tmpTokens[0].pos - 2; // eat( '\'') + return mStyle; // send saved Style + } + } else { // not needs to rollback + mTokens = readyTokens.concat( tmpTokens ); // send all saved tokens + } + // return first saved token + t = mTokens.shift(); + stream.pos = t.pos; + state = t.state; + return t.style; }, blankLine: function( state ) { if ( state.extName ) {