From 135ae566a7d4645adb5279dc252bf6c0cbeafccf Mon Sep 17 00:00:00 2001 From: Nezznee Date: Sat, 26 Sep 2026 14:03:42 +0200 Subject: [PATCH 1/8] MVP for the Joplin plugin --- integrations/joplin/.gitignore | 3 + integrations/joplin/.npmignore | 8 + integrations/joplin/GENERATOR_DOC.md | 78 ++ integrations/joplin/README.md | 8 +- integrations/joplin/api/Global.d.ts | 19 + integrations/joplin/api/Joplin.d.ts | 93 ++ integrations/joplin/api/JoplinAi.d.ts | 80 ++ integrations/joplin/api/JoplinClipboard.d.ts | 61 + integrations/joplin/api/JoplinCommands.d.ts | 102 ++ .../joplin/api/JoplinContentScripts.d.ts | 41 + integrations/joplin/api/JoplinData.d.ts | 74 ++ integrations/joplin/api/JoplinFilters.d.ts | 11 + integrations/joplin/api/JoplinFs.d.ts | 22 + integrations/joplin/api/JoplinImaging.d.ts | 96 ++ integrations/joplin/api/JoplinInterop.d.ts | 20 + integrations/joplin/api/JoplinPlugins.d.ts | 43 + integrations/joplin/api/JoplinSettings.d.ts | 74 ++ integrations/joplin/api/JoplinViews.d.ts | 45 + .../joplin/api/JoplinViewsDialogs.d.ts | 84 ++ .../joplin/api/JoplinViewsEditor.d.ts | 118 ++ .../joplin/api/JoplinViewsMenuItems.d.ts | 19 + integrations/joplin/api/JoplinViewsMenus.d.ts | 21 + .../joplin/api/JoplinViewsNoteList.d.ts | 42 + .../joplin/api/JoplinViewsPanels.d.ts | 89 ++ .../joplin/api/JoplinViewsToolbarButtons.d.ts | 17 + integrations/joplin/api/JoplinWindow.d.ts | 30 + integrations/joplin/api/JoplinWorkspace.d.ts | 107 ++ integrations/joplin/api/index.ts | 5 + integrations/joplin/api/noteListType.d.ts | 254 ++++ integrations/joplin/api/noteListType.ts | 327 +++++ integrations/joplin/api/types.ts | 1049 +++++++++++++++++ integrations/joplin/package.json | 31 + integrations/joplin/plugin.config.json | 6 + integrations/joplin/src/butterfly.css | 18 + integrations/joplin/src/butterflyDocument.ts | 9 + .../src/contentScripts/markdownItPlugin.js | 14 + .../joplin/src/contentScripts/webview.css | 7 + .../joplin/src/contentScripts/webview.ts | 40 + integrations/joplin/src/img/icon.svg | 79 ++ integrations/joplin/src/index.ts | 206 ++++ integrations/joplin/src/manifest.json | 16 + integrations/joplin/src/views/panel.ts | 112 ++ integrations/joplin/tsconfig.json | 11 + integrations/joplin/webpack.config.js | 421 +++++++ packages/shared/src/embed.ts | 117 +- pnpm-lock.yaml | 891 +++++++++++++- pnpm-workspace.yaml | 1 + 47 files changed, 4930 insertions(+), 89 deletions(-) create mode 100644 integrations/joplin/.gitignore create mode 100644 integrations/joplin/.npmignore create mode 100644 integrations/joplin/GENERATOR_DOC.md create mode 100644 integrations/joplin/api/Global.d.ts create mode 100644 integrations/joplin/api/Joplin.d.ts create mode 100644 integrations/joplin/api/JoplinAi.d.ts create mode 100644 integrations/joplin/api/JoplinClipboard.d.ts create mode 100644 integrations/joplin/api/JoplinCommands.d.ts create mode 100644 integrations/joplin/api/JoplinContentScripts.d.ts create mode 100644 integrations/joplin/api/JoplinData.d.ts create mode 100644 integrations/joplin/api/JoplinFilters.d.ts create mode 100644 integrations/joplin/api/JoplinFs.d.ts create mode 100644 integrations/joplin/api/JoplinImaging.d.ts create mode 100644 integrations/joplin/api/JoplinInterop.d.ts create mode 100644 integrations/joplin/api/JoplinPlugins.d.ts create mode 100644 integrations/joplin/api/JoplinSettings.d.ts create mode 100644 integrations/joplin/api/JoplinViews.d.ts create mode 100644 integrations/joplin/api/JoplinViewsDialogs.d.ts create mode 100644 integrations/joplin/api/JoplinViewsEditor.d.ts create mode 100644 integrations/joplin/api/JoplinViewsMenuItems.d.ts create mode 100644 integrations/joplin/api/JoplinViewsMenus.d.ts create mode 100644 integrations/joplin/api/JoplinViewsNoteList.d.ts create mode 100644 integrations/joplin/api/JoplinViewsPanels.d.ts create mode 100644 integrations/joplin/api/JoplinViewsToolbarButtons.d.ts create mode 100644 integrations/joplin/api/JoplinWindow.d.ts create mode 100644 integrations/joplin/api/JoplinWorkspace.d.ts create mode 100644 integrations/joplin/api/index.ts create mode 100644 integrations/joplin/api/noteListType.d.ts create mode 100644 integrations/joplin/api/noteListType.ts create mode 100644 integrations/joplin/api/types.ts create mode 100644 integrations/joplin/package.json create mode 100644 integrations/joplin/plugin.config.json create mode 100644 integrations/joplin/src/butterfly.css create mode 100644 integrations/joplin/src/butterflyDocument.ts create mode 100644 integrations/joplin/src/contentScripts/markdownItPlugin.js create mode 100644 integrations/joplin/src/contentScripts/webview.css create mode 100644 integrations/joplin/src/contentScripts/webview.ts create mode 100644 integrations/joplin/src/img/icon.svg create mode 100644 integrations/joplin/src/index.ts create mode 100644 integrations/joplin/src/manifest.json create mode 100644 integrations/joplin/src/views/panel.ts create mode 100644 integrations/joplin/tsconfig.json create mode 100644 integrations/joplin/webpack.config.js diff --git a/integrations/joplin/.gitignore b/integrations/joplin/.gitignore new file mode 100644 index 0000000..622f443 --- /dev/null +++ b/integrations/joplin/.gitignore @@ -0,0 +1,3 @@ +dist/ +node_modules/ +publish/ diff --git a/integrations/joplin/.npmignore b/integrations/joplin/.npmignore new file mode 100644 index 0000000..80f70d7 --- /dev/null +++ b/integrations/joplin/.npmignore @@ -0,0 +1,8 @@ +*.md +!README.md +/*.jpl +/api +/src +/dist +tsconfig.json +webpack.config.js diff --git a/integrations/joplin/GENERATOR_DOC.md b/integrations/joplin/GENERATOR_DOC.md new file mode 100644 index 0000000..db5bbc8 --- /dev/null +++ b/integrations/joplin/GENERATOR_DOC.md @@ -0,0 +1,78 @@ +# Plugin development + +This documentation describes how to create a plugin, and how to work with the plugin builder framework and API. + +## Installation + +First, install [Yeoman](http://yeoman.io) and generator-joplin using [npm](https://www.npmjs.com/) (we assume you have pre-installed [node.js](https://nodejs.org/)). + +```bash +npm install -g yo@4.3.1 +npm install -g generator-joplin +``` + +Then generate your new project: + +```bash +yo --node-package-manager npm joplin +``` + +## Structure + +The main two files you will want to look at are: + +- `/src/index.ts`, which contains the entry point for the plugin source code. +- `/src/manifest.json`, which is the plugin manifest. It contains information such as the plugin a name, version, etc. + +The file `/plugin.config.json` could also be useful if you intend to use [external scripts](#external-script-files), such as content scripts or webview scripts. + +## Building the plugin + +The plugin is built using Webpack, which creates the compiled code in `/dist`. A JPL archive will also be created at the root, which can use to distribute the plugin. + +To build the plugin, simply run `npm run dist`. + +The project is setup to use TypeScript, although you can change the configuration to use plain JavaScript. + +## Updating the manifest version number + +You can run `npm run updateVersion` to bump the patch part of the version number, so for example 1.0.3 will become 1.0.4. This script will update both the package.json and manifest.json version numbers so as to keep them in sync. + +## Publishing the plugin + +To publish the plugin, add it to npmjs.com by running `npm publish`. Later on, a script will pick up your plugin and add it automatically to the Joplin plugin repository as long as the package satisfies these conditions: + +- In `package.json`, the name starts with "joplin-plugin-". For example, "joplin-plugin-toc". +- In `package.json`, the keywords include "joplin-plugin". +- In the `publish/` directory, there should be a .jpl and .json file (which are built by `npm run dist`) + +In general all this is done automatically by the plugin generator, which will set the name and keywords of package.json, and will put the right files in the "publish" directory. But if something doesn't work and your plugin doesn't appear in the repository, double-check the above conditions. + +## Updating the plugin framework + +To update the plugin framework, run `npm run update`. + +In general this command tries to do the right thing - in particular it's going to merge the changes in package.json and .gitignore instead of overwriting. It will also leave "/src" as well as README.md untouched. + +The file that may cause problem is "webpack.config.js" because it's going to be overwritten. For that reason, if you want to change it, consider creating a separate JavaScript file and include it in webpack.config.js. That way, when you update, you only have to restore the line that include your file. + +## External script files + +By default, the compiler (webpack) is going to compile `src/index.ts` only (as well as any file it imports), and any other file will simply be copied to the plugin package. In some cases this is sufficient, however if you have [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplincontentscripts.html) or [webview scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinviewspanels.html#addscript) you might want to compile them too, in particular in these two cases: + +- The script is a TypeScript file - in which case it has to be compiled to JavaScript. + +- The script requires modules you've added to package.json. In that case, the script, whether JS or TS, must be compiled so that the dependencies are bundled with the JPL file. + +To get such an external script file to compile, you need to add it to the `extraScripts` array in `plugin.config.json`. The path you add should be relative to /src. For example, if you have a file in "/src/webviews/index.ts", the path should be set to "webviews/index.ts". Once compiled, the file will always be named with a .js extension. So you will get "webviews/index.js" in the plugin package, and that's the path you should use to reference the file. + +## More information + +- [Joplin Plugin API](https://joplinapp.org/api/references/plugin_api/classes/joplin.html) +- [Joplin Data API](https://joplinapp.org/help/api/references/rest_api) +- [Joplin Plugin Manifest](https://joplinapp.org/api/references/plugin_manifest/) +- Ask for help on the [forum](https://discourse.joplinapp.org/) or our [Discord channel](https://discord.gg/VSj7AFHvpq) + +## License + +MIT © Laurent Cozic diff --git a/integrations/joplin/README.md b/integrations/joplin/README.md index e41336b..9d5dd65 100644 --- a/integrations/joplin/README.md +++ b/integrations/joplin/README.md @@ -1,7 +1,5 @@ -# Butterfly for Joplin +# Joplin Plugin -This directory reserves the Joplin integration in the monorepo. It is not -implemented, installed, built, or released yet. +This is your new Joplin plugin. It is suggested that you use this README file to document your plugin. -When development starts, add its package here and opt it into the root pnpm -workspace and CI explicitly. +For information on how to build or publish the plugin, please see [GENERATOR_DOC.md](./GENERATOR_DOC.md) diff --git a/integrations/joplin/api/Global.d.ts b/integrations/joplin/api/Global.d.ts new file mode 100644 index 0000000..6a4c657 --- /dev/null +++ b/integrations/joplin/api/Global.d.ts @@ -0,0 +1,19 @@ +import Plugin from '../Plugin'; +import Joplin from './Joplin'; +import BasePlatformImplementation from '../BasePlatformImplementation'; +import type {Store} from 'redux'; +/** + * @ignore + */ +/** + * @ignore + */ +export default class Global { + private joplin_; + + constructor(implementation: BasePlatformImplementation, plugin: Plugin, store: Store); + + get joplin(): Joplin; + + get process(): NodeJS.Process; +} \ No newline at end of file diff --git a/integrations/joplin/api/Joplin.d.ts b/integrations/joplin/api/Joplin.d.ts new file mode 100644 index 0000000..ca1617e --- /dev/null +++ b/integrations/joplin/api/Joplin.d.ts @@ -0,0 +1,93 @@ +import Plugin from '../Plugin'; +import JoplinData from './JoplinData'; +import JoplinPlugins from './JoplinPlugins'; +import JoplinWorkspace from './JoplinWorkspace'; +import JoplinFilters from './JoplinFilters'; +import JoplinCommands from './JoplinCommands'; +import JoplinViews from './JoplinViews'; +import JoplinInterop from './JoplinInterop'; +import JoplinSettings from './JoplinSettings'; +import JoplinContentScripts from './JoplinContentScripts'; +import JoplinClipboard from './JoplinClipboard'; +import JoplinWindow from './JoplinWindow'; +import BasePlatformImplementation from '../BasePlatformImplementation'; +import JoplinImaging from './JoplinImaging'; +import JoplinFs from './JoplinFs'; +import JoplinAi from './JoplinAi'; +import type { Store } from 'redux'; +/** + * This is the main entry point to the Joplin API. You can access various services using the provided accessors. + * + * The API is now relatively stable and in general maintaining backward compatibility is a top priority, so you shouldn't except much breakages. + * + * If a breaking change ever becomes needed, best effort will be done to: + * + * - Deprecate features instead of removing them, so as to give you time to fix the issue; + * - Document breaking changes in the changelog; + * + * So if you are developing a plugin, please keep an eye on the changelog as everything will be in there with information about how to update your code. + */ +export default class Joplin { + private data_; + private plugins_; + private imaging_; + private fs_; + private workspace_; + private filters_; + private commands_; + private views_; + private interop_; + private settings_; + private contentScripts_; + private clipboard_; + private window_; + private ai_; + private implementation_; + constructor(implementation: BasePlatformImplementation, plugin: Plugin, store: Store); + get data(): JoplinData; + get clipboard(): JoplinClipboard; + get imaging(): JoplinImaging; + get fs(): JoplinFs; + get window(): JoplinWindow; + get plugins(): JoplinPlugins; + get workspace(): JoplinWorkspace; + get contentScripts(): JoplinContentScripts; + /** + * @ignore + * + * Not sure if it's the best way to hook into the app + * so for now disable filters. + */ + get filters(): JoplinFilters; + get commands(): JoplinCommands; + get views(): JoplinViews; + get interop(): JoplinInterop; + get settings(): JoplinSettings; + /** + * Access to AI features: chat completions and semantic search over the + * local embeddings index. See {@link JoplinAi}. + * + * desktop + */ + get ai(): JoplinAi; + /** + * It is not possible to bundle native packages with a plugin, because they + * need to work cross-platforms. Instead access to certain useful native + * packages is provided using this function. + * + * Currently these packages are available: + * + * - [sqlite3](https://www.npmjs.com/package/sqlite3) + * - [fs-extra](https://www.npmjs.com/package/fs-extra) + * + * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/nativeModule) + * + * desktop + */ + require(_path: string): any; + versionInfo(): Promise; + /** + * Tells whether the current theme is a dark one or not. + */ + shouldUseDarkColors(): Promise; +} diff --git a/integrations/joplin/api/JoplinAi.d.ts b/integrations/joplin/api/JoplinAi.d.ts new file mode 100644 index 0000000..9e611b2 --- /dev/null +++ b/integrations/joplin/api/JoplinAi.d.ts @@ -0,0 +1,80 @@ +import { ChatMessage, ChatOptions, SearchOptions, SearchResult } from './types'; +/** + * Provides access to AI models configured by the user. The active provider + * (Joplin Cloud AI, OpenAI-compatible, or Anthropic) and the model are picked + * by the user in the Joplin settings — plugins inherit whichever is active. + * + * AI is disabled by default. The user must enable it in the settings, and + * separately grant permission to use a remote (cloud-hosted) provider before + * any plugin call will succeed. + * + * If the user is signed into Joplin Cloud, AI works zero-config — they only + * need to flip the master toggle on. + * + * desktop + */ +export default class JoplinAi { + /** + * Sends a chat completion request to the active AI provider and returns the + * assistant's text response. + * + * The active provider and model are controlled by the user in Settings → + * AI. Plugins should not assume any particular provider or model. + * + * This call throws when: + * + * - AI features are disabled (`AI features are disabled`). + * - The active provider is remote and the user has not allowed remote + * providers (`Remote AI access is not allowed`). + * - The provider is misconfigured, e.g. missing API key or model name + * (`*provider* has no API key configured`). + * - The provider returns an HTTP error (the message includes the status + * and any detail returned by the provider). + * + * Plugins should catch these errors and present a user-friendly message + * pointing the user at the Joplin settings. + * + * @example + * ```typescript + * const reply = await joplin.ai.chat([ + * { role: 'system', content: 'You are a concise assistant.' }, + * { role: 'user', content: 'Summarise this note: ...' }, + * ]); + * console.log(reply); + * ``` + */ + chat(messages: ChatMessage[], options?: ChatOptions): Promise; + /** + * Runs a semantic search against the locally-indexed embeddings and + * returns matching chunks ranked by similarity. + * + * The `query` is either plain text (which gets embedded internally) or + * `{ noteId }`, which reuses the note's already-indexed chunks as the + * query — useful for "find related notes" / tag suggestion / semantic + * graph use cases without spending another embedding pass. + * + * The `scope` restricts the search: `'all'` (default), `'note'`, + * `'folder'` (by folder id), or `'tag'` (by tag id). + * Trashed and conflict notes are excluded from results. + * + * The `relevance` preset controls how strict the match is: + * `'strict' | 'normal' | 'loose'`. Joplin owns the mapping from preset + * to model-specific (k, minScore) — plugins write against the preset + * and stay compatible when the bundled model changes. + * + * Throws when AI features are disabled or no embedding provider is + * active (e.g. ONNX failed to load on this platform). + * + * @example + * ```typescript + * const results = await joplin.ai.search({ + * query: { text: 'pizza dough hydration' }, + * relevance: 'normal', + * }); + * for (const r of results) { + * console.log(r.score, r.noteId, r.chunkText.slice(0, 80)); + * } + * ``` + */ + search(options: SearchOptions): Promise; +} diff --git a/integrations/joplin/api/JoplinClipboard.d.ts b/integrations/joplin/api/JoplinClipboard.d.ts new file mode 100644 index 0000000..60c4c1c --- /dev/null +++ b/integrations/joplin/api/JoplinClipboard.d.ts @@ -0,0 +1,61 @@ +import { ClipboardContent } from './types'; +interface ElectronClipboardLike { + readText(): string; + writeText(text: string): void; + readHTML(): string; + writeHTML(html: string): void; + readImage(): { + toDataURL(): string; + } | null; + writeImage(image: unknown): void; + availableFormats(): string[]; + write(data: Record): void; +} +interface ElectronNativeImageLike { + createFromDataURL(dataUrl: string): unknown; +} +export default class JoplinClipboard { + private electronClipboard_; + private electronNativeImage_; + constructor(electronClipboard: ElectronClipboardLike, electronNativeImage: ElectronNativeImageLike); + readText(): Promise; + writeText(text: string): Promise; + /** desktop */ + readHtml(): Promise; + /** desktop */ + writeHtml(html: string): Promise; + /** + * Returns the image in [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) format. + * + * desktop + */ + readImage(): Promise; + /** + * Takes an image in [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) format. + * + * desktop + */ + writeImage(dataUrl: string): Promise; + /** + * Returns the list available formats (mime types). + * + * For example [ 'text/plain', 'text/html' ] + */ + availableFormats(): Promise; + /** + * Writes multiple formats to the clipboard simultaneously. + * This allows setting both text/plain and text/html at the same time. + * + * desktop + * + * @example + * ```typescript + * await joplin.clipboard.write({ + * text: 'Plain text version', + * html: 'HTML version' + * }); + * ``` + */ + write(content: ClipboardContent): Promise; +} +export {}; diff --git a/integrations/joplin/api/JoplinCommands.d.ts b/integrations/joplin/api/JoplinCommands.d.ts new file mode 100644 index 0000000..17d1004 --- /dev/null +++ b/integrations/joplin/api/JoplinCommands.d.ts @@ -0,0 +1,102 @@ +import { Command } from './types'; +import Plugin from '../Plugin'; +/** + * This class allows executing or registering new Joplin commands. Commands + * can be executed or associated with + * {@link JoplinViewsToolbarButtons | toolbar buttons} or + * {@link JoplinViewsMenuItems | menu items}. + * + * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/register_command) + * + * ## Executing Joplin's internal commands + * + * It is also possible to execute internal Joplin's commands which, as of + * now, are not well documented. You can find the list directly on GitHub + * though at the following locations: + * + * * [Main screen commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/gui/WindowCommandsAndDialogs/commands) + * * [Global commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/commands) + * * [Editor commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.ts) + * + * To view what arguments are supported, you can open any of these files + * and look at the `execute()` command. + * + * Note that many of these commands only work on desktop. The more limited list of mobile + * commands can be found in these places: + * + * * [Global commands](https://github.com/laurent22/joplin/tree/dev/packages/app-mobile/commands) + * * [Note screen commands](https://github.com/laurent22/joplin/tree/dev/packages/app-mobile/components/screens/Note/commands) + * * [Editor commands](https://github.com/laurent22/joplin/blob/dev/packages/app-mobile/components/NoteEditor/commandDeclarations.ts) + * + * Additionally, certain global commands have the same implementation on both platforms: + * + * * [Shared global commands](https://github.com/laurent22/joplin/tree/dev/packages/lib/commands) + * + * ## Executing editor commands + * + * There might be a situation where you want to invoke editor commands + * without using a {@link JoplinContentScripts | contentScript}. For this + * reason Joplin provides the built in `editor.execCommand` command. + * + * `editor.execCommand` should work with any core command in both the + * [CodeMirror](https://codemirror.net/doc/manual.html#execCommand) and + * [TinyMCE](https://www.tiny.cloud/docs/api/tinymce/tinymce.editorcommands/#execcommand) editors, + * as well as most functions calls directly on a CodeMirror editor object (extensions). + * + * * [CodeMirror commands](https://codemirror.net/doc/manual.html#commands) + * * [TinyMCE core editor commands](https://www.tiny.cloud/docs/advanced/editor-command-identifiers/#coreeditorcommands) + * + * `editor.execCommand` supports adding arguments for the commands. + * + * ```typescript + * await joplin.commands.execute('editor.execCommand', { + * name: 'madeUpCommand', // CodeMirror and TinyMCE + * args: [], // CodeMirror and TinyMCE + * ui: false, // TinyMCE only + * value: '', // TinyMCE only + * }); + * ``` + * + * [View the example using the CodeMirror editor](https://github.com/laurent22/joplin/blob/dev/packages/app-cli/tests/support/plugins/codemirror_content_script/src/index.ts) + * + */ +export default class JoplinCommands { + private plugin_; + constructor(plugin_: Plugin); + /** + * Executes the given command. + * + * The command can take any number of arguments, and the supported + * arguments will vary based on the command. For custom commands, this + * is the `args` passed to the `execute()` function. For built-in + * commands, you can find the supported arguments by checking the links + * above. + * + * ```typescript + * // Create a new note in the current notebook: + * await joplin.commands.execute('newNote'); + * + * // Create a new sub-notebook under the provided notebook + * // Note: internally, notebooks are called "folders". + * await joplin.commands.execute('newFolder', "SOME_FOLDER_ID"); + * ``` + */ + execute(commandName: string, ...args: any[]): Promise; + /** + * Registers a new command. + * + * ```typescript + * // Register a new commmand called "testCommand1" + * + * await joplin.commands.register({ + * name: 'testCommand1', + * label: 'My Test Command 1', + * iconName: 'fas fa-music', + * execute: () => { + * alert('Testing plugin command 1'); + * }, + * }); + * ``` + */ + register(command: Command): Promise; +} diff --git a/integrations/joplin/api/JoplinContentScripts.d.ts b/integrations/joplin/api/JoplinContentScripts.d.ts new file mode 100644 index 0000000..d391ce0 --- /dev/null +++ b/integrations/joplin/api/JoplinContentScripts.d.ts @@ -0,0 +1,41 @@ +import Plugin, { MessageListenerCallback } from '../Plugin'; +import { ContentScriptType } from './types'; +export default class JoplinContentScripts { + private plugin; + constructor(plugin: Plugin); + /** + * Registers a new content script. Unlike regular plugin code, which runs in + * a separate process, content scripts run within the main process code and + * thus allow improved performances and more customisations in specific + * cases. It can be used for example to load a Markdown or editor plugin. + * + * Note that registering a content script in itself will do nothing - it + * will only be loaded in specific cases by the relevant app modules (eg. + * the Markdown renderer or the code editor). So it is not a way to inject + * and run arbitrary code in the app, which for safety and performance + * reasons is not supported. + * + * The plugin generator provides a way to build any content script you might + * want to package as well as its dependencies. See the [Plugin Generator + * doc](https://github.com/laurent22/joplin/blob/dev/packages/generator-joplin/README.md) + * for more information. + * + * * [View the renderer demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script) + * * [View the editor plugin tutorial](https://joplinapp.org/help/api/tutorials/cm6_plugin) + * * [View the legacy editor demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/codemirror_content_script) + * + * See also the [postMessage demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages) + * + * @param type Defines how the script will be used. See the type definition for more information about each supported type. + * @param id A unique ID for the content script. + * @param scriptPath Must be a path relative to the plugin main script. For example, if your file content_script.js is next to your index.ts file, you would set `scriptPath` to `"./content_script.js`. + */ + register(type: ContentScriptType, id: string, scriptPath: string): Promise; + /** + * Listens to a messages sent from the content script using postMessage(). + * See {@link ContentScriptType} for more information as well as the + * [postMessage + * demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages) + */ + onMessage(contentScriptId: string, callback: MessageListenerCallback): Promise; +} diff --git a/integrations/joplin/api/JoplinData.d.ts b/integrations/joplin/api/JoplinData.d.ts new file mode 100644 index 0000000..e76db36 --- /dev/null +++ b/integrations/joplin/api/JoplinData.d.ts @@ -0,0 +1,74 @@ +import { ModelType } from '../../../BaseModel'; +import { RequestFile } from '../../rest/Api'; +import Plugin from '../Plugin'; +import { Path } from './types'; +/** + * This module provides access to the Joplin data API: https://joplinapp.org/help/api/references/rest_api + * This is the main way to retrieve data, such as notes, notebooks, tags, etc. + * or to update them or delete them. + * + * This is also what you would use to search notes, via the `search` endpoint. + * + * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/simple) + * + * In general you would use the methods in this class as if you were using a REST API. There are four methods that map to GET, POST, PUT and DELETE calls. + * And each method takes these parameters: + * + * * `path`: This is an array that represents the path to the resource in the form `["resourceName", "resourceId", "resourceLink"]` (eg. ["tags", ":id", "notes"]). The "resources" segment is the name of the resources you want to access (eg. "notes", "folders", etc.). If not followed by anything, it will refer to all the resources in that collection. The optional "resourceId" points to a particular resources within the collection. Finally, an optional "link" can be present, which links the resource to a collection of resources. This can be used in the API for example to retrieve all the notes associated with a tag. + * * `query`: (Optional) The query parameters. In a URL, this is the part after the question mark "?". In this case, it should be an object with key/value pairs. + * * `data`: (Optional) Applies to PUT and POST calls only. The request body contains the data you want to create or modify, for example the content of a note or folder. + * * `files`: (Optional) Used to create new resources and associate them with files. + * + * Please refer to the [Joplin API documentation](https://joplinapp.org/help/api/references/rest_api) for complete details about each call. As the plugin runs within the Joplin application **you do not need an authorisation token** to use this API. + * + * For example: + * + * ```typescript + * // Get a note ID, title and body + * const noteId = 'some_note_id'; + * const note = await joplin.data.get(['notes', noteId], { fields: ['id', 'title', 'body'] }); + * + * // Get all folders + * const folders = await joplin.data.get(['folders']); + * + * // Set the note body + * await joplin.data.put(['notes', noteId], null, { body: "New note body" }); + * + * // Create a new note under one of the folders + * await joplin.data.post(['notes'], null, { body: "my new note", title: "some title", parent_id: folders[0].id }); + * ``` + */ +export default class JoplinData { + private api_; + private pathSegmentRegex_; + private plugin; + constructor(plugin: Plugin); + private serializeApiBody; + private pathToString; + get(path: Path, query?: any): Promise; + post(path: Path, query?: any, body?: any, files?: RequestFile[]): Promise; + put(path: Path, query?: any, body?: any, files?: RequestFile[]): Promise; + delete(path: Path, query?: any): Promise; + itemType(itemId: string): Promise; + resourcePath(resourceId: string): Promise; + /** + * Gets an item user data. User data are key/value pairs. The `key` can be any + * arbitrary string, while the `value` can be of any type supported by + * [JSON.stringify](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#description) + * + * User data is synchronised across devices, and each value wil be merged based on their timestamp: + * + * - If value is modified by client 1, then modified by client 2, it will take the value from client 2 + * - If value is modified by client 1, then deleted by client 2, the value will be deleted after merge + * - If value is deleted by client 1, then updated by client 2, the value will be restored and set to the value from client 2 after merge + */ + userDataGet(itemType: ModelType, itemId: string, key: string): Promise; + /** + * Sets a note user data. See {@link JoplinData.userDataGet} for more details. + */ + userDataSet(itemType: ModelType, itemId: string, key: string, value: T): Promise; + /** + * Deletes a note user data. See {@link JoplinData.userDataGet} for more details. + */ + userDataDelete(itemType: ModelType, itemId: string, key: string): Promise; +} diff --git a/integrations/joplin/api/JoplinFilters.d.ts b/integrations/joplin/api/JoplinFilters.d.ts new file mode 100644 index 0000000..6a50479 --- /dev/null +++ b/integrations/joplin/api/JoplinFilters.d.ts @@ -0,0 +1,11 @@ +import { FilterHandler } from '../../../eventManager'; +/** + * @ignore + * + * Not sure if it's the best way to hook into the app + * so for now disable filters. + */ +export default class JoplinFilters { + on(name: string, callback: FilterHandler): Promise; + off(name: string, callback: FilterHandler): Promise; +} diff --git a/integrations/joplin/api/JoplinFs.d.ts b/integrations/joplin/api/JoplinFs.d.ts new file mode 100644 index 0000000..238d5da --- /dev/null +++ b/integrations/joplin/api/JoplinFs.d.ts @@ -0,0 +1,22 @@ +export interface ArchiveEntry { + entryName: string; + name: string; +} +/** + * Provides file system utilities for plugins. + * + * desktop + */ +export default class JoplinFs { + /** + * Extracts an archive to the specified directory. Currently only ZIP files + * are supported. + * + * desktop + * + * @param sourcePath Path to the archive file to extract + * @param destinationPath Path to the directory where the contents should be extracted + * @returns List of entries extracted from the archive + */ + archiveExtract(sourcePath: string, destinationPath: string): Promise; +} diff --git a/integrations/joplin/api/JoplinImaging.d.ts b/integrations/joplin/api/JoplinImaging.d.ts new file mode 100644 index 0000000..7c5d469 --- /dev/null +++ b/integrations/joplin/api/JoplinImaging.d.ts @@ -0,0 +1,96 @@ +import { ResourceEntity } from '../../database/types'; +import { Rectangle } from './types'; +export interface CreateFromBufferOptions { + width?: number; + height?: number; + scaleFactor?: number; +} +export interface CreateFromPdfOptions { + /** + * The first page to export. Defaults to `1`, the first page in + * the document. + */ + minPage?: number; + /** + * The number of the last page to convert. Defaults to the last page + * if not given. + * + * If `maxPage` is greater than the number of pages in the PDF, all pages + * in the PDF will be converted to images. + */ + maxPage?: number; + scaleFactor?: number; +} +export interface PdfInfo { + pageCount: number; +} +export interface Implementation { + createFromPath: (path: string) => Promise; + createFromPdf: (path: string, options: CreateFromPdfOptions) => Promise; + getPdfInfo: (path: string) => Promise; +} +export interface ResizeOptions { + width?: number; + height?: number; + quality?: 'good' | 'better' | 'best'; +} +export type Handle = string; +/** + * Provides imaging functions to resize or process images. You create an image + * using one of the `createFrom` functions, then use the other functions to + * process the image. + * + * Images are associated with a handle which is what will be available to the + * plugin. Once you are done with an image, free it using the `free()` function. + * + * [View the + * example](https://github.com/laurent22/joplin/blob/dev/packages/app-cli/tests/support/plugins/imaging/src/index.ts) + * + * desktop + */ +export default class JoplinImaging { + private implementation_; + private images_; + constructor(implementation: Implementation); + private createImageHandle; + private imageByHandle; + private cacheImage; + /** + * Creates an image from the provided path. Note that images and PDFs are supported. If you + * provide a URL instead of a local path, the file will be downloaded first then converted to an + * image. + */ + createFromPath(filePath: string): Promise; + createFromResource(resourceId: string): Promise; + createFromPdfPath(path: string, options?: CreateFromPdfOptions): Promise; + createFromPdfResource(resourceId: string, options?: CreateFromPdfOptions): Promise; + getPdfInfoFromPath(path: string): Promise; + getPdfInfoFromResource(resourceId: string): Promise; + getSize(handle: Handle): Promise<{ + width: number; + height: number; + }>; + resize(handle: Handle, options?: ResizeOptions): Promise; + crop(handle: Handle, rectangle: Rectangle): Promise; + toPngFile(handle: Handle, filePath: string): Promise; + /** + * Quality is between 0 and 100 + */ + toJpgFile(handle: Handle, filePath: string, quality?: number): Promise; + private tempFilePath; + /** + * Creates a new Joplin resource from the image data. The image will be + * first converted to a JPEG. + */ + toJpgResource(handle: Handle, resourceProps: Partial, quality?: number): Promise; + /** + * Creates a new Joplin resource from the image data. The image will be + * first converted to a PNG. + */ + toPngResource(handle: Handle, resourceProps: Partial): Promise; + /** + * Image data is not automatically deleted by Joplin so make sure you call + * this method on the handle once you are done. + */ + free(handles: Handle[] | Handle): Promise; +} diff --git a/integrations/joplin/api/JoplinInterop.d.ts b/integrations/joplin/api/JoplinInterop.d.ts new file mode 100644 index 0000000..349edd2 --- /dev/null +++ b/integrations/joplin/api/JoplinInterop.d.ts @@ -0,0 +1,20 @@ +import { ExportModule, ImportModule } from './types'; +/** + * Provides a way to create modules to import external data into Joplin or to export notes into any arbitrary format. + * + * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/json_export) + * + * To implement an import or export module, you would simply define an object with various event handlers that are called + * by the application during the import/export process. + * + * See the documentation of the [[ExportModule]] and [[ImportModule]] for more information. + * + * You may also want to refer to the Joplin API documentation to see the list of properties for each item (note, notebook, etc.) - https://joplinapp.org/help/api/references/rest_api + * + * desktop: While it is possible to register import and export + * modules on mobile, there is no GUI to activate them. + */ +export default class JoplinInterop { + registerExportModule(module: ExportModule): Promise; + registerImportModule(module: ImportModule): Promise; +} diff --git a/integrations/joplin/api/JoplinPlugins.d.ts b/integrations/joplin/api/JoplinPlugins.d.ts new file mode 100644 index 0000000..a1e4a83 --- /dev/null +++ b/integrations/joplin/api/JoplinPlugins.d.ts @@ -0,0 +1,43 @@ +import Plugin from '../Plugin'; +import { ContentScriptType, Script } from './types'; +/** + * This class provides access to plugin-related features. + */ +export default class JoplinPlugins { + private plugin; + constructor(plugin: Plugin); + /** + * Registers a new plugin. This is the entry point when creating a plugin. You should pass a simple object with an `onStart` method to it. + * That `onStart` method will be executed as soon as the plugin is loaded. + * + * ```typescript + * joplin.plugins.register({ + * onStart: async function() { + * // Run your plugin code here + * } + * }); + * ``` + */ + register(script: Script): Promise; + /** + * @deprecated Use joplin.contentScripts.register() + */ + registerContentScript(type: ContentScriptType, id: string, scriptPath: string): Promise; + /** + * Gets the plugin own data directory path. Use this to store any + * plugin-related data. Unlike [[installationDir]], any data stored here + * will be persisted. + */ + dataDir(): Promise; + /** + * Gets the plugin installation directory. This can be used to access any + * asset that was packaged with the plugin. This directory should be + * considered read-only because any data you store here might be deleted or + * re-created at any time. To store new persistent data, use [[dataDir]]. + */ + installationDir(): Promise; + /** + * @deprecated Use joplin.require() + */ + require(_path: string): any; +} diff --git a/integrations/joplin/api/JoplinSettings.d.ts b/integrations/joplin/api/JoplinSettings.d.ts new file mode 100644 index 0000000..a3aa324 --- /dev/null +++ b/integrations/joplin/api/JoplinSettings.d.ts @@ -0,0 +1,74 @@ +import Plugin from '../Plugin'; +import { SettingItem, SettingSection } from './types'; +export interface ChangeEvent { + /** + * Setting keys that have been changed + */ + keys: string[]; +} +export type ChangeHandler = (event: ChangeEvent) => void; +/** + * This API allows registering new settings and setting sections, as well as getting and setting settings. Once a setting has been registered it will appear in the config screen and be editable by the user. + * + * Settings are essentially key/value pairs. + * + * Note: Currently this API does **not** provide access to Joplin's built-in settings. This is by design as plugins that modify user settings could give unexpected results + * + * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/settings) + */ +export default class JoplinSettings { + private plugin_; + constructor(plugin: Plugin); + /** + * Registers new settings. + * Note that registering a setting item is dynamic and will be gone next time Joplin starts. + * What it means is that you need to register the setting every time the plugin starts (for example in the onStart event). + * The setting value however will be preserved from one launch to the next so there is no risk that it will be lost even if for some + * reason the plugin fails to start at some point. + */ + registerSettings(settings: Record): Promise; + /** + * @deprecated Use joplin.settings.registerSettings() + * + * Registers a new setting. + */ + registerSetting(key: string, settingItem: SettingItem): Promise; + /** + * Registers a new setting section. Like for registerSetting, it is dynamic and needs to be done every time the plugin starts. + */ + registerSection(name: string, section: SettingSection): Promise; + /** + * Gets setting values (only applies to setting you registered from your plugin) + */ + values(keys: string[] | string): Promise>; + /** + * Gets a setting value (only applies to setting you registered from your plugin). + * + * Note: If you want to retrieve all your plugin settings, for example when the plugin starts, + * it is recommended to use the `values()` function instead - it will be much faster than + * calling `value()` multiple times. + */ + value(key: string): Promise; + /** + * Sets a setting value (only applies to setting you registered from your plugin) + */ + setValue(key: string, value: any): Promise; + /** + * Gets global setting values, including app-specific settings and those set by other plugins. + * + * The list of available settings is not documented yet, but can be found by looking at the source code: + * + * https://github.com/laurent22/joplin/blob/dev/packages/lib/models/settings/builtInMetadata.ts + */ + globalValues(keys: string[]): Promise; + /** + * @deprecated Use joplin.settings.globalValues() + */ + globalValue(key: string): Promise; + /** + * Called when one or multiple settings of your plugin have been changed. + * - For performance reasons, this event is triggered with a delay. + * - You will only get events for your own plugin settings. + */ + onChange(handler: ChangeHandler): Promise; +} diff --git a/integrations/joplin/api/JoplinViews.d.ts b/integrations/joplin/api/JoplinViews.d.ts new file mode 100644 index 0000000..237286b --- /dev/null +++ b/integrations/joplin/api/JoplinViews.d.ts @@ -0,0 +1,45 @@ +import { JoplinViews as JoplinViewsImplementation } from '../BasePlatformImplementation'; +import Plugin from '../Plugin'; +import { PluginStore } from '../ViewController'; +import JoplinViewsDialogs from './JoplinViewsDialogs'; +import JoplinViewsMenuItems from './JoplinViewsMenuItems'; +import JoplinViewsMenus from './JoplinViewsMenus'; +import JoplinViewsToolbarButtons from './JoplinViewsToolbarButtons'; +import JoplinViewsPanels from './JoplinViewsPanels'; +import JoplinViewsNoteList from './JoplinViewsNoteList'; +import JoplinViewsEditors from './JoplinViewsEditor'; +/** + * This namespace provides access to view-related services. + * + * ## Creating a view + * + * All view services provide a `create()` method which you would use to create the view object, + * whether it's a dialog, a toolbar button or a menu item. In some cases, the `create()` method will + * return a [[ViewHandle]], which you would use to act on the view, for example to set certain + * properties or call some methods. + * + * ## The `webviewApi` object + * + * Within a view, you can use the global object `webviewApi` for various utility functions, such as + * sending messages or displaying context menu. Refer to [[WebviewApi]] for the full documentation. + */ +export default class JoplinViews { + private store; + private plugin; + private panels_; + private menuItems_; + private menus_; + private toolbarButtons_; + private dialogs_; + private editors_; + private noteList_; + private implementation_; + constructor(implementation: JoplinViewsImplementation, plugin: Plugin, store: PluginStore); + get dialogs(): JoplinViewsDialogs; + get panels(): JoplinViewsPanels; + get editors(): JoplinViewsEditors; + get menuItems(): JoplinViewsMenuItems; + get menus(): JoplinViewsMenus; + get toolbarButtons(): JoplinViewsToolbarButtons; + get noteList(): JoplinViewsNoteList; +} diff --git a/integrations/joplin/api/JoplinViewsDialogs.d.ts b/integrations/joplin/api/JoplinViewsDialogs.d.ts new file mode 100644 index 0000000..a331545 --- /dev/null +++ b/integrations/joplin/api/JoplinViewsDialogs.d.ts @@ -0,0 +1,84 @@ +import Plugin from '../Plugin'; +import { ButtonSpec, ViewHandle, DialogResult, Toast } from './types'; +import { JoplinViewsDialogs as JoplinViewsDialogsImplementation, ShowOpenDialogOptions } from '../BasePlatformImplementation'; +import { PluginStore } from '../ViewController'; +/** + * Allows creating and managing dialogs. A dialog is modal window that + * contains a webview and a row of buttons. You can update the + * webview using the `setHtml` method. Dialogs are hidden by default and + * you need to call `open()` to open them. Once the user clicks on a + * button, the `open` call will return an object indicating what button was + * clicked on. + * + * ## Retrieving form values + * + * If your HTML content included one or more forms, a `formData` object + * will also be included with the key/value for each form. + * + * ## Special button IDs + * + * The following buttons IDs have a special meaning: + * + * - `ok`, `yes`, `submit`, `confirm`: They are considered "submit" buttons + * - `cancel`, `no`, `reject`: They are considered "dismiss" buttons + * + * This information is used by the application to determine what action + * should be done when the user presses "Enter" or "Escape" within the + * dialog. If they press "Enter", the first "submit" button will be + * automatically clicked. If they press "Escape" the first "dismiss" button + * will be automatically clicked. + * + * [View the demo + * plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/dialog) + */ +export default class JoplinViewsDialogs { + private store; + private plugin; + private implementation_; + constructor(implementation: JoplinViewsDialogsImplementation, plugin: Plugin, store: PluginStore); + private controller; + /** + * Creates a new dialog + */ + create(id: string): Promise; + /** + * Displays a message box with OK/Cancel buttons. Returns the button index that was clicked - "0" for OK and "1" for "Cancel" + */ + showMessageBox(message: string): Promise; + /** + * Displays a Toast notification in the corner of the application screen. + */ + showToast(toast: Toast): Promise; + /** + * Displays a dialog to select a file or a directory. Same options and + * output as + * https://www.electronjs.org/docs/latest/api/dialog#dialogshowopendialogbrowserwindow-options + * + * desktop + */ + showOpenDialog(options: ShowOpenDialogOptions): Promise; + /** + * Sets the dialog HTML content + */ + setHtml(handle: ViewHandle, html: string): Promise; + /** + * Adds and loads a new JS or CSS files into the dialog. + */ + addScript(handle: ViewHandle, scriptPath: string): Promise; + /** + * Sets the dialog buttons. + */ + setButtons(handle: ViewHandle, buttons: ButtonSpec[]): Promise; + /** + * Opens the dialog. + * + * On desktop, this closes any copies of the dialog open in different windows. + */ + open(handle: ViewHandle): Promise; + /** + * Toggle on whether to fit the dialog size to the content or not. + * When set to false, the dialog is set to 90vw and 80vh + * @default true + */ + setFitToContent(handle: ViewHandle, status: boolean): Promise; +} diff --git a/integrations/joplin/api/JoplinViewsEditor.d.ts b/integrations/joplin/api/JoplinViewsEditor.d.ts new file mode 100644 index 0000000..72bd953 --- /dev/null +++ b/integrations/joplin/api/JoplinViewsEditor.d.ts @@ -0,0 +1,118 @@ +import Plugin, { MessageListenerCallback } from '../Plugin'; +import { PluginStore } from '../ViewController'; +import { ActivationCheckCallback, ViewHandle, UpdateCallback, EditorPluginCallbacks } from './types'; +interface SaveNoteOptions { + /** + * The ID of the note to save. This should match either: + * - The ID of the note currently being edited + * - The ID of a note that was very recently open in the editor. + * + * This property is present to ensure that the note editor doesn't write + * to the wrong note just after switching notes. + */ + noteId: string; + /** The note's new content. */ + body: string; +} +/** + * Allows creating alternative note editors. You can create a view to handle loading and saving the + * note, and do your own rendering. + * + * Although it may be used to implement an alternative text editor, the more common use case may be + * to render the note in a different, graphical way - for example displaying a graph, and + * saving/loading the graph data in the associated note. In that case, you would detect whether the + * current note contains graph data and, in this case, you'd display your viewer. + * + * Terminology: An editor is **active** when it can be used to edit the current note. Note that it + * doesn't necessarily mean that your editor is visible - it just means that the user has the option + * to switch to it (via the "toggle editor" button). A **visible** editor is active and is currently + * being displayed. + * + * To implement an editor you need to listen to two events: + * + * - `onActivationCheck`: This is a way for the app to know whether your editor should be active or + * not. Return `true` from this handler to activate your editor. + * + * - `onUpdate`: When this is called you should update your editor based on the current note + * content. Call `joplin.workspace.selectedNote()` to get the current note. + * + * - `showEditorPlugin` and `toggleEditorPlugin` commands. Additionally you can use these commands + * to display your editor via `joplin.commands.execute('showEditorPlugin')`. This is not always + * necessary since the user can switch to your editor using the "toggle editor" button, however + * you may want to programmatically display the editor in some cases - for example when creating a + * new note specific to your editor. + * + * Note that only one editor view can be active at a time. This is why it is important not to + * activate your view if it's not relevant to the current note. If more than one is active, it is + * undefined which editor is going to be used to display the note. + * + * For an example of editor plugin, see the [YesYouKan + * plugin](https://github.com/joplin/plugin-yesyoukan/blob/master/src/index.ts). In particular, + * check the logic around `onActivationCheck` and `onUpdate` since this is the entry points for + * using this API. + */ +export default class JoplinViewsEditors { + private store; + private plugin; + private activationCheckHandlers_; + private unhandledActivationCheck_; + constructor(plugin: Plugin, store: PluginStore); + private controller; + /** + * Registers a new editor plugin. Joplin will call the provided callback to create new editor views + * associated with the plugin as necessary (e.g. when a new editor is created in a new window). + */ + register(viewId: string, callbacks: EditorPluginCallbacks): Promise; + /** + * Creates a new editor view + * + * @deprecated + */ + create(id: string): Promise; + /** + * Sets the editor HTML content + */ + setHtml(handle: ViewHandle, html: string): Promise; + /** + * Adds and loads a new JS or CSS file into the panel. + */ + addScript(handle: ViewHandle, scriptPath: string): Promise; + /** + * See [[JoplinViewPanels]] + */ + onMessage(handle: ViewHandle, callback: MessageListenerCallback): Promise; + /** + * Saves the content of the editor, without calling `onUpdate` for editors in the same window. + */ + saveNote(handle: ViewHandle, props: SaveNoteOptions): Promise; + /** + * Emitted when the editor can potentially be activated - this is for example when the current + * note is changed, or when the application is opened. At that point you should check the + * current note and decide whether your editor should be activated or not. If it should, return + * `true`, otherwise return `false`. + * + * @deprecated - `onActivationCheck` should be provided when the editor is first created with + * `editor.register`. + */ + onActivationCheck(handle: ViewHandle, callback: ActivationCheckCallback): Promise; + /** + * Emitted when your editor content should be updated. This is for example when the currently + * selected note changes, or when the user makes the editor visible. + */ + onUpdate(handle: ViewHandle, callback: UpdateCallback): Promise; + /** + * See [[JoplinViewPanels]] + */ + postMessage(handle: ViewHandle, message: any): void; + /** + * Tells whether the editor is active or not. + */ + isActive(handle: ViewHandle): Promise; + /** + * Tells whether the editor is effectively visible or not. If the editor is inactive, this will + * return `false`. If the editor is active and the user has switched to it, it will return + * `true`. Otherwise it will return `false`. + */ + isVisible(handle: ViewHandle): Promise; +} +export {}; diff --git a/integrations/joplin/api/JoplinViewsMenuItems.d.ts b/integrations/joplin/api/JoplinViewsMenuItems.d.ts new file mode 100644 index 0000000..d8b46f5 --- /dev/null +++ b/integrations/joplin/api/JoplinViewsMenuItems.d.ts @@ -0,0 +1,19 @@ +import { CreateMenuItemOptions, MenuItemLocation } from './types'; +import Plugin from '../Plugin'; +import { PluginStore } from '../ViewController'; +/** + * Allows creating and managing menu items. + * + * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/register_command) + * + * desktop + */ +export default class JoplinViewsMenuItems { + private store; + private plugin; + constructor(plugin: Plugin, store: PluginStore); + /** + * Creates a new menu item and associate it with the given command. You can specify under which menu the item should appear using the `location` parameter. + */ + create(id: string, commandName: string, location?: MenuItemLocation, options?: CreateMenuItemOptions): Promise; +} diff --git a/integrations/joplin/api/JoplinViewsMenus.d.ts b/integrations/joplin/api/JoplinViewsMenus.d.ts new file mode 100644 index 0000000..67f6d6d --- /dev/null +++ b/integrations/joplin/api/JoplinViewsMenus.d.ts @@ -0,0 +1,21 @@ +import { MenuItem, MenuItemLocation } from './types'; +import Plugin from '../Plugin'; +import { PluginStore } from '../ViewController'; +/** + * Allows creating menus. + * + * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/menu) + * + * desktop + */ +export default class JoplinViewsMenus { + private store; + private plugin; + constructor(plugin: Plugin, store: PluginStore); + private registerCommandAccelerators; + /** + * Creates a new menu from the provided menu items and place it at the given location. As of now, it is only possible to place the + * menu as a sub-menu of the application build-in menus. + */ + create(id: string, label: string, menuItems: MenuItem[], location?: MenuItemLocation): Promise; +} diff --git a/integrations/joplin/api/JoplinViewsNoteList.d.ts b/integrations/joplin/api/JoplinViewsNoteList.d.ts new file mode 100644 index 0000000..6edf9b5 --- /dev/null +++ b/integrations/joplin/api/JoplinViewsNoteList.d.ts @@ -0,0 +1,42 @@ +import { Store } from 'redux'; +import Plugin from '../Plugin'; +import { ListRenderer } from './noteListType'; +/** + * This API allows you to customise how each note in the note list is rendered. + * The renderer you implement follows a unidirectional data flow. + * + * The app provides the required dependencies whenever a note is updated - you + * process these dependencies, and return some props, which are then passed to + * your template and rendered. See [[ListRenderer]] for a detailed description + * of each property of the renderer. + * + * ## Reference + * + * * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/note_list_renderer) + * + * * [Default simple renderer](https://github.com/laurent22/joplin/tree/dev/packages/lib/services/noteList/defaultListRenderer.ts) + * + * * [Default detailed renderer](https://github.com/laurent22/joplin/tree/dev/packages/lib/services/noteList/defaultMultiColumnsRenderer.ts) + * + * ## Screenshots: + * + * ### Top to bottom with title, date and body + * + * + * + * ### Left to right with thumbnails + * + * + * + * ### Top to bottom with editable title + * + * + * + * desktop + */ +export default class JoplinViewsNoteList { + private plugin_; + private store_; + constructor(plugin: Plugin, store: Store); + registerRenderer(renderer: ListRenderer): Promise; +} diff --git a/integrations/joplin/api/JoplinViewsPanels.d.ts b/integrations/joplin/api/JoplinViewsPanels.d.ts new file mode 100644 index 0000000..73259da --- /dev/null +++ b/integrations/joplin/api/JoplinViewsPanels.d.ts @@ -0,0 +1,89 @@ +import Plugin, { MessageListenerCallback } from '../Plugin'; +import { PluginStore } from '../ViewController'; +import { ViewHandle } from './types'; +/** + * Allows creating and managing view panels. View panels allow displaying any HTML + * content (within a webview) and updating it in real-time. For example it + * could be used to display a table of content for the active note, or + * display various metadata or graph. + * + * On desktop, view panels currently are displayed at the right of the sidebar, though can + * be moved with "View" > "Change application layout". + * + * On mobile, view panels are shown in a tabbed dialog that can be opened using a + * toolbar button. + * + * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/toc) + */ +export default class JoplinViewsPanels { + private store; + private plugin; + constructor(plugin: Plugin, store: PluginStore); + private controller; + /** + * Creates a new panel + */ + create(id: string): Promise; + /** + * Sets the panel webview HTML + */ + setHtml(handle: ViewHandle, html: string): Promise; + /** + * Adds and loads a new JS or CSS files into the panel. + */ + addScript(handle: ViewHandle, scriptPath: string): Promise; + /** + * Called when a message is sent from the webview (using postMessage). + * + * To post a message from the webview to the plugin use: + * + * ```javascript + * const response = await webviewApi.postMessage(message); + * ``` + * + * - `message` can be any JavaScript object, string or number + * - `response` is whatever was returned by the `onMessage` handler + * + * Using this mechanism, you can have two-way communication between the + * plugin and webview. + * + * See the [postMessage + * demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages) for more details. + * + */ + onMessage(handle: ViewHandle, callback: MessageListenerCallback): Promise; + /** + * Sends a message to the webview. + * + * The webview must have registered a message handler prior, otherwise the message is ignored. Use; + * + * ```javascript + * webviewApi.onMessage((message) => { ... }); + * ``` + * + * - `message` can be any JavaScript object, string or number + * + * The view API may have only one onMessage handler defined. + * This method is fire and forget so no response is returned. + * + * It is particularly useful when the webview needs to react to events emitted by the plugin or the joplin api. + */ + postMessage(handle: ViewHandle, message: any): void; + /** + * Shows the panel + */ + show(handle: ViewHandle, show?: boolean): Promise; + /** + * Hides the panel + */ + hide(handle: ViewHandle): Promise; + /** + * Tells whether the panel is visible or not + */ + visible(handle: ViewHandle): Promise; + /** + * Assuming that the current panel is an editor plugin view, returns + * whether the editor plugin view supports editing the current note. + */ + isActive(handle: ViewHandle): Promise; +} diff --git a/integrations/joplin/api/JoplinViewsToolbarButtons.d.ts b/integrations/joplin/api/JoplinViewsToolbarButtons.d.ts new file mode 100644 index 0000000..c1d12c2 --- /dev/null +++ b/integrations/joplin/api/JoplinViewsToolbarButtons.d.ts @@ -0,0 +1,17 @@ +import { ToolbarButtonLocation } from './types'; +import Plugin from '../Plugin'; +import { PluginStore } from '../ViewController'; +/** + * Allows creating and managing toolbar buttons. + * + * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/register_command) + */ +export default class JoplinViewsToolbarButtons { + private store; + private plugin; + constructor(plugin: Plugin, store: PluginStore); + /** + * Creates a new toolbar button and associate it with the given command. + */ + create(id: string, commandName: string, location: ToolbarButtonLocation): Promise; +} diff --git a/integrations/joplin/api/JoplinWindow.d.ts b/integrations/joplin/api/JoplinWindow.d.ts new file mode 100644 index 0000000..ed9b3cb --- /dev/null +++ b/integrations/joplin/api/JoplinWindow.d.ts @@ -0,0 +1,30 @@ +import Plugin from '../Plugin'; +type DispatchStore = { + dispatch: (action: { + type: string; + [k: string]: unknown; + }) => void; +}; +export default class JoplinWindow { + private store_; + constructor(_plugin: Plugin, store: DispatchStore); + /** + * Loads a chrome CSS file. It will apply to the window UI elements, except + * for the note viewer. It is the same as the "Custom stylesheet for + * Joplin-wide app styles" setting. See the [Load CSS Demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/load_css) + * for an example. + * + * desktop + */ + loadChromeCssFile(filePath: string): Promise; + /** + * Loads a note CSS file. It will apply to the note viewer, as well as any + * exported or printed note. It is the same as the "Custom stylesheet for + * rendered Markdown" setting. See the [Load CSS Demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/load_css) + * for an example. + * + * desktop + */ + loadNoteCssFile(filePath: string): Promise; +} +export {}; diff --git a/integrations/joplin/api/JoplinWorkspace.d.ts b/integrations/joplin/api/JoplinWorkspace.d.ts new file mode 100644 index 0000000..e89f7e6 --- /dev/null +++ b/integrations/joplin/api/JoplinWorkspace.d.ts @@ -0,0 +1,107 @@ +import Plugin from '../Plugin'; +import { PluginStore } from '../ViewController'; +import { FolderEntity, NoteEntity } from '../../database/types'; +import { Disposable, EditContextMenuFilterObject, FilterHandler } from './types'; +declare enum ItemChangeEventType { + Create = 1, + Update = 2, + Delete = 3 +} +interface ItemChangeEvent { + id: string; + event: ItemChangeEventType; +} +interface ResourceChangeEvent { + id: string; +} +interface NoteContentChangeEvent { + note: any; +} +interface NoteSelectionChangeEvent { + value: string[]; +} +interface NoteAlarmTriggerEvent { + noteId: string; +} +interface SyncCompleteEvent { + withErrors: boolean; +} +type WorkspaceEventHandler = (event: EventType) => void; +type ItemChangeHandler = WorkspaceEventHandler; +type SyncStartHandler = () => void; +type ResourceChangeHandler = WorkspaceEventHandler; +/** + * The workspace service provides access to all the parts of Joplin that + * are being worked on - i.e. the currently selected notes or notebooks as + * well as various related events, such as when a new note is selected, or + * when the note content changes. + * + * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins) + */ +export default class JoplinWorkspace { + private store; + private plugin; + constructor(plugin: Plugin, store: PluginStore); + /** + * Called when a new note or notes are selected. + */ + onNoteSelectionChange(callback: WorkspaceEventHandler): Promise; + /** + * Called when the content of a note changes. + * @deprecated Use `onNoteChange()` instead, which is reliably triggered whenever the note content, or any note property changes. + */ + onNoteContentChange(callback: WorkspaceEventHandler): Promise; + /** + * Called when the content of the current note changes. + */ + onNoteChange(handler: ItemChangeHandler): Promise; + /** + * Called when a resource is changed. Currently this handled will not be + * called when a resource is added or deleted. + */ + onResourceChange(handler: ResourceChangeHandler): Promise; + /** + * Called when an alarm associated with a to-do is triggered. + */ + onNoteAlarmTrigger(handler: WorkspaceEventHandler): Promise; + /** + * Called when the synchronisation process is starting. + */ + onSyncStart(handler: SyncStartHandler): Promise; + /** + * Called when the synchronisation process has finished. + */ + onSyncComplete(callback: WorkspaceEventHandler): Promise; + /** + * Called just before the editor context menu is about to open. Allows + * adding items to it. + * + * desktop + */ + filterEditorContextMenu(handler: FilterHandler): void; + /** + * Gets the currently selected note. Will be `null` if no note is selected. + * + * On desktop, this returns the selected note in the focused window. + */ + selectedNote(): Promise; + /** + * Gets the currently selected folder. In some cases, for example during + * search or when viewing a tag, no folder is actually selected in the user + * interface. In that case, that function would return the last selected + * folder. + */ + selectedFolder(): Promise; + /** + * Gets the IDs of the selected notes (can be zero, one, or many). Use the data API to retrieve information about these notes. + */ + selectedNoteIds(): Promise; + /** + * Gets the last hash (note section ID) from cross-note link targeting specific section. + * New hash is available after `onNoteSelectionChange()` is triggered. + * Example of cross-note link where `hello-world` is a hash: [Other Note Title](:/9bc9a5cb83f04554bf3fd3e41b4bb415#hello-world). + * Method returns empty value when a note was navigated with method other than cross-note link containing valid hash. + */ + selectedNoteHash(): Promise; +} +export {}; diff --git a/integrations/joplin/api/index.ts b/integrations/joplin/api/index.ts new file mode 100644 index 0000000..10ff655 --- /dev/null +++ b/integrations/joplin/api/index.ts @@ -0,0 +1,5 @@ +import type Joplin from './Joplin'; + +declare const joplin: Joplin; + +export default joplin; diff --git a/integrations/joplin/api/noteListType.d.ts b/integrations/joplin/api/noteListType.d.ts new file mode 100644 index 0000000..7862a63 --- /dev/null +++ b/integrations/joplin/api/noteListType.d.ts @@ -0,0 +1,254 @@ +import { Size } from './types'; +type ListRendererDatabaseDependency = 'folder.created_time' | 'folder.deleted_time' | 'folder.encryption_applied' | 'folder.encryption_cipher_text' | 'folder.icon' | 'folder.id' | 'folder.is_shared' | 'folder.master_key_id' | 'folder.parent_id' | 'folder.share_id' | 'folder.title' | 'folder.updated_time' | 'folder.user_created_time' | 'folder.user_data' | 'folder.user_updated_time' | 'folder.type_' | 'note.altitude' | 'note.application_data' | 'note.author' | 'note.body' | 'note.conflict_original_id' | 'note.created_time' | 'note.deleted_time' | 'note.encryption_applied' | 'note.encryption_cipher_text' | 'note.extracted_resource_ids' | 'note.id' | 'note.is_conflict' | 'note.is_locked' | 'note.is_shared' | 'note.is_todo' | 'note.latitude' | 'note.longitude' | 'note.markup_language' | 'note.master_key_id' | 'note.order' | 'note.parent_id' | 'note.share_id' | 'note.source' | 'note.source_application' | 'note.source_url' | 'note.title' | 'note.todo_completed' | 'note.todo_due' | 'note.updated_time' | 'note.user_created_time' | 'note.user_data' | 'note.user_updated_time' | 'note.type_'; +export declare enum ItemFlow { + TopToBottom = "topToBottom", + LeftToRight = "leftToRight" +} +export type RenderNoteView = Record; +export interface OnChangeEvent { + elementId: string; + value: any; + noteId: string; +} +export interface OnClickEvent { + elementId: string; +} +export type OnRenderNoteHandler = (props: any) => Promise; +export type OnChangeHandler = (event: OnChangeEvent) => Promise; +export type OnClickHandler = (event: OnClickEvent) => Promise; +/** + * Most of these are the built-in note properties, such as `note.title`, `note.todo_completed`, etc. + * complemented with special properties such as `note.isWatched`, to know if a note is currently + * opened in the external editor, and `note.tags` to get the list tags associated with the note. + * + * The `note.todoStatusText` property is a localised description of the to-do status (e.g. + * "to-do, incomplete"). If you include an `` for to-do items that would + * otherwise be unlabelled, consider adding `note.todoStatusText` as the checkbox's `aria-label`. + * + * ## Item properties + * + * The `item.*` properties are specific to the rendered item. The most important being + * `item.selected`, which you can use to display the selected note in a different way. + */ +export type ListRendererDependency = ListRendererDatabaseDependency | 'item.index' | 'item.selected' | 'item.size.height' | 'item.size.width' | 'note.checkboxes' | 'note.folder.title' | 'note.isWatched' | 'note.tags' | 'note.todoStatusText' | 'note.titleHtml'; +export type ListRendererItemValueTemplates = Record; +export declare const columnNames: readonly ["note.checkboxes", "note.folder.title", "note.is_todo", "note.latitude", "note.longitude", "note.source_url", "note.tags", "note.title", "note.todo_completed", "note.todo_due", "note.user_created_time", "note.user_updated_time"]; +export type ColumnName = typeof columnNames[number]; +export interface ListRenderer { + /** + * It must be unique to your plugin. + */ + id: string; + /** + * Can be top to bottom or left to right. Left to right gives you more + * option to set the size of the items since you set both its width and + * height. + */ + flow: ItemFlow; + /** + * Whether the renderer supports multiple columns. Applies only when `flow` + * is `topToBottom`. Defaults to `false`. + */ + multiColumns?: boolean; + /** + * The size of each item must be specified in advance for performance + * reasons, and cannot be changed afterwards. If the item flow is top to + * bottom, you only need to specify the item height (the width will be + * ignored). + */ + itemSize: Size; + /** + * The CSS is relative to the list item container. What will appear in the + * page is essentially `.note-list-item { YOUR_CSS; }`. It means you can use + * child combinator with guarantee it will only apply to your own items. In + * this example, the styling will apply to `.note-list-item > .content`: + * + * ```css + * > .content { + * padding: 10px; + * } + * ``` + * + * In order to get syntax highlighting working here, it's recommended + * installing an editor extension such as [es6-string-html VSCode + * extension](https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html) + */ + itemCss?: string; + /** + * List the dependencies that your plugin needs to render the note list + * items. Only these will be passed to your `onRenderNote` handler. Ensure + * that you do not add more than what you need since there is a performance + * penalty for each property. + */ + dependencies?: ListRendererDependency[]; + headerTemplate?: string; + headerHeight?: number; + onHeaderClick?: OnClickHandler; + /** + * This property is set differently depending on the `multiColumns` property. + * + * ## If `multiColumns` is `false` + * + * There is only one column and the template is used to render the entire row. + * + * This is the HTML template that will be used to render the note list item. This is a [Mustache + * template](https://github.com/janl/mustache.js) and it will receive the variable you return + * from `onRenderNote` as tags. For example, if you return a property named `formattedDate` from + * `onRenderNote`, you can insert it in the template using `Created date: {{formattedDate}}` + * + * ## If `multiColumns` is `true` + * + * Since there is multiple columns, this template will be used to render each note property + * within the row. For example if the current columns are the Updated and Title properties, this + * template will be called once to render the updated time and a second time to render the + * title. To display the current property, the generic `value` property is provided - it will be + * replaced at runtime by the actual note property. To render something different depending on + * the note property, use `itemValueTemplate`. A minimal example would be + * `{{value}}` which will simply render the current property inside a span tag. + * + * In order to get syntax highlighting working here, it's recommended installing an editor + * extension such as [es6-string-html VSCode + * extension](https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html) + * + * ## Default property rendering + * + * Certain properties are automatically rendered once inserted in the Mustache template. Those + * are in particular all the date-related fields, such as `note.user_updated_time` or + * `note.todo_completed`. Internally, those are timestamps in milliseconds, however when + * rendered we display them as date/time strings using the user's preferred time format. Another + * notable auto-rendered property is `note.title` which is going to include additional HTML, + * such as the search markers. + * + * If you do not want this default rendering behaviour, for example if you want to display the + * raw timestamps in milliseconds, you can simply return custom properties from + * `onRenderNote()`. For example: + * + * ```typescript + * onRenderNote: async (props: any) => { + * return { + * ...props, + * // Return the property under a different name + * updatedTimeMs: props.note.user_updated_time, + * } + * }, + * + * itemTemplate: // html + * ` + *
+ * Raw timestamp: {{updatedTimeMs}} + *
+ * `, + * + * ``` + * + * See + * `[https://github.com/laurent22/joplin/blob/dev/packages/lib/services/noteList/renderViewProps.ts](renderViewProps.ts)` + * for the list of properties that have a default rendering. + */ + itemTemplate: string; + /** + * This property applies only when `multiColumns` is `true`. It is used to render something + * different for each note property. + * + * This is a map of actual dependencies to templates - you only need to return something if the + * default, as specified in `template`, is not enough. + * + * Again you need to return a Mustache template and it will be combined with the `template` + * property to create the final template. For example if you return a property named + * `formattedDate` from `onRenderNote`, you can insert it in the template using + * `{{formattedDate}}`. This string will replace `{{value}}` in the `template` property. + * + * So if the template property is set to `{{value}}`, the final template will be + * `{{formattedDate}}`. + * + * The property would be set as so: + * + * ```javascript + * itemValueTemplates: { + * 'note.user_updated_time': '{{formattedDate}}', + * } + * ``` + */ + itemValueTemplates?: ListRendererItemValueTemplates; + /** + * This user-facing text is used for example in the View menu, so that your + * renderer can be selected. + */ + label: () => Promise; + /** + * This is where most of the real-time processing will happen. When a note + * is rendered for the first time and every time it changes, this handler + * receives the properties specified in the `dependencies` property. You can + * then process them, load any additional data you need, and once done you + * need to return the properties that are needed in the `itemTemplate` HTML. + * Again, to use the formatted date example, you could have such a renderer: + * + * ```typescript + * dependencies: [ + * 'note.title', + * 'note.created_time', + * ], + * + * itemTemplate: // html + * ` + *
+ * Title: {{note.title}}
+ * Date: {{formattedDate}} + *
+ * `, + * + * onRenderNote: async (props: any) => { + * const formattedDate = dayjs(props.note.created_time).format(); + * return { + * // Also return the props, so that note.title is available from the + * // template + * ...props, + * formattedDate, + * } + * }, + * ``` + */ + onRenderNote: OnRenderNoteHandler; + /** + * This handler allows adding some interactivity to the note renderer - whenever an input element + * within the item is changed (for example, when a checkbox is clicked, or a text input is + * changed), this `onChange` handler is going to be called. + * + * You can inspect `event.elementId` to know which element had some changes, and `event.value` + * to know the new value. `event.noteId` also tells you what note is affected, so that you can + * potentially apply changes to it. + * + * You specify the element ID, by setting a `data-id` attribute on the input. + * + * For example, if you have such a template: + * + * ```html + *
+ * + *
+ * ``` + * + * The event handler will receive an event with `elementId` set to `noteTitleInput`. + * + * ## Default event handlers + * + * Currently one click event is automatically handled: + * + * If there is a checkbox with a `data-id="todo-checkbox"` attribute is present, it is going to + * automatically toggle the note to-do "completed" status. + * + * For example this is what is used in the default list renderer: + * + * `` + */ + onChange?: OnChangeHandler; +} +export interface NoteListColumn { + name: ColumnName; + width: number; +} +export type NoteListColumns = NoteListColumn[]; +export declare const defaultWidth = 100; +export declare const defaultListColumns: () => NoteListColumns; +export {}; diff --git a/integrations/joplin/api/noteListType.ts b/integrations/joplin/api/noteListType.ts new file mode 100644 index 0000000..cf09294 --- /dev/null +++ b/integrations/joplin/api/noteListType.ts @@ -0,0 +1,327 @@ +/* eslint-disable multiline-comment-style */ + +import { Size } from './types'; + +// AUTO-GENERATED by generate-database-type +type ListRendererDatabaseDependency = 'folder.created_time' | 'folder.deleted_time' | 'folder.encryption_applied' | 'folder.encryption_cipher_text' | 'folder.icon' | 'folder.id' | 'folder.is_shared' | 'folder.master_key_id' | 'folder.parent_id' | 'folder.share_id' | 'folder.title' | 'folder.updated_time' | 'folder.user_created_time' | 'folder.user_data' | 'folder.user_updated_time' | 'folder.type_' | 'note.altitude' | 'note.application_data' | 'note.author' | 'note.body' | 'note.conflict_original_id' | 'note.created_time' | 'note.deleted_time' | 'note.encryption_applied' | 'note.encryption_cipher_text' | 'note.extracted_resource_ids' | 'note.id' | 'note.is_conflict' | 'note.is_locked' | 'note.is_shared' | 'note.is_todo' | 'note.latitude' | 'note.longitude' | 'note.markup_language' | 'note.master_key_id' | 'note.order' | 'note.parent_id' | 'note.share_id' | 'note.source' | 'note.source_application' | 'note.source_url' | 'note.title' | 'note.todo_completed' | 'note.todo_due' | 'note.updated_time' | 'note.user_created_time' | 'note.user_data' | 'note.user_updated_time' | 'note.type_'; +// AUTO-GENERATED by generate-database-type + +export enum ItemFlow { + TopToBottom = 'topToBottom', + LeftToRight = 'leftToRight', +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Plugin-API output map; values are heterogeneous (HTML strings, formatted numbers, booleans) and indexed dynamically per-plugin +export type RenderNoteView = Record; + +export interface OnChangeEvent { + elementId: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Plugin API: value depends on the input element type + value: any; + noteId: string; +} + +export interface OnClickEvent { + elementId: string; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Plugin-API callback; props is a per-renderer subset of the note shape declared via itemProps, indexed dynamically +export type OnRenderNoteHandler = (props: any)=> Promise; +export type OnChangeHandler = (event: OnChangeEvent)=> Promise; +export type OnClickHandler = (event: OnClickEvent)=> Promise; + +/** + * Most of these are the built-in note properties, such as `note.title`, `note.todo_completed`, etc. + * complemented with special properties such as `note.isWatched`, to know if a note is currently + * opened in the external editor, and `note.tags` to get the list tags associated with the note. + * + * The `note.todoStatusText` property is a localised description of the to-do status (e.g. + * "to-do, incomplete"). If you include an `` for to-do items that would + * otherwise be unlabelled, consider adding `note.todoStatusText` as the checkbox's `aria-label`. + * + * ## Item properties + * + * The `item.*` properties are specific to the rendered item. The most important being + * `item.selected`, which you can use to display the selected note in a different way. + */ +export type ListRendererDependency = + ListRendererDatabaseDependency | + 'item.index' | + 'item.selected' | + 'item.size.height' | + 'item.size.width' | + 'note.checkboxes' | + 'note.folder.title' | + 'note.isWatched' | + 'note.tags' | + 'note.todoStatusText' | + 'note.titleHtml'; + +export type ListRendererItemValueTemplates = Record; + +export const columnNames = [ + 'note.checkboxes', + 'note.folder.title', + 'note.is_todo', + 'note.latitude', + 'note.longitude', + 'note.source_url', + 'note.tags', + 'note.title', + 'note.todo_completed', + 'note.todo_due', + 'note.user_created_time', + 'note.user_updated_time', +] as const; + +export type ColumnName = typeof columnNames[number]; + +export interface ListRenderer { + /** + * It must be unique to your plugin. + */ + id: string; + + /** + * Can be top to bottom or left to right. Left to right gives you more + * option to set the size of the items since you set both its width and + * height. + */ + flow: ItemFlow; + + /** + * Whether the renderer supports multiple columns. Applies only when `flow` + * is `topToBottom`. Defaults to `false`. + */ + multiColumns?: boolean; + + /** + * The size of each item must be specified in advance for performance + * reasons, and cannot be changed afterwards. If the item flow is top to + * bottom, you only need to specify the item height (the width will be + * ignored). + */ + itemSize: Size; + + /** + * The CSS is relative to the list item container. What will appear in the + * page is essentially `.note-list-item { YOUR_CSS; }`. It means you can use + * child combinator with guarantee it will only apply to your own items. In + * this example, the styling will apply to `.note-list-item > .content`: + * + * ```css + * > .content { + * padding: 10px; + * } + * ``` + * + * In order to get syntax highlighting working here, it's recommended + * installing an editor extension such as [es6-string-html VSCode + * extension](https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html) + */ + itemCss?: string; + + /** + * List the dependencies that your plugin needs to render the note list + * items. Only these will be passed to your `onRenderNote` handler. Ensure + * that you do not add more than what you need since there is a performance + * penalty for each property. + */ + dependencies?: ListRendererDependency[]; + + headerTemplate?: string; + headerHeight?: number; + onHeaderClick?: OnClickHandler; + + /** + * This property is set differently depending on the `multiColumns` property. + * + * ## If `multiColumns` is `false` + * + * There is only one column and the template is used to render the entire row. + * + * This is the HTML template that will be used to render the note list item. This is a [Mustache + * template](https://github.com/janl/mustache.js) and it will receive the variable you return + * from `onRenderNote` as tags. For example, if you return a property named `formattedDate` from + * `onRenderNote`, you can insert it in the template using `Created date: {{formattedDate}}` + * + * ## If `multiColumns` is `true` + * + * Since there is multiple columns, this template will be used to render each note property + * within the row. For example if the current columns are the Updated and Title properties, this + * template will be called once to render the updated time and a second time to render the + * title. To display the current property, the generic `value` property is provided - it will be + * replaced at runtime by the actual note property. To render something different depending on + * the note property, use `itemValueTemplate`. A minimal example would be + * `{{value}}` which will simply render the current property inside a span tag. + * + * In order to get syntax highlighting working here, it's recommended installing an editor + * extension such as [es6-string-html VSCode + * extension](https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html) + * + * ## Default property rendering + * + * Certain properties are automatically rendered once inserted in the Mustache template. Those + * are in particular all the date-related fields, such as `note.user_updated_time` or + * `note.todo_completed`. Internally, those are timestamps in milliseconds, however when + * rendered we display them as date/time strings using the user's preferred time format. Another + * notable auto-rendered property is `note.title` which is going to include additional HTML, + * such as the search markers. + * + * If you do not want this default rendering behaviour, for example if you want to display the + * raw timestamps in milliseconds, you can simply return custom properties from + * `onRenderNote()`. For example: + * + * ```typescript + * onRenderNote: async (props: any) => { + * return { + * ...props, + * // Return the property under a different name + * updatedTimeMs: props.note.user_updated_time, + * } + * }, + * + * itemTemplate: // html + * ` + *
+ * Raw timestamp: {{updatedTimeMs}} + *
+ * `, + * + * ``` + * + * See + * `[https://github.com/laurent22/joplin/blob/dev/packages/lib/services/noteList/renderViewProps.ts](renderViewProps.ts)` + * for the list of properties that have a default rendering. + */ + itemTemplate: string; + + /** + * This property applies only when `multiColumns` is `true`. It is used to render something + * different for each note property. + * + * This is a map of actual dependencies to templates - you only need to return something if the + * default, as specified in `template`, is not enough. + * + * Again you need to return a Mustache template and it will be combined with the `template` + * property to create the final template. For example if you return a property named + * `formattedDate` from `onRenderNote`, you can insert it in the template using + * `{{formattedDate}}`. This string will replace `{{value}}` in the `template` property. + * + * So if the template property is set to `{{value}}`, the final template will be + * `{{formattedDate}}`. + * + * The property would be set as so: + * + * ```javascript + * itemValueTemplates: { + * 'note.user_updated_time': '{{formattedDate}}', + * } + * ``` + */ + itemValueTemplates?: ListRendererItemValueTemplates; + + /** + * This user-facing text is used for example in the View menu, so that your + * renderer can be selected. + */ + label: ()=> Promise; + + /** + * This is where most of the real-time processing will happen. When a note + * is rendered for the first time and every time it changes, this handler + * receives the properties specified in the `dependencies` property. You can + * then process them, load any additional data you need, and once done you + * need to return the properties that are needed in the `itemTemplate` HTML. + * Again, to use the formatted date example, you could have such a renderer: + * + * ```typescript + * dependencies: [ + * 'note.title', + * 'note.created_time', + * ], + * + * itemTemplate: // html + * ` + *
+ * Title: {{note.title}}
+ * Date: {{formattedDate}} + *
+ * `, + * + * onRenderNote: async (props: any) => { + * const formattedDate = dayjs(props.note.created_time).format(); + * return { + * // Also return the props, so that note.title is available from the + * // template + * ...props, + * formattedDate, + * } + * }, + * ``` + */ + onRenderNote: OnRenderNoteHandler; + + /** + * This handler allows adding some interactivity to the note renderer - whenever an input element + * within the item is changed (for example, when a checkbox is clicked, or a text input is + * changed), this `onChange` handler is going to be called. + * + * You can inspect `event.elementId` to know which element had some changes, and `event.value` + * to know the new value. `event.noteId` also tells you what note is affected, so that you can + * potentially apply changes to it. + * + * You specify the element ID, by setting a `data-id` attribute on the input. + * + * For example, if you have such a template: + * + * ```html + *
+ * + *
+ * ``` + * + * The event handler will receive an event with `elementId` set to `noteTitleInput`. + * + * ## Default event handlers + * + * Currently one click event is automatically handled: + * + * If there is a checkbox with a `data-id="todo-checkbox"` attribute is present, it is going to + * automatically toggle the note to-do "completed" status. + * + * For example this is what is used in the default list renderer: + * + * `` + */ + onChange?: OnChangeHandler; +} + +export interface NoteListColumn { + name: ColumnName; + width: number; +} + +export type NoteListColumns = NoteListColumn[]; + +export const defaultWidth = 100; + +export const defaultListColumns = () => { + const columns: NoteListColumns = [ + { + name: 'note.is_todo', + width: 30, + }, + { + name: 'note.user_updated_time', + width: defaultWidth, + }, + { + name: 'note.title', + width: 0, + }, + ]; + + return columns; +}; diff --git a/integrations/joplin/api/types.ts b/integrations/joplin/api/types.ts new file mode 100644 index 0000000..58c1614 --- /dev/null +++ b/integrations/joplin/api/types.ts @@ -0,0 +1,1049 @@ +/* eslint-disable multiline-comment-style */ + +// ================================================================= +// Command API types +// ================================================================= + +export interface Command { + /** + * Name of command - must be globally unique + */ + name: string; + + /** + * Label to be displayed on menu items or keyboard shortcut editor for example. + * If it is missing, it's assumed it's a private command, to be called programmatically only. + * In that case the command will not appear in the shortcut editor or command panel, and logically + * should not be used as a menu item. + */ + label?: string; + + /** + * Icon to be used on toolbar buttons for example + */ + iconName?: string; + + /** + * Code to be ran when the command is executed. It may return a result. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Plugin commands accept arbitrary args and return arbitrary results; this is part of the public plugin API + execute(...args: any[]): Promise; + + /** + * Defines whether the command should be enabled or disabled, which in turns + * affects the enabled state of any associated button or menu item. + * + * The condition should be expressed as a "when-clause" (as in Visual Studio + * Code). It's a simple boolean expression that evaluates to `true` or + * `false`. It supports the following operators: + * + * Operator | Symbol | Example + * -- | -- | -- + * Equality | == | "editorType == markdown" + * Inequality | != | "currentScreen != config" + * Or | \|\| | "noteIsTodo \|\| noteTodoCompleted" + * And | && | "oneNoteSelected && !inConflictFolder" + * + * Joplin, unlike VSCode, also supports parentheses, which allows creating + * more complex expressions such as `cond1 || (cond2 && cond3)`. Only one + * level of parentheses is possible (nested ones aren't supported). + * + * Currently the supported context variables aren't documented, but you can + * find the list below: + * + * - [Global When Clauses](https://github.com/laurent22/joplin/blob/dev/packages/lib/services/commands/stateToWhenClauseContext.ts) + * - [Desktop app When Clauses](https://github.com/laurent22/joplin/blob/dev/packages/app-desktop/services/commands/stateToWhenClauseContext.ts) + * + * Note: Commands are enabled by default unless you use this property. + */ + enabledCondition?: string; +} + +// ================================================================= +// Interop API types +// ================================================================= + +export enum FileSystemItem { + File = 'file', + Directory = 'directory', +} + +export enum ImportModuleOutputFormat { + Markdown = 'md', + Html = 'html', +} + +/** + * Used to implement a module to export data from Joplin. [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/json_export) for an example. + * + * In general, all the event handlers you'll need to implement take a `context` object as a first argument. This object will contain the export or import path as well as various optional properties, such as which notes or notebooks need to be exported. + * + * To get a better sense of what it will contain it can be useful to print it using `console.info(context)`. + */ +export interface ExportModule { + /** + * The format to be exported, eg "enex", "jex", "json", etc. + */ + format: string; + + /** + * The description that will appear in the UI, for example in the menu item. + */ + description: string; + + /** + * Whether the module will export a single file or multiple files in a directory. It affects the open dialog that will be presented to the user when using your exporter. + */ + target: FileSystemItem; + + /** + * Only applies to single file exporters or importers + * It tells whether the format can package multiple notes into one file. + * For example JEX or ENEX can, but HTML cannot. + */ + isNoteArchive: boolean; + + /** + * The extensions of the files exported by your module. For example, it is `["htm", "html"]` for the HTML module, and just `["jex"]` for the JEX module. + */ + fileExtensions?: string[]; + + /** + * Called when the export process starts. + */ + onInit(context: ExportContext): Promise; + + /** + * Called when an item needs to be processed. An "item" can be any Joplin object, such as a note, a folder, a notebook, etc. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Plugin API: item type depends on itemType (NoteEntity, FolderEntity, ResourceEntity, etc.); plugin authors discriminate at use site + onProcessItem(context: ExportContext, itemType: number, item: any): Promise; + + /** + * Called when a resource file needs to be exported. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- See onProcessItem; resource here is a ResourceEntity but the plugin API keeps it loosely typed + onProcessResource(context: ExportContext, resource: any, filePath: string): Promise; + + /** + * Called when the export process is done. + */ + onClose(context: ExportContext): Promise; +} + +export interface ImportModule { + /** + * The format to be exported, eg "enex", "jex", "json", etc. + */ + format: string; + + /** + * The description that will appear in the UI, for example in the menu item. + */ + description: string; + + /** + * Only applies to single file exporters or importers + * It tells whether the format can package multiple notes into one file. + * For example JEX or ENEX can, but HTML cannot. + */ + isNoteArchive: boolean; + + /** + * The type of sources that are supported by the module. Tells whether the module can import files or directories or both. + */ + sources: FileSystemItem[]; + + /** + * Tells the file extensions of the exported files. + */ + fileExtensions?: string[]; + + /** + * Tells the type of notes that will be generated, either HTML or Markdown (default). + */ + outputFormat?: ImportModuleOutputFormat; + + /** + * Called when the import process starts. There is only one event handler within which you should import the complete data. + */ + onExec(context: ImportContext): Promise; +} + +export interface ExportOptions { + format?: string; + path?: string; + sourceFolderIds?: string[]; + sourceNoteIds?: string[]; + // modulePath?: string; + target?: FileSystemItem; +} + +export interface ExportContext { + destPath: string; + options: ExportOptions; + + /** + * You can attach your own custom data using this property - it will then be passed to each event handler, allowing you to keep state from one event to the next. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Plugin API: userData is arbitrary per-plugin state + userData?: any; +} + +export interface ImportContext { + sourcePath: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Plugin API: import options are arbitrary per-importer + options: any; + warnings: string[]; +} + +// ================================================================= +// Misc types +// ================================================================= + +export interface Script { + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Plugin API: event payload shape depends on the host context + onStart?(event: any): Promise; +} + +export interface Disposable { + // dispose():void; +} + +export enum ModelType { + Note = 1, + Folder = 2, + Setting = 3, + Resource = 4, + Tag = 5, + NoteTag = 6, + Search = 7, + Alarm = 8, + MasterKey = 9, + ItemChange = 10, + NoteResource = 11, + ResourceLocalState = 12, + Revision = 13, + Migration = 14, + SmartFilter = 15, + Command = 16, +} + +export interface VersionInfo { + version: string; + profileVersion: number; + syncVersion: number; + + platform: 'desktop'|'mobile'; +} + +// ================================================================= +// Menu types +// ================================================================= + +export interface CreateMenuItemOptions { + accelerator: string; +} + +export enum MenuItemLocation { + File = 'file', + Edit = 'edit', + View = 'view', + Note = 'note', + Tools = 'tools', + Help = 'help', + + /** + * @deprecated Do not use - same as NoteListContextMenu + */ + Context = 'context', + + // If adding an item here, don't forget to update isContextMenuItemLocation() + + /** + * When a command is called from the note list context menu, the + * command will receive the following arguments: + * + * - `noteIds:string[]`: IDs of the notes that were right-clicked on. + */ + NoteListContextMenu = 'noteListContextMenu', + + EditorContextMenu = 'editorContextMenu', + + /** + * When a command is called from a folder context menu, the + * command will receive the following arguments: + * + * - `folderId:string`: ID of the folder that was right-clicked on + */ + FolderContextMenu = 'folderContextMenu', + + /** + * When a command is called from a tag context menu, the + * command will receive the following arguments: + * + * - `tagId:string`: ID of the tag that was right-clicked on + */ + TagContextMenu = 'tagContextMenu', +} + +export function isContextMenuItemLocation(location: MenuItemLocation): boolean { + return [ + MenuItemLocation.Context, + MenuItemLocation.NoteListContextMenu, + MenuItemLocation.EditorContextMenu, + MenuItemLocation.FolderContextMenu, + MenuItemLocation.TagContextMenu, + ].includes(location); +} + +export interface MenuItem { + /** + * Command that should be associated with the menu item. All menu item should + * have a command associated with them unless they are a sub-menu. + */ + commandName?: string; + + /** + * Arguments that should be passed to the command. They will be as rest + * parameters. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Plugin API: command args depend on the command + commandArgs?: any[]; + + /** + * Set to "separator" to create a divider line + */ + type?: ('normal' | 'separator' | 'submenu' | 'checkbox' | 'radio'); + + /** + * Accelerator associated with the menu item + */ + accelerator?: string; + + /** + * Menu items that should appear below this menu item. Allows creating a menu tree. + */ + submenu?: MenuItem[]; + + /** + * Menu item label. If not specified, the command label will be used instead. + */ + label?: string; +} + +// ================================================================= +// View API types +// ================================================================= + +export interface ButtonSpec { + id: ButtonId; + title?: string; + onClick?(): void; +} + +export type ButtonId = string; + +export enum ToolbarButtonLocation { + /** + * This toolbar in the top right corner of the application. It applies to the note as a whole, including its metadata. + * + * desktop + */ + NoteToolbar = 'noteToolbar', + + /** + * This toolbar is right above the text editor. It applies to the note body only. + */ + EditorToolbar = 'editorToolbar', +} + +export type ViewHandle = string; + +export interface EditorCommand { + name: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Plugin API: command value depends on the command + value?: any; +} + +export interface DialogResult { + id: ButtonId; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Plugin API: form data shape depends on the dialog + formData?: any; +} + +export enum ToastType { + Info = 'info', + Success = 'success', + Error = 'error', +} + +export interface Toast { + message: string; + type?: ToastType; + duration?: number; + timestamp?: number; +} + +export interface Size { + width?: number; + height?: number; +} + +export interface Rectangle { + x?: number; + y?: number; + width?: number; + height?: number; +} + +export interface EditorUpdateEvent { + newBody: string; + noteId: string; +} +export type UpdateCallback = (event: EditorUpdateEvent)=> Promise; + + +export interface ActivationCheckEvent { + handle: ViewHandle; + noteId: string; +} +export type ActivationCheckCallback = (event: ActivationCheckEvent)=> Promise; + +/** + * Required callbacks for creating an editor plugin. + */ +export interface EditorPluginCallbacks { + /** + * Emitted when the editor can potentially be activated - this is for example when the current + * note is changed, or when the application is opened. At that point you should check the + * current note and decide whether your editor should be activated or not. If it should, return + * `true`, otherwise return `false`. + */ + onActivationCheck: ActivationCheckCallback; + + /** + * Emitted when an editor view is created. This happens, for example, when a new window containing + * a new editor is created. + * + * This callback should set the editor plugin's HTML using `editors.setHtml`, add scripts to the editor + * with `editors.addScript`, and optionally listen for external changes using `editors.onUpdate`. + */ + onSetup: (handle: ViewHandle)=> Promise; +} + +export type VisibleHandler = ()=> Promise; + +/** + * Identifies the type of element that was right-clicked in the editor context menu. + */ +export enum ContextMenuItemType { + None = '', + Image = 'image', + Resource = 'resource', + Text = 'text', + Link = 'link', + NoteLink = 'noteLink', +} + +export interface EditContextMenuFilterObject { + items: MenuItem[]; + /** + * Context about what was right-clicked. Plugins should use this instead of + * checking the editor cursor position, as the cursor may not reflect the + * actual click location. + */ + context?: { + resourceId?: string; + itemType?: ContextMenuItemType; + textToCopy?: string; + }; +} + +export interface EditorActivationCheckFilterObject { + effectiveNoteId: string; + windowId: string; + activatedEditors: { + pluginId: string; + viewId: string; + isActive: boolean; + }[]; +} + +export type FilterHandler = (object: T)=> Promise; + +export type CommandArgument = string|number|object|boolean|null; + +export interface MenuTemplateItem { + label?: string; + command?: string; + commandArgs?: CommandArgument[]; +} + +export interface WebviewApi { + postMessage: (message: object)=> unknown; + onMessage: (message: object)=> void; + menuPopupFromTemplate: (template: MenuTemplateItem[])=> void; +} + +// ================================================================= +// Settings types +// ================================================================= + +export enum SettingItemType { + Int = 1, + String = 2, + Bool = 3, + Array = 4, + Object = 5, + Button = 6, +} + +export enum SettingItemSubType { + FilePathAndArgs = 'file_path_and_args', + FilePath = 'file_path', // Not supported on mobile! + DirectoryPath = 'directory_path', // Not supported on mobile! +} + +export enum AppType { + Desktop = 'desktop', + Mobile = 'mobile', + Cli = 'cli', +} + +export enum SettingStorage { + Database = 1, + File = 2, +} + +// Redefine a simplified interface to mask internal details +// and to remove function calls as they would have to be async. +export interface SettingItem { + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Setting values are heterogeneous per setting (string/number/bool/Record/Array); plugin authors narrow at use site + value: any; + type: SettingItemType; + + /** + * Currently only used to display a file or directory selector. Always set + * `type` to `SettingItemType.String` when using this property. + */ + subType?: SettingItemSubType; + + label: string; + description?: string; + + /** + * A public setting will appear in the Configuration screen and will be + * modifiable by the user. A private setting however will not appear there, + * and can only be changed programmatically. You may use this to store some + * values that you do not want to directly expose. + */ + public: boolean; + + /** + * You would usually set this to a section you would have created + * specifically for the plugin. + */ + section?: string; + + /** + * To create a setting with multiple options, set this property to `true`. + * That setting will render as a dropdown list in the configuration screen. + */ + isEnum?: boolean; + + /** + * This property is required when `isEnum` is `true`. In which case, it + * should contain a map of value => label. + */ + options?: Record; + + /** + * Reserved property. Not used at the moment. + */ + appTypes?: AppType[]; + + /** + * Set this to `true` to store secure data, such as passwords. Any such + * setting will be stored in the system keychain if one is available. + */ + secure?: boolean; + + /** + * An advanced setting will be moved under the "Advanced" button in the + * config screen. + */ + advanced?: boolean; + + /** + * Set the min, max and step values if you want to restrict an int setting + * to a particular range. + */ + minimum?: number; + maximum?: number; + step?: number; + + /** + * Either store the setting in the database or in settings.json. Defaults to database. + */ + storage?: SettingStorage; +} + +export interface SettingSection { + label: string; + iconName?: string; + description?: string; + name?: string; +} + +// ================================================================= +// Data API types +// ================================================================= + +/** + * An array of at least one element and at most three elements. + * + * - **[0]**: Resource name (eg. "notes", "folders", "tags", etc.) + * - **[1]**: (Optional) Resource ID. + * - **[2]**: (Optional) Resource link. + */ +export type Path = string[]; + +// ================================================================= +// Clipboard API types +// ================================================================= + +/** + * Represents content that can be written to the clipboard in multiple formats. + */ +export interface ClipboardContent { + /** + * Plain text representation of the content + */ + text?: string; + + /** + * HTML representation of the content + */ + html?: string; + + /** + * Image in [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) format + */ + image?: string; +} + +// ================================================================= +// Content Script types +// ================================================================= + +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Plugin API: messages between content scripts and plugins are arbitrary serialisable data +export type PostMessageHandler = (message: any)=> Promise; + +/** + * When a content script is initialised, it receives a `context` object. + */ +export interface ContentScriptContext { + /** + * The plugin ID that registered this content script + */ + pluginId: string; + + /** + * The content script ID, which may be necessary to post messages + */ + contentScriptId: string; + + /** + * Can be used by CodeMirror content scripts to post a message to the plugin + */ + postMessage: PostMessageHandler; +} + +export interface ContentScriptModuleLoadedEvent { + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Plugin API: userData is arbitrary per-plugin state + userData?: any; +} + +export interface ContentScriptModule { + onLoaded?: (event: ContentScriptModuleLoadedEvent)=> void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Plugin entry point returns a plugin-specific module (markdown-it plugin, CodeMirror plugin, etc.); shape varies per content script type + plugin: ()=> any; + assets?: ()=> void; +} + +export interface MarkdownItContentScriptModule extends Omit { + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- markdown-it and options are external library types not imported here; plugin authors annotate concretely + plugin: (markdownIt: any, options: any)=> any; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- CodeMirror command callbacks accept and return arbitrary values; matches CM6 Command type +type EditorCommandCallback = (...args: any[])=> any; + +export interface CodeMirrorControl { + /** Points to a CodeMirror 6 EditorView instance. */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- CM6 EditorView is an external library type not imported here + editor: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- CM6 module namespace; types come from the external library + cm6: any; + + /** `extension` should be a [CodeMirror 6 extension](https://codemirror.net/docs/ref/#state.Extension). */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- CM6 Extension type comes from the external library + addExtension(extension: any|any[]): void; + + supportsCommand(name: string): boolean; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- See EditorCommandCallback + execCommand(name: string, ...args: any[]): any; + registerCommand(name: string, callback: EditorCommandCallback): void; + + joplinExtensions: { + /** + * Returns a [CodeMirror 6 extension](https://codemirror.net/docs/ref/#state.Extension) that + * registers the given [CompletionSource](https://codemirror.net/docs/ref/#autocomplete.CompletionSource). + * + * Use this extension rather than the built-in CodeMirror [`autocompletion`](https://codemirror.net/docs/ref/#autocomplete.autocompletion) + * if you don't want to use [languageData-based autocompletion](https://codemirror.net/docs/ref/#autocomplete.autocompletion^config.override). + * + * Using `autocompletion({ override: [ ... ]})` causes errors when done by multiple plugins. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- CM6 CompletionSource and Extension types come from the external library + completionSource(completionSource: any): any; + + /** + * Creates an extension that enables or disables [`languageData`-based autocompletion](https://codemirror.net/docs/ref/#autocomplete.autocompletion^config.override). + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- See completionSource above + enableLanguageDataAutocomplete: { of: (enabled: boolean)=> any }; + + /** + * A CodeMirror [facet](https://codemirror.net/docs/ref/#state.EditorState.facet) that contains + * the ID of the note currently open in the editor. + * + * Access the value of this facet using + * ```ts + * const noteIdFacet = editorControl.joplinExtensions.noteIdFacet; + * const editorState = editorControl.editor.state; + * const noteId = editorState.facet(noteIdFacet); + * ``` + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- No better type available + noteIdFacet: any; + /** + * A CodeMirror [StateEffect](https://codemirror.net/docs/ref/#state.StateEffect) that is + * included in a [Transaction](https://codemirror.net/docs/ref/#state.Transaction) when the + * note ID changes. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- No better type available + setNoteIdEffect: any; + }; +} + +export interface MarkdownEditorContentScriptModule extends Omit { + plugin: (editorControl: CodeMirrorControl)=> void; +} + +export enum ContentScriptType { + /** + * Registers a new Markdown-It plugin, which should follow the template + * below. + * + * ```javascript + * module.exports = { + * default: function(context) { + * return { + * plugin: function(markdownIt, pluginOptions) { + * // ... + * }, + * assets: { + * // ... + * }, + * } + * } + * } + * ``` + * + * See [the + * demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script) + * for a simple Markdown-it plugin example. + * + * ## Exported members + * + * - The `context` argument is currently unused but could be used later on + * to provide access to your own plugin so that the content script and + * plugin can communicate. + * + * - The **required** `plugin` key is the actual Markdown-It plugin - check + * the [official doc](https://github.com/markdown-it/markdown-it) for more + * information. + * + * - Using the **optional** `assets` key you may specify assets such as JS + * or CSS that should be loaded in the rendered HTML document. Check for + * example the Joplin [Mermaid + * plugin](https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml/rules/mermaid.ts) + * to see how the data should be structured. + * + * ## Supporting the Rich Text Editor + * + * Joplin's Rich Text Editor works with rendered HTML, which is converted back + * to markdown when saving. To prevent the original markdown for your plugin from + * being lost, Joplin needs additional metadata. + * + * To provide this, + * 1. Wrap the HTML generated by your plugin in an element with class `joplin-editable`. + * For example, + * ```html + *
+ * ...your html... + *
+ * ``` + * 2. Add a child with class `joplin-source` that contains the original markdown that + * was rendered by your plugin. Include `data-joplin-source-open`, `data-joplin-source-close`, + * and `data-joplin-language` attributes. + * For example, if your plugin rendered the following code block, + * ```` + * ```foo + * ... original source here ... + * ``` + * ```` + * then it should render to + * ```html + *
+ *
 ... original source here ... 
