Merge "Revert "Revert "Switch editor to Ace and provide syntax highlight"""

This commit is contained in:
jenkins-bot 2018-03-30 15:29:01 +00:00 committed by Gerrit Code Review
commit c67ab4a061
8 changed files with 331 additions and 26 deletions

View file

@ -20,6 +20,7 @@ module.exports = function ( grunt ) {
all: [
'**/*.js',
'!node_modules/**',
'!modules/mode-abusefilter.js',
'!vendor/**'
]
},
@ -27,6 +28,7 @@ module.exports = function ( grunt ) {
all: [
'**/*.css',
'!node_modules/**',
'!modules/mode-abusefilter.js',
'!vendor/**'
]
}

View file

@ -179,6 +179,10 @@
"jquery.spinner",
"mediawiki.api"
]
},
"ext.abuseFilter.ace": {
"scripts": "mode-abusefilter.js",
"dependencies": "ext.codeEditor.ace"
}
},
"ResourceFileModulePaths": {

View file

@ -162,6 +162,7 @@
"abusefilter-edit-new": "New filter",
"abusefilter-edit-save": "Save filter",
"abusefilter-edit-id": "Filter ID:",
"abusefilter-edit-switch-editor": "Switch editor",
"abusefilter-edit-description": "Description:\n:''(publicly viewable)''",
"abusefilter-edit-group": "Filter group:",
"abusefilter-edit-flags": "Flags:",

View file

@ -194,6 +194,7 @@
"abusefilter-edit-new": "Field value in case an edited filter is new.",
"abusefilter-edit-save": "Submit button text to save a filter.",
"abusefilter-edit-id": "Field label for filter identifier.\n{{Identical|Filter ID}}",
"abusefilter-edit-switch-editor": "Button to switch between classic editor and Ace editor",
"abusefilter-edit-description": "Field label for publicly viewable abuse filter description.",
"abusefilter-edit-group": "\"Filter group\" a filter is in. Filters can be grouped, and only one group is run per action. The default group, \"default\", will be used in 99% of cases.",
"abusefilter-edit-flags": "Field label for abuse filter flags (checkboxes for \"hidden\", \"enabled\" and \"deleted\").\n{{Identical|Flag}}",

View file

@ -1997,6 +1997,23 @@ class AbuseFilter {
return $user;
}
/**
* Extract values for syntax highlight
*
* @param bool $canEdit
* @return array
*/
public static function getAceConfig( $canEdit ) {
$values = self::getBuilderValues();
$builderVariables = implode( '|', array_keys( $values['vars'] ) );
$builderFunctions = implode( '|', array_keys( AbuseFilterParser::$mFunctions ) );
return [
'variables' => $builderVariables,
'functions' => $builderFunctions,
'aceReadOnly' => !$canEdit
];
}
/**
* @param string $rules
* @param string $textName
@ -2008,10 +2025,7 @@ class AbuseFilter {
$canEdit = true ) {
global $wgOut;
$textareaAttrib = [ 'dir' => 'ltr' ]; # Rules are in English
if ( !$canEdit ) {
$textareaAttrib['readonly'] = 'readonly';
}
$editorAttrib = [ 'dir' => 'ltr' ]; # Rules are in English
global $wgUser;
$noTestAttrib = [];
@ -2021,7 +2035,36 @@ class AbuseFilter {
}
$rules = rtrim( $rules ) . "\n";
$rules = Xml::textarea( $textName, $rules, 40, 15, $textareaAttrib );
if ( ExtensionRegistry::getInstance()->isLoaded( 'CodeEditor' ) ) {
$editorAttrib['name'] = 'wpAceFilterEditor';
$editorAttrib['id'] = 'wpAceFilterEditor';
$editorAttrib['class'] = 'mw-abusefilter-editor';
$switchEditor =
Xml::element(
'input',
[
'type' => 'button',
'value' => wfMessage( 'abusefilter-edit-switch-editor' )->text(),
'id' => 'mw-abusefilter-switcheditor'
] + $noTestAttrib
);
$rules = Xml::element( 'div', $editorAttrib, $rules );
// Dummy textarea for submitting form
$rules .= Xml::textarea( $textName, '', 40, 15, [ 'style' => 'display: none;' ] );
$editorConfig = self::getAceConfig( $canEdit );
// Add Ace configuration variable
$wgOut->addJsConfigVars( 'aceConfig', $editorConfig );
} else {
if ( !$canEdit ) {
$editorAttrib['readonly'] = 'readonly';
}
$rules = Xml::textarea( $textName, $rules, 40, 15, $editorAttrib );
}
if ( $canEdit ) {
$dropDown = self::getBuilderValues();
@ -2057,15 +2100,32 @@ class AbuseFilter {
'select',
[ 'id' => 'wpFilterBuilder', ],
$builder
) . ' ';
);
// Add syntax checking
$rules .= Xml::element( 'input',
[
'type' => 'button',
'value' => wfMessage( 'abusefilter-edit-check' )->text(),
'id' => 'mw-abusefilter-syntaxcheck'
] + $noTestAttrib );
// Button for syntax check
$syntaxCheck =
Xml::element(
'input',
[
'type' => 'button',
'value' => wfMessage( 'abusefilter-edit-check' )->text(),
'id' => 'mw-abusefilter-syntaxcheck'
] + $noTestAttrib
);
// Button for switching editor (if Ace is used)
if ( isset( $switchEditor ) ) {
$syntaxCheck = $switchEditor . ' ' . $syntaxCheck;
}
$toolsContainer =
Xml::tags(
'div',
null,
$syntaxCheck
);
$rules .= $toolsContainer;
}
if ( $addResultDiv ) {

View file

@ -73,6 +73,13 @@ li.mw-abusefilter-changeslist-nomatch {
background-image: url( red_x.png );
}
div.mw-abusefilter-editor {
max-width: 75em;
height: 30em;
line-height: 1.5em;
border: 1px solid #a2a9b1;
}
#mw-abusefilter-syntaxresult,
ul li.mw-abusefilter-changeslist-nomatch,
ul li.mw-abusefilter-changeslist-match {

View file

@ -4,13 +4,21 @@
* @author John Du Hart
* @author Marius Hoch <hoo@online.de>
*/
/* global ace */
( function ( mw, $ ) {
'use strict';
// Filter textarea
// Filter editor for JS and jQuery handling
// @var {jQuery}
var $filterBox;
var $filterBox,
// Filter editor for Ace specific functions
filterEditor,
// Hidden textarea for submitting form
// @var {jQuery}
$plainTextBox,
// Bool to determine what editor to use
useAce = false;
/**
* Returns the currently selected warning message
@ -45,12 +53,54 @@
.data( 'syntaxOk', syntaxOk );
}
/**
* Converts index (used in textareas) in position {row, column} for ace
*
* @author danyaPostfactum (https://github.com/ajaxorg/ace/issues/1162)
*
* @param {string} index Part of data returned from the AJAX request
* @return {Object} row and column
*/
function indexToPosition( index ) {
var lines = filterEditor.session.getDocument().$lines,
newLineChar = filterEditor.session.doc.getNewLineCharacter(),
currentIndex = 0,
row, length;
for ( row = 0; row < lines.length; row++ ) {
length = filterEditor.session.getLine( row ).length;
if ( currentIndex + length >= index ) {
return {
row: row,
column: index - currentIndex
};
}
currentIndex += length + newLineChar.length;
}
}
/**
* Switch between Ace Editor and classic textarea
*/
function switchEditor() {
if ( useAce ) {
useAce = false;
$filterBox.hide();
$plainTextBox.show();
} else {
useAce = true;
filterEditor.session.setValue( $plainTextBox.val() );
$filterBox.show();
$plainTextBox.hide();
}
}
/**
* Takes the data retrieved in doSyntaxCheck and processes it
*
* @param {Object} data Data returned from the AJAX request
*/
function processSyntaxResult( data ) {
var position;
data = data.abusefilterchecksyntax;
if ( data.status === 'ok' ) {
@ -68,9 +118,16 @@
false
);
$filterBox
.focus()
.textSelection( 'setSelection', { start: data.character } );
if ( useAce ) {
filterEditor.focus();
position = indexToPosition( data.character );
filterEditor.navigateTo( position.row, position.column );
filterEditor.scrollToRow( position.row );
} else {
$plainTextBox
.focus()
.textSelection( 'setSelection', { start: data.character } );
}
}
}
@ -96,7 +153,7 @@
* @param {jQuery.Event} e
*/
function doSyntaxCheck() {
var filter = $filterBox.val(),
var filter = $plainTextBox.val(),
api = new mw.Api();
$( this )
@ -122,9 +179,14 @@
return;
}
$filterBox.textSelection(
'encapsulateSelection', { pre: $filterBuilder.val() + ' ' }
);
if ( useAce ) {
filterEditor.insert( $filterBuilder.val() + ' ' );
$plainTextBox.val( filterEditor.getSession().getValue() );
} else {
$plainTextBox.textSelection(
'encapsulateSelection', { pre: $filterBuilder.val() + ' ' }
);
}
$filterBuilder.prop( 'selectedIndex', 0 );
}
@ -159,7 +221,10 @@
} )
.done( function ( data ) {
if ( data.query.abusefilters[ 0 ] !== undefined ) {
$filterBox.val( data.query.abusefilters[ 0 ].pattern );
if ( useAce ) {
filterEditor.setValue( data.query.abusefilters[ 0 ].pattern );
}
$plainTextBox.val( data.query.abusefilters[ 0 ].pattern );
}
} );
}
@ -281,10 +346,58 @@
// On ready initialization
$( document ).ready( function () {
var $exportBox = $( '#mw-abusefilter-export' );
$filterBox = $( '#' + mw.config.get( 'abuseFilterBoxName' ) );
var basePath, readOnly,
$exportBox = $( '#mw-abusefilter-export' );
$plainTextBox = $( '#' + mw.config.get( 'abuseFilterBoxName' ) );
if ( $( '#wpAceFilterEditor' ).length ) {
// CodeEditor is installed.
mw.loader.using( [ 'ext.abuseFilter.ace' ] ).then( function () {
useAce = true;
$filterBox = $( '#wpAceFilterEditor' );
filterEditor = ace.edit( 'wpAceFilterEditor' );
filterEditor.session.setMode( 'ace/mode/abusefilter' );
// Ace setup from codeEditor extension
basePath = mw.config.get( 'wgExtensionAssetsPath', '' );
if ( basePath.slice( 0, 2 ) === '//' ) {
// ACE uses web workers, which have importScripts, which don't like relative links.
// This is a problem only when the assets are on another server, so this rewrite should suffice
// Protocol relative
basePath = window.location.protocol + basePath;
}
ace.config.set( 'basePath', basePath + '/CodeEditor/modules/ace' );
// Settings for Ace editor box
readOnly = mw.config.get( 'aceConfig' ).aceReadOnly;
filterEditor.setTheme( 'ace/theme/textmate' );
filterEditor.session.setOption( 'useWorker', false );
filterEditor.setReadOnly( readOnly );
filterEditor.$blockScrolling = Infinity;
// Copy editor in dummy textarea
$plainTextBox.val( filterEditor.getSession().getValue() );
// Hide the syntax ok message when the text changes and sync dummy box
$filterBox.keyup( function () {
var $el = $( '#mw-abusefilter-syntaxresult' );
if ( $el.data( 'syntaxOk' ) ) {
$el.hide();
}
$plainTextBox.val( filterEditor.getSession().getValue() );
} );
$( '#mw-abusefilter-switcheditor' ).click( switchEditor );
} );
}
// Hide the syntax ok message when the text changes
$filterBox.keyup( function () {
$plainTextBox.keyup( function () {
var $el = $( '#mw-abusefilter-syntaxresult' );
if ( $el.data( 'syntaxOk' ) ) {

117
modules/mode-abusefilter.js Normal file
View file

@ -0,0 +1,117 @@
/* global ace, mw */
ace.define( 'ace/mode/abusefilter_highlight_rules', [ 'require', 'exports', 'module', 'ace/lib/oop', 'ace/mode/text_highlight_rules' ], function ( require, exports, module ) {
'use strict';
var oop = require( 'ace/lib/oop' ),
TextHighlightRules = require( './text_highlight_rules' ).TextHighlightRules,
AFHighlightRules = function () {
var keywords = ( 'like|matches|in|rlike|regex|irlike|contains|if|then|else|end' ),
constants = ( 'true|false|null' ),
functions = ( mw.config.get( 'aceConfig' ).functions ),
variables = ( mw.config.get( 'aceConfig' ).variables ),
deprecated = ( '' ), // Template for deprecated vars, already registered within ace settings.
keywordMapper = this.createKeywordMapper(
{
'keyword': keywords,
'support.function': functions,
'constant.language': constants,
'variable.language': variables,
'keyword.deprecated': deprecated
},
'identifier'
),
decimalInteger = '(?:(?:[1-9]\\d*)|(?:0))',
hexInteger = '(?:0[xX][\\dA-Fa-f]+)',
integer = '(?:' + decimalInteger + '|' + hexInteger + ')',
fraction = '(?:\\.\\d+)',
intPart = '(?:\\d+)',
pointFloat = '(?:(?:' + intPart + '?' + fraction + ')|(?:' + intPart + '\\.))',
floatNumber = '(?:' + pointFloat + ')';
this.$rules = {
'start': [ {
token: 'comment',
regex: '\\/\\*',
next: 'comment'
}, {
token: 'string', // " string
regex: '"(?:[^\\\\]|\\\\.)*?"'
}, {
token: 'string', // ' string
regex: "'(?:[^\\\\]|\\\\.)*?'"
}, {
token: 'constant.numeric', // float
regex: floatNumber
}, {
token: 'constant.numeric', // integer
regex: integer + '\\b'
}, {
token: keywordMapper,
regex: '[a-zA-Z_$][a-zA-Z0-9_$]*\\b'
}, {
token: 'keyword.operator',
regex: '\\+|\\-|\\*\\*|\\*|\\/|%|\\^|&|\\||<|>|<=|=>|==|!=|===|!==|:=|=|!'
}, {
token: 'paren.lparen',
regex: '[\\[\\(]'
}, {
token: 'paren.rparen',
regex: '[\\]\\)]'
}, {
token: 'text',
regex: '\\s+|\\w+'
} ],
'comment': [ {
token: 'comment',
regex: '\\*\\/',
next: 'start'
}, {
defaultToken: 'comment'
} ]
};
this.normalizeRules();
};
oop.inherits( AFHighlightRules, TextHighlightRules );
exports.AFHighlightRules = AFHighlightRules;
} );
ace.define( 'ace/mode/abusefilter', [ 'require', 'exports', 'module', 'ace/lib/oop', 'ace/mode/text', 'ace/mode/abusefilter_highlight_rules' ], function ( require, exports, module ) {
'use strict';
var oop = require( 'ace/lib/oop' ),
TextMode = require( './text' ).Mode,
AFHighlightRules = require( './abusefilter_highlight_rules' ).AFHighlightRules,
MatchingBraceOutdent = require( './matching_brace_outdent' ).MatchingBraceOutdent,
Mode = function () {
this.HighlightRules = AFHighlightRules;
this.$behaviour = this.$defaultBehaviour;
this.$outdent = new MatchingBraceOutdent();
};
oop.inherits( Mode, TextMode );
( function () {
this.blockComment = {
start: '/*',
end: '*/'
};
this.getNextLineIndent = function ( state, line, tab ) {
var indent = this.$getIndent( line );
return indent;
};
this.checkOutdent = function ( state, line, input ) {
return this.$outdent.checkOutdent( line, input );
};
this.autoOutdent = function ( state, doc, row ) {
this.$outdent.autoOutdent( doc, row );
};
this.$id = 'ace/mode/abusefilter';
} )
.call( Mode.prototype );
exports.Mode = Mode;
} );