var tabSize = 4, tab = '    ';
var keyLowercase = true;


function traverse(phrases, curIdx) {
	var root = phrases[curIdx];
	
	var node = {
		key: root.key,
		value: root.value
	};
	
	var i = curIdx+1, len = phrases.length;
	
	while(i < len) {
		var next = phrases[i];
		
		if( next.level - root.level > 1 ) { // error
			throw new Error('Invalid indent.');
		}
		
		else if( next.level <= root.level ) {// end
			break;
		}
		
		var res = traverse(phrases, i);
		
		(node.children || (node.children = [])).push(res[0]);
		
		i = res[1];
	}
	
	return [node, i];
}


function splitLines(str) {
	return str
	.replace(/\r\n/g,'\n') //for window
	.split('\n');
}


//TODO allow leading spaces also
function lineToPhrase(line) {
	var tabs = 0;
	
	var match = line.match(/^[\t]+/); 		// find leading tabs
	if( match !== null ) tabs = match[0].length;
	
	
	var contents = line.substr(tabs);		// exclude leading tabs
	if( contents === '' ) return; 			// ignore empty line
	
	
	if( contents[0] === '#' ) return;			// ignore comment
	
	
	var colonIdx = contents.indexOf(':'); //find first colon
	if( colonIdx === -1 ) throw new Error(`Invalid line.`);
	
	
	var key = contents.substr(0, colonIdx).trim(), 
		value = contents.substr(colonIdx+1).trim();
	
	if( key === '' ) throw new Error(`Invalid line.`);
		
		
	key = keyLowercase ? key.toLowerCase() : key;
	return {
		level: tabs,
		key: key,
		value: value
	};
}


function parse(str) {
	var lines = splitLines(str);
	var phrases = lines.map(lineToPhrase).filter(val => val !== void 0);
	
	phrases.unshift({level: -1});	// root
	return traverse(phrases, 0)[0];
}

exports.parse = parse;
@@ -64,121 +64,1273 @@
 ion 
-traverse(phrases, curIdx) %7B%0A%09var root = phrases%5BcurIdx%5D
+generateNode(root, idx) %7B%0A%09if( idx === 0 ) %7B%0A%09%09return %7B%0A%09%09%09type: 'Program',%0A%09%09%09body: %5B%5D,%0A%09%09%09sourceType: 'script'%0A%09%09%7D%0A%09%7D%0A%09%0A%09else %7B%0A%09%09return %7B %0A%09%09%09type: 'ExpressionStatement',%0A       %09%09expression: %7B %0A       %09%09%09type: 'CallExpression',%0A          %09%09callee: %7B %0A          %09%09%09type: 'Identifier', %0A          %09%09%09name: root.key%0A          %09%09%7D,%0A          %09%09arguments: %5B %0A           %09%09%09%7B %0A           %09%09%09%09type: 'Literal', %0A           %09%09%09%09value: root.value, %0A           %09%09%09%09raw: %60'$%7Broot.value%7D'%60%0A           %09%09%09%7D,%0A             %09%09%7B %0A             %09%09%09type: 'FunctionExpression',%0A               %09%09%09id: null,%0A               %09%09%09params: %5B%5D,%0A               %09%09%09defaults: %5B%5D,%0A               %09%09%09expression: false,%0A               %09%09%09generator: false,%0A               %09%09%09body: %7B %0A               %09%09%09%09type: 'BlockStatement',%0A                  %09%09%09body: %5B%0A                       %09%09%5D%0A               %09%09%09%7D%0A             %09%09%7D%0A             %09%5D%0A       %09%09%7D%0A%09%09%7D         %09%09%09%09%0A%09%7D;%0A%7D%0A%0A%0Afunction insertChildIntoNode(node, child) %7B%0A%09if( node.type === 'ExpressionStatement' ) %7B%0A%09%09node.expression.arguments%5B1%5D.body.body.push(child)
 ;%0A%09
+%7D
 %0A%09
-var node = %7B%0A%09%09key: root.key,
+%0A%09else if( node.type === 'Program' ) %7B%0A%09%09node.body.push(child);%0A%09%7D%0A%7D%0A%0A%0Afunction traverse(phrases, curIdx) %7B%0A%09var root = phrases%5BcurIdx%5D;
 %0A%09
+%0A
 %09va
-lue: root.value%0A%09%7D;
+r node = generateNode(root, curIdx);%0A%09
 %0A%09%0A%09
@@ -1616,53 +1616,34 @@
 %09%0A%09%09
-(node.children %7C%7C (node.children = %5B%5D)).push(
+insertChildIntoNode(node, 
 res%5B

Modify custom ast to estree.