1 module jin.tree;
2 
3 import std.stdio;
4 import std.string;
5 import std.conv;
6 import std.outbuffer;
7 import std.algorithm;
8 import std.array;
9 import std.json;
10 import std.xml;
11 import std.file;
12 import std.path;
13 
14 class Tree {
15 
16 	string name;
17 	string value;
18 	string baseUri;
19 	size_t row;
20 	size_t col;
21 	Tree[] childs;
22 
23 	this(
24 		 string name ,
25 		 string value ,
26 		 Tree[] childs ,
27 		 string baseUri = "" ,
28 		 size_t row = 0 ,
29 		 size_t col = 0 
30 	) {
31 		this.name = name;
32 		this.value = value;
33 		this.childs = childs;
34 		this.baseUri = baseUri;
35 		this.row = row;
36 		this.col = col;
37 	}
38 
39 	this (
40 		  DirEntry input ,
41 		  string baseUri = null ,
42 		  size_t row = 1 ,
43 		  size_t col = 1
44 	) {
45 		if( baseUri is null ) baseUri = absolutePath( input.name );
46 		this( cast( string ) read( input.name ) , baseUri , row , col );
47 	}
48 
49 	this (
50 		  File input ,
51 		  string baseUri = null ,
52 		  size_t row = 1 ,
53 		  size_t col = 1
54 	) {
55 		if( baseUri is null ) baseUri = input.name.absolutePath.asNormalizedPath.array;
56 		this( cast( string ) read( input.name ) , baseUri , row , col );
57 	}
58 
59 	unittest {
60 		assert( new Tree( "foo\nbar\n" , "" ).length == 2 );
61 		assert( new Tree( "foo\nbar\n" , "" )[1].name == "bar" );
62 		assert( new Tree( "foo\n\n\n" , "" ).length == 1 );
63 
64 		assert( new Tree( "\\foo\n\\bar\n" , "" ).value == "foo\nbar" );
65 		assert( new Tree( "\\foo\n\\bar\n" , "" ).length == 0 );
66 
67 		assert( new Tree( "foo bar \\pol" , "" )[0][0].value == "pol" );
68 		assert( new Tree( "foo bar\n\t\\pol\n\t\\men" , "" )[0][0].value == "pol\nmen" );
69 	}
70 	this (
71 		  string input ,
72 		  string baseUri ,
73 		  size_t row = 1 ,
74 		  size_t col = 1
75 	) {
76 		this( "" , null , [] , baseUri , row , col );
77 		Tree[] stack = [ this ];
78 
79 		Tree parent = this;
80 		while( input.length ) {
81 
82 			auto name = input.takeUntil( "\t\n \\" );
83 			if( name.length ) {
84 				auto next = new Tree( name , null , [] , baseUri , row , col );
85 				parent.childs ~= next;
86 				parent = next;
87 				col += name.length + input.take( " " ).length;
88 				continue;
89 			}
90 			if( !input.length ) break;
91 
92 			if( input[0] == '\\' ) {
93 				auto value = input.takeUntil( "\n" )[ 1 .. $ ];
94 				if( parent.value is null ) parent.value = value;
95 				else parent.value ~= "\n" ~ value;
96 			}
97 			if( !input.length ) break;
98 
99 			if( input[0] != '\n' ) {
100 				throw new Exception( "Unexpected symbol (" ~ input[0] ~ ")" );
101 			}
102 			input = input[ 1 .. $ ];
103 			col = 1;
104 			row += 1;
105 
106 			auto indent = input.take( "\t" ).length;
107 			col = indent;
108 			if( indent > stack.length ) {
109 				throw new Exception( "Too many TABs " ~ row.to!string ~ ":" ~ col.to!string );
110 			}
111 
112 			stack ~= parent;
113 			stack.length  = indent + 1;
114 			parent = stack[ indent ];
115 		}
116 	}
117 
118 	Tree make(
119 		string name = null ,
120 		string value = null ,
121 		Tree[] childs = null ,
122 		string baseUri = null ,
123 		size_t row = 0 ,
124 		size_t col = 0 
125 	) {
126 		return new Tree(
127 			name ? name : this.name ,
128 			value ? value : this.value ,
129 			childs ? childs : this.childs ,
130 			baseUri ? baseUri : this.baseUri ,
131 			row ? row : this.row ,
132 			col ? col : this.col
133 		);
134 	}
135 
136 	static Tree fromJSON( string json ) {
137 		return Tree.fromJSON( parseJSON( json ) );
138 	}
139 	static Tree fromJSON( JSONValue json ) {
140 		switch( json.type ) {
141 			case JSON_TYPE.FALSE :
142 				return new Tree( "false" , "" , [] );
143 			case JSON_TYPE.TRUE :
144 				return new Tree( "true" , "" , [] );
145 			case JSON_TYPE.NULL :
146 				return new Tree( "null" , "" , [] );
147 			case JSON_TYPE.FLOAT :
148 				return new Tree( "float" , json.floating.to!string , [] );
149 			case JSON_TYPE.INTEGER :
150 				return new Tree( "int" , json.integer.to!string , [] );
151 			case JSON_TYPE.UINTEGER :
152 				return new Tree( "int" , json.uinteger.to!string , [] );
153 			case JSON_TYPE.STRING :
154 				return new Tree( "string" , json.str , [] );
155 			case JSON_TYPE.ARRAY :
156 				return new Tree( "list" , "" , json.array.map!( json => Tree.fromJSON( json ) ).array );
157 			case JSON_TYPE.OBJECT :
158 				Tree[] childs = [];
159 				foreach( key , value ; json.object ) {
160 					childs ~= new Tree( "*" , key , [ new Tree( ":" , "" , [ Tree.fromJSON( value ) ] ) ] );
161 				}
162 				return new Tree( "dict" , "" , childs );
163 			default:
164 				throw new Error( "Unsupported type: " ~ json.type );
165 		}
166 	}
167 
168 	static Tree fromXML( string xml ) {
169 		return Tree.fromXML( new Document( xml ) );
170 	}
171 	static Tree fromXML( Item xml ) {
172 
173 		auto el = cast( Element ) xml;
174 		if( el ) {
175 			Tree[] attrs;
176 			foreach( key , val ; el.tag.attr ){
177 				attrs ~= new Tree( "@" , "" , [ new Tree( key , val ) ] );
178 			}
179 			auto childs = el.items.map!( Tree.fromXML ).filter!( a => a ).array;
180 			return new Tree( el.tag.name , "" , attrs ~ childs );
181 		}
182 
183 		auto com = cast( Comment ) xml;
184 		if( com ) {
185 			return new Tree( "--" , com.to!string[ 4 .. $ - 3 ] , [] );
186 		}
187 
188 		auto txt = cast( Text ) xml;
189 		if( txt ) {
190 			if( txt.to!string.all!( isSpace ) ) return null;
191 			return new Tree( "'" , com.to!string , [] );
192 		}
193 
194 		throw new Error( "Unsupported node type!" );
195 	}
196 
197 	OutputType pipe(
198 		OutputType
199 	) (
200 		OutputType output ,
201 		string prefix = ""
202 	) {
203 		if( this.name.length ) output.write( this.name ~ " " );
204 
205 		auto chunks = this.value.length ? this.value.split( "\n" ) : [];
206 
207 		if( chunks.length + this.childs.length == 1 ) {
208 			if( chunks.length ) output.write( "\\" ~ chunks[0] ~ "\n" );
209 			else childs[0].pipe( output , prefix );
210 		} else {
211 			output.write( "\n" );
212 			if( this.name.length ) prefix ~= "\t";
213 
214 			foreach( chunk ; chunks ) output.write( prefix ~ "\\" ~ chunk ~ "\n" );
215 
216 			foreach( child ; this.childs ) {
217 				output.write( prefix );
218 				child.pipe( output , prefix );
219 			}
220 		}
221 
222 		return output;
223 	}
224 
225 	override string toString() {
226 		OutBuffer buf = new OutBuffer;
227 		this.pipe( buf );
228 		return buf.to!string;
229 	}
230 
231 	Tree expand() {
232 		return this.make( null , null , [ new Tree( "@" , this.uri , [] ) ] ~ this.childs.map!( child => child.expand ).array );
233 	}
234 
235 	string uri( ) {
236 		return this.baseUri ~ "#" ~ this.row.to!string ~ ":" ~ this.col.to!string;
237 	}
238 
239 	unittest {
240 		auto tree = new Tree( "foo \\1\nbar \\2" , "" );
241 		assert( tree["bar"][0].to!string == "bar \\2\n" );
242 	}
243 	auto opIndex( string path ) {
244 		return this[ path.split( " " ) ];
245 	}
246 
247 	unittest {
248 		assert( new Tree( "foo bar \\2" , "" )[ [ "foo" , "bar" ] ].to!string == "bar \\2\n" );
249 	}
250 	auto opIndex( string[] path ) {
251 		Tree[] next = [ this ];
252 		foreach( string name ; path ) {
253 			if( !next.length ) break;
254 			Tree[] prev = next;
255 			next = [];
256 			foreach( Tree item ; prev ) {
257 				foreach( Tree child ; item.childs ) {
258 					if( child.name != name ) continue;
259 					next ~= child;
260 				}
261 			}
262 		}
263 		return new Tree( "" , "" , next );
264 	}
265 
266 	Tree opIndex( size_t index ) {
267 		return this.childs[ index ];
268 	}
269 
270 	Tree[] opSlice( size_t start , size_t end ) {
271 		return this.childs[ start .. end ];
272 	}
273 
274 	size_t length( ) {
275 		return this.childs.length;
276 	}
277 
278 	size_t opDollar( ) {
279 		return this.childs.length;
280 	}
281 
282 }
283 
284 string take( ref string input , string symbols ) {
285 	auto i = 0;
286 	while( i < input.length ) {
287 		auto symbol = input[i];
288 		if( symbols.indexOf( symbol ) == -1 ) {
289 			break;
290 		} else {
291 			i += 1;
292 		}
293 	}
294 	auto res = input[ 0 .. i ];
295 	input = input[ i .. $ ];
296 	return res;
297 }
298 
299 string takeUntil( ref string input , string symbols ) {
300 	auto i = 0;
301 	while( i < input.length ) {
302 		auto symbol = input[i];
303 		if( symbols.indexOf( symbol ) == -1 ) {
304 			i += 1;
305 		} else {
306 			break;
307 		}
308 	}
309 	auto res = input[ 0 .. i ];
310 	input = input[ i .. $ ];
311 	return res;
312 }