Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ var compiler = require('./lib/compiler');

module.exports = {
parse: function(input) {
var nodes = parser.parse(input.toString());
return compiler.compile(nodes);
var str = input.toString();
var nodes = parser.parse(str);
return compiler.compile(nodes, str);
}
};
133 changes: 56 additions & 77 deletions lib/compiler.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
"use strict";
function compile(nodes) {
var assignedPaths = [];
var valueAssignments = [];
var explicitTablePaths = [];
var tableArrayPaths = [];
function compile(nodes, inputText) {
var assignedPaths = new Set();
var valueAssignments = new Set();
var explicitTablePaths = new Set();
var currentPath = "";
var data = Object.create(null);
var context = data;
var arrayMode = false;

return reduce(nodes);

Expand All @@ -31,41 +29,46 @@ function compile(nodes) {
return data;
}

function genError(err, line, col) {
function resolveLineCol(off) {
var line = 1, col = 1;
for (var i = 0; i < off; i++) {
if (inputText.charCodeAt(i) === 10) { line++; col = 1; }
else { col++; }
}
return { line: line, column: col };
}

function genError(err, off) {
var pos = resolveLineCol(off);
var ex = new Error(err);
ex.line = line;
ex.column = col;
ex.line = pos.line;
ex.column = pos.column;
throw ex;
}

function assign(node) {
var keys = node.key;
var value = node.value;
var line = node.line;
var column = node.column;
var off = node.offset;

// Support both legacy single-string keys and new array-of-keys format
if (!Array.isArray(keys)) keys = [keys];

var reduced = reduceValueNode(value);

// Navigate to the right context for dotted keys, creating intermediate tables
var target = context;
for (var i = 0; i < keys.length - 1; i++) {
var k = keys[i];
var intermediatePath = currentPath ? currentPath + "." + keys.slice(0, i + 1).join(".") : keys.slice(0, i + 1).join(".");

if (typeof target[k] === "undefined") {
target[k] = Object.create(null);
if (!pathAssigned(intermediatePath)) {
assignedPaths.push(intermediatePath);
}
assignedPaths.add(intermediatePath);
} else if (typeof target[k] !== "object" || target[k] === null || Array.isArray(target[k])) {
genError("Cannot redefine existing key '" + intermediatePath + "'.", line, column);
} else if (valueAssignments.indexOf(intermediatePath) > -1) {
genError("Cannot redefine existing key '" + intermediatePath + "'.", line, column);
} else if (explicitTablePaths.indexOf(intermediatePath) > -1 && intermediatePath !== (Array.isArray(currentPath) ? currentPath.join(".") : currentPath)) {
genError("Cannot use dotted keys to extend table '" + intermediatePath + "' defined elsewhere.", line, column);
genError("Cannot redefine existing key '" + intermediatePath + "'.", off);
} else if (valueAssignments.has(intermediatePath)) {
genError("Cannot redefine existing key '" + intermediatePath + "'.", off);
} else if (explicitTablePaths.has(intermediatePath) && intermediatePath !== (Array.isArray(currentPath) ? currentPath.join(".") : currentPath)) {
genError("Cannot use dotted keys to extend table '" + intermediatePath + "' defined elsewhere.", off);
}
target = target[k];
}
Expand All @@ -74,22 +77,16 @@ function compile(nodes) {
var fullPath = currentPath ? currentPath + "." + keys.join(".") : keys.join(".");

if (typeof target[lastKey] !== "undefined") {
genError("Cannot redefine existing key '" + fullPath + "'.", line, column);
genError("Cannot redefine existing key '" + fullPath + "'.", off);
}

target[lastKey] = reduced;

if (!pathAssigned(fullPath)) {
assignedPaths.push(fullPath);
valueAssignments.push(fullPath);
}
assignedPaths.add(fullPath);
valueAssignments.add(fullPath);
}


function pathAssigned(path) {
return assignedPaths.indexOf(path) !== -1;
}

function reduceValueNode(node) {
if (node.type === "Array") {
return reduceArray(node.value);
Expand All @@ -102,10 +99,7 @@ function compile(nodes) {

function reduceInlineTableNode(values) {
var obj = Object.create(null);
// Track paths that were explicitly defined (either as values or as inline
// table results). Dotted keys can create implicit intermediate tables but
// cannot modify explicitly defined ones.
var definedKeys = [];
var definedKeys = new Set();

for (var i = 0; i < values.length; i++) {
var val = values[i];
Expand All @@ -115,107 +109,92 @@ function compile(nodes) {
if (!Array.isArray(keys)) keys = [keys];

var reduced = reduceValueNode(val.value);
setNestedKey(obj, keys, reduced, val.line, val.column, definedKeys);
setNestedKey(obj, keys, reduced, val.offset, definedKeys);

// Track the full path as a defined key
definedKeys.push(keys.join("."));
definedKeys.add(keys.join("."));
}

return obj;
}

function setNestedKey(obj, keys, value, line, column, definedKeys) {
function setNestedKey(obj, keys, value, off, definedKeys) {
for (var i = 0; i < keys.length - 1; i++) {
var k = keys[i];
var intermediatePath = keys.slice(0, i + 1).join(".");
if (typeof obj[k] === "undefined") {
obj[k] = Object.create(null);
} else if (typeof obj[k] !== "object" || obj[k] === null || Array.isArray(obj[k])) {
genError("Cannot redefine existing key '" + intermediatePath + "'.", line, column);
} else if (definedKeys && definedKeys.indexOf(intermediatePath) > -1) {
// Cannot extend an explicitly-defined inline table
genError("Cannot extend inline table '" + intermediatePath + "'.", line, column);
genError("Cannot redefine existing key '" + intermediatePath + "'.", off);
} else if (definedKeys && definedKeys.has(intermediatePath)) {
genError("Cannot extend inline table '" + intermediatePath + "'.", off);
}
obj = obj[k];
}
var lastKey = keys[keys.length - 1];
if (typeof obj[lastKey] !== "undefined") {
genError("Cannot redefine existing key '" + keys.join(".") + "'.", line, column);
genError("Cannot redefine existing key '" + keys.join(".") + "'.", off);
}
obj[lastKey] = value;
}

function setPath(node) {
var path = node.value;
var quotedPath = path.map(quoteDottedString).join(".");
var line = node.line;
var column = node.column;
var off = node.offset;

if (pathAssigned(quotedPath)) {
genError("Cannot redefine existing key '" + path + "'.", line, column);
if (assignedPaths.has(quotedPath)) {
genError("Cannot redefine existing key '" + path + "'.", off);
}
assignedPaths.push(quotedPath);
explicitTablePaths.push(quotedPath);
context = deepRef(data, path, Object.create(null), line, column);
assignedPaths.add(quotedPath);
explicitTablePaths.add(quotedPath);
context = deepRef(data, path, Object.create(null), off);
currentPath = path;
}

function addTableArray(node) {
var path = node.value;
var quotedPath = path.map(quoteDottedString).join(".");
var line = node.line;
var column = node.column;
var off = node.offset;

// Check before filtering: cannot append to a statically-defined array
if (valueAssignments.indexOf(quotedPath) > -1) {
genError("Cannot append to statically defined array '" + quotedPath + "'.", line, column);
if (valueAssignments.has(quotedPath)) {
genError("Cannot append to statically defined array '" + quotedPath + "'.", off);
}

if (!pathAssigned(quotedPath)) {
assignedPaths.push(quotedPath);
}
assignedPaths = assignedPaths.filter(function(p) {
return p.indexOf(quotedPath) !== 0;
// Clear paths that start with this table array path
assignedPaths.forEach(function(p) {
if (p.indexOf(quotedPath) === 0) assignedPaths.delete(p);
});
valueAssignments = valueAssignments.filter(function(p) {
return p.indexOf(quotedPath) !== 0;
valueAssignments.forEach(function(p) {
if (p.indexOf(quotedPath) === 0) valueAssignments.delete(p);
});
assignedPaths.push(quotedPath);
context = deepRef(data, path, [], line, column);
assignedPaths.add(quotedPath);
context = deepRef(data, path, [], off);
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);
genError("Cannot redefine existing key '" + path + "'.", off);
}
}

// 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 = [];
function deepRef(start, keys, value, off) {
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(".");
traversedPath = traversedPath ? traversedPath + "." + key : key;
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);
} else if (i !== keys.length - 1 && valueAssignments.has(traversedPath)) {
genError("Cannot redefine existing key '" + traversedPath + "'.", off);
}

ctx = ctx[key];
Expand Down
Loading
Loading