Skip to content

Commit

Permalink
feat: print better error message on unpaired @if
Browse files Browse the repository at this point in the history
Drop xregexp too.
  • Loading branch information
3cp committed May 15, 2020
1 parent 996ede8 commit 0126c48
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 39 deletions.
172 changes: 136 additions & 36 deletions lib/preprocess/index.js
Original file line number Diff line number Diff line change
@@ -1,43 +1,144 @@
const XRegExp = require('xregexp');
const { codeFrameColumns } = require('@babel/code-frame');
const {html, js} = require('./regex-rules');
const applicable = require('../applicable');

class Token {
constructor(start, end, value) {
this.start = start;
this.end = end;
this.value = value;
}
}

class MatchToken extends Token {
constructor(match) {
super(match.index, match.index + match[0].length, match[0]);
}
}

class StartMatchToken extends MatchToken {
constructor(match) {
super(match);
this.expression = match[1];
}
}

class EndMatchToken extends MatchToken {
constructor(match) {
super(match);
}
}

function replaceRecursive(rv, rule, cb) {
const startRegex = new RegExp(rule.start, 'mi');
const endRegex = new RegExp(rule.end, 'mi');

function matchReplacePass(content) {
const matches = XRegExp.matchRecursive(content, rule.start, rule.end, 'gmi', {
valueNames: ['between', 'left', 'match', 'right']
});

const matchGroup = {
left: null,
match: null,
right: null
const startRegex = new RegExp(rule.start, 'gmi');
const endRegex = new RegExp(rule.end, 'gmi');
const tokens = [];
let m;

while ((m = startRegex.exec(rv)) !== null) {
tokens.push(new StartMatchToken(m));
}

while ((m = endRegex.exec(rv)) !== null) {
tokens.push(new EndMatchToken(m));
}

tokens.sort((a, b) => a.start - b.start);

// Fill up in-between tokens
let end = rv.length;
for (let i = tokens.length - 1; i >= 0; i--) {
const token = tokens[i];
if (token.end < end) {
tokens.splice(i + 1, 0, new Token(token.end, end, rv.slice(token.end, end)));
}
end = token.start;
}

if (!tokens.length) {
tokens.unshift(new Token(0, rv.length, rv));
} else if (tokens[0].start > 0) {
tokens.unshift(new Token(0, tokens[0].start, rv.slice(0, tokens[0].start)));
}

// Fill up line/column for error print
let lastLine = 1, lastColumn = 0;
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
const loc = {
start: {
line: lastLine,
column: lastColumn
}
};
for (let c of token.value) {
if (c === '\n') { // only support \r\n or \n
lastLine += 1;
lastColumn = 0;
} else {
lastColumn += 1;
}
}
loc.end = {
line: lastLine,
column: lastColumn
};
token.loc = loc;
}

return matches.reduce((builder, match) => {
switch(match.name) {
case 'between':
builder += match.value;
break;
case 'left':
matchGroup.left = startRegex.exec(match.value);
break;
case 'match':
matchGroup.match = match.value;
break;
case 'right':
matchGroup.right = endRegex.exec(match.value);
builder += cb(matchGroup.left, matchGroup.right, matchGroup.match, matchReplacePass);
break;
function matchReplacePass(tokens) {
let depth = 0;
let start = null;
let inside = [];
let builder = '';
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (token instanceof StartMatchToken) {
if (depth === 0) {
// Found first @if
start = token;
} else {
inside.push(token);
}
depth += 1;
} else if (token instanceof EndMatchToken) {
if (depth === 0) {
throw new Error(
'Could not find a starting @if for the @endif\n' +
codeFrameColumns(rv, token.loc)
);
} else if (depth === 1) {
// Got a pair
builder += cb(start.expression, inside, matchReplacePass);
// Reset
start = null;
inside = [];
} else {
inside.push(token);
}
depth -= 1;
} else {
if (depth === 0) {
// in-between
builder += token.value;
} else {
// inside a pair
inside.push(token);
}
}
return builder;
}, '');
}

if (depth > 0) {
throw new Error(
'Could not find a ending @endif for the @if\n' +
codeFrameColumns(rv, start.loc)
);
}

return builder;
}

return matchReplacePass(rv);
return matchReplacePass(tokens);
}

function replace(rv, rule, cb) {
Expand Down Expand Up @@ -75,13 +176,12 @@ module.exports = function(source, properties, features, mode) {
// normalize line break
let rv = source.replace(/(?:\r\n)|\r/g, '\n');

rv = replaceRecursive(rv, rules.if, (startMatches, endMatches, include, recurse) =>
applicable(features, startMatches[1].trim()) ? recurse(include) : ''
rv = replaceRecursive(rv, rules.if, (expression, inside, recurse) =>
applicable(features, expression) ? recurse(inside) : ''
);

rv = replace(rv, rules.echo, (match, variable) => properties[variable] || '');

rv = replace(rv, rules.eval, (match, expression) => dangerousEval(expression, properties));
rv = replace(rv, rules.echo, (_, variable) => properties[variable] || '');
rv = replace(rv, rules.eval, (_, expression) => dangerousEval(expression, properties));

return rv;
};
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
},
"homepage": "https://github.com/makesjs/makes#readme",
"devDependencies": {
"@babel/code-frame": "^7.8.3",
"@zeit/ncc": "^0.22.1",
"ansi-colors": "^4.1.1",
"ava": "^3.8.2",
Expand All @@ -60,7 +61,6 @@
"standard-changelog": "^2.0.24",
"tar-fs": "^2.1.0",
"tmp": "^0.2.1",
"vinyl": "^2.2.0",
"xregexp": "^4.3.0"
"vinyl": "^2.2.0"
}
}
5 changes: 5 additions & 0 deletions test/preprocess/echo.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
const test = require('ava');
const preprocess = require('../../lib/preprocess');

test('preprocess can handle empty input', t => {
t.is(preprocess('', {}, null, 'html'), '');
t.is(preprocess('', {}, null), '');
});

test('@echo resolves and echoes variable in html syntax', t => {
t.is(
preprocess('a<!-- @echo FINGERPRINT -->c', {FINGERPRINT: '0xDEADBEEF'}, null, 'html'),
Expand Down
2 changes: 1 addition & 1 deletion test/write-project/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ test.serial('writeProject reports error', async t => {
targetDir: 'here'
});
} catch (e) {
t.is(e.message, `Error in skeleton file: ${path.join('skeleton', 'feature1', 'file.js')}\nUnbalanced delimiter found in string`);
t.truthy(e.message.startsWith(`Error in skeleton file: ${path.join('skeleton', 'feature1', 'file.js')}\nCould not find a ending @endif for the @if`));
return;
}

Expand Down

0 comments on commit 0126c48

Please sign in to comment.