mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/AbuseFilter.git
synced 2024-11-23 21:53:35 +00:00
Merge "Revert "Revert "Switch editor to Ace and provide syntax highlight"""
This commit is contained in:
commit
c67ab4a061
|
@ -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/**'
|
||||
]
|
||||
}
|
||||
|
|
|
@ -179,6 +179,10 @@
|
|||
"jquery.spinner",
|
||||
"mediawiki.api"
|
||||
]
|
||||
},
|
||||
"ext.abuseFilter.ace": {
|
||||
"scripts": "mode-abusefilter.js",
|
||||
"dependencies": "ext.codeEditor.ace"
|
||||
}
|
||||
},
|
||||
"ResourceFileModulePaths": {
|
||||
|
|
|
@ -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:",
|
||||
|
|
|
@ -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}}",
|
||||
|
|
|
@ -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 ) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
117
modules/mode-abusefilter.js
Normal 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;
|
||||
} );
|
Loading…
Reference in a new issue