"use strict"; const fs = require("fs"); const path = require("path"); const hasOwn = Object.prototype.hasOwnProperty; // ============================================================================ // [Tokenizer] // ============================================================================ // The list of "token types" which our lexer understands: const tokenizerPatterns = [ { type: "space" , re: /^\s+/ }, { type: "comment" , re: /^(\/\/.*(\n|$)|\/\*.*\*\/)/ }, { type: "symbol" , re: /^[a-zA-Z_]\w*/ }, { type: "integer" , re: /^(-?\d+|0[x|X][0-9A-Fa-f]+)(l)?(l)?(u)?\b/ }, { type: "comma" , re: /^,/ }, { type: "operator", re: /(\+|\+\+|-|--|\/|\*|<<|>>|=|==|<|<=|>|>=|&|&&|\||\|\||\^|~|!)/ }, { type: "paren" , re: /^[\(\)\{\}\[\]]/ } ]; function nextToken(input, from, patterns) { if (from >= input.length) { return { type: "end", begin: from, end: from, content: "" } } const s = input.slice(from); for (var i = 0; i < patterns.length; i++) { const pattern = patterns[i]; const result = s.match(pattern.re); if (result !== null) { const content = result[0]; return { type: pattern.type, begin: from, end: from + content.length, content: content }; } } return { type: "invalid", begin: from, end: from + 1, content: input[from] }; } class Tokenizer { constructor(input, patterns) { this.input = input; this.index = 0; this.patterns = patterns; } next() { for (;;) { const token = nextToken(this.input, this.index, this.patterns); this.index = token.end; if (token.type === "space" || token.type === "comment") continue; return token; } } revert(token) { this.index = token.begin; } } // ============================================================================ // [Parser] // ============================================================================ function parseEnum(input) { const map = Object.create(null); const hasOwn = Object.prototype.hasOwnProperty; const tokenizer = new Tokenizer(input, tokenizerPatterns); var value = -1; for (;;) { var token = tokenizer.next(); if (token.type === "end") break; if (token.type === "symbol") { const symbol = token.content; token = tokenizer.next(); if (token.content === "=") { token = tokenizer.next(); if (token.type !== "integer") throw Error(`Expected an integer after symbol '${symbol} = '`); value = parseInt(token.content); } else { value++; } if (!hasOwn.call(map, symbol)) map[symbol] = value; else console.log(`${symbol} already defined, skipping...`); token = tokenizer.next(); if (token.type !== "comma") tokenizer.revert(token); continue; } throw Error(`Unexpected token ${token.type} (${token.content})`); } return map; } // ============================================================================ // [Stringify] // ============================================================================ function compare(a, b) { return a < b ? -1 : a == b ? 0 : 1; } function compactedSize(table) { var size = 0; for (var i = 0; i < table.length; i++) size += table[i].name.length + 1; return size; } function indexTypeFromSize(size) { if (size <= 256) return 'uint8_t'; else if (size <= 65536) return 'uint16_t'; else return 'uint32_t'; } function indent(s, indentation) { var lines = s.split(/\r?\n/g); if (indentation) { for (var i = 0; i < lines.length; i++) { var line = lines[i]; if (line) lines[i] = indentation + line; } } return lines.join("\n"); } function stringifyEnum(map, options) { var output = ""; const stripPrefix = options.strip; const outputPrefix = options.output; var max = -1; var table = []; for (var k in map) { var name = k; if (stripPrefix) { if (name.startsWith(stripPrefix)) name = name.substring(stripPrefix.length); else throw Error(`Cannot strip prefix '${stripPrefix}' in '${key}'`); } table.push({ name: name, value: map[k] }); max = Math.max(max, map[k]); } table.sort(function(a, b) { return compare(a.value, b.value); }); const unknownIndex = compactedSize(table); table.push({ name: "", value: max + 1 }); const indexType = indexTypeFromSize(compactedSize(table)); function buildStringData() { var s = ""; for (var i = 0; i < table.length; i++) { s += ` "${table[i].name}\\0"`; if (i == table.length - 1) s += `;`; s += `\n`; } return s; } function buildIndexData() { var index = 0; var indexArray = []; for (var i = 0; i < table.length; i++) { while (indexArray.length < table[i].value) indexArray.push(unknownIndex); indexArray.push(index); index += table[i].name.length + 1; } var s = ""; var line = ""; var pos = 0; for (var i = 0; i < indexArray.length; i++) { if (line) line += " "; line += `${indexArray[i]}`; if (i != indexArray.length - 1) line += `,`; if (i == indexArray.length - 1 || line.length >= 72) { s += ` ${line}\n`; line = ""; } } return s; } output += `static const char ${outputPrefix}String[] =\n` + buildStringData() + `\n`; output += `static const ${indexType} ${outputPrefix}Index[] = {\n` + buildIndexData() + `};\n`; return output; } // ============================================================================ // [FileSystem] // ============================================================================ function walkDir(baseDir) { function walk(baseDir, nestedPath, out) { fs.readdirSync(baseDir).forEach((file) => { const stat = fs.statSync(path.join(baseDir, file)); if (stat.isDirectory()) { if (!stat.isSymbolicLink()) walk(path.join(baseDir, file), path.join(nestedPath, file), out) } else { out.push(path.join(nestedPath, file)); } }); return out; } return walk(baseDir, "", []); } // ============================================================================ // [Generator] // ============================================================================ class Generator { constructor(options) { this.enumMap = Object.create(null); this.outputs = []; this.verify = options.verify; this.baseDir = options.baseDir; this.noBackup = options.noBackup; } readEnums() { console.log(`Scanning: ${this.baseDir}`); walkDir(this.baseDir).forEach((fileName) => { if (/\.(cc|cpp|h|hpp)$/.test(fileName)) { const content = fs.readFileSync(path.join(this.baseDir, fileName), "utf8"); this.addEnumsFromSource(fileName, content); if (/@EnumStringBegin(\{.*\})@/.test(content)) this.outputs.push(fileName); } }); } writeEnums() { this.outputs.forEach((fileName) => { console.log(`Output: ${fileName}`); const oldContent = fs.readFileSync(path.join(this.baseDir, fileName), "utf8"); const newContent = this.injectEnumsToSource(oldContent); if (oldContent != newContent) { if (this.verify) { console.log(` FAILED: File is not up to date.`); process.exit(1); } else { if (!this.noBackup) { fs.writeFileSync(path.join(this.baseDir, fileName + ".backup"), oldContent, "utf8"); console.log(` Created ${fileName}.backup`); } fs.writeFileSync(path.join(this.baseDir, fileName), newContent, "utf8"); console.log(` Updated ${fileName}`); } } else { console.log(` File is up to date.`); } }); } addEnumsFromSource(fileName, src) { var found = false; const matches = [...src.matchAll(/(?:@EnumValuesBegin(\{.*\})@|@EnumValuesEnd@)/g)]; for (var i = 0; i < matches.length; i += 2) { const def = matches[i]; const end = matches[i + 1]; if (!def[0].startsWith("@EnumValuesBegin")) throw new Error(`Cannot start with '${def[0]}'`); if (!end) throw new Error(`Missing @EnumValuesEnd for '${def[0]}'`); if (!end[0].startsWith("@EnumValuesEnd@")) throw new Error(`Expected @EnumValuesEnd@ for '${def[0]}' and not '${end[0]}'`); const options = JSON.parse(def[1]); const enumName = options.enum; if (!enumName) throw Error(`Missing 'enum' in '${def[0]}`); if (hasOwn.call(this.enumMap, enumName)) throw new Error(`Enumeration '${enumName}' is already defined`); const startIndex = src.lastIndexOf("\n", def.index) + 1; const endIndex = end.index + end[0].length; if (startIndex === -1 || startIndex > endIndex) throw new Error(`Internal Error, indexes have unexpected values: startIndex=${startIndex} endIndex=${endIndex}`); if (!found) { found = true; console.log(`Found: ${fileName}`); } console.log(` Parsing Enum: ${enumName}`); this.enumMap[enumName] = parseEnum(src.substring(startIndex, endIndex)); } } injectEnumsToSource(src) { const matches = [...src.matchAll(/(?:@EnumStringBegin(\{.*\})@|@EnumStringEnd@)/g)]; var delta = 0; for (var i = 0; i < matches.length; i += 2) { const def = matches[i]; const end = matches[i + 1]; if (!def[0].startsWith("@EnumStringBegin")) throw new Error(`Cannot start with '${def[0]}'`); if (!end) throw new Error(`Missing @EnumStringEnd@ for '${def[0]}'`); if (!end[0].startsWith("@EnumStringEnd@")) throw new Error(`Expected @EnumStringEnd@ for '${def[0]}' and not '${end[0]}'`); const options = JSON.parse(def[1]); const enumName = options.enum; if (!enumName) throwError(`Missing 'name' in '${def[0]}`); if (!hasOwn.call(this.enumMap, enumName)) throw new Error(`Enumeration '${enumName}' not found`); console.log(` Injecting Enum: ${enumName}`); const startIndex = src.indexOf("\n", def.index + delta) + 1; const endIndex = src.lastIndexOf("\n", end.index + delta) + 1; if (startIndex === -1 || endIndex === -1 || startIndex > endIndex) throw new Error(`Internal Error, indexes have unexpected values: startIndex=${startIndex} endIndex=${endIndex}`); // Calculate the indentation. const indentation = (function() { const begin = src.lastIndexOf("\n", def.index + delta) + 1; const end = src.indexOf("/", begin); return src.substring(begin, end); })(); const newContent = indent(stringifyEnum(this.enumMap[enumName], options), indentation); src = src.substring(0, startIndex) + newContent + src.substring(endIndex); delta -= endIndex - startIndex; delta += newContent.length; } return src; } } const generator = new Generator({ baseDir : path.resolve(__dirname, "../src"), verify : process.argv.indexOf("--verify") !== -1, noBackup: process.argv.indexOf("--no-backup") !== -1 }); generator.readEnums(); generator.writeEnums();