usercss validator: more precise error position report
This commit is contained in:
parent
06a2a4c04d
commit
d660e6bd72
|
@ -244,13 +244,20 @@ function createSourceEditor(style) {
|
|||
function drawLinePointer(pos) {
|
||||
const SIZE = 60;
|
||||
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 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 leftPad = start !== 0 ? '...' : '';
|
||||
const rightPad = end !== line.length ? '...' : '';
|
||||
return leftPad + line.slice(start, end) + rightPad + '\n' +
|
||||
' '.repeat(leftPad.length) + pointer.slice(start, end);
|
||||
return (
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
204
js/usercss.js
204
js/usercss.js
|
@ -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_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) {
|
||||
const commentRe = /\/\*[\s\S]*?\*\//g;
|
||||
|
@ -119,9 +122,10 @@ var usercss = (() => {
|
|||
}
|
||||
|
||||
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) {
|
||||
throw new Error(error);
|
||||
throw new Error((state.errorPrefix || '') + error);
|
||||
}
|
||||
state.value = match[1];
|
||||
state.re.lastIndex += match[0].length;
|
||||
|
@ -139,6 +143,7 @@ var usercss = (() => {
|
|||
|
||||
parseWord(state, 'missing type');
|
||||
result.type = state.type = state.value;
|
||||
|
||||
if (!META_VARS.includes(state.type)) {
|
||||
throw new Error(`unknown type: ${state.type}`);
|
||||
}
|
||||
|
@ -149,52 +154,69 @@ var usercss = (() => {
|
|||
parseString(state);
|
||||
result.label = state.value;
|
||||
|
||||
if (state.type === 'checkbox') {
|
||||
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 = {};
|
||||
const {re, type, text} = state;
|
||||
|
||||
parseStringUnquoted(state);
|
||||
option.name = state.value;
|
||||
|
||||
parseString(state);
|
||||
option.label = state.value;
|
||||
|
||||
if (state.type === 'dropdown') {
|
||||
parseEOT(state);
|
||||
} else {
|
||||
parseString(state);
|
||||
switch (type === 'image' && state.key === 'var' ? '@image@var' : type) {
|
||||
case 'checkbox': {
|
||||
const match = text.slice(re.lastIndex).match(/([01])\s+/);
|
||||
if (!match) {
|
||||
throw new Error('value must be 0 or 1');
|
||||
}
|
||||
option.value = state.value;
|
||||
|
||||
result.options.push(option);
|
||||
re.lastIndex += match[0].length;
|
||||
result.default = match[1];
|
||||
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;
|
||||
validVar(result);
|
||||
|
@ -228,19 +250,20 @@ var usercss = (() => {
|
|||
}
|
||||
|
||||
function parseStringUnquoted(state) {
|
||||
const re = /[^"]*/y;
|
||||
re.lastIndex = state.re.lastIndex;
|
||||
const match = state.text.match(re);
|
||||
state.re.lastIndex += match[0].length;
|
||||
state.value = match[0].trim().replace(/\s+/g, '-');
|
||||
const pos = state.re.lastIndex;
|
||||
const nextQuoteOrEOL = posOrEnd(state.text, '"', pos);
|
||||
state.re.lastIndex = Math.max(0, nextQuoteOrEOL - 1);
|
||||
state.value = state.text.slice(pos, nextQuoteOrEOL).trim().replace(/\s+/g, '-');
|
||||
}
|
||||
|
||||
function parseString(state) {
|
||||
const match = state.text.slice(state.re.lastIndex).match(
|
||||
state.text[state.re.lastIndex] === '`' ?
|
||||
/^(`(?:\\`|[\s\S])*?`)\s*/ :
|
||||
/^((['"])(?:\\\2|[^\n])*?\2|\w+)\s*/
|
||||
);
|
||||
const pos = state.re.lastIndex;
|
||||
const rx = state.text[pos] === '`' ? RX_STRING_BACKTICK : RX_STRING_QUOTED;
|
||||
rx.lastIndex = pos;
|
||||
const match = rx.exec(state.text);
|
||||
if (!match) {
|
||||
throw new Error((state.errorPrefix || '') + 'Quoted string expected');
|
||||
}
|
||||
state.re.lastIndex += match[0].length;
|
||||
state.value = unquote(match[1]);
|
||||
}
|
||||
|
@ -252,59 +275,60 @@ var usercss = (() => {
|
|||
'true': true,
|
||||
'false': false
|
||||
};
|
||||
if (state.text[state.re.lastIndex] === '{') {
|
||||
const {text, re, errorPrefix} = state;
|
||||
if (text[re.lastIndex] === '{') {
|
||||
// object
|
||||
const obj = {};
|
||||
state.re.lastIndex++;
|
||||
re.lastIndex++;
|
||||
eatWhitespace(state);
|
||||
while (state.text[state.re.lastIndex] !== '}') {
|
||||
while (text[re.lastIndex] !== '}') {
|
||||
parseString(state);
|
||||
const key = state.value;
|
||||
if (state.text[state.re.lastIndex] !== ':') {
|
||||
throw new Error('missing \':\'');
|
||||
if (text[re.lastIndex] !== ':') {
|
||||
throw new Error(`${errorPrefix}missing ':'`);
|
||||
}
|
||||
state.re.lastIndex++;
|
||||
re.lastIndex++;
|
||||
eatWhitespace(state);
|
||||
parseJSONValue(state);
|
||||
obj[key] = state.value;
|
||||
if (state.text[state.re.lastIndex] === ',') {
|
||||
state.re.lastIndex++;
|
||||
if (text[re.lastIndex] === ',') {
|
||||
re.lastIndex++;
|
||||
eatWhitespace(state);
|
||||
} else if (state.text[state.re.lastIndex] !== '}') {
|
||||
throw new Error('missing \',\' or \'}\'');
|
||||
} else if (text[re.lastIndex] !== '}') {
|
||||
throw new Error(`${errorPrefix}missing ',' or '}'`);
|
||||
}
|
||||
}
|
||||
state.re.lastIndex++;
|
||||
re.lastIndex++;
|
||||
eatWhitespace(state);
|
||||
state.value = obj;
|
||||
} else if (state.text[state.re.lastIndex] === '[') {
|
||||
} else if (text[re.lastIndex] === '[') {
|
||||
// array
|
||||
const arr = [];
|
||||
state.re.lastIndex++;
|
||||
re.lastIndex++;
|
||||
eatWhitespace(state);
|
||||
while (state.text[state.re.lastIndex] !== ']') {
|
||||
while (text[re.lastIndex] !== ']') {
|
||||
parseJSONValue(state);
|
||||
arr.push(state.value);
|
||||
if (state.text[state.re.lastIndex] === ',') {
|
||||
state.re.lastIndex++;
|
||||
if (text[re.lastIndex] === ',') {
|
||||
re.lastIndex++;
|
||||
eatWhitespace(state);
|
||||
} else if (state.text[state.re.lastIndex] !== ']') {
|
||||
throw new Error('missing \',\' or \']\'');
|
||||
} else if (text[re.lastIndex] !== ']') {
|
||||
throw new Error(`${errorPrefix}missing ',' or ']'`);
|
||||
}
|
||||
}
|
||||
state.re.lastIndex++;
|
||||
re.lastIndex++;
|
||||
eatWhitespace(state);
|
||||
state.value = arr;
|
||||
} else if (state.text[state.re.lastIndex] === '"' || state.text[state.re.lastIndex] === '`') {
|
||||
} else if (text[re.lastIndex] === '"' || text[re.lastIndex] === '`') {
|
||||
// string
|
||||
parseString(state);
|
||||
} else if (/\d/.test(state.text[state.re.lastIndex])) {
|
||||
} else if (/\d/.test(text[re.lastIndex])) {
|
||||
// number
|
||||
parseNumber(state);
|
||||
} else {
|
||||
parseWord(state);
|
||||
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];
|
||||
}
|
||||
|
@ -314,7 +338,7 @@ var usercss = (() => {
|
|||
RX_NUMBER.lastIndex = state.re.lastIndex;
|
||||
const match = RX_NUMBER.exec(state.text);
|
||||
if (!match) {
|
||||
throw new Error('invalid number');
|
||||
throw new Error((state.errorPrefix || '') + 'invalid number');
|
||||
}
|
||||
state.value = Number(match[0].trim());
|
||||
state.re.lastIndex += match[0].length;
|
||||
|
@ -326,8 +350,9 @@ var usercss = (() => {
|
|||
}
|
||||
|
||||
function parseStringToEnd(state) {
|
||||
const EOL = state.text.indexOf('\n', state.re.lastIndex);
|
||||
const match = state.text.slice(state.re.lastIndex, EOL >= 0 ? EOL : undefined);
|
||||
rewindToEOL(state);
|
||||
const EOL = posOrEnd(state.text, '\n', state.re.lastIndex);
|
||||
const match = state.text.slice(state.re.lastIndex, EOL);
|
||||
state.value = unquote(match.trim());
|
||||
state.re.lastIndex += match.length;
|
||||
}
|
||||
|
@ -349,6 +374,15 @@ var usercss = (() => {
|
|||
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) {
|
||||
sourceCode = sourceCode.replace(/\r\n?/g, '\n');
|
||||
|
||||
|
@ -374,10 +408,6 @@ var usercss = (() => {
|
|||
if (!(state.key in METAS)) {
|
||||
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 === 'advanced') {
|
||||
state.maybeUSO = true;
|
||||
|
@ -404,7 +434,9 @@ var usercss = (() => {
|
|||
doParse();
|
||||
} catch (e) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user