diff --git a/edit/edit.js b/edit/edit.js
index d7edd6cc..dbd873fd 100644
--- a/edit/edit.js
+++ b/edit/edit.js
@@ -1557,7 +1557,7 @@ function fromMozillaFormat() {
const parser = new parserlib.css.Parser();
const lines = mozStyle.split('\n');
const sectionStack = [{code: '', start: {line: 1, col: 1}}];
- let errors = '';
+ const errors = [];
// let oldSectionCount = editors.length;
let firstAddedCM;
@@ -1575,12 +1575,16 @@ function fromMozillaFormat() {
doAddSection(sectionStack.last);
sectionStack.last.code = '';
}
- e.functions.forEach(f => {
- const m = f.match(/^(url|url-prefix|domain|regexp)\((['"]?)(.+?)\2?\)$/);
+ for (const f of e.functions) {
+ const m = f && f.match(/^([\w-]*)\((['"]?)(.+?)\2?\)$/);
+ if (!m || !/^(url|url-prefix|domain|regexp)$/.test(m[1])) {
+ errors.push(`${e.line}:${e.col + 1} invalid function "${m ? m[1] : f || ''}"`);
+ continue;
+ }
const aType = CssToProperty[m[1]];
const aValue = aType !== 'regexps' ? m[3] : m[3].replace(/\\\\/g, '\\');
(section[aType] = section[aType] || []).push(aValue);
- });
+ }
sectionStack.push(section);
});
@@ -1607,12 +1611,16 @@ function fromMozillaFormat() {
firstAddedCM.focus();
if (errors) {
- showHelp(t('issues'), errors);
+ showHelp(t('issues'), $element({
+ tag: 'pre',
+ textContent: errors.join('\n'),
+ }));
}
});
parser.addListener('error', e => {
- errors += e.line + ':' + e.col + ' ' + e.message.replace(/ at line \d.+$/, '') + '
';
+ errors.push(e.line + ':' + e.col + ' ' +
+ e.message.replace(/ at line \d.+$/, ''));
});
parser.parse(mozStyle);
@@ -1834,13 +1842,16 @@ function showRegExpTester(event, section = getSectionForChild(this)) {
rxData.urls = urlsNow;
}
}
- const moreInfoLink = template.regexpTestPartial.outerHTML;
const stats = {
full: {data: [], label: t('styleRegexpTestFull')},
- partial: {data: [], label: t('styleRegexpTestPartial') + moreInfoLink},
+ partial: {data: [], label: [
+ t('styleRegexpTestPartial'),
+ template.regexpTestPartial.cloneNode(true),
+ ]},
none: {data: [], label: t('styleRegexpTestNone')},
invalid: {data: [], label: t('styleRegexpTestInvalid')},
};
+ // collect stats
for (const {text, rx, urls} of regexps) {
if (!rx) {
stats.invalid.data.push({text});
@@ -1856,12 +1867,18 @@ function showRegExpTester(event, section = getSectionForChild(this)) {
const faviconUrl = url.startsWith(URLS.ownOrigin)
? OWN_ICON
: GET_FAVICON_URL + new URL(url).hostname;
- const icon = ``;
+ const icon = $element({tag: 'img', src: faviconUrl});
if (match.length === url.length) {
- full.push(`
${icon + url}
`);
+ full.push($element({appendChild: [
+ icon,
+ url,
+ ]}));
} else {
- partial.push(`${icon}${match}` +
- url.substr(match.length) + '
');
+ partial.push($element({appendChild: [
+ icon,
+ $element({tag: 'mark', textContent: match}),
+ url.substr(match.length),
+ ]}));
}
}
if (full.length) {
@@ -1871,17 +1888,42 @@ function showRegExpTester(event, section = getSectionForChild(this)) {
stats.partial.data.push({text, urls: partial});
}
}
- showHelp(t('styleRegexpTestTitle'),
- '' +
- Object.keys(stats).map(type => (!stats[type].data.length ? '' :
- `
- ${stats[type].label}
` +
- stats[type].data.map(({text, urls}) => (!urls ? text :
- `${text}
${urls.join('')} `
- )).join('
') +
- ' '
- )).join('') +
- '
');
+ // render stats
+ const report = $element({className: 'regexp-report'});
+ const br = $element({tag: 'br'});
+ for (const type in stats) {
+ // top level groups: full, partial, none, invalid
+ const {label, data} = stats[type];
+ if (!data.length) {
+ continue;
+ }
+ // 2nd level: regexp text
+ const summary = $element({tag: 'summary', appendChild: label});
+ const block = [summary];
+ for (const {text, urls} of data) {
+ if (!urls) {
+ block.push(text, br.cloneNode());
+ continue;
+ }
+ block.push($element({
+ tag: 'details',
+ open: true,
+ appendChild: [
+ $element({tag: 'summary', textContent: text}),
+ // 3rd level: tab urls
+ ...urls,
+ ],
+ }));
+ }
+ report.appendChild($element({
+ tag: 'details',
+ open: true,
+ dataset: {type},
+ appendChild: block,
+ }));
+ }
+ showHelp(t('styleRegexpTestTitle'), report);
+
document.querySelector('.regexp-report').onclick = event => {
const target = event.target.closest('a, .regexp-report div');
if (target) {
@@ -1893,10 +1935,17 @@ function showRegExpTester(event, section = getSectionForChild(this)) {
}
function showHelp(title, text) {
- const div = document.getElementById('help-popup');
+ const div = $('#help-popup');
div.classList.remove('big');
- div.querySelector('.contents').innerHTML = text;
- div.querySelector('.title').innerHTML = title;
+
+ const contents = $('.contents', div);
+ if (text instanceof HTMLElement) {
+ contents.textContent = '';
+ contents.appendChild(text);
+ } else {
+ contents.innerHTML = text;
+ }
+ $('.title', div).textContent = title;
if (getComputedStyle(div).display === 'none') {
document.addEventListener('keydown', closeHelp);
diff --git a/js/dom.js b/js/dom.js
index 6a71a10a..ced73414 100644
--- a/js/dom.js
+++ b/js/dom.js
@@ -106,7 +106,7 @@ function $$(selector, base = document) {
function $element(opt) {
// tag: string, default 'div', may include namespace like 'ns#tag'
- // appendChild: element or an array of elements
+ // appendChild: element/string or an array of elements/strings
// dataset: object
// any DOM property: assigned as is
const [ns, tag] = opt.tag && opt.tag.includes('#')
@@ -115,8 +115,12 @@ function $element(opt) {
const element = ns
? document.createElementNS(ns === 'SVG' || ns === 'svg' ? 'http://www.w3.org/2000/svg' : ns, tag)
: document.createElement(tag || 'div');
- (opt.appendChild instanceof Array ? opt.appendChild : [opt.appendChild])
- .forEach(child => child && element.appendChild(child));
+ const children = opt.appendChild instanceof Array ? opt.appendChild : [opt.appendChild];
+ for (const child of children) {
+ if (child) {
+ element.appendChild(child instanceof Node ? child : document.createTextNode(child));
+ }
+ }
delete opt.appendChild;
delete opt.tag;
if (opt.dataset) {