diff --git a/.gitmodules b/.gitmodules index b4efb25..bad25e9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "test_vault/.obsidian/plugins/hot-reload"] path = test_vault/.obsidian/plugins/hot-reload url = https://github.com/pjeby/hot-reload.git +[submodule "recipe-rs"] + path = recipe-rs + url = git@github.com:tmayoff/recipe-rs.git diff --git a/bun.lockb b/bun.lockb index fd0f5a1..0e41552 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/flake.nix b/flake.nix index aed00ce..4c9cb78 100644 --- a/flake.nix +++ b/flake.nix @@ -15,6 +15,27 @@ system: let overlays = [ (final: prev: { + wasm-pack = prev.rustPlatform.buildRustPackage rec { + pname = "wasm-pack"; + version = "0.13.0"; + + src = prev.fetchFromGitHub { + owner = "rustwasm"; + repo = "wasm-pack"; + rev = "refs/tags/v${version}"; + hash = "sha256-NEujk4ZPQ2xHWBCVjBCD7H6f58P4KrwCNoDHKa0d5JE="; + }; + + cargoHash = "sha256-pFKGQcWW1/GaIIWMyWBzts4w1hMu27hTG/uUMjkfDMo="; + nativeBuildInputs = with prev; [cmake]; + + buildInputs = prev.lib.optional prev.stdenv.isDarwin prev.darwin.apple_sdk.frameworks.Security; + + # Most tests rely on external resources and build artifacts. + # Disabling check here to work with build sandboxing. + doCheck = false; + }; + biome = prev.rustPlatform.buildRustPackage rec { pname = "biome"; version = "1.8.1"; @@ -82,6 +103,7 @@ biome act just + wasm-pack ]; }; } diff --git a/package.json b/package.json index 3c18da6..74e4fe2 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "scripts": { "dev": "vite build --mode development --watch", "build": "vite build --mode production", - "check": "svelte-check --tsconfig ./tsconfig.json" + "check": "svelte-check --tsconfig ./tsconfig.json", + "wasm": "wasm-pack build ./recipe-rs --target web" }, "keywords": [], "author": "", @@ -17,7 +18,7 @@ "@popperjs/core": "^2.11.8", "@sveltejs/vite-plugin-svelte": "^3.1.1", "@tsconfig/svelte": "^5.0.4", - "@types/node": "^20.14.9", + "@types/node": "^20.14.10", "@types/pluralize": "^0.0.33", "autoprefixer": "^10.4.19", "builtin-modules": "^4.0.0", @@ -30,12 +31,14 @@ "tailwindcss": "^3.4.4", "tslib": "^2.6.3", "typescript": "^5.5.3", - "vite": "^5.3.3" + "vite": "^5.3.3", + "vite-plugin-wasm-pack": "^0.1.12" }, "dependencies": { "@unocss/extractor-svelte": "^0.61.2", "moment": "^2.30.1", "pluralize": "^8.0.0", + "recipe-rs": "^0.1.5", "unocss": "^0.61.2" } } diff --git a/recipe-rs b/recipe-rs new file mode 160000 index 0000000..a774823 --- /dev/null +++ b/recipe-rs @@ -0,0 +1 @@ +Subproject commit a7748236e1ced503fd10e3c9a75ab3814a80a5dc diff --git a/src/main.ts b/src/main.ts index d9583ed..dd8e986 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,6 +6,8 @@ import { AddFileToShoppingList, AddMealPlanToShoppingList, ClearCheckedIngredien import SearchRecipe from './recipe/SearchRecipe.svelte'; import { MealSettings, RecipeFormat } from './settings'; import 'virtual:uno.css'; +import init from 'recipe-rs'; +import { DownloadRecipeCommand } from './recipe/downloader'; // biome-ignore lint/style/noDefaultExport: export default class MealPlugin extends Plugin { @@ -14,6 +16,11 @@ export default class MealPlugin extends Plugin { async onload() { await this.loadSettings(); + const wasmPath = this.app.vault.adapter.getResourcePath( + `${this.app.vault.configDir}/plugins/${this.manifest.id}/assets/recipe_rs_bg.wasm`, + ); + await init(wasmPath); + this.ctx.loadRecipes(undefined); this.registerEvent( @@ -62,6 +69,14 @@ export default class MealPlugin extends Plugin { }, }); + this.addCommand({ + id: 'download-url', + name: 'Download recipe from url', + callback: async () => { + await DownloadRecipeCommand(this.ctx); + }, + }); + this.registerEvent( this.app.workspace.on('file-menu', (e, t) => { if (t instanceof TFile && t.path.contains(get(this.ctx.settings).recipeDirectory)) { @@ -76,6 +91,8 @@ export default class MealPlugin extends Plugin { } }), ); + + console.info('obisidan-meals plugin loaded'); } async loadSettings() { diff --git a/src/recipe/downloader.ts b/src/recipe/downloader.ts new file mode 100644 index 0000000..ff87a83 --- /dev/null +++ b/src/recipe/downloader.ts @@ -0,0 +1,88 @@ +import { type App, Modal, SuggestModal, requestUrl } from 'obsidian'; +import { type Recipe, format, scrape } from 'recipe-rs'; +import { get } from 'svelte/store'; +import type { Context } from '../context'; +import { AppendMarkdownExt, NoteExists, OpenNotePath } from '../utils/filesystem'; + +class DownloadRecipeModal extends SuggestModal { + query = ''; + context: Context; + + constructor(ctx: Context) { + super(ctx.app); + this.context = ctx; + } + + getSuggestions(query: string): string[] | Promise { + this.query = query; + return ['download']; + } + + renderSuggestion(value: string, el: HTMLElement) { + el.createEl('div', { text: value }); + } + + async onChooseSuggestion(item: string, _evt: MouseEvent | KeyboardEvent) { + if (item === 'download') { + await DownloadRecipe(this.context, this.query); + } + } +} + +class ErrorDialog extends Modal { + message = 'unknown error'; + + constructor(app: App, message: string) { + super(app); + this.message = message; + } + + onOpen() { + this.contentEl.createEl('h4', { text: 'An error occured' }); + this.contentEl.createEl('p', { text: this.message }); + this.contentEl.createEl('a', { + href: 'https://github.com/tmayoff/recipe-rs/issues/new', + text: 'Please make an issue here so I can help resolve the issue', + }); + } + + onClose() { + this.contentEl.empty(); + } +} + +export function DownloadRecipeCommand(ctx: Context) { + new DownloadRecipeModal(ctx).open(); +} + +async function DownloadRecipe(ctx: Context, url: string) { + const dom = await requestUrl(url).text; + + let recipe: Recipe | null = null; + let formatted = ''; + try { + recipe = scrape(url, dom); + formatted = format(recipe); + } catch (exception) { + new ErrorDialog(ctx.app, `${exception}`).open(); + return; + } + + const newRecipeNotePath = AppendMarkdownExt(`${get(ctx.settings).recipeDirectory}/${recipe.name}`); + if (NoteExists(ctx.app, newRecipeNotePath)) { + new ErrorDialog(ctx.app, 'Recipe with that name already exists').open(); + await OpenNotePath(ctx.app, newRecipeNotePath); + return; + } + + let content = '---\n'; + content += `source: ${url}\n`; + content += '---\n'; + + content += '\n'; + content += formatted; + + await ctx.app.vault.create(newRecipeNotePath, content); + + await OpenNotePath(ctx.app, newRecipeNotePath); +} diff --git a/vite.config.ts b/vite.config.ts index 506efd0..024b6a4 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,8 @@ import { svelte, vitePreprocess } from '@sveltejs/vite-plugin-svelte'; import builtins from 'builtin-modules'; import UnoCSS from 'unocss/vite'; -import { PluginOption, defineConfig } from 'vite'; +import { type PluginOption, defineConfig } from 'vite'; +import wasmPack from 'vite-plugin-wasm-pack'; const setOutDir = (mode: string) => { switch (mode) { @@ -12,47 +13,50 @@ const setOutDir = (mode: string) => { } }; +// biome-ignore lint/style/noDefaultExport: export default defineConfig(({ mode }) => { - return { - plugins: [ - UnoCSS(), - svelte({ - preprocess: vitePreprocess(), - compilerOptions: { - customElement: true, - }, - }) as PluginOption, - ], - build: { - lib: { - entry: 'src/main', - formats: ['cjs'], - }, - rollupOptions: { - output: { - entryFileNames: 'main.js', - assetFileNames: 'styles.css', - }, - external: [ - 'obsidian', - 'electron', - '@codemirror/autocomplete', - '@codemirror/collab', - '@codemirror/commands', - '@codemirror/language', - '@codemirror/lint', - '@codemirror/search', - '@codemirror/state', - '@codemirror/view', - '@lezer/common', - '@lezer/highlight', - '@lezer/lr', - ...builtins, + return { + plugins: [ + wasmPack([], ['recipe-rs']), + UnoCSS(), + svelte({ + preprocess: vitePreprocess(), + compilerOptions: { + customElement: true, + }, + }) as PluginOption, ], - }, - outDir: setOutDir(mode), - emptyOutDir: false, - sourcemap: mode === 'production' ? false : 'inline', - }, - }; + build: { + ssrEmitAssets: true, + lib: { + entry: 'src/main', + formats: ['cjs'], + }, + rollupOptions: { + output: { + entryFileNames: 'main.js', + assetFileNames: 'styles.css', + }, + external: [ + 'obsidian', + 'electron', + '@codemirror/autocomplete', + '@codemirror/collab', + '@codemirror/commands', + '@codemirror/language', + '@codemirror/lint', + '@codemirror/search', + '@codemirror/state', + '@codemirror/view', + '@lezer/common', + '@lezer/highlight', + '@lezer/lr', + ...builtins, + ], + }, + outDir: setOutDir(mode), + emptyOutDir: false, + sourcemap: mode === 'production' ? false : 'inline', + }, + }; });