odin-blend2d

Odin bindings to Blend2D
Log | Files | Refs | README | LICENSE

enumgen.js (11232B)


      1 "use strict";
      2 
      3 const fs = require("fs");
      4 const path = require("path");
      5 
      6 // ============================================================================
      7 // [Tokenizer]
      8 // ============================================================================
      9 
     10 // The list of "token types" which our lexer understands:
     11 const tokenizerPatterns = [
     12   { type: "space"   , re: /^\s+/ },
     13   { type: "comment" , re: /^(\/\/.*(\n|$)|\/\*.*\*\/)/ },
     14   { type: "symbol"  , re: /^[a-zA-Z_]\w*/ },
     15   { type: "integer" , re: /^(-?\d+|0[x|X][0-9A-Fa-f]+)(l)?(l)?(u)?\b/ },
     16   { type: "comma"   , re: /^,/ },
     17   { type: "operator", re: /(\+|\+\+|-|--|\/|\*|<<|>>|=|==|<|<=|>|>=|&|&&|\||\|\||\^|~|!)/ },
     18   { type: "paren"   , re: /^[\(\)\{\}\[\]]/ }
     19 ];
     20 
     21 function nextToken(input, from, patterns) {
     22   if (from >= input.length) {
     23     return {
     24       type: "end",
     25       begin: from,
     26       end: from,
     27       content: ""
     28     }
     29   }
     30 
     31   const s = input.slice(from);
     32   for (var i = 0; i < patterns.length; i++) {
     33     const pattern = patterns[i];
     34     const result = s.match(pattern.re);
     35 
     36     if (result !== null) {
     37       const content = result[0];
     38       return {
     39         type: pattern.type,
     40         begin: from,
     41         end: from + content.length,
     42         content: content
     43       };
     44     }
     45   }
     46 
     47   return {
     48     type: "invalid",
     49     begin: from,
     50     end: from + 1,
     51     content: input[from]
     52   };
     53 }
     54 
     55 class Tokenizer {
     56   constructor(input, patterns) {
     57     this.input = input;
     58     this.index = 0;
     59     this.patterns = patterns;
     60   }
     61 
     62   next() {
     63     for (;;) {
     64       const token = nextToken(this.input, this.index, this.patterns);
     65       this.index = token.end;
     66       if (token.type === "space" || token.type === "comment")
     67         continue;
     68       return token;
     69     }
     70   }
     71 
     72   revert(token) {
     73     this.index = token.begin;
     74   }
     75 }
     76 
     77 // ============================================================================
     78 // [Parser]
     79 // ============================================================================
     80 
     81 function parseEnum(input) {
     82   const map = Object.create(null);
     83   const tokenizer = new Tokenizer(input, tokenizerPatterns);
     84 
     85   var value = -1;
     86 
     87   for (;;) {
     88     var token = tokenizer.next();
     89     if (token.type === "end")
     90       break;
     91 
     92     if (token.type === "symbol") {
     93       const symbol = token.content;
     94       token = tokenizer.next();
     95       if (token.content === "=") {
     96         token = tokenizer.next();
     97         if (token.type !== "integer")
     98           throw Error(`Expected an integer after symbol '${symbol} = '`);
     99         value = parseInt(token.content);
    100       }
    101       else {
    102         value++;
    103       }
    104 
    105       if (!Object.hasOwn(map, symbol))
    106         map[symbol] = value;
    107       else
    108         console.log(`${symbol} already defined, skipping...`);
    109 
    110       token = tokenizer.next();
    111       if (token.type !== "comma")
    112         tokenizer.revert(token);
    113       continue;
    114     }
    115 
    116     throw Error(`Unexpected token ${token.type} (${token.content})`);
    117   }
    118 
    119   return map;
    120 }
    121 
    122 // ============================================================================
    123 // [Stringify]
    124 // ============================================================================
    125 
    126 function compare(a, b) {
    127   return a < b ? -1 : a == b ? 0 : 1;
    128 }
    129 
    130 function compactedSize(table) {
    131   var size = 0;
    132   for (var i = 0; i < table.length; i++)
    133     size += table[i].name.length + 1;
    134   return size;
    135 }
    136 
    137 function indexTypeFromSize(size) {
    138   if (size <= 256)
    139     return 'uint8_t';
    140   else if (size <= 65536)
    141     return 'uint16_t';
    142   else
    143     return 'uint32_t';
    144 }
    145 
    146 function indent(s, indentation) {
    147   var lines = s.split(/\r?\n/g);
    148   if (indentation) {
    149     for (var i = 0; i < lines.length; i++) {
    150       var line = lines[i];
    151       if (line) lines[i] = indentation + line;
    152     }
    153   }
    154 
    155   return lines.join("\n");
    156 }
    157 
    158 function stringifyEnum(map, options) {
    159   var output = "";
    160 
    161   const stripPrefix = options.strip;
    162   const outputPrefix = options.output;
    163 
    164   var max = -1;
    165   var table = [];
    166 
    167   for (var k in map) {
    168     var name = k;
    169     if (stripPrefix) {
    170       if (name.startsWith(stripPrefix))
    171         name = name.substring(stripPrefix.length);
    172       else
    173         throw Error(`Cannot strip prefix '${stripPrefix}' in '${k}'`);
    174     }
    175 
    176     table.push({ name: name, value: map[k] });
    177     max = Math.max(max, map[k]);
    178   }
    179 
    180   table.sort(function(a, b) { return compare(a.value, b.value); });
    181 
    182   const unknownIndex = compactedSize(table);
    183   table.push({ name: "<Unknown>", value: max + 1 });
    184 
    185   const indexType = indexTypeFromSize(compactedSize(table));
    186 
    187   function buildStringData() {
    188     var s = "";
    189     for (var i = 0; i < table.length; i++) {
    190       s += `  "${table[i].name}\\0"`;
    191       if (i == table.length - 1)
    192         s += `;`;
    193       s += `\n`;
    194     }
    195     return s;
    196   }
    197 
    198   function buildIndexData() {
    199     var index = 0;
    200     var indexArray = [];
    201 
    202     for (var i = 0; i < table.length; i++) {
    203       while (indexArray.length < table[i].value)
    204         indexArray.push(unknownIndex);
    205 
    206       indexArray.push(index);
    207       index += table[i].name.length + 1;
    208     }
    209 
    210     var s = "";
    211     var line = "";
    212     var pos = 0;
    213 
    214     for (var i = 0; i < indexArray.length; i++) {
    215       if (line)
    216         line += " ";
    217 
    218       line += `${indexArray[i]}`;
    219       if (i != indexArray.length - 1)
    220         line += `,`;
    221 
    222       if (i == indexArray.length - 1 || line.length >= 72) {
    223         s += `  ${line}\n`;
    224         line = "";
    225       }
    226     }
    227 
    228     return s;
    229   }
    230 
    231   output += `static const char ${outputPrefix}_data[] =\n` + buildStringData() + `\n`;
    232   output += `static const ${indexType} ${outputPrefix}_index[] = {\n` + buildIndexData() + `};\n`;
    233 
    234   return output;
    235 }
    236 
    237 // ============================================================================
    238 // [FileSystem]
    239 // ============================================================================
    240 
    241 function walkDir(baseDir) {
    242   function walk(baseDir, nestedPath, out) {
    243     fs.readdirSync(baseDir).forEach((file) => {
    244       const stat = fs.statSync(path.join(baseDir, file));
    245       if (stat.isDirectory()) {
    246         if (!stat.isSymbolicLink())
    247           walk(path.join(baseDir, file), path.join(nestedPath, file), out)
    248       }
    249       else {
    250         out.push(path.join(nestedPath, file));
    251       }
    252     });
    253     return out;
    254   }
    255 
    256   return walk(baseDir, "", []);
    257 }
    258 
    259 // ============================================================================
    260 // [Generator]
    261 // ============================================================================
    262 
    263 class Generator {
    264   constructor(options) {
    265     this.enumMap = Object.create(null);
    266     this.outputs = [];
    267 
    268     this.verify = options.verify;
    269     this.baseDir = options.baseDir;
    270     this.noBackup = options.noBackup;
    271   }
    272 
    273   readEnums() {
    274     console.log(`Scanning: ${this.baseDir}`);
    275     walkDir(this.baseDir).forEach((fileName) => {
    276       if (/\.(cc|cpp|h|hpp)$/.test(fileName)) {
    277         const content = fs.readFileSync(path.join(this.baseDir, fileName), "utf8");
    278         this.addEnumsFromSource(fileName, content);
    279 
    280         if (/@EnumStringBegin(\{.*\})@/.test(content))
    281           this.outputs.push(fileName);
    282       }
    283     });
    284   }
    285 
    286   writeEnums() {
    287     this.outputs.forEach((fileName) => {
    288       console.log(`Output: ${fileName}`);
    289 
    290       const oldContent = fs.readFileSync(path.join(this.baseDir, fileName), "utf8");
    291       const newContent = this.injectEnumsToSource(oldContent);
    292 
    293       if (oldContent != newContent) {
    294         if (this.verify) {
    295           console.log(`  FAILED: File is not up to date.`);
    296           process.exit(1);
    297         }
    298         else {
    299           if (!this.noBackup) {
    300             fs.writeFileSync(path.join(this.baseDir, fileName + ".backup"), oldContent, "utf8");
    301             console.log(`  Created ${fileName}.backup`);
    302           }
    303           fs.writeFileSync(path.join(this.baseDir, fileName), newContent, "utf8");
    304           console.log(`  Updated ${fileName}`);
    305         }
    306       }
    307       else {
    308         console.log(`  File is up to date.`);
    309       }
    310     });
    311   }
    312 
    313   addEnumsFromSource(fileName, src) {
    314     var found = false;
    315     const matches = [...src.matchAll(/(?:@EnumValuesBegin(\{.*\})@|@EnumValuesEnd@)/g)];
    316 
    317     for (var i = 0; i < matches.length; i += 2) {
    318       const def = matches[i];
    319       const end = matches[i + 1];
    320 
    321       if (!def[0].startsWith("@EnumValuesBegin"))
    322         throw new Error(`Cannot start with '${def[0]}'`);
    323 
    324       if (!end)
    325         throw new Error(`Missing @EnumValuesEnd for '${def[0]}'`);
    326 
    327       if (!end[0].startsWith("@EnumValuesEnd@"))
    328         throw new Error(`Expected @EnumValuesEnd@ for '${def[0]}' and not '${end[0]}'`);
    329 
    330       const options = JSON.parse(def[1]);
    331       const enumName = options.enum;
    332 
    333       if (!enumName)
    334         throw Error(`Missing 'enum' in '${def[0]}`);
    335 
    336       if (Object.hasOwn(this.enumMap, enumName))
    337         throw new Error(`Enumeration '${enumName}' is already defined`);
    338 
    339       const startIndex = src.lastIndexOf("\n", def.index) + 1;
    340       const endIndex = end.index + end[0].length;
    341 
    342       if (startIndex === -1 || startIndex > endIndex)
    343         throw new Error(`Internal Error, indexes have unexpected values: startIndex=${startIndex} endIndex=${endIndex}`);
    344 
    345       if (!found) {
    346         found = true;
    347         console.log(`Found: ${fileName}`);
    348       }
    349 
    350       console.log(`  Parsing Enum: ${enumName}`);
    351       this.enumMap[enumName] = parseEnum(src.substring(startIndex, endIndex));
    352     }
    353   }
    354 
    355   injectEnumsToSource(src) {
    356     const matches = [...src.matchAll(/(?:@EnumStringBegin(\{.*\})@|@EnumStringEnd@)/g)];
    357     var delta = 0;
    358 
    359     for (var i = 0; i < matches.length; i += 2) {
    360       const def = matches[i];
    361       const end = matches[i + 1];
    362 
    363       if (!def[0].startsWith("@EnumStringBegin"))
    364         throw new Error(`Cannot start with '${def[0]}'`);
    365 
    366       if (!end)
    367         throw new Error(`Missing @EnumStringEnd@ for '${def[0]}'`);
    368 
    369       if (!end[0].startsWith("@EnumStringEnd@"))
    370         throw new Error(`Expected @EnumStringEnd@ for '${def[0]}' and not '${end[0]}'`);
    371 
    372       const options = JSON.parse(def[1]);
    373       const enumName = options.enum;
    374 
    375       if (!enumName)
    376         throwError(`Missing 'name' in '${def[0]}`);
    377 
    378       if (!Object.hasOwn(this.enumMap, enumName))
    379         throw new Error(`Enumeration '${enumName}' not found`);
    380 
    381       console.log(`  Injecting Enum: ${enumName}`);
    382 
    383       const startIndex = src.indexOf("\n", def.index + delta) + 1;
    384       const endIndex = src.lastIndexOf("\n", end.index + delta) + 1;
    385 
    386       if (startIndex === -1 || endIndex === -1 || startIndex > endIndex)
    387         throw new Error(`Internal Error, indexes have unexpected values: startIndex=${startIndex} endIndex=${endIndex}`);
    388 
    389       // Calculate the indentation.
    390       const indentation = (function() {
    391         const begin = src.lastIndexOf("\n", def.index + delta) + 1;
    392         const end = src.indexOf("/", begin);
    393         return src.substring(begin, end);
    394       })();
    395 
    396       const newContent = indent(stringifyEnum(this.enumMap[enumName], options), indentation);
    397       src = src.substring(0, startIndex) + newContent + src.substring(endIndex);
    398 
    399       delta -= endIndex - startIndex;
    400       delta += newContent.length;
    401     }
    402 
    403     return src;
    404   }
    405 }
    406 
    407 const generator = new Generator({
    408   baseDir : path.resolve(__dirname, "../src"),
    409   verify  : process.argv.indexOf("--verify") !== -1,
    410   noBackup: process.argv.indexOf("--no-backup") !== -1
    411 });
    412 
    413 generator.readEnums();
    414 generator.writeEnums();