usercss validator: more precise error position report

This commit is contained in:
tophf 2017-11-27 12:44:16 +03:00
parent 06a2a4c04d
commit d660e6bd72
2 changed files with 127 additions and 88 deletions

View File

@ -244,13 +244,20 @@ function createSourceEditor(style) {
function drawLinePointer(pos) { function drawLinePointer(pos) {
const SIZE = 60; const SIZE = 60;
const line = cm.getLine(pos.line); const line = cm.getLine(pos.line);
const numTabs = pos.ch + 1 - line.slice(0, pos.ch + 1).replace(/\t/g, '').length;
const pointer = ' '.repeat(pos.ch) + '^'; const pointer = ' '.repeat(pos.ch) + '^';
const start = Math.max(Math.min(pos.ch - SIZE / 2, line.length - SIZE), 0); const start = Math.max(Math.min(pos.ch - SIZE / 2, line.length - SIZE), 0);
const end = Math.min(Math.max(pos.ch + SIZE / 2, SIZE), line.length); const end = Math.min(Math.max(pos.ch + SIZE / 2, SIZE), line.length);
const leftPad = start !== 0 ? '...' : ''; const leftPad = start !== 0 ? '...' : '';
const rightPad = end !== line.length ? '...' : ''; const rightPad = end !== line.length ? '...' : '';
return leftPad + line.slice(start, end) + rightPad + '\n' + return (
' '.repeat(leftPad.length) + pointer.slice(start, end); leftPad +
line.slice(start, end).replace(/\t/g, ' '.repeat(cm.options.tabSize)) +
rightPad +
'\n' +
' '.repeat(leftPad.length + numTabs * cm.options.tabSize) +
pointer.slice(start, end)
);
} }
} }

View File

