196 lines
5 KiB
JavaScript
196 lines
5 KiB
JavaScript
|
"use strict";
|
||
|
function compile(nodes) {
|
||
|
var assignedPaths = [];
|
||
|
var valueAssignments = [];
|
||
|
var currentPath = "";
|
||
|
var data = Object.create(null);
|
||
|
var context = data;
|
||
|
var arrayMode = false;
|
||
|
|
||
|
return reduce(nodes);
|
||
|
|
||
|
function reduce(nodes) {
|
||
|
var node;
|
||
|
for (var i = 0; i < nodes.length; i++) {
|
||
|
node = nodes[i];
|
||
|
switch (node.type) {
|
||
|
case "Assign":
|
||
|
assign(node);
|
||
|
break;
|
||
|
case "ObjectPath":
|
||
|
setPath(node);
|
||
|
break;
|
||
|
case "ArrayPath":
|
||
|
addTableArray(node);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return data;
|
||
|
}
|
||
|
|
||
|
function genError(err, line, col) {
|
||
|
var ex = new Error(err);
|
||
|
ex.line = line;
|
||
|
ex.column = col;
|
||
|
throw ex;
|
||
|
}
|
||
|
|
||
|
function assign(node) {
|
||
|
var key = node.key;
|
||
|
var value = node.value;
|
||
|
var line = node.line;
|
||
|
var column = node.column;
|
||
|
|
||
|
var fullPath;
|
||
|
if (currentPath) {
|
||
|
fullPath = currentPath + "." + key;
|
||
|
} else {
|
||
|
fullPath = key;
|
||
|
}
|
||
|
if (typeof context[key] !== "undefined") {
|
||
|
genError("Cannot redefine existing key '" + fullPath + "'.", line, column);
|
||
|
}
|
||
|
|
||
|
context[key] = reduceValueNode(value);
|
||
|
|
||
|
if (!pathAssigned(fullPath)) {
|
||
|
assignedPaths.push(fullPath);
|
||
|
valueAssignments.push(fullPath);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
function pathAssigned(path) {
|
||
|
return assignedPaths.indexOf(path) !== -1;
|
||
|
}
|
||
|
|
||
|
function reduceValueNode(node) {
|
||
|
if (node.type === "Array") {
|
||
|
return reduceArrayWithTypeChecking(node.value);
|
||
|
} else if (node.type === "InlineTable") {
|
||
|
return reduceInlineTableNode(node.value);
|
||
|
} else {
|
||
|
return node.value;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function reduceInlineTableNode(values) {
|
||
|
var obj = Object.create(null);
|
||
|
for (var i = 0; i < values.length; i++) {
|
||
|
var val = values[i];
|
||
|
if (val.value.type === "InlineTable") {
|
||
|
obj[val.key] = reduceInlineTableNode(val.value.value);
|
||
|
} else if (val.type === "InlineTableValue") {
|
||
|
obj[val.key] = reduceValueNode(val.value);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return obj;
|
||
|
}
|
||
|
|
||
|
function setPath(node) {
|
||
|
var path = node.value;
|
||
|
var quotedPath = path.map(quoteDottedString).join(".");
|
||
|
var line = node.line;
|
||
|
var column = node.column;
|
||
|
|
||
|
if (pathAssigned(quotedPath)) {
|
||
|
genError("Cannot redefine existing key '" + path + "'.", line, column);
|
||
|
}
|
||
|
assignedPaths.push(quotedPath);
|
||
|
context = deepRef(data, path, Object.create(null), line, column);
|
||
|
currentPath = path;
|
||
|
}
|
||
|
|
||
|
function addTableArray(node) {
|
||
|
var path = node.value;
|
||
|
var quotedPath = path.map(quoteDottedString).join(".");
|
||
|
var line = node.line;
|
||
|
var column = node.column;
|
||
|
|
||
|
if (!pathAssigned(quotedPath)) {
|
||
|
assignedPaths.push(quotedPath);
|
||
|
}
|
||
|
assignedPaths = assignedPaths.filter(function(p) {
|
||
|
return p.indexOf(quotedPath) !== 0;
|
||
|
});
|
||
|
assignedPaths.push(quotedPath);
|
||
|
context = deepRef(data, path, [], line, column);
|
||
|
currentPath = quotedPath;
|
||
|
|
||
|
if (context instanceof Array) {
|
||
|
var newObj = Object.create(null);
|
||
|
context.push(newObj);
|
||
|
context = newObj;
|
||
|
} else {
|
||
|
genError("Cannot redefine existing key '" + path + "'.", line, column);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Given a path 'a.b.c', create (as necessary) `start.a`,
|
||
|
// `start.a.b`, and `start.a.b.c`, assigning `value` to `start.a.b.c`.
|
||
|
// If `a` or `b` are arrays and have items in them, the last item in the
|
||
|
// array is used as the context for the next sub-path.
|
||
|
function deepRef(start, keys, value, line, column) {
|
||
|
var traversed = [];
|
||
|
var traversedPath = "";
|
||
|
var path = keys.join(".");
|
||
|
var ctx = start;
|
||
|
|
||
|
for (var i = 0; i < keys.length; i++) {
|
||
|
var key = keys[i];
|
||
|
traversed.push(key);
|
||
|
traversedPath = traversed.join(".");
|
||
|
if (typeof ctx[key] === "undefined") {
|
||
|
if (i === keys.length - 1) {
|
||
|
ctx[key] = value;
|
||
|
} else {
|
||
|
ctx[key] = Object.create(null);
|
||
|
}
|
||
|
} else if (i !== keys.length - 1 && valueAssignments.indexOf(traversedPath) > -1) {
|
||
|
// already a non-object value at key, can't be used as part of a new path
|
||
|
genError("Cannot redefine existing key '" + traversedPath + "'.", line, column);
|
||
|
}
|
||
|
|
||
|
ctx = ctx[key];
|
||
|
if (ctx instanceof Array && ctx.length && i < keys.length - 1) {
|
||
|
ctx = ctx[ctx.length - 1];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return ctx;
|
||
|
}
|
||
|
|
||
|
function reduceArrayWithTypeChecking(array) {
|
||
|
// Ensure that all items in the array are of the same type
|
||
|
var firstType = null;
|
||
|
for (var i = 0; i < array.length; i++) {
|
||
|
var node = array[i];
|
||
|
if (firstType === null) {
|
||
|
firstType = node.type;
|
||
|
} else {
|
||
|
if (node.type !== firstType) {
|
||
|
genError("Cannot add value of type " + node.type + " to array of type " +
|
||
|
firstType + ".", node.line, node.column);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Recursively reduce array of nodes into array of the nodes' values
|
||
|
return array.map(reduceValueNode);
|
||
|
}
|
||
|
|
||
|
function quoteDottedString(str) {
|
||
|
if (str.indexOf(".") > -1) {
|
||
|
return "\"" + str + "\"";
|
||
|
} else {
|
||
|
return str;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
module.exports = {
|
||
|
compile: compile
|
||
|
};
|