123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421 |
- require("string.prototype.codepointat");
- var fs = require("fs");
-
- function Iterator(text) {
- var pos = 0, length = text.length;
-
- this.peek = function(num) {
- num = num || 0;
- if(pos + num >= length) { return null; }
-
- return text.charAt(pos + num);
- };
- this.next = function(inc) {
- inc = inc || 1;
-
- if(pos >= length) { return null; }
-
- return text.charAt((pos += inc) - inc);
- };
- this.pos = function() {
- return pos;
- };
- }
-
- var rWhitespace = /\s/;
- function isWhitespace(chr) {
- return rWhitespace.test(chr);
- }
- function consumeWhiteSpace(iter) {
- var start = iter.pos();
-
- while(isWhitespace(iter.peek())) { iter.next(); }
-
- return { type: "whitespace", start: start, end: iter.pos() };
- }
-
- function startsComment(chr) {
- return chr === "!" || chr === "#";
- }
- function isEOL(chr) {
- return chr == null || chr === "\n" || chr === "\r";
- }
- function consumeComment(iter) {
- var start = iter.pos();
-
- while(!isEOL(iter.peek())) { iter.next(); }
-
- return { type: "comment", start: start, end: iter.pos() };
- }
-
- function startsKeyVal(chr) {
- return !isWhitespace(chr) && !startsComment(chr);
- }
- function startsSeparator(chr) {
- return chr === "=" || chr === ":" || isWhitespace(chr);
- }
- function startsEscapedVal(chr) {
- return chr === "\\";
- }
- function consumeEscapedVal(iter) {
- var start = iter.pos();
-
- iter.next(); // move past "\"
- var curChar = iter.next();
- if(curChar === "u") { // encoded unicode char
- iter.next(4); // Read in the 4 hex values
- }
-
- return { type: "escaped-value", start: start, end: iter.pos() };
- }
- function consumeKey(iter) {
- var start = iter.pos(), children = [];
-
- var curChar;
- while((curChar = iter.peek()) !== null) {
- if(startsSeparator(curChar)) { break; }
- if(startsEscapedVal(curChar)) { children.push(consumeEscapedVal(iter)); continue; }
-
- iter.next();
- }
-
- return { type: "key", start: start, end: iter.pos(), children: children };
- }
- function consumeKeyValSeparator(iter) {
- var start = iter.pos();
-
- var seenHardSep = false, curChar;
- while((curChar = iter.peek()) !== null) {
- if(isEOL(curChar)) { break; }
-
- if(isWhitespace(curChar)) { iter.next(); continue; }
-
- if(seenHardSep) { break; }
-
- seenHardSep = (curChar === ":" || curChar === "=");
- if(seenHardSep) { iter.next(); continue; }
-
- break; // curChar is a non-separtor char
- }
-
- return { type: "key-value-separator", start: start, end: iter.pos() };
- }
- function startsLineBreak(iter) {
- return iter.peek() === "\\" && isEOL(iter.peek(1));
- }
- function consumeLineBreak(iter) {
- var start = iter.pos();
-
- iter.next(); // consume \
- if(iter.peek() === "\r") { iter.next(); }
- iter.next(); // consume \n
-
- var curChar;
- while((curChar = iter.peek()) !== null) {
- if(isEOL(curChar)) { break; }
- if(!isWhitespace(curChar)) { break; }
-
- iter.next();
- }
-
- return { type: "line-break", start: start, end: iter.pos() };
- }
- function consumeVal(iter) {
- var start = iter.pos(), children = [];
-
- var curChar;
- while((curChar = iter.peek()) !== null) {
- if(startsLineBreak(iter)) { children.push(consumeLineBreak(iter)); continue; }
- if(startsEscapedVal(curChar)) { children.push(consumeEscapedVal(iter)); continue; }
- if(isEOL(curChar)) { break; }
-
- iter.next();
- }
-
- return { type: "value", start: start, end: iter.pos(), children: children };
- }
- function consumeKeyVal(iter) {
- return {
- type: "key-value",
- start: iter.pos(),
- children: [
- consumeKey(iter),
- consumeKeyValSeparator(iter),
- consumeVal(iter)
- ],
- end: iter.pos()
- };
- }
-
- var renderChild = {
- "escaped-value": function(child, text) {
- var type = text.charAt(child.start + 1);
-
- if(type === "t") { return "\t"; }
- if(type === "r") { return "\r"; }
- if(type === "n") { return "\n"; }
- if(type === "f") { return "\f"; }
- if(type !== "u") { return type; }
-
- return String.fromCharCode(parseInt(text.substr(child.start + 2, 4), 16));
- },
- "line-break": function (child, text) {
- return "";
- }
- };
- function rangeToBuffer(range, text) {
- var start = range.start, buffer = [];
-
- for(var i = 0; i < range.children.length; i++) {
- var child = range.children[i];
-
- buffer.push(text.substring(start, child.start));
- buffer.push(renderChild[child.type](child, text));
- start = child.end;
- }
- buffer.push(text.substring(start, range.end));
-
- return buffer;
- }
- function rangesToObject(ranges, text) {
- var obj = Object.create(null); // Creates to a true hash map
-
- for(var i = 0; i < ranges.length; i++) {
- var range = ranges[i];
-
- if(range.type !== "key-value") { continue; }
-
- var key = rangeToBuffer(range.children[0], text).join("");
- var val = rangeToBuffer(range.children[2], text).join("");
- obj[key] = val;
- }
-
- return obj;
- }
-
- function stringToRanges(text) {
- var iter = new Iterator(text), ranges = [];
-
- var curChar;
- while((curChar = iter.peek()) !== null) {
- if(isWhitespace(curChar)) { ranges.push(consumeWhiteSpace(iter)); continue; }
- if(startsComment(curChar)) { ranges.push(consumeComment(iter)); continue; }
- if(startsKeyVal(curChar)) { ranges.push(consumeKeyVal(iter)); continue; }
-
- throw Error("Something crazy happened. text: '" + text + "'; curChar: '" + curChar + "'");
- }
-
- return ranges;
- }
-
- function isNewLineRange(range) {
- if(!range) { return false; }
-
- if(range.type === "whitespace") { return true; }
-
- if(range.type === "literal") {
- return isWhitespace(range.text) && range.text.indexOf("\n") > -1;
- }
-
- return false;
- }
-
- function escapeMaker(escapes) {
- return function escapeKey(key) {
- var zeros = [ "", "0", "00", "000" ];
- var buf = [];
-
- for(var i = 0; i < key.length; i++) {
- var chr = key.charAt(i);
-
- if(escapes[chr]) { buf.push(escapes[chr]); continue; }
-
- var code = chr.codePointAt(0);
-
- if(code <= 0x7F) { buf.push(chr); continue; }
-
- var hex = code.toString(16);
-
- buf.push("\\u");
- buf.push(zeros[4 - hex.length]);
- buf.push(hex);
- }
-
- return buf.join("");
- };
- }
-
- var escapeKey = escapeMaker({ " ": "\\ ", "\n": "\\n", ":": "\\:", "=": "\\=" });
- var escapeVal = escapeMaker({ "\n": "\\n" });
-
- function Editor(text, options) {
- if (typeof text === 'object') {
- options = text;
- text = null;
- }
- text = text || "";
- var path = options.path;
- var separator = options.separator || '=';
-
- var ranges = stringToRanges(text);
- var obj = rangesToObject(ranges, text);
- var keyRange = Object.create(null); // Creates to a true hash map
-
- for(var i = 0; i < ranges.length; i++) {
- var range = ranges[i];
-
- if(range.type !== "key-value") { continue; }
-
- var key = rangeToBuffer(range.children[0], text).join("");
- keyRange[key] = range;
- }
-
- this.addHeadComment = function(comment) {
- if(comment == null) { return; }
-
- ranges.unshift({ type: "literal", text: "# " + comment.replace(/\n/g, "\n# ") + "\n" });
- };
-
- this.get = function(key) { return obj[key]; };
- this.set = function(key, val, comment) {
- if(val == null) { this.unset(key); return; }
-
- obj[key] = val;
- var escapedKey = escapeKey(key);
- var escapedVal = escapeVal(val);
-
- var range = keyRange[key];
- if(!range) {
- keyRange[key] = range = {
- type: "literal",
- text: escapedKey + separator + escapedVal
- };
-
- var prevRange = ranges[ranges.length - 1];
- if(prevRange != null && !isNewLineRange(prevRange)) {
- ranges.push({ type: "literal", text: "\n" });
- }
- ranges.push(range);
- }
-
- // comment === null deletes comment. if comment === undefined, it's left alone
- if(comment !== undefined) {
- range.comment = comment && "# " + comment.replace(/\n/g, "\n# ") + "\n";
- }
-
- if(range.type === "literal") {
- range.text = escapedKey + separator + escapedVal;
- if(range.comment != null) { range.text = range.comment + range.text; }
- } else if(range.type === "key-value") {
- range.children[2] = { type: "literal", text: escapedVal };
- } else {
- throw "Unknown node type: " + range.type;
- }
- };
- this.unset = function(key) {
- if(!(key in obj)) { return; }
-
- var range = keyRange[key];
- var idx = ranges.indexOf(range);
-
- ranges.splice(idx, (isNewLineRange(ranges[idx + 1]) ? 2 : 1));
-
- delete keyRange[key];
- delete obj[key];
- };
- this.valueOf = this.toString = function() {
- var buffer = [], stack = [].concat(ranges);
-
- var node;
- while((node = stack.shift()) != null) {
- switch(node.type) {
- case "literal":
- buffer.push(node.text);
- break;
- case "key":
- case "value":
- case "comment":
- case "whitespace":
- case "key-value-separator":
- case "escaped-value":
- case "line-break":
- buffer.push(text.substring(node.start, node.end));
- break;
- case "key-value":
- Array.prototype.unshift.apply(stack, node.children);
- if(node.comment) { stack.unshift({ type: "literal", text: node.comment }); }
- break;
- }
- }
-
- return buffer.join("");
- };
- this.save = function(newPath, callback) {
- if(typeof newPath === 'function') {
- callback = newPath;
- newPath = path;
- }
- newPath = newPath || path;
-
- if(!newPath) {
- if (callback) {
- return callback("Unknown path");
- }
- throw new Error("Unknown path");
- }
-
- if (callback) {
- fs.writeFile(newPath, this.toString(), callback);
- } else {
- fs.writeFileSync(newPath, this.toString());
- }
-
- };
- }
- function createEditor(/*path, options, callback*/) {
- var path, options, callback;
- var args = Array.prototype.slice.call(arguments);
- for (var i = 0; i < args.length; i ++) {
- var arg = args[i];
- if (!path && typeof arg === 'string') {
- path = arg;
- } else if (!options && typeof arg === 'object') {
- options = arg;
- } else if (!callback && typeof arg === 'function') {
- callback = arg;
- }
- }
- options = options || {};
- path = path || options.path;
- callback = callback || options.callback;
- options.path = path;
-
- if(!path) { return new Editor(options); }
-
- if(!callback) { return new Editor(fs.readFileSync(path).toString(), options); }
-
- return fs.readFile(path, function(err, text) {
- if(err) { return callback(err, null); }
-
- text = text.toString();
- return callback(null, new Editor(text, options));
- });
- }
-
- function parse(text) {
- text = text.toString();
- var ranges = stringToRanges(text);
- return rangesToObject(ranges, text);
- }
-
- function read(path, callback) {
- if(!callback) { return parse(fs.readFileSync(path)); }
-
- return fs.readFile(path, function(err, data) {
- if(err) { return callback(err, null); }
-
- return callback(null, parse(data));
- });
- }
-
- module.exports = { parse: parse, read: read, createEditor: createEditor };
|