@ -96,8 +96,11 @@ var usercss = (() => {
} }
}; };
const RX_NUMBER = /^-?\d+(\.\d+)?\s*/y; const RX_NUMBER = /-?\d+(\.\d+)?\s*/y;
const RX_WHITESPACE = /\s*/y; const RX_WHITESPACE = /\s*/y;
const RX_WORD = /([\w-]+)\s*/y;
const RX_STRING_BACKTICK = /(`(?:\\`|[\s\S])*?`)\s*/y;
const RX_STRING_QUOTED = /((['"])(?:\\\2|[^\n])*?\2|\w+)\s*/y;
function getMetaSource(source) { function getMetaSource(source) {
const commentRe = /\/\*[\s\S]*?\*\//g; const commentRe = /\/\*[\s\S]*?\*\//g;
@ -119,9 +122,10 @@ var usercss = (() => {
} }
function parseWord(state, error = 'invalid word') { function parseWord(state, error = 'invalid word') {
const match = state.text.slice(state.re.lastIndex).match(/^([\w-]+)\s*/); RX_WORD.lastIndex = state.re.lastIndex;
const match = RX_WORD.exec(state.text);
if (!match) { if (!match) {
throw new Error(error); throw new Error((state.errorPrefix || '') + error);
} }
state.value = match[1]; state.value = match[1];
state.re.lastIndex += match[0].length; state.re.lastIndex += match[0].length;
@ -139,6 +143,7 @@ var usercss = (() => {
parseWord(state, 'missing type'); parseWord(state, 'missing type');
result.type = state.type = state.value; result.type = state.type = state.value;
if (!META_VARS.includes(state.type)) { if (!META_VARS.includes(state.type)) {
throw new Error(`unknown type: ${state.type}`); throw new Error(`unknown type: ${state.type}`);
} }
@ -149,52 +154,69 @@ var usercss = (() => {
parseString(state); parseString(state);
result.label = state.value; result.label = state.value;
if (state.type === 'checkbox') { const {re, type, text} = state;
const match = state.text.slice(state.re.lastIndex).match(/([01])\s+/);
if (!match) {
throw new Error('value must be 0 or 1');
}
state.re.lastIndex += match[0].length;
result.default = match[1];
} else if (state.type === 'select' || (state.type === 'image' && state.key === 'var')) {
parseJSONValue(state);
if (Array.isArray(state.value)) {
result.options = state.value.map(text => createOption(text));
} else {
result.options = Object.keys(state.value).map(k => createOption(k, state.value[k]));
}
result.default = result.options[0].name;
} else if (state.type === 'dropdown' || state.type === 'image') {
if (state.text[state.re.lastIndex] !== '{') {
throw new Error('no open {');
}
result.options = [];
state.re.lastIndex++;
while (state.text[state.re.lastIndex] !== '}') {
const option = {};
parseStringUnquoted(state); switch (type === 'image' && state.key === 'var' ? '@image@var' : type) {
option.name = state.value; case 'checkbox': {
const match = text.slice(re.lastIndex).match(/([01])\s+/);
parseString(state); if (!match) {
option.label = state.value; throw new Error('value must be 0 or 1');
if (state.type === 'dropdown') {
parseEOT(state);
} else {
parseString(state);
} }
option.value = state.value; re.lastIndex += match[0].length;
result.default = match[1];
result.options.push(option); break;
}
case 'select':
case '@image@var': {
state.errorPrefix = 'Invalid JSON: ';
parseJSONValue(state);
state.errorPrefix = '';
if (Array.isArray(state.value)) {
result.options = state.value.map(text => createOption(text));
} else {
result.options = Object.keys(state.value).map(k => createOption(k, state.value[k]));
}
result.default = (result.options[0] || {}).name || '';
break;
}
case 'dropdown':
case 'image': {
if (text[re.lastIndex] !== '{') {
throw new Error('no open {');
}
result.options = [];
re.lastIndex++;
while (text[re.lastIndex] !== '}') {
const option = {};
parseStringUnquoted(state);
option.name = state.value;
parseString(state);
option.label = state.value;
if (type === 'dropdown') {
parseEOT(state);
} else {
parseString(state);
}
option.value = state.value;
result.options.push(option);
}
re.lastIndex++;
eatWhitespace(state);
result.default = result.options[0].name;
break;
}
default: {
// text, color
parseStringToEnd(state);
result.default = state.value;
} }
state.re.lastIndex++;
eatWhitespace(state);
result.default = result.options[0].name;
} else {
// text, color
parseStringToEnd(state);
result.default = state.value;
} }
state.usercssData.vars[result.name] = result; state.usercssData.vars[result.name] = result;
validVar(result); validVar(result);
@ -228,19 +250,20 @@ var usercss = (() => {
} }
function parseStringUnquoted(state) { function parseStringUnquoted(state) {
const re = /[^"]*/y; const pos = state.re.lastIndex;
re.lastIndex = state.re.lastIndex; const nextQuoteOrEOL = posOrEnd(state.text, '"', pos);
const match = state.text.match(re); state.re.lastIndex = Math.max(0, nextQuoteOrEOL - 1);
state.re.lastIndex += match[0].length; state.value = state.text.slice(pos, nextQuoteOrEOL).trim().replace(/\s+/g, '-');
state.value = match[0].trim().replace(/\s+/g, '-');
} }
function parseString(state) { function parseString(state) {
const match = state.text.slice(state.re.lastIndex).match( const pos = state.re.lastIndex;
state.text[state.re.lastIndex] === '`' ? const rx = state.text[pos] === '`' ? RX_STRING_BACKTICK : RX_STRING_QUOTED;
/^(`(?:\\`|[\s\S])*?`)\s*/ : rx.lastIndex = pos;
/^((['"])(?:\\\2|[^\n])*?\2|\w+)\s*/ const match = rx.exec(state.text);
); if (!match) {
throw new Error((state.errorPrefix || '') + 'Quoted string expected');
}
state.re.lastIndex += match[0].length; state.re.lastIndex += match[0].length;
state.value = unquote(match[1]); state.value = unquote(match[1]);
} }
@ -252,59 +275,60 @@ var usercss = (() => {
'true': true, 'true': true,
'false': false 'false': false
}; };
if (state.text[state.re.lastIndex] === '{') { const {text, re, errorPrefix} = state;
if (text[re.lastIndex] === '{') {
// object // object
const obj = {}; const obj = {};
state.re.lastIndex++; re.lastIndex++;
eatWhitespace(state); eatWhitespace(state);
while (state.text[state.re.lastIndex] !== '}') { while (text[re.lastIndex] !== '}') {
parseString(state); parseString(state);
const key = state.value; const key = state.value;
if (state.text[state.re.lastIndex] !== ':') { if (text[re.lastIndex] !== ':') {
throw new Error('missing \':\''); throw new Error(`${errorPrefix}missing ':'`);
} }
state.re.lastIndex++; re.lastIndex++;
eatWhitespace(state); eatWhitespace(state);
parseJSONValue(state); parseJSONValue(state);
obj[key] = state.value; obj[key] = state.value;
if (state.text[state.re.lastIndex] === ',') { if (text[re.lastIndex] === ',') {
state.re.lastIndex++; re.lastIndex++;
eatWhitespace(state); eatWhitespace(state);
} else if (state.text[state.re.lastIndex] !== '}') { } else if (text[re.lastIndex] !== '}') {
throw new Error('missing \',\' or \'}\''); throw new Error(`${errorPrefix}missing ',' or '}'`);
} }
} }
state.re.lastIndex++; re.lastIndex++;
eatWhitespace(state); eatWhitespace(state);
state.value = obj; state.value = obj;
} else if (state.text[state.re.lastIndex] === '[') { } else if (text[re.lastIndex] === '[') {
// array // array
const arr = []; const arr = [];
state.re.lastIndex++; re.lastIndex++;
eatWhitespace(state); eatWhitespace(state);
while (state.text[state.re.lastIndex] !== ']') { while (text[re.lastIndex] !== ']') {
parseJSONValue(state); parseJSONValue(state);
arr.push(state.value); arr.push(state.value);
if (state.text[state.re.lastIndex] === ',') { if (text[re.lastIndex] === ',') {
state.re.lastIndex++; re.lastIndex++;
eatWhitespace(state); eatWhitespace(state);
} else if (state.text[state.re.lastIndex] !== ']') { } else if (text[re.lastIndex] !== ']') {
throw new Error('missing \',\' or \']\''); throw new Error(`${errorPrefix}missing ',' or ']'`);
} }
} }
state.re.lastIndex++; re.lastIndex++;
eatWhitespace(state); eatWhitespace(state);
state.value = arr; state.value = arr;
} else if (state.text[state.re.lastIndex] === '"' || state.text[state.re.lastIndex] === '`') { } else if (text[re.lastIndex] === '"' || text[re.lastIndex] === '`') {
// string // string
parseString(state); parseString(state);
} else if (/\d/.test(state.text[state.re.lastIndex])) { } else if (/\d/.test(text[re.lastIndex])) {
// number // number
parseNumber(state); parseNumber(state);
} else { } else {
parseWord(state); parseWord(state);
if (!(state.value in JSON_PRIME)) { if (!(state.value in JSON_PRIME)) {
throw new Error(`unknown literal '${state.value}'`); throw new Error(`${errorPrefix}unknown literal '${state.value}'`);
} }
state.value = JSON_PRIME[state.value]; state.value = JSON_PRIME[state.value];
} }
@ -314,7 +338,7 @@ var usercss = (() => {
RX_NUMBER.lastIndex = state.re.lastIndex; RX_NUMBER.lastIndex = state.re.lastIndex;
const match = RX_NUMBER.exec(state.text); const match = RX_NUMBER.exec(state.text);
if (!match) { if (!match) {
throw new Error('invalid number'); throw new Error((state.errorPrefix || '') + 'invalid number');
} }
state.value = Number(match[0].trim()); state.value = Number(match[0].trim());
state.re.lastIndex += match[0].length; state.re.lastIndex += match[0].length;
@ -326,8 +350,9 @@ var usercss = (() => {
} }
function parseStringToEnd(state) { function parseStringToEnd(state) {
const EOL = state.text.indexOf('\n', state.re.lastIndex); rewindToEOL(state);
const match = state.text.slice(state.re.lastIndex, EOL >= 0 ? EOL : undefined); const EOL = posOrEnd(state.text, '\n', state.re.lastIndex);
const match = state.text.slice(state.re.lastIndex, EOL);
state.value = unquote(match.trim()); state.value = unquote(match.trim());
state.re.lastIndex += match.length; state.re.lastIndex += match.length;
} }
@ -349,6 +374,15 @@ var usercss = (() => {
return s; return s;
} }
function posOrEnd(haystack, needle, start) {
const pos = haystack.indexOf(needle, start);
return pos < 0 ? haystack.length : pos;
}
function rewindToEOL({re, text}) {
re.lastIndex -= text[re.lastIndex - 1] === '\n' ? 1 : 0;
}
function buildMeta(sourceCode) { function buildMeta(sourceCode) {
sourceCode = sourceCode.replace(/\r\n?/g, '\n'); sourceCode = sourceCode.replace(/\r\n?/g, '\n');
@ -374,10 +408,6 @@ var usercss = (() => {
if (!(state.key in METAS)) { if (!(state.key in METAS)) {
continue; continue;
} }
if (text[re.lastIndex - 1] === '\n') {
// an empty value should point to EOL
re.lastIndex--;
}
if (state.key === 'var' || state.key === 'advanced') { if (state.key === 'var' || state.key === 'advanced') {
if (state.key === 'advanced') { if (state.key === 'advanced') {
state.maybeUSO = true; state.maybeUSO = true;
@ -404,7 +434,9 @@ var usercss = (() => {
doParse(); doParse();
} catch (e) { } catch (e) {
// grab additional info // grab additional info
e.index = metaIndex + state.re.lastIndex; let pos = state.re.lastIndex;
while (pos && /[\s\n]/.test(state.text[--pos])) { /**/ }
e.index = metaIndex + pos;
throw e; throw e;
} }