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) {
|
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)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
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_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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user