JSDOC/TokenReader.js
[gnome.introspection-doc-generator] / JSDOC / TokenReader.js
1 //<script type="text/javascript">
2
3  
4 const XObject = imports.XObject.XObject;
5 const console = imports.console.console;
6
7
8 const Token   = imports.Token.Token;
9 const Lang    = imports.Lang.Lang;
10
11 /**
12         @class Search a {@link JSDOC.TextStream} for language tokens.
13 */
14 const TokenReader = XObject.define(
15     function(o) {
16         
17         XObject.extend(this, o || {});
18         
19     },
20     Object,
21     {
22         /** @cfg {Boolean} collapseWhite merge multiple whitespace/comments into a single token **/
23         collapseWhite : false, // only reduces white space...
24         /** @cfg {Boolean} keepDocs keep JSDOC comments **/
25         keepDocs : true,
26         /** @cfg {Boolean} keepWhite keep White space **/
27         keepWhite : false,
28         /** @cfg {Boolean} keepComments  keep all comments **/
29         keepComments : false,
30         /** @cfg {Boolean} sepIdents seperate identifiers (eg. a.b.c into ['a', '.', 'b', '.', 'c'] ) **/
31         sepIdents : false,
32         /** @cfg {String} filename name of file being parsed. **/
33         filename : '',
34         /** @config {Boolean} ignoreBadGrammer do not throw errors if we find stuff that might break compression **/
35         ignoreBadGrammer : false,
36         /**
37          * tokenize a stream
38          * @return {Array} of tokens
39          * 
40          * ts = new TextStream(File.read(str));
41          * tr = TokenReader({ keepComments : true, keepWhite : true });
42          * tr.tokenize(ts)
43          * 
44          */
45         tokenize : function(/**JSDOC.TextStream*/stream) {
46             this.line =1;
47             var tokens = [];
48             /**@ignore*/ 
49             tokens.last    = function() { return tokens[tokens.length-1]; }
50             /**@ignore*/ 
51             tokens.lastSym = function() {
52                 for (var i = tokens.length-1; i >= 0; i--) {
53                     if (!(tokens[i].is("WHIT") || tokens[i].is("COMM"))) return tokens[i];
54                 }
55                 return true;
56             }
57
58             while (!stream.look().eof) {
59                 print_r(tokens);
60                 if (this.read_mlcomment(stream, tokens)) continue;
61                 if (this.read_slcomment(stream, tokens)) continue;
62                 if (this.read_dbquote(stream, tokens))   continue;
63                 if (this.read_snquote(stream, tokens))   continue;
64                 if (this.read_regx(stream, tokens))      continue;
65                 if (this.read_numb(stream, tokens))      continue;
66                 if (this.read_punc(stream, tokens))      continue;
67                 if (this.read_newline(stream, tokens))   continue;
68                 if (this.read_space(stream, tokens))     continue;
69                 if (this.read_word(stream, tokens))      continue;
70                 
71                 // if execution reaches here then an error has happened
72                 tokens.push(new Token(stream.next(), "TOKN", "UNKNOWN_TOKEN", this.line));
73             }
74             
75             
76             
77             return tokens;
78         },
79
80         /**
81          * findPuncToken - find the id of a token (previous to current)
82          * need to back check syntax..
83          * 
84          * @arg {Array} tokens the array of tokens.
85          * @arg {String} token data (eg. '(')
86          * @arg {Number} offset where to start reading from
87          * @return {Number} position of token
88          */
89         findPuncToken : function(tokens, data, n) {
90             n = n || tokens.length -1;
91             var stack = 0;
92             while (n > -1) {
93                 
94                 if (!stack && tokens[n].data == data) {
95                     return n;
96                 }
97                 
98                 if (tokens[n].data  == ')' || tokens[n].data  == '}') {
99                     stack++;
100                     n--;
101                     continue;
102                 }
103                 if (stack && (tokens[n].data  == '{' || tokens[n].data  == '(')) {
104                     stack--;
105                     n--;
106                     continue;
107                 }
108                 
109                 
110                 n--;
111             }
112             return -1;
113         },
114         /**
115          * lastSym - find the last token symbol
116          * need to back check syntax..
117          * 
118          * @arg {Array} tokens the array of tokens.
119          * @arg {Number} offset where to start..
120          * @return {Token} the token
121          */
122         lastSym : function(tokens, n) {
123             for (var i = n-1; i >= 0; i--) {
124                 if (!(tokens[i].is("WHIT") || tokens[i].is("COMM"))) return tokens[i];
125             }
126             return null;
127         },
128         
129          
130         
131         /**
132             @returns {Boolean} Was the token found?
133          */
134         read_word : function(/**JSDOC.TokenStream*/stream, tokens) {
135             var found = "";
136             while (!stream.look().eof && Lang.isWordChar(stream.look())) {
137                 found += stream.next();
138             }
139             
140             if (found === "") {
141                 return false;
142             }
143             
144             var name;
145             if ((name = Lang.keyword(found))) {
146                 if (found == 'return' && tokens.lastSym().data == ')') {
147                     //Seed.print('@' + tokens.length);
148                     var n = this.findPuncToken(tokens, ')');
149                     //Seed.print(')@' + n);
150                     n = this.findPuncToken(tokens, '(', n-1);
151                     //Seed.print('(@' + n);
152                     
153                     var lt = this.lastSym(tokens, n);
154                     print(JSON.stringify(lt));
155                     if (lt.type != 'KEYW' || ['IF', 'WHILE'].indexOf(lt.name) < -1) {
156                         if (!this.ignoreBadGrammer) {
157                             throw {
158                                 name : "ArgumentError", 
159                                 message: "\n" + this.filename + ':' + this.line + " Error - return found after )"
160                             }
161                         }
162                     }
163                     
164                     
165                     
166                 }
167                 
168                 tokens.push(new Token(found, "KEYW", name, this.line));
169                 return true;
170             }
171             if (!this.sepIdents || found.indexOf('.') < 0 ) {
172                 tokens.push(new Token(found, "NAME", "NAME", this.line));
173                 return true;
174             }
175             var n = found.split('.');
176             var p = false;
177             var _this = this;
178             n.forEach(function(nm) {
179                 if (p) {
180                     tokens.push(new Token('.', "PUNC", "DOT", _this.line));
181                 }
182                 p=true;
183                 tokens.push(new Token(nm, "NAME", "NAME", _this.line));
184             });
185             return true;
186                 
187
188         },
189
190         /**
191             @returns {Boolean} Was the token found?
192          */
193         read_punc : function(/**JSDOC.TokenStream*/stream, tokens) {
194             var found = "";
195             var name;
196             while (!stream.look().eof && Lang.punc(found+stream.look())) {
197                 found += stream.next();
198             }
199             
200             
201             if (found === "") {
202                 return false;
203             }
204             
205             if ((found == '}' || found == ']') && tokens.lastSym().data == ',') {
206                 //print("Error - comma found before " + found);
207                 //print(JSON.stringify(tokens.lastSym(), null,4));
208                 if (this.ignoreBadGrammer) {
209                     print("\n" + this.filename + ':' + this.line + " Error - comma found before " + found);
210                 } else {
211                     
212                     throw {
213                         name : "ArgumentError", 
214                         message: "\n" + this.filename + ':' + this.line + " Error - comma found before " + found
215                     }
216                 }
217             }
218             
219             tokens.push(new Token(found, "PUNC", Lang.punc(found), this.line));
220             return true;
221             
222         },
223
224         /**
225             @returns {Boolean} Was the token found?
226          */
227         read_space : function(/**JSDOC.TokenStream*/stream, tokens) {
228             var found = "";
229             
230             while (!stream.look().eof && Lang.isSpace(stream.look()) && !Lang.isNewline(stream.look())) {
231                 found += stream.next();
232             }
233             
234             if (found === "") {
235                 return false;
236             }
237             //print("WHITE = " + JSON.stringify(found)); 
238             if (this.collapseWhite) found = " ";
239             if (this.keepWhite) tokens.push(new Token(found, "WHIT", "SPACE", this.line));
240             return true;
241         
242         },
243
244         /**
245             @returns {Boolean} Was the token found?
246          */
247         read_newline : function(/**JSDOC.TokenStream*/stream, tokens) {
248             var found = "";
249             var line = this.line;
250             while (!stream.look().eof && Lang.isNewline(stream.look())) {
251                 this.line++;
252                 found += stream.next();
253             }
254             
255             if (found === "") {
256                 return false;
257             }
258             //this.line++;
259             if (this.collapseWhite) {
260                 found = "\n";
261             }
262             if (this.keepWhite) {
263                 var last = tokens.pop();
264                 if (last && last.name != "WHIT") {
265                     tokens.push(last);
266                 }
267                 
268                 tokens.push(new Token(found, "WHIT", "NEWLINE", line));
269             }
270             return true;
271         },
272
273         /**
274             @returns {Boolean} Was the token found?
275          */
276         read_mlcomment : function(/**JSDOC.TokenStream*/stream, tokens) {
277             if (stream.look() == "/" && stream.look(1) == "*") {
278                 var found = stream.next(2);
279                 var c = '';
280                 var line = this.line;
281                 while (!stream.look().eof && !(stream.look(-1) == "/" && stream.look(-2) == "*")) {
282                     c = stream.next();
283                     if (c == "\n") this.line++;
284                     found += c;
285                 }
286                 
287                 // to start doclet we allow /** or /*** but not /**/ or /****
288                 if (/^\/\*\*([^\/]|\*[^*])/.test(found) && this.keepDocs) tokens.push(new Token(found, "COMM", "JSDOC", this.line));
289                 else if (this.keepComments) tokens.push(new Token(found, "COMM", "MULTI_LINE_COMM", line));
290                 return true;
291             }
292             return false;
293         },
294
295         /**
296             @returns {Boolean} Was the token found?
297          */
298         read_slcomment : function(/**JSDOC.TokenStream*/stream, tokens) {
299             var found;
300             if (
301                 (stream.look() == "/" && stream.look(1) == "/" && (found=stream.next(2)))
302                 || 
303                 (stream.look() == "<" && stream.look(1) == "!" && stream.look(2) == "-" && stream.look(3) == "-" && (found=stream.next(4)))
304             ) {
305                 var line = this.line;
306                 while (!stream.look().eof && !Lang.isNewline(stream.look())) {
307                     found += stream.next();
308                 }
309                 if (!stream.look().eof) {
310                     found += stream.next();
311                 }
312                 if (this.keepComments) {
313                     tokens.push(new Token(found, "COMM", "SINGLE_LINE_COMM", line));
314                 }
315                 this.line++;
316                 return true;
317             }
318             return false;
319         },
320
321         /**
322             @returns {Boolean} Was the token found?
323          */
324         read_dbquote : function(/**JSDOC.TokenStream*/stream, tokens) {
325             if (stream.look() == "\"") {
326                 // find terminator
327                 var string = stream.next();
328                 
329                 while (!stream.look().eof) {
330                     if (stream.look() == "\\") {
331                         if (Lang.isNewline(stream.look(1))) {
332                             do {
333                                 stream.next();
334                             } while (!stream.look().eof && Lang.isNewline(stream.look()));
335                             string += "\\\n";
336                         }
337                         else {
338                             string += stream.next(2);
339                         }
340                     }
341                     else if (stream.look() == "\"") {
342                         string += stream.next();
343                         tokens.push(new Token(string, "STRN", "DOUBLE_QUOTE", this.line));
344                         return true;
345                     }
346                     else {
347                         string += stream.next();
348                     }
349                 }
350             }
351             return false; // error! unterminated string
352         },
353
354         /**
355             @returns {Boolean} Was the token found?
356          */
357         read_snquote : function(/**JSDOC.TokenStream*/stream, tokens) {
358             if (stream.look() == "'") {
359                 // find terminator
360                 var string = stream.next();
361                 
362                 while (!stream.look().eof) {
363                     if (stream.look() == "\\") { // escape sequence
364                         string += stream.next(2);
365                     }
366                     else if (stream.look() == "'") {
367                         string += stream.next();
368                         tokens.push(new Token(string, "STRN", "SINGLE_QUOTE", this.line));
369                         return true;
370                     }
371                     else {
372                         string += stream.next();
373                     }
374                 }
375             }
376             return false; // error! unterminated string
377         },
378
379         /**
380             @returns {Boolean} Was the token found?
381          */
382         read_numb : function(/**JSDOC.TokenStream*/stream, tokens) {
383             if (stream.look() === "0" && stream.look(1) == "x") {
384                 return this.read_hex(stream, tokens);
385             }
386             
387             var found = "";
388             
389             while (!stream.look().eof && Lang.isNumber(found+stream.look())){
390                 found += stream.next();
391             }
392             
393             if (found === "") {
394                 return false;
395             }
396             else {
397                 if (/^0[0-7]/.test(found)) tokens.push(new Token(found, "NUMB", "OCTAL", this.line));
398                 else tokens.push(new Token(found, "NUMB", "DECIMAL", this.line));
399                 return true;
400             }
401         },
402         /*t:
403             requires("../lib/JSDOC/TextStream.js");
404             requires("../lib/JSDOC/Token.js");
405             requires("../lib/JSDOC/Lang.js");
406             
407             plan(3, "testing read_numb");
408             
409             //// setup
410             var src = "function foo(num){while (num+8.0 >= 0x20 && num < 0777){}}";
411             var tr = new TokenReader();
412             var tokens = tr.tokenize(new TextStream(src));
413             
414             var hexToken, octToken, decToken;
415             for (var i = 0; i < tokens.length; i++) {
416                 if (tokens[i].name == "HEX_DEC") hexToken = tokens[i];
417                 if (tokens[i].name == "OCTAL") octToken = tokens[i];
418                 if (tokens[i].name == "DECIMAL") decToken = tokens[i];
419             }
420             ////
421             
422             is(decToken.data, "8.0", "decimal number is found in source.");
423             is(hexToken.data, "0x20", "hexdec number is found in source (issue #99).");
424             is(octToken.data, "0777", "octal number is found in source.");
425         */
426
427         /**
428             @returns {Boolean} Was the token found?
429          */
430         read_hex : function(/**JSDOC.TokenStream*/stream, tokens) {
431             var found = stream.next(2);
432             
433             while (!stream.look().eof) {
434                 if (Lang.isHexDec(found) && !Lang.isHexDec(found+stream.look())) { // done
435                     tokens.push(new Token(found, "NUMB", "HEX_DEC", this.line));
436                     return true;
437                 }
438                 else {
439                     found += stream.next();
440                 }
441             }
442             return false;
443         },
444
445         /**
446             @returns {Boolean} Was the token found?
447          */
448         read_regx : function(/**JSDOC.TokenStream*/stream, tokens) {
449             var last;
450             if (
451                 stream.look() == "/"
452                 && 
453                 (
454                     
455                     (
456                         !(last = tokens.lastSym()) // there is no last, the regex is the first symbol
457                         || 
458                         (
459                                !last.is("NUMB")
460                             && !last.is("NAME")
461                             && !last.is("RIGHT_PAREN")
462                             && !last.is("RIGHT_BRACKET")
463                         )
464                     )
465                 )
466             ) {
467                 var regex = stream.next();
468                 
469                 while (!stream.look().eof) {
470                     if (stream.look() == "\\") { // escape sequence
471                         regex += stream.next(2);
472                     }
473                     else if (stream.look() == "/") {
474                         regex += stream.next();
475                         
476                         while (/[gmi]/.test(stream.look())) {
477                             regex += stream.next();
478                         }
479                         
480                         tokens.push(new Token(regex, "REGX", "REGX", this.line));
481                         return true;
482                     }
483                     else {
484                         regex += stream.next();
485                     }
486                 }
487                 // error: unterminated regex
488             }
489             return false;
490         }
491 });