make use of CodeMirror for Whitelist pane

This commit is contained in:
Raymond Hill 2018-03-12 08:28:07 -04:00
parent 42a05746e5
commit 9715d1e8b9
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
5 changed files with 127 additions and 151 deletions

View File

@ -5,36 +5,7 @@ div > p:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
#whitelist { #whitelist {
border: 1px solid gray;
height: 60vh; height: 60vh;
margin: 0;
padding: 1px;
position: relative;
resize: vertical;
}
#whitelist.invalid {
border-color: red;
}
#whitelist textarea {
border: none;
box-sizing: border-box;
height: 100%;
padding: 0.4em;
resize: none;
text-align: left; text-align: left;
white-space: pre;
width: 100%; width: 100%;
} }
#whitelist textarea + div {
background-color: red;
bottom: 0;
color: white;
display: none;
padding: 2px 4px;
pointer-events: none;
position: absolute;
right: 0;
}
#whitelist.invalid textarea + div {
display: block;
}

View File

@ -135,7 +135,11 @@ var onMessage = function(request, sender, callback) {
break; break;
case 'getWhitelist': case 'getWhitelist':
response = µb.stringFromWhitelist(µb.netWhitelist); response = {
whitelist: µb.stringFromWhitelist(µb.netWhitelist),
reBadHostname: µb.reWhitelistBadHostname.source,
reHostnameExtractor: µb.reWhitelistHostnameExtractor.source
};
break; break;
case 'launchElementPicker': case 'launchElementPicker':
@ -985,10 +989,6 @@ var onMessage = function(request, sender, callback) {
resetUserData(); resetUserData();
break; break;
case 'validateWhitelistString':
response = µb.validateWhitelistString(request.raw);
break;
case 'writeHiddenSettings': case 'writeHiddenSettings':
µb.hiddenSettings = µb.hiddenSettingsFromString(request.content); µb.hiddenSettings = µb.hiddenSettingsFromString(request.content);
µb.saveHiddenSettings(); µb.saveHiddenSettings();

View File

@ -219,7 +219,7 @@ var matchBucket = function(url, hostname, bucket, start) {
} }
// Plain hostname // Plain hostname
else if ( line.indexOf('/') === -1 ) { else if ( line.indexOf('/') === -1 ) {
if ( reInvalidHostname.test(line) ) { if ( this.reWhitelistBadHostname.test(line) ) {
key = '#'; key = '#';
directive = '# ' + line; directive = '# ' + line;
} else { } else {
@ -242,7 +242,7 @@ var matchBucket = function(url, hostname, bucket, start) {
// label (or else it would be just impossible to make an efficient // label (or else it would be just impossible to make an efficient
// dict. // dict.
else { else {
matches = reHostnameExtractor.exec(line); matches = this.reWhitelistHostnameExtractor.exec(line);
if ( !matches || matches.length !== 2 ) { if ( !matches || matches.length !== 2 ) {
key = '#'; key = '#';
directive = '# ' + line; directive = '# ' + line;
@ -266,27 +266,8 @@ var matchBucket = function(url, hostname, bucket, start) {
return whitelist; return whitelist;
}; };
µBlock.validateWhitelistString = function(s) { µBlock.reWhitelistBadHostname = /[^a-z0-9.\-\[\]:]/;
var lineIter = new this.LineIterator(s), line; µBlock.reWhitelistHostnameExtractor = /([a-z0-9\[][a-z0-9.\-]*[a-z0-9\]])(?::[\d*]+)?\/(?:[^\x00-\x20\/]|$)[^\x00-\x20]*$/;
while ( !lineIter.eot() ) {
line = lineIter.next().trim();
if ( line === '' ) { continue; }
if ( line.startsWith('#') ) { continue; } // Comment
if ( line.indexOf('/') === -1 ) { // Plain hostname
if ( reInvalidHostname.test(line) ) { return false; }
continue;
}
if ( line.length > 2 && line.startsWith('/') && line.endsWith('/') ) { // Regex-based
try { new RegExp(line.slice(1, -1)); } catch(ex) { return false; }
continue;
}
if ( reHostnameExtractor.test(line) === false ) { return false; } // URL
}
return true;
};
var reInvalidHostname = /[^a-z0-9.\-\[\]:]/,
reHostnameExtractor = /([a-z0-9\[][a-z0-9.\-]*[a-z0-9\]])(?::[\d*]+)?\/(?:[^\x00-\x20\/]|$)[^\x00-\x20]*$/;
/******************************************************************************/ /******************************************************************************/

View File

@ -1,7 +1,7 @@
/******************************************************************************* /*******************************************************************************
uBlock Origin - a browser extension to block requests. uBlock Origin - a browser extension to block requests.
Copyright (C) 2014-2016 Raymond Hill Copyright (C) 2014-2018 Raymond Hill
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
@ -19,79 +19,94 @@
Home: https://github.com/gorhill/uBlock Home: https://github.com/gorhill/uBlock
*/ */
/* global uDom, uBlockDashboard */ /* global CodeMirror, uDom, uBlockDashboard */
/******************************************************************************/
(function() {
'use strict'; 'use strict';
/******************************************************************************/ /******************************************************************************/
(function() {
/******************************************************************************/
CodeMirror.defineMode("ubo-whitelist-directives", function() {
var reComment = /^\s*#/,
reRegex = /^\/.+\/$/;
return {
token: function(stream) {
var line = stream.string.trim();
stream.skipToEnd();
if ( reBadHostname === undefined ) {
return null;
}
if ( reComment.test(line) ) {
return 'comment';
}
if ( line.indexOf('/') === -1 ) {
return reBadHostname.test(line) ? 'error' : null;
}
if ( reRegex.test(line) ) {
try {
new RegExp(line.slice(1, -1));
} catch(ex) {
return 'error';
}
return null;
}
return reHostnameExtractor.test(line) ? null : 'error';
}
};
});
var reBadHostname,
reHostnameExtractor;
/******************************************************************************/
var messaging = vAPI.messaging, var messaging = vAPI.messaging,
cachedWhitelist = ''; cachedWhitelist = '',
noopFunc = function(){};
/******************************************************************************/ var cmEditor = new CodeMirror(
document.getElementById('whitelist'),
var getTextareaNode = function() { {
var me = getTextareaNode, autofocus: true,
node = me.theNode; inputStyle: 'contenteditable',
if ( node === undefined ) { lineNumbers: true,
node = me.theNode = uDom.nodeFromSelector('#whitelist textarea'); lineWrapping: true,
styleActiveLine: true
} }
return node;
};
var setErrorNodeHorizontalOffset = function(px) {
var me = setErrorNodeHorizontalOffset,
offset = me.theOffset || 0;
if ( px === offset ) { return; }
var node = me.theNode;
if ( node === undefined ) {
node = me.theNode = uDom.nodeFromSelector('#whitelist textarea + div');
}
node.style.right = px + 'px';
me.theOffset = px;
};
/******************************************************************************/
var whitelistChanged = (function() {
var changedWhitelist, changed, timer;
var updateUI = function(good) {
uDom.nodeFromId('whitelistApply').disabled = changed || !good;
uDom.nodeFromId('whitelistRevert').disabled = changed;
uDom.nodeFromId('whitelist').classList.toggle('invalid', !good);
};
var validate = function() {
timer = undefined;
messaging.send(
'dashboard',
{ what: 'validateWhitelistString', raw: changedWhitelist },
updateUI
); );
/******************************************************************************/
var whitelistChanged = function() {
var whitelistElem = uDom.nodeFromId('whitelist');
var bad = whitelistElem.querySelector('.cm-error') !== null;
var changedWhitelist = cmEditor.getValue().trim();
var changed = changedWhitelist !== cachedWhitelist;
uDom.nodeFromId('whitelistApply').disabled = !changed || bad;
uDom.nodeFromId('whitelistRevert').disabled = !changed;
CodeMirror.commands.save = changed && !bad ? applyChanges : noopFunc;
}; };
return function() { cmEditor.on('changes', whitelistChanged);
changedWhitelist = getTextareaNode().value.trim();
changed = changedWhitelist === cachedWhitelist;
if ( timer !== undefined ) { clearTimeout(timer); }
timer = vAPI.setTimeout(validate, 251);
var textarea = getTextareaNode();
setErrorNodeHorizontalOffset(textarea.offsetWidth - textarea.clientWidth);
};
})();
/******************************************************************************/ /******************************************************************************/
var renderWhitelist = function() { var renderWhitelist = function() {
var onRead = function(whitelist) { var onRead = function(details) {
cachedWhitelist = whitelist.trim(); var first = reBadHostname === undefined;
getTextareaNode().value = cachedWhitelist + '\n'; if ( first ) {
whitelistChanged(); reBadHostname = new RegExp(details.reBadHostname);
reHostnameExtractor = new RegExp(details.reHostnameExtractor);
}
cachedWhitelist = details.whitelist.trim();
cmEditor.setValue(cachedWhitelist + '\n');
if ( first ) {
cmEditor.clearHistory();
}
}; };
messaging.send('dashboard', { what: 'getWhitelist' }, onRead); messaging.send('dashboard', { what: 'getWhitelist' }, onRead);
}; };
@ -100,17 +115,16 @@ var renderWhitelist = function() {
var handleImportFilePicker = function() { var handleImportFilePicker = function() {
var fileReaderOnLoadHandler = function() { var fileReaderOnLoadHandler = function() {
var textarea = getTextareaNode(); cmEditor.setValue(
textarea.value = [textarea.value.trim(), this.result.trim()].join('\n').trim(); [
whitelistChanged(); cmEditor.getValue().trim(),
this.result.trim()
].join('\n').trim()
);
}; };
var file = this.files[0]; var file = this.files[0];
if ( file === undefined || file.name === '' ) { if ( file === undefined || file.name === '' ) { return; }
return; if ( file.type.indexOf('text') !== 0 ) { return; }
}
if ( file.type.indexOf('text') !== 0 ) {
return;
}
var fr = new FileReader(); var fr = new FileReader();
fr.onload = fileReaderOnLoadHandler; fr.onload = fileReaderOnLoadHandler;
fr.readAsText(file); fr.readAsText(file);
@ -130,7 +144,7 @@ var startImportFilePicker = function() {
/******************************************************************************/ /******************************************************************************/
var exportWhitelistToFile = function() { var exportWhitelistToFile = function() {
var val = getTextareaNode().value.trim(); var val = cmEditor.getValue().trim();
if ( val === '' ) { return; } if ( val === '' ) { return; }
var filename = vAPI.i18n('whitelistExportFilename') var filename = vAPI.i18n('whitelistExportFilename')
.replace('{{datetime}}', uBlockDashboard.dateNowToSensibleString()) .replace('{{datetime}}', uBlockDashboard.dateNowToSensibleString())
@ -144,35 +158,35 @@ var exportWhitelistToFile = function() {
/******************************************************************************/ /******************************************************************************/
var applyChanges = function() { var applyChanges = function() {
cachedWhitelist = getTextareaNode().value.trim(); cachedWhitelist = cmEditor.getValue().trim();
var request = { messaging.send(
'dashboard',
{
what: 'setWhitelist', what: 'setWhitelist',
whitelist: cachedWhitelist whitelist: cachedWhitelist
}; },
messaging.send('dashboard', request, renderWhitelist); renderWhitelist
);
}; };
var revertChanges = function() { var revertChanges = function() {
getTextareaNode().value = cachedWhitelist + '\n'; var content = cachedWhitelist;
whitelistChanged(); if ( content !== '' ) { content += '\n'; }
cmEditor.setValue(content);
}; };
/******************************************************************************/ /******************************************************************************/
var getCloudData = function() { var getCloudData = function() {
return getTextareaNode().value; return cmEditor.getValue();
}; };
var setCloudData = function(data, append) { var setCloudData = function(data, append) {
if ( typeof data !== 'string' ) { if ( typeof data !== 'string' ) { return; }
return;
}
var textarea = getTextareaNode();
if ( append ) { if ( append ) {
data = uBlockDashboard.mergeNewLines(textarea.value.trim(), data); data = uBlockDashboard.mergeNewLines(cmEditor.getValue().trim(), data);
} }
textarea.value = data.trim() + '\n'; cmEditor.setValue(data.trim() + '\n');
whitelistChanged();
}; };
self.cloud.onPush = getCloudData; self.cloud.onPush = getCloudData;
@ -183,7 +197,6 @@ self.cloud.onPull = setCloudData;
uDom('#importWhitelistFromFile').on('click', startImportFilePicker); uDom('#importWhitelistFromFile').on('click', startImportFilePicker);
uDom('#importFilePicker').on('change', handleImportFilePicker); uDom('#importFilePicker').on('change', handleImportFilePicker);
uDom('#exportWhitelistToFile').on('click', exportWhitelistToFile); uDom('#exportWhitelistToFile').on('click', exportWhitelistToFile);
uDom('#whitelist textarea').on('input', whitelistChanged);
uDom('#whitelistApply').on('click', applyChanges); uDom('#whitelistApply').on('click', applyChanges);
uDom('#whitelistRevert').on('click', revertChanges); uDom('#whitelistRevert').on('click', revertChanges);

View File

@ -4,10 +4,15 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>uBlock — Whitelist</title> <title>uBlock — Whitelist</title>
<link rel="stylesheet" type="text/css" href="css/common.css">
<link rel="stylesheet" type="text/css" href="css/dashboard-common.css"> <link rel="stylesheet" href="lib/codemirror/lib/codemirror.css">
<link rel="stylesheet" type="text/css" href="css/cloud-ui.css"> <link rel="stylesheet" href="lib/codemirror/addon/search/matchesonscrollbar.css">
<link rel="stylesheet" type="text/css" href="css/whitelist.css">
<link rel="stylesheet" href="css/common.css">
<link rel="stylesheet" href="css/dashboard-common.css">
<link rel="stylesheet" href="css/cloud-ui.css">
<link rel="stylesheet" href="css/whitelist.css">
<link rel="stylesheet" href="css/codemirror.css">
</head> </head>
<body> <body>
@ -18,16 +23,22 @@
<p> <p>
<button id="whitelistApply" class="custom important" type="button" disabled="true" data-i18n="whitelistApply"></button>&ensp; <button id="whitelistApply" class="custom important" type="button" disabled="true" data-i18n="whitelistApply"></button>&ensp;
<button id="whitelistRevert" class="custom" type="button" disabled="true" data-i18n="genericRevert"></button> <button id="whitelistRevert" class="custom" type="button" disabled="true" data-i18n="genericRevert"></button>
<p><section id="whitelist"> <p><div id="whitelist" class="codeMirrorContainer"></div>
<textarea dir="auto" spellcheck="false"></textarea>
<div>E</div>
</section>
<p> <p>
<button id="importWhitelistFromFile" class="custom" data-i18n="whitelistImport"></button>&ensp; <button id="importWhitelistFromFile" class="custom" data-i18n="whitelistImport"></button>&ensp;
<button id="exportWhitelistToFile" class="custom" data-i18n="whitelistExport"></button> <button id="exportWhitelistToFile" class="custom" data-i18n="whitelistExport"></button>
<input id="importFilePicker" type="file" accept="text/plain" class="hiddenFileInput"> <input id="importFilePicker" type="file" accept="text/plain" class="hiddenFileInput">
<script src="lib/codemirror/lib/codemirror.js"></script>
<script src="lib/codemirror/addon/display/panel.js"></script>
<script src="lib/codemirror/addon/scroll/annotatescrollbar.js"></script>
<script src="lib/codemirror/addon/search/matchesonscrollbar.js"></script>
<script src="lib/codemirror/addon/search/searchcursor.js"></script>
<script src="lib/codemirror/addon/selection/active-line.js"></script>
<script src="lib/punycode.js"></script> <script src="lib/punycode.js"></script>
<script src="js/codemirror/search.js"></script>
<script src="js/vapi.js"></script> <script src="js/vapi.js"></script>
<script src="js/vapi-common.js"></script> <script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script> <script src="js/vapi-client.js"></script>