Skip to content

Commit 754a17a

Browse files
committed
base JS/TS class prepared for method hook ups
1 parent 3f0cd52 commit 754a17a

5 files changed

Lines changed: 269 additions & 4 deletions

File tree

typescript/README.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# cooklang (TypeScript wrapper)
2+
3+
Lightweight TypeScript wrapper through WASM for the Rust-based Cooklang parser.
4+
5+
This folder provides a thin JS/TS convenience layer around the WASM parser based on `cooklang-rs`. The primary exported class in this module is `CooklangParser` which can be used either as an instance (hold a recipe and operate on it) or as a functional utility (pass a recipe string to each method).
6+
7+
## Examples
8+
9+
### Instance Usage
10+
11+
This pattern holds a recipe on the parser instance in which all properties and methods then act upon.
12+
13+
```ts
14+
import { CooklangParser } from "@cooklang/parser";
15+
16+
const fancyRecipe = "Write your @recipe here!";
17+
18+
// create a parser instance with a raw recipe string
19+
const recipe = new CooklangParser(fancyRecipe);
20+
21+
// read basic fields populated by the wrapper
22+
console.log(recipe.metadata); // TODO sample response
23+
console.log(recipe.ingredients); // TODO sample response
24+
console.log(recipe.sections); // TODO sample response
25+
26+
// render methods return the original string in the minimal implementation
27+
console.log(recipe.renderPrettyString()); // TODO sample response
28+
console.log(recipe.renderHTML()); // TODO sample response
29+
```
30+
31+
### Functional Usage
32+
33+
This pattern passes a string directly and doesn't require keeping an instance around.
34+
35+
```ts
36+
import { CooklangParser } from "@cooklang/parser";
37+
38+
const parser = new CooklangParser();
39+
const recipeString = "Write your @recipe here!";
40+
41+
// functional helpers accept a recipe string and return rendered output
42+
console.log(parser.renderPrettyString(recipeString)); // TODO sample response
43+
console.log(parser.renderHTML(recipeString)); // TODO sample response
44+
45+
// `parse` returns a recipe class
46+
const parsed = parser.parse(recipeString);
47+
console.log(parsed.metadata); // TODO sample response
48+
console.log(parsed.ingredients); // TODO sample response
49+
console.log(parsed.sections); // TODO sample response
50+
```

typescript/index.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1 @@
1-
import { version, Parser } from "./pkg/cooklang_wasm";
2-
3-
export { version, Parser };
4-
export type { ScaledRecipeWithReport } from "./pkg/cooklang_wasm";
1+
export * from "./src/parser";

typescript/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ pub struct ScaledRecipeWithReport {
2727
report: String,
2828
}
2929

30+
// TODO see if we can pull this out of an impl
31+
// and use simple functions which may make our TS
32+
// easier to manage and check, move the class creation to JS
3033
#[wasm_bindgen]
3134
impl Parser {
3235
#[wasm_bindgen(constructor)]

typescript/src/parser.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import {
2+
version,
3+
Parser as RustParser,
4+
type ScaledRecipeWithReport,
5+
} from "../pkg/cooklang_wasm";
6+
7+
// for temporary backwards compatibility, let's export it with the old name
8+
const Parser = RustParser;
9+
export { version, Parser, type ScaledRecipeWithReport };
10+
11+
class CooklangRecipe {
12+
metadata = {};
13+
ingredients = new Map();
14+
// TODO should we use something other than array here?
15+
sections = [];
16+
cookware = new Map();
17+
timers = [];
18+
constructor(rawParsed?: ScaledRecipeWithReport) {
19+
if (rawParsed) {
20+
this.setRecipe(rawParsed);
21+
}
22+
}
23+
24+
setRecipe(rawParsed: ScaledRecipeWithReport) {
25+
this.metadata = {};
26+
// this.ingredients = [];
27+
// this.steps = [];
28+
// this.cookware = [];
29+
// this.timers = [];
30+
}
31+
}
32+
33+
class CooklangParser extends CooklangRecipe {
34+
public version: string;
35+
public extensionList: string[];
36+
constructor(public rawContent?: string) {
37+
super();
38+
this.version = version();
39+
this.extensionList = [] as string[];
40+
}
41+
42+
set raw(raw: string) {
43+
this.rawContent = raw;
44+
}
45+
46+
get raw() {
47+
if (!this.rawContent)
48+
throw new Error("recipe not set, call .raw(content) to set it first");
49+
return this.rawContent;
50+
}
51+
52+
#handleFunctionalOrInstance(instanceInput: string | undefined) {
53+
if (this.rawContent) {
54+
if (instanceInput)
55+
throw new Error("recipe already set, create a new instance");
56+
return this.rawContent;
57+
}
58+
if (!instanceInput) {
59+
throw new Error("pass a recipe as a string or generate a new instance");
60+
}
61+
return instanceInput;
62+
}
63+
64+
// TODO create issue to fill this in
65+
set extensions(extensions: string[]) {
66+
this.extensionList = extensions;
67+
}
68+
69+
get extensions() {
70+
if (!this.extensionList) throw new Error("TODO");
71+
return this.extensionList;
72+
}
73+
74+
// TODO create issue for this
75+
renderPrettyString(recipeString?: string) {
76+
const input = this.#handleFunctionalOrInstance(recipeString);
77+
// TODO renderPrettyString this then return
78+
return input;
79+
}
80+
81+
renderHTML(recipeString?: string) {
82+
const input = this.#handleFunctionalOrInstance(recipeString);
83+
// TODO renderHTML this then return
84+
return input;
85+
}
86+
87+
parseRaw(recipeString?: string) {
88+
const input = this.#handleFunctionalOrInstance(recipeString);
89+
// TODO parseRaw this then return
90+
return input;
91+
}
92+
93+
// TODO return fully typed JS Object
94+
parse(recipeString?: string) {
95+
const input = this.#handleFunctionalOrInstance(recipeString);
96+
// TODO actually parse
97+
const parsed = {
98+
recipe: { ingredients: [input] },
99+
} as unknown as ScaledRecipeWithReport;
100+
if (this.rawContent) {
101+
this.setRecipe(parsed);
102+
}
103+
if (!this.rawContent && recipeString) {
104+
const direct = new CooklangRecipe(parsed);
105+
return direct;
106+
} else {
107+
throw new Error("should never reach this");
108+
}
109+
}
110+
111+
debug(recipeString?: string): {
112+
version: string;
113+
ast: string;
114+
events: string;
115+
} {
116+
const input = this.#handleFunctionalOrInstance(recipeString);
117+
// TODO debug parse this then return
118+
return { version: this.version, ast: input, events: input };
119+
}
120+
}
121+
122+
export { CooklangParser };

