port lexer to cpp

This commit is contained in:
Asaki Yuki 2026-01-18 20:46:18 +07:00
parent cacc641378
commit b3b21f3101
13 changed files with 276 additions and 32 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
build
dist
cache
node_modules

16
binding.gyp Normal file
View file

@ -0,0 +1,16 @@
{
"targets": [
{
"target_name": "asajs-compiler",
"sources": [
"src/native/main.cc",
],
"dependencies": [
"<!(node -p \"require('node-addon-api').targets\"):node_addon_api",
],
"cflags_cc": [ "-std=c++17" ],
}
]
}

54
package-lock.json generated
View file

@ -9,29 +9,72 @@
"version": "4.0.0",
"license": "MIT",
"dependencies": {
"jsonc-parser": "^3.3.1"
"@types/bindings": "^1.5.5",
"bindings": "^1.5.0",
"jsonc-parser": "^3.3.1",
"node-addon-api": "^8.5.0"
},
"devDependencies": {
"@types/node": "^25.0.3",
"node-api-headers": "^1.7.0",
"typescript": "^5.9.3"
}
},
"node_modules/@types/bindings": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@types/bindings/-/bindings-1.5.5.tgz",
"integrity": "sha512-y59PRZBTo2/HuN94qRjyJD+465vGoXMsqz9MMJDbtJL9oT5/B+tAL6c3k10epIinC2/BBkLqKzKC6keukl8wdQ==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": {
"version": "25.0.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz",
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
"dev": true,
"version": "25.0.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.9.tgz",
"integrity": "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.16.0"
}
},
"node_modules/bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"license": "MIT",
"dependencies": {
"file-uri-to-path": "1.0.0"
}
},
"node_modules/file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"license": "MIT"
},
"node_modules/jsonc-parser": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz",
"integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==",
"license": "MIT"
},
"node_modules/node-addon-api": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
"license": "MIT",
"engines": {
"node": "^18 || ^20 || >= 21"
}
},
"node_modules/node-api-headers": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/node-api-headers/-/node-api-headers-1.7.0.tgz",
"integrity": "sha512-uJMGdkhVwu9+I3UsVvI3KW6ICAy/yDfsu5Br9rSnTtY3WpoaComXvKloiV5wtx0Md2rn0B9n29Ys2WMNwWxj9A==",
"dev": true,
"license": "MIT"
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@ -50,7 +93,6 @@
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"dev": true,
"license": "MIT"
}
}

View file

@ -22,9 +22,9 @@
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"test": "bun test/app.ts",
"test:watch": "bun --watch test/app.ts",
"prefetch": "bun scripts/prefetch",
"native:build": "node-gyp build",
"native:rebuild": "node-gyp rebuild",
"vanilla:defs": "bun scripts/vanilladefs",
"gen:enums": "bun scripts/enum",
"gen:items": "bun scripts/items",
@ -32,9 +32,13 @@
},
"devDependencies": {
"@types/node": "^25.0.3",
"node-api-headers": "^1.7.0",
"typescript": "^5.9.3"
},
"dependencies": {
"jsonc-parser": "^3.3.1"
"@types/bindings": "^1.5.5",
"bindings": "^1.5.0",
"jsonc-parser": "^3.3.1",
"node-addon-api": "^8.5.0"
}
}

14
src/compilers/Bindings.ts Normal file
View file

@ -0,0 +1,14 @@
import bindings from "bindings"
import { Token } from "./bindings/types.js"
export const {
Lexer,
isBlankChar,
isWordChar,
isNumberChar,
}: {
Lexer: (input: string) => Token[]
isBlankChar: (char: string) => boolean
isWordChar: (char: string) => boolean
isNumberChar: (char: string) => boolean
} = bindings("asajs-compiler")

View file

