mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/CodeMirror
synced 2024-11-23 22:03:28 +00:00
Merge "CodeMirror 6: Add bidi isolation to HTML tags"
This commit is contained in:
commit
6281f799c8
2
resources/dist/main.js
vendored
2
resources/dist/main.js
vendored
File diff suppressed because one or more lines are too long
2
resources/dist/main.js.map.json
vendored
2
resources/dist/main.js.map.json
vendored
File diff suppressed because one or more lines are too long
|
@ -14,3 +14,9 @@
|
|||
line-height: 1.2;
|
||||
padding: 0 1px;
|
||||
}
|
||||
|
||||
.cm-bidi-isolate {
|
||||
/* @noflip */
|
||||
direction: ltr;
|
||||
unicode-bidi: isolate;
|
||||
}
|
||||
|
|
|
@ -14,7 +14,8 @@
|
|||
"commonjs": true
|
||||
},
|
||||
"globals": {
|
||||
"__non_webpack_require__": "readonly"
|
||||
"__non_webpack_require__": "readonly",
|
||||
"Tree": "readonly"
|
||||
},
|
||||
"rules": {
|
||||
"es-x/no-array-prototype-includes": 0,
|
||||
|
|
109
src/codemirror.bidiIsolation.js
Normal file
109
src/codemirror.bidiIsolation.js
Normal file
|
@ -0,0 +1,109 @@
|
|||
import {
|
||||
Decoration,
|
||||
DecorationSet,
|
||||
Direction,
|
||||
EditorView,
|
||||
PluginSpec,
|
||||
ViewPlugin,
|
||||
ViewUpdate
|
||||
} from '@codemirror/view';
|
||||
import { Prec, RangeSet, RangeSetBuilder } from '@codemirror/state';
|
||||
import { syntaxTree } from '@codemirror/language';
|
||||
import { mwModeConfig } from './codemirror.mode.mediawiki.config';
|
||||
|
||||
/**
|
||||
* @type {Decoration}
|
||||
*/
|
||||
const isolate = Decoration.mark( {
|
||||
class: 'cm-bidi-isolate',
|
||||
bidiIsolate: Direction.LTR
|
||||
} );
|
||||
|
||||
/**
|
||||
* @param {EditorView} view
|
||||
* @return {RangeSet}
|
||||
*/
|
||||
function computeIsolates( view ) {
|
||||
const set = new RangeSetBuilder();
|
||||
|
||||
for ( const { from, to } of view.visibleRanges ) {
|
||||
let startPos;
|
||||
syntaxTree( view.state ).iterate( {
|
||||
from,
|
||||
to,
|
||||
enter( node ) {
|
||||
// Determine if this is a bracket node (start or end of a tag).
|
||||
const isBracket = node.name.split( '_' )
|
||||
.some( ( tag ) => [
|
||||
mwModeConfig.tags.htmlTagBracket,
|
||||
mwModeConfig.tags.extTagBracket
|
||||
].includes( tag ) );
|
||||
|
||||
if ( !startPos && isBracket ) {
|
||||
// If we find a bracket node, we keep track of the start position.
|
||||
startPos = node.from;
|
||||
} else if ( isBracket ) {
|
||||
// When we find the closing bracket, add the isolate.
|
||||
set.add( startPos, node.to, isolate );
|
||||
startPos = null;
|
||||
}
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
return set.finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* @class
|
||||
* @property {DecorationSet} isolates
|
||||
* @property {Tree} tree
|
||||
*/
|
||||
class CodeMirrorBidiIsolation {
|
||||
/**
|
||||
* @constructor
|
||||
* @param {EditorView} view
|
||||
*/
|
||||
constructor( view ) {
|
||||
this.isolates = computeIsolates( view );
|
||||
this.tree = syntaxTree( view.state );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ViewUpdate} update
|
||||
*/
|
||||
update( update ) {
|
||||
if ( update.docChanged || update.viewportChanged ||
|
||||
syntaxTree( update.state ) !== this.tree
|
||||
) {
|
||||
this.isolates = computeIsolates( update.view );
|
||||
this.tree = syntaxTree( update.state );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {PluginSpec}
|
||||
*/
|
||||
const bidiIsolationSpec = {
|
||||
provide: ( plugin ) => {
|
||||
/**
|
||||
* @param {EditorView} view
|
||||
* @return {DecorationSet}
|
||||
*/
|
||||
const access = ( view ) => {
|
||||
return view.plugin( plugin ) ?
|
||||
( view.plugin( plugin ).isolates || Decoration.none ) :
|
||||
Decoration.none;
|
||||
};
|
||||
|
||||
// Use the lowest precedence to ensure that other decorations
|
||||
// don't break up the isolating decorations.
|
||||
return Prec.lowest( [
|
||||
EditorView.decorations.of( access ),
|
||||
EditorView.bidiIsolatedRanges.of( access )
|
||||
] );
|
||||
}
|
||||
};
|
||||
|
||||
export default ViewPlugin.fromClass( CodeMirrorBidiIsolation, bidiIsolationSpec );
|
|
@ -1,5 +1,6 @@
|
|||
import { EditorSelection, EditorState, Extension } from '@codemirror/state';
|
||||
import { EditorView, lineNumbers, highlightSpecialChars } from '@codemirror/view';
|
||||
import bidiIsolationExtension from './codemirror.bidiIsolation';
|
||||
|
||||
/**
|
||||
* @class CodeMirror
|
||||
|
@ -33,9 +34,14 @@ export default class CodeMirror {
|
|||
this.specialCharsExtension,
|
||||
this.heightExtension
|
||||
];
|
||||
const namespaces = mw.config.get( 'wgCodeMirrorLineNumberingNamespaces' );
|
||||
|
||||
// Add bidi isolation to tags on RTL pages (T358804).
|
||||
if ( this.$textarea.attr( 'dir' ) === 'rtl' ) {
|
||||
extensions.push( bidiIsolationExtension );
|
||||
}
|
||||
|
||||
// Set to [] to disable everywhere, or null to enable everywhere
|
||||
const namespaces = mw.config.get( 'wgCodeMirrorLineNumberingNamespaces' );
|
||||
if ( !namespaces || namespaces.includes( mw.config.get( 'wgNamespaceNumber' ) ) ) {
|
||||
extensions.push( lineNumbers() );
|
||||
}
|
||||
|
|
37
tests/jest/codemirror.bidiIsolation.test.js
Normal file
37
tests/jest/codemirror.bidiIsolation.test.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
import CodeMirror from '../../src/codemirror.js';
|
||||
import { mediaWikiLang } from '../../src/codemirror.mode.mediawiki.js';
|
||||
|
||||
const testCases = [
|
||||
{
|
||||
title: 'wraps HTML tags with span.cm-bidi-isolate',
|
||||
input: 'שלום<span class="foobar">שלום</span>שלום',
|
||||
output: '<div class="cm-line">שלום<span class="cm-bidi-isolate"><span class="cm-mw-htmltag-bracket"><</span><span class="cm-mw-htmltag-name">span </span><span class="cm-mw-htmltag-attribute">class="foobar"</span><span class="cm-mw-htmltag-bracket">></span></span>שלום<span class="cm-bidi-isolate"><span class="cm-mw-htmltag-bracket"></</span><span class="cm-mw-htmltag-name">span</span><span class="cm-mw-htmltag-bracket">></span></span>שלום</div>'
|
||||
}
|
||||
];
|
||||
|
||||
// Setup CodeMirror instance.
|
||||
const textarea = document.createElement( 'textarea' );
|
||||
textarea.dir = 'rtl';
|
||||
document.body.appendChild( textarea );
|
||||
const cm = new CodeMirror( textarea );
|
||||
const mwLang = mediaWikiLang( {
|
||||
tags: {}
|
||||
} );
|
||||
cm.initialize( [ ...cm.defaultExtensions, mwLang ] );
|
||||
|
||||
describe( 'CodeMirrorBidiIsolation', () => {
|
||||
it.each( testCases )(
|
||||
'bidi isolation ($title)',
|
||||
( { input, output } ) => {
|
||||
cm.view.dispatch( {
|
||||
changes: {
|
||||
from: 0,
|
||||
to: cm.view.state.doc.length,
|
||||
insert: input
|
||||
}
|
||||
} );
|
||||
cm.$textarea.textSelection = jest.fn().mockReturnValue( input );
|
||||
expect( cm.view.dom.querySelector( '.cm-content' ).innerHTML ).toStrictEqual( output );
|
||||
}
|
||||
);
|
||||
} );
|
Loading…
Reference in a new issue