StackGenVis: Alignment of Data, Algorithms, and Models for Stacking Ensemble Learning Using Performance Metrics
https://doi.org/10.1109/TVCG.2020.3030352
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
516 lines
16 KiB
516 lines
16 KiB
4 years ago
|
'use strict';
|
||
|
|
||
|
var names = require('../utils/names');
|
||
|
var MULTIPLIER_DEFAULT = {
|
||
|
comma: false,
|
||
|
min: 1,
|
||
|
max: 1
|
||
|
};
|
||
|
|
||
|
function skipSpaces(node) {
|
||
|
while (node !== null && (node.data.type === 'WhiteSpace' || node.data.type === 'Comment')) {
|
||
|
node = node.next;
|
||
|
}
|
||
|
|
||
|
return node;
|
||
|
}
|
||
|
|
||
|
function putResult(buffer, match) {
|
||
|
var type = match.type || match.syntax.type;
|
||
|
|
||
|
// ignore groups
|
||
|
if (type === 'Group') {
|
||
|
buffer.push.apply(buffer, match.match);
|
||
|
} else {
|
||
|
buffer.push(match);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function matchToJSON() {
|
||
|
return {
|
||
|
type: this.syntax.type,
|
||
|
name: this.syntax.name,
|
||
|
match: this.match,
|
||
|
node: this.node
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function buildMatchNode(badNode, lastNode, next, match) {
|
||
|
if (badNode) {
|
||
|
return {
|
||
|
badNode: badNode,
|
||
|
lastNode: null,
|
||
|
next: null,
|
||
|
match: null
|
||
|
};
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
badNode: null,
|
||
|
lastNode: lastNode,
|
||
|
next: next,
|
||
|
match: match
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function matchGroup(lexer, syntaxNode, node) {
|
||
|
var result = [];
|
||
|
var buffer;
|
||
|
var multiplier = syntaxNode.multiplier || MULTIPLIER_DEFAULT;
|
||
|
var min = multiplier.min;
|
||
|
var max = multiplier.max === 0 ? Infinity : multiplier.max;
|
||
|
var lastCommaTermCount;
|
||
|
var lastComma;
|
||
|
var matchCount = 0;
|
||
|
var lastNode = null;
|
||
|
var badNode = null;
|
||
|
|
||
|
mismatch:
|
||
|
while (matchCount < max) {
|
||
|
node = skipSpaces(node);
|
||
|
buffer = [];
|
||
|
|
||
|
switch (syntaxNode.combinator) {
|
||
|
case '|':
|
||
|
for (var i = 0; i < syntaxNode.terms.length; i++) {
|
||
|
var term = syntaxNode.terms[i];
|
||
|
var res = matchSyntax(lexer, term, node);
|
||
|
|
||
|
if (res.match) {
|
||
|
putResult(buffer, res.match);
|
||
|
node = res.next;
|
||
|
break; // continue matching
|
||
|
} else if (res.badNode) {
|
||
|
badNode = res.badNode;
|
||
|
break mismatch;
|
||
|
} else if (res.lastNode) {
|
||
|
lastNode = res.lastNode;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (buffer.length === 0) {
|
||
|
break mismatch; // nothing found -> stop matching
|
||
|
}
|
||
|
|
||
|
break;
|
||
|
|
||
|
case ' ':
|
||
|
var beforeMatchNode = node;
|
||
|
var lastMatchedTerm = null;
|
||
|
var hasTailMatch = false;
|
||
|
var commaMissed = false;
|
||
|
|
||
|
for (var i = 0; i < syntaxNode.terms.length; i++) {
|
||
|
var term = syntaxNode.terms[i];
|
||
|
var res = matchSyntax(lexer, term, node);
|
||
|
|
||
|
if (res.match) {
|
||
|
if (term.type === 'Comma' && i !== 0 && !hasTailMatch) {
|
||
|
// recover cursor to state before last match and stop matching
|
||
|
lastNode = node && node.data;
|
||
|
node = beforeMatchNode;
|
||
|
break mismatch;
|
||
|
}
|
||
|
|
||
|
// non-empty match (res.next will refer to another node)
|
||
|
if (res.next !== node) {
|
||
|
// match should be preceded by a comma
|
||
|
if (commaMissed) {
|
||
|
lastNode = node && node.data;
|
||
|
node = beforeMatchNode;
|
||
|
break mismatch;
|
||
|
}
|
||
|
|
||
|
hasTailMatch = term.type !== 'Comma';
|
||
|
lastMatchedTerm = term;
|
||
|
}
|
||
|
|
||
|
putResult(buffer, res.match);
|
||
|
node = skipSpaces(res.next);
|
||
|
} else if (res.badNode) {
|
||
|
badNode = res.badNode;
|
||
|
break mismatch;
|
||
|
} else {
|
||
|
if (res.lastNode) {
|
||
|
lastNode = res.lastNode;
|
||
|
}
|
||
|
|
||
|
// it's ok when comma doesn't match when no matches yet
|
||
|
// but only if comma is not first or last term
|
||
|
if (term.type === 'Comma' && i !== 0 && i !== syntaxNode.terms.length - 1) {
|
||
|
if (hasTailMatch) {
|
||
|
commaMissed = true;
|
||
|
}
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// recover cursor to state before last match and stop matching
|
||
|
lastNode = res.lastNode || (node && node.data);
|
||
|
node = beforeMatchNode;
|
||
|
break mismatch;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// don't allow empty match when [ ]!
|
||
|
if (!lastMatchedTerm && syntaxNode.disallowEmpty) {
|
||
|
// empty match but shouldn't
|
||
|
// recover cursor to state before last match and stop matching
|
||
|
lastNode = node && node.data;
|
||
|
node = beforeMatchNode;
|
||
|
break mismatch;
|
||
|
}
|
||
|
|
||
|
// don't allow comma at the end but only if last term isn't a comma
|
||
|
if (lastMatchedTerm && lastMatchedTerm.type === 'Comma' && term.type !== 'Comma') {
|
||
|
lastNode = node && node.data;
|
||
|
node = beforeMatchNode;
|
||
|
break mismatch;
|
||
|
}
|
||
|
|
||
|
break;
|
||
|
|
||
|
case '&&':
|
||
|
var beforeMatchNode = node;
|
||
|
var lastMatchedTerm = null;
|
||
|
var terms = syntaxNode.terms.slice();
|
||
|
|
||
|
while (terms.length) {
|
||
|
var wasMatch = false;
|
||
|
var emptyMatched = 0;
|
||
|
|
||
|
for (var i = 0; i < terms.length; i++) {
|
||
|
var term = terms[i];
|
||
|
var res = matchSyntax(lexer, term, node);
|
||
|
|
||
|
if (res.match) {
|
||
|
// non-empty match (res.next will refer to another node)
|
||
|
if (res.next !== node) {
|
||
|
lastMatchedTerm = term;
|
||
|
} else {
|
||
|
emptyMatched++;
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
wasMatch = true;
|
||
|
terms.splice(i--, 1);
|
||
|
putResult(buffer, res.match);
|
||
|
node = skipSpaces(res.next);
|
||
|
break;
|
||
|
} else if (res.badNode) {
|
||
|
badNode = res.badNode;
|
||
|
break mismatch;
|
||
|
} else if (res.lastNode) {
|
||
|
lastNode = res.lastNode;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!wasMatch) {
|
||
|
// terms left, but they all are optional
|
||
|
if (emptyMatched === terms.length) {
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
// not ok
|
||
|
lastNode = node && node.data;
|
||
|
node = beforeMatchNode;
|
||
|
break mismatch;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!lastMatchedTerm && syntaxNode.disallowEmpty) { // don't allow empty match when [ ]!
|
||
|
// empty match but shouldn't
|
||
|
// recover cursor to state before last match and stop matching
|
||
|
lastNode = node && node.data;
|
||
|
node = beforeMatchNode;
|
||
|
break mismatch;
|
||
|
}
|
||
|
|
||
|
break;
|
||
|
|
||
|
case '||':
|
||
|
var beforeMatchNode = node;
|
||
|
var lastMatchedTerm = null;
|
||
|
var terms = syntaxNode.terms.slice();
|
||
|
|
||
|
while (terms.length) {
|
||
|
var wasMatch = false;
|
||
|
var emptyMatched = 0;
|
||
|
|
||
|
for (var i = 0; i < terms.length; i++) {
|
||
|
var term = terms[i];
|
||
|
var res = matchSyntax(lexer, term, node);
|
||
|
|
||
|
if (res.match) {
|
||
|
// non-empty match (res.next will refer to another node)
|
||
|
if (res.next !== node) {
|
||
|
lastMatchedTerm = term;
|
||
|
} else {
|
||
|
emptyMatched++;
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
wasMatch = true;
|
||
|
terms.splice(i--, 1);
|
||
|
putResult(buffer, res.match);
|
||
|
node = skipSpaces(res.next);
|
||
|
break;
|
||
|
} else if (res.badNode) {
|
||
|
badNode = res.badNode;
|
||
|
break mismatch;
|
||
|
} else if (res.lastNode) {
|
||
|
lastNode = res.lastNode;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!wasMatch) {
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// don't allow empty match
|
||
|
if (!lastMatchedTerm && (emptyMatched !== terms.length || syntaxNode.disallowEmpty)) {
|
||
|
// empty match but shouldn't
|
||
|
// recover cursor to state before last match and stop matching
|
||
|
lastNode = node && node.data;
|
||
|
node = beforeMatchNode;
|
||
|
break mismatch;
|
||
|
}
|
||
|
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
// flush buffer
|
||
|
result.push.apply(result, buffer);
|
||
|
matchCount++;
|
||
|
|
||
|
if (!node) {
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
if (multiplier.comma) {
|
||
|
if (lastComma && lastCommaTermCount === result.length) {
|
||
|
// nothing match after comma
|
||
|
break mismatch;
|
||
|
}
|
||
|
|
||
|
node = skipSpaces(node);
|
||
|
if (node !== null && node.data.type === 'Operator' && node.data.value === ',') {
|
||
|
result.push({
|
||
|
syntax: syntaxNode,
|
||
|
match: [{
|
||
|
type: 'ASTNode',
|
||
|
node: node.data,
|
||
|
childrenMatch: null
|
||
|
}]
|
||
|
});
|
||
|
lastCommaTermCount = result.length;
|
||
|
lastComma = node;
|
||
|
node = node.next;
|
||
|
} else {
|
||
|
lastNode = node !== null ? node.data : null;
|
||
|
break mismatch;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// console.log(syntaxNode.type, badNode, lastNode);
|
||
|
|
||
|
if (lastComma && lastCommaTermCount === result.length) {
|
||
|
// nothing match after comma
|
||
|
node = lastComma;
|
||
|
result.pop();
|
||
|
}
|
||
|
|
||
|
return buildMatchNode(badNode, lastNode, node, matchCount < min ? null : {
|
||
|
syntax: syntaxNode,
|
||
|
match: result,
|
||
|
toJSON: matchToJSON
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function matchSyntax(lexer, syntaxNode, node) {
|
||
|
var badNode = null;
|
||
|
var lastNode = null;
|
||
|
var match = null;
|
||
|
|
||
|
switch (syntaxNode.type) {
|
||
|
case 'Group':
|
||
|
return matchGroup(lexer, syntaxNode, node);
|
||
|
|
||
|
case 'Function':
|
||
|
// expect a function node
|
||
|
if (!node || node.data.type !== 'Function') {
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
var keyword = names.keyword(node.data.name);
|
||
|
var name = syntaxNode.name.toLowerCase();
|
||
|
|
||
|
// check function name with vendor consideration
|
||
|
if (name !== keyword.name) {
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
var res = matchSyntax(lexer, syntaxNode.children, node.data.children.head);
|
||
|
if (!res.match || res.next) {
|
||
|
badNode = res.badNode || res.lastNode || (res.next ? res.next.data : null) || node.data;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
match = [{
|
||
|
type: 'ASTNode',
|
||
|
node: node.data,
|
||
|
childrenMatch: res.match.match
|
||
|
}];
|
||
|
|
||
|
// Use node.next instead of res.next here since syntax is matching
|
||
|
// for internal list and it should be completelly matched (res.next is null at this point).
|
||
|
// Therefore function is matched and we are going to next node
|
||
|
node = node.next;
|
||
|
break;
|
||
|
|
||
|
case 'Parentheses':
|
||
|
if (!node || node.data.type !== 'Parentheses') {
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
var res = matchSyntax(lexer, syntaxNode.children, node.data.children.head);
|
||
|
if (!res.match || res.next) {
|
||
|
badNode = res.badNode || res.lastNode || (res.next ? res.next.data : null) || node.data; // TODO: case when res.next === null
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
match = [{
|
||
|
type: 'ASTNode',
|
||
|
node: node.data,
|
||
|
childrenMatch: res.match.match
|
||
|
}];
|
||
|
|
||
|
node = res.next;
|
||
|
break;
|
||
|
|
||
|
case 'Type':
|
||
|
var typeSyntax = lexer.getType(syntaxNode.name);
|
||
|
if (!typeSyntax) {
|
||
|
throw new Error('Unknown syntax type `' + syntaxNode.name + '`');
|
||
|
}
|
||
|
|
||
|
var res = typeSyntax.match(node);
|
||
|
if (!res.match) {
|
||
|
badNode = res && res.badNode; // TODO: case when res.next === null
|
||
|
lastNode = (res && res.lastNode) || (node && node.data);
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
node = res.next;
|
||
|
putResult(match = [], res.match);
|
||
|
if (match.length === 0) {
|
||
|
match = null;
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case 'Property':
|
||
|
var propertySyntax = lexer.getProperty(syntaxNode.name);
|
||
|
if (!propertySyntax) {
|
||
|
throw new Error('Unknown property `' + syntaxNode.name + '`');
|
||
|
}
|
||
|
|
||
|
var res = propertySyntax.match(node);
|
||
|
if (!res.match) {
|
||
|
badNode = res && res.badNode; // TODO: case when res.next === null
|
||
|
lastNode = (res && res.lastNode) || (node && node.data);
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
node = res.next;
|
||
|
putResult(match = [], res.match);
|
||
|
if (match.length === 0) {
|
||
|
match = null;
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case 'Keyword':
|
||
|
if (!node) {
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
if (node.data.type === 'Identifier') {
|
||
|
var keyword = names.keyword(node.data.name);
|
||
|
var keywordName = keyword.name;
|
||
|
var name = syntaxNode.name.toLowerCase();
|
||
|
|
||
|
// drop \0 and \9 hack from keyword name
|
||
|
if (keywordName.indexOf('\\') !== -1) {
|
||
|
keywordName = keywordName.replace(/\\[09].*$/, '');
|
||
|
}
|
||
|
|
||
|
if (name !== keywordName) {
|
||
|
break;
|
||
|
}
|
||
|
} else {
|
||
|
// keyword may to be a number (e.g. font-weight: 400 )
|
||
|
if (node.data.type !== 'Number' || node.data.value !== syntaxNode.name) {
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
match = [{
|
||
|
type: 'ASTNode',
|
||
|
node: node.data,
|
||
|
childrenMatch: null
|
||
|
}];
|
||
|
node = node.next;
|
||
|
break;
|
||
|
|
||
|
case 'Slash':
|
||
|
case 'Comma':
|
||
|
if (!node || node.data.type !== 'Operator' || node.data.value !== syntaxNode.value) {
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
match = [{
|
||
|
type: 'ASTNode',
|
||
|
node: node.data,
|
||
|
childrenMatch: null
|
||
|
}];
|
||
|
node = node.next;
|
||
|
break;
|
||
|
|
||
|
case 'String':
|
||
|
if (!node || node.data.type !== 'String') {
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
match = [{
|
||
|
type: 'ASTNode',
|
||
|
node: node.data,
|
||
|
childrenMatch: null
|
||
|
}];
|
||
|
node = node.next;
|
||
|
break;
|
||
|
|
||
|
case 'ASTNode':
|
||
|
if (node && syntaxNode.match(node)) {
|
||
|
match = {
|
||
|
type: 'ASTNode',
|
||
|
node: node.data,
|
||
|
childrenMatch: null
|
||
|
};
|
||
|
node = node.next;
|
||
|
}
|
||
|
return buildMatchNode(badNode, lastNode, node, match);
|
||
|
|
||
|
default:
|
||
|
throw new Error('Not implemented yet node type: ' + syntaxNode.type);
|
||
|
}
|
||
|
|
||
|
return buildMatchNode(badNode, lastNode, node, match === null ? null : {
|
||
|
syntax: syntaxNode,
|
||
|
match: match,
|
||
|
toJSON: matchToJSON
|
||
|
});
|
||
|
|
||
|
};
|
||
|
|
||
|
module.exports = matchSyntax;
|