@ -9,3 +9,7 @@ export function isWordChar(char: string) {
export function isNumberChar(char: string) {
return /\d/.test(char)
}
export function isCompileBinding(input: string) {
return input.startsWith("[") && input.endsWith("]")
}

View file

@ -1,8 +1,8 @@
import { Expression, GenBinding, Token, TokenKind, TSToken, TSTokenKind } from "./types.js"
import { BindingType } from "../../types/enums/BindingType.js"
import { BindingItem } from "../../types/properties/value.js"
import { FuntionMap } from "./Funtion.js"
import { Lexer } from "./Lexer.js"
import { Expression, GenBinding, Token, TokenKind, TSToken, TSTokenKind } from "./types.js"
export class Parser {
position: number = 0
@ -240,14 +240,14 @@ export class Parser {
this.eat()
return this.funtionCall(<string>callerToken.value, ...args)
} else if (left.kind === TokenKind.OPERATOR) {
} else if (left?.kind === TokenKind.OPERATOR) {
this.warn(
`Implicit string literal '${callerToken.value}'. Use quoted string ('${callerToken.value}') for clarity!`,
callerToken,
)
return <string>callerToken.value
} else {
this.expect(TokenKind.OPERATOR, "Unexpected token!")
this.expect(TokenKind.WORD, "Unexpected token!")
return <string>callerToken.value
}
}

View file

@ -1,8 +0,0 @@
import { IntelliSense, Namespace, Element, VanillaType } from "../types/vanilla/intellisense.js"
import { paths } from "../types/vanilla/paths.js"
import { UI } from "./UI.js"
export function Modify<T extends Namespace, K extends Element<T>>(namespace: T, name: K) {
// @ts-ignore -- TS cannot prove this, but runtime guarantees it
return new UI<VanillaType<T, K>>(undefined, name, namespace, paths[namespace][name])
}

View file

@ -1,8 +1,11 @@
import { isCompileBinding } from "../compilers/bindings/Checker.js"
import { Parser } from "../compilers/bindings/Parser.js"
import { FormatProperties } from "../compilers/FormatProperties.js"
import { Memory } from "../compilers/Memory.js"
import { BindingType } from "../types/enums/BindingType.js"
import { Renderer } from "../types/enums/Renderer.js"
import { Type } from "../types/enums/Type.js"
import { Properties } from "../types/properties/components.js"
import { BindingItem } from "../types/properties/value.js"
import { Class } from "./Class.js"
import { RandomString } from "./Utils.js"
@ -18,6 +21,7 @@ export class UI<T extends Type, K extends Renderer | null = null> extends Class
extendable: boolean
controls = new Map<string, [UI<Type, Renderer | null>, Properties<Type, Renderer | null>]>()
bindings: BindingItem[] = []
properties: Properties<T, K> = <any>{}
constructor(
@ -47,6 +51,23 @@ export class UI<T extends Type, K extends Renderer | null = null> extends Class
return this
}
addBindings(...bindings: BindingItem[]) {
for (const binding of bindings) {
if (binding.source_property_name) {
if (isCompileBinding(binding.source_property_name)) {
const { gen, out } = new Parser(binding.source_property_name.slice(1, -1)).out()
if (gen) this.bindings.push(...gen)
binding.source_property_name = out
}
binding.binding_type = BindingType.VIEW
if (!binding.target_property_name) throw new Error("Binding must have a target property name")
}
this.bindings.push(binding)
}
}
addChild<T extends Type, K extends Renderer | null>(child: UI<T, K>, properties?: Properties<T, K>, name?: string) {
if (this === <any>child) {
throw new Error("Cannot add a child to itself")
@ -69,6 +90,10 @@ export class UI<T extends Type, K extends Renderer | null = null> extends Class
obj.type = this.type
}
if (this.bindings.length) {
obj.bindings = this.bindings
}
if (this.controls.size) {
obj.controls = []
this.controls.forEach((e, key) => obj.controls.push({ [key + e[0]]: e[1] }))
@ -78,7 +103,13 @@ export class UI<T extends Type, K extends Renderer | null = null> extends Class
}
[util.inspect.custom]($: any, opts: any) {
const obj: any = FormatProperties(this.properties)
const obj: any = {
...FormatProperties(this.properties),
}
if (this.bindings.length) {
obj.bindings = this.bindings
}
if (this.controls.size) {
obj.controls = []
@ -90,3 +121,10 @@ export class UI<T extends Type, K extends Renderer | null = null> extends Class
}\x1b[0m> \x1b[92m"${this}\x1b[92m"\x1b[0m ${util.inspect(obj, opts)}\n`
}
}
export class ModifyUI<T extends Type> extends UI<T, null> {
constructor(namespace: string, name: string, path: string) {
if (!path) throw new Error("ModifyUI cannot have a path")
super(undefined, name, namespace, path)
}
}

View file

@ -1,6 +1,6 @@
import { Type } from "../types/enums/Type.js"
import { Array3, Binding } from "../types/properties/value.js"
import { UI } from "./UI.js"
import { ModifyUI, UI } from "./UI.js"
import { Renderer } from "../types/enums/Renderer.js"
import {
@ -27,6 +27,8 @@ import {
SliderBox,
} from "../types/properties/components.js"
import { ItemAuxID } from "../types/enums/Items.js"
import { Element, Namespace, VanillaType } from "../types/vanilla/intellisense.js"
import { paths } from "../types/vanilla/paths.js"
const CHARS = "0123456789abcdefghijklmnopqrstuvwxyz"
@ -86,6 +88,11 @@ export function GetItemByAuxID(auxID: number) {
}
// Quick Elements
export function Modify<T extends Namespace, K extends Element<T>>(namespace: T, name: K) {
// @ts-ignore -- TS cannot prove this, but runtime guarantees it
return new ModifyUI<VanillaType<T, K>>(name, namespace, paths[namespace][name])
}
export function Panel(properties?: Panel, name?: string, namespace?: string) {
return new UI(Type.PANEL, name, namespace).setProperties(properties || {})
}

View file

@ -3,10 +3,11 @@ import "./compilers/RunEnd.js"
export * from "./components/Animation.js"
export * from "./components/UI.js"
export * from "./components/Modify.js"
export * from "./components/Utils.js"
export * from "./types/enums/index.js"
export * as Properties from "./types/properties/index.js"
export * from "./compilers/Bindings.js"
export { ItemAuxID } from "./types/enums/Items.js"

131
src/native/main.cc Normal file
View file

@ -0,0 +1,131 @@
#include <napi.h>
#include <regex>
enum TokenKind {
VARIABLE,
NUMBER,
STRING,
TEMPLATE_STRING,
WORD,
OPEN_PARENTHESIS,
CLOSE_PARENTHESIS,
OPERATOR,
COMMA,
};
namespace Lexer {
bool isBlankChar(char c) {
std::regex pattern(R"(\s)");
return std::regex_match(std::string(1, c), pattern);
};
bool isWordChar(char c) {
std::regex pattern(R"(\w)");
return std::regex_match(std::string(1, c), pattern);
}
bool isNumberChar(char c) {
std::regex pattern(R"(\d)");
return std::regex_match(std::string(1, c), pattern);
}
Napi::Object makeToken(Napi::Env &env, const std::string &input, TokenKind kind, size_t start, size_t length) {
Napi::Object token = Napi::Object::New(env);
token.Set(Napi::String::New(env, "start"), Napi::Number::New(env, start));
token.Set(Napi::String::New(env, "length"), Napi::Number::New(env, length));
token.Set(Napi::String::New(env, "kind"), Napi::Number::New(env, kind));
token.Set(Napi::String::New(env, "value"), Napi::String::New(env, input.substr(start, length)));
return token;
}
Napi::Array Lexer(const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();
Napi::Function consoleLog = env.Global().Get("console").As<Napi::Object>().Get("log").As<Napi::Function>();
if (info.Length() != 1) {
Napi::TypeError::New(env, "One string argument required").ThrowAsJavaScriptException();
return Napi::Array::New(env, 0);
}
if (!info[0].IsString()) {
Napi::TypeError::New(env, "Input must be a string").ThrowAsJavaScriptException();
return Napi::Array::New(env, 0);
}
const std::string input = info[0].As<Napi::String>().Utf8Value();
const size_t length = input.size();
if (length == 0) {
return Napi::Array::New(env, 0);
}
std::vector<Napi::Object> tokens;
size_t index = 0;
while (index < length) {
char c = input[index];
switch (c) {
default: {
size_t start = index;
if (Lexer::isNumberChar(c)) {
while (Lexer::isNumberChar(input[index + 1])) index++;
tokens.push_back(
Lexer::makeToken(env, input, TokenKind::NUMBER, start, index - start + 1)
);
} else if (Lexer::isWordChar(c)) {
while (Lexer::isWordChar(input[index + 1])) index++;
tokens.push_back(
Lexer::makeToken(env, input, TokenKind::WORD, start, index - start + 1)
);
} else if (!Lexer::isBlankChar(c)) {
return Napi::Array::New(env, 0);
}
}
};
index++;
}
size_t tokenLength = tokens.size();
Napi::Array result = Napi::Array::New(env, tokenLength);
for (size_t i = 0; i < tokenLength; i++) {
result.Set(i, tokens[i]);
}
return result;
}
};
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set(
Napi::String::New(env, "Lexer"),
Napi::Function::New(env, Lexer::Lexer)
);
exports.Set(
Napi::String::New(env, "isBlankChar"),
Napi::Function::New(env, Lexer::isBlankChar)
);
exports.Set(
Napi::String::New(env, "isWordChar"),
Napi::Function::New(env, Lexer::isWordChar)
);
exports.Set(
Napi::String::New(env, "isNumberChar"),
Napi::Function::New(env, Lexer::isNumberChar)
);
return exports;
}
NODE_API_MODULE(NODE_GYP_MODULE_NAME, Init)

View file

@ -1,9 +1,3 @@
import { Anchor, Modify, Properties } from ".."
import { AsaJSCompiler } from ".."
const vanilla = Modify("authentication_modals", "ad_modal_dialog").setProperties({
ignored: true,
anchor: Anchor.CENTER,
offset: [10, 10],
})
console.log(vanilla)
console.log(AsaJSCompiler.Lexer("SAYGEX69"))