Maintenance update; updated packages, updated jest tests to use ESM

This commit is contained in:
Franco Speziali 2026-05-27 14:31:10 +02:00
parent e800147eaa
commit 0020381c08
32 changed files with 5467 additions and 15071 deletions

View File

@ -1,27 +1,22 @@
# generator-joplin # Plugin development
Scaffolds out a new Joplin plugin This documentation describes how to create a plugin, and how to work with the plugin builder framework and API.
## Installation ## 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/)). 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 ```bash
npm install -g yo npm install -g yo@4.3.1
npm install -g generator-joplin npm install -g generator-joplin
``` ```
Then generate your new project: Then generate your new project:
```bash ```bash
yo joplin yo --node-package-manager npm joplin
``` ```
## Development
To test the generator for development purposes, follow the instructions there: https://yeoman.io/authoring/#running-the-generator
This is a template to create a new Joplin plugin.
## Structure ## Structure
The main two files you will want to look at are: The main two files you will want to look at are:
@ -39,6 +34,10 @@ 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. 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 ## 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: 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:
@ -67,6 +66,13 @@ By default, the compiler (webpack) is going to compile `src/index.ts` only (as w
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. 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 ## License
MIT © Laurent Cozic MIT © Laurent Cozic

14
api/Joplin.d.ts vendored
View File

@ -10,6 +10,8 @@ import JoplinSettings from './JoplinSettings';
import JoplinContentScripts from './JoplinContentScripts'; import JoplinContentScripts from './JoplinContentScripts';
import JoplinClipboard from './JoplinClipboard'; import JoplinClipboard from './JoplinClipboard';
import JoplinWindow from './JoplinWindow'; import JoplinWindow from './JoplinWindow';
import BasePlatformImplementation from '../BasePlatformImplementation';
import JoplinImaging from './JoplinImaging';
/** /**
* This is the main entry point to the Joplin API. You can access various services using the provided accessors. * This is the main entry point to the Joplin API. You can access various services using the provided accessors.
* *
@ -25,6 +27,7 @@ import JoplinWindow from './JoplinWindow';
export default class Joplin { export default class Joplin {
private data_; private data_;
private plugins_; private plugins_;
private imaging_;
private workspace_; private workspace_;
private filters_; private filters_;
private commands_; private commands_;
@ -34,9 +37,11 @@ export default class Joplin {
private contentScripts_; private contentScripts_;
private clipboard_; private clipboard_;
private window_; private window_;
constructor(implementation: any, plugin: Plugin, store: any); private implementation_;
constructor(implementation: BasePlatformImplementation, plugin: Plugin, store: any);
get data(): JoplinData; get data(): JoplinData;
get clipboard(): JoplinClipboard; get clipboard(): JoplinClipboard;
get imaging(): JoplinImaging;
get window(): JoplinWindow; get window(): JoplinWindow;
get plugins(): JoplinPlugins; get plugins(): JoplinPlugins;
get workspace(): JoplinWorkspace; get workspace(): JoplinWorkspace;
@ -63,6 +68,13 @@ export default class Joplin {
* - [fs-extra](https://www.npmjs.com/package/fs-extra) * - [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) * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/nativeModule)
*
* <span class="platform-desktop">desktop</span>
*/ */
require(_path: string): any; require(_path: string): any;
versionInfo(): Promise<import("./types").VersionInfo>;
/**
* Tells whether the current theme is a dark one or not.
*/
shouldUseDarkColors(): Promise<boolean>;
} }

View File

@ -1,17 +1,24 @@
import { ClipboardContent } from './types';
export default class JoplinClipboard { export default class JoplinClipboard {
private electronClipboard_; private electronClipboard_;
private electronNativeImage_; private electronNativeImage_;
constructor(electronClipboard: any, electronNativeImage: any); constructor(electronClipboard: any, electronNativeImage: any);
readText(): Promise<string>; readText(): Promise<string>;
writeText(text: string): Promise<void>; writeText(text: string): Promise<void>;
/** <span class="platform-desktop">desktop</span> */
readHtml(): Promise<string>; readHtml(): Promise<string>;
/** <span class="platform-desktop">desktop</span> */
writeHtml(html: string): Promise<void>; writeHtml(html: string): Promise<void>;
/** /**
* Returns the image in [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) format. * Returns the image in [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) format.
*
* <span class="platform-desktop">desktop</span>
*/ */
readImage(): Promise<string>; readImage(): Promise<string>;
/** /**
* Takes an image in [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) format. * Takes an image in [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) format.
*
* <span class="platform-desktop">desktop</span>
*/ */
writeImage(dataUrl: string): Promise<void>; writeImage(dataUrl: string): Promise<void>;
/** /**
@ -20,4 +27,19 @@ export default class JoplinClipboard {
* For example [ 'text/plain', 'text/html' ] * For example [ 'text/plain', 'text/html' ]
*/ */
availableFormats(): Promise<string[]>; availableFormats(): Promise<string[]>;
/**
* Writes multiple formats to the clipboard simultaneously.
* This allows setting both text/plain and text/html at the same time.
*
* <span class="platform-desktop">desktop</span>
*
* @example
* ```typescript
* await joplin.clipboard.write({
* text: 'Plain text version',
* html: '<strong>HTML version</strong>'
* });
* ```
*/
write(content: ClipboardContent): Promise<void>;
} }

View File

