stylus/js/moz-parser.js
tophf fdbfb23547
API groups + use executeScript for early injection (#1149)
* parserlib: fast section extraction, tweaks and speedups
* csslint: "simple-not" rule
* csslint: enable and fix "selector-newline" rule
* simplify db: resolve with result
* simplify download()
* remove noCode param as it wastes more time/memory on copying
* styleManager: switch style<->data names to reflect their actual contents
* inline method bodies to avoid indirection and enable better autocomplete/hint/jump support in IDE
* upgrade getEventKeyName to handle mouse clicks
* don't trust location.href as it hides text fragment
* getAllKeys is implemented since Chrome48, FF44
* allow recoverable css errors + async'ify usercss.js
* openManage: unminimize windows
* remove the obsolete Chrome pre-65 workaround
* fix temporal dead zone in apply.js
* ff bug workaround for simple editor window
* consistent window scrolling in scrollToEditor and jumpToPos
* rework waitForSelector and collapsible <details>
* blank paint frame workaround for new Chrome
* extract stuff from edit.js and load on demand
* simplify regexpTester::isShown
* move MozDocMapper to sections-util.js
* extract fitSelectBox()
* initialize router earlier
* use helpPopup.close()
* fix autofocus in popups, follow-up to 5bb1b5ef
* clone objects in prefs.get() + cosmetics
* reuse getAll result for INC
2021-01-01 17:27:58 +03:00

147 lines
4.7 KiB
JavaScript

'use strict';
require([
'/js/csslint/parserlib', /* global parserlib */
'/js/sections-util', /* global MozDocMapper */
]);
/* exported extractSections */
/**
* Extracts @-moz-document blocks into sections and the code between them into global sections.
* Puts the global comments into the following section to minimize the amount of global sections.
* Doesn't move the comment with ==UserStyle== inside.
* @param {Object} _
* @param {string} _.code
* @param {boolean} [_.fast] - uses topDocOnly option to extract sections as text
* @param {number} [_.styleId] - used to preserve parserCache on subsequent runs over the same style
* @returns {{sections: Array, errors: Array}}
* @property {?number} lastStyleId
*/
function extractSections({code, styleId, fast = true}) {
const hasSingleEscapes = /([^\\]|^)\\([^\\]|$)/;
const parser = new parserlib.css.Parser({
starHack: true,
skipValidation: true,
topDocOnly: fast,
});
const sectionStack = [{code: '', start: 0}];
const errors = [];
const sections = [];
const mozStyle = code.replace(/\r\n?/g, '\n'); // same as parserlib.StringReader
parser.addListener('startdocument', e => {
const lastSection = sectionStack[sectionStack.length - 1];
let outerText = mozStyle.slice(lastSection.start, e.offset);
const lastCmt = getLastComment(outerText);
const section = {
code: '',
start: parser._tokenStream._token.offset + 1,
};
// move last comment before @-moz-document inside the section
if (!lastCmt.includes('AGENT_SHEET') &&
!/==userstyle==/i.test(lastCmt)) {
if (lastCmt) {
section.code = lastCmt + '\n';
outerText = outerText.slice(0, -lastCmt.length);
}
outerText = outerText.match(/^\s*/)[0] + outerText.trim();
}
if (outerText.trim()) {
lastSection.code = outerText;
doAddSection(lastSection);
lastSection.code = '';
}
for (const {name, expr, uri} of e.functions) {
const aType = MozDocMapper.FROM_CSS[name.toLowerCase()];
const p0 = expr && expr.parts[0];
if (p0 && aType === 'regexps') {
const s = p0.text;
if (hasSingleEscapes.test(p0.text)) {
const isQuoted = /^['"]/.test(s) && s.endsWith(s[0]);
p0.value = isQuoted ? s.slice(1, -1) : s;
}
}
(section[aType] = section[aType] || []).push(uri || p0 && p0.value || '');
}
sectionStack.push(section);
});
parser.addListener('enddocument', e => {
const section = sectionStack.pop();
const lastSection = sectionStack[sectionStack.length - 1];
section.code += mozStyle.slice(section.start, e.offset);
lastSection.start = e.offset + 1;
doAddSection(section);
});
parser.addListener('endstylesheet', () => {
// add nonclosed outer sections (either broken or the last global one)
const lastSection = sectionStack[sectionStack.length - 1];
lastSection.code += mozStyle.slice(lastSection.start);
sectionStack.forEach(doAddSection);
});
parser.addListener('error', e => {
errors.push(e);
});
try {
parser.parse(mozStyle, {
reuseCache: !extractSections.lastStyleId || styleId === extractSections.lastStyleId,
});
} catch (e) {
errors.push(e);
}
for (const err of errors) {
for (const [k, v] of Object.entries(err)) {
if (typeof v === 'object') delete err[k];
}
err.message = `${err.line}:${err.col} ${err.message}`;
}
extractSections.lastStyleId = styleId;
return {sections, errors};
function doAddSection(section) {
section.code = section.code.trim();
// don't add empty sections
if (
!section.code &&
!section.urls &&
!section.urlPrefixes &&
!section.domains &&
!section.regexps
) {
return;
}
/* ignore boilerplate NS */
if (section.code === '@namespace url(http://www.w3.org/1999/xhtml);') {
return;
}
sections.push(Object.assign({}, section));
}
function getLastComment(text) {
let open = text.length;
let close;
while (open) {
// at this point we're guaranteed to be outside of a comment
close = text.lastIndexOf('*/', open - 2);
if (close < 0) {
break;
}
// stop if a non-whitespace precedes and return what we currently have
const tailEmpty = !text.substring(close + 2, open).trim();
if (!tailEmpty) {
break;
}
// find a closed preceding comment
const prevClose = text.lastIndexOf('*/', close - 2);
// then find the real start of current comment
// e.g. /* preceding */ /* current /* current /* current */
open = text.indexOf('/*', prevClose < 0 ? 0 : prevClose + 2);
}
return open ? text.slice(open) : text;
}
}