Skip to content

Commit 7672246

Browse files
authored
Merge pull request #8 from Rhea-OS/documentation
Documentation
2 parents 0cdafe0 + 3a1b308 commit 7672246

File tree

8 files changed

+257
-86
lines changed

8 files changed

+257
-86
lines changed

expression-js/build.js

Lines changed: 42 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,56 +3,48 @@ import cp from "node:child_process";
33
import {esbuild} from 'builder';
44

55
export default {
6-
async "build:main.wasm"(config) {
7-
const out = config.out.join("mod");
8-
const wasmPack = cp.spawn('wasm-pack', [
9-
'build',
10-
'--out-dir',
11-
out.path,
12-
], {
13-
stdio: "inherit"
14-
});
15-
await new Promise((ok, err) => wasmPack.once("exit", code => code === 0 ? ok() : err(code)));
16-
},
17-
async "build:main.js"(config) {
18-
await esbuild.build({
19-
entryPoints: ["lib.ts"],
20-
bundle: true,
21-
sourcemap: true,
22-
format: 'esm',
23-
loader: {
24-
".wasm": "binary"
25-
},
26-
outdir: config.out.join("pkg").path
27-
});
28-
},
29-
async "build:lib.d.ts"(config) {
30-
await fs.copyFile(config.out.join("mod/expression_js.d.ts").path, config.out.join("pkg/lib.d.ts").path);
31-
},
32-
async "build:package.json"(config) {
33-
await fs.writeFile(config.out.join("pkg/package.json").path, JSON.stringify({
34-
name: "expression",
35-
type: "module",
36-
main: "./lib.js",
37-
typings: "./lib.d.ts"
38-
}, null, 4));
39-
},
40-
async "test:lib.js"(config) {
41-
const file = config.out.join("lib.js");
42-
/**
43-
* @type {typeof import('./build/mod/expression_js.d.ts')}
44-
*/
45-
const mod = await import(file.path).then(mod => mod.default());
6+
async "build:main.wasm"(config) {
7+
const out = config.out.join("mod");
8+
const wasmPack = cp.spawn('wasm-pack', [
9+
'build',
10+
'--out-dir',
11+
out.path,
12+
], {
13+
stdio: "inherit"
14+
});
15+
await new Promise((ok, err) => wasmPack.once("exit", code => code === 0 ? ok() : err(code)));
16+
},
17+
async "build:main.js"(config) {
18+
await esbuild.build({
19+
entryPoints: ["lib.ts"],
20+
bundle: true,
21+
sourcemap: true,
22+
format: 'esm',
23+
loader: {
24+
".wasm": "binary"
25+
},
26+
outdir: config.out.join("pkg").path
27+
});
28+
},
29+
async "build:lib.d.ts"(config) {
30+
const lib = await fs.readFile('./lib.d.ts', {encoding: 'utf8'});
31+
const mod = await fs.readFile(config.out.join("mod/expression_js.d.ts").path, {encoding: 'utf8'});
4632

47-
const cx = new mod.Context(new mod.DataSource({
48-
listRows: () => [],
49-
listColumns: () => [],
50-
countRows: () => 0,
51-
getRow: row => void 0
52-
}));
33+
await fs.writeFile(config.out.join("pkg/expr.d.ts").path, mod);
5334

54-
cx.pushGlobal("x", "Hello")
55-
56-
console.log(cx.evaluate(`x+x`));
57-
}
35+
await fs.writeFile(config.out.join("pkg/lib.d.ts").path, lib
36+
.replaceAll(lib.match(/( {2,})/)[0], '\t')
37+
.replaceAll(/\n{2,}/g, '\n'));
38+
},
39+
async "build:package.json"(config) {
40+
await fs.writeFile(config.out.join("pkg/package.json").path, JSON.stringify({
41+
name: "expression",
42+
type: "module",
43+
main: "./lib.js",
44+
typings: "./lib.d.ts"
45+
}, null, 4));
46+
},
47+
async "build:readme.md"(config) {
48+
await fs.writeFile(config.out.join('pkg/README.md'), await fs.readFile('./dist.md', {encoding: 'utf8'}));
49+
}
5850
}

