stylus/js/moz-parser.js
2017-08-30 17:41:07 +08:00

150 lines
5.3 KiB
JavaScript

/* global parserlib, loadScript */
'use strict';
// eslint-disable-next-line no-var
var mozParser = (function () {
// direct & reverse mapping of @-moz-document keywords and internal property names
const propertyToCss = {urls: 'url', urlPrefixes: 'url-prefix', domains: 'domain', regexps: 'regexp'};
const CssToProperty = {'url': 'urls', 'url-prefix': 'urlPrefixes', 'domain': 'domains', 'regexp': 'regexps'};
function backtrackTo(parser, tokenType, startEnd) {
const tokens = parser._tokenStream._lt;
for (let i = parser._tokenStream._ltIndex - 1; i >= 0; --i) {
if (tokens[i].type === tokenType) {
return {line: tokens[i][startEnd + 'Line'], col: tokens[i][startEnd + 'Col']};
}
}
}
function trimNewLines(s) {
return s.replace(/^[\s\n]+/, '').replace(/[\s\n]+$/, '');
}
function parseMozFormat(mozStyle) {
return new Promise((resolve, reject) => {
const parser = new parserlib.css.Parser();
const lines = mozStyle.split('\n');
const sectionStack = [{code: '', start: {line: 1, col: 1}}];
const errors = [];
const sections = [];
parser.addListener('startdocument', function (e) {
const lastSection = sectionStack[sectionStack.length - 1];
let outerText = getRange(lastSection.start, (--e.col, e));
const gapComment = outerText.match(/(\/\*[\s\S]*?\*\/)[\s\n]*$/);
const section = {code: '', start: backtrackTo(this, parserlib.css.Tokens.LBRACE, 'end')};
// move last comment before @-moz-document inside the section
if (gapComment && !gapComment[1].match(/\/\*\s*AGENT_SHEET\s*\*\//)) {
section.code = gapComment[1] + '\n';
outerText = trimNewLines(outerText.substring(0, gapComment.index));
}
if (outerText.trim()) {
lastSection.code = outerText;
doAddSection(lastSection);
lastSection.code = '';
}
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);
});
parser.addListener('enddocument', function () {
const end = backtrackTo(this, parserlib.css.Tokens.RBRACE, 'start');
const section = sectionStack.pop();
const lastSection = sectionStack[sectionStack.length - 1];
section.code += getRange(section.start, end);
lastSection.start = (++end.col, end);
doAddSection(section);
});
parser.addListener('endstylesheet', () => {
// add nonclosed outer sections (either broken or the last global one)
const lastLine = lines[lines.length - 1];
const endOfText = {line: lines.length, col: lastLine.length + 1};
const lastSection = sectionStack[sectionStack.length - 1];
lastSection.code += getRange(lastSection.start, endOfText);
sectionStack.forEach(doAddSection);
if (errors.length) {
reject(errors);
} else {
resolve(sections);
}
});
parser.addListener('error', e => {
errors.push(e.line + ':' + e.col + ' ' +
e.message.replace(/ at line \d.+$/, ''));
});
parser.parse(mozStyle);
function getRange(start, end) {
const L1 = start.line - 1;
const C1 = start.col - 1;
const L2 = end.line - 1;
const C2 = end.col - 1;
if (L1 === L2) {
return lines[L1].substr(C1, C2 - C1 + 1);
} else {
const middle = lines.slice(L1 + 1, L2).join('\n');
return lines[L1].substr(C1) + '\n' + middle +
(L2 >= lines.length ? '' : ((middle ? '\n' : '') + lines[L2].substring(0, C2)));
}
}
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));
}
});
}
return {
// Parse mozilla-format userstyle into sections
parse(text) {
if (typeof parserlib === 'undefined') {
return loadScript('vendor-overwrites/csslint/csslint-worker.js')
.then(() => parseMozFormat(text));
}
return parseMozFormat(text);
},
format(style) {
return style.sections.map(section => {
let cssMds = [];
for (const i in propertyToCss) {
if (section[i]) {
cssMds = cssMds.concat(section[i].map(v =>
propertyToCss[i] + '("' + v.replace(/\\/g, '\\\\') + '")'
));
}
}
return cssMds.length ? '@-moz-document ' + cssMds.join(', ') + ' {\n' + section.code + '\n}' : section.code;
}).join('\n\n');
}
};
})();