Merge "CodeMirror 6: Add bidi isolation to HTML tags"

This commit is contained in:
jenkins-bot 2024-03-12 18:47:22 +00:00 committed by Gerrit Code Review
commit 6281f799c8
7 changed files with 163 additions and 4 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -14,3 +14,9 @@
line-height: 1.2;
padding: 0 1px;
}
.cm-bidi-isolate {
/* @noflip */
direction: ltr;
unicode-bidi: isolate;
}

View file

@ -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,

View 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 );

View file

@ -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() );
}

View 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">&lt;</span><span class="cm-mw-htmltag-name">span </span><span class="cm-mw-htmltag-attribute">class="foobar"</span><span class="cm-mw-htmltag-bracket">&gt;</span></span>שלום<span class="cm-bidi-isolate"><span class="cm-mw-htmltag-bracket">&lt;/</span><span class="cm-mw-htmltag-name">span</span><span class="cm-mw-htmltag-bracket">&gt;</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 );
}
);
} );