@ -1,4 +1,5 @@
import { Command } from './types'; import { Command } from './types';
import Plugin from '../Plugin';
/** /**
* This class allows executing or registering new Joplin commands. Commands * This class allows executing or registering new Joplin commands. Commands
* can be executed or associated with * can be executed or associated with
@ -13,13 +14,24 @@ import { Command } from './types';
* now, are not well documented. You can find the list directly on GitHub * now, are not well documented. You can find the list directly on GitHub
* though at the following locations: * though at the following locations:
* *
* * [Main screen commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/gui/MainScreen/commands) * * [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) * * [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) * * [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 * To view what arguments are supported, you can open any of these files
* and look at the `execute()` command. * 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 * ## Executing editor commands
* *
* There might be a situation where you want to invoke editor commands * There might be a situation where you want to invoke editor commands
@ -49,9 +61,10 @@ import { Command } from './types';
* *
*/ */
export default class JoplinCommands { export default class JoplinCommands {
private plugin_;
constructor(plugin_: Plugin);
/** /**
* <span class="platform-desktop">desktop</span> Executes the given * Executes the given command.
* command.
* *
* The command can take any number of arguments, and the supported * The command can take any number of arguments, and the supported
* arguments will vary based on the command. For custom commands, this * arguments will vary based on the command. For custom commands, this
@ -70,7 +83,7 @@ export default class JoplinCommands {
*/ */
execute(commandName: string, ...args: any[]): Promise<any | void>; execute(commandName: string, ...args: any[]): Promise<any | void>;
/** /**
* <span class="platform-desktop">desktop</span> Registers a new command. * Registers a new command.
* *
* ```typescript * ```typescript
* // Register a new commmand called "testCommand1" * // Register a new commmand called "testCommand1"

View File

@ -21,7 +21,8 @@ export default class JoplinContentScripts {
* for more information. * 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 renderer demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script)
* * [View the editor demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/codemirror_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) * See also the [postMessage demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages)
* *

29
api/JoplinData.d.ts vendored
View File

@ -1,7 +1,8 @@
import { ModelType } from '../../../BaseModel'; import { ModelType } from '../../../BaseModel';
import Plugin from '../Plugin';
import { Path } from './types'; import { Path } from './types';
/** /**
* This module provides access to the Joplin data API: https://joplinapp.org/api/references/rest_api/ * 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. * This is the main way to retrieve data, such as notes, notebooks, tags, etc.
* or to update them or delete them. * or to update them or delete them.
* *
@ -12,12 +13,12 @@ import { Path } from './types';
* 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. * 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: * And each method takes these parameters:
* *
* * `path`: This is an array that represents the path to the resource in the form `["resouceName", "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. * * `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. * * `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. * * `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. * * `files`: (Optional) Used to create new resources and associate them with files.
* *
* Please refer to the [Joplin API documentation](https://joplinapp.org/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. * 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: * For example:
* *
@ -39,6 +40,8 @@ import { Path } from './types';
export default class JoplinData { export default class JoplinData {
private api_; private api_;
private pathSegmentRegex_; private pathSegmentRegex_;
private plugin;
constructor(plugin: Plugin);
private serializeApiBody; private serializeApiBody;
private pathToString; private pathToString;
get(path: Path, query?: any): Promise<any>; get(path: Path, query?: any): Promise<any>;
@ -47,4 +50,24 @@ export default class JoplinData {
delete(path: Path, query?: any): Promise<any>; delete(path: Path, query?: any): Promise<any>;
itemType(itemId: string): Promise<ModelType>; itemType(itemId: string): Promise<ModelType>;
resourcePath(resourceId: string): Promise<string>; resourcePath(resourceId: string): Promise<string>;
/**
* 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<T>(itemType: ModelType, itemId: string, key: string): Promise<T>;
/**
* Sets a note user data. See {@link JoplinData.userDataGet} for more details.
*/
userDataSet<T>(itemType: ModelType, itemId: string, key: string, value: T): Promise<void>;
/**
* Deletes a note user data. See {@link JoplinData.userDataGet} for more details.
*/
userDataDelete(itemType: ModelType, itemId: string, key: string): Promise<void>;
} }

View File

@ -1,3 +1,4 @@
import { FilterHandler } from '../../../eventManager';
/** /**
* @ignore * @ignore
* *
@ -5,6 +6,6 @@
* so for now disable filters. * so for now disable filters.
*/ */
export default class JoplinFilters { export default class JoplinFilters {
on(name: string, callback: Function): Promise<void>; on(name: string, callback: FilterHandler): Promise<void>;
off(name: string, callback: Function): Promise<void>; off(name: string, callback: FilterHandler): Promise<void>;
} }

92
api/JoplinImaging.d.ts vendored Normal file
View File

