;
96 | /**
97 | * This is where most of the real-time processing will happen. When a note
98 | * is rendered for the first time and every time it changes, this handler
99 | * receives the properties specified in the `dependencies` property. You can
100 | * then process them, load any additional data you need, and once done you
101 | * need to return the properties that are needed in the `itemTemplate` HTML.
102 | * Again, to use the formatted date example, you could have such a renderer:
103 | *
104 | * ```typescript
105 | * dependencies: [
106 | * 'note.title',
107 | * 'note.created_time',
108 | * ],
109 | *
110 | * itemTemplate: // html
111 | * `
112 | *
113 | * Title: {{note.title}}
114 | * Date: {{formattedDate}}
115 | *
116 | * `,
117 | *
118 | * onRenderNote: async (props: any) => {
119 | * const formattedDate = dayjs(props.note.created_time).format();
120 | * return {
121 | * // Also return the props, so that note.title is available from the
122 | * // template
123 | * ...props,
124 | * formattedDate,
125 | * }
126 | * },
127 | * ```
128 | */
129 | onRenderNote: OnRenderNoteHandler;
130 | /**
131 | * This handler allows adding some interacivity to the note renderer -
132 | * whenever an input element within the item is changed (for example, when a
133 | * checkbox is clicked, or a text input is changed), this `onChange` handler
134 | * is going to be called.
135 | *
136 | * You can inspect `event.elementId` to know which element had some changes,
137 | * and `event.value` to know the new value. `event.noteId` also tells you
138 | * what note is affected, so that you can potentially apply changes to it.
139 | *
140 | * You specify the element ID, by setting a `data-id` attribute on the
141 | * input.
142 | *
143 | * For example, if you have such a template:
144 | *
145 | * ```html
146 | *
147 | *
148 | *
149 | * ```
150 | *
151 | * The event handler will receive an event with `elementId` set to
152 | * `noteTitleInput`.
153 | */
154 | onChange?: OnChangeHandler;
155 | }
156 | export {};
157 |
--------------------------------------------------------------------------------
/api/noteListType.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable multiline-comment-style */
2 |
3 | import { Size } from './types';
4 |
5 | // AUTO-GENERATED by generate-database-type
6 | type ListRendererDatabaseDependency = 'folder.created_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.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_';
7 | // AUTO-GENERATED by generate-database-type
8 |
9 | export enum ItemFlow {
10 | TopToBottom = 'topToBottom',
11 | LeftToRight = 'leftToRight',
12 | }
13 |
14 | export type RenderNoteView = Record;
15 |
16 | export interface OnChangeEvent {
17 | elementId: string;
18 | value: any;
19 | noteId: string;
20 | }
21 |
22 | export type OnRenderNoteHandler = (props: any)=> Promise;
23 | export type OnChangeHandler = (event: OnChangeEvent)=> Promise;
24 |
25 | /**
26 | * Most of these are the built-in note properties, such as `note.title`,
27 | * `note.todo_completed`, etc.
28 | *
29 | * Additionally, the `item.*` properties are specific to the rendered item. The
30 | * most important being `item.selected`, which you can use to display the
31 | * selected note in a different way.
32 | *
33 | * Finally some special properties are provided to make it easier to render
34 | * notes. In particular, if possible prefer `note.titleHtml` to `note.title`
35 | * since some important processing has already been done on the string, such as
36 | * handling the search highlighter and escaping. Since it's HTML and already
37 | * escaped you would insert it using `{{{titleHtml}}}` (triple-mustache syntax,
38 | * which disables escaping).
39 | *
40 | * `notes.tag` gives you the list of tags associated with the note.
41 | *
42 | * `note.isWatched` tells you if the note is currently opened in an external
43 | * editor. In which case you would generally display some indicator.
44 | */
45 | export type ListRendererDepependency =
46 | ListRendererDatabaseDependency |
47 | 'item.size.width' |
48 | 'item.size.height' |
49 | 'item.selected' |
50 | 'note.titleHtml' |
51 | 'note.isWatched' |
52 | 'note.tags';
53 |
54 | export interface ListRenderer {
55 | /**
56 | * It must be unique to your plugin.
57 | */
58 | id: string;
59 |
60 | /**
61 | * Can be top to bottom or left to right. Left to right gives you more
62 | * option to set the size of the items since you set both its width and
63 | * height.
64 | */
65 | flow: ItemFlow;
66 |
67 | /**
68 | * The size of each item must be specified in advance for performance
69 | * reasons, and cannot be changed afterwards. If the item flow is top to
70 | * bottom, you only need to specificy the item height (the width will be
71 | * ignored).
72 | */
73 | itemSize: Size;
74 |
75 | /**
76 | * The CSS is relative to the list item container. What will appear in the
77 | * page is essentially `.note-list-item { YOUR_CSS; }`. It means you can use
78 | * child combinator with guarantee it will only apply to your own items. In
79 | * this example, the styling will apply to `.note-list-item > .content`:
80 | *
81 | * ```css
82 | * > .content {
83 | * padding: 10px;
84 | * }
85 | * ```
86 | *
87 | * In order to get syntax highlighting working here, it's recommended
88 | * installing an editor extension such as [es6-string-html VSCode
89 | * extension](https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html)
90 | */
91 | itemCss?: string;
92 |
93 | /**
94 | * List the dependencies that your plugin needs to render the note list
95 | * items. Only these will be passed to your `onRenderNote` handler. Ensure
96 | * that you do not add more than what you need since there is a performance
97 | * penalty for each property.
98 | */
99 | dependencies: ListRendererDepependency[];
100 |
101 | /**
102 | * This is the HTML template that will be used to render the note list item.
103 | * This is a [Mustache template](https://github.com/janl/mustache.js) and it
104 | * will receive the variable you return from `onRenderNote` as tags. For
105 | * example, if you return a property named `formattedDate` from
106 | * `onRenderNote`, you can insert it in the template using `Created date:
107 | * {{formattedDate}}`.
108 | *
109 | * In order to get syntax highlighting working here, it's recommended
110 | * installing an editor extension such as [es6-string-html VSCode
111 | * extension](https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html)
112 | */
113 | itemTemplate: string;
114 |
115 | /**
116 | * This user-facing text is used for example in the View menu, so that your
117 | * renderer can be selected.
118 | */
119 | label: ()=> Promise;
120 |
121 | /**
122 | * This is where most of the real-time processing will happen. When a note
123 | * is rendered for the first time and every time it changes, this handler
124 | * receives the properties specified in the `dependencies` property. You can
125 | * then process them, load any additional data you need, and once done you
126 | * need to return the properties that are needed in the `itemTemplate` HTML.
127 | * Again, to use the formatted date example, you could have such a renderer:
128 | *
129 | * ```typescript
130 | * dependencies: [
131 | * 'note.title',
132 | * 'note.created_time',
133 | * ],
134 | *
135 | * itemTemplate: // html
136 | * `
137 | *
138 | * Title: {{note.title}}
139 | * Date: {{formattedDate}}
140 | *
141 | * `,
142 | *
143 | * onRenderNote: async (props: any) => {
144 | * const formattedDate = dayjs(props.note.created_time).format();
145 | * return {
146 | * // Also return the props, so that note.title is available from the
147 | * // template
148 | * ...props,
149 | * formattedDate,
150 | * }
151 | * },
152 | * ```
153 | */
154 | onRenderNote: OnRenderNoteHandler;
155 |
156 | /**
157 | * This handler allows adding some interacivity to the note renderer -
158 | * whenever an input element within the item is changed (for example, when a
159 | * checkbox is clicked, or a text input is changed), this `onChange` handler
160 | * is going to be called.
161 | *
162 | * You can inspect `event.elementId` to know which element had some changes,
163 | * and `event.value` to know the new value. `event.noteId` also tells you
164 | * what note is affected, so that you can potentially apply changes to it.
165 | *
166 | * You specify the element ID, by setting a `data-id` attribute on the
167 | * input.
168 | *
169 | * For example, if you have such a template:
170 | *
171 | * ```html
172 | *
173 | *
174 | *
175 | * ```
176 | *
177 | * The event handler will receive an event with `elementId` set to
178 | * `noteTitleInput`.
179 | */
180 | onChange?: OnChangeHandler;
181 | }
182 |
--------------------------------------------------------------------------------
/api/types.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable multiline-comment-style */
2 |
3 | // =================================================================
4 | // Command API types
5 | // =================================================================
6 |
7 | export interface Command {
8 | /**
9 | * Name of command - must be globally unique
10 | */
11 | name: string;
12 |
13 | /**
14 | * Label to be displayed on menu items or keyboard shortcut editor for example.
15 | * If it is missing, it's assumed it's a private command, to be called programmatically only.
16 | * In that case the command will not appear in the shortcut editor or command panel, and logically
17 | * should not be used as a menu item.
18 | */
19 | label?: string;
20 |
21 | /**
22 | * Icon to be used on toolbar buttons for example
23 | */
24 | iconName?: string;
25 |
26 | /**
27 | * Code to be ran when the command is executed. It may return a result.
28 | */
29 | execute(...args: any[]): Promise;
30 |
31 | /**
32 | * Defines whether the command should be enabled or disabled, which in turns
33 | * affects the enabled state of any associated button or menu item.
34 | *
35 | * The condition should be expressed as a "when-clause" (as in Visual Studio
36 | * Code). It's a simple boolean expression that evaluates to `true` or
37 | * `false`. It supports the following operators:
38 | *
39 | * Operator | Symbol | Example
40 | * -- | -- | --
41 | * Equality | == | "editorType == markdown"
42 | * Inequality | != | "currentScreen != config"
43 | * Or | \|\| | "noteIsTodo \|\| noteTodoCompleted"
44 | * And | && | "oneNoteSelected && !inConflictFolder"
45 | *
46 | * Joplin, unlike VSCode, also supports parenthesis, which allows creating
47 | * more complex expressions such as `cond1 || (cond2 && cond3)`. Only one
48 | * level of parenthesis is possible (nested ones aren't supported).
49 | *
50 | * Currently the supported context variables aren't documented, but you can
51 | * find the list below:
52 | *
53 | * - [Global When Clauses](https://github.com/laurent22/joplin/blob/dev/packages/lib/services/commands/stateToWhenClauseContext.ts)
54 | * - [Desktop app When Clauses](https://github.com/laurent22/joplin/blob/dev/packages/app-desktop/services/commands/stateToWhenClauseContext.ts)
55 | *
56 | * Note: Commands are enabled by default unless you use this property.
57 | */
58 | enabledCondition?: string;
59 | }
60 |
61 | // =================================================================
62 | // Interop API types
63 | // =================================================================
64 |
65 | export enum FileSystemItem {
66 | File = 'file',
67 | Directory = 'directory',
68 | }
69 |
70 | export enum ImportModuleOutputFormat {
71 | Markdown = 'md',
72 | Html = 'html',
73 | }
74 |
75 | /**
76 | * Used to implement a module to export data from Joplin. [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/json_export) for an example.
77 | *
78 | * In general, all the event handlers you'll need to implement take a `context` object as a first argument. This object will contain the export or import path as well as various optional properties, such as which notes or notebooks need to be exported.
79 | *
80 | * To get a better sense of what it will contain it can be useful to print it using `console.info(context)`.
81 | */
82 | export interface ExportModule {
83 | /**
84 | * The format to be exported, eg "enex", "jex", "json", etc.
85 | */
86 | format: string;
87 |
88 | /**
89 | * The description that will appear in the UI, for example in the menu item.
90 | */
91 | description: string;
92 |
93 | /**
94 | * Whether the module will export a single file or multiple files in a directory. It affects the open dialog that will be presented to the user when using your exporter.
95 | */
96 | target: FileSystemItem;
97 |
98 | /**
99 | * Only applies to single file exporters or importers
100 | * It tells whether the format can package multiple notes into one file.
101 | * For example JEX or ENEX can, but HTML cannot.
102 | */
103 | isNoteArchive: boolean;
104 |
105 | /**
106 | * The extensions of the files exported by your module. For example, it is `["htm", "html"]` for the HTML module, and just `["jex"]` for the JEX module.
107 | */
108 | fileExtensions?: string[];
109 |
110 | /**
111 | * Called when the export process starts.
112 | */
113 | onInit(context: ExportContext): Promise;
114 |
115 | /**
116 | * Called when an item needs to be processed. An "item" can be any Joplin object, such as a note, a folder, a notebook, etc.
117 | */
118 | onProcessItem(context: ExportContext, itemType: number, item: any): Promise;
119 |
120 | /**
121 | * Called when a resource file needs to be exported.
122 | */
123 | onProcessResource(context: ExportContext, resource: any, filePath: string): Promise;
124 |
125 | /**
126 | * Called when the export process is done.
127 | */
128 | onClose(context: ExportContext): Promise;
129 | }
130 |
131 | export interface ImportModule {
132 | /**
133 | * The format to be exported, eg "enex", "jex", "json", etc.
134 | */
135 | format: string;
136 |
137 | /**
138 | * The description that will appear in the UI, for example in the menu item.
139 | */
140 | description: string;
141 |
142 | /**
143 | * Only applies to single file exporters or importers
144 | * It tells whether the format can package multiple notes into one file.
145 | * For example JEX or ENEX can, but HTML cannot.
146 | */
147 | isNoteArchive: boolean;
148 |
149 | /**
150 | * The type of sources that are supported by the module. Tells whether the module can import files or directories or both.
151 | */
152 | sources: FileSystemItem[];
153 |
154 | /**
155 | * Tells the file extensions of the exported files.
156 | */
157 | fileExtensions?: string[];
158 |
159 | /**
160 | * Tells the type of notes that will be generated, either HTML or Markdown (default).
161 | */
162 | outputFormat?: ImportModuleOutputFormat;
163 |
164 | /**
165 | * Called when the import process starts. There is only one event handler within which you should import the complete data.
166 | */
167 | onExec(context: ImportContext): Promise;
168 | }
169 |
170 | export interface ExportOptions {
171 | format?: string;
172 | path?: string;
173 | sourceFolderIds?: string[];
174 | sourceNoteIds?: string[];
175 | // modulePath?: string;
176 | target?: FileSystemItem;
177 | }
178 |
179 | export interface ExportContext {
180 | destPath: string;
181 | options: ExportOptions;
182 |
183 | /**
184 | * 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.
185 | */
186 | userData?: any;
187 | }
188 |
189 | export interface ImportContext {
190 | sourcePath: string;
191 | options: any;
192 | warnings: string[];
193 | }
194 |
195 | // =================================================================
196 | // Misc types
197 | // =================================================================
198 |
199 | export interface Script {
200 | onStart?(event: any): Promise;
201 | }
202 |
203 | export interface Disposable {
204 | // dispose():void;
205 | }
206 |
207 | export enum ModelType {
208 | Note = 1,
209 | Folder = 2,
210 | Setting = 3,
211 | Resource = 4,
212 | Tag = 5,
213 | NoteTag = 6,
214 | Search = 7,
215 | Alarm = 8,
216 | MasterKey = 9,
217 | ItemChange = 10,
218 | NoteResource = 11,
219 | ResourceLocalState = 12,
220 | Revision = 13,
221 | Migration = 14,
222 | SmartFilter = 15,
223 | Command = 16,
224 | }
225 |
226 | export interface VersionInfo {
227 | version: string;
228 | profileVersion: number;
229 | syncVersion: number;
230 | }
231 |
232 | // =================================================================
233 | // Menu types
234 | // =================================================================
235 |
236 | export interface CreateMenuItemOptions {
237 | accelerator: string;
238 | }
239 |
240 | export enum MenuItemLocation {
241 | File = 'file',
242 | Edit = 'edit',
243 | View = 'view',
244 | Note = 'note',
245 | Tools = 'tools',
246 | Help = 'help',
247 |
248 | /**
249 | * @deprecated Do not use - same as NoteListContextMenu
250 | */
251 | Context = 'context',
252 |
253 | // If adding an item here, don't forget to update isContextMenuItemLocation()
254 |
255 | /**
256 | * When a command is called from the note list context menu, the
257 | * command will receive the following arguments:
258 | *
259 | * - `noteIds:string[]`: IDs of the notes that were right-clicked on.
260 | */
261 | NoteListContextMenu = 'noteListContextMenu',
262 |
263 | EditorContextMenu = 'editorContextMenu',
264 |
265 | /**
266 | * When a command is called from a folder context menu, the
267 | * command will receive the following arguments:
268 | *
269 | * - `folderId:string`: ID of the folder that was right-clicked on
270 | */
271 | FolderContextMenu = 'folderContextMenu',
272 |
273 | /**
274 | * When a command is called from a tag context menu, the
275 | * command will receive the following arguments:
276 | *
277 | * - `tagId:string`: ID of the tag that was right-clicked on
278 | */
279 | TagContextMenu = 'tagContextMenu',
280 | }
281 |
282 | export function isContextMenuItemLocation(location: MenuItemLocation): boolean {
283 | return [
284 | MenuItemLocation.Context,
285 | MenuItemLocation.NoteListContextMenu,
286 | MenuItemLocation.EditorContextMenu,
287 | MenuItemLocation.FolderContextMenu,
288 | MenuItemLocation.TagContextMenu,
289 | ].includes(location);
290 | }
291 |
292 | export interface MenuItem {
293 | /**
294 | * Command that should be associated with the menu item. All menu item should
295 | * have a command associated with them unless they are a sub-menu.
296 | */
297 | commandName?: string;
298 |
299 | /**
300 | * Arguments that should be passed to the command. They will be as rest
301 | * parameters.
302 | */
303 | commandArgs?: any[];
304 |
305 | /**
306 | * Set to "separator" to create a divider line
307 | */
308 | type?: ('normal' | 'separator' | 'submenu' | 'checkbox' | 'radio');
309 |
310 | /**
311 | * Accelerator associated with the menu item
312 | */
313 | accelerator?: string;
314 |
315 | /**
316 | * Menu items that should appear below this menu item. Allows creating a menu tree.
317 | */
318 | submenu?: MenuItem[];
319 |
320 | /**
321 | * Menu item label. If not specified, the command label will be used instead.
322 | */
323 | label?: string;
324 | }
325 |
326 | // =================================================================
327 | // View API types
328 | // =================================================================
329 |
330 | export interface ButtonSpec {
331 | id: ButtonId;
332 | title?: string;
333 | onClick?(): void;
334 | }
335 |
336 | export type ButtonId = string;
337 |
338 | export enum ToolbarButtonLocation {
339 | /**
340 | * This toolbar in the top right corner of the application. It applies to the note as a whole, including its metadata.
341 | */
342 | NoteToolbar = 'noteToolbar',
343 |
344 | /**
345 | * This toolbar is right above the text editor. It applies to the note body only.
346 | */
347 | EditorToolbar = 'editorToolbar',
348 | }
349 |
350 | export type ViewHandle = string;
351 |
352 | export interface EditorCommand {
353 | name: string;
354 | value?: any;
355 | }
356 |
357 | export interface DialogResult {
358 | id: ButtonId;
359 | formData?: any;
360 | }
361 |
362 | export interface Size {
363 | width?: number;
364 | height?: number;
365 | }
366 |
367 | export interface Rectangle {
368 | x?: number;
369 | y?: number;
370 | width?: number;
371 | height?: number;
372 | }
373 |
374 | // =================================================================
375 | // Settings types
376 | // =================================================================
377 |
378 | export enum SettingItemType {
379 | Int = 1,
380 | String = 2,
381 | Bool = 3,
382 | Array = 4,
383 | Object = 5,
384 | Button = 6,
385 | }
386 |
387 | export enum SettingItemSubType {
388 | FilePathAndArgs = 'file_path_and_args',
389 | FilePath = 'file_path', // Not supported on mobile!
390 | DirectoryPath = 'directory_path', // Not supported on mobile!
391 | }
392 |
393 | export enum AppType {
394 | Desktop = 'desktop',
395 | Mobile = 'mobile',
396 | Cli = 'cli',
397 | }
398 |
399 | export enum SettingStorage {
400 | Database = 1,
401 | File = 2,
402 | }
403 |
404 | // Redefine a simplified interface to mask internal details
405 | // and to remove function calls as they would have to be async.
406 | export interface SettingItem {
407 | value: any;
408 | type: SettingItemType;
409 |
410 | /**
411 | * Currently only used to display a file or directory selector. Always set
412 | * `type` to `SettingItemType.String` when using this property.
413 | */
414 | subType?: SettingItemSubType;
415 |
416 | label: string;
417 | description?: string;
418 |
419 | /**
420 | * A public setting will appear in the Configuration screen and will be
421 | * modifiable by the user. A private setting however will not appear there,
422 | * and can only be changed programmatically. You may use this to store some
423 | * values that you do not want to directly expose.
424 | */
425 | public: boolean;
426 |
427 | /**
428 | * You would usually set this to a section you would have created
429 | * specifically for the plugin.
430 | */
431 | section?: string;
432 |
433 | /**
434 | * To create a setting with multiple options, set this property to `true`.
435 | * That setting will render as a dropdown list in the configuration screen.
436 | */
437 | isEnum?: boolean;
438 |
439 | /**
440 | * This property is required when `isEnum` is `true`. In which case, it
441 | * should contain a map of value => label.
442 | */
443 | options?: Record;
444 |
445 | /**
446 | * Reserved property. Not used at the moment.
447 | */
448 | appTypes?: AppType[];
449 |
450 | /**
451 | * Set this to `true` to store secure data, such as passwords. Any such
452 | * setting will be stored in the system keychain if one is available.
453 | */
454 | secure?: boolean;
455 |
456 | /**
457 | * An advanced setting will be moved under the "Advanced" button in the
458 | * config screen.
459 | */
460 | advanced?: boolean;
461 |
462 | /**
463 | * Set the min, max and step values if you want to restrict an int setting
464 | * to a particular range.
465 | */
466 | minimum?: number;
467 | maximum?: number;
468 | step?: number;
469 |
470 | /**
471 | * Either store the setting in the database or in settings.json. Defaults to database.
472 | */
473 | storage?: SettingStorage;
474 | }
475 |
476 | export interface SettingSection {
477 | label: string;
478 | iconName?: string;
479 | description?: string;
480 | name?: string;
481 | }
482 |
483 | // =================================================================
484 | // Data API types
485 | // =================================================================
486 |
487 | /**
488 | * An array of at least one element and at most three elements.
489 | *
490 | * - **[0]**: Resource name (eg. "notes", "folders", "tags", etc.)
491 | * - **[1]**: (Optional) Resource ID.
492 | * - **[2]**: (Optional) Resource link.
493 | */
494 | export type Path = string[];
495 |
496 | // =================================================================
497 | // Content Script types
498 | // =================================================================
499 |
500 | export type PostMessageHandler = (message: any)=> Promise;
501 |
502 | /**
503 | * When a content script is initialised, it receives a `context` object.
504 | */
505 | export interface ContentScriptContext {
506 | /**
507 | * The plugin ID that registered this content script
508 | */
509 | pluginId: string;
510 |
511 | /**
512 | * The content script ID, which may be necessary to post messages
513 | */
514 | contentScriptId: string;
515 |
516 | /**
517 | * Can be used by CodeMirror content scripts to post a message to the plugin
518 | */
519 | postMessage: PostMessageHandler;
520 | }
521 |
522 | export interface ContentScriptModuleLoadedEvent {
523 | userData?: any;
524 | }
525 |
526 | export interface ContentScriptModule {
527 | onLoaded?: (event: ContentScriptModuleLoadedEvent)=> void;
528 | plugin: ()=> any;
529 | assets?: ()=> void;
530 | }
531 |
532 | export interface MarkdownItContentScriptModule extends Omit {
533 | plugin: (markdownIt: any, options: any)=> any;
534 | }
535 |
536 | export enum ContentScriptType {
537 | /**
538 | * Registers a new Markdown-It plugin, which should follow the template
539 | * below.
540 | *
541 | * ```javascript
542 | * module.exports = {
543 | * default: function(context) {
544 | * return {
545 | * plugin: function(markdownIt, pluginOptions) {
546 | * // ...
547 | * },
548 | * assets: {
549 | * // ...
550 | * },
551 | * }
552 | * }
553 | * }
554 | * ```
555 | *
556 | * See [the
557 | * demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script)
558 | * for a simple Markdown-it plugin example.
559 | *
560 | * ## Exported members
561 | *
562 | * - The `context` argument is currently unused but could be used later on
563 | * to provide access to your own plugin so that the content script and
564 | * plugin can communicate.
565 | *
566 | * - The **required** `plugin` key is the actual Markdown-It plugin - check
567 | * the [official doc](https://github.com/markdown-it/markdown-it) for more
568 | * information.
569 | *
570 | * - Using the **optional** `assets` key you may specify assets such as JS
571 | * or CSS that should be loaded in the rendered HTML document. Check for
572 | * example the Joplin [Mermaid
573 | * plugin](https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml/rules/mermaid.ts)
574 | * to see how the data should be structured.
575 | *
576 | * ## Getting the settings from the renderer
577 | *
578 | * You can access your plugin settings from the renderer by calling
579 | * `pluginOptions.settingValue("your-setting-key')`.
580 | *
581 | * ## Posting messages from the content script to your plugin
582 | *
583 | * The application provides the following function to allow executing
584 | * commands from the rendered HTML code:
585 | *
586 | * ```javascript
587 | * const response = await webviewApi.postMessage(contentScriptId, message);
588 | * ```
589 | *
590 | * - `contentScriptId` is the ID you've defined when you registered the
591 | * content script. You can retrieve it from the
592 | * {@link ContentScriptContext | context}.
593 | * - `message` can be any basic JavaScript type (number, string, plain
594 | * object), but it cannot be a function or class instance.
595 | *
596 | * When you post a message, the plugin can send back a `response` thus
597 | * allowing two-way communication:
598 | *
599 | * ```javascript
600 | * await joplin.contentScripts.onMessage(contentScriptId, (message) => {
601 | * // Process message
602 | * return response; // Can be any object, string or number
603 | * });
604 | * ```
605 | *
606 | * See {@link JoplinContentScripts.onMessage} for more details, as well as
607 | * the [postMessage
608 | * demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages).
609 | *
610 | * ## Registering an existing Markdown-it plugin
611 | *
612 | * To include a regular Markdown-It plugin, that doesn't make use of any
613 | * Joplin-specific features, you would simply create a file such as this:
614 | *
615 | * ```javascript
616 | * module.exports = {
617 | * default: function(context) {
618 | * return {
619 | * plugin: require('markdown-it-toc-done-right');
620 | * }
621 | * }
622 | * }
623 | * ```
624 | */
625 | MarkdownItPlugin = 'markdownItPlugin',
626 |
627 | /**
628 | * Registers a new CodeMirror plugin, which should follow the template
629 | * below.
630 | *
631 | * ```javascript
632 | * module.exports = {
633 | * default: function(context) {
634 | * return {
635 | * plugin: function(CodeMirror) {
636 | * // ...
637 | * },
638 | * codeMirrorResources: [],
639 | * codeMirrorOptions: {
640 | * // ...
641 | * },
642 | * assets: {
643 | * // ...
644 | * },
645 | * }
646 | * }
647 | * }
648 | * ```
649 | *
650 | * - The `context` argument is currently unused but could be used later on
651 | * to provide access to your own plugin so that the content script and
652 | * plugin can communicate.
653 | *
654 | * - The `plugin` key is your CodeMirror plugin. This is where you can
655 | * register new commands with CodeMirror or interact with the CodeMirror
656 | * instance as needed.
657 | *
658 | * - The `codeMirrorResources` key is an array of CodeMirror resources that
659 | * will be loaded and attached to the CodeMirror module. These are made up
660 | * of addons, keymaps, and modes. For example, for a plugin that want's to
661 | * enable clojure highlighting in code blocks. `codeMirrorResources` would
662 | * be set to `['mode/clojure/clojure']`.
663 | *
664 | * - The `codeMirrorOptions` key contains all the
665 | * [CodeMirror](https://codemirror.net/doc/manual.html#config) options
666 | * that will be set or changed by this plugin. New options can alse be
667 | * declared via
668 | * [`CodeMirror.defineOption`](https://codemirror.net/doc/manual.html#defineOption),
669 | * and then have their value set here. For example, a plugin that enables
670 | * line numbers would set `codeMirrorOptions` to `{'lineNumbers': true}`.
671 | *
672 | * - Using the **optional** `assets` key you may specify **only** CSS assets
673 | * that should be loaded in the rendered HTML document. Check for example
674 | * the Joplin [Mermaid
675 | * plugin](https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml/rules/mermaid.ts)
676 | * to see how the data should be structured.
677 | *
678 | * One of the `plugin`, `codeMirrorResources`, or `codeMirrorOptions` keys
679 | * must be provided for the plugin to be valid. Having multiple or all
680 | * provided is also okay.
681 | *
682 | * See also the [demo
683 | * plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/codemirror_content_script)
684 | * for an example of all these keys being used in one plugin.
685 | *
686 | * ## Posting messages from the content script to your plugin
687 | *
688 | * In order to post messages to the plugin, you can use the postMessage
689 | * function passed to the {@link ContentScriptContext | context}.
690 | *
691 | * ```javascript
692 | * const response = await context.postMessage('messageFromCodeMirrorContentScript');
693 | * ```
694 | *
695 | * When you post a message, the plugin can send back a `response` thus
696 | * allowing two-way communication:
697 | *
698 | * ```javascript
699 | * await joplin.contentScripts.onMessage(contentScriptId, (message) => {
700 | * // Process message
701 | * return response; // Can be any object, string or number
702 | * });
703 | * ```
704 | *
705 | * See {@link JoplinContentScripts.onMessage} for more details, as well as
706 | * the [postMessage
707 | * demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages).
708 | *
709 | */
710 | CodeMirrorPlugin = 'codeMirrorPlugin',
711 | }
712 |
--------------------------------------------------------------------------------
/img/example_image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JackGruber/joplin-plugin-note-overview/82d8336e931fc47706144af0c465ce0aa4f09571/img/example_image.jpg
--------------------------------------------------------------------------------
/img/example_option_details.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JackGruber/joplin-plugin-note-overview/82d8336e931fc47706144af0c465ce0aa4f09571/img/example_option_details.jpg
--------------------------------------------------------------------------------
/img/example_option_excerpt_regex_checkbox.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JackGruber/joplin-plugin-note-overview/82d8336e931fc47706144af0c465ce0aa4f09571/img/example_option_excerpt_regex_checkbox.png
--------------------------------------------------------------------------------
/img/example_option_listview_nolb.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JackGruber/joplin-plugin-note-overview/82d8336e931fc47706144af0c465ce0aa4f09571/img/example_option_listview_nolb.jpg
--------------------------------------------------------------------------------
/img/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
34 |
--------------------------------------------------------------------------------
/img/icon_256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JackGruber/joplin-plugin-note-overview/82d8336e931fc47706144af0c465ce0aa4f09571/img/icon_256.png
--------------------------------------------------------------------------------
/img/icon_32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JackGruber/joplin-plugin-note-overview/82d8336e931fc47706144af0c465ce0aa4f09571/img/icon_32.png
--------------------------------------------------------------------------------
/img/main.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JackGruber/joplin-plugin-note-overview/82d8336e931fc47706144af0c465ce0aa4f09571/img/main.png
--------------------------------------------------------------------------------
/img/showcase1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JackGruber/joplin-plugin-note-overview/82d8336e931fc47706144af0c465ce0aa4f09571/img/showcase1.png
--------------------------------------------------------------------------------
/img/showcase2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JackGruber/joplin-plugin-note-overview/82d8336e931fc47706144af0c465ce0aa4f09571/img/showcase2.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "joplin-plugin-note-overview",
3 | "version": "1.7.1",
4 | "scripts": {
5 | "dist": "webpack --env joplin-plugin-config=buildMain && webpack --env joplin-plugin-config=buildExtraScripts && webpack --env joplin-plugin-config=createArchive",
6 | "prepare": "npm run dist && husky install",
7 | "update": "npm install -g generator-joplin && yo joplin --node-package-manager npm --update --force",
8 | "release": "npm test && node ./node_modules/joplinplugindevtools/dist/createRelease.js",
9 | "preRelease": "npm test && node ./node_modules/joplinplugindevtools/dist/createRelease.js --prerelease",
10 | "gitRelease": "node ./node_modules/joplinplugindevtools/dist/createRelease.js --upload",
11 | "gitPreRelease": "node ./node_modules/joplinplugindevtools/dist/createRelease.js --upload --prerelease",
12 | "test": "jest",
13 | "updateVersion": "webpack --env joplin-plugin-config=updateVersion"
14 | },
15 | "license": "MIT",
16 | "keywords": [
17 | "joplin-plugin"
18 | ],
19 | "devDependencies": {
20 | "@joplin/lib": "~2.9",
21 | "@types/jest": "^26.0.23",
22 | "@types/node": "^18.7.13",
23 | "axios": "^0.21.1",
24 | "chalk": "^4.1.0",
25 | "copy-webpack-plugin": "^11.0.0",
26 | "dotenv": "^10.0.0",
27 | "fs-extra": "^10.1.0",
28 | "glob": "^8.0.3",
29 | "husky": "^6.0.0",
30 | "jest": "^26.6.3",
31 | "joplinplugindevtools": "^1.0.15",
32 | "lint-staged": "^11.0.0",
33 | "mime": "^2.5.2",
34 | "on-build-webpack": "^0.1.0",
35 | "prettier": "2.3.0",
36 | "tar": "^6.1.11",
37 | "ts-jest": "^26.5.6",
38 | "ts-loader": "^9.3.1",
39 | "typescript": "^4.8.2",
40 | "webpack": "^5.74.0",
41 | "webpack-cli": "^4.10.0",
42 | "yargs": "^16.2.0"
43 | },
44 | "dependencies": {
45 | "electron-log": "^4.4.1",
46 | "i18n": "^0.15.1",
47 | "jest-when": "^3.3.1",
48 | "moment": "^2.29.1",
49 | "remark": "^13.0.0",
50 | "string-natural-compare": "^3.0.1",
51 | "strip-markdown": "^4.0.0",
52 | "yaml": "^1.10.2"
53 | },
54 | "browser": {
55 | "fs": false
56 | },
57 | "jest": {
58 | "transform": {
59 | ".(ts|tsx)": "ts-jest"
60 | },
61 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$",
62 | "moduleFileExtensions": [
63 | "ts",
64 | "tsx",
65 | "js"
66 | ],
67 | "moduleNameMapper": {
68 | "^api$": "/node_modules/joplinplugindevtools/dist/apiMock.js",
69 | "^api/(.*)$": "/api/$1"
70 | }
71 | },
72 | "lint-staged": {
73 | "**/*": "prettier --write --ignore-unknown"
74 | },
75 | "files": [
76 | "publish"
77 | ]
78 | }
79 |
--------------------------------------------------------------------------------
/plugin.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extraScripts": []
3 | }
4 |
--------------------------------------------------------------------------------
/src/helper.ts:
--------------------------------------------------------------------------------
1 | export async function mergeObject(...objArg): Promise {
2 | // create a new object
3 | let target = {};
4 | // deep merge the object into the target object
5 | // iterate through all objects and
6 | // deep merge them with target
7 | for (let i = 0; i < objArg.length; i++) {
8 | for (let prop in objArg[i]) {
9 | if (objArg[i].hasOwnProperty(prop)) {
10 | if (
11 | Object.prototype.toString.call(objArg[i][prop]) === "[object Object]"
12 | ) {
13 | // if the property is a nested object
14 | target[prop] = await mergeObject(target[prop], objArg[i][prop]);
15 | } else {
16 | // for regular property
17 | target[prop] = objArg[i][prop];
18 | }
19 | }
20 | }
21 | }
22 |
23 | return target;
24 | }
25 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import joplin from "api";
2 | import { noteoverview } from "./noteoverview";
3 |
4 | joplin.plugins.register({
5 | onStart: async function () {
6 | await noteoverview.init();
7 | },
8 | });
9 |
--------------------------------------------------------------------------------
/src/locales/de_DE.json:
--------------------------------------------------------------------------------
1 | {
2 | "settings": {
3 | "updateInterval": {
4 | "label": "Aktualisierungs interval in minutes",
5 | "description": "0 = deaktivert das automatische aktualisieren der note-overview"
6 | },
7 | "updateOnSync": {
8 | "label": "Aktualisier nach Joplin sync",
9 | "values": {
10 | "yes": "Ja",
11 | "no": "Nein"
12 | },
13 | "description": "Aktualisiert die note-overview nach einer Joplin-Syncronisation. Unabhängig vom Aktualisierungsintervall"
14 | },
15 | "showNoteCount": {
16 | "label": "Anzahl Notizen anzeigen",
17 | "values": {
18 | "off": "Aus",
19 | "above": "Darüber",
20 | "below": "Darunter"
21 | },
22 | "description": "Zeigt die Anzahl der Notizen an, die gefunden wurden"
23 | },
24 | "showNoteCountText": {
25 | "label": "Notiz anzahl text",
26 | "description": "Text für die Anzeige der gefundenen Notizen, %s wird durch die Anzahl der gefundenen Notizen ersetzt"
27 | },
28 | "todoStatusOpen": {
29 | "label": "Feld status: %s",
30 | "description": "Text für das Feld `Status`, wenn die Aufgabe nicht abgeschlossen ist"
31 | },
32 | "todoStatusDone": {
33 | "label": "Feld status: %s",
34 | "description": "Text für das Feld `Status`, wenn die Aufgabe abgeschlossen ist"
35 | },
36 | "todoStatusOverdue": {
37 | "label": "Feld status: %s",
38 | "description": "Text für das Feld `Status`, wenn das Fälligkeitsdatum der Aufgabe überschritten ist"
39 | },
40 | "colorTodoOpen": {
41 | "label": "Farbe: %s",
42 | "description": "HTML Farbe für das Fälligkeitsdatum (%s), wenn die Aufgabe nicht abgeschlossen ist"
43 | },
44 | "colorTodoWarning": {
45 | "label": "Farbe: %s",
46 | "description": "HTML Farbe für das Fälligkeitsdatum (%s), wenn das Fälligkeitsdatum in x Stunden erreicht ist"
47 | },
48 | "todoWarningHours": {
49 | "label": "%s Stunden",
50 | "description": "Wie viele Stunden vor dem Fälligkeitsdatum (%s) soll die Warnfarbe angewendet werden. 0 = Deaktiviert"
51 | },
52 | "colorTodoOpenOverdue": {
53 | "label": "Farbe: %s",
54 | "description": "HTML Farbe für das Fälligkeitsdatum (%s), wenn die Aufgabe das Fälligkeitsdatum überschritten hat"
55 | },
56 | "colorTodoDone": {
57 | "label": "Farbe: %s",
58 | "description": "HTML Farbe für das Fälligkeitsdatum ({{field_due_date}}) und das Abschlussdatum ({{field_todo_completed}}) , wenn die Aufgabe abgeschlossen ist. Trennen Sie die Farbe für {{field_due_date}} und {{field_todo_completed}} durch ein Komma"
59 | },
60 |
61 | "colorTodoDoneOverdue": {
62 | "label": "Farbe: %s",
63 | "description": "HTML Farbe für das Fälligkeitsdatum ({{field_due_date}}) und das Abschlussdatum ({{field_todo_completed}}), wenn die Aufgabe nach dem Fälligkeitsdatum abgeschlossen wurde. Trennen Sie die Farbe für {{field_due_date}} und {{field_todo_completed}} durch ein Komma"
64 | },
65 | "colorTodoDoneNodue": {
66 | "label": "Farbe: %s",
67 | "description": "HTML Farbe für das Abschlussdatum (%s), wenn die Aufgabe abgeschlossen wurde, aber kein Fälligkeitsdatum gesetzt wurde"
68 | },
69 | "noteStatus": {
70 | "label": "Feld status: %s",
71 | "description": "Text für das Statusfeld wenn es sich um eine Notiz und keine Aufgabe handelt"
72 | },
73 | "fileLogLevel": {
74 | "label": "Loglevel",
75 | "description": "Einstellung für das Loglevel",
76 | "values": {
77 | "false": "Aus",
78 | "verbose": "Verbose",
79 | "info": "Info",
80 | "warn": "Warnung",
81 | "error": "Fehler"
82 | }
83 | }
84 | },
85 | "msg": {
86 | "error": {
87 | "regexParseError": "RegEx parse Fehler",
88 | "yamlParseError": "YAML parse Fehler"
89 | }
90 | },
91 | "command": {
92 | "createNoteOverview": "Aktualisiere note overview"
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/locales/en_US.json:
--------------------------------------------------------------------------------
1 | {
2 | "settings": {
3 | "updateInterval": {
4 | "label": "Update interval in minutes",
5 | "description": "0 = disable automatic note overview creation"
6 | },
7 | "updateOnSync": {
8 | "label": "Update after Joplin sync",
9 | "values": {
10 | "yes": "Yes",
11 | "no": "No"
12 | },
13 | "description": "Update the Noteoverview after a Joplin syncronisation. Independent of the update interval"
14 | },
15 | "showNoteCount": {
16 | "label": "Show note count",
17 | "values": {
18 | "off": "Off",
19 | "above": "Above",
20 | "below": "Below"
21 | },
22 | "description": "Show the number of notes matching the search"
23 | },
24 | "showNoteCountText": {
25 | "label": "Note count text",
26 | "description": "Text for the display of the found notes, %s is replace with the number of matched notes"
27 | },
28 | "todoStatusOpen": {
29 | "label": "Field status: %s",
30 | "description": "Text for the status field, when the todo is not completed"
31 | },
32 | "todoStatusDone": {
33 | "label": "Field status: %s",
34 | "description": "Text for the status field, when the todo is completed"
35 | },
36 | "todoStatusOverdue": {
37 | "label": "Field status: %s",
38 | "description": "Text for the `status` field, when the due date of the todo is exceeded"
39 | },
40 | "colorTodoOpen": {
41 | "label": "Color: %s",
42 | "description": "HTML color for the %s, when the todo is not completed"
43 | },
44 | "colorTodoWarning": {
45 | "label": "Color: %s",
46 | "description": "HTML color for the %s when the due date is reached in x hours"
47 | },
48 | "todoWarningHours": {
49 | "label": "%s hours",
50 | "description": "How many hours before %s the warning color should be applied. 0 = Disabled"
51 | },
52 | "colorTodoOpenOverdue": {
53 | "label": "Color: %s",
54 | "description": "HTML color for the %s, when the todo is over the due date"
55 | },
56 | "colorTodoDone": {
57 | "label": "Color: %s",
58 | "description": "HTML color for the {{field_due_date}} and {{field_todo_completed}}, when the todo is completed. Seperate the color for {{field_due_date}} and {{field_todo_completed}} by a comma"
59 | },
60 | "colorTodoDoneOverdue": {
61 | "label": "Color: %s",
62 | "description": "HTML color for the {{field_due_date}} and {{field_todo_completed}}, when the todo was completed after the due date. Seperate the color for {{field_due_date}} and {{field_todo_completed}} by a comma"
63 | },
64 | "colorTodoDoneNodue": {
65 | "label": "Color: %s",
66 | "description": "HTML color for the %s, when the todo was completed but no due date was set"
67 | },
68 | "noteStatus": {
69 | "label": "Field status: %s",
70 | "description": "Text for the status field if it is a note and not a todo"
71 | },
72 | "fileLogLevel": {
73 | "label": "Loglevel",
74 | "description": "Setting for the Loglevel",
75 | "values": {
76 | "false": "Off",
77 | "verbose": "Verbose",
78 | "info": "Info",
79 | "warn": "Warn",
80 | "error": "Error"
81 | }
82 | }
83 | },
84 | "msg": {
85 | "error": {
86 | "regexParseError": "RegEx parse error",
87 | "yamlParseError": "YAML parse error"
88 | }
89 | },
90 | "command": {
91 | "createNoteOverview": "Update note overview"
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 1,
3 | "id": "io.github.jackgruber.note-overview",
4 | "app_min_version": "1.8.1",
5 | "version": "1.7.1",
6 | "name": "Note overview",
7 | "description": "A note overview is created based on the defined search and the specified fields.",
8 | "author": "JackGruber",
9 | "homepage_url": "https://github.com/JackGruber/joplin-plugin-note-overview/blob/master/README.md",
10 | "repository_url": "https://github.com/JackGruber/joplin-plugin-note-overview",
11 | "keywords": ["search", "overview", "savedsearch", "saved", "query"],
12 | "categories": ["productivity", "search"],
13 | "screenshots": [
14 | {
15 | "src": "img/main.png",
16 | "label": "Screenshot: Main Screenshot"
17 | },
18 | {
19 | "src": "img/showcase1.png",
20 | "label": "Screenshot: Showcase 1"
21 | },
22 | {
23 | "src": "img/showcase2.png",
24 | "label": "Screenshot: Showcase 2"
25 | }
26 | ],
27 | "icons": {
28 | "256": "img/icon_256.png"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/settings.ts:
--------------------------------------------------------------------------------
1 | import joplin from "api";
2 | import { SettingItemType } from "api/types";
3 | import { i18n } from "./noteoverview";
4 |
5 | export namespace settings {
6 | export async function register() {
7 | await joplin.settings.registerSection("noteOverviewSection", {
8 | label: "Note overview",
9 | iconName: "fas fa-binoculars",
10 | });
11 |
12 | await joplin.settings.registerSettings({
13 | updateInterval: {
14 | value: 5,
15 | minimum: 0,
16 | maximum: 2880,
17 | type: SettingItemType.Int,
18 | section: "noteOverviewSection",
19 | public: true,
20 | label: i18n.__("settings.updateInterval.label"),
21 | description: i18n.__("settings.updateInterval.description"),
22 | },
23 | updateOnSync: {
24 | value: "no",
25 | type: SettingItemType.String,
26 | section: "noteOverviewSection",
27 | isEnum: true,
28 | public: true,
29 | label: i18n.__("settings.updateOnSync.label"),
30 | options: {
31 | yes: i18n.__("settings.updateOnSync.values.yes"),
32 | no: i18n.__("settings.updateOnSync.values.no"),
33 | },
34 | description: i18n.__("settings.updateOnSync.description"),
35 | },
36 | showNoteCount: {
37 | value: "off",
38 | type: SettingItemType.String,
39 | section: "noteOverviewSection",
40 | isEnum: true,
41 | public: true,
42 | label: i18n.__("settings.showNoteCount.label"),
43 | options: {
44 | off: i18n.__("settings.showNoteCount.values.off"),
45 | above: i18n.__("settings.showNoteCount.values.above"),
46 | below: i18n.__("settings.showNoteCount.values.below"),
47 | },
48 | description: i18n.__("settings.showNoteCount.description"),
49 | },
50 | showNoteCountText: {
51 | value: "Note count: {{count}}",
52 | type: SettingItemType.String,
53 | section: "noteOverviewSection",
54 | public: true,
55 | advanced: true,
56 | label: i18n.__("settings.showNoteCountText.label"),
57 | description: i18n.__(
58 | "settings.showNoteCountText.description",
59 | "{{count}}"
60 | ),
61 | },
62 |
63 | noteStatus: {
64 | value: "",
65 | advanced: true,
66 | type: SettingItemType.String,
67 | section: "noteOverviewSection",
68 | public: true,
69 | label: i18n.__("settings.noteStatus.label", "note"),
70 | description: i18n.__("settings.noteStatus.description"),
71 | },
72 | todoStatusOpen: {
73 | value: "",
74 | advanced: true,
75 | type: SettingItemType.String,
76 | section: "noteOverviewSection",
77 | public: true,
78 | label: i18n.__("settings.todoStatusOpen.label", "open todo"),
79 | description: i18n.__("settings.todoStatusOpen.description"),
80 | },
81 | todoStatusDone: {
82 | value: "✔",
83 | advanced: true,
84 | type: SettingItemType.String,
85 | section: "noteOverviewSection",
86 | public: true,
87 | label: i18n.__("settings.todoStatusDone.label", "todo completed"),
88 | description: i18n.__("settings.todoStatusDone.description"),
89 | },
90 | todoStatusOverdue: {
91 | value: "❗",
92 | advanced: true,
93 | type: SettingItemType.String,
94 | section: "noteOverviewSection",
95 | public: true,
96 | label: i18n.__("settings.todoStatusOverdue.label", "todo over due"),
97 | description: i18n.__("settings.todoStatusOverdue.description"),
98 | },
99 |
100 | colorTodoOpen: {
101 | value: "",
102 | advanced: true,
103 | type: SettingItemType.String,
104 | section: "noteOverviewSection",
105 | public: true,
106 | label: i18n.__("settings.colorTodoOpen.label", "todo [open]"),
107 | description: i18n.__("settings.colorTodoOpen.description", "due_date"),
108 | },
109 | colorTodoWarning: {
110 | value: "",
111 | advanced: true,
112 | type: SettingItemType.String,
113 | section: "noteOverviewSection",
114 | public: true,
115 | label: i18n.__("settings.colorTodoWarning.label", "todo [warning]"),
116 | description: i18n.__(
117 | "settings.colorTodoWarning.description",
118 | "due_date"
119 | ),
120 | },
121 | todoWarningHours: {
122 | value: 0,
123 | minimum: 0,
124 | maximum: 2880,
125 | type: SettingItemType.Int,
126 | section: "noteOverviewSection",
127 | advanced: true,
128 | public: true,
129 | label: i18n.__("settings.todoWarningHours.label", "todo [warning]"),
130 | description: i18n.__(
131 | "settings.todoWarningHours.description",
132 | "due_date"
133 | ),
134 | },
135 | colorTodoOpenOverdue: {
136 | value: "red",
137 | advanced: true,
138 | type: SettingItemType.String,
139 | section: "noteOverviewSection",
140 | public: true,
141 | label: i18n.__(
142 | "settings.colorTodoOpenOverdue.label",
143 | "todo [open_overdue]"
144 | ),
145 | description: i18n.__(
146 | "settings.colorTodoOpenOverdue.description",
147 | "due_date"
148 | ),
149 | },
150 | colorTodoDone: {
151 | value: "limegreen,limegreen",
152 | advanced: true,
153 | type: SettingItemType.String,
154 | section: "noteOverviewSection",
155 | public: true,
156 | label: i18n.__("settings.colorTodoDone.label", "todo [done]"),
157 | description: i18n.__("settings.colorTodoDone.description", {
158 | field_due_date: "due_date",
159 | field_todo_completed: "todo_completed",
160 | }),
161 | },
162 | colorTodoDoneOverdue: {
163 | value: "orange,orange",
164 | advanced: true,
165 | type: SettingItemType.String,
166 | section: "noteOverviewSection",
167 | public: true,
168 | label: i18n.__(
169 | "settings.colorTodoDoneOverdue.label",
170 | "todo [done_overdue]"
171 | ),
172 | description: i18n.__("settings.colorTodoDoneOverdue.description", {
173 | field_due_date: "due_date",
174 | field_todo_completed: "todo_completed",
175 | }),
176 | },
177 | colorTodoDoneNodue: {
178 | value: "",
179 | advanced: true,
180 | type: SettingItemType.String,
181 | section: "noteOverviewSection",
182 | public: true,
183 | label: i18n.__(
184 | "settings.colorTodoDoneNodue.label",
185 | "todo [done_nodue]"
186 | ),
187 | description: i18n.__(
188 | "settings.colorTodoDoneNodue.description",
189 | "todo_completed"
190 | ),
191 | },
192 | fileLogLevel: {
193 | value: "info",
194 | type: SettingItemType.String,
195 | section: "noteOverviewSection",
196 | advanced: true,
197 | isEnum: true,
198 | public: true,
199 | label: i18n.__("settings.fileLogLevel.label"),
200 | description: i18n.__("settings.fileLogLevel.description"),
201 | options: {
202 | false: i18n.__("settings.fileLogLevel.values.false"),
203 | verbose: i18n.__("settings.fileLogLevel.values.verbose"),
204 | info: i18n.__("settings.fileLogLevel.values.info"),
205 | warn: i18n.__("settings.fileLogLevel.values.warn"),
206 | error: i18n.__("settings.fileLogLevel.values.error"),
207 | },
208 | },
209 | });
210 | }
211 | }
212 |
--------------------------------------------------------------------------------
/src/type.ts:
--------------------------------------------------------------------------------
1 | type OverviewOptions = {
2 | statusText: any;
3 | fields: string;
4 | sortStr: string;
5 | orderBy: string;
6 | orderDir: string;
7 | alias: string;
8 | imageSettings: any;
9 | excerptSettings: any;
10 | coloring: any;
11 | count: OverviewOptionsCount;
12 | details: OverviewOptionsDetails;
13 | listview: OverviewListview;
14 | escapeForTable: boolean;
15 | link: OverviewOptionsLink;
16 | datetimeSettings: OverviewOptionsDatetime;
17 | };
18 |
19 | type OverviewOptionsLink = {
20 | caption: string;
21 | };
22 |
23 | type OverviewOptionsDetails = {
24 | summary: string;
25 | open: boolean;
26 | };
27 |
28 | type OverviewOptionsCount = {
29 | enable: boolean;
30 | text: string;
31 | position: string;
32 | };
33 |
34 | type OverviewListview = {
35 | separator: string;
36 | text: string;
37 | linebreak: boolean;
38 | prefix: string;
39 | suffix: string;
40 | };
41 |
42 | type OverviewOptionsDatetime = {
43 | date: string;
44 | time: string;
45 | humanize: OverviewOptionsDatetimeHumanize;
46 | };
47 |
48 | type OverviewOptionsDatetimeHumanize = {
49 | enabled: boolean;
50 | withSuffix: boolean;
51 | };
52 |
53 | export { OverviewOptions };
54 |
--------------------------------------------------------------------------------
/src/webview.css:
--------------------------------------------------------------------------------
1 | #joplin-plugin-content {
2 | width: fit-content;
3 | background-color: var(--joplin-background-color);
4 | color: var(--joplin-color);
5 | }
6 |
7 | #noteoverview {
8 | width: fit-content;
9 | display: block;
10 | flex-direction: column;
11 | min-width: 400px;
12 | overflow-wrap: break-word;
13 | font-size: var(--joplin-font-size);
14 | font-family: var(--joplin-font-family);
15 | }
16 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "./dist/",
4 | "module": "commonjs",
5 | "target": "es2015",
6 | "jsx": "react",
7 | "allowJs": true,
8 | "baseUrl": "."
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | // -----------------------------------------------------------------------------
2 | // This file is used to build the plugin file (.jpl) and plugin info (.json). It
3 | // is recommended not to edit this file as it would be overwritten when updating
4 | // the plugin framework. If you do make some changes, consider using an external
5 | // JS file and requiring it here to minimize the changes. That way when you
6 | // update, you can easily restore the functionality you've added.
7 | // -----------------------------------------------------------------------------
8 |
9 | /* eslint-disable no-console */
10 |
11 | const path = require('path');
12 | const crypto = require('crypto');
13 | const fs = require('fs-extra');
14 | const chalk = require('chalk');
15 | const CopyPlugin = require('copy-webpack-plugin');
16 | const tar = require('tar');
17 | const glob = require('glob');
18 | const execSync = require('child_process').execSync;
19 | const allPossibleCategories = require('@joplin/lib/pluginCategories.json');
20 |
21 | const rootDir = path.resolve(__dirname);
22 | const userConfigFilename = './plugin.config.json';
23 | const userConfigPath = path.resolve(rootDir, userConfigFilename);
24 | const distDir = path.resolve(rootDir, 'dist');
25 | const srcDir = path.resolve(rootDir, 'src');
26 | const publishDir = path.resolve(rootDir, 'publish');
27 |
28 | const userConfig = { extraScripts: [], ...(fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {}) };
29 |
30 | const manifestPath = `${srcDir}/manifest.json`;
31 | const packageJsonPath = `${rootDir}/package.json`;
32 | const allPossibleScreenshotsType = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
33 | const manifest = readManifest(manifestPath);
34 | const pluginArchiveFilePath = path.resolve(publishDir, `${manifest.id}.jpl`);
35 | const pluginInfoFilePath = path.resolve(publishDir, `${manifest.id}.json`);
36 |
37 | const { builtinModules } = require('node:module');
38 |
39 | // Webpack5 doesn't polyfill by default and displays a warning when attempting to require() built-in
40 | // node modules. Set these to false to prevent Webpack from warning about not polyfilling these modules.
41 | // We don't need to polyfill because the plugins run in Electron's Node environment.
42 | const moduleFallback = {};
43 | for (const moduleName of builtinModules) {
44 | moduleFallback[moduleName] = false;
45 | }
46 |
47 | const getPackageJson = () => {
48 | return JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
49 | };
50 |
51 | function validatePackageJson() {
52 | const content = getPackageJson();
53 | if (!content.name || content.name.indexOf('joplin-plugin-') !== 0) {
54 | console.warn(chalk.yellow(`WARNING: To publish the plugin, the package name should start with "joplin-plugin-" (found "${content.name}") in ${packageJsonPath}`));
55 | }
56 |
57 | if (!content.keywords || content.keywords.indexOf('joplin-plugin') < 0) {
58 | console.warn(chalk.yellow(`WARNING: To publish the plugin, the package keywords should include "joplin-plugin" (found "${JSON.stringify(content.keywords)}") in ${packageJsonPath}`));
59 | }
60 |
61 | if (content.scripts && content.scripts.postinstall) {
62 | console.warn(chalk.yellow(`WARNING: package.json contains a "postinstall" script. It is recommended to use a "prepare" script instead so that it is executed before publish. In ${packageJsonPath}`));
63 | }
64 | }
65 |
66 | function fileSha256(filePath) {
67 | const content = fs.readFileSync(filePath);
68 | return crypto.createHash('sha256').update(content).digest('hex');
69 | }
70 |
71 | function currentGitInfo() {
72 | try {
73 | let branch = execSync('git rev-parse --abbrev-ref HEAD', { stdio: 'pipe' }).toString().trim();
74 | const commit = execSync('git rev-parse HEAD', { stdio: 'pipe' }).toString().trim();
75 | if (branch === 'HEAD') branch = 'master';
76 | return `${branch}:${commit}`;
77 | } catch (error) {
78 | const messages = error.message ? error.message.split('\n') : [''];
79 | console.info(chalk.cyan('Could not get git commit (not a git repo?):', messages[0].trim()));
80 | console.info(chalk.cyan('Git information will not be stored in plugin info file'));
81 | return '';
82 | }
83 | }
84 |
85 | function validateCategories(categories) {
86 | if (!categories) return null;
87 | if ((categories.length !== new Set(categories).size)) throw new Error('Repeated categories are not allowed');
88 | // eslint-disable-next-line github/array-foreach -- Old code before rule was applied
89 | categories.forEach(category => {
90 | 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`);
91 | });
92 | }
93 |
94 | function validateScreenshots(screenshots) {
95 | if (!screenshots) return null;
96 | for (const screenshot of screenshots) {
97 | if (!screenshot.src) throw new Error('You must specify a src for each screenshot');
98 |
99 | // Avoid attempting to download and verify URL screenshots.
100 | if (screenshot.src.startsWith('https://') || screenshot.src.startsWith('http://')) {
101 | continue;
102 | }
103 |
104 | const screenshotType = screenshot.src.split('.').pop();
105 | if (!allPossibleScreenshotsType.includes(screenshotType)) throw new Error(`${screenshotType} is not a valid screenshot type. Valid types are: \n${allPossibleScreenshotsType}\n`);
106 |
107 | const screenshotPath = path.resolve(rootDir, screenshot.src);
108 |
109 | // Max file size is 1MB
110 | const fileMaxSize = 1024;
111 | const fileSize = fs.statSync(screenshotPath).size / 1024;
112 | if (fileSize > fileMaxSize) throw new Error(`Max screenshot file size is ${fileMaxSize}KB. ${screenshotPath} is ${fileSize}KB`);
113 | }
114 | }
115 |
116 | function readManifest(manifestPath) {
117 | const content = fs.readFileSync(manifestPath, 'utf8');
118 | const output = JSON.parse(content);
119 | if (!output.id) throw new Error(`Manifest plugin ID is not set in ${manifestPath}`);
120 | validateCategories(output.categories);
121 | validateScreenshots(output.screenshots);
122 | return output;
123 | }
124 |
125 | function createPluginArchive(sourceDir, destPath) {
126 | const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true, windowsPathsNoEscape: true })
127 | .map(f => f.substr(sourceDir.length + 1));
128 |
129 | if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty');
130 | fs.removeSync(destPath);
131 |
132 | tar.create(
133 | {
134 | strict: true,
135 | portable: true,
136 | file: destPath,
137 | cwd: sourceDir,
138 | sync: true,
139 | },
140 | distFiles,
141 | );
142 |
143 | console.info(chalk.cyan(`Plugin archive has been created in ${destPath}`));
144 | }
145 |
146 | const writeManifest = (manifestPath, content) => {
147 | fs.writeFileSync(manifestPath, JSON.stringify(content, null, '\t'), 'utf8');
148 | };
149 |
150 | function createPluginInfo(manifestPath, destPath, jplFilePath) {
151 | const contentText = fs.readFileSync(manifestPath, 'utf8');
152 | const content = JSON.parse(contentText);
153 | content._publish_hash = `sha256:${fileSha256(jplFilePath)}`;
154 | content._publish_commit = currentGitInfo();
155 | writeManifest(destPath, content);
156 | }
157 |
158 | function onBuildCompleted() {
159 | try {
160 | fs.removeSync(path.resolve(publishDir, 'index.js'));
161 | createPluginArchive(distDir, pluginArchiveFilePath);
162 | createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
163 | validatePackageJson();
164 | } catch (error) {
165 | console.error(chalk.red(error.message));
166 | }
167 | }
168 |
169 | const baseConfig = {
170 | mode: 'production',
171 | target: 'node',
172 | stats: 'errors-only',
173 | module: {
174 | rules: [
175 | {
176 | test: /\.tsx?$/,
177 | use: 'ts-loader',
178 | exclude: /node_modules/,
179 | },
180 | ],
181 | },
182 | };
183 |
184 | const pluginConfig = { ...baseConfig, entry: './src/index.ts',
185 | resolve: {
186 | alias: {
187 | api: path.resolve(__dirname, 'api'),
188 | },
189 | fallback: moduleFallback,
190 | // JSON files can also be required from scripts so we include this.
191 | // https://github.com/joplin/plugin-bibtex/pull/2
192 | extensions: ['.js', '.tsx', '.ts', '.json'],
193 | },
194 | output: {
195 | filename: 'index.js',
196 | path: distDir,
197 | },
198 | plugins: [
199 | new CopyPlugin({
200 | patterns: [
201 | {
202 | from: '**/*',
203 | context: path.resolve(__dirname, 'src'),
204 | to: path.resolve(__dirname, 'dist'),
205 | globOptions: {
206 | ignore: [
207 | // All TypeScript files are compiled to JS and
208 | // already copied into /dist so we don't copy them.
209 | '**/*.ts',
210 | '**/*.tsx',
211 | ],
212 | },
213 | },
214 | ],
215 | }),
216 | ] };
217 |
218 | const extraScriptConfig = { ...baseConfig, resolve: {
219 | alias: {
220 | api: path.resolve(__dirname, 'api'),
221 | },
222 | fallback: moduleFallback,
223 | extensions: ['.js', '.tsx', '.ts', '.json'],
224 | } };
225 |
226 | const createArchiveConfig = {
227 | stats: 'errors-only',
228 | entry: './dist/index.js',
229 | resolve: {
230 | fallback: moduleFallback,
231 | },
232 | output: {
233 | filename: 'index.js',
234 | path: publishDir,
235 | },
236 | plugins: [{
237 | apply(compiler) {
238 | compiler.hooks.done.tap('archiveOnBuildListener', onBuildCompleted);
239 | },
240 | }],
241 | };
242 |
243 | function resolveExtraScriptPath(name) {
244 | const relativePath = `./src/${name}`;
245 |
246 | const fullPath = path.resolve(`${rootDir}/${relativePath}`);
247 | if (!fs.pathExistsSync(fullPath)) throw new Error(`Could not find extra script: "${name}" at "${fullPath}"`);
248 |
249 | const s = name.split('.');
250 | s.pop();
251 | const nameNoExt = s.join('.');
252 |
253 | return {
254 | entry: relativePath,
255 | output: {
256 | filename: `${nameNoExt}.js`,
257 | path: distDir,
258 | library: 'default',
259 | libraryTarget: 'commonjs',
260 | libraryExport: 'default',
261 | },
262 | };
263 | }
264 |
265 | function buildExtraScriptConfigs(userConfig) {
266 | if (!userConfig.extraScripts.length) return [];
267 |
268 | const output = [];
269 |
270 | for (const scriptName of userConfig.extraScripts) {
271 | const scriptPaths = resolveExtraScriptPath(scriptName);
272 | output.push({ ...extraScriptConfig, entry: scriptPaths.entry,
273 | output: scriptPaths.output });
274 | }
275 |
276 | return output;
277 | }
278 |
279 | const increaseVersion = version => {
280 | try {
281 | const s = version.split('.');
282 | const d = Number(s[s.length - 1]) + 1;
283 | s[s.length - 1] = `${d}`;
284 | return s.join('.');
285 | } catch (error) {
286 | error.message = `Could not parse version number: ${version}: ${error.message}`;
287 | throw error;
288 | }
289 | };
290 |
291 | const updateVersion = () => {
292 | const packageJson = getPackageJson();
293 | packageJson.version = increaseVersion(packageJson.version);
294 | fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, 'utf8');
295 |
296 | const manifest = readManifest(manifestPath);
297 | manifest.version = increaseVersion(manifest.version);
298 | writeManifest(manifestPath, manifest);
299 |
300 | if (packageJson.version !== manifest.version) {
301 | 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.`));
302 | }
303 | };
304 |
305 | function main(environ) {
306 | const configName = environ['joplin-plugin-config'];
307 | if (!configName) throw new Error('A config file must be specified via the --joplin-plugin-config flag');
308 |
309 | // Webpack configurations run in parallel, while we need them to run in
310 | // sequence, and to do that it seems the only way is to run webpack multiple
311 | // times, with different config each time.
312 |
313 | const configs = {
314 | // Builds the main src/index.ts and copy the extra content from /src to
315 | // /dist including scripts, CSS and any other asset.
316 | buildMain: [pluginConfig],
317 |
318 | // Builds the extra scripts as defined in plugin.config.json. When doing
319 | // so, some JavaScript files that were copied in the previous might be
320 | // overwritten here by the compiled version. This is by design. The
321 | // result is that JS files that don't need compilation, are simply
322 | // copied to /dist, while those that do need it are correctly compiled.
323 | buildExtraScripts: buildExtraScriptConfigs(userConfig),
324 |
325 | // Ths config is for creating the .jpl, which is done via the plugin, so
326 | // it doesn't actually need an entry and output, however webpack won't
327 | // run without this. So we give it an entry that we know is going to
328 | // exist and output in the publish dir. Then the plugin will delete this
329 | // temporary file before packaging the plugin.
330 | createArchive: [createArchiveConfig],
331 | };
332 |
333 | // If we are running the first config step, we clean up and create the build
334 | // directories.
335 | if (configName === 'buildMain') {
336 | fs.removeSync(distDir);
337 | fs.removeSync(publishDir);
338 | fs.mkdirpSync(publishDir);
339 | }
340 |
341 | if (configName === 'updateVersion') {
342 | updateVersion();
343 | return [];
344 | }
345 |
346 | return configs[configName];
347 | }
348 |
349 |
350 | module.exports = (env) => {
351 | let exportedConfigs = [];
352 |
353 | try {
354 | exportedConfigs = main(env);
355 | } catch (error) {
356 | console.error(error.message);
357 | process.exit(1);
358 | }
359 |
360 | if (!exportedConfigs.length) {
361 | // Nothing to do - for example where there are no external scripts to
362 | // compile.
363 | process.exit(0);
364 | }
365 |
366 | return exportedConfigs;
367 | };
368 |
--------------------------------------------------------------------------------