+ * ... rendered HTML here ... + *
+ * ``` + * + * See [the demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script) + * for a complete example. + * + * ## Getting the settings from the renderer + * + * You can access your plugin settings from the renderer by calling + * `pluginOptions.settingValue("your-setting-key')`. + * + * ## Posting messages from the content script to your plugin + * + * The application provides the following function to allow executing + * commands from the rendered HTML code: + * + * ```javascript + * const response = await webviewApi.postMessage(contentScriptId, message); + * ``` + * + * - `contentScriptId` is the ID you've defined when you registered the + * content script. You can retrieve it from the + * {@link ContentScriptContext | context}. + * - `message` can be any basic JavaScript type (number, string, plain + * object), but it cannot be a function or class instance. + * + * When you post a message, the plugin can send back a `response` thus + * allowing two-way communication: + * + * ```javascript + * await joplin.contentScripts.onMessage(contentScriptId, (message) => { + * // Process message + * return response; // Can be any object, string or number + * }); + * ``` + * + * See {@link JoplinContentScripts.onMessage} for more details, as well as + * the [postMessage + * demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages). + * + * ## Registering an existing Markdown-it plugin + * + * To include a regular Markdown-It plugin, that doesn't make use of any + * Joplin-specific features, you would simply create a file such as this: + * + * ```javascript + * module.exports = { + * default: function(context) { + * return { + * plugin: require('markdown-it-toc-done-right'); + * } + * } + * } + * ``` + */ + MarkdownItPlugin = 'markdownItPlugin', + + /** + * Registers a new CodeMirror plugin, which should follow the template + * below. + * + * ```javascript + * module.exports = { + * default: function(context) { + * return { + * plugin: function(CodeMirror) { + * // ... + * }, + * codeMirrorResources: [], + * codeMirrorOptions: { + * // ... + * }, + * assets: { + * // ... + * }, + * } + * } + * } + * ``` + * + * - The `context` argument allows communicating with other parts of + * your plugin (see below). + * + * - The `plugin` key is your CodeMirror plugin. This is where you can + * register new commands with CodeMirror or interact with the CodeMirror + * instance as needed. + * + * - **CodeMirror 5 only**: The `codeMirrorResources` key is an array of CodeMirror resources that + * will be loaded and attached to the CodeMirror module. These are made up + * of addons, keymaps, and modes. For example, for a plugin that want's to + * enable clojure highlighting in code blocks. `codeMirrorResources` would + * be set to `['mode/clojure/clojure']`. + * This field is ignored on mobile and when the desktop beta editor is enabled. + * + * - **CodeMirror 5 only**: The `codeMirrorOptions` key contains all the + * [CodeMirror](https://codemirror.net/doc/manual.html#config) options + * that will be set or changed by this plugin. New options can alse be + * declared via + * [`CodeMirror.defineOption`](https://codemirror.net/doc/manual.html#defineOption), + * and then have their value set here. For example, a plugin that enables + * line numbers would set `codeMirrorOptions` to `{'lineNumbers': true}`. + * + * - Using the **optional** `assets` key you may specify **only** CSS assets + * that should be loaded in the rendered HTML document. Check for example + * the Joplin [Mermaid + * plugin](https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml/rules/mermaid.ts) + * to see how the data should be structured. + * + * One of the `plugin`, `codeMirrorResources`, or `codeMirrorOptions` keys + * must be provided for the plugin to be valid. Having multiple or all + * provided is also okay. + * + * See also: + * - The [demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/codemirror_content_script) + * for an example of all these keys being used in one plugin. + * - See [the editor plugin tutorial](https://joplinapp.org/help/api/tutorials/cm6_plugin) + * for how to develop a plugin for the mobile editor and the desktop beta markdown editor. + * + * ## Posting messages from the content script to your plugin + * + * In order to post messages to the plugin, you can use the postMessage + * function passed to the {@link ContentScriptContext | context}. + * + * ```javascript + * const response = await context.postMessage('messageFromCodeMirrorContentScript'); + * ``` + * + * When you post a message, the plugin can send back a `response` thus + * allowing two-way communication: + * + * ```javascript + * await joplin.contentScripts.onMessage(contentScriptId, (message) => { + * // Process message + * return response; // Can be any object, string or number + * }); + * ``` + * + * See {@link JoplinContentScripts.onMessage} for more details, as well as + * the [postMessage + * demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages). + * + */ + CodeMirrorPlugin = 'codeMirrorPlugin', +} + +// ================================================================= +// AI API types +// ================================================================= + +/** + * Role of a chat message. `system` messages set the assistant's behaviour, + * `user` messages come from the end user, and `assistant` messages are model + * responses fed back as conversation history. + */ +export type ChatMessageRole = 'system' | 'user' | 'assistant'; + +/** + * A single message in a chat conversation. + */ +export interface ChatMessage { + role: ChatMessageRole; + content: string; +} + +/** + * Optional parameters for a chat call. The active model and provider are + * controlled by the user in the Joplin settings — plugins cannot pick a model. + */ +export interface ChatOptions { + /** Sampling temperature, typically between 0 and 1. Provider default if omitted. */ + temperature?: number; + /** Maximum number of tokens to generate. Provider default if omitted. */ + maxTokens?: number; +} + +/** + * Relevance preset for semantic search. Maps internally to model-specific + * `(k, minScore)` tuning — the preset is the public contract so plugins keep + * working when the bundled embedding model changes. + */ +export type SearchRelevance = 'strict' | 'normal' | 'loose'; + +/** + * Where to look for matches. + * + * - `all`: every indexed note (default). + * - `note`: a single note (rarely useful directly — mainly an internal + * building block). + * - `folder`: all notes in the given folder (a "notebook" in the UI). + * - `tag`: all notes tagged with the given tag. + * + * Trashed and conflict notes are always excluded. + */ +export type SearchScope = + | { type: 'all' } + | { type: 'note'; noteId: string } + | { type: 'folder'; folderId: string } + | { type: 'tag'; tagId: string }; + +/** + * What to search for: free text (embedded internally), or an existing note + * whose stored chunks are reused as the query — useful for "related notes", + * tag suggestions, and graph-style use cases without a second embedding pass. + */ +export type SearchQuery = + | { text: string } + | { noteId: string }; + +/** + * Parameters for {@link JoplinAi.search}. + */ +export interface SearchOptions { + query: SearchQuery; + scope?: SearchScope; + relevance?: SearchRelevance; +} + +/** + * A single hit from {@link JoplinAi.search}. + */ +export interface SearchResult { + noteId: string; + chunkIndex: number; + chunkText: string; + /** + * Cosine similarity in `[0, 1]`. Higher means more similar. Plugins should + * use this for ranking but not as an absolute threshold — that's what the + * `relevance` preset is for. + */ + score: number; +} + diff --git a/integrations/joplin/package.json b/integrations/joplin/package.json new file mode 100644 index 0000000..9864236 --- /dev/null +++ b/integrations/joplin/package.json @@ -0,0 +1,31 @@ +{ + "name": "joplin-plugin-butterfly", + "version": "0.1.0", + "scripts": { + "dist": "webpack --env joplin-plugin-config=buildMain && webpack --env joplin-plugin-config=buildExtraScripts && webpack --env joplin-plugin-config=createArchive", + "updateVersion": "webpack --env joplin-plugin-config=updateVersion" + }, + "license": "Apache-2.0", + "keywords": [ + "joplin-plugin" + ], + "files": [ + "publish" + ], + "dependencies": { + "@linwood/butterfly-integration-shared": "workspace:*" + }, + "devDependencies": { + "@types/node": "^18.7.13", + "chalk": "^4.1.0", + "copy-webpack-plugin": "^14.0.0", + "fs-extra": "^10.1.0", + "glob": "^8.0.3", + "tar": "^7.5.22", + "ts-loader": "^9.3.1", + "typescript": "^4.8.2", + "webpack": "^5.74.0", + "webpack-cli": "^4.10.0", + "uuid": "^14.0.2" + } +} diff --git a/integrations/joplin/plugin.config.json b/integrations/joplin/plugin.config.json new file mode 100644 index 0000000..31c91b5 --- /dev/null +++ b/integrations/joplin/plugin.config.json @@ -0,0 +1,6 @@ +{ + "extraScripts": [ + "contentScripts/webview.ts", + "views/panel.ts" + ] +} \ No newline at end of file diff --git a/integrations/joplin/src/butterfly.css b/integrations/joplin/src/butterfly.css new file mode 100644 index 0000000..388388b --- /dev/null +++ b/integrations/joplin/src/butterfly.css @@ -0,0 +1,18 @@ +.butterfly-icon::before { + content: ""; + aspect-ratio: 1 / 1; + background: currentColor; + display: block; + -webkit-mask: url("img/icon.svg") center / contain no-repeat; + mask: url("img/icon.svg") center / contain no-repeat; +} + +.butterfly-icon-editor::before { + width: 24px; +} + +.butterfly-icon-settings::before { + width: 1.25em; + margin-right: -2px; + margin-left: -2px; +} \ No newline at end of file diff --git a/integrations/joplin/src/butterflyDocument.ts b/integrations/joplin/src/butterflyDocument.ts new file mode 100644 index 0000000..1ec48b8 --- /dev/null +++ b/integrations/joplin/src/butterflyDocument.ts @@ -0,0 +1,9 @@ +export class ButterflyDocument { + documentId: string; + previewId: string; + + constructor(documentId: string, previewId: string) { + this.documentId = documentId; + this.previewId = previewId; + } +} \ No newline at end of file diff --git a/integrations/joplin/src/contentScripts/markdownItPlugin.js b/integrations/joplin/src/contentScripts/markdownItPlugin.js new file mode 100644 index 0000000..ce5ffba --- /dev/null +++ b/integrations/joplin/src/contentScripts/markdownItPlugin.js @@ -0,0 +1,14 @@ +module.exports = { + default: function (context) { + return { + plugin: function (markdownIt, _pluginOptions) { + }, + assets: function () { + return [ + {name: 'webview.js'}, + {name: 'webview.css'} + ]; + }, + }; + }, +}; \ No newline at end of file diff --git a/integrations/joplin/src/contentScripts/webview.css b/integrations/joplin/src/contentScripts/webview.css new file mode 100644 index 0000000..8a9c851 --- /dev/null +++ b/integrations/joplin/src/contentScripts/webview.css @@ -0,0 +1,7 @@ +.butterfly-clickable-image { + cursor: pointer; +} + +.butterfly-clickable-image:hover { + opacity: 0.95; +} \ No newline at end of file diff --git a/integrations/joplin/src/contentScripts/webview.ts b/integrations/joplin/src/contentScripts/webview.ts new file mode 100644 index 0000000..15502d1 --- /dev/null +++ b/integrations/joplin/src/contentScripts/webview.ts @@ -0,0 +1,40 @@ +declare const webviewApi: { postMessage: (contentScriptId: string, message: any) => any; }; + +document.addEventListener('click', (event) => { + handleInteraction(event); +}); + +document.addEventListener("keydown", (event) => { + if (!(event.key === 'Enter' || event.key === ' ')) return; + handleInteraction(event); +}); + +function handleInteraction(event: UIEvent) { + const target: HTMLImageElement | null = (event.target as HTMLImageElement).closest('img[alt^="bfly@"]'); + if (!target) return; + const documentIdMatcher = target.alt.match(/^bfly@([0-9a-f]{32})$/); + if (!documentIdMatcher) return; + const documentId = documentIdMatcher[1]; + const previewId = target.dataset.resourceId; + if (!previewId) return; + const message = {documentId, previewId}; + webviewApi.postMessage('butterfly-md', message); +} + +const processImages = () => { + const images = document.querySelectorAll('img[alt^="bfly@"]'); + for (const image of images) { + if (!image.alt.match(/^bfly@([0-9a-f]{32})$/)) + continue; + image.classList.add('butterfly-clickable-image'); + image.tabIndex = 0; + image.role = 'button'; + image.title = 'Butterfly'; // ask plugin which title to use? + } +}; + +document.addEventListener('joplin-noteDidUpdate', () => { + processImages(); +}); + +processImages(); diff --git a/integrations/joplin/src/img/icon.svg b/integrations/joplin/src/img/icon.svg new file mode 100644 index 0000000..6defd7f --- /dev/null +++ b/integrations/joplin/src/img/icon.svg @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/integrations/joplin/src/index.ts b/integrations/joplin/src/index.ts new file mode 100644 index 0000000..426c4bd --- /dev/null +++ b/integrations/joplin/src/index.ts @@ -0,0 +1,206 @@ +import joplin from 'api'; +import {ContentScriptType, SettingItemType, ToolbarButtonLocation} from "../api/types"; +import {tmpdir} from 'os' +import {sep} from 'path' +import {v4 as uuidv4} from 'uuid' +import {ButterflyDocument} from "./butterflyDocument"; +import {ChangeEvent} from "../api/JoplinSettings"; + +const fs = joplin.require('fs-extra'); + +joplin.plugins.register({ + onStart: async function () { + let loaded: ButterflyDocument | null = null; + const tempFolder = `${tmpdir}${sep}joplin-butterfly-plugin${sep}`; + + await joplin.settings.registerSection('LinwoodButterfly', { + label: 'Butterfly', iconName: 'butterfly-icon butterfly-icon-settings', + description: 'Linwood Butterfly' + }); + await joplin.settings.registerSettings({ + 'butterflyOrigin': { + value: 'https://preview.butterfly.linwood.dev', + type: SettingItemType.String, + public: true, + label: 'Butterfly Origin', + section: 'LinwoodButterfly', + }, + }); + await joplin.settings.registerSettings({ + 'butterflyLanguage': { + value: (await joplin.settings.globalValues(['locale']))[0].toLowerCase().substring(0, 2), + type: SettingItemType.String, + public: true, + label: 'Language', + section: 'LinwoodButterfly', + }, + }); + await joplin.settings.onChange(async (event: ChangeEvent) => { + await updateIFrameUrl(); + }); + let panelReadinessResolver: (() => void) | null = null; + const panel = await joplin.views.panels.create('butterfly-panel'); + await joplin.views.panels.setHtml(panel, + `` + ) + const installDir = await joplin.plugins.installationDir(); + await joplin.window.loadChromeCssFile(installDir + '/butterfly.css'); + + await joplin.contentScripts.register( + ContentScriptType.MarkdownItPlugin, + 'butterfly-md', + 'contentScripts/markdownItPlugin.js' + ); + + await joplin.commands.register({ + name: 'clickEditorButton', + label: 'Butterfly', + iconName: 'butterfly-icon butterfly-icon-editor', + execute: async () => { + if (!await joplin.views.panels.visible(panel)) { + await joplin.views.panels.show(panel, true); + await waitForPanelReadiness(); + } else { + joplin.views.panels.postMessage(panel, {type: 'reset'}); + } + loaded = null; + } + }); + await joplin.views.toolbarButtons.create('butterfly-editor-button', 'clickEditorButton', ToolbarButtonLocation.EditorToolbar); + + async function updateIFrameUrl() { + const language: string = await joplin.settings.value('butterflyLanguage') + joplin.views.panels.postMessage(panel, { + type: 'config', + origin: await joplin.settings.value('butterflyOrigin'), + language: language + }); + } + + await joplin.views.panels.onMessage(panel, async (message: any) => { + switch (message.type) { + case 'panelReady': + panelReadinessResolver?.(); + panelReadinessResolver = null; + await updateIFrameUrl(); + return; + case 'getSetting': + return await joplin.settings.value(message.data); + case 'exit': + loaded = null; + await joplin.views.panels.show(panel, false); + return; + case 'save': + if (loaded) { + fs.outputFileSync(`${tempFolder}${loaded.documentId}.bfly`, message.data); + fs.outputFileSync(`${tempFolder}${loaded.previewId}.svg`, message.img); + await joplin.data.put( + ['resources', loaded.previewId], + null, + { + title: 'temp.svg', + filename: 'temp.svg', + updated_time: Date.now(), + user_updated_time: Date.now(), + blob_updated_time: Date.now(), + mime: 'image/svg+xml' + }, + [{ + path: `${tempFolder}${loaded.previewId}.svg` + }] + ); + await joplin.data.put( + ['resources', loaded.documentId], + null, + { + title: 'myfile.bfly', + filename: 'myfile.bfly', + updated_time: Date.now(), + user_updated_time: Date.now(), + blob_updated_time: Date.now(), + mime: 'application/x-butterfly' + }, + [{ + path: `${tempFolder}${loaded.documentId}.bfly` + }] + ); + } else { + loaded = new ButterflyDocument(generateId(), generateId()); + fs.outputFileSync(`${tempFolder}${loaded.documentId}.bfly`, message.data); + fs.outputFileSync(`${tempFolder}${loaded.previewId}.svg`, message.img); + await joplin.data.post( + ['resources'], + null, + { + id: loaded.previewId, + title: 'temp.svg', + filename: 'temp.svg', + created_time: Date.now(), + user_created_time: Date.now(), + updated_time: Date.now(), + user_updated_time: Date.now(), + blob_updated_time: Date.now(), + mime: 'image/svg+xml' + }, + [{ + path: `${tempFolder}${loaded.previewId}.svg` + }] + ); + await joplin.data.post( + ['resources'], + null, + { + id: loaded.documentId, + title: 'temp.bfly', + filename: 'temp.bfly', + created_time: Date.now(), + user_created_time: Date.now(), + updated_time: Date.now(), + user_updated_time: Date.now(), + blob_updated_time: Date.now(), + mime: 'application/x-butterfly' + }, + [{ + path: `${tempFolder}${loaded.documentId}.bfly` + }] + ); + await joplin.commands.execute('insertText', `\n![bfly@${loaded.documentId}](:/${loaded.previewId})\n`) + } + return; + } + }); + await joplin.views.panels.addScript(panel, '/views/panel.js'); + await joplin.contentScripts.onMessage('butterfly-md', async (message: any) => { + loaded = new ButterflyDocument(message.documentId, message.previewId); + if (!await joplin.views.panels.visible(panel)) { + await joplin.views.panels.show(panel, true); + await waitForPanelReadiness(); + } + const filePath = await joplin.data.resourcePath(message.documentId); + const file: Uint8Array = await fs.readFile(filePath); + + joplin.views.panels.postMessage(panel, { + type: 'setData', + data: file, + }); + }); + + async function waitForPanelReadiness(): Promise { + return new Promise((resolve, reject) => { + panelReadinessResolver = resolve; + window.setTimeout(() => { + if (panelReadinessResolver === resolve) { + panelReadinessResolver = null; + reject(new Error('Panel did not become ready')); + } + }, 5000); + }); + } + + function generateId() { + return uuidv4().replace(/-/g, '') + } + } + + +}); \ No newline at end of file diff --git a/integrations/joplin/src/manifest.json b/integrations/joplin/src/manifest.json new file mode 100644 index 0000000..6391520 --- /dev/null +++ b/integrations/joplin/src/manifest.json @@ -0,0 +1,16 @@ +{ + "manifest_version": 1, + "id": "dev.linwood.butterfly-joplin", + "app_min_version": "3.6", + "version": "0.1.0", + "name": "Infinite Canvas with Butterfly", + "description": "Seamlessly create and attach your Linwood Butterfly canvases as .png/.svg to your notes.", + "author": "Linwood", + "homepage_url": "https://butterfly.linwood.dev/", + "repository_url": "https://github.com/LinwoodDev/ButterflyIntegrations/", + "keywords": [], + "categories": [], + "screenshots": [], + "icons": {}, + "promo_tile": {} +} \ No newline at end of file diff --git a/integrations/joplin/src/views/panel.ts b/integrations/joplin/src/views/panel.ts new file mode 100644 index 0000000..8cb35f8 --- /dev/null +++ b/integrations/joplin/src/views/panel.ts @@ -0,0 +1,112 @@ +import { + ButterflyEditorMessage, + ButterflyHostMessageType, createButterflyEmbedUrl, + parseButterflyEditorMessageEvent, + postButterflyHostMessage, + toDocumentBytes, + toPostMessageBytes +// @ts-ignore +} from "@linwood/butterfly-integration-shared"; +import {WebviewApi} from "../../api/types"; + +declare const webviewApi: WebviewApi; + +let pendingDocument: number[] | null; + +let previewResolver: ((value: string) => void) | null; + +let butterflyOrigin: string; + +const iframe = document.querySelector("#butterfly") as HTMLIFrameElement; + +window.addEventListener('message', async (event: MessageEvent) => { + const parsed: ButterflyEditorMessage | null = parseButterflyEditorMessageEvent(event, butterflyOrigin, iframe?.contentWindow ?? null); + if (!parsed) return; + switch (parsed.type) { + case 'getData': + if (pendingDocument) { + sendToButterfly('setData', pendingDocument); + pendingDocument = null; + } + break; + case 'exit': + webviewApi.postMessage({type: 'exit'}); + break; + case 'save': + webviewApi.postMessage({ + type: 'save', + data: toDocumentBytes(parsed.message), + img: await retrievePreview() // calls 'render' + }); + break; + case 'renderSVG': + previewResolver?.(parsed.message); + previewResolver = null; + break; + } +}); + +async function retrievePreview(): Promise { + sendToButterfly('renderSVG', {}); + return new Promise((resolve, reject) => { + previewResolver = resolve; + window.setTimeout(() => { + if (previewResolver === resolve) { + previewResolver = null; + reject(new Error('Fetching preview timed out')); + } + }, 5000); + }); +} + +webviewApi.onMessage((pluginMsg: any) => { + switch (pluginMsg.message.type) { + case 'setData': + pendingDocument = toPostMessageBytes(pluginMsg.message.data); + requestEmbedReadiness(); + break; + case 'reset': + sendToButterfly('reset'); + break; + case 'config': + butterflyOrigin = pluginMsg.message.origin; + iframe.src = createButterflyEmbedUrl(butterflyOrigin + '/embed', window.location.href, { + save: true, + language: pluginMsg.message.language + }); + break; + } +}); + +function sendToButterfly(type: ButterflyHostMessageType, message?: unknown) { + postButterflyHostMessage(iframe?.contentWindow ?? null, butterflyOrigin, type, message) +} + +// start: readinessHandshake +let readinessTimer: number | undefined; +let readinessAttempts = 0; + +function stopReadinessHandshake() { + if (readinessTimer !== undefined) { + window.clearTimeout(readinessTimer) + readinessTimer = undefined; + } +} + +function requestEmbedReadiness() { + stopReadinessHandshake() + if (!pendingDocument) return; + if (readinessAttempts >= 60) { + pendingDocument = null; + webviewApi.postMessage({type: 'embedNotReady'}); + readinessAttempts = 0; + return; + } + readinessAttempts += 1 + sendToButterfly('getData') + readinessTimer = window.setTimeout(requestEmbedReadiness, 500) +} + +// end: readinessHandshake + +webviewApi.postMessage({type: 'panelReady'}); \ No newline at end of file diff --git a/integrations/joplin/tsconfig.json b/integrations/joplin/tsconfig.json new file mode 100644 index 0000000..426046f --- /dev/null +++ b/integrations/joplin/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "outDir": "./dist/", + "module": "commonjs", + "target": "es2015", + "jsx": "react", + "allowJs": true, + "baseUrl": ".", + "skipLibCheck": true, + } +} diff --git a/integrations/joplin/webpack.config.js b/integrations/joplin/webpack.config.js new file mode 100644 index 0000000..9f7cbc8 --- /dev/null +++ b/integrations/joplin/webpack.config.js @@ -0,0 +1,421 @@ +// ----------------------------------------------------------------------------- +// This file is used to build the plugin file (.jpl) and plugin info (.json). It +// is recommended not to edit this file as it would be overwritten when updating +// the plugin framework. If you do make some changes, consider using an external +// JS file and requiring it here to minimize the changes. That way when you +// update, you can easily restore the functionality you've added. +// ----------------------------------------------------------------------------- + +/* eslint-disable no-console */ + +const path = require('path'); +const crypto = require('crypto'); +const fs = require('fs-extra'); +const chalk = require('chalk'); +const CopyPlugin = require('copy-webpack-plugin'); +const tar = require('tar'); +const glob = require('glob'); +const execSync = require('child_process').execSync; + +// AUTO-GENERATED by updateCategories +const allPossibleCategories = [{'name': 'appearance'}, {'name': 'developer tools'}, {'name': 'productivity'}, {'name': 'themes'}, {'name': 'integrations'}, {'name': 'viewer'}, {'name': 'search'}, {'name': 'tags'}, {'name': 'editor'}, {'name': 'files'}, {'name': 'personal knowledge management'}]; +// AUTO-GENERATED by updateCategories + +const rootDir = path.resolve(__dirname); +const userConfigFilename = './plugin.config.json'; +const userConfigPath = path.resolve(rootDir, userConfigFilename); +const distDir = path.resolve(rootDir, 'dist'); +const srcDir = path.resolve(rootDir, 'src'); +const publishDir = path.resolve(rootDir, 'publish'); + +const userConfig = { + extraScripts: [], + ...(fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {}), +}; + +const manifestPath = `${srcDir}/manifest.json`; +const packageJsonPath = `${rootDir}/package.json`; +const allPossibleScreenshotsType = ['jpg', 'jpeg', 'png', 'gif', 'webp']; +const manifest = readManifest(manifestPath); +const pluginArchiveFilePath = path.resolve(publishDir, `${manifest.id}.jpl`); +const pluginInfoFilePath = path.resolve(publishDir, `${manifest.id}.json`); + +const {builtinModules} = require('node:module'); + +// Webpack5 doesn't polyfill by default and displays a warning when attempting to require() built-in +// node modules. Set these to false to prevent Webpack from warning about not polyfilling these modules. +// We don't need to polyfill because the plugins run in Electron's Node environment. +const moduleFallback = {}; +for (const moduleName of builtinModules) { + moduleFallback[moduleName] = false; +} + +const getPackageJson = () => { + return JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); +}; + +function validatePackageJson() { + const content = getPackageJson(); + if (!content.name || content.name.indexOf('joplin-plugin-') !== 0) { + console.warn(chalk.yellow(`WARNING: To publish the plugin, the package name should start with "joplin-plugin-" (found "${content.name}") in ${packageJsonPath}`)); + } + + if (!content.keywords || content.keywords.indexOf('joplin-plugin') < 0) { + console.warn(chalk.yellow(`WARNING: To publish the plugin, the package keywords should include "joplin-plugin" (found "${JSON.stringify(content.keywords)}") in ${packageJsonPath}`)); + } + + if (content.scripts && content.scripts.postinstall) { + console.warn(chalk.yellow(`WARNING: package.json contains a "postinstall" script. It is recommended to use a "prepare" script instead so that it is executed before publish. In ${packageJsonPath}`)); + } +} + +function fileSha256(filePath) { + const content = fs.readFileSync(filePath); + return crypto.createHash('sha256').update(content).digest('hex'); +} + +function currentGitInfo() { + try { + let branch = execSync('git rev-parse --abbrev-ref HEAD', {stdio: 'pipe'}).toString().trim(); + const commit = execSync('git rev-parse HEAD', {stdio: 'pipe'}).toString().trim(); + if (branch === 'HEAD') branch = 'master'; + return `${branch}:${commit}`; + } catch (error) { + const messages = error.message ? error.message.split('\n') : ['']; + console.info(chalk.cyan('Could not get git commit (not a git repo?):', messages[0].trim())); + console.info(chalk.cyan('Git information will not be stored in plugin info file')); + return ''; + } +} + +function validateCategories(categories) { + if (!categories) return null; + if ((categories.length !== new Set(categories).size)) throw new Error('Repeated categories are not allowed'); + // eslint-disable-next-line github/array-foreach -- Old code before rule was applied + categories.forEach(category => { + if (!allPossibleCategories.map(category => { + return category.name; + }).includes(category)) throw new Error(`${category} is not a valid category. Please make sure that the category name is lowercase. Valid categories are: \n${allPossibleCategories.map(category => { + return category.name; + })}\n`); + }); +} + +function validateScreenshots(screenshots) { + if (!screenshots) return null; + for (const screenshot of screenshots) { + if (!screenshot.src) throw new Error('You must specify a src for each screenshot'); + + // Avoid attempting to download and verify URL screenshots. + if (screenshot.src.startsWith('https://') || screenshot.src.startsWith('http://')) { + continue; + } + + const screenshotType = screenshot.src.split('.').pop(); + if (!allPossibleScreenshotsType.includes(screenshotType)) throw new Error(`${screenshotType} is not a valid screenshot type. Valid types are: \n${allPossibleScreenshotsType}\n`); + + const screenshotPath = path.resolve(rootDir, screenshot.src); + + // Max file size is 1MB + const fileMaxSize = 1024; + const fileSize = fs.statSync(screenshotPath).size / 1024; + if (fileSize > fileMaxSize) throw new Error(`Max screenshot file size is ${fileMaxSize}KB. ${screenshotPath} is ${fileSize}KB`); + } +} + +function readManifest(manifestPath) { + const content = fs.readFileSync(manifestPath, 'utf8'); + const output = JSON.parse(content); + if (!output.id) throw new Error(`Manifest plugin ID is not set in ${manifestPath}`); + validateCategories(output.categories); + validateScreenshots(output.screenshots); + return output; +} + +function createPluginArchive(sourceDir, destPath) { + const distFiles = glob.sync(`${sourceDir}/**/*`, {nodir: true, windowsPathsNoEscape: true}) + .map(f => f.substr(sourceDir.length + 1)); + + if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty'); + fs.removeSync(destPath); + + tar.create( + { + strict: true, + portable: true, + file: destPath, + cwd: sourceDir, + sync: true, + }, + distFiles, + ); + + console.info(chalk.cyan(`Plugin archive has been created in ${destPath}`)); +} + +const writeManifest = (manifestPath, content) => { + fs.writeFileSync(manifestPath, JSON.stringify(content, null, '\t'), 'utf8'); +}; + +function createPluginInfo(manifestPath, destPath, jplFilePath) { + const contentText = fs.readFileSync(manifestPath, 'utf8'); + const content = JSON.parse(contentText); + content._publish_hash = `sha256:${fileSha256(jplFilePath)}`; + content._publish_commit = currentGitInfo(); + writeManifest(destPath, content); +} + +function onBuildCompleted() { + try { + fs.removeSync(path.resolve(publishDir, 'index.js')); + createPluginArchive(distDir, pluginArchiveFilePath); + createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath); + validatePackageJson(); + } catch (error) { + console.error(chalk.red(error.message)); + } +} + +const baseConfig = { + mode: 'production', + target: 'node', + stats: 'errors-only', + module: { + rules: [ + { + test: /\.tsx?$/, + use: { + loader: 'ts-loader', + options: { + transpileOnly: true, // no type / syntax checking for files from shared + }, + }, + exclude: /node_modules/, + }, + ], + }, + ...userConfig.webpackOverrides, +}; + +const pluginConfig = { + ...baseConfig, entry: './src/index.ts', + resolve: { + alias: { + api: path.resolve(__dirname, 'api'), + }, + fallback: moduleFallback, + // JSON files can also be required from scripts so we include this. + // https://github.com/joplin/plugin-bibtex/pull/2 + extensions: ['.js', '.tsx', '.ts', '.json'], + }, + output: { + filename: 'index.js', + path: distDir, + }, + plugins: [ + new CopyPlugin({ + patterns: [ + { + from: '**/*', + context: path.resolve(__dirname, 'src'), + to: path.resolve(__dirname, 'dist'), + globOptions: { + ignore: [ + // All TypeScript files are compiled to JS and + // already copied into /dist so we don't copy them. + '**/*.ts', + '**/*.tsx', + ], + }, + }, + ], + }), + ] +}; + + +// These libraries can be included with require(...) or +// joplin.require(...) from content scripts. +const externalContentScriptLibraries = [ + '@codemirror/view', + '@codemirror/state', + '@codemirror/search', + '@codemirror/language', + '@codemirror/autocomplete', + '@codemirror/commands', + '@codemirror/highlight', + '@codemirror/lint', + '@codemirror/lang-html', + '@codemirror/lang-markdown', + '@codemirror/language-data', + '@lezer/common', + '@lezer/markdown', + '@lezer/highlight', +]; + +const extraScriptExternals = {}; +for (const library of externalContentScriptLibraries) { + extraScriptExternals[library] = {commonjs: library}; +} + +const extraScriptConfig = { + ...baseConfig, + resolve: { + alias: { + api: path.resolve(__dirname, 'api'), + }, + fallback: moduleFallback, + extensions: ['.js', '.tsx', '.ts', '.json'], + }, + + // We support requiring @codemirror/... libraries through require('@codemirror/...') + externalsType: 'commonjs', + externals: extraScriptExternals, +}; + +const createArchiveConfig = { + stats: 'errors-only', + entry: './dist/index.js', + resolve: { + fallback: moduleFallback, + }, + output: { + filename: 'index.js', + path: publishDir, + }, + plugins: [{ + apply(compiler) { + compiler.hooks.done.tap('archiveOnBuildListener', onBuildCompleted); + }, + }], +}; + +function resolveExtraScriptPath(name) { + const relativePath = `./src/${name}`; + + const fullPath = path.resolve(`${rootDir}/${relativePath}`); + if (!fs.pathExistsSync(fullPath)) throw new Error(`Could not find extra script: "${name}" at "${fullPath}"`); + + const s = name.split('.'); + s.pop(); + const nameNoExt = s.join('.'); + + return { + entry: relativePath, + output: { + filename: `${nameNoExt}.js`, + path: distDir, + library: 'default', + libraryTarget: 'commonjs', + libraryExport: 'default', + }, + }; +} + +function buildExtraScriptConfigs(userConfig) { + if (!userConfig.extraScripts.length) return []; + + const output = []; + + for (const scriptName of userConfig.extraScripts) { + const scriptPaths = resolveExtraScriptPath(scriptName); + output.push({ + ...extraScriptConfig, entry: scriptPaths.entry, + output: scriptPaths.output + }); + } + + return output; +} + +const increaseVersion = version => { + try { + const s = version.split('.'); + const d = Number(s[s.length - 1]) + 1; + s[s.length - 1] = `${d}`; + return s.join('.'); + } catch (error) { + error.message = `Could not parse version number: ${version}: ${error.message}`; + throw error; + } +}; + +const updateVersion = () => { + const packageJson = getPackageJson(); + packageJson.version = increaseVersion(packageJson.version); + fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, 'utf8'); + + const manifest = readManifest(manifestPath); + manifest.version = increaseVersion(manifest.version); + writeManifest(manifestPath, manifest); + + if (packageJson.version !== manifest.version) { + console.warn(chalk.yellow(`Version numbers have been updated but they do not match: package.json (${packageJson.version}), manifest.json (${manifest.version}). Set them to the required values to get them in sync.`)); + } else { + console.info(packageJson.version); + } +}; + +function main(environ) { + const configName = environ['joplin-plugin-config']; + if (!configName) throw new Error('A config file must be specified via the --joplin-plugin-config flag'); + + // Webpack configurations run in parallel, while we need them to run in + // sequence, and to do that it seems the only way is to run webpack multiple + // times, with different config each time. + + const configs = { + // Builds the main src/index.ts and copy the extra content from /src to + // /dist including scripts, CSS and any other asset. + buildMain: [pluginConfig], + + // Builds the extra scripts as defined in plugin.config.json. When doing + // so, some JavaScript files that were copied in the previous might be + // overwritten here by the compiled version. This is by design. The + // result is that JS files that don't need compilation, are simply + // copied to /dist, while those that do need it are correctly compiled. + buildExtraScripts: buildExtraScriptConfigs(userConfig), + + // Ths config is for creating the .jpl, which is done via the plugin, so + // it doesn't actually need an entry and output, however webpack won't + // run without this. So we give it an entry that we know is going to + // exist and output in the publish dir. Then the plugin will delete this + // temporary file before packaging the plugin. + createArchive: [createArchiveConfig], + }; + + // If we are running the first config step, we clean up and create the build + // directories. + if (configName === 'buildMain') { + fs.removeSync(distDir); + fs.removeSync(publishDir); + fs.mkdirpSync(publishDir); + } + + if (configName === 'updateVersion') { + updateVersion(); + return []; + } + + return configs[configName]; +} + + +module.exports = (env) => { + let exportedConfigs = []; + + try { + exportedConfigs = main(env); + } catch (error) { + console.error(error.message); + process.exit(1); + } + + if (!exportedConfigs.length) { + // Nothing to do - for example where there are no external scripts to + // compile. + process.exit(0); + } + + return exportedConfigs; +}; diff --git a/packages/shared/src/embed.ts b/packages/shared/src/embed.ts index b21c771..963720d 100644 --- a/packages/shared/src/embed.ts +++ b/packages/shared/src/embed.ts @@ -1,90 +1,95 @@ export const BUTTERFLY_EDITOR_MESSAGE_TYPES = [ - 'error', - 'exit', - 'getData', - 'save', + 'error', + 'exit', + 'getData', + 'save', + 'render', + 'renderSVG' ] as const export const BUTTERFLY_HOST_MESSAGE_TYPES = [ - 'getData', - 'setData', + 'getData', + 'setData', + 'render', + 'renderSVG', + 'reset' ] as const export type ButterflyEditorMessageType = typeof BUTTERFLY_EDITOR_MESSAGE_TYPES[number] export type ButterflyHostMessageType = typeof BUTTERFLY_HOST_MESSAGE_TYPES[number] export interface ButterflyEditorMessage { - type: ButterflyEditorMessageType - message?: unknown + type: ButterflyEditorMessageType + message?: unknown } export interface ButterflyHostMessage { - type: ButterflyHostMessageType - message?: unknown + type: ButterflyHostMessageType + message?: unknown } export interface ButterflyEmbedOptions { - editable?: boolean - fileName?: string - language?: string - save?: boolean + editable?: boolean + fileName?: string + language?: string + save?: boolean } export interface ButterflyEditorMessageEvent { - data: unknown - origin: string - source: MessageEventSource | null + data: unknown + origin: string + source: MessageEventSource | null } export function createButterflyEmbedUrl( - baseUrl: string, - currentUrl: string, - options: ButterflyEmbedOptions = {}, + baseUrl: string, + currentUrl: string, + options: ButterflyEmbedOptions = {}, ): string { - const url = new URL(baseUrl, currentUrl) - if (options.save !== undefined) { - url.searchParams.set('save', String(options.save)) - } - if (options.editable !== undefined) { - url.searchParams.set('editable', String(options.editable)) - } - if (options.language) { - url.searchParams.set('language', options.language) - } - if (options.fileName) { - url.searchParams.set('fileName', options.fileName) - } - return url.toString() + const url = new URL(baseUrl, currentUrl) + if (options.save !== undefined) { + url.searchParams.set('save', String(options.save)) + } + if (options.editable !== undefined) { + url.searchParams.set('editable', String(options.editable)) + } + if (options.language) { + url.searchParams.set('language', options.language) + } + if (options.fileName) { + url.searchParams.set('fileName', options.fileName) + } + return url.toString() } export function parseButterflyEditorMessageEvent( - event: ButterflyEditorMessageEvent, - expectedOrigin: string, - expectedSource: MessageEventSource | null, + event: ButterflyEditorMessageEvent, + expectedOrigin: string, + expectedSource: MessageEventSource | null, ): ButterflyEditorMessage | null { - if (event.origin !== expectedOrigin || event.source !== expectedSource) { - return null - } - if (event.data === null || typeof event.data !== 'object') { - return null - } + if (event.origin !== expectedOrigin || event.source !== expectedSource) { + return null + } + if (event.data === null || typeof event.data !== 'object') { + return null + } - const { type, message } = event.data as { type?: unknown, message?: unknown } - if ( - typeof type !== 'string' - || !BUTTERFLY_EDITOR_MESSAGE_TYPES.some((allowedType) => allowedType === type) - ) { - return null - } + const {type, message} = event.data as { type?: unknown, message?: unknown } + if ( + typeof type !== 'string' + || !BUTTERFLY_EDITOR_MESSAGE_TYPES.some((allowedType) => allowedType === type) + ) { + return null + } - return { type: type as ButterflyEditorMessageType, message } + return {type: type as ButterflyEditorMessageType, message} } export function postButterflyHostMessage( - target: WindowProxy | null, - targetOrigin: string, - type: ButterflyHostMessageType, - message?: unknown, + target: WindowProxy | null, + targetOrigin: string, + type: ButterflyHostMessageType, + message?: unknown, ): void { - target?.postMessage({ type, message } satisfies ButterflyHostMessage, targetOrigin) + target?.postMessage({type, message} satisfies ButterflyHostMessage, targetOrigin) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b9330fe..bed23ab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,6 +13,46 @@ importers: .: {} + integrations/joplin: + dependencies: + '@linwood/butterfly-integration-shared': + specifier: workspace:* + version: link:../../packages/shared + devDependencies: + '@types/node': + specifier: ^18.7.13 + version: 18.19.130 + chalk: + specifier: ^4.1.0 + version: 4.1.2 + copy-webpack-plugin: + specifier: ^14.0.0 + version: 14.0.0(webpack@5.110.1) + fs-extra: + specifier: ^10.1.0 + version: 10.1.0 + glob: + specifier: ^8.0.3 + version: 8.1.0 + tar: + specifier: ^7.5.22 + version: 7.5.22 + ts-loader: + specifier: ^9.3.1 + version: 9.6.2(typescript@4.9.5)(webpack@5.110.1) + typescript: + specifier: ^4.8.2 + version: 4.9.5 + uuid: + specifier: ^14.0.2 + version: 14.0.2 + webpack: + specifier: ^5.74.0 + version: 5.110.1(esbuild@0.28.1)(lightningcss@1.33.0)(postcss@8.5.26)(webpack-cli@4.10.0) + webpack-cli: + specifier: ^4.10.0 + version: 4.10.0(webpack@5.110.1) + integrations/nextcloud: dependencies: '@linwood/butterfly-integration-shared': @@ -35,7 +75,7 @@ importers: version: 3.1.0 '@nextcloud/vue': specifier: ^9.9.0 - version: 9.9.0(@vue/compiler-sfc@3.5.41)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(supports-color@10.2.2)(typescript@6.0.3)(vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(yaml@2.9.0)) + version: 9.9.0(@vue/compiler-sfc@3.5.41)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(supports-color@10.2.2)(typescript@6.0.3)(vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(terser@5.51.2)(yaml@2.9.0))(webpack@5.110.1) vue: specifier: ^3.5.41 version: 3.5.41(typescript@6.0.3) @@ -51,7 +91,7 @@ importers: version: 3.2.2(stylelint-config-recommended-scss@17.0.1(postcss@8.5.26)(stylelint@17.14.1(supports-color@10.2.2)(typescript@6.0.3)))(stylelint-config-recommended-vue@1.6.1(postcss-html@1.8.1)(stylelint@17.14.1(supports-color@10.2.2)(typescript@6.0.3)))(stylelint@17.14.1(supports-color@10.2.2)(typescript@6.0.3)) '@nextcloud/vite-config': specifier: ^2.5.4 - version: 2.5.4(browserslist@4.28.8)(picomatch@4.0.5)(rollup@4.62.4)(sass@1.102.0)(supports-color@10.2.2)(typescript@6.0.3)(vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)) + version: 2.5.4(browserslist@4.28.8)(picomatch@4.0.5)(rollup@4.62.4)(sass@1.102.0)(supports-color@10.2.2)(typescript@6.0.3)(vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(terser@5.51.2)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)) '@vue/tsconfig': specifier: ^0.9.1 version: 0.9.1(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)) @@ -75,7 +115,7 @@ importers: version: 6.0.3 vite: specifier: ^7.3.6 - version: 7.3.6(lightningcss@1.33.0)(sass@1.102.0)(yaml@2.9.0) + version: 7.3.6(lightningcss@1.33.0)(sass@1.102.0)(terser@5.51.2)(yaml@2.9.0) vue-tsc: specifier: ^3.3.9 version: 3.3.9(typescript@6.0.3) @@ -197,6 +237,10 @@ packages: resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==} engines: {node: '>=10'} + '@discoveryjs/json-ext@0.5.7': + resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} + engines: {node: '>=10.0.0'} + '@es-joy/jsdoccomment@0.91.0': resolution: {integrity: sha512-vgqlMGNNhZxwDYbUNIHj3Hskb4R28iqdXx90ufHyt/NeuTQkeqjTDslAs9I0/GCAfbxP5BpH5WsL1R1fht5Lxg==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} @@ -608,6 +652,10 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -618,6 +666,9 @@ packages: resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} @@ -1192,6 +1243,9 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@18.19.130': + resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} + '@types/semver@7.8.0': resolution: {integrity: sha512-1mAINjtQCXXeLkJ9ehXkwOcBpqtLxiVtKhpUf83DdRNdQKV0iXZpaHYqRr7nj+wvxuJzoAmAwXI+sCNMv1CzLQ==} @@ -1387,6 +1441,77 @@ packages: peerDependencies: vue: ^3.5.0 + '@webassemblyjs/ast@1.14.1': + resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} + + '@webassemblyjs/floating-point-hex-parser@1.13.2': + resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} + + '@webassemblyjs/helper-api-error@1.13.2': + resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} + + '@webassemblyjs/helper-buffer@1.14.1': + resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} + + '@webassemblyjs/helper-numbers@1.13.2': + resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': + resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} + + '@webassemblyjs/helper-wasm-section@1.14.1': + resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} + + '@webassemblyjs/ieee754@1.13.2': + resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} + + '@webassemblyjs/leb128@1.13.2': + resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} + + '@webassemblyjs/utf8@1.13.2': + resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} + + '@webassemblyjs/wasm-edit@1.14.1': + resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} + + '@webassemblyjs/wasm-gen@1.14.1': + resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} + + '@webassemblyjs/wasm-opt@1.14.1': + resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} + + '@webassemblyjs/wasm-parser@1.14.1': + resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} + + '@webassemblyjs/wast-printer@1.14.1': + resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + + '@webpack-cli/configtest@1.2.0': + resolution: {integrity: sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg==} + peerDependencies: + webpack: 4.x.x || 5.x.x + webpack-cli: 4.x.x + + '@webpack-cli/info@1.5.0': + resolution: {integrity: sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ==} + peerDependencies: + webpack-cli: 4.x.x + + '@webpack-cli/serve@1.7.0': + resolution: {integrity: sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q==} + peerDependencies: + webpack-cli: 4.x.x + webpack-dev-server: '*' + peerDependenciesMeta: + webpack-dev-server: + optional: true + + '@xtuc/ieee754@1.2.0': + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + + '@xtuc/long@4.2.2': + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1409,6 +1534,14 @@ packages: ajv: optional: true + ajv-formats@2.1.1: + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv-formats@3.0.1: resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} peerDependencies: @@ -1417,6 +1550,11 @@ packages: ajv: optional: true + ajv-keywords@5.1.0: + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} + peerDependencies: + ajv: ^8.8.2 + ajv@6.15.0: resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} @@ -1574,6 +1712,9 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer-xor@1.0.3: resolution: {integrity: sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==} @@ -1614,6 +1755,10 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + character-entities-html4@2.1.0: resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} @@ -1633,10 +1778,22 @@ packages: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + chrome-trace-event@1.0.4: + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} + engines: {node: '>=6.0'} + cipher-base@1.0.7: resolution: {integrity: sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==} engines: {node: '>= 0.10'} + clone-deep@4.0.1: + resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} + engines: {node: '>=6'} + clone@2.1.2: resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} engines: {node: '>=0.8'} @@ -1651,6 +1808,9 @@ packages: colord@2.9.3: resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -1658,6 +1818,13 @@ packages: comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + comment-parser@1.4.7: resolution: {integrity: sha512-0h+uSNtQGW3D98eQt3jJ8L06Fves8hncB4V/PKdw/Qb8Hnk19VaKuTr55UNRYiSoVa7WwrFls+rh3ux9agmkeQ==} engines: {node: '>= 12.0.0'} @@ -1680,6 +1847,12 @@ packages: constants-browserify@1.0.0: resolution: {integrity: sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==} + copy-webpack-plugin@14.0.0: + resolution: {integrity: sha512-3JLW90aBGeaTLpM7mYQKpnVdgsUZRExY55giiZgLuX/xTQRUs1dOCwbBnWnvY6Q6rfZoXMNwzOQJCSZPppfqXA==} + engines: {node: '>= 20.9.0'} + peerDependencies: + webpack: ^5.1.0 + core-js-compat@3.50.0: resolution: {integrity: sha512-XGpFGbMLHwSt74YLTKho7Ib242qi6O8MSX+sRokV4oz7iKXvQWGYZthjIhjRGMxjzVkAubBO512dKGYcefmX3Q==} engines: {node: '>=6.4.0'} @@ -1846,6 +2019,10 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + enhanced-resolve@5.24.5: + resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==} + engines: {node: '>=10.13.0'} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -1862,6 +2039,11 @@ packages: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} + envinfo@7.21.0: + resolution: {integrity: sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==} + engines: {node: '>=4'} + hasBin: true + error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} @@ -1873,6 +2055,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + es-object-atoms@1.1.2: resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} engines: {node: '>= 0.4'} @@ -2071,6 +2256,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -2082,6 +2271,10 @@ packages: flat-cache@6.1.23: resolution: {integrity: sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==} + flat@5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + flatted@3.4.4: resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} @@ -2118,10 +2311,17 @@ packages: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + fs-extra@11.3.6: resolution: {integrity: sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==} engines: {node: '>=14.14'} + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2157,6 +2357,11 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + global-modules@2.0.0: resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} engines: {node: '>=6'} @@ -2295,6 +2500,11 @@ packages: resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} engines: {node: '>=8'} + import-local@3.2.0: + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} + engines: {node: '>=8'} + hasBin: true + import-meta-resolve@4.2.0: resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} @@ -2302,6 +2512,10 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -2311,6 +2525,10 @@ packages: inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + interpret@2.2.0: + resolution: {integrity: sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==} + engines: {node: '>= 0.10'} + is-absolute-url@4.0.1: resolution: {integrity: sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -2377,6 +2595,10 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-plain-object@2.0.4: + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} + is-plain-object@5.0.0: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} engines: {node: '>=0.10.0'} @@ -2405,10 +2627,18 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} + isomorphic-timers-promises@1.0.1: resolution: {integrity: sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ==} engines: {node: '>=10'} + jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + jju@1.4.0: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} @@ -2561,6 +2791,10 @@ packages: resolution: {integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==} engines: {node: '>=14'} + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -2644,6 +2878,9 @@ packages: resolution: {integrity: sha512-EDYo6VlmtnumlcBCbh1gLJ//9jvM/ndXHfVXIFrZVr6fGcwTUyCTFNTLCKuY3ffbK8L/+3Mzqnd58RojiZqHVw==} engines: {node: '>=20'} + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -2723,6 +2960,10 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} @@ -2741,10 +2982,65 @@ packages: resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} engines: {node: 18 || 20 || >=22} + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + minimatch@9.0.9: resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} engines: {node: '>=16 || 14 >=14.17'} + minimizer-webpack-plugin@5.8.0: + resolution: {integrity: sha512-2cT9+goJfBhtMz+gJqejSf09ClgmYhPhccRb/fb0ztVbixt0BkO8mRuI26FoPPuUNkRk6iEDryWAIouc5W8eJA==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@minify-html/node': '*' + '@swc/core': '*' + '@swc/css': '*' + '@swc/html': '*' + clean-css: '*' + cssnano: '*' + csso: '*' + esbuild: '*' + html-minifier-terser: '*' + lightningcss: '*' + postcss: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@minify-html/node': + optional: true + '@swc/core': + optional: true + '@swc/css': + optional: true + '@swc/html': + optional: true + clean-css: + optional: true + cssnano: + optional: true + csso: + optional: true + esbuild: + optional: true + html-minifier-terser: + optional: true + lightningcss: + optional: true + postcss: + optional: true + uglify-js: + optional: true + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} @@ -2769,6 +3065,9 @@ packages: resolution: {integrity: sha512-kKHJhxwpR/Okycz4HhQKKlhWe4ASEfPgkSWNmKFHd7+ezuQlxkA5cM3+XkBPvm1gmHen3w53qsYAv+8GwRrBlg==} engines: {node: '>=18'} + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + nested-property@4.0.0: resolution: {integrity: sha512-yFehXNWRs4cM0+dz7QxCd06hTbWbSkV0ISsqBfkntU6TOY4Qm3Q88fRRLOddkGh2Qq6dZvnKVAahfhjcUvLnyA==} @@ -2821,6 +3120,9 @@ packages: resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} engines: {node: '>= 0.4'} + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -2828,10 +3130,18 @@ packages: os-browserify@0.3.0: resolution: {integrity: sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==} + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} @@ -2844,6 +3154,10 @@ packages: resolution: {integrity: sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==} engines: {node: '>=20'} + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + package-name-regex@2.0.6: resolution: {integrity: sha512-gFL35q7kbE/zBaPA3UKhp2vSzcPYx2ecbYuwv1ucE9Il6IIgBDweBlH8D68UFGZic2MkllKa2KHCfC1IQBQUYA==} engines: {node: '>=12'} @@ -2914,6 +3228,10 @@ packages: resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} engines: {node: '>=12'} + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + pkg-dir@5.0.0: resolution: {integrity: sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==} engines: {node: '>=10'} @@ -3033,6 +3351,10 @@ packages: resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==} engines: {node: '>= 20.19.0'} + rechoir@0.7.1: + resolution: {integrity: sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==} + engines: {node: '>= 0.10'} + rehype-external-links@3.0.0: resolution: {integrity: sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==} @@ -3065,10 +3387,18 @@ packages: resolution: {integrity: sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==} engines: {node: '>=18'} + resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + resolve@1.22.12: resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} engines: {node: '>= 0.4'} @@ -3144,6 +3474,10 @@ packages: resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==} engines: {node: '>=11.0.0'} + schema-utils@4.3.3: + resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} + engines: {node: '>= 10.13.0'} + scule@1.3.0: resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} @@ -3157,6 +3491,10 @@ packages: engines: {node: '>=10'} hasBin: true + serialize-javascript@7.1.0: + resolution: {integrity: sha512-RNEqWOyhhUQYN9V1GfHwu9AR/g+NTciH6Z5u3/no6X3/w+04J2lVDL+svFQVXgXrEGBMG2puMVN3gq2SNGuTGw==} + engines: {node: '>=20.0.0'} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -3169,6 +3507,10 @@ packages: engines: {node: '>= 0.10'} hasBin: true + shallow-clone@3.0.1: + resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} + engines: {node: '>=8'} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -3217,10 +3559,17 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} @@ -3361,6 +3710,10 @@ packages: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + supports-color@8.1.1: resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} engines: {node: '>=10'} @@ -3383,6 +3736,19 @@ packages: resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} engines: {node: '>=10.0.0'} + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + tar@7.5.22: + resolution: {integrity: sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==} + engines: {node: '>=18'} + + terser@5.51.2: + resolution: {integrity: sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q==} + engines: {node: '>=10'} + hasBin: true + timers-browserify@2.0.12: resolution: {integrity: sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==} engines: {node: '>=0.6.0'} @@ -3418,6 +3784,17 @@ packages: peerDependencies: typescript: '>=4.8.4' + ts-loader@9.6.2: + resolution: {integrity: sha512-R4iuczmtgxvtuI556s+hTZ6/7Ee03VCAk/l/M8LY1OAsUgB7YydsCxkgq9D9pKRaD7GJqUi2u8fp9zZP/ufjKA==} + engines: {node: '>=12.0.0'} + peerDependencies: + loader-utils: '*' + typescript: '*' + webpack: ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + loader-utils: + optional: true + ts-md5@2.0.1: resolution: {integrity: sha512-yF35FCoEOFBzOclSkMNEUbFQZuv89KEQ+5Xz03HrMSGUGB1+r+El+JiGOFwsP4p9RFNzwlrydYoTLvPOuICl9w==} engines: {node: '>=18'} @@ -3443,6 +3820,11 @@ packages: typescript-event-target@1.1.2: resolution: {integrity: sha512-TvkrTUpv7gCPlcnSoEwUVUBwsdheKm+HF5u2tPAKubkIGMfovdSizCTaZRY/NhR8+Ijy8iZZUapbVQAsNrkFrw==} + typescript@4.9.5: + resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} + engines: {node: '>=4.2.0'} + hasBin: true + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -3456,6 +3838,9 @@ packages: ufo@1.6.4: resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + unicorn-magic@0.4.0: resolution: {integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==} engines: {node: '>=20'} @@ -3551,6 +3936,10 @@ packages: util@0.12.5: resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + uuid@14.0.2: + resolution: {integrity: sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==} + hasBin: true + vfile-message@4.0.3: resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} @@ -3665,6 +4054,10 @@ packages: typescript: optional: true + watchpack@2.5.2: + resolution: {integrity: sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==} + engines: {node: '>=10.13.0'} + web-streams-polyfill@3.3.3: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} @@ -3673,9 +4066,47 @@ packages: resolution: {integrity: sha512-fVPuRLtcduVGvSO7Tn/6TQCzIvI/g6BO/+xPRctCvi/GytYpjn4czxWbh4HsArsdom9qz9BI63k9/v2HBUui1A==} engines: {node: '>=14'} + webpack-cli@4.10.0: + resolution: {integrity: sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + '@webpack-cli/generators': '*' + '@webpack-cli/migrate': '*' + webpack: 4.x.x || 5.x.x + webpack-bundle-analyzer: '*' + webpack-dev-server: '*' + peerDependenciesMeta: + '@webpack-cli/generators': + optional: true + '@webpack-cli/migrate': + optional: true + webpack-bundle-analyzer: + optional: true + webpack-dev-server: + optional: true + + webpack-merge@5.10.0: + resolution: {integrity: sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==} + engines: {node: '>=10.0.0'} + + webpack-sources@3.5.1: + resolution: {integrity: sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==} + engines: {node: '>=10.13.0'} + webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + webpack@5.110.1: + resolution: {integrity: sha512-gInQB+jxXxgnZyvPwuzT5NGQmECDqeu85oxcrjinrYHqPoBex0hCAN2SFTJVyPVrK0Pq9E44VFP+e89fAc10/w==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + which-typed-array@1.1.22: resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==} engines: {node: '>= 0.4'} @@ -3689,10 +4120,16 @@ packages: engines: {node: '>= 8'} hasBin: true + wildcard@2.0.1: + resolution: {integrity: sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + write-file-atomic@7.0.1: resolution: {integrity: sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==} engines: {node: ^20.17.0 || >=22.9.0} @@ -3709,6 +4146,10 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + yaml@2.9.0: resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} engines: {node: '>= 14.6'} @@ -3818,6 +4259,8 @@ snapshots: '@ctrl/tinycolor@3.6.1': {} + '@discoveryjs/json-ext@0.5.7': {} + '@es-joy/jsdoccomment@0.91.0': dependencies: '@types/estree': 1.0.9 @@ -4069,6 +4512,10 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.3 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -4081,6 +4528,11 @@ snapshots: '@jridgewell/resolve-uri@3.1.2': {} + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/sourcemap-codec@1.5.5': {} '@jridgewell/trace-mapping@0.3.31': @@ -4258,23 +4710,23 @@ snapshots: dependencies: '@types/jquery': 3.5.16 - '@nextcloud/vite-config@2.5.4(browserslist@4.28.8)(picomatch@4.0.5)(rollup@4.62.4)(sass@1.102.0)(supports-color@10.2.2)(typescript@6.0.3)(vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))': + '@nextcloud/vite-config@2.5.4(browserslist@4.28.8)(picomatch@4.0.5)(rollup@4.62.4)(sass@1.102.0)(supports-color@10.2.2)(typescript@6.0.3)(vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(terser@5.51.2)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))': dependencies: '@rollup/plugin-replace': 6.0.3(rollup@4.62.4) - '@vitejs/plugin-vue': 6.0.8(vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)) + '@vitejs/plugin-vue': 6.0.8(vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(terser@5.51.2)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)) browserslist: 4.28.8 browserslist-to-esbuild: 2.1.1(browserslist@4.28.8) magic-string: 0.30.21 rollup-plugin-corejs: 1.0.2(rollup@4.62.4) rollup-plugin-esbuild-minify: 1.3.0(rollup@4.62.4) rollup-plugin-license: 3.7.1(picomatch@4.0.5)(rollup@4.62.4) - rollup-plugin-node-externals: 9.0.1(rollup@4.62.4)(vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(yaml@2.9.0)) + rollup-plugin-node-externals: 9.0.1(rollup@4.62.4)(vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(terser@5.51.2)(yaml@2.9.0)) sass: 1.102.0 spdx-expression-parse: 4.0.0 - vite: 7.3.6(lightningcss@1.33.0)(sass@1.102.0)(yaml@2.9.0) - vite-plugin-css-injected-by-js: 4.0.1(vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(yaml@2.9.0)) - vite-plugin-dts: 4.5.4(rollup@4.62.4)(supports-color@10.2.2)(typescript@6.0.3)(vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(yaml@2.9.0)) - vite-plugin-node-polyfills: 0.28.0(rollup@4.62.4)(vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(yaml@2.9.0)) + vite: 7.3.6(lightningcss@1.33.0)(sass@1.102.0)(terser@5.51.2)(yaml@2.9.0) + vite-plugin-css-injected-by-js: 4.0.1(vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(terser@5.51.2)(yaml@2.9.0)) + vite-plugin-dts: 4.5.4(rollup@4.62.4)(supports-color@10.2.2)(typescript@6.0.3)(vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(terser@5.51.2)(yaml@2.9.0)) + vite-plugin-node-polyfills: 0.28.0(rollup@4.62.4)(vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(terser@5.51.2)(yaml@2.9.0)) transitivePeerDependencies: - '@types/node' - picomatch @@ -4287,7 +4739,7 @@ snapshots: dependencies: vue: 3.5.41(typescript@6.0.3) - '@nextcloud/vue@9.9.0(@vue/compiler-sfc@3.5.41)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(supports-color@10.2.2)(typescript@6.0.3)(vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(yaml@2.9.0))': + '@nextcloud/vue@9.9.0(@vue/compiler-sfc@3.5.41)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(supports-color@10.2.2)(typescript@6.0.3)(vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(terser@5.51.2)(yaml@2.9.0))(webpack@5.110.1)': dependencies: '@ckpack/vue-color': 1.6.0(vue@3.5.41(typescript@6.0.3)) '@floating-ui/dom': 1.8.0 @@ -4332,7 +4784,7 @@ snapshots: unist-builder: 4.0.0 unist-util-visit-parents: 6.0.2 vue: 3.5.41(typescript@6.0.3) - vue-router: 5.2.0(@vue/compiler-sfc@3.5.41)(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)) + vue-router: 5.2.0(@vue/compiler-sfc@3.5.41)(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(terser@5.51.2)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(webpack@5.110.1) transitivePeerDependencies: - '@farmfe/core' - '@nuxt/kit' @@ -4650,6 +5102,10 @@ snapshots: '@types/ms@2.1.0': {} + '@types/node@18.19.130': + dependencies: + undici-types: 5.26.5 + '@types/semver@7.8.0': {} '@types/sizzle@2.3.10': {} @@ -4756,10 +5212,10 @@ snapshots: '@ungap/structured-clone@1.3.3': {} - '@vitejs/plugin-vue@6.0.8(vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))': + '@vitejs/plugin-vue@6.0.8(vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(terser@5.51.2)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 7.3.6(lightningcss@1.33.0)(sass@1.102.0)(yaml@2.9.0) + vite: 7.3.6(lightningcss@1.33.0)(sass@1.102.0)(terser@5.51.2)(yaml@2.9.0) vue: 3.5.41(typescript@6.0.3) '@volar/language-core@2.4.28': @@ -4910,6 +5366,100 @@ snapshots: dependencies: vue: 3.5.41(typescript@6.0.3) + '@webassemblyjs/ast@1.14.1': + dependencies: + '@webassemblyjs/helper-numbers': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + + '@webassemblyjs/floating-point-hex-parser@1.13.2': {} + + '@webassemblyjs/helper-api-error@1.13.2': {} + + '@webassemblyjs/helper-buffer@1.14.1': {} + + '@webassemblyjs/helper-numbers@1.13.2': + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.13.2 + '@webassemblyjs/helper-api-error': 1.13.2 + '@xtuc/long': 4.2.2 + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} + + '@webassemblyjs/helper-wasm-section@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/wasm-gen': 1.14.1 + + '@webassemblyjs/ieee754@1.13.2': + dependencies: + '@xtuc/ieee754': 1.2.0 + + '@webassemblyjs/leb128@1.13.2': + dependencies: + '@xtuc/long': 4.2.2 + + '@webassemblyjs/utf8@1.13.2': {} + + '@webassemblyjs/wasm-edit@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/helper-wasm-section': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-opt': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + '@webassemblyjs/wast-printer': 1.14.1 + + '@webassemblyjs/wasm-gen@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wasm-opt@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + + '@webassemblyjs/wasm-parser@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-api-error': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wast-printer@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@xtuc/long': 4.2.2 + + '@webpack-cli/configtest@1.2.0(webpack-cli@4.10.0)(webpack@5.110.1)': + dependencies: + webpack: 5.110.1(esbuild@0.28.1)(lightningcss@1.33.0)(postcss@8.5.26)(webpack-cli@4.10.0) + webpack-cli: 4.10.0(webpack@5.110.1) + + '@webpack-cli/info@1.5.0(webpack-cli@4.10.0)': + dependencies: + envinfo: 7.21.0 + webpack-cli: 4.10.0(webpack@5.110.1) + + '@webpack-cli/serve@1.7.0(webpack-cli@4.10.0)': + dependencies: + webpack-cli: 4.10.0(webpack@5.110.1) + + '@xtuc/ieee754@1.2.0': {} + + '@xtuc/long@4.2.2': {} + acorn-jsx@5.3.2(acorn@8.18.0): dependencies: acorn: 8.18.0 @@ -4926,10 +5476,19 @@ snapshots: optionalDependencies: ajv: 8.20.0 + ajv-formats@2.1.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + ajv-formats@3.0.1(ajv@8.20.0): optionalDependencies: ajv: 8.20.0 + ajv-keywords@5.1.0(ajv@8.20.0): + dependencies: + ajv: 8.20.0 + fast-deep-equal: 3.1.3 + ajv@6.15.0: dependencies: fast-deep-equal: 3.1.3 @@ -5115,6 +5674,8 @@ snapshots: node-releases: 2.0.53 update-browserslist-db: 1.3.0(browserslist@4.28.8) + buffer-from@1.1.2: {} + buffer-xor@1.0.3: {} buffer@5.7.1: @@ -5160,6 +5721,11 @@ snapshots: ccount@2.0.1: {} + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + character-entities-html4@2.1.0: {} character-entities-legacy@3.0.0: {} @@ -5174,12 +5740,22 @@ snapshots: dependencies: readdirp: 5.1.1 + chownr@3.0.0: {} + + chrome-trace-event@1.0.4: {} + cipher-base@1.0.7: dependencies: inherits: 2.0.4 safe-buffer: 5.2.1 to-buffer: 1.2.2 + clone-deep@4.0.1: + dependencies: + is-plain-object: 2.0.4 + kind-of: 6.0.3 + shallow-clone: 3.0.1 + clone@2.1.2: {} color-convert@2.0.1: @@ -5190,12 +5766,18 @@ snapshots: colord@2.9.3: {} + colorette@2.0.20: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 comma-separated-tokens@2.0.3: {} + commander@2.20.3: {} + + commander@7.2.0: {} + comment-parser@1.4.7: {} commenting@1.1.0: {} @@ -5210,6 +5792,15 @@ snapshots: constants-browserify@1.0.0: {} + copy-webpack-plugin@14.0.0(webpack@5.110.1): + dependencies: + glob-parent: 6.0.2 + normalize-path: 3.0.0 + schema-utils: 4.3.3 + serialize-javascript: 7.1.0 + tinyglobby: 0.2.17 + webpack: 5.110.1(esbuild@0.28.1)(lightningcss@1.33.0)(postcss@8.5.26)(webpack-cli@4.10.0) + core-js-compat@3.50.0: dependencies: browserslist: 4.28.8 @@ -5395,6 +5986,11 @@ snapshots: emoji-regex@8.0.0: {} + enhanced-resolve@5.24.5: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + entities@4.5.0: {} entities@6.0.1: {} @@ -5403,6 +5999,8 @@ snapshots: env-paths@2.2.1: {} + envinfo@7.21.0: {} + error-ex@1.3.4: dependencies: is-arrayish: 0.2.1 @@ -5411,6 +6009,8 @@ snapshots: es-errors@1.3.0: {} + es-module-lexer@2.3.2: {} + es-object-atoms@1.1.2: dependencies: es-errors: 1.3.0 @@ -5691,6 +6291,11 @@ snapshots: dependencies: to-regex-range: 5.0.1 + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -5707,6 +6312,8 @@ snapshots: flatted: 3.4.4 hookified: 1.15.1 + flat@5.0.2: {} + flatted@3.4.4: {} floating-vue@5.2.2(vue@3.5.41(typescript@6.0.3)): @@ -5739,12 +6346,20 @@ snapshots: dependencies: fetch-blob: 3.2.0 + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + fs-extra@11.3.6: dependencies: graceful-fs: 4.2.11 jsonfile: 6.2.1 universalify: 2.0.1 + fs.realpath@1.0.0: {} + fsevents@2.3.3: optional: true @@ -5782,6 +6397,14 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@8.1.0: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.9 + once: 1.4.0 + global-modules@2.0.0: dependencies: global-prefix: 3.0.0 @@ -5936,16 +6559,28 @@ snapshots: import-lazy@4.0.0: {} + import-local@3.2.0: + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + import-meta-resolve@4.2.0: {} imurmurhash@0.1.4: {} + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + inherits@2.0.4: {} ini@1.3.8: {} inline-style-parser@0.2.7: {} + interpret@2.2.0: {} + is-absolute-url@4.0.1: {} is-alphabetical@2.0.1: {} @@ -6001,6 +6636,10 @@ snapshots: is-plain-obj@4.1.0: {} + is-plain-object@2.0.4: + dependencies: + isobject: 3.0.1 + is-plain-object@5.0.0: {} is-regex@1.2.1: @@ -6026,8 +6665,16 @@ snapshots: isexe@2.0.0: {} + isobject@3.0.1: {} + isomorphic-timers-promises@1.0.1: {} + jest-worker@27.5.1: + dependencies: + '@types/node': 18.19.130 + merge-stream: 2.0.0 + supports-color: 8.1.1 + jju@1.4.0: {} js-tokens@4.0.0: {} @@ -6141,6 +6788,10 @@ snapshots: pkg-types: 2.3.1 quansync: 0.2.11 + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -6295,6 +6946,8 @@ snapshots: meow@14.1.0: {} + merge-stream@2.0.0: {} + merge2@1.4.1: {} micromark-core-commonmark@2.0.3: @@ -6442,6 +7095,8 @@ snapshots: mime-db@1.52.0: {} + mime-db@1.54.0: {} + mime-types@2.1.35: dependencies: mime-db: 1.52.0 @@ -6458,10 +7113,32 @@ snapshots: dependencies: brace-expansion: 5.0.9 + minimatch@5.1.9: + dependencies: + brace-expansion: 2.1.4 + minimatch@9.0.9: dependencies: brace-expansion: 2.1.4 + minimizer-webpack-plugin@5.8.0(esbuild@0.28.1)(lightningcss@1.33.0)(postcss@8.5.26)(webpack@5.110.1): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + terser: 5.51.2 + webpack: 5.110.1(esbuild@0.28.1)(lightningcss@1.33.0)(postcss@8.5.26)(webpack-cli@4.10.0) + optionalDependencies: + esbuild: 0.28.1 + lightningcss: 1.33.0 + postcss: 8.5.26 + + minipass@7.1.3: {} + + minizlib@3.1.0: + dependencies: + minipass: 7.1.3 + mlly@1.8.2: dependencies: acorn: 8.18.0 @@ -6481,6 +7158,8 @@ snapshots: natural-orderby@5.0.0: {} + neo-async@2.6.2: {} + nested-property@4.0.0: {} node-addon-api@7.1.1: @@ -6554,6 +7233,10 @@ snapshots: has-symbols: 1.1.0 object-keys: 1.1.1 + once@1.4.0: + dependencies: + wrappy: 1.0.2 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -6565,10 +7248,18 @@ snapshots: os-browserify@0.3.0: {} + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + p-locate@5.0.0: dependencies: p-limit: 3.1.0 @@ -6580,6 +7271,8 @@ snapshots: p-timeout@7.0.1: {} + p-try@2.2.0: {} + package-name-regex@2.0.6: {} pako@1.0.11: {} @@ -6650,6 +7343,10 @@ snapshots: picomatch@4.0.5: {} + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + pkg-dir@5.0.0: dependencies: find-up: 5.0.0 @@ -6771,6 +7468,10 @@ snapshots: readdirp@5.1.1: {} + rechoir@0.7.1: + dependencies: + resolve: 1.22.12 + rehype-external-links@3.0.0: dependencies: '@types/hast': 3.0.5 @@ -6831,8 +7532,14 @@ snapshots: reserved-identifiers@1.2.0: {} + resolve-cwd@3.0.0: + dependencies: + resolve-from: 5.0.0 + resolve-from@4.0.0: {} + resolve-from@5.0.0: {} + resolve@1.22.12: dependencies: es-errors: 1.3.0 @@ -6896,10 +7603,10 @@ snapshots: transitivePeerDependencies: - picomatch - rollup-plugin-node-externals@9.0.1(rollup@4.62.4)(vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(yaml@2.9.0)): + rollup-plugin-node-externals@9.0.1(rollup@4.62.4)(vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(terser@5.51.2)(yaml@2.9.0)): optionalDependencies: rollup: 4.62.4 - vite: 7.3.6(lightningcss@1.33.0)(sass@1.102.0)(yaml@2.9.0) + vite: 7.3.6(lightningcss@1.33.0)(sass@1.102.0)(terser@5.51.2)(yaml@2.9.0) rollup@4.62.4: dependencies: @@ -6957,12 +7664,21 @@ snapshots: sax@1.6.1: {} + schema-utils@4.3.3: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 8.20.0 + ajv-formats: 2.1.1(ajv@8.20.0) + ajv-keywords: 5.1.0(ajv@8.20.0) + scule@1.3.0: {} semver@7.7.4: {} semver@7.8.5: {} + serialize-javascript@7.1.0: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -6980,6 +7696,10 @@ snapshots: safe-buffer: 5.2.1 to-buffer: 1.2.2 + shallow-clone@3.0.1: + dependencies: + kind-of: 6.0.3 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -7038,8 +7758,15 @@ snapshots: source-map-js@1.2.1: {} + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + source-map@0.6.1: {} + source-map@0.7.6: {} + space-separated-tokens@2.0.2: {} spdx-compare@1.0.0: @@ -7237,6 +7964,10 @@ snapshots: supports-color@10.2.2: {} + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + supports-color@8.1.1: dependencies: has-flag: 4.0.0 @@ -7260,6 +7991,23 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + tapable@2.3.3: {} + + tar@7.5.22: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.3 + minizlib: 3.1.0 + yallist: 5.0.0 + + terser@5.51.2: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.18.0 + commander: 2.20.3 + source-map-support: 0.5.21 + timers-browserify@2.0.12: dependencies: setimmediate: 1.0.5 @@ -7294,6 +8042,14 @@ snapshots: dependencies: typescript: 6.0.3 + ts-loader@9.6.2(typescript@4.9.5)(webpack@5.110.1): + dependencies: + chalk: 4.1.2 + picomatch: 4.0.5 + source-map: 0.7.6 + typescript: 4.9.5 + webpack: 5.110.1(esbuild@0.28.1)(lightningcss@1.33.0)(postcss@8.5.26)(webpack-cli@4.10.0) + ts-md5@2.0.1: {} tty-browserify@0.0.1: {} @@ -7321,12 +8077,16 @@ snapshots: typescript-event-target@1.1.2: {} + typescript@4.9.5: {} + typescript@5.9.3: {} typescript@6.0.3: {} ufo@1.6.4: {} + undici-types@5.26.5: {} + unicorn-magic@0.4.0: {} unified@11.0.5: @@ -7378,7 +8138,7 @@ snapshots: pathe: 2.0.3 picomatch: 4.0.5 - unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(yaml@2.9.0)): + unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(terser@5.51.2)(yaml@2.9.0))(webpack@5.110.1): dependencies: '@jridgewell/remapping': 2.3.5 picomatch: 4.0.5 @@ -7387,7 +8147,8 @@ snapshots: esbuild: 0.28.1 rolldown: 1.2.3 rollup: 4.62.4 - vite: 7.3.6(lightningcss@1.33.0)(sass@1.102.0)(yaml@2.9.0) + vite: 7.3.6(lightningcss@1.33.0)(sass@1.102.0)(terser@5.51.2)(yaml@2.9.0) + webpack: 5.110.1(esbuild@0.28.1)(lightningcss@1.33.0)(postcss@8.5.26)(webpack-cli@4.10.0) update-browserslist-db@1.3.0(browserslist@4.28.8): dependencies: @@ -7421,6 +8182,8 @@ snapshots: is-typed-array: 1.1.15 which-typed-array: 1.1.22 + uuid@14.0.2: {} + vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 @@ -7431,11 +8194,11 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-plugin-css-injected-by-js@4.0.1(vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(yaml@2.9.0)): + vite-plugin-css-injected-by-js@4.0.1(vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(terser@5.51.2)(yaml@2.9.0)): dependencies: - vite: 7.3.6(lightningcss@1.33.0)(sass@1.102.0)(yaml@2.9.0) + vite: 7.3.6(lightningcss@1.33.0)(sass@1.102.0)(terser@5.51.2)(yaml@2.9.0) - vite-plugin-dts@4.5.4(rollup@4.62.4)(supports-color@10.2.2)(typescript@6.0.3)(vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(yaml@2.9.0)): + vite-plugin-dts@4.5.4(rollup@4.62.4)(supports-color@10.2.2)(typescript@6.0.3)(vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(terser@5.51.2)(yaml@2.9.0)): dependencies: '@microsoft/api-extractor': 7.58.12 '@rollup/pluginutils': 5.4.0(rollup@4.62.4) @@ -7448,21 +8211,21 @@ snapshots: magic-string: 0.30.21 typescript: 6.0.3 optionalDependencies: - vite: 7.3.6(lightningcss@1.33.0)(sass@1.102.0)(yaml@2.9.0) + vite: 7.3.6(lightningcss@1.33.0)(sass@1.102.0)(terser@5.51.2)(yaml@2.9.0) transitivePeerDependencies: - '@types/node' - rollup - supports-color - vite-plugin-node-polyfills@0.28.0(rollup@4.62.4)(vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(yaml@2.9.0)): + vite-plugin-node-polyfills@0.28.0(rollup@4.62.4)(vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(terser@5.51.2)(yaml@2.9.0)): dependencies: '@rollup/plugin-inject': 5.0.5(rollup@4.62.4) node-stdlib-browser: 1.3.1 - vite: 7.3.6(lightningcss@1.33.0)(sass@1.102.0)(yaml@2.9.0) + vite: 7.3.6(lightningcss@1.33.0)(sass@1.102.0)(terser@5.51.2)(yaml@2.9.0) transitivePeerDependencies: - rollup - vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(yaml@2.9.0): + vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(terser@5.51.2)(yaml@2.9.0): dependencies: esbuild: 0.28.1 fdir: 6.5.0(picomatch@4.0.5) @@ -7474,6 +8237,7 @@ snapshots: fsevents: 2.3.3 lightningcss: 1.33.0 sass: 1.102.0 + terser: 5.51.2 yaml: 2.9.0 vm-browserify@1.1.2: {} @@ -7496,7 +8260,7 @@ snapshots: dependencies: vue: 3.5.41(typescript@6.0.3) - vue-router@5.2.0(@vue/compiler-sfc@3.5.41)(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)): + vue-router@5.2.0(@vue/compiler-sfc@3.5.41)(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(terser@5.51.2)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(webpack@5.110.1): dependencies: '@babel/generator': 8.0.0 '@vue-macros/common': 3.1.4(vue@3.5.41(typescript@6.0.3)) @@ -7513,13 +8277,13 @@ snapshots: picomatch: 4.0.5 scule: 1.3.0 tinyglobby: 0.2.17 - unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(yaml@2.9.0)) + unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@7.3.6(lightningcss@1.33.0)(sass@1.102.0)(terser@5.51.2)(yaml@2.9.0))(webpack@5.110.1) unplugin-utils: 0.3.2 vue: 3.5.41(typescript@6.0.3) yaml: 2.9.0 optionalDependencies: '@vue/compiler-sfc': 3.5.41 - vite: 7.3.6(lightningcss@1.33.0)(sass@1.102.0)(yaml@2.9.0) + vite: 7.3.6(lightningcss@1.33.0)(sass@1.102.0)(terser@5.51.2)(yaml@2.9.0) transitivePeerDependencies: - '@farmfe/core' - '@rspack/core' @@ -7546,6 +8310,10 @@ snapshots: optionalDependencies: typescript: 6.0.3 + watchpack@2.5.2: + dependencies: + graceful-fs: 4.2.11 + web-streams-polyfill@3.3.3: {} webdav@5.10.0: @@ -7565,8 +8333,69 @@ snapshots: url-join: 5.0.0 url-parse: 1.5.10 + webpack-cli@4.10.0(webpack@5.110.1): + dependencies: + '@discoveryjs/json-ext': 0.5.7 + '@webpack-cli/configtest': 1.2.0(webpack-cli@4.10.0)(webpack@5.110.1) + '@webpack-cli/info': 1.5.0(webpack-cli@4.10.0) + '@webpack-cli/serve': 1.7.0(webpack-cli@4.10.0) + colorette: 2.0.20 + commander: 7.2.0 + cross-spawn: 7.0.6 + fastest-levenshtein: 1.0.16 + import-local: 3.2.0 + interpret: 2.2.0 + rechoir: 0.7.1 + webpack: 5.110.1(esbuild@0.28.1)(lightningcss@1.33.0)(postcss@8.5.26)(webpack-cli@4.10.0) + webpack-merge: 5.10.0 + + webpack-merge@5.10.0: + dependencies: + clone-deep: 4.0.1 + flat: 5.0.2 + wildcard: 2.0.1 + + webpack-sources@3.5.1: {} + webpack-virtual-modules@0.6.2: {} + webpack@5.110.1(esbuild@0.28.1)(lightningcss@1.33.0)(postcss@8.5.26)(webpack-cli@4.10.0): + dependencies: + '@types/estree': 1.0.9 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.18.0 + browserslist: 4.28.8 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.24.5 + es-module-lexer: 2.3.2 + events: 3.3.0 + graceful-fs: 4.2.11 + mime-db: 1.54.0 + minimizer-webpack-plugin: 5.8.0(esbuild@0.28.1)(lightningcss@1.33.0)(postcss@8.5.26)(webpack@5.110.1) + neo-async: 2.6.2 + schema-utils: 4.3.3 + tapable: 2.3.3 + watchpack: 2.5.2 + webpack-sources: 3.5.1 + optionalDependencies: + webpack-cli: 4.10.0(webpack@5.110.1) + transitivePeerDependencies: + - '@minify-html/node' + - '@swc/core' + - '@swc/css' + - '@swc/html' + - clean-css + - cssnano + - csso + - esbuild + - html-minifier-terser + - lightningcss + - postcss + - uglify-js + which-typed-array@1.1.22: dependencies: available-typed-arrays: 1.0.7 @@ -7585,8 +8414,12 @@ snapshots: dependencies: isexe: 2.0.0 + wildcard@2.0.1: {} + word-wrap@1.2.5: {} + wrappy@1.0.2: {} + write-file-atomic@7.0.1: dependencies: signal-exit: 4.1.0 @@ -7597,6 +8430,8 @@ snapshots: xtend@4.0.2: {} + yallist@5.0.0: {} + yaml@2.9.0: {} yocto-queue@0.1.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ac9274d..30c220f 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,6 @@ packages: - integrations/nextcloud + - integrations/joplin - packages/* allowBuilds: From b4564862a7485bb898e6ac8c8b2daab765c5154d Mon Sep 17 00:00:00 2001 From: Nezznee Date: Sat, 26 Sep 2026 14:38:46 +0200 Subject: [PATCH 2/8] Integrate Joplin plugin into the CI --- .github/workflows/ci.yml | 30 ++++++ .github/workflows/release-joplin.yml | 132 +++++++++++++++++++++++++++ integrations/joplin/CHANGELOG.md | 18 ++++ integrations/joplin/integration.json | 17 ++++ package.json | 4 +- scripts/package-joplin.sh | 34 +++++++ 6 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/release-joplin.yml create mode 100644 integrations/joplin/CHANGELOG.md create mode 100644 integrations/joplin/integration.json create mode 100755 scripts/package-joplin.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6faac82..8a165ef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,6 +46,36 @@ jobs: name: butterfly-nextcloud path: dist/nextcloud + joplin: + name: Joplin + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Check out repository + uses: actions/checkout@v7 + + - name: Set up pnpm + uses: pnpm/action-setup@v6 + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: '24.11' + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build Joplin plugin + run: pnpm check:joplin + + - name: Upload Joplin build + uses: actions/upload-artifact@v6 + with: + name: butterfly-joplin + path: integrations/joplin/publish + php: name: PHP runs-on: ubuntu-latest diff --git a/.github/workflows/release-joplin.yml b/.github/workflows/release-joplin.yml new file mode 100644 index 0000000..ca68f51 --- /dev/null +++ b/.github/workflows/release-joplin.yml @@ -0,0 +1,132 @@ +name: Release Joplin integration + +on: + push: + tags: + - 'joplin/v*' + workflow_dispatch: + inputs: + version: + description: Version to package without the leading v + required: true + type: string + publish: + description: Publish the versioned and stable GitHub releases + required: true + default: false + type: boolean + +permissions: + contents: write + +concurrency: + group: release-joplin + cancel-in-progress: false + +jobs: + release: + name: Build and release Joplin + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: Check out repository + uses: actions/checkout@v7 + + - name: Resolve release version + id: release + env: + DISPATCH_VERSION: ${{ inputs.version }} + run: | + if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then + version="${GITHUB_REF_NAME#joplin/v}" + else + version="${DISPATCH_VERSION#v}" + fi + if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then + echo "Invalid version: $version" >&2 + exit 1 + fi + if [[ "$version" == *-* ]]; then + stable=false + prerelease=true + else + stable=true + prerelease=false + fi + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "stable=$stable" >> "$GITHUB_OUTPUT" + echo "prerelease=$prerelease" >> "$GITHUB_OUTPUT" + + - name: Verify integration versions + env: + RELEASE_VERSION: ${{ steps.release.outputs.version }} + run: node scripts/integration-version.mjs joplin "$RELEASE_VERSION" + + - name: Set up pnpm + uses: pnpm/action-setup@v6 + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: '24.11' + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build shared package and Joplin plugin + run: | + pnpm --filter @linwood/butterfly-integration-shared check + pnpm check:joplin + + - name: Create release archives + run: pnpm package:joplin + + - name: Create release notes from changelog + env: + RELEASE_VERSION: ${{ steps.release.outputs.version }} + run: node scripts/changelog-entry.mjs joplin "$RELEASE_VERSION" dist/joplin/release-notes.md + + - name: Upload workflow artifact + uses: actions/upload-artifact@v6 + with: + name: butterfly-joplin-v${{ steps.release.outputs.version }} + path: dist/joplin + + - name: Publish GitHub release + if: github.ref_type == 'tag' || inputs.publish + uses: softprops/action-gh-release@v3 + with: + tag_name: joplin/v${{ steps.release.outputs.version }} + name: Joplin v${{ steps.release.outputs.version }} + body_path: dist/joplin/release-notes.md + prerelease: ${{ steps.release.outputs.prerelease }} + make_latest: false + files: | + dist/joplin/dev.linwood.butterfly-joplin.jpl + dist/joplin/dev.linwood.butterfly-joplin.json + dist/joplin/checksums.txt + + - name: Move stable tag to this release + if: ${{ (github.ref_type == 'tag' || inputs.publish) && steps.release.outputs.stable == 'true' }} + env: + STABLE_TAG: joplin/stable + run: | + git tag --force "$STABLE_TAG" "$GITHUB_SHA" + git push origin "refs/tags/$STABLE_TAG" --force + + - name: Update stable GitHub release + if: ${{ (github.ref_type == 'tag' || inputs.publish) && steps.release.outputs.stable == 'true' }} + uses: softprops/action-gh-release@v3 + with: + tag_name: joplin/stable + name: Joplin Stable + body_path: dist/joplin/release-notes.md + prerelease: true + make_latest: false + overwrite_files: true + files: | + dist/joplin/dev.linwood.butterfly-joplin.jpl + dist/joplin/dev.linwood.butterfly-joplin.json + dist/joplin/checksums.txt diff --git a/integrations/joplin/CHANGELOG.md b/integrations/joplin/CHANGELOG.md new file mode 100644 index 0000000..5944173 --- /dev/null +++ b/integrations/joplin/CHANGELOG.md @@ -0,0 +1,18 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.1.0] - 2026-09-26 + +### Added + +- Open and edit `.bfly` documents in a joplin panel. +- Load and save documents through the Butterfly embed protocol. +- Attach canvas exports as `.svg` to your notes. +- Set the origin (URL) for the butterfly embed as a joplin setting. +- Set the language for the butterfly embed as a joplin setting. diff --git a/integrations/joplin/integration.json b/integrations/joplin/integration.json new file mode 100644 index 0000000..c0ba06c --- /dev/null +++ b/integrations/joplin/integration.json @@ -0,0 +1,17 @@ +{ + "name": "joplin", + "changelogPath": "CHANGELOG.md", + "releaseTagPrefix": "joplin/v", + "versionSources": [ + { + "path": "package.json", + "type": "json", + "property": "version" + }, + { + "path": "src/manifest.json", + "type": "json", + "property": "version" + } + ] +} diff --git a/package.json b/package.json index 20444c5..6293d16 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,9 @@ "check:nextcloud": "pnpm --filter @linwood/butterfly-nextcloud check", "check:policies": "node --test scripts/shared-policy.test.mjs", "check:versions": "node scripts/integration-version.mjs && node --test scripts/integration-version.test.mjs scripts/changelog-entry.test.mjs", - "package:nextcloud": "bash scripts/package-nextcloud.sh" + "check:joplin": "pnpm --filter joplin-plugin-butterfly dist", + "package:nextcloud": "bash scripts/package-nextcloud.sh", + "package:joplin": "bash scripts/package-joplin.sh" }, "engines": { "node": ">=24.11.0 <25", diff --git a/scripts/package-joplin.sh b/scripts/package-joplin.sh new file mode 100755 index 0000000..f67a92e --- /dev/null +++ b/scripts/package-joplin.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +integration_dir="$repo_root/integrations/joplin" +output_dir="${1:-$repo_root/dist/joplin}" + +( + cd "$repo_root" + node scripts/integration-version.mjs joplin +) + +plugin_id="dev.linwood.butterfly-joplin" +jpl_file="$integration_dir/publish/${plugin_id}.jpl" +json_file="$integration_dir/publish/${plugin_id}.json" + +if [[ ! -f "$jpl_file" ]]; then + echo "Missing $jpl_file; build the Joplin plugin first (pnpm --filter joplin-plugin-butterfly dist)." >&2 + exit 1 +fi + +mkdir -p "$output_dir" +output_dir="$(cd "$output_dir" && pwd)" + +cp "$jpl_file" "$output_dir/" +cp "$json_file" "$output_dir/" + +( + cd "$output_dir" + sha256sum "${plugin_id}.jpl" "${plugin_id}.json" > checksums.txt +) + +echo "Joplin plugin written to $output_dir" From 2cc1626f39aa5e048556808a69f7a7df9c8544f5 Mon Sep 17 00:00:00 2001 From: Nezznee Date: Sat, 26 Sep 2026 14:44:37 +0200 Subject: [PATCH 3/8] Fix CI Test --- scripts/integration-version.test.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/integration-version.test.mjs b/scripts/integration-version.test.mjs index 0bf113e..021ed4c 100644 --- a/scripts/integration-version.test.mjs +++ b/scripts/integration-version.test.mjs @@ -7,7 +7,7 @@ import test from 'node:test' import { integrationNames, validateIntegrationVersion } from './integration-version.mjs' test('discovers only integrations with release metadata', async () => { - assert.deepEqual(await integrationNames(), ['nextcloud']) + assert.deepEqual(await integrationNames(), ['joplin','nextcloud']) }) test('validates every declared version source and builds the release tag', async () => { From 04ee8120e3a8aca044367550d3383e24e78cf5b0 Mon Sep 17 00:00:00 2001 From: Nezznee Date: Sat, 26 Sep 2026 15:37:43 +0200 Subject: [PATCH 4/8] Update READMEs for joplin plugin --- README.md | 4 +-- integrations/joplin/README.md | 52 +++++++++++++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3fc9360..e8ddbe5 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ built yet. ```text integrations/ nextcloud/ # implemented and built in CI - joplin/ # reserved; not built + joplin/ # implemented and built in CI obsidian/ # reserved; not built packages/ shared/ # framework-neutral TypeScript helpers @@ -78,7 +78,7 @@ pnpm check ``` See the [Nextcloud integration guide](integrations/nextcloud/README.md) for PHP, -Composer, and Docker setup. Joplin and Obsidian are intentionally excluded from +Composer, and Docker setup. Obsidian is intentionally excluded from the workspace until their implementations exist. ## Releases diff --git a/integrations/joplin/README.md b/integrations/joplin/README.md index 9d5dd65..eb0bf99 100644 --- a/integrations/joplin/README.md +++ b/integrations/joplin/README.md @@ -1,5 +1,51 @@ -# Joplin Plugin +# Butterfly for Joplin -This is your new Joplin plugin. It is suggested that you use this README file to document your plugin. +This Joplin plugin opens and edits `.bfly` files with Butterfly's supported +embed API. Documents are loaded into the editor only while they are open. +The plugin is based on the official [Joplin plugin template](https://github.com/laurent22/joplin/commits/dev/packages/generator-joplin/generators/app/templates) at commit +`e58fcf9402973d8b9f5ae7b340b9b426cf07988e`. -For information on how to build or publish the plugin, please see [GENERATOR_DOC.md](./GENERATOR_DOC.md) +## Requirements + +- Joplin 3.6 or newer + +## Build + +```bash +pnpm install --frozen-lockfile +cd integrations/joplin +pnpm run dist +``` + +This creates the plugin in `publish/dev.linwood.butterfly-joplin.jpl` + +## Checks + +```bash +pnpm --filter @linwood/butterfly-integration-shared check +pnpm check:joplin +``` + +See [the repository contribution guide](../../CONTRIBUTING.md) for the complete +development workflow. + +## How documents are handled + +The plugin adds an action for clicking Markdown image-links in the form of +!\[bfly@](previewId). Doing so will open a joplin panel. +It reads the file from the joplin resource directory, sends the bytes to the +Butterfly iframe with `postMessage`, and writes bytes back when Butterfly emits `save` +by storing them in the temporary directory of the os under `joplin-butterfly-plugin/` +and then uploading them to the joplin resource directory. + +By default, the editor iframe uses `https://preview.butterfly.linwood.dev/embed`. +The user may configure another Butterfly origin under **Tools → Options → Butterfly**. + +Exiting does not save the document but closes the joplin panel containing the iframe. +Only messages from that exact origin and iframe window are accepted. + +## Contributing and security + +Contributions are welcome. Read [CONTRIBUTING.md](../../CONTRIBUTING.md) before +opening a pull request. Please report vulnerabilities according to +[SECURITY.md](../../SECURITY.md), rather than through a public issue. From c6300e9f76b83f079f45156c66e71e5a8b135c89 Mon Sep 17 00:00:00 2001 From: Nezznee Date: Sat, 26 Sep 2026 15:46:04 +0200 Subject: [PATCH 5/8] Update README --- README.md | 5 +++-- integrations/joplin/README.md | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e8ddbe5..fdef65d 100644 --- a/README.md +++ b/README.md @@ -42,8 +42,9 @@ window and origin. ### 📝 Joplin -Planned. The directory is reserved, but the integration is not implemented or -built yet. +Open, create, edit, and save Butterfly documents from within a Joplin panel. + +[**Learn more**](integrations/joplin/README.md) · ### 💎 Obsidian diff --git a/integrations/joplin/README.md b/integrations/joplin/README.md index eb0bf99..f08a620 100644 --- a/integrations/joplin/README.md +++ b/integrations/joplin/README.md @@ -12,6 +12,7 @@ The plugin is based on the official [Joplin plugin template](https://github.com/ ## Build ```bash +corepack enable pnpm install --frozen-lockfile cd integrations/joplin pnpm run dist From b80f6612ac1f6fb18de0acd83bbbe2ac7ad46473 Mon Sep 17 00:00:00 2001 From: Nezznee Date: Sat, 26 Sep 2026 15:47:00 +0200 Subject: [PATCH 6/8] Typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fdef65d..1ff91ce 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ window and origin. Open, create, edit, and save Butterfly documents from within a Joplin panel. -[**Learn more**](integrations/joplin/README.md) · +[**Learn more**](integrations/joplin/README.md) ### 💎 Obsidian From b825809ea470f8e00719c1fed982b69db3bcbf7c Mon Sep 17 00:00:00 2001 From: Nezznee Date: Sat, 17 Oct 2026 23:17:35 +0200 Subject: [PATCH 7/8] - Fix potential cross-document data corruption by using a 'operationLock' - use actual joplin resources in the md so they are recognized by joplin -> use codemirror plugin to render the preview as md-editor and cm.editor read nested markdown links differently - Fix reset readiness attempts afterwards - normalise butterflyOrigin url - Remove transpileOnly - add typescript typecheck to ci --- .github/workflows/release-joplin.yml | 3 + integrations/joplin/package.json | 6 +- integrations/joplin/plugin.config.json | 1 + integrations/joplin/src/butterfly.css | 1 + .../joplin/src/contentScripts/codemirror.ts | 90 +++++++++++++++++++ .../src/contentScripts/markdownItPlugin.js | 4 +- .../joplin/src/contentScripts/webview.ts | 28 +++--- integrations/joplin/src/index.ts | 45 +++++++--- integrations/joplin/src/manifest.json | 2 +- integrations/joplin/src/views/panel.ts | 20 ++++- integrations/joplin/tsconfig.json | 11 ++- integrations/joplin/webpack.config.js | 6 +- packages/shared/package.json | 1 + packages/shared/src/index.ts | 6 +- pnpm-lock.yaml | 43 +++++++++ 15 files changed, 222 insertions(+), 45 deletions(-) create mode 100644 integrations/joplin/src/contentScripts/codemirror.ts diff --git a/.github/workflows/release-joplin.yml b/.github/workflows/release-joplin.yml index ca68f51..4090c55 100644 --- a/.github/workflows/release-joplin.yml +++ b/.github/workflows/release-joplin.yml @@ -75,6 +75,9 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile + - name: TypeScript typecheck + run: pnpm --filter joplin-plugin-butterfly exec tsc --noEmit + - name: Build shared package and Joplin plugin run: | pnpm --filter @linwood/butterfly-integration-shared check diff --git a/integrations/joplin/package.json b/integrations/joplin/package.json index 9864236..a09853c 100644 --- a/integrations/joplin/package.json +++ b/integrations/joplin/package.json @@ -16,6 +16,8 @@ "@linwood/butterfly-integration-shared": "workspace:*" }, "devDependencies": { + "@codemirror/state": "^6.7.6", + "@codemirror/view": "^6.43.13", "@types/node": "^18.7.13", "chalk": "^4.1.0", "copy-webpack-plugin": "^14.0.0", @@ -24,8 +26,8 @@ "tar": "^7.5.22", "ts-loader": "^9.3.1", "typescript": "^4.8.2", + "uuid": "^14.0.2", "webpack": "^5.74.0", - "webpack-cli": "^4.10.0", - "uuid": "^14.0.2" + "webpack-cli": "^4.10.0" } } diff --git a/integrations/joplin/plugin.config.json b/integrations/joplin/plugin.config.json index 31c91b5..a959994 100644 --- a/integrations/joplin/plugin.config.json +++ b/integrations/joplin/plugin.config.json @@ -1,5 +1,6 @@ { "extraScripts": [ + "contentScripts/codemirror.ts", "contentScripts/webview.ts", "views/panel.ts" ] diff --git a/integrations/joplin/src/butterfly.css b/integrations/joplin/src/butterfly.css index 388388b..c078beb 100644 --- a/integrations/joplin/src/butterfly.css +++ b/integrations/joplin/src/butterfly.css @@ -15,4 +15,5 @@ width: 1.25em; margin-right: -2px; margin-left: -2px; + /* use position relative and move it -> no negative padding */ } \ No newline at end of file diff --git a/integrations/joplin/src/contentScripts/codemirror.ts b/integrations/joplin/src/contentScripts/codemirror.ts new file mode 100644 index 0000000..a1ae008 --- /dev/null +++ b/integrations/joplin/src/contentScripts/codemirror.ts @@ -0,0 +1,90 @@ +import {Decoration, type DecorationSet, EditorView, ViewPlugin, ViewUpdate, WidgetType,} from '@codemirror/view'; +import {RangeSet, RangeSetBuilder} from '@codemirror/state'; +import {CodeMirrorControl, ContentScriptContext} from "../../api/types"; + +class ImageWidget extends WidgetType { + private readonly previewId: string; + private readonly resourceDir: string; + private readonly width: number | undefined; + private readonly height: number | undefined; + + constructor(resourceDir: string, previewId: string, width: number | undefined, height: number | undefined) { + super(); + this.previewId = previewId; + this.resourceDir = resourceDir; + this.width = width; + this.height = height; + } + + toDOM() { + const wrapper = document.createElement('div'); + const img = document.createElement('img'); + img.src = 'joplin-content://note-viewer/' + this.resourceDir + '/' + this.previewId + '.svg?t=' + Date.now(); + if (this.width) img.width = this.width; + if (this.height) img.height = this.height; + + wrapper.appendChild(img); + return wrapper; + } + +} + +const imagePlugin = (codeMirrorWrapper: CodeMirrorControl, resourceDir: string) => { + return ViewPlugin.fromClass(class { + decorations: DecorationSet; + + constructor(view: EditorView) { + this.decorations = this.buildDecorations(view); + codeMirrorWrapper.registerCommand('butterfly_updateCodeMirrorEditorHook', () => { + this.decorations = this.buildDecorations(view); + view.update([]); + }) + } + + buildDecorations(view: EditorView): RangeSet { + const builder = new RangeSetBuilder(); + const text = view.state.doc.toString(); + + const butterflyRegex = + /\[(?:!\[bfly\]\(:\/([0-9a-f]{32})\)|]*\bsrc=["']:\/([0-9a-f]{32})["'])[^>]*>)\]\(:\/(?:[0-9a-f]{32})\)/gi; + butterflyRegex.lastIndex = 0; + let match: RegExpExecArray | null; + while ((match = butterflyRegex.exec(text)) !== null) { + const widthMatch = match[0].match(/\bwidth=["'](\d+)["']/i); + const width = widthMatch ? Number(widthMatch[1]) : undefined; + const heightMatch = match[0].match(/\bheight=["'](\d+)["']/i); + const height = heightMatch ? Number(heightMatch[1]) : undefined; + + + const previewId = match[1] || match[2]; + builder.add( + match.index + match[0].length, + match.index + match[0].length, + Decoration.widget({ + widget: new ImageWidget(resourceDir, previewId, width, height), + side: -1, + }), + ); + } + + return builder.finish(); + } + + update(update: ViewUpdate) { + if (update.docChanged || update.viewportChanged) { + this.decorations = this.buildDecorations(update.view); + } + } + }, { + decorations: value => value.decorations, + }); + +} +export default (context: ContentScriptContext) => { + return { + plugin: async (codeMirrorWrapper: CodeMirrorControl) => { + const resourceDir: string = await context.postMessage({type: 'resourceDir'}); + codeMirrorWrapper.addExtension(imagePlugin(codeMirrorWrapper, resourceDir)); + }, + }; +}; \ No newline at end of file diff --git a/integrations/joplin/src/contentScripts/markdownItPlugin.js b/integrations/joplin/src/contentScripts/markdownItPlugin.js index ce5ffba..ff50ece 100644 --- a/integrations/joplin/src/contentScripts/markdownItPlugin.js +++ b/integrations/joplin/src/contentScripts/markdownItPlugin.js @@ -1,7 +1,7 @@ module.exports = { - default: function (context) { + default: function (_context) { return { - plugin: function (markdownIt, _pluginOptions) { + plugin: function (_markdownIt, _pluginOptions) { }, assets: function () { return [ diff --git a/integrations/joplin/src/contentScripts/webview.ts b/integrations/joplin/src/contentScripts/webview.ts index 15502d1..b05072d 100644 --- a/integrations/joplin/src/contentScripts/webview.ts +++ b/integrations/joplin/src/contentScripts/webview.ts @@ -2,33 +2,35 @@ declare const webviewApi: { postMessage: (contentScriptId: string, message: any) document.addEventListener('click', (event) => { handleInteraction(event); -}); +}, true); document.addEventListener("keydown", (event) => { if (!(event.key === 'Enter' || event.key === ' ')) return; handleInteraction(event); }); +const idPattern = /^[0-9a-f]{32}$/; + function handleInteraction(event: UIEvent) { - const target: HTMLImageElement | null = (event.target as HTMLImageElement).closest('img[alt^="bfly@"]'); - if (!target) return; - const documentIdMatcher = target.alt.match(/^bfly@([0-9a-f]{32})$/); - if (!documentIdMatcher) return; - const documentId = documentIdMatcher[1]; - const previewId = target.dataset.resourceId; - if (!previewId) return; + const imgTarget: HTMLImageElement | null = (event.target as HTMLImageElement); + if (!imgTarget || imgTarget.alt !== 'bfly') return; + const previewId = imgTarget.dataset.resourceId; + if (!previewId || !idPattern.test(previewId)) return; + const aTarget: HTMLAnchorElement | null = imgTarget.parentElement as HTMLAnchorElement; + if (!aTarget) return; + const documentId = aTarget.dataset.resourceId; + if (!documentId || !idPattern.test(documentId)) return; const message = {documentId, previewId}; + + event.preventDefault(); + event.stopPropagation(); webviewApi.postMessage('butterfly-md', message); } const processImages = () => { - const images = document.querySelectorAll('img[alt^="bfly@"]'); + const images = document.querySelectorAll('img[alt="bfly"]'); for (const image of images) { - if (!image.alt.match(/^bfly@([0-9a-f]{32})$/)) - continue; image.classList.add('butterfly-clickable-image'); - image.tabIndex = 0; - image.role = 'button'; image.title = 'Butterfly'; // ask plugin which title to use? } }; diff --git a/integrations/joplin/src/index.ts b/integrations/joplin/src/index.ts index 426c4bd..047510c 100644 --- a/integrations/joplin/src/index.ts +++ b/integrations/joplin/src/index.ts @@ -1,5 +1,5 @@ import joplin from 'api'; -import {ContentScriptType, SettingItemType, ToolbarButtonLocation} from "../api/types"; +import {ContentScriptType, SettingItemType, ToastType, ToolbarButtonLocation} from "../api/types"; import {tmpdir} from 'os' import {sep} from 'path' import {v4 as uuidv4} from 'uuid' @@ -35,7 +35,7 @@ joplin.plugins.register({ section: 'LinwoodButterfly', }, }); - await joplin.settings.onChange(async (event: ChangeEvent) => { + await joplin.settings.onChange(async (_event: ChangeEvent) => { await updateIFrameUrl(); }); let panelReadinessResolver: (() => void) | null = null; @@ -51,6 +51,11 @@ joplin.plugins.register({ 'butterfly-md', 'contentScripts/markdownItPlugin.js' ); + await joplin.contentScripts.register( + ContentScriptType.CodeMirrorPlugin, + 'butterfly-cm', + 'contentScripts/codemirror.js', + ); await joplin.commands.register({ name: 'clickEditorButton', @@ -79,6 +84,12 @@ joplin.plugins.register({ await joplin.views.panels.onMessage(panel, async (message: any) => { switch (message.type) { + case 'error': + await joplin.views.dialogs.showToast({ + type: ToastType.Error, + message: 'Butterfly-Embed returned an error.', + }); + return; case 'panelReady': panelReadinessResolver?.(); panelReadinessResolver = null; @@ -86,6 +97,12 @@ joplin.plugins.register({ return; case 'getSetting': return await joplin.settings.value(message.data); + case 'embedNotReady': + await joplin.views.dialogs.showToast({ + type: ToastType.Error, + message: 'Butterfly-Embed did not become ready.', + }); + // fallthrough case 'exit': loaded = null; await joplin.views.panels.show(panel, false); @@ -98,12 +115,9 @@ joplin.plugins.register({ ['resources', loaded.previewId], null, { - title: 'temp.svg', - filename: 'temp.svg', updated_time: Date.now(), user_updated_time: Date.now(), blob_updated_time: Date.now(), - mime: 'image/svg+xml' }, [{ path: `${tempFolder}${loaded.previewId}.svg` @@ -113,12 +127,9 @@ joplin.plugins.register({ ['resources', loaded.documentId], null, { - title: 'myfile.bfly', - filename: 'myfile.bfly', updated_time: Date.now(), user_updated_time: Date.now(), blob_updated_time: Date.now(), - mime: 'application/x-butterfly' }, [{ path: `${tempFolder}${loaded.documentId}.bfly` @@ -164,12 +175,18 @@ joplin.plugins.register({ path: `${tempFolder}${loaded.documentId}.bfly` }] ); - await joplin.commands.execute('insertText', `\n![bfly@${loaded.documentId}](:/${loaded.previewId})\n`) + await joplin.commands.execute('insertText', `\n[![bfly](:/${loaded.previewId})](:/${loaded.documentId})\n`); } + await joplin.commands.execute('editor.execCommand', {name: 'butterfly_updateCodeMirrorEditorHook'}); return; } - }); + }) await joplin.views.panels.addScript(panel, '/views/panel.js'); + await joplin.contentScripts.onMessage('butterfly-cm', async (message: { type: string }) => { + if (message.type === 'resourceDir') { + return await joplin.settings.globalValue('resourceDir'); + } + }); await joplin.contentScripts.onMessage('butterfly-md', async (message: any) => { loaded = new ButterflyDocument(message.documentId, message.previewId); if (!await joplin.views.panels.visible(panel)) { @@ -188,10 +205,14 @@ joplin.plugins.register({ async function waitForPanelReadiness(): Promise { return new Promise((resolve, reject) => { panelReadinessResolver = resolve; - window.setTimeout(() => { + window.setTimeout(async () => { if (panelReadinessResolver === resolve) { panelReadinessResolver = null; reject(new Error('Panel did not become ready')); + await joplin.views.dialogs.showToast({ + type: ToastType.Error, + message: 'Butterfly-Panel in Joplin did not become ready.', + }); } }, 5000); }); @@ -201,6 +222,4 @@ joplin.plugins.register({ return uuidv4().replace(/-/g, '') } } - - }); \ No newline at end of file diff --git a/integrations/joplin/src/manifest.json b/integrations/joplin/src/manifest.json index 6391520..019ebfd 100644 --- a/integrations/joplin/src/manifest.json +++ b/integrations/joplin/src/manifest.json @@ -4,7 +4,7 @@ "app_min_version": "3.6", "version": "0.1.0", "name": "Infinite Canvas with Butterfly", - "description": "Seamlessly create and attach your Linwood Butterfly canvases as .png/.svg to your notes.", + "description": "Seamlessly create and attach your Linwood Butterfly canvases as .svg to your notes.", "author": "Linwood", "homepage_url": "https://butterfly.linwood.dev/", "repository_url": "https://github.com/LinwoodDev/ButterflyIntegrations/", diff --git a/integrations/joplin/src/views/panel.ts b/integrations/joplin/src/views/panel.ts index 8cb35f8..7805824 100644 --- a/integrations/joplin/src/views/panel.ts +++ b/integrations/joplin/src/views/panel.ts @@ -1,11 +1,11 @@ import { + BUTTERFLY_EDITOR_MESSAGE_TYPES, ButterflyEditorMessage, ButterflyHostMessageType, createButterflyEmbedUrl, parseButterflyEditorMessageEvent, postButterflyHostMessage, toDocumentBytes, toPostMessageBytes -// @ts-ignore } from "@linwood/butterfly-integration-shared"; import {WebviewApi} from "../../api/types"; @@ -17,11 +17,15 @@ let previewResolver: ((value: string) => void) | null; let butterflyOrigin: string; +let operationLock: ButterflyHostMessageType | null; + const iframe = document.querySelector("#butterfly") as HTMLIFrameElement; window.addEventListener('message', async (event: MessageEvent) => { const parsed: ButterflyEditorMessage | null = parseButterflyEditorMessageEvent(event, butterflyOrigin, iframe?.contentWindow ?? null); if (!parsed) return; + if (parsed.type === operationLock) operationLock = null; + console.log(`Received from Butterfly: ${parsed.type}`); switch (parsed.type) { case 'getData': if (pendingDocument) { @@ -40,9 +44,12 @@ window.addEventListener('message', async (event: MessageEvent) => { }); break; case 'renderSVG': - previewResolver?.(parsed.message); + previewResolver?.(parsed.message as string); previewResolver = null; break; + case 'error': + webviewApi.postMessage({type: 'error'}); + break; } }); @@ -60,6 +67,8 @@ async function retrievePreview(): Promise { } webviewApi.onMessage((pluginMsg: any) => { + console.log(`Received from plugin: ${pluginMsg.message.type};opLock=${operationLock}`); + if (operationLock) return; switch (pluginMsg.message.type) { case 'setData': pendingDocument = toPostMessageBytes(pluginMsg.message.data); @@ -69,8 +78,8 @@ webviewApi.onMessage((pluginMsg: any) => { sendToButterfly('reset'); break; case 'config': - butterflyOrigin = pluginMsg.message.origin; - iframe.src = createButterflyEmbedUrl(butterflyOrigin + '/embed', window.location.href, { + butterflyOrigin = new URL(pluginMsg.message.origin).origin; + iframe.src = createButterflyEmbedUrl(new URL('/embed', butterflyOrigin).toString(), window.location.href, { save: true, language: pluginMsg.message.language }); @@ -79,6 +88,9 @@ webviewApi.onMessage((pluginMsg: any) => { }); function sendToButterfly(type: ButterflyHostMessageType, message?: unknown) { + if ((BUTTERFLY_EDITOR_MESSAGE_TYPES as readonly string[]).includes(type)) + operationLock = type; + console.log(`Sending to Butterfly: ${type}`); postButterflyHostMessage(iframe?.contentWindow ?? null, butterflyOrigin, type, message) } diff --git a/integrations/joplin/tsconfig.json b/integrations/joplin/tsconfig.json index 426046f..d336eda 100644 --- a/integrations/joplin/tsconfig.json +++ b/integrations/joplin/tsconfig.json @@ -2,10 +2,17 @@ "compilerOptions": { "outDir": "./dist/", "module": "commonjs", - "target": "es2015", + "target": "es2022", "jsx": "react", "allowJs": true, "baseUrl": ".", + "lib": [ + "ES2022", + "DOM", + "DOM.Iterable" + ], "skipLibCheck": true, + "strict": true, + "noImplicitAny": true, } -} +} \ No newline at end of file diff --git a/integrations/joplin/webpack.config.js b/integrations/joplin/webpack.config.js index 9f7cbc8..66309bf 100644 --- a/integrations/joplin/webpack.config.js +++ b/integrations/joplin/webpack.config.js @@ -185,10 +185,7 @@ const baseConfig = { { test: /\.tsx?$/, use: { - loader: 'ts-loader', - options: { - transpileOnly: true, // no type / syntax checking for files from shared - }, + loader: 'ts-loader' }, exclude: /node_modules/, }, @@ -400,7 +397,6 @@ function main(environ) { return configs[configName]; } - module.exports = (env) => { let exportedConfigs = []; diff --git a/packages/shared/package.json b/packages/shared/package.json index 447cece..0d853d3 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -5,6 +5,7 @@ "description": "Shared browser helpers for Butterfly integrations", "license": "AGPL-3.0-or-later", "type": "module", + "main": "./src/index.ts", "exports": { ".": "./src/index.ts" }, diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 85e7d11..5b69496 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1,3 +1,3 @@ -export * from './document.ts' -export * from './embed.ts' -export * from './version.ts' +export * from './document' +export * from './embed' +export * from './version' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bed23ab..1232337 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,6 +19,12 @@ importers: specifier: workspace:* version: link:../../packages/shared devDependencies: + '@codemirror/state': + specifier: ^6.7.6 + version: 6.7.6 + '@codemirror/view': + specifier: ^6.43.13 + version: 6.43.13 '@types/node': specifier: ^18.7.13 version: 18.19.130 @@ -189,6 +195,12 @@ packages: peerDependencies: vue: ^3.2.0 + '@codemirror/state@6.7.6': + resolution: {integrity: sha512-kAz+AncRtKuIknedxT1bq4XwXv4UowhbkHU1myPrtVb/jZtImWuV5BXzv5vK6i3kYACsdiZiQKFQQ5Mq7elW8w==} + + '@codemirror/view@6.43.13': + resolution: {integrity: sha512-sihaFrUzAsYBQsL9J2t69y8nfMQGwcYmggAZsk+kjPbjYZMyuf2hU8tUNTZ+P+isb6XRr8JE22TZlJxBoVdH1A==} + '@csstools/css-calc@3.3.0': resolution: {integrity: sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==} engines: {node: '>=20.19.0'} @@ -684,6 +696,9 @@ packages: '@keyv/serialize@1.1.1': resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + '@marijn/find-cluster-break@1.0.4': + resolution: {integrity: sha512-Wy0V7+SGUjnF9/TkiM1hKVDPj7jKXduPNboMVtHTA8dySMURWqfg/JZ9E2Sq8JgSJmkl7k7Qe9FLeMSrSraWmQ==} + '@microsoft/api-extractor-model@7.33.10': resolution: {integrity: sha512-uPUK17xGxeQ3av6TN7awp+dTTSTvx8fZC0XFL1gyt7hFapYuxND3XLXH8vkmI7UjfW92oogzFAFqHSifmJROaw==} @@ -1884,6 +1899,9 @@ packages: create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + crelt@1.0.7: + resolution: {integrity: sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -3653,6 +3671,9 @@ packages: resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==} engines: {node: '>=18'} + style-mod@4.1.4: + resolution: {integrity: sha512-XXWIQt633/EpAFx8aZDOTjBzrCaGmhvEQlQo6MVPfa2OzO2cWo+4hV9h+6UkHYlXGfy+ODXKUdP7Pthmcu5ATw==} + style-to-js@1.1.21: resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} @@ -4054,6 +4075,9 @@ packages: typescript: optional: true + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + watchpack@2.5.2: resolution: {integrity: sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==} engines: {node: '>=10.13.0'} @@ -4229,6 +4253,17 @@ snapshots: material-colors: 1.2.6 vue: 3.5.41(typescript@6.0.3) + '@codemirror/state@6.7.6': + dependencies: + '@marijn/find-cluster-break': 1.0.4 + + '@codemirror/view@6.43.13': + dependencies: + '@codemirror/state': 6.7.6 + crelt: 1.0.7 + style-mod: 4.1.4 + w3c-keyname: 2.2.8 + '@csstools/css-calc@3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': dependencies: '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) @@ -4548,6 +4583,8 @@ snapshots: '@keyv/serialize@1.1.1': {} + '@marijn/find-cluster-break@1.0.4': {} + '@microsoft/api-extractor-model@7.33.10': dependencies: '@microsoft/tsdoc': 0.16.0 @@ -5842,6 +5879,8 @@ snapshots: create-require@1.1.1: {} + crelt@1.0.7: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -7868,6 +7907,8 @@ snapshots: dependencies: '@tokenizer/token': 0.3.0 + style-mod@4.1.4: {} + style-to-js@1.1.21: dependencies: style-to-object: 1.0.14 @@ -8310,6 +8351,8 @@ snapshots: optionalDependencies: typescript: 6.0.3 + w3c-keyname@2.2.8: {} + watchpack@2.5.2: dependencies: graceful-fs: 4.2.11 From b5f2cb965017fa8ac5c76f9f7d52a2c028b4caf3 Mon Sep 17 00:00:00 2001 From: Nezznee Date: Sat, 17 Oct 2026 23:35:20 +0200 Subject: [PATCH 8/8] Fix negative margin for settings icon --- integrations/joplin/src/butterfly.css | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/integrations/joplin/src/butterfly.css b/integrations/joplin/src/butterfly.css index c078beb..a24d5ea 100644 --- a/integrations/joplin/src/butterfly.css +++ b/integrations/joplin/src/butterfly.css @@ -13,7 +13,6 @@ .butterfly-icon-settings::before { width: 1.25em; - margin-right: -2px; - margin-left: -2px; - /* use position relative and move it -> no negative padding */ + position: relative; + left: -2px; } \ No newline at end of file