Add regexp tester to the editor

This commit is contained in:
tophf 2017-03-27 00:37:45 +03:00
parent 486d4258d3
commit c356fb9be5
3 changed files with 207 additions and 1 deletions

View File

@ -362,6 +362,30 @@
"message": "Regexp is invalid.",
"description": "Validation message for a bad regexp in a style"
},
"styleRegexpTestButton": {
"message": "RegExp test",
"description": "RegExp test button label in the editor shown when applies-to list has a regexp value"
},
"styleRegexpTestTitle": {
"message": "List of matching opened tabs (click on URL to focus its tab)",
"description": "RegExp test report: title of the report"
},
"styleRegexpTestFull": {
"message": "Matching tabs",
"description": "RegExp test report: label for the fully matching expressions"
},
"styleRegexpTestPartial": {
"message": "Not matching fully, hence skipped",
"description": "RegExp test report: label for the partially matching expressions"
},
"styleRegexpTestNone": {
"message": "No matching tabs",
"description": "RegExp test report: label for expressions that didn't match any tabs"
},
"styleRegexpTestInvalid": {
"message": "Invalid regexps skipped",
"description": "RegExp test report: label for the invalid expressions"
},
"styleBeautify": {
"message": "Beautify",
"description": "Label for the CSS-beautifier button on the edit style page"

View File

@ -207,6 +207,14 @@
background-color: none;
}
}
@keyframes fadein {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.resize-grip {
position: absolute;
display: block;
@ -261,6 +269,61 @@
.applies-to img {
vertical-align: bottom;
}
.test-regexp {
display: none;
}
.has-regexp .test-regexp {
display: inline-block;
}
.regexp-report summary, .regexp-report div {
cursor: pointer;
}
.regexp-report mark {
background-color: rgba(255, 255, 0, .5);
}
.regexp-report details {
margin-left: 1rem;
}
.regexp-report details:not(:last-child) {
margin-bottom: 1rem;
}
.regexp-report summary {
font-weight: bold;
margin-left: -1rem;
margin-bottom: .5rem;
outline: none;
cursor: default;
}
.regexp-report details[data-type="full"] {
color: darkgreen;
}
.regexp-report details[data-type="partial"] {
color: darkgray;
}
.regexp-report details[data-type="invalid"] {
color: maroon;
}
.regexp-report details details {
margin-left: 2rem;
margin-top: .5rem;
}
.regexp-report .svg-icon {
position: absolute;
margin-top: -1px;
}
.regexp-report details div:hover {
text-decoration: underline;
text-decoration-skip: ink;
}
.regexp-report details div img {
width: 16px;
max-height: 16px;
position: absolute;
margin-left: -20px;
margin-top: -1px;
animation: fadein 1s cubic-bezier(.03, .67, .08, .94);
animation-fill-mode: both;
}
/************ help popup ************/
#help-popup {
top: 3rem;
@ -285,7 +348,7 @@
font-weight: bold;
background-color: rgba(0,0,0,0.05);
margin: -0.5rem -0.5rem 0.5rem;
padding: 0.5rem;
padding: .5rem 32px .5rem .5rem;
}
#help-popup .contents {
max-height: calc(100vh - 8rem);
@ -537,6 +600,7 @@
<button class="remove-section" i18n-text="sectionRemove"></button>
<button class="add-section" i18n-text="sectionAdd"></button>
<button class="beautify-section" i18n-text="styleBeautify"></button>
<button class="test-regexp" i18n-text="styleRegexpTestButton"></button>
</div>
</template>
<template data-id="find">

118
edit.js
View File

@ -500,6 +500,23 @@ function addSection(event, section) {
appliesTo.addEventListener("change", onChange);
appliesTo.addEventListener("input", onChange);
toggleTestRegExpVisibility();
appliesTo.addEventListener('change', toggleTestRegExpVisibility);
div.querySelector('.test-regexp').onclick = showRegExpTester;
function toggleTestRegExpVisibility() {
const show = [...appliesTo.children].some(item =>
!item.matches('.applies-to-everything') &&
item.querySelector('.applies-type').value == 'regexp' &&
item.querySelector('.applies-value').value.trim());
div.classList.toggle('has-regexp', show);
appliesTo.oninput = appliesTo.oninput || show && (event => {
if (event.target.matches('.applies-value')
&& event.target.parentElement.querySelector('.applies-type').value == 'regexp') {
showRegExpTester(null, div);
}
});
}
var sections = document.getElementById("sections");
if (event) {
var clickedSection = getSectionForChild(event.target);
@ -1605,6 +1622,107 @@ function showLintHelp() {
);
}
function showRegExpTester(event, section = getSectionForChild(this)) {
const GET_FAVICON_URL = 'https://www.google.com/s2/favicons?domain=';
const OWN_ICON = chrome.app.getDetails().icons['16'];
const RX_SUPPORTED_URLS = new RegExp(`^(file|https?|ftps?):|^${OWN_ORIGIN}`);
const cachedRegexps = showRegExpTester.cachedRegexps =
showRegExpTester.cachedRegexps || new Map();
const regexps = [...section.querySelector('.applies-to-list').children]
.map(item =>
!item.matches('.applies-to-everything') &&
item.querySelector('.applies-type').value == 'regexp' &&
item.querySelector('.applies-value').value.trim())
.filter(item => item)
.map(text => {
const rxData = Object.assign({text}, cachedRegexps.get(text));
if (!rxData.urls) {
cachedRegexps.set(text, Object.assign(rxData, {
rx: tryRegExp(text),
urls: new Map(),
}));
}
return rxData;
});
chrome.tabs.query({}, tabs => {
const supported = tabs.map(tab => tab.url)
.filter(url => RX_SUPPORTED_URLS.test(url));
const unique = [...new Set(supported).values()];
for (const rxData of regexps) {
const {rx, urls} = rxData;
if (rx) {
const urlsNow = new Map();
for (const url of unique) {
const match = urls.get(url) || (url.match(rx) || [])[0];
if (match) {
urlsNow.set(url, match);
}
}
rxData.urls = urlsNow;
}
}
const moreInfoLink = '<a target="_blank" ' +
'href="https://github.com/stylish-userstyles/' +
'stylish/wiki/Applying-styles-to-specific-sites' +
'#advanced-matching-with-regular-expressions">' +
'<img src="/images/help.png"></a>';
const stats = {
full: {data: [], label: t('styleRegexpTestFull')},
partial: {data: [], label: t('styleRegexpTestPartial') + moreInfoLink},
none: {data: [], label: t('styleRegexpTestNone')},
invalid: {data: [], label: t('styleRegexpTestInvalid')},
};
for (const {text, rx, urls} of regexps) {
if (!rx) {
stats.invalid.data.push({text});
continue;
}
if (!urls.size) {
stats.none.data.push({text});
continue;
}
const full = [];
const partial = [];
for (const [url, match] of urls.entries()) {
const faviconUrl = url.startsWith(OWN_ORIGIN)
? OWN_ICON
: GET_FAVICON_URL + new URL(url).hostname;
const icon = `<img src="${faviconUrl}">`;
if (match.length == url.length) {
full.push(`<div>${icon + url}</div>`);
} else {
partial.push(`<div>${icon}<mark>${match}</mark>` +
url.substr(match.length) + '</div>');
}
}
if (full.length) {
stats.full.data.push({text, urls: full});
}
if (partial.length) {
stats.partial.data.push({text, urls: partial});
}
}
showHelp(t('styleRegexpTestTitle'),
'<div class="regexp-report">' +
Object.keys(stats).map(type => (!stats[type].data.length ? '' :
`<details open data-type="${type}">
<summary>${stats[type].label}</summary>` +
stats[type].data.map(({text, urls}) => (!urls ? text :
`<details open><summary>${text}</summary>${urls.join('')}</details>`
)).join('<br>') +
'</details>'
)).join('') +
'</div>');
document.querySelector('.regexp-report').onclick = event => {
const target = event.target.closest('a, .regexp-report div');
if (target) {
openURL({url: target.href || target.textContent});
event.preventDefault();
}
};
});
}
function showHelp(title, text) {
var div = document.getElementById("help-popup");
div.classList.remove("big");