-
Notifications
You must be signed in to change notification settings - Fork 25
base JS/TS class prepared for method hook ups #58
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
754a17a
b071f3f
ff45a89
9a4c99e
6a32ee4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| # cooklang (TypeScript wrapper) | ||
|
|
||
| Lightweight TypeScript wrapper through WASM for the Rust-based Cooklang parser. | ||
|
|
||
| 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). | ||
|
|
||
| ## Examples | ||
|
|
||
| ### Instance Usage | ||
|
|
||
| This pattern holds a recipe on the parser instance in which all properties and methods then act upon. | ||
|
|
||
| ```ts | ||
| import { CooklangParser } from "@cooklang/parser"; | ||
|
|
||
| const fancyRecipe = "Write your @recipe here!"; | ||
|
|
||
| // create a parser instance with a raw recipe string | ||
| const recipe = new CooklangParser(fancyRecipe); | ||
|
|
||
| // read basic fields populated by the wrapper | ||
| console.log(recipe.metadata); // TODO sample response | ||
| console.log(recipe.ingredients); // TODO sample response | ||
| console.log(recipe.sections); // TODO sample response | ||
|
|
||
| // render methods return the original string in the minimal implementation | ||
| console.log(recipe.renderPrettyString()); // TODO sample response | ||
| console.log(recipe.renderHTML()); // TODO sample response | ||
| ``` | ||
|
|
||
| ### Functional Usage | ||
|
|
||
| This pattern passes a string directly and doesn't require keeping an instance around. | ||
|
|
||
| ```ts | ||
| import { CooklangParser } from "@cooklang/parser"; | ||
|
|
||
| const parser = new CooklangParser(); | ||
| const recipeString = "Write your @recipe here!"; | ||
|
|
||
| // functional helpers accept a recipe string and return rendered output | ||
| console.log(parser.renderPrettyString(recipeString)); // TODO sample response | ||
| console.log(parser.renderHTML(recipeString)); // TODO sample response | ||
|
|
||
| // `parse` returns a recipe class | ||
| const parsed = parser.parse(recipeString); | ||
| console.log(parsed.metadata); // TODO sample response | ||
| console.log(parsed.ingredients); // TODO sample response | ||
| console.log(parsed.sections); // TODO sample response | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1 @@ | ||
| import { version, Parser } from "./pkg/cooklang_wasm"; | ||
|
|
||
| export { version, Parser }; | ||
| export type { ScaledRecipeWithReport } from "./pkg/cooklang_wasm"; | ||
| export * from "./src/parser"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| import { | ||
| version, | ||
| Parser as RustParser, | ||
| type ScaledRecipeWithReport, | ||
| } from "../pkg/cooklang_wasm"; | ||
|
|
||
| // for temporary backwards compatibility, let's export it with the old name | ||
| const Parser = RustParser; | ||
| export { version, Parser, type ScaledRecipeWithReport }; | ||
|
|
||
| class CooklangRecipe { | ||
| metadata = {}; | ||
| ingredients = new Map(); | ||
| // TODO should we use something other than array here? | ||
| sections = []; | ||
| cookware = new Map(); | ||
| timers = []; | ||
| constructor(rawParsed?: ScaledRecipeWithReport) { | ||
|
||
| if (rawParsed) { | ||
| this.setRecipe(rawParsed); | ||
| } | ||
| } | ||
|
|
||
| setRecipe(rawParsed: ScaledRecipeWithReport) { | ||
| this.metadata = {}; | ||
| // this.ingredients = []; | ||
| // this.steps = []; | ||
| // this.cookware = []; | ||
| // this.timers = []; | ||
| } | ||
| } | ||
|
|
||
| class CooklangParser extends CooklangRecipe { | ||
|
||
| public version: string; | ||
| public extensionList: string[]; | ||
| constructor(public rawContent?: string) { | ||
| super(); | ||
| this.version = version(); | ||
| this.extensionList = [] as string[]; | ||
| } | ||
|
|
||
| set raw(raw: string) { | ||
| this.rawContent = raw; | ||
| } | ||
|
|
||
| get raw() { | ||
| if (!this.rawContent) | ||
| throw new Error("recipe not set, call .raw(content) to set it first"); | ||
| return this.rawContent; | ||
| } | ||
|
|
||
| #handleFunctionalOrInstance(instanceInput: string | undefined) { | ||
|
||
| if (this.rawContent) { | ||
| if (instanceInput) | ||
| throw new Error("recipe already set, create a new instance"); | ||
| return this.rawContent; | ||
| } | ||
| if (!instanceInput) { | ||
| throw new Error("pass a recipe as a string or generate a new instance"); | ||
| } | ||
| return instanceInput; | ||
| } | ||
|
|
||
| // TODO create issue to fill this in | ||
| set extensions(extensions: string[]) { | ||
| this.extensionList = extensions; | ||
| } | ||
|
|
||
| get extensions() { | ||
| if (!this.extensionList) throw new Error("TODO"); | ||
| return this.extensionList; | ||
| } | ||
|
|
||
| // TODO create issue for this | ||
| renderPrettyString(recipeString?: string) { | ||
|
||
| const input = this.#handleFunctionalOrInstance(recipeString); | ||
| // TODO renderPrettyString this then return | ||
| return input; | ||
| } | ||
|
|
||
| renderHTML(recipeString?: string) { | ||
| const input = this.#handleFunctionalOrInstance(recipeString); | ||
| // TODO renderHTML this then return | ||
| return input; | ||
| } | ||
|
|
||
| parseRaw(recipeString?: string) { | ||
| const input = this.#handleFunctionalOrInstance(recipeString); | ||
| // TODO parseRaw this then return | ||
| return input; | ||
| } | ||
|
|
||
| // TODO return fully typed JS Object | ||
| parse(recipeString?: string) { | ||
| const input = this.#handleFunctionalOrInstance(recipeString); | ||
| // TODO actually parse | ||
| const parsed = { | ||
| recipe: { ingredients: [input] }, | ||
| } as unknown as ScaledRecipeWithReport; | ||
| if (this.rawContent) { | ||
| this.setRecipe(parsed); | ||
| } | ||
| if (!this.rawContent && recipeString) { | ||
|
||
| const direct = new CooklangRecipe(parsed); | ||
| return direct; | ||
| } else { | ||
| throw new Error("should never reach this"); | ||
| } | ||
| } | ||
|
|
||
| debug(recipeString?: string): { | ||
| version: string; | ||
| ast: string; | ||
| events: string; | ||
| } { | ||
| const input = this.#handleFunctionalOrInstance(recipeString); | ||
| // TODO debug parse this then return | ||
| return { version: this.version, ast: input, events: input }; | ||
| } | ||
| } | ||
|
|
||
| export { CooklangParser }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| import { beforeAll, describe, expect, it } from "vitest"; | ||
| import { CooklangParser } from "../src/parser"; | ||
|
|
||
| it("returns version number", () => { | ||
| const parser = new CooklangParser(); | ||
| expect(parser.version).toBeDefined(); | ||
| }); | ||
|
|
||
| describe("create and use instance", () => { | ||
| let recipe: CooklangParser; | ||
| beforeAll(() => { | ||
| const recipeString = "hello"; | ||
| recipe = new CooklangParser(recipeString); | ||
| }); | ||
|
|
||
| it("returns pretty stringified recipe", () => { | ||
| expect(recipe.renderPrettyString()).toEqual("hello"); | ||
| }); | ||
|
|
||
| it("returns basic html recipe", () => { | ||
| expect(recipe.renderPrettyString()).toEqual("hello"); | ||
| }); | ||
|
|
||
| it("returns metadata list", () => { | ||
| expect(recipe.metadata).toEqual({}); | ||
| }); | ||
|
|
||
| it("returns ingredients list", () => { | ||
| expect(recipe.ingredients).toEqual(new Map()); | ||
| }); | ||
|
|
||
| it("returns sections list", () => { | ||
| expect(recipe.sections).toEqual([]); | ||
| }); | ||
|
|
||
| it("returns cookware list", () => { | ||
| expect(recipe.cookware).toEqual(new Map()); | ||
| }); | ||
|
|
||
| it("returns timers list", () => { | ||
| expect(recipe.timers).toEqual([]); | ||
| }); | ||
| }); | ||
|
|
||
| describe("functional", () => { | ||
| const parser = new CooklangParser(); | ||
| const recipe = "hello"; | ||
| it("returns pretty stringified recipe", () => { | ||
| const parsedRecipe = parser.renderPrettyString(recipe); | ||
| expect(parsedRecipe).toEqual("hello"); | ||
| }); | ||
|
|
||
| it("returns basic html recipe", () => { | ||
| const parsedRecipe = parser.renderHTML(recipe); | ||
| expect(parsedRecipe).toEqual("hello"); | ||
| }); | ||
|
|
||
| it("returns full parse of recipe string", () => { | ||
| const parsedRecipe = parser.parse(recipe); | ||
| expect(parsedRecipe).toEqual({ | ||
| cookware: new Map(), | ||
| ingredients: new Map(), | ||
| metadata: {}, | ||
| sections: [], | ||
| timers: [], | ||
| }); | ||
| }); | ||
|
|
||
| it("returns metadata list", () => { | ||
| const parsedRecipe = parser.parse(recipe); | ||
| expect(parsedRecipe.metadata).toEqual({}); | ||
| }); | ||
|
|
||
| it("returns ingredients list", () => { | ||
| const parsedRecipe = parser.parse(recipe); | ||
| expect(parsedRecipe.ingredients).toEqual(new Map()); | ||
| }); | ||
|
|
||
| it("returns sections list", () => { | ||
| const parsedRecipe = parser.parse(recipe); | ||
| expect(parsedRecipe.sections).toEqual([]); | ||
| }); | ||
|
|
||
| it("returns cookware list", () => { | ||
| const parsedRecipe = parser.parse(recipe); | ||
| expect(parsedRecipe.cookware).toEqual(new Map()); | ||
| }); | ||
|
|
||
| it("returns timers list", () => { | ||
| const parsedRecipe = parser.parse(recipe); | ||
| expect(parsedRecipe.timers).toEqual([]); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm sure you have a great reason for this, but I was a bit confused why we wouldn't use the
interface Recipethat is exposed by the wasm build. It neatly provides types for each of the members as well.On one hand, we do expose the TS package to a wide array of changes on the Rust package. On the other hand, we might have a lot of manual work to keep the packages in-sync with each other.
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't purely for types. My expectation which in retrospect didn't come across very obvious is that we use the types generated from the WASM build in this class. Some we may be able to use directly, like ingredients. However, the sections is rather verbose. It felt reasonable to provide a bit of sugar on top to improve the ability to render it.
I split this off as it's own class so we could access each of these like a property on an object, e.g.
recipe.ingredients.