@ -0,0 +1,92 @@
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<unknown>;
createFromPdf: (path: string, options: CreateFromPdfOptions) => Promise<unknown[]>;
getPdfInfo: (path: string) => Promise<PdfInfo>;
}
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)
*
* <span class="platform-desktop">desktop</span>
*/
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<Handle>;
createFromResource(resourceId: string): Promise<Handle>;
createFromPdfPath(path: string, options?: CreateFromPdfOptions): Promise<Handle[]>;
createFromPdfResource(resourceId: string, options?: CreateFromPdfOptions): Promise<Handle[]>;
getPdfInfoFromPath(path: string): Promise<PdfInfo>;
getPdfInfoFromResource(resourceId: string): Promise<PdfInfo>;
getSize(handle: Handle): Promise<any>;
resize(handle: Handle, options?: ResizeOptions): Promise<string>;
crop(handle: Handle, rectangle: Rectangle): Promise<string>;
toPngFile(handle: Handle, filePath: string): Promise<void>;
/**
* Quality is between 0 and 100
*/
toJpgFile(handle: Handle, filePath: string, quality?: number): Promise<void>;
private tempFilePath;
/**
* Creates a new Joplin resource from the image data. The image will be
* first converted to a JPEG.
*/
toJpgResource(handle: Handle, resourceProps: any, quality?: number): Promise<import("../../database/types").ResourceEntity>;
/**
* Creates a new Joplin resource from the image data. The image will be
* first converted to a PNG.
*/
toPngResource(handle: Handle, resourceProps: any): Promise<import("../../database/types").ResourceEntity>;
/**
* 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<void>;
}

View File

@ -9,7 +9,10 @@ import { ExportModule, ImportModule } from './types';
* *
* See the documentation of the [[ExportModule]] and [[ImportModule]] for more information. * 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/api/references/rest_api/ * 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
*
* <span class="platform-desktop">desktop</span>: While it is possible to register import and export
* modules on mobile, there is no GUI to activate them.
*/ */
export default class JoplinInterop { export default class JoplinInterop {
registerExportModule(module: ExportModule): Promise<void>; registerExportModule(module: ExportModule): Promise<void>;

View File

@ -6,7 +6,7 @@ export interface ChangeEvent {
*/ */
keys: string[]; keys: string[];
} }
export declare type ChangeHandler = (event: ChangeEvent) => void; 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. * 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.
* *
@ -19,8 +19,6 @@ export declare type ChangeHandler = (event: ChangeEvent) => void;
export default class JoplinSettings { export default class JoplinSettings {
private plugin_; private plugin_;
constructor(plugin: Plugin); constructor(plugin: Plugin);
private get keyPrefix();
private namespacedKey;
/** /**
* Registers new settings. * Registers new settings.
* Note that registering a setting item is dynamic and will be gone next time Joplin starts. * Note that registering a setting item is dynamic and will be gone next time Joplin starts.
@ -40,7 +38,15 @@ export default class JoplinSettings {
*/ */
registerSection(name: string, section: SettingSection): Promise<void>; registerSection(name: string, section: SettingSection): Promise<void>;
/** /**
* Gets a setting value (only applies to setting you registered from your plugin) * Gets setting values (only applies to setting you registered from your plugin)
*/
values(keys: string[] | string): Promise<Record<string, unknown>>;
/**
* 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<any>; value(key: string): Promise<any>;
/** /**
@ -48,11 +54,15 @@ export default class JoplinSettings {
*/ */
setValue(key: string, value: any): Promise<void>; setValue(key: string, value: any): Promise<void>;
/** /**
* Gets a global setting value, including app-specific settings and those set by other plugins. * 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: * 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/Setting.ts#L142 * https://github.com/laurent22/joplin/blob/dev/packages/lib/models/settings/builtInMetadata.ts
*/
globalValues(keys: string[]): Promise<any[]>;
/**
* @deprecated Use joplin.settings.globalValues()
*/ */
globalValue(key: string): Promise<any>; globalValue(key: string): Promise<any>;
/** /**

21
api/JoplinViews.d.ts vendored
View File

@ -4,25 +4,40 @@ import JoplinViewsMenuItems from './JoplinViewsMenuItems';
import JoplinViewsMenus from './JoplinViewsMenus'; import JoplinViewsMenus from './JoplinViewsMenus';
import JoplinViewsToolbarButtons from './JoplinViewsToolbarButtons'; import JoplinViewsToolbarButtons from './JoplinViewsToolbarButtons';
import JoplinViewsPanels from './JoplinViewsPanels'; import JoplinViewsPanels from './JoplinViewsPanels';
import JoplinViewsNoteList from './JoplinViewsNoteList';
import JoplinViewsEditors from './JoplinViewsEditor';
/** /**
* This namespace provides access to view-related services. * This namespace provides access to view-related services.
* *
* 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. * ## Creating a view
* 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. *
* 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 { export default class JoplinViews {
private store; private store;
private plugin; private plugin;
private dialogs_;
private panels_; private panels_;
private menuItems_; private menuItems_;
private menus_; private menus_;
private toolbarButtons_; private toolbarButtons_;
private dialogs_;
private editors_;
private noteList_;
private implementation_; private implementation_;
constructor(implementation: any, plugin: Plugin, store: any); constructor(implementation: any, plugin: Plugin, store: any);
get dialogs(): JoplinViewsDialogs; get dialogs(): JoplinViewsDialogs;
get panels(): JoplinViewsPanels; get panels(): JoplinViewsPanels;
get editors(): JoplinViewsEditors;
get menuItems(): JoplinViewsMenuItems; get menuItems(): JoplinViewsMenuItems;
get menus(): JoplinViewsMenus; get menus(): JoplinViewsMenus;
get toolbarButtons(): JoplinViewsToolbarButtons; get toolbarButtons(): JoplinViewsToolbarButtons;
get noteList(): JoplinViewsNoteList;
} }

View File

@ -1,5 +1,5 @@
import Plugin from '../Plugin'; import Plugin from '../Plugin';
import { ButtonSpec, ViewHandle, DialogResult } from './types'; import { ButtonSpec, ViewHandle, DialogResult, Toast } from './types';
/** /**
* Allows creating and managing dialogs. A dialog is modal window that * Allows creating and managing dialogs. A dialog is modal window that
* contains a webview and a row of buttons. You can update the * contains a webview and a row of buttons. You can update the
@ -43,6 +43,18 @@ export default class JoplinViewsDialogs {
* Displays a message box with OK/Cancel buttons. Returns the button index that was clicked - "0" for OK and "1" for "Cancel" * 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<number>; showMessageBox(message: string): Promise<number>;
/**
* Displays a Toast notification in the corner of the application screen.
*/
showToast(toast: Toast): Promise<void>;
/**
* 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
*
* <span class="platform-desktop">desktop</span>
*/
showOpenDialog(options: any): Promise<any>;
/** /**
* Sets the dialog HTML content * Sets the dialog HTML content
*/ */
@ -56,7 +68,9 @@ export default class JoplinViewsDialogs {
*/ */
setButtons(handle: ViewHandle, buttons: ButtonSpec[]): Promise<ButtonSpec[]>; setButtons(handle: ViewHandle, buttons: ButtonSpec[]): Promise<ButtonSpec[]>;
/** /**
* Opens the dialog * Opens the dialog.
*
* On desktop, this closes any copies of the dialog open in different windows.
*/ */
open(handle: ViewHandle): Promise<DialogResult>; open(handle: ViewHandle): Promise<DialogResult>;
/** /**

117
api/JoplinViewsEditor.d.ts vendored Normal file
View File

@ -0,0 +1,117 @@
import Plugin from '../Plugin';
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: any);
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<void>;
/**
* Creates a new editor view
*
* @deprecated
*/
create(id: string): Promise<ViewHandle>;
/**
* Sets the editor HTML content
*/
setHtml(handle: ViewHandle, html: string): Promise<string>;
/**
* Adds and loads a new JS or CSS file into the panel.
*/
addScript(handle: ViewHandle, scriptPath: string): Promise<void>;
/**
* See [[JoplinViewPanels]]
*/
onMessage(handle: ViewHandle, callback: Function): Promise<void>;
/**
* Saves the content of the editor, without calling `onUpdate` for editors in the same window.
*/
saveNote(handle: ViewHandle, props: SaveNoteOptions): Promise<void>;
/**
* 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<void>;
/**
* 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<void>;
/**
* See [[JoplinViewPanels]]
*/
postMessage(handle: ViewHandle, message: any): void;
/**
* Tells whether the editor is active or not.
*/
isActive(handle: ViewHandle): Promise<boolean>;
/**
* 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<boolean>;
}
export {};

View File

@ -4,6 +4,8 @@ import Plugin from '../Plugin';
* Allows creating and managing menu items. * 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) * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/register_command)
*
* <span class="platform-desktop">desktop</span>
*/ */
export default class JoplinViewsMenuItems { export default class JoplinViewsMenuItems {
private store; private store;

View File

@ -4,6 +4,8 @@ import Plugin from '../Plugin';
* Allows creating menus. * Allows creating menus.
* *
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/menu) * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/menu)
*
* <span class="platform-desktop">desktop</span>
*/ */
export default class JoplinViewsMenus { export default class JoplinViewsMenus {
private store; private store;

42
api/JoplinViewsNoteList.d.ts vendored Normal file
View File

@ -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
*
* <img width="250px" src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/note_list/TopToBottom.png"/>
*
* ### Left to right with thumbnails
*
* <img width="250px" src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/note_list/LeftToRight_Thumbnails.png"/>
*
* ### Top to bottom with editable title
*
* <img width="250px" src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/note_list/TopToBottom_Editable.png"/>
*
* <span class="platform-desktop">desktop</span>
*/
export default class JoplinViewsNoteList {
private plugin_;
private store_;
constructor(plugin: Plugin, store: Store);
registerRenderer(renderer: ListRenderer): Promise<void>;
}

View File

@ -1,12 +1,17 @@
import Plugin from '../Plugin'; import Plugin from '../Plugin';
import { ViewHandle } from './types'; import { ViewHandle } from './types';
/** /**
* Allows creating and managing view panels. View panels currently are * Allows creating and managing view panels. View panels allow displaying any HTML
* displayed at the right of the sidebar and allows displaying any HTML * content (within a webview) and updating it in real-time. For example it
* content (within a webview) and update it in real-time. For example it
* could be used to display a table of content for the active note, or * could be used to display a table of content for the active note, or
* display various metadata or graph. * 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) * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/toc)
*/ */
export default class JoplinViewsPanels { export default class JoplinViewsPanels {
@ -75,4 +80,9 @@ export default class JoplinViewsPanels {
* Tells whether the panel is visible or not * Tells whether the panel is visible or not
*/ */
visible(handle: ViewHandle): Promise<boolean>; visible(handle: ViewHandle): Promise<boolean>;
/**
* 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<boolean>;
} }

11
api/JoplinWindow.d.ts vendored
View File

@ -1,17 +1,14 @@
import Plugin from '../Plugin'; import Plugin from '../Plugin';
export interface Implementation {
injectCustomStyles(elementId: string, cssFilePath: string): Promise<void>;
}
export default class JoplinWindow { export default class JoplinWindow {
private plugin_;
private store_; private store_;
private implementation_; constructor(_plugin: Plugin, store: any);
constructor(implementation: Implementation, plugin: Plugin, store: any);
/** /**
* Loads a chrome CSS file. It will apply to the window UI elements, except * 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 * 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) * 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. * for an example.
*
* <span class="platform-desktop">desktop</span>
*/ */
loadChromeCssFile(filePath: string): Promise<void>; loadChromeCssFile(filePath: string): Promise<void>;
/** /**
@ -19,6 +16,8 @@ export default class JoplinWindow {
* exported or printed note. It is the same as the "Custom stylesheet for * 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) * 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. * for an example.
*
* <span class="platform-desktop">desktop</span>
*/ */
loadNoteCssFile(filePath: string): Promise<void>; loadNoteCssFile(filePath: string): Promise<void>;
} }

View File

@ -1,9 +1,6 @@
import Plugin from '../Plugin';
import { FolderEntity } from '../../database/types'; import { FolderEntity } from '../../database/types';
import { Disposable, MenuItem } from './types'; import { Disposable, EditContextMenuFilterObject, FilterHandler } from './types';
export interface EditContextMenuFilterObject {
items: MenuItem[];
}
declare type FilterHandler<T> = (object: T) => Promise<void>;
declare enum ItemChangeEventType { declare enum ItemChangeEventType {
Create = 1, Create = 1,
Update = 2, Update = 2,
@ -13,15 +10,25 @@ interface ItemChangeEvent {
id: string; id: string;
event: ItemChangeEventType; event: ItemChangeEventType;
} }
interface SyncStartEvent {
withErrors: boolean;
}
interface ResourceChangeEvent { interface ResourceChangeEvent {
id: string; id: string;
} }
declare type ItemChangeHandler = (event: ItemChangeEvent) => void; interface NoteContentChangeEvent {
declare type SyncStartHandler = (event: SyncStartEvent) => void; note: any;
declare type ResourceChangeHandler = (event: ResourceChangeEvent) => void; }
interface NoteSelectionChangeEvent {
value: string[];
}
interface NoteAlarmTriggerEvent {
noteId: string;
}
interface SyncCompleteEvent {
withErrors: boolean;
}
type WorkspaceEventHandler<EventType> = (event: EventType) => void;
type ItemChangeHandler = WorkspaceEventHandler<ItemChangeEvent>;
type SyncStartHandler = () => void;
type ResourceChangeHandler = WorkspaceEventHandler<ResourceChangeEvent>;
/** /**
* The workspace service provides access to all the parts of Joplin that * 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 * are being worked on - i.e. the currently selected notes or notebooks as
@ -32,16 +39,17 @@ declare type ResourceChangeHandler = (event: ResourceChangeEvent) => void;
*/ */
export default class JoplinWorkspace { export default class JoplinWorkspace {
private store; private store;
constructor(store: any); private plugin;
constructor(plugin: Plugin, store: any);
/** /**
* Called when a new note or notes are selected. * Called when a new note or notes are selected.
*/ */
onNoteSelectionChange(callback: Function): Promise<Disposable>; onNoteSelectionChange(callback: WorkspaceEventHandler<NoteSelectionChangeEvent>): Promise<Disposable>;
/** /**
* Called when the content of a note changes. * 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. * @deprecated Use `onNoteChange()` instead, which is reliably triggered whenever the note content, or any note property changes.
*/ */
onNoteContentChange(callback: Function): Promise<void>; onNoteContentChange(callback: WorkspaceEventHandler<NoteContentChangeEvent>): Promise<void>;
/** /**
* Called when the content of the current note changes. * Called when the content of the current note changes.
*/ */
@ -54,7 +62,7 @@ export default class JoplinWorkspace {
/** /**
* Called when an alarm associated with a to-do is triggered. * Called when an alarm associated with a to-do is triggered.
*/ */
onNoteAlarmTrigger(handler: Function): Promise<Disposable>; onNoteAlarmTrigger(handler: WorkspaceEventHandler<NoteAlarmTriggerEvent>): Promise<Disposable>;
/** /**
* Called when the synchronisation process is starting. * Called when the synchronisation process is starting.
*/ */
@ -62,14 +70,18 @@ export default class JoplinWorkspace {
/** /**
* Called when the synchronisation process has finished. * Called when the synchronisation process has finished.
*/ */
onSyncComplete(callback: Function): Promise<Disposable>; onSyncComplete(callback: WorkspaceEventHandler<SyncCompleteEvent>): Promise<Disposable>;
/** /**
* Called just before the editor context menu is about to open. Allows * Called just before the editor context menu is about to open. Allows
* adding items to it. * adding items to it.
*
* <span class="platform-desktop">desktop</span>
*/ */
filterEditorContextMenu(handler: FilterHandler<EditContextMenuFilterObject>): void; filterEditorContextMenu(handler: FilterHandler<EditContextMenuFilterObject>): void;
/** /**
* Gets the currently selected note * 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<any>; selectedNote(): Promise<any>;
/** /**
@ -83,5 +95,12 @@ export default class JoplinWorkspace {
* Gets the IDs of the selected notes (can be zero, one, or many). Use the data API to retrieve information about these notes. * 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<string[]>; selectedNoteIds(): Promise<string[]>;
/**
* 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<string>;
} }
export {}; export {};

254
api/noteListType.d.ts vendored Normal file
View File

@ -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.id' | 'note.is_conflict' | '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<string, any>;
export interface OnChangeEvent {
elementId: string;
value: any;
noteId: string;
}
export interface OnClickEvent {
elementId: string;
}
export type OnRenderNoteHandler = (props: any) => Promise<RenderNoteView>;
export type OnChangeHandler = (event: OnChangeEvent) => Promise<void>;
export type OnClickHandler = (event: OnClickEvent) => Promise<void>;
/**
* 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 `<input type='checkbox' ... />` 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.folder.title' | 'note.isWatched' | 'note.tags' | 'note.todoStatusText' | 'note.titleHtml';
export type ListRendererItemValueTemplates = Record<string, string>;
export declare const columnNames: readonly ["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
* `<span>{{value}}</span>` 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
* `
* <div>
* Raw timestamp: {{updatedTimeMs}} <!-- This is **not** auto-rendered ->
* Formatted time: {{note.user_updated_time}} <!-- This is -->
* </div>
* `,
*
* ```
*
* 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 `<span>{{value}}</span>`, the final template will be
* `<span>{{formattedDate}}</span>`.
*
* 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<string>;
/**
* 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
* `
* <div>
* Title: {{note.title}}<br/>
* Date: {{formattedDate}}
* </div>
* `,
*
* 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
* <div>
* <input type="text" value="{{note.title}}" data-id="noteTitleInput"/>
* </div>
* ```
*
* 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:
*
* `<input data-id="todo-checkbox" type="checkbox" {{#note.todo_completed}}checked="checked"{{/note.todo_completed}}>`
*/
onChange?: OnChangeHandler;
}
export interface NoteListColumn {
name: ColumnName;
width: number;
}
export type NoteListColumns = NoteListColumn[];
export declare const defaultWidth = 100;
export declare const defaultListColumns: () => NoteListColumns;
export {};

325
api/noteListType.ts Normal file
View File

@ -0,0 +1,325 @@
/* 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.id' | 'note.is_conflict' | '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 -- Old code before rule was applied
export type RenderNoteView = Record<string, any>;
export interface OnChangeEvent {
elementId: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
value: any;
noteId: string;
}
export interface OnClickEvent {
elementId: string;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
export type OnRenderNoteHandler = (props: any)=> Promise<RenderNoteView>;
export type OnChangeHandler = (event: OnChangeEvent)=> Promise<void>;
export type OnClickHandler = (event: OnClickEvent)=> Promise<void>;
/**
* 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 `<input type='checkbox' ... />` 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.folder.title' |
'note.isWatched' |
'note.tags' |
'note.todoStatusText' |
'note.titleHtml';
export type ListRendererItemValueTemplates = Record<string, string>;
export const columnNames = [
'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
* `<span>{{value}}</span>` 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
* `
* <div>
* Raw timestamp: {{updatedTimeMs}} <!-- This is **not** auto-rendered ->
* Formatted time: {{note.user_updated_time}} <!-- This is -->
* </div>
* `,
*
* ```
*
* 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 `<span>{{value}}</span>`, the final template will be
* `<span>{{formattedDate}}</span>`.
*
* 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<string>;
/**
* 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
* `
* <div>
* Title: {{note.title}}<br/>
* Date: {{formattedDate}}
* </div>
* `,
*
* 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
* <div>
* <input type="text" value="{{note.title}}" data-id="noteTitleInput"/>
* </div>
* ```
*
* 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:
*
* `<input data-id="todo-checkbox" type="checkbox" {{#note.todo_completed}}checked="checked"{{/note.todo_completed}}>`
*/
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;
};

View File

@ -1,3 +1,5 @@
/* eslint-disable multiline-comment-style */
// ================================================================= // =================================================================
// Command API types // Command API types
// ================================================================= // =================================================================
@ -24,6 +26,7 @@ export interface Command {
/** /**
* Code to be ran when the command is executed. It may return a result. * Code to be ran when the command is executed. It may return a result.
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
execute(...args: any[]): Promise<any | void>; execute(...args: any[]): Promise<any | void>;
/** /**
@ -41,9 +44,9 @@ export interface Command {
* Or | \|\| | "noteIsTodo \|\| noteTodoCompleted" * Or | \|\| | "noteIsTodo \|\| noteTodoCompleted"
* And | && | "oneNoteSelected && !inConflictFolder" * And | && | "oneNoteSelected && !inConflictFolder"
* *
* Joplin, unlike VSCode, also supports parenthesis, which allows creating * Joplin, unlike VSCode, also supports parentheses, which allows creating
* more complex expressions such as `cond1 || (cond2 && cond3)`. Only one * more complex expressions such as `cond1 || (cond2 && cond3)`. Only one
* level of parenthesis is possible (nested ones aren't supported). * level of parentheses is possible (nested ones aren't supported).
* *
* Currently the supported context variables aren't documented, but you can * Currently the supported context variables aren't documented, but you can
* find the list below: * find the list below:
@ -113,11 +116,13 @@ export interface ExportModule {
/** /**
* Called when an item needs to be processed. An "item" can be any Joplin object, such as a note, a folder, a notebook, etc. * 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 -- Old code before rule was applied
onProcessItem(context: ExportContext, itemType: number, item: any): Promise<void>; onProcessItem(context: ExportContext, itemType: number, item: any): Promise<void>;
/** /**
* Called when a resource file needs to be exported. * Called when a resource file needs to be exported.
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
onProcessResource(context: ExportContext, resource: any, filePath: string): Promise<void>; onProcessResource(context: ExportContext, resource: any, filePath: string): Promise<void>;
/** /**
@ -179,13 +184,15 @@ export interface ExportContext {
options: ExportOptions; options: ExportOptions;
/** /**
* You can attach your own custom data using this propery - it will then be passed to each event handler, allowing you to keep state from one event to the next. * 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 -- Old code before rule was applied
userData?: any; userData?: any;
} }
export interface ImportContext { export interface ImportContext {
sourcePath: string; sourcePath: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
options: any; options: any;
warnings: string[]; warnings: string[];
} }
@ -195,6 +202,7 @@ export interface ImportContext {
// ================================================================= // =================================================================
export interface Script { export interface Script {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
onStart?(event: any): Promise<void>; onStart?(event: any): Promise<void>;
} }
@ -221,6 +229,14 @@ export enum ModelType {
Command = 16, Command = 16,
} }
export interface VersionInfo {
version: string;
profileVersion: number;
syncVersion: number;
platform: 'desktop'|'mobile';
}
// ================================================================= // =================================================================
// Menu types // Menu types
// ================================================================= // =================================================================
@ -292,6 +308,7 @@ export interface MenuItem {
* Arguments that should be passed to the command. They will be as rest * Arguments that should be passed to the command. They will be as rest
* parameters. * parameters.
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
commandArgs?: any[]; commandArgs?: any[];
/** /**
@ -330,6 +347,8 @@ export type ButtonId = string;
export enum ToolbarButtonLocation { export enum ToolbarButtonLocation {
/** /**
* This toolbar in the top right corner of the application. It applies to the note as a whole, including its metadata. * This toolbar in the top right corner of the application. It applies to the note as a whole, including its metadata.
*
* <span class="platform-desktop">desktop</span>
*/ */
NoteToolbar = 'noteToolbar', NoteToolbar = 'noteToolbar',
@ -343,14 +362,108 @@ export type ViewHandle = string;
export interface EditorCommand { export interface EditorCommand {
name: string; name: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
value?: any; value?: any;
} }
export interface DialogResult { export interface DialogResult {
id: ButtonId; id: ButtonId;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
formData?: any; 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<void>;
export interface ActivationCheckEvent {
handle: ViewHandle;
noteId: string;
}
export type ActivationCheckCallback = (event: ActivationCheckEvent)=> Promise<boolean>;
/**
* 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<void>;
}
export type VisibleHandler = ()=> Promise<void>;
export interface EditContextMenuFilterObject {
items: MenuItem[];
}
export interface EditorActivationCheckFilterObject {
effectiveNoteId: string;
windowId: string;
activatedEditors: {
pluginId: string;
viewId: string;
isActive: boolean;
}[];
}
export type FilterHandler<T> = (object: T)=> Promise<T>;
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 // Settings types
// ================================================================= // =================================================================
@ -384,6 +497,7 @@ export enum SettingStorage {
// Redefine a simplified interface to mask internal details // Redefine a simplified interface to mask internal details
// and to remove function calls as they would have to be async. // and to remove function calls as they would have to be async.
export interface SettingItem { export interface SettingItem {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
value: any; value: any;
type: SettingItemType; type: SettingItemType;
@ -420,6 +534,7 @@ export interface SettingItem {
* This property is required when `isEnum` is `true`. In which case, it * This property is required when `isEnum` is `true`. In which case, it
* should contain a map of value => label. * should contain a map of value => label.
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
options?: Record<any, any>; options?: Record<any, any>;
/** /**
@ -473,10 +588,35 @@ export interface SettingSection {
*/ */
export type Path = string[]; 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 // Content Script types
// ================================================================= // =================================================================
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
export type PostMessageHandler = (message: any)=> Promise<any>; export type PostMessageHandler = (message: any)=> Promise<any>;
/** /**
@ -499,6 +639,88 @@ export interface ContentScriptContext {
postMessage: PostMessageHandler; postMessage: PostMessageHandler;
} }
export interface ContentScriptModuleLoadedEvent {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
userData?: any;
}
export interface ContentScriptModule {
onLoaded?: (event: ContentScriptModuleLoadedEvent)=> void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
plugin: ()=> any;
assets?: ()=> void;
}
export interface MarkdownItContentScriptModule extends Omit<ContentScriptModule, 'plugin'> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
plugin: (markdownIt: any, options: any)=> any;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
type EditorCommandCallback = (...args: any[])=> any;
export interface CodeMirrorControl {
/** Points to a CodeMirror 6 EditorView instance. */
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
editor: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
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 -- Old code before rule was applied
addExtension(extension: any|any[]): void;
supportsCommand(name: string): boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
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 -- Old code before rule was applied
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 -- Old code before rule was applied
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<ContentScriptModule, 'plugin'> {
plugin: (editorControl: CodeMirrorControl)=> void;
}
export enum ContentScriptType { export enum ContentScriptType {
/** /**
* Registers a new Markdown-It plugin, which should follow the template * Registers a new Markdown-It plugin, which should follow the template
@ -508,7 +730,7 @@ export enum ContentScriptType {
* module.exports = { * module.exports = {
* default: function(context) { * default: function(context) {
* return { * return {
* plugin: function(markdownIt, options) { * plugin: function(markdownIt, pluginOptions) {
* // ... * // ...
* }, * },
* assets: { * assets: {
@ -518,6 +740,7 @@ export enum ContentScriptType {
* } * }
* } * }
* ``` * ```
*
* See [the * See [the
* demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script) * demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script)
* for a simple Markdown-it plugin example. * for a simple Markdown-it plugin example.
@ -530,10 +753,7 @@ export enum ContentScriptType {
* *
* - The **required** `plugin` key is the actual Markdown-It plugin - check * - The **required** `plugin` key is the actual Markdown-It plugin - check
* the [official doc](https://github.com/markdown-it/markdown-it) for more * the [official doc](https://github.com/markdown-it/markdown-it) for more
* information. The `options` parameter is of type * information.
* [RuleOptions](https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml.ts),
* which contains a number of options, mostly useful for Joplin's internal
* code.
* *
* - Using the **optional** `assets` key you may specify assets such as JS * - 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 * or CSS that should be loaded in the rendered HTML document. Check for
@ -541,6 +761,50 @@ export enum ContentScriptType {
* plugin](https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml/rules/mermaid.ts) * plugin](https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml/rules/mermaid.ts)
* to see how the data should be structured. * 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
* <div class="joplin-editable">
* ...your html...
* </div>
* ```
* 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
* <div class="joplin-editable">
* <pre
* class="joplin-source"
* data-joplin-language="foo"
* data-joplin-source-open="```foo&NewLine;"
* data-joplin-source-close="```"
* > ... original source here ... </pre>
* ... rendered HTML here ...
* </div>
* ```
*
* 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 * ## Posting messages from the content script to your plugin
* *
* The application provides the following function to allow executing * The application provides the following function to allow executing
@ -610,21 +874,21 @@ export enum ContentScriptType {
* } * }
* ``` * ```
* *
* - The `context` argument is currently unused but could be used later on * - The `context` argument allows communicating with other parts of
* to provide access to your own plugin so that the content script and * your plugin (see below).
* plugin can communicate.
* *
* - The `plugin` key is your CodeMirror plugin. This is where you can * - The `plugin` key is your CodeMirror plugin. This is where you can
* register new commands with CodeMirror or interact with the CodeMirror * register new commands with CodeMirror or interact with the CodeMirror
* instance as needed. * instance as needed.
* *
* - The `codeMirrorResources` key is an array of CodeMirror resources that * - **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 * 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 * of addons, keymaps, and modes. For example, for a plugin that want's to
* enable clojure highlighting in code blocks. `codeMirrorResources` would * enable clojure highlighting in code blocks. `codeMirrorResources` would
* be set to `['mode/clojure/clojure']`. * be set to `['mode/clojure/clojure']`.
* This field is ignored on mobile and when the desktop beta editor is enabled.
* *
* - The `codeMirrorOptions` key contains all the * - **CodeMirror 5 only**: The `codeMirrorOptions` key contains all the
* [CodeMirror](https://codemirror.net/doc/manual.html#config) options * [CodeMirror](https://codemirror.net/doc/manual.html#config) options
* that will be set or changed by this plugin. New options can alse be * that will be set or changed by this plugin. New options can alse be
* declared via * declared via
@ -642,9 +906,11 @@ export enum ContentScriptType {
* must be provided for the plugin to be valid. Having multiple or all * must be provided for the plugin to be valid. Having multiple or all
* provided is also okay. * provided is also okay.
* *
* See also the [demo * See also:
* plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/codemirror_content_script) * - 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. * 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 * ## Posting messages from the content script to your plugin
* *

18871
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,33 +1,34 @@
{ {
"name": "joplin-plugin-event-calendar", "name": "joplin-plugin-event-calendar",
"version": "0.2.2", "version": "0.2.3",
"scripts": { "scripts": {
"test": "jest", "test": "jest",
"dist": "webpack --joplin-plugin-config buildMain && webpack --joplin-plugin-config buildExtraScripts && webpack --joplin-plugin-config createArchive", "dist": "webpack --env joplin-plugin-config=buildMain && webpack --env joplin-plugin-config=buildExtraScripts && webpack --env joplin-plugin-config=createArchive",
"prepare": "npm run dist", "prepare": "npm run dist",
"update": "npm install -g generator-joplin && yo joplin --update" "update": "npm install -g generator-joplin && yo joplin --node-package-manager npm --update --force",
"updateVersion": "webpack --env joplin-plugin-config=updateVersion"
}, },
"license": "MIT", "license": "MIT",
"keywords": [ "keywords": [
"joplin-plugin" "joplin-plugin"
], ],
"devDependencies": { "devDependencies": {
"@types/jest": "^27.4.1", "@types/jest": "^30.0.0",
"@types/node": "^14.0.14", "@types/node": "^18.7.13",
"chalk": "^4.1.0", "chalk": "^4.1.0",
"copy-webpack-plugin": "^6.1.0", "copy-webpack-plugin": "^11.0.0",
"fs-extra": "^9.0.1", "fs-extra": "^10.1.0",
"glob": "^7.1.6", "glob": "^8.0.3",
"jest": "^27.5.1", "jest": "^30.4.2",
"on-build-webpack": "^0.1.0", "on-build-webpack": "^0.1.0",
"prettier": "^2.5.1", "prettier": "^2.5.1",
"tar": "^6.0.5", "tar": "^6.1.11",
"ts-jest": "^27.1.3", "ts-jest": "^29.4.11",
"ts-loader": "^7.0.5", "ts-loader": "^9.3.1",
"ts-node": "^10.7.0", "ts-node": "^10.7.0",
"typescript": "^3.9.3", "typescript": "^4.8.2",
"webpack": "^4.43.0", "webpack": "^5.74.0",
"webpack-cli": "^3.3.11", "webpack-cli": "^4.10.0",
"yargs": "^16.2.0" "yargs": "^16.2.0"
}, },
"dependencies": { "dependencies": {

View File

@ -1,9 +1,8 @@
const { GroupTypes } = require("../../../types"); import DayGrouping from "../../Group/Day/DayGrouping";
const DayGrouping = require("../../Group/Day/DayGrouping").default; import mockSortedEvents from "./mockSortedEvents";
const mockSortedEvents = require("./mockSortedEvents").default;
describe("groupEventsByDay should", () => { describe("groupEventsByDay should", () => {
const eventGrouping = new DayGrouping(mockSortedEvents, GroupTypes.Day); const eventGrouping = new DayGrouping(mockSortedEvents);
test("generate the correct number of groups", () => { test("generate the correct number of groups", () => {
expect(eventGrouping.groups.length).toEqual(8); expect(eventGrouping.groups.length).toEqual(8);

View File

@ -1,9 +1,8 @@
const { GroupTypes } = require("../../../types"); import MonthGrouping from "../../Group/Month/MonthGrouping";
const MonthGrouping = require("../../Group/Month/MonthGrouping").default; import mockSortedEvents from "./mockSortedEvents";
const mockSortedEvents = require("./mockSortedEvents").default;
describe("groupEventsByMonth should", () => { describe("groupEventsByMonth should", () => {
const eventGrouping = new MonthGrouping(mockSortedEvents, GroupTypes.Month); const eventGrouping = new MonthGrouping(mockSortedEvents);
test("generate the correct number of groups", () => { test("generate the correct number of groups", () => {
expect(eventGrouping.groups.length).toEqual(1); expect(eventGrouping.groups.length).toEqual(1);

View File

@ -1,9 +1,8 @@
const { GroupTypes } = require("../../../types"); import WeekGrouping from "../../Group/Week/WeekGrouping";
const WeekGrouping = require("../../Group/Week/WeekGrouping").default; import mockSortedEvents from "./mockSortedEvents";
const mockSortedEvents = require("./mockSortedEvents").default;
describe("groupEventsByWeek should", () => { describe("groupEventsByWeek should", () => {
const eventGrouping = new WeekGrouping(mockSortedEvents, GroupTypes.Week); const eventGrouping = new WeekGrouping(mockSortedEvents);
test("generate the correct number of groups", () => { test("generate the correct number of groups", () => {
expect(eventGrouping.groups.length).toEqual(2); expect(eventGrouping.groups.length).toEqual(2);

View File

@ -1,4 +1,5 @@
const Events = require("../../Events").default; import Event from "src/Calendar/Events/Event";
import Events from "../../Events";
const mockEventsAllValid = [ const mockEventsAllValid = [
{ {
@ -31,7 +32,14 @@ const mockEventsAllInvalid = [
describe("Events constructor should", () => { describe("Events constructor should", () => {
describe("When the events are valid", () => { describe("When the events are valid", () => {
const events = new Events(mockEventsAllValid); const mockData = mockEventsAllValid.map(
({ date, title }) =>
new Event({
date: new Date(date),
title: title,
}),
);
const events = new Events(mockData);
test("generate the correct number of events", () => { test("generate the correct number of events", () => {
expect(events.sortedEvents.length).toEqual(4); expect(events.sortedEvents.length).toEqual(4);
@ -40,7 +48,7 @@ describe("Events constructor should", () => {
test("sort the events by date, ascending", () => { test("sort the events by date, ascending", () => {
const firstEventDate = new Date(events.sortedEvents[0].date).getDate(); const firstEventDate = new Date(events.sortedEvents[0].date).getDate();
const lastEventDate = new Date( const lastEventDate = new Date(
events.sortedEvents[events.sortedEvents.length - 1].date events.sortedEvents[events.sortedEvents.length - 1].date,
).getDate(); ).getDate();
expect(firstEventDate).toEqual(20); expect(firstEventDate).toEqual(20);
expect(lastEventDate).toEqual(27); expect(lastEventDate).toEqual(27);
@ -48,7 +56,14 @@ describe("Events constructor should", () => {
}); });
describe("When the events are invalid", () => { describe("When the events are invalid", () => {
const events = new Events(mockEventsAllInvalid); const mockData = mockEventsAllInvalid.map(
({ date, title }) =>
new Event({
date: new Date(date),
title: title,
}),
);
const events = new Events(mockData);
test("generate the correct number of events", () => { test("generate the correct number of events", () => {
expect(events.sortedEvents.length).toEqual(0); expect(events.sortedEvents.length).toEqual(0);

View File

@ -2,7 +2,7 @@
"manifest_version": 1, "manifest_version": 1,
"id": "com.wemakemachines.joplin.plugin.event-calendar", "id": "com.wemakemachines.joplin.plugin.event-calendar",
"app_min_version": "2.7", "app_min_version": "2.7",
"version": "0.2.2", "version": "0.2.3",
"name": "Event Calendar", "name": "Event Calendar",
"description": "A simple event calendar", "description": "A simple event calendar",
"author": "Franco Speziali", "author": "Franco Speziali",

View File

@ -6,8 +6,5 @@
"jsx": "react", "jsx": "react",
"allowJs": true, "allowJs": true,
"baseUrl": "." "baseUrl": "."
}, }
"exclude": [
"src/**/*.test.ts"
]
} }

View File

@ -6,16 +6,21 @@
// update, you can easily restore the functionality you've added. // update, you can easily restore the functionality you've added.
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
/* eslint-disable no-console */
const path = require('path'); const path = require('path');
const crypto = require('crypto'); const crypto = require('crypto');
const fs = require('fs-extra'); const fs = require('fs-extra');
const chalk = require('chalk'); const chalk = require('chalk');
const CopyPlugin = require('copy-webpack-plugin'); const CopyPlugin = require('copy-webpack-plugin');
const WebpackOnBuildPlugin = require('on-build-webpack');
const tar = require('tar'); const tar = require('tar');
const glob = require('glob'); const glob = require('glob');
const execSync = require('child_process').execSync; 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 rootDir = path.resolve(__dirname);
const userConfigFilename = './plugin.config.json'; const userConfigFilename = './plugin.config.json';
const userConfigPath = path.resolve(rootDir, userConfigFilename); const userConfigPath = path.resolve(rootDir, userConfigFilename);
@ -23,19 +28,34 @@ const distDir = path.resolve(rootDir, 'dist');
const srcDir = path.resolve(rootDir, 'src'); const srcDir = path.resolve(rootDir, 'src');
const publishDir = path.resolve(rootDir, 'publish'); const publishDir = path.resolve(rootDir, 'publish');
const userConfig = Object.assign({}, { const userConfig = {
extraScripts: [], extraScripts: [],
}, fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {}); ...(fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {}),
};
const manifestPath = `${srcDir}/manifest.json`; const manifestPath = `${srcDir}/manifest.json`;
const packageJsonPath = `${rootDir}/package.json`; const packageJsonPath = `${rootDir}/package.json`;
const allPossibleCategories = ['appearance', 'developer tools', 'productivity', 'themes', 'integrations', 'viewer', 'search', 'tags', 'editor', 'files', 'personal knowledge management']; const allPossibleScreenshotsType = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
const manifest = readManifest(manifestPath); const manifest = readManifest(manifestPath);
const pluginArchiveFilePath = path.resolve(publishDir, `${manifest.id}.jpl`); const pluginArchiveFilePath = path.resolve(publishDir, `${manifest.id}.jpl`);
const pluginInfoFilePath = path.resolve(publishDir, `${manifest.id}.json`); 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() { function validatePackageJson() {
const content = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); const content = getPackageJson();
if (!content.name || content.name.indexOf('joplin-plugin-') !== 0) { 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}`)); console.warn(chalk.yellow(`WARNING: To publish the plugin, the package name should start with "joplin-plugin-" (found "${content.name}") in ${packageJsonPath}`));
} }
@ -71,21 +91,45 @@ function currentGitInfo() {
function validateCategories(categories) { function validateCategories(categories) {
if (!categories) return null; if (!categories) return null;
if ((categories.length !== new Set(categories).size)) throw new Error('Repeated categories are not allowed'); 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 => { categories.forEach(category => {
if (!allPossibleCategories.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}\n`); 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) { function readManifest(manifestPath) {
const content = fs.readFileSync(manifestPath, 'utf8'); const content = fs.readFileSync(manifestPath, 'utf8');
const output = JSON.parse(content); const output = JSON.parse(content);
if (!output.id) throw new Error(`Manifest plugin ID is not set in ${manifestPath}`); if (!output.id) throw new Error(`Manifest plugin ID is not set in ${manifestPath}`);
validateCategories(output.categories); validateCategories(output.categories);
validateScreenshots(output.screenshots);
return output; return output;
} }
function createPluginArchive(sourceDir, destPath) { function createPluginArchive(sourceDir, destPath) {
const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true }) const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true, windowsPathsNoEscape: true })
.map(f => f.substr(sourceDir.length + 1)); .map(f => f.substr(sourceDir.length + 1));
if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty'); if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty');
@ -99,18 +143,22 @@ function createPluginArchive(sourceDir, destPath) {
cwd: sourceDir, cwd: sourceDir,
sync: true, sync: true,
}, },
distFiles distFiles,
); );
console.info(chalk.cyan(`Plugin archive has been created in ${destPath}`)); 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) { function createPluginInfo(manifestPath, destPath, jplFilePath) {
const contentText = fs.readFileSync(manifestPath, 'utf8'); const contentText = fs.readFileSync(manifestPath, 'utf8');
const content = JSON.parse(contentText); const content = JSON.parse(contentText);
content._publish_hash = `sha256:${fileSha256(jplFilePath)}`; content._publish_hash = `sha256:${fileSha256(jplFilePath)}`;
content._publish_commit = currentGitInfo(); content._publish_commit = currentGitInfo();
fs.writeFileSync(destPath, JSON.stringify(content, null, '\t'), 'utf8'); writeManifest(destPath, content);
} }
function onBuildCompleted() { function onBuildCompleted() {
@ -137,14 +185,15 @@ const baseConfig = {
}, },
], ],
}, },
...userConfig.webpackOverrides,
}; };
const pluginConfig = Object.assign({}, baseConfig, { const pluginConfig = { ...baseConfig, entry: './src/index.ts',
entry: './src/index.ts',
resolve: { resolve: {
alias: { alias: {
api: path.resolve(__dirname, 'api'), api: path.resolve(__dirname, 'api'),
}, },
fallback: moduleFallback,
// JSON files can also be required from scripts so we include this. // JSON files can also be required from scripts so we include this.
// https://github.com/joplin/plugin-bibtex/pull/2 // https://github.com/joplin/plugin-bibtex/pull/2
extensions: ['.js', '.tsx', '.ts', '.json'], extensions: ['.js', '.tsx', '.ts', '.json'],
@ -171,26 +220,63 @@ const pluginConfig = Object.assign({}, baseConfig, {
}, },
], ],
}), }),
], ] };
});
const extraScriptConfig = Object.assign({}, baseConfig, {
// 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: { resolve: {
alias: { alias: {
api: path.resolve(__dirname, 'api'), api: path.resolve(__dirname, 'api'),
}, },
fallback: moduleFallback,
extensions: ['.js', '.tsx', '.ts', '.json'], extensions: ['.js', '.tsx', '.ts', '.json'],
}, },
});
// We support requiring @codemirror/... libraries through require('@codemirror/...')
externalsType: 'commonjs',
externals: extraScriptExternals,
};
const createArchiveConfig = { const createArchiveConfig = {
stats: 'errors-only', stats: 'errors-only',
entry: './dist/index.js', entry: './dist/index.js',
resolve: {
fallback: moduleFallback,
},
output: { output: {
filename: 'index.js', filename: 'index.js',
path: publishDir, path: publishDir,
}, },
plugins: [new WebpackOnBuildPlugin(onBuildCompleted)], plugins: [{
apply(compiler) {
compiler.hooks.done.tap('archiveOnBuildListener', onBuildCompleted);
},
}],
}; };
function resolveExtraScriptPath(name) { function resolveExtraScriptPath(name) {
@ -222,20 +308,43 @@ function buildExtraScriptConfigs(userConfig) {
for (const scriptName of userConfig.extraScripts) { for (const scriptName of userConfig.extraScripts) {
const scriptPaths = resolveExtraScriptPath(scriptName); const scriptPaths = resolveExtraScriptPath(scriptName);
output.push(Object.assign({}, extraScriptConfig, { output.push({ ...extraScriptConfig, entry: scriptPaths.entry,
entry: scriptPaths.entry, output: scriptPaths.output });
output: scriptPaths.output,
}));
} }
return output; return output;
} }
function main(processArgv) { const increaseVersion = version => {
const yargs = require('yargs/yargs'); try {
const argv = yargs(processArgv).argv; 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 configName = argv['joplin-plugin-config']; 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'); 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 // Webpack configurations run in parallel, while we need them to run in
@ -270,15 +379,22 @@ function main(processArgv) {
fs.mkdirpSync(publishDir); fs.mkdirpSync(publishDir);
} }
if (configName === 'updateVersion') {
updateVersion();
return [];
}
return configs[configName]; return configs[configName];
} }
module.exports = (env) => {
let exportedConfigs = []; let exportedConfigs = [];
try { try {
exportedConfigs = main(process.argv); exportedConfigs = main(env);
} catch (error) { } catch (error) {
console.error(chalk.red(error.message)); console.error(error.message);
process.exit(1); process.exit(1);
} }
@ -288,4 +404,5 @@ if (!exportedConfigs.length) {
process.exit(0); process.exit(0);
} }
module.exports = exportedConfigs; return exportedConfigs;
};