typescript/test/parser.test.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { beforeAll, describe, expect, it } from "vitest";
2+
import { CooklangParser } from "../src/parser";
3+
4+
it("returns version number", () => {
5+
const parser = new CooklangParser();
6+
expect(parser.version).toBeDefined();
7+
});
8+
9+
describe("create and use instance", () => {
10+
let recipe: CooklangParser;
11+
beforeAll(() => {
12+
const recipeString = "hello";
13+
recipe = new CooklangParser(recipeString);
14+
});
15+
16+
it("returns pretty stringified recipe", () => {
17+
expect(recipe.renderPrettyString()).toEqual("hello");
18+
});
19+
20+
it("returns basic html recipe", () => {
21+
expect(recipe.renderPrettyString()).toEqual("hello");
22+
});
23+
24+
it("returns metadata list", () => {
25+
expect(recipe.metadata).toEqual({});
26+
});
27+
28+
it("returns ingredients list", () => {
29+
expect(recipe.ingredients).toEqual(new Map());
30+
});
31+
32+
it("returns sections list", () => {
33+
expect(recipe.sections).toEqual([]);
34+
});
35+
36+
it("returns cookware list", () => {
37+
expect(recipe.cookware).toEqual(new Map());
38+
});
39+
40+
it("returns timers list", () => {
41+
expect(recipe.timers).toEqual([]);
42+
});
43+
});
44+
45+
describe("functional", () => {
46+
const parser = new CooklangParser();
47+
const recipe = "hello";
48+
it("returns pretty stringified recipe", () => {
49+
const parsedRecipe = parser.renderPrettyString(recipe);
50+
expect(parsedRecipe).toEqual("hello");
51+
});
52+
53+
it("returns basic html recipe", () => {
54+
const parsedRecipe = parser.renderHTML(recipe);
55+
expect(parsedRecipe).toEqual("hello");
56+
});
57+
58+
it("returns full parse of recipe string", () => {
59+
const parsedRecipe = parser.parse(recipe);
60+
expect(parsedRecipe).toEqual({
61+
cookware: new Map(),
62+
ingredients: new Map(),
63+
metadata: {},
64+
sections: [],
65+
timers: [],
66+
});
67+
});
68+
69+
it("returns metadata list", () => {
70+
const parsedRecipe = parser.parse(recipe);
71+
expect(parsedRecipe.metadata).toEqual({});
72+
});
73+
74+
it("returns ingredients list", () => {
75+
const parsedRecipe = parser.parse(recipe);
76+
expect(parsedRecipe.ingredients).toEqual(new Map());
77+
});
78+
79+
it("returns sections list", () => {
80+
const parsedRecipe = parser.parse(recipe);
81+
expect(parsedRecipe.sections).toEqual([]);
82+
});
83+
84+
it("returns cookware list", () => {
85+
const parsedRecipe = parser.parse(recipe);
86+
expect(parsedRecipe.cookware).toEqual(new Map());
87+
});
88+
89+
it("returns timers list", () => {
90+
const parsedRecipe = parser.parse(recipe);
91+
expect(parsedRecipe.timers).toEqual([]);
92+
});
93+
});

0 commit comments

Comments
 (0)