expression-js/dist.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# expressions
2+
3+
Parser for the expression language used in the spreadsheet plugin. This document outlines the JavaScript portion of the
4+
library. For Rust see [the Rust crate](../README.md).
5+
6+
## Quickstart
7+
8+
In order to evaluate expressions, three things must happen;
9+
10+
1. A context object containing a [Data Provider](#Data Provider) must be registered
11+
2. The list of all functions, variables and operators needs to be defined
12+
3. The expression needs to be passed into the parser.
13+
14+
## Data Provider
15+
16+
A data provider is an object which converts addresses into values. Addresses are arbitrary text tokens wrapped in
17+
braces. The provider determines their meaning. [See the `repl.ts`](examples/repl.ts) for a feature-complete REPL.
18+
19+
```typescript
20+
import * as expr from 'expression';
21+
22+
class Table implements expr.DataSource {
23+
query(cx:any, query:string) {
24+
// Parse the query however you need to.
25+
// For example, the format `column:row`
26+
27+
return "Hey!";
28+
}
29+
}
30+
```
31+
32+
## Context
33+
34+
Next you'll need a context object which holds state and variables and acts as an API to the expression engine. You can
35+
define your own functions, globals and operators here.
36+
37+
```typescript
38+
import * as assert from 'node:assert';
39+
import * as expr from 'expression';
40+
41+
const cx = new expr.Context(new Table())
42+
.withGlobal("twelve", 12)
43+
.withGlobal("print", args => console.log(args));
44+
45+
assert.eq(cx.evaluateStr("twelve") == 12);
46+
```

expression-js/examples/repl.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import * as rl from 'node:readline/promises';
2+
import * as expr from 'expression';
3+
4+
class Table implements expr.DataSource {
5+
public readonly columns: string[] = [];
6+
private data: any[] = [];
7+
8+
constructor(columns: string[] = []) {
9+
this.columns = columns;
10+
}
11+
12+
query(cx: any, query: string) {
13+
const [, col, row] = query.match(/^(.+):(\d+)$/) ?? [];
14+
return this.data[Number(row) * this.columns.length + this.columns.indexOf(col)] ?? null;
15+
}
16+
17+
set(address: string, value: any) {
18+
const [, col, row] = address.match(/^(.+):(\d+)$/) ?? [];
19+
this.data[Number(row) * this.columns.length + this.columns.indexOf(col)] = value;
20+
}
21+
}
22+
23+
const cx = new expr.Context(new Table(["a", "b", "c"]));
24+
const prompt = rl.createInterface(process.stdin, process.stderr);
25+
26+
const commands = {
27+
async set(address: string) {
28+
const body = await prompt.question('--> ');
29+
cx.provider().set(address, cx.evaluateStr(body, {}));
30+
},
31+
32+
async func(args: string) {
33+
const [name, ...params] = args.split(/\s+/);
34+
const body = await prompt.question('--> ');
35+
cx.pushGlobal(name, function(args: any[]) {
36+
const inner = cx.clone();
37+
38+
for (const [a, arg] of params.entries())
39+
inner.pushGlobal(arg, args[a]);
40+
41+
return inner.evaluateStr(body, {});
42+
});
43+
},
44+
45+
async glob(name) {
46+
const body = await prompt.question('--> ');
47+
cx.pushGlobal(name, cx.evaluateStr(body, {}));
48+
}
49+
} satisfies Record<string, (arg: string) => Promise<void>>;
50+
51+
while (true) {
52+
const cmd = await prompt.question('> ').then(res => res.trim());
53+
54+
if (cmd === '')
55+
continue;
56+
else if (cmd === '/exit')
57+
break;
58+
else if (cmd === '/dump')
59+
console.log(cx.provider());
60+
else if (cmd.startsWith('/set '))
61+
await commands.set(cmd.slice(5).trim());
62+
else if (cmd.startsWith('/func '))
63+
await commands.func(cmd.slice(6).trim());
64+
else if (cmd.startsWith('/glob '))
65+
await commands.glob(cmd.slice(6).trim());
66+
else
67+
console.log(cx.evaluateStr(cmd, {}));
68+
}

expression-js/lib.d.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
declare module 'expression' {
2+
enum TokenType {
3+
Name = 0,
4+
Operator = 1,
5+
LParen = 2,
6+
LBracket = 3,
7+
LBrace = 4,
8+
RParen = 5,
9+
RBracket = 6,
10+
RBrace = 7,
11+
Dot = 8,
12+
Comma = 9,
13+
Nothing = 10,
14+
Num = 11,
15+
String = 12,
16+
Bool = 13,
17+
Address = 14,
18+
}
19+
20+
class Token {
21+
token(): string;
22+
type: TokenType
23+
}
24+
25+
class Operator {
26+
constructor(token: string, handler: (...operands: any[]) => any);
27+
}
28+
29+
class Context<Provider extends DataSource> {
30+
constructor(provider: Provider);
31+
provider(): Provider;
32+
clone(): Context<Provider>;
33+
34+
pushGlobal(name: string, value: any): void;
35+
withGlobal(name: string, value: any): Context<Provider>;
36+
37+
pushOperator(name: string, operator: Operator): void;
38+
withOperator(name: string, operator: Operator): Context<Provider>;
39+
40+
evaluateStr(expr: string, cx: any): any;
41+
parseStr(expr: string): Token[];
42+
}
43+
44+
interface DataSource {
45+
query(cx: any, query: string): any;
46+
}
47+
}

expression-js/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
"builder": "link:../../../builder",
1212
"chalk": "latest",
1313
"typescript": "latest",
14-
"esbuild-plugin-d.ts": "latest"
14+
"esbuild-plugin-d.ts": "latest",
15+
"expression": "link:./build/pkg"
1516
},
1617
"imports": {
1718
"#mod/main.wasm": "./build/mod/expression_js_bg.wasm",
@@ -20,6 +21,10 @@
2021
"deploy": {
2122
"main": "run.js",
2223
"imports": {},
24+
"exports": {
25+
"default": "./lib.js",
26+
"types": "./lib.d.ts"
27+
},
2328
"scripts": null
2429
}
2530
}

expression-js/src/context.rs

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ pub(crate) fn js_value_to_object(value: JsValue) -> Option<Object> {
4343

4444
value if value.is_function() => {
4545
let value = value.clone();
46-
Object::Function(Rc::new(Box::new(move |args| {
46+
Object::function(move |args| {
4747
let args = js_sys::Array::from_iter(args.into_iter()
4848
.map(value_to_js_object)
4949
.map(|i| i.ok_or(Into::<Error>::into(ManualError::ConversionFailed)))
@@ -53,7 +53,7 @@ pub(crate) fn js_value_to_object(value: JsValue) -> Option<Object> {
5353
.unwrap_throw();
5454
js_value_to_object(result)
5555
.ok_or(ManualError::ConversionFailed.into())
56-
})))
56+
})
5757
},
5858

5959
// TODO: Detect Addresses
@@ -117,15 +117,15 @@ pub(crate) fn value_to_js_object(value: Object) -> Option<JsValue> {
117117
#[wasm_bindgen(js_class=Context)]
118118
impl Context {
119119
#[wasm_bindgen(constructor)]
120-
pub fn new(config: DataSource) -> Context {
121-
let cx = config.cx.clone();
120+
pub fn new(config: js_sys::Object) -> Context {
121+
let cx = DataSource::from_js(config);
122122

123123
Self {
124-
global_context: cx.clone(),
125-
cx: expression::Context::new(config)
126-
.with_global("cx", Object::function(move |_| {
127-
Ok(cx.borrow().clone())
128-
})),
124+
global_context: cx.cx.clone(),
125+
cx: expression::Context::new(cx)
126+
.with_fn("cx", move |cx, _| {
127+
Ok(cx.provider().cx.borrow().clone())
128+
})
129129
}
130130
}
131131

@@ -211,6 +211,8 @@ impl Context {
211211
},
212212
Value::Expression(parse::expression::Expression { operator, .. }) => Some(vec![Token::new(operator.clone(), TokenType::Operator)]),
213213
Value::Literal(lit) => Some(vec![match lit {
214+
Literal::Nothing => Token::new("nothing".to_owned(), TokenType::Nothing),
215+
Literal::Bool(bool) => Token::new(format!("{}", bool), TokenType::Bool),
214216
Literal::Name(name) => Token::new(name.clone(), TokenType::Name),
215217
Literal::String(str) => Token::new(str.clone(), TokenType::String),
216218
Literal::Number(num) => Token::new(format!("{}", num), TokenType::Num),
@@ -238,6 +240,19 @@ impl Context {
238240
Err(err) => wasm_bindgen::throw_str(&format!("{:#?}", err))
239241
}).unwrap_or(vec![])
240242
}
243+
244+
#[wasm_bindgen(js_name="clone")]
245+
pub fn clone(&self) -> Self {
246+
Self {
247+
cx: self.cx.clone(),
248+
global_context: self.global_context.clone(),
249+
}
250+
}
251+
252+
#[wasm_bindgen(js_name="provider")]
253+
pub fn provider(&self) -> JsValue {
254+
self.cx.provider().inner.clone().into()
255+
}
241256
}
242257

243258
#[wasm_bindgen]
@@ -259,8 +274,6 @@ pub struct Token {
259274
#[wasm_bindgen(js_name="type")]
260275
pub token_type: TokenType,
261276
token: String,
262-
// #[wasm_bindgen(js_name="tokenOffset")]
263-
// pub token_start: usize,
264277
}
265278

266279
#[wasm_bindgen]
@@ -293,6 +306,7 @@ pub enum TokenType {
293306
RBrace,
294307
Dot,
295308
Comma,
309+
Nothing,
296310
Num,
297311
String,
298312
Bool,

0 commit comments

Comments
 (0)