mirror of
				https://github.com/Telecominfraproject/ols-ucentral-schema.git
				synced 2025-10-31 01:58:03 +00:00 
			
		
		
		
	 404a9a34c2
			
		
	
	404a9a34c2
	
	
	
		
			
			- Introduce virtual uCentral class hierarchy - Add documentation for toplevel helper functions Signed-off-by: Jo-Philipp Wich <jo@mein.io>
		
			
				
	
	
		
			651 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			651 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /*
 | |
|  * ucode-transpiler.js - JSDoc plugin to naively transpile ucode into JS.
 | |
|  *
 | |
|  * Copyright (C) 2021 Jo-Philipp Wich <jo@mein.io>
 | |
|  *
 | |
|  * Permission to use, copy, modify, and/or distribute this software for any
 | |
|  * purpose with or without fee is hereby granted, provided that the above
 | |
|  * copyright notice and this permission notice appear in all copies.
 | |
|  *
 | |
|  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | |
|  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 | |
|  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 | |
|  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | |
|  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 | |
|  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 | |
|  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | |
|  */
 | |
| 
 | |
| 'use strict';
 | |
| 
 | |
| function skipString(s) {
 | |
|   let q = s.charAt(0);
 | |
|   let esc = false;
 | |
| 
 | |
|   for (let i = 1; i < s.length; i++) {
 | |
|     let c = s.charAt(i);
 | |
| 
 | |
|     if (esc) {
 | |
|       esc = false;
 | |
|       continue;
 | |
|     }
 | |
|     else if (c == '\\') {
 | |
|       esc = true;
 | |
|       continue;
 | |
|     }
 | |
|     else if (c == q) {
 | |
|       // consume regex literal flags
 | |
|       while (q == '/' && s.charAt(i + 1).match(/[gis]/))
 | |
|         i++;
 | |
| 
 | |
|       return [ s.substring(0, i + 1), s.substring(i + 1) ];
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   throw 'Unterminated string literal';
 | |
| }
 | |
| 
 | |
| function skipComment(s) {
 | |
|   let q = s.charAt(1),
 | |
|       end = (q == '/') ? '\n' : '*/',
 | |
|       esc = false;
 | |
| 
 | |
|   for (let i = 2; i < s.length; i++) {
 | |
|     let c = s.charAt(i);
 | |
| 
 | |
|     if (esc) {
 | |
|       esc = false;
 | |
|       continue;
 | |
|     }
 | |
|     else if (c == '\\') {
 | |
|       esc = true;
 | |
|       continue;
 | |
|     }
 | |
|     else if (s.substring(i, i + end.length) == end) {
 | |
|       return [ s.substring(0, i + end.length), s.substring(i + end.length) ];
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   if (q == '*')
 | |
|     throw 'Unterminated multiline comment';
 | |
| 
 | |
|   return [ s, '' ];
 | |
| }
 | |
| 
 | |
| function escapeString(s) {
 | |
|   return "'" + s.replace(/[\\\n']/g, '\\$&') + "';";
 | |
| }
 | |
| 
 | |
| const keywords = [
 | |
|   'break',
 | |
|   'case',
 | |
|   'catch',
 | |
|   'const',
 | |
|   'continue',
 | |
|   'default',
 | |
|   'delete',
 | |
|   'elif',
 | |
|   'else',
 | |
|   'endfor',
 | |
|   'endfunction',
 | |
|   'endif',
 | |
|   'endwhile',
 | |
|   'false',
 | |
|   'for',
 | |
|   'function',
 | |
|   'if',
 | |
|   'in',
 | |
|   'let',
 | |
|   'null',
 | |
|   'return',
 | |
|   'switch',
 | |
|   'this',
 | |
|   'true',
 | |
|   'try',
 | |
|   'while'
 | |
| ];
 | |
| 
 | |
| const reserved = [
 | |
|   'await',
 | |
|   'class',
 | |
|   'debugger',
 | |
|   'enum',
 | |
|   'export',
 | |
|   'extends',
 | |
|   'finally',
 | |
|   'implements',
 | |
|   'import',
 | |
|   'instanceof',
 | |
|   'interface',
 | |
|   'new',
 | |
|   'package',
 | |
|   'private',
 | |
|   'protected',
 | |
|   'public',
 | |
|   'super',
 | |
|   'throw',
 | |
|   'typeof',
 | |
|   'var',
 | |
|   'void',
 | |
|   'with',
 | |
|   'yield'
 | |
| ];
 | |
| 
 | |
| function Transpiler(s, raw) {
 | |
|   this.source = s;
 | |
|   this.offset = 0;
 | |
|   this.tokens = [];
 | |
| 
 | |
|   if (raw) {
 | |
|     this.state = 'identify_token';
 | |
|     this.block = 'block_statement';
 | |
|   }
 | |
|   else {
 | |
|     this.state = 'identify_block';
 | |
|   }
 | |
| 
 | |
|   let token = null;
 | |
| 
 | |
|   do {
 | |
|     token = this.parse();
 | |
| 
 | |
|     switch (token.type) {
 | |
|     case '-}}':
 | |
|     case '-%}':
 | |
|     case '}}':
 | |
|     case '%}':
 | |
|       if (raw)
 | |
|         throw 'Unexpected token "' + token.type + '"';
 | |
| 
 | |
|       break;
 | |
|     }
 | |
| 
 | |
|     this.tokens.push(token);
 | |
|   }
 | |
|   while (token.type != 'eof');
 | |
| }
 | |
| 
 | |
| Transpiler.prototype = {
 | |
|   parse: function() {
 | |
|     let m;
 | |
| 
 | |
|     switch (this.state) {
 | |
|     case 'identify_block':
 | |
|       m = this.source.match(/^((?:.|\n)*?)((\{[{%#])(?:.|\n)*)$/);
 | |
| 
 | |
|       if (m) {
 | |
|         switch (m[3]) {
 | |
|         case '{#':
 | |
|           this.state = 'block_comment';
 | |
|           break;
 | |
| 
 | |
|         case '{{':
 | |
|           this.state = 'block_expression';
 | |
|           break;
 | |
| 
 | |
|         case '{%':
 | |
|           this.state = 'block_statement';
 | |
|           break;
 | |
|         }
 | |
| 
 | |
|         this.source = m[2];
 | |
| 
 | |
|         return { type: 'text', value: escapeString(m[1]), prefix: '' };
 | |
|       }
 | |
|       else if (this.source.length) {
 | |
|         let t = { type: 'text', value: escapeString(this.source), prefix: '' };
 | |
| 
 | |
|         this.source = '';
 | |
| 
 | |
|         return t;
 | |
|       }
 | |
|       else {
 | |
|         return { type: 'eof', value: '', prefix: '' };
 | |
|       }
 | |
| 
 | |
|       break;
 | |
| 
 | |
|     case 'block_comment':
 | |
|       m = this.source.match(/^((?:.|\n)*?#\})((?:.|\n)*)$/);
 | |
| 
 | |
|       if (!m)
 | |
|         throw 'Unterminated comment block';
 | |
| 
 | |
|       this.source = m[2];
 | |
|       this.state = 'identify_block';
 | |
|       this.block = null;
 | |
| 
 | |
|       return {
 | |
|         type: 'comment',
 | |
|         value: m[1].replace(/\*\}/g, '*\\}').replace(/^\{##/, '/**').replace(/^\{#/, '/*').replace(/#\}$/, '*/'),
 | |
|         prefix: ''
 | |
|       };
 | |
| 
 | |
|     case 'block_expression':
 | |
|       this.state = 'identify_token';
 | |
|       this.block = 'expression';
 | |
|       this.source = this.source.replace(/^\{\{[+-]?/, '');
 | |
| 
 | |
|       return this.parse();
 | |
| 
 | |
|     case 'block_statement':
 | |
|       this.state = 'identify_token';
 | |
|       this.block = 'statement';
 | |
|       this.source = this.source.replace(/^\{%[+-]?/, '');
 | |
| 
 | |
|       return this.parse();
 | |
| 
 | |
|     case 'identify_token':
 | |
|       let t = this.parsetoken();
 | |
| 
 | |
|       if ((this.block == 'expression' && (t.type == '-}}' || t.type == '}}')) ||
 | |
|           (this.block == 'statement' && (t.type == '-%}' || t.type == '%}'))) {
 | |
|         this.state = 'identify_block';
 | |
|         this.block = null;
 | |
| 
 | |
|         return {
 | |
|           type: ';',
 | |
|           value: ';',
 | |
|           prefix: t.prefix
 | |
|         };
 | |
|       }
 | |
| 
 | |
|       if (this.block == 'expression' && t.type == 'eof')
 | |
|         throw 'Unterminated expression block';
 | |
| 
 | |
|       return t;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   parsetoken: function() {
 | |
|     let token = this.source.match(/^((?:\s|\n)*)(-[%}]\}|<<=|>>=|===|!==|\.\.\.|\?\.\[|\?\.\(|[%}]\}|\/\*|\/\/|&&|[+&|^\/%*-=!<>]=|--|\+\+|<<|>>|\|\||=>|\?\.|[+=&|[\]\^{}:,~\/>!<%*()?;.'"-]|(\d+(?:\.\d+)?)|(\w+))((?:.|\n)*)$/);
 | |
|     let rv, r, t;
 | |
| 
 | |
|     if (token) {
 | |
|       switch (token[2]) {
 | |
|       case '"':
 | |
|       case "'":
 | |
|         r = skipString(token[2] + token[5]);
 | |
|         rv = r[0];
 | |
|         t = 'string';
 | |
|         this.source = r[1];
 | |
|         break;
 | |
| 
 | |
|       case '//':
 | |
|       case '/*':
 | |
|         r = skipComment(token[2] + token[5]);
 | |
|         rv = r[0];
 | |
|         t = 'comment';
 | |
|         this.source = r[1];
 | |
|         break;
 | |
| 
 | |
|       case '/':
 | |
|       case '/=':
 | |
|         if (this.lastToken.match(/[(,=:[!&|?{};]/)) {
 | |
|           r = skipString(token[2] + token[5]);
 | |
|           rv = r[0];
 | |
|           t = 'regexp';
 | |
|           this.source = r[1];
 | |
|         }
 | |
|         else {
 | |
|           rv = token[2];
 | |
|           t = token[2];
 | |
|           this.source = token[5];
 | |
|         }
 | |
| 
 | |
|         break;
 | |
| 
 | |
|       default:
 | |
|         this.source = token[5];
 | |
| 
 | |
|         if (token[3]) {
 | |
|           rv = token[3];
 | |
| 
 | |
|           if (token[3].indexOf('.') != -1)
 | |
|             t = 'double';
 | |
|           else
 | |
|             t = 'number';
 | |
|         }
 | |
|         else if (token[4]) {
 | |
|           rv = token[4];
 | |
| 
 | |
|           if (keywords.indexOf(token[4]) != -1) {
 | |
|             t = token[4];
 | |
|           }
 | |
|           else {
 | |
|             t = 'label';
 | |
| 
 | |
|             if (reserved.indexOf(token[4]) != -1)
 | |
|               rv += '_';
 | |
|           }
 | |
|         }
 | |
|         else {
 | |
|           rv = token[2];
 | |
|           t = token[2];
 | |
|         }
 | |
| 
 | |
|         break;
 | |
|       }
 | |
| 
 | |
|       this.lastToken = token[2];
 | |
| 
 | |
|       return {
 | |
|         type: t,
 | |
|         value: rv,
 | |
|         prefix: token[1]
 | |
|       };
 | |
|     }
 | |
|     else if (this.source.match(/^\s*$/)) {
 | |
|       rv = this.source;
 | |
|       this.source = '';
 | |
| 
 | |
|       return {
 | |
|         type: 'eof',
 | |
|         value: '',
 | |
|         prefix: rv
 | |
|       };
 | |
|     }
 | |
|     else {
 | |
|       throw 'Unrecognized character near [...' + this.source + ']';
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   next: function() {
 | |
|     let idx = this.offset++;
 | |
| 
 | |
|     return this.tokens[Math.min(idx, this.tokens.length - 1)];
 | |
|   },
 | |
| 
 | |
|   skip_statement: function(tokens, ends) {
 | |
|     let nest = 0;
 | |
| 
 | |
|     while (true) {
 | |
|       let token = this.next();
 | |
| 
 | |
|       if (token.type == 'eof') {
 | |
|         this.offset--;
 | |
| 
 | |
|         break;
 | |
|       }
 | |
| 
 | |
|       if (nest == 0 && ends.indexOf(token.type) != -1) {
 | |
|         this.offset--;
 | |
| 
 | |
|         break;
 | |
|       }
 | |
| 
 | |
|       switch (token.type) {
 | |
|       case '(':
 | |
|       case '[':
 | |
|       case '{':
 | |
|         nest++;
 | |
|         break;
 | |
| 
 | |
|       case ')':
 | |
|       case ']':
 | |
|       case '}':
 | |
|         nest--;
 | |
|         break;
 | |
|       }
 | |
| 
 | |
|       tokens.push(token);
 | |
| 
 | |
|       if (token.type == ';')
 | |
|         break;
 | |
|     }
 | |
| 
 | |
|     return tokens;
 | |
|   },
 | |
| 
 | |
|   skip_paren: function(tokens) {
 | |
|     let token = this.next();
 | |
|     let depth = 0;
 | |
| 
 | |
|     if (token.type != '(')
 | |
|       throw 'Unexpected token, expected "(", got "' + token.type + '"';
 | |
| 
 | |
|     do {
 | |
|       tokens.push(token);
 | |
| 
 | |
|       switch (token.type) {
 | |
|       case '(':
 | |
|         depth++;
 | |
|         break;
 | |
| 
 | |
|       case ')':
 | |
|         depth--;
 | |
| 
 | |
|         if (depth == 0)
 | |
|           return token;
 | |
| 
 | |
|         break;
 | |
| 
 | |
|       case 'eof':
 | |
|         throw 'Unexpected EOF';
 | |
|       }
 | |
| 
 | |
|       token = this.next();
 | |
|     }
 | |
|     while (depth != 0);
 | |
|   },
 | |
| 
 | |
|   assert_token: function(tokens, type) {
 | |
|     let token = this.next();
 | |
| 
 | |
|     if (token.type != type)
 | |
|       throw 'Unexpected token, expected "' + type + '", got "' + token.type + '"';
 | |
| 
 | |
|     tokens.push(token);
 | |
| 
 | |
|     return tokens;
 | |
|   },
 | |
| 
 | |
|   check_token: function(tokens, type) {
 | |
|     let token = this.next();
 | |
| 
 | |
|     if (token.type != type) {
 | |
|       this.offset--;
 | |
| 
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     tokens.push(token);
 | |
| 
 | |
|     return true;
 | |
|   },
 | |
| 
 | |
|   patch: function(tokens, type, value) {
 | |
|     tokens[tokens.length - 1].type = type;
 | |
|     tokens[tokens.length - 1].value = (value != null) ? value : type;
 | |
|   },
 | |
| 
 | |
|   skip_block: function(tokens, ends) {
 | |
|     while (true) {
 | |
|       let off = tokens.length;
 | |
| 
 | |
|       if (this.check_token(tokens, 'if')) {
 | |
|         this.skip_paren(tokens);
 | |
| 
 | |
|         if (this.check_token(tokens, ':')) {
 | |
|           this.patch(tokens, '{');
 | |
| 
 | |
|           this.skip_block(tokens, ['else', 'elif', 'endif']);
 | |
| 
 | |
|           while (tokens[tokens.length - 1].type == 'elif') {
 | |
|             let elif = tokens.pop();
 | |
| 
 | |
|             tokens.push(
 | |
|               { type: '}',    value: '}',    prefix: '' },
 | |
|               { type: 'else', value: 'else', prefix: elif.prefix },
 | |
|               { type: 'if',   value: 'if',   prefix: ' ' }
 | |
|             );
 | |
| 
 | |
|             this.skip_paren(tokens);
 | |
| 
 | |
|             this.assert_token(tokens, ':');
 | |
|             this.patch(tokens, '{');
 | |
| 
 | |
|             this.skip_block(tokens, ['elif', 'else', 'endif']);
 | |
|           }
 | |
| 
 | |
|           if (tokens[tokens.length - 1].type == 'else') {
 | |
|             let else_ = tokens.pop();
 | |
| 
 | |
|             tokens.push(
 | |
|               { type: '}',    value: '}',    prefix: '' },
 | |
|               { type: 'else', value: 'else', prefix: else_.prefix },
 | |
|               { type: '{',    value: '{',    prefix: ' ' }
 | |
|             );
 | |
| 
 | |
|             this.skip_block(tokens, ['endif']);
 | |
|           }
 | |
| 
 | |
|           this.patch(tokens, '}');
 | |
|         }
 | |
|         else if (this.check_token(tokens, '{')) {
 | |
|           this.skip_block(tokens, ['}']);
 | |
| 
 | |
|           if (!this.check_token(tokens, 'else'))
 | |
|             continue;
 | |
| 
 | |
|           if (this.check_token(tokens, '{'))
 | |
|             this.skip_block(tokens, ['}']);
 | |
|           else
 | |
|             this.skip_statement(tokens, ends);
 | |
|         }
 | |
|         else {
 | |
|           this.skip_statement(tokens, ends);
 | |
|         }
 | |
|       }
 | |
|       else if (this.check_token(tokens, 'for')) {
 | |
|         let cond = [];
 | |
| 
 | |
|         this.skip_paren(cond);
 | |
| 
 | |
|         // Transform `for (x, y in ...)` into `for (x/*, y*/ in ...)`
 | |
|         if (cond.length > 5 &&
 | |
|             cond[1].type == 'label' &&
 | |
|             cond[2].type == ',' &&
 | |
|             cond[3].type == 'label' &&
 | |
|             cond[4].type == 'in') {
 | |
|           cond[2].type = 'comment';
 | |
|           cond[2].value = '/*' + cond[2].value;
 | |
|           cond[3].type = 'comment';
 | |
|           cond[3].value = cond[3].value + '*/';
 | |
|         }
 | |
| 
 | |
|         // Transform `for (let x, y in ...)` into `for (let x/*, y*/ in ...)`
 | |
|         else if (cond.length > 6 &&
 | |
|                  cond[1].type == 'let' &&
 | |
|                  cond[2].type == 'label' &&
 | |
|                  cond[3].type == ',' &&
 | |
|                  cond[4].type == 'label' &&
 | |
|                  cond[5].type == 'in') {
 | |
|           cond[3].type = 'comment';
 | |
|           cond[3].value = '/*' + cond[3].value;
 | |
|           cond[4].type = 'comment';
 | |
|           cond[4].value = cond[4].value + '*/';
 | |
|         }
 | |
| 
 | |
|         tokens.push(...cond);
 | |
| 
 | |
|         if (this.check_token(tokens, ':')) {
 | |
|           this.patch(tokens, '{');
 | |
|           this.skip_block(tokens, ['endfor']);
 | |
|           this.patch(tokens, '}');
 | |
|         }
 | |
|         else if (this.check_token(tokens, '{'))
 | |
|           this.skip_block(tokens, ['}']);
 | |
|         else
 | |
|           this.skip_statement(tokens, ends);
 | |
|       }
 | |
|       else if (this.check_token(tokens, 'while')) {
 | |
|         this.skip_paren(tokens);
 | |
| 
 | |
|         if (this.check_token(tokens, ':')) {
 | |
|           this.patch(tokens, '{');
 | |
|           this.skip_block(tokens, ['endwhile']);
 | |
|           this.patch(tokens, '}');
 | |
|         }
 | |
|         else if (this.check_token(tokens, '{'))
 | |
|           this.skip_block(tokens, ['}']);
 | |
|         else
 | |
|           this.skip_statement(tokens, ends);
 | |
|       }
 | |
|       else if (this.check_token(tokens, 'function')) {
 | |
|         this.check_token(tokens, 'label');
 | |
|         this.skip_paren(tokens);
 | |
| 
 | |
|         if (this.check_token(tokens, ':')) {
 | |
|           this.patch(tokens, '{');
 | |
|           this.skip_block(tokens, ['endfunction']);
 | |
|           this.patch(tokens, '}');
 | |
|         }
 | |
|         else if (this.check_token(tokens, '{'))
 | |
|           this.skip_block(tokens, ['}']);
 | |
|       }
 | |
|       else if (this.check_token(tokens, 'try')) {
 | |
|         this.assert_token(tokens, '{');
 | |
|         this.skip_block(tokens, ['}']);
 | |
|         this.assert_token(tokens, 'catch');
 | |
| 
 | |
|         // Transform `try { ... } catch { ... }` into `try { ... } catch(e) { ... }`
 | |
|         if (this.tokens[this.offset].type == '(')
 | |
|           this.skip_paren(tokens);
 | |
|         else
 | |
|           tokens.push(
 | |
|             { type: '(',     value: '(', prefix: '' },
 | |
|             { type: 'label', value: 'e', prefix: '' },
 | |
|             { type: ')',     value: ')', prefix: '' }
 | |
|           );
 | |
| 
 | |
|         this.assert_token(tokens, '{');
 | |
|         this.skip_block(tokens, ['}']);
 | |
|       }
 | |
|       else if (this.check_token(tokens, 'switch')) {
 | |
|         this.skip_paren(tokens);
 | |
|         this.assert_token(tokens, '{');
 | |
|         this.skip_block(tokens, ['}']);
 | |
|       }
 | |
|       else if (this.check_token(tokens, '{')) {
 | |
|         this.skip_block(tokens, ['}']);
 | |
|       }
 | |
|       else if (this.check_token(tokens, 'text')) {
 | |
|         /* pass */
 | |
|       }
 | |
|       else if (this.check_token(tokens, 'comment')) {
 | |
|         /* pass */
 | |
|       }
 | |
|       else {
 | |
|         this.skip_statement(tokens, ends);
 | |
|       }
 | |
| 
 | |
|       for (let type of ends)
 | |
|         if (this.check_token(tokens, type))
 | |
|           return tokens;
 | |
| 
 | |
|       if (this.check_token([], 'eof'))
 | |
|         break;
 | |
|     }
 | |
| 
 | |
|     throw 'Unexpected EOF';
 | |
|   },
 | |
| 
 | |
|   transpile: function() {
 | |
|     let tokens = [];
 | |
| 
 | |
|     this.skip_block(tokens, ['eof']);
 | |
| 
 | |
|     return tokens.map(t => t.prefix + t.value).join('');
 | |
|   }
 | |
| };
 | |
| 
 | |
| exports.handlers = {
 | |
|   beforeParse: function(e) {
 | |
|     let raw = !e.source.match(/\{[{%]/) || e.source.match(/^#!([a-z\/]*)ucode[ \t]+-[A-Z]*R/),
 | |
|         t = new Transpiler(e.source, raw);
 | |
| 
 | |
|     e.source = t.transpile();
 | |
|   }
 | |
| };
 |