├── .gitignore ├── .idea ├── .gitignore ├── joplin-plugin-better-image.iml ├── modules.xml └── vcs.xml ├── .npmignore ├── GENERATOR_DOC.md ├── License ├── README.md ├── api ├── Global.d.ts ├── Joplin.d.ts ├── JoplinClipboard.d.ts ├── JoplinCommands.d.ts ├── JoplinContentScripts.d.ts ├── JoplinData.d.ts ├── JoplinFilters.d.ts ├── JoplinImaging.d.ts ├── JoplinInterop.d.ts ├── JoplinPlugins.d.ts ├── JoplinSettings.d.ts ├── JoplinViews.d.ts ├── JoplinViewsDialogs.d.ts ├── JoplinViewsMenuItems.d.ts ├── JoplinViewsMenus.d.ts ├── JoplinViewsNoteList.d.ts ├── JoplinViewsPanels.d.ts ├── JoplinViewsToolbarButtons.d.ts ├── JoplinWindow.d.ts ├── JoplinWorkspace.d.ts ├── index.ts ├── noteListType.d.ts ├── noteListType.ts └── types.ts ├── package-lock.json ├── package.json ├── plugin.config.json ├── screenshot ├── admonition.png ├── codeblock.png ├── colorfulBlockquote.png ├── enhancedQuote.png ├── example.png ├── frontmatter.png ├── indentBorder.png ├── linkRender.png ├── mathRender.png ├── mermaidRender.png ├── pseudocode.png ├── quoteRender.png └── taskRender.png ├── src ├── common.ts ├── driver │ ├── codemirror │ │ ├── admonition │ │ │ ├── admonition.css │ │ │ └── index.ts │ │ ├── blockquote │ │ │ ├── blockquote.css │ │ │ └── index.ts │ │ ├── commands │ │ │ └── index.ts │ │ ├── common.ts │ │ ├── focusMode │ │ │ ├── focusModeStyle.css │ │ │ └── index.ts │ │ ├── formattingBar │ │ │ ├── formattingBar.css │ │ │ ├── initFormattingBar.ts │ │ │ ├── v5 │ │ │ │ └── formattingBar.ts │ │ │ └── v6 │ │ │ │ └── formattingBar.ts │ │ ├── indentBorder │ │ │ ├── indentBorder.css │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── inlineMarker │ │ │ ├── index.ts │ │ │ └── inlineMarkerStyle.css │ │ ├── linkFolder │ │ │ ├── codeMarker.ts │ │ │ ├── default.min.css │ │ │ ├── fonts │ │ │ │ ├── KaTeX_AMS-Regular.ttf │ │ │ │ ├── KaTeX_AMS-Regular.woff │ │ │ │ ├── KaTeX_AMS-Regular.woff2 │ │ │ │ ├── KaTeX_Caligraphic-Bold.ttf │ │ │ │ ├── KaTeX_Caligraphic-Bold.woff │ │ │ │ ├── KaTeX_Caligraphic-Bold.woff2 │ │ │ │ ├── KaTeX_Caligraphic-Regular.ttf │ │ │ │ ├── KaTeX_Caligraphic-Regular.woff │ │ │ │ ├── KaTeX_Caligraphic-Regular.woff2 │ │ │ │ ├── KaTeX_Fraktur-Bold.ttf │ │ │ │ ├── KaTeX_Fraktur-Bold.woff │ │ │ │ ├── KaTeX_Fraktur-Bold.woff2 │ │ │ │ ├── KaTeX_Fraktur-Regular.ttf │ │ │ │ ├── KaTeX_Fraktur-Regular.woff │ │ │ │ ├── KaTeX_Fraktur-Regular.woff2 │ │ │ │ ├── KaTeX_Main-Bold.ttf │ │ │ │ ├── KaTeX_Main-Bold.woff │ │ │ │ ├── KaTeX_Main-Bold.woff2 │ │ │ │ ├── KaTeX_Main-BoldItalic.ttf │ │ │ │ ├── KaTeX_Main-BoldItalic.woff │ │ │ │ ├── KaTeX_Main-BoldItalic.woff2 │ │ │ │ ├── KaTeX_Main-Italic.ttf │ │ │ │ ├── KaTeX_Main-Italic.woff │ │ │ │ ├── KaTeX_Main-Italic.woff2 │ │ │ │ ├── KaTeX_Main-Regular.ttf │ │ │ │ ├── KaTeX_Main-Regular.woff │ │ │ │ ├── KaTeX_Main-Regular.woff2 │ │ │ │ ├── KaTeX_Math-BoldItalic.ttf │ │ │ │ ├── KaTeX_Math-BoldItalic.woff │ │ │ │ ├── KaTeX_Math-BoldItalic.woff2 │ │ │ │ ├── KaTeX_Math-Italic.ttf │ │ │ │ ├── KaTeX_Math-Italic.woff │ │ │ │ ├── KaTeX_Math-Italic.woff2 │ │ │ │ ├── KaTeX_SansSerif-Bold.ttf │ │ │ │ ├── KaTeX_SansSerif-Bold.woff │ │ │ │ ├── KaTeX_SansSerif-Bold.woff2 │ │ │ │ ├── KaTeX_SansSerif-Italic.ttf │ │ │ │ ├── KaTeX_SansSerif-Italic.woff │ │ │ │ ├── KaTeX_SansSerif-Italic.woff2 │ │ │ │ ├── KaTeX_SansSerif-Regular.ttf │ │ │ │ ├── KaTeX_SansSerif-Regular.woff │ │ │ │ ├── KaTeX_SansSerif-Regular.woff2 │ │ │ │ ├── KaTeX_Script-Regular.ttf │ │ │ │ ├── KaTeX_Script-Regular.woff │ │ │ │ ├── KaTeX_Script-Regular.woff2 │ │ │ │ ├── KaTeX_Size1-Regular.ttf │ │ │ │ ├── KaTeX_Size1-Regular.woff │ │ │ │ ├── KaTeX_Size1-Regular.woff2 │ │ │ │ ├── KaTeX_Size2-Regular.ttf │ │ │ │ ├── KaTeX_Size2-Regular.woff │ │ │ │ ├── KaTeX_Size2-Regular.woff2 │ │ │ │ ├── KaTeX_Size3-Regular.ttf │ │ │ │ ├── KaTeX_Size3-Regular.woff │ │ │ │ ├── KaTeX_Size3-Regular.woff2 │ │ │ │ ├── KaTeX_Size4-Regular.ttf │ │ │ │ ├── KaTeX_Size4-Regular.woff │ │ │ │ ├── KaTeX_Size4-Regular.woff2 │ │ │ │ ├── KaTeX_Typewriter-Regular.ttf │ │ │ │ ├── KaTeX_Typewriter-Regular.woff │ │ │ │ └── KaTeX_Typewriter-Regular.woff2 │ │ │ ├── footnoteMarker.ts │ │ │ ├── horizontalLineMarker.ts │ │ │ ├── htmlTagRender.ts │ │ │ ├── imageMarker.ts │ │ │ ├── index.ts │ │ │ ├── katex.min.css │ │ │ ├── linkFolder.css │ │ │ ├── linkMarker.ts │ │ │ ├── mathMaker.ts │ │ │ ├── plantumlMarker.ts │ │ │ └── regexps.ts │ │ ├── listNumber │ │ │ └── index.ts │ │ ├── mermaidRender │ │ │ ├── index.ts │ │ │ └── mermaid.css │ │ ├── mode │ │ │ └── index.ts │ │ ├── overlay │ │ │ ├── bullet-list.css │ │ │ ├── bulletList.ts │ │ │ ├── indent.ts │ │ │ ├── index.ts │ │ │ └── overlay.css │ │ ├── quickCommands │ │ │ ├── DateHints.ts │ │ │ ├── MermaidHints.ts │ │ │ ├── PlantumlHints.ts │ │ │ ├── ShortTypeHints.ts │ │ │ ├── quickCommands.css │ │ │ └── quickCommands.ts │ │ ├── searchReplace │ │ │ ├── index.ts │ │ │ └── search.ts │ │ ├── tableFormatter │ │ │ ├── index.ts │ │ │ ├── markdownTableData.ts │ │ │ ├── markdownTableUtility.ts │ │ │ ├── markdowntable.ts │ │ │ ├── tableCommands.ts │ │ │ └── tableFormatterBridge.ts │ │ └── taskAndHeaderRenderer │ │ │ ├── index.ts │ │ │ ├── taskRender.css │ │ │ ├── v5 │ │ │ ├── index.ts │ │ │ ├── render-h-tags.ts │ │ │ ├── render-tables.ts │ │ │ ├── renderer │ │ │ │ └── context.ts │ │ │ ├── table-editor │ │ │ │ ├── build-grid.ts │ │ │ │ ├── build-pipe.ts │ │ │ │ ├── build-simple.ts │ │ │ │ ├── calculate-col-sizes.ts │ │ │ │ ├── compute-css.ts │ │ │ │ ├── index.ts │ │ │ │ ├── parse-grid.ts │ │ │ │ ├── parse-pipe.ts │ │ │ │ ├── parse-simple.ts │ │ │ │ ├── table-editor.ts │ │ │ │ └── types.ts │ │ │ ├── taskRender.ts │ │ │ └── window-register │ │ │ │ └── application-menu-helper.ts │ │ │ └── v6 │ │ │ ├── checklistPlugin.ts │ │ │ └── index.ts │ ├── markdownItRenderer │ │ ├── filePreview │ │ │ ├── filePreviewRenderer.ts │ │ │ └── index.ts │ │ ├── image │ │ │ ├── imageRenderer.ts │ │ │ ├── index.ts │ │ │ └── markdownItPlugin.css │ │ ├── pseudocode │ │ │ ├── index.ts │ │ │ ├── pseudocode.min.css │ │ │ └── pseudocode.ts │ │ └── quote │ │ │ ├── index.ts │ │ │ ├── quoteRender.css │ │ │ └── quoteRender.ts │ └── markdownItRuler │ │ ├── frontMatter │ │ └── index.ts │ │ └── image │ │ ├── index.ts │ │ └── utils.ts ├── index.ts ├── manifest.json ├── settings.ts └── utils │ ├── CMBlockMarkerHelper.ts │ ├── CMBlockMarkerHelperV2.ts │ ├── CMInlineMarkerHelper.ts │ ├── CMInlineMarkerHelperV2.ts │ ├── can-render-element.ts │ ├── citeproc.d.ts │ ├── click-and-clear.ts │ ├── cm-dynamic-require.ts │ ├── cm-utils.ts │ ├── extract-citations.ts │ ├── md-to-html.ts │ ├── reg.ts │ ├── regular-expressions.ts │ └── string-render.ts ├── tsconfig.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | publish/ 4 | dev_commons.ts 5 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /.idea/joplin-plugin-better-image.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.md 2 | !README.md 3 | /*.jpl 4 | /api 5 | /src 6 | /dist 7 | tsconfig.json 8 | webpack.config.js 9 | -------------------------------------------------------------------------------- /GENERATOR_DOC.md: -------------------------------------------------------------------------------- 1 | # Plugin development 2 | 3 | This documentation describes how to create a plugin, and how to work with the plugin builder framework and API. 4 | 5 | ## Installation 6 | 7 | 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/)). 8 | 9 | ```bash 10 | npm install -g yo@4.3.1 11 | npm install -g generator-joplin 12 | ``` 13 | 14 | Then generate your new project: 15 | 16 | ```bash 17 | yo --node-package-manager npm joplin 18 | ``` 19 | 20 | ## Structure 21 | 22 | The main two files you will want to look at are: 23 | 24 | - `/src/index.ts`, which contains the entry point for the plugin source code. 25 | - `/src/manifest.json`, which is the plugin manifest. It contains information such as the plugin a name, version, etc. 26 | 27 | The file `/plugin.config.json` could also be useful if you intend to use [external scripts](#external-script-files), such as content scripts or webview scripts. 28 | 29 | ## Building the plugin 30 | 31 | The plugin is built using Webpack, which creates the compiled code in `/dist`. A JPL archive will also be created at the root, which can use to distribute the plugin. 32 | 33 | To build the plugin, simply run `npm run dist`. 34 | 35 | The project is setup to use TypeScript, although you can change the configuration to use plain JavaScript. 36 | 37 | ## Updating the manifest version number 38 | 39 | 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. 40 | 41 | ## Publishing the plugin 42 | 43 | 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: 44 | 45 | - In `package.json`, the name starts with "joplin-plugin-". For example, "joplin-plugin-toc". 46 | - In `package.json`, the keywords include "joplin-plugin". 47 | - In the `publish/` directory, there should be a .jpl and .json file (which are built by `npm run dist`) 48 | 49 | In general all this is done automatically by the plugin generator, which will set the name and keywords of package.json, and will put the right files in the "publish" directory. But if something doesn't work and your plugin doesn't appear in the repository, double-check the above conditions. 50 | 51 | ## Updating the plugin framework 52 | 53 | To update the plugin framework, run `npm run update`. 54 | 55 | In general this command tries to do the right thing - in particular it's going to merge the changes in package.json and .gitignore instead of overwriting. It will also leave "/src" as well as README.md untouched. 56 | 57 | The file that may cause problem is "webpack.config.js" because it's going to be overwritten. For that reason, if you want to change it, consider creating a separate JavaScript file and include it in webpack.config.js. That way, when you update, you only have to restore the line that include your file. 58 | 59 | ## External script files 60 | 61 | By default, the compiler (webpack) is going to compile `src/index.ts` only (as well as any file it imports), and any other file will simply be copied to the plugin package. In some cases this is sufficient, however if you have [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplincontentscripts.html) or [webview scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinviewspanels.html#addscript) you might want to compile them too, in particular in these two cases: 62 | 63 | - The script is a TypeScript file - in which case it has to be compiled to JavaScript. 64 | 65 | - The script requires modules you've added to package.json. In that case, the script, whether JS or TS, must be compiled so that the dependencies are bundled with the JPL file. 66 | 67 | 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. 68 | 69 | ## More information 70 | 71 | - [Joplin Plugin API](https://joplinapp.org/api/references/plugin_api/classes/joplin.html) 72 | - [Joplin Data API](https://joplinapp.org/help/api/references/rest_api) 73 | - [Joplin Plugin Manifest](https://joplinapp.org/api/references/plugin_manifest/) 74 | - Ask for help on the [forum](https://discourse.joplinapp.org/) or our [Discord channel](https://discord.gg/VSj7AFHvpq) 75 | 76 | ## License 77 | 78 | MIT © Laurent Cozic 79 | -------------------------------------------------------------------------------- /api/Global.d.ts: -------------------------------------------------------------------------------- 1 | import Plugin from '../Plugin'; 2 | import Joplin from './Joplin'; 3 | /** 4 | * @ignore 5 | */ 6 | /** 7 | * @ignore 8 | */ 9 | export default class Global { 10 | private joplin_; 11 | constructor(implementation: any, plugin: Plugin, store: any); 12 | get joplin(): Joplin; 13 | get process(): any; 14 | } 15 | -------------------------------------------------------------------------------- /api/Joplin.d.ts: -------------------------------------------------------------------------------- 1 | import Plugin from '../Plugin'; 2 | import JoplinData from './JoplinData'; 3 | import JoplinPlugins from './JoplinPlugins'; 4 | import JoplinWorkspace from './JoplinWorkspace'; 5 | import JoplinFilters from './JoplinFilters'; 6 | import JoplinCommands from './JoplinCommands'; 7 | import JoplinViews from './JoplinViews'; 8 | import JoplinInterop from './JoplinInterop'; 9 | import JoplinSettings from './JoplinSettings'; 10 | import JoplinContentScripts from './JoplinContentScripts'; 11 | import JoplinClipboard from './JoplinClipboard'; 12 | import JoplinWindow from './JoplinWindow'; 13 | import BasePlatformImplementation from '../BasePlatformImplementation'; 14 | import JoplinImaging from './JoplinImaging'; 15 | /** 16 | * This is the main entry point to the Joplin API. You can access various services using the provided accessors. 17 | * 18 | * The API is now relatively stable and in general maintaining backward compatibility is a top priority, so you shouldn't except much breakages. 19 | * 20 | * If a breaking change ever becomes needed, best effort will be done to: 21 | * 22 | * - Deprecate features instead of removing them, so as to give you time to fix the issue; 23 | * - Document breaking changes in the changelog; 24 | * 25 | * So if you are developing a plugin, please keep an eye on the changelog as everything will be in there with information about how to update your code. 26 | */ 27 | export default class Joplin { 28 | private data_; 29 | private plugins_; 30 | private imaging_; 31 | private workspace_; 32 | private filters_; 33 | private commands_; 34 | private views_; 35 | private interop_; 36 | private settings_; 37 | private contentScripts_; 38 | private clipboard_; 39 | private window_; 40 | private implementation_; 41 | constructor(implementation: BasePlatformImplementation, plugin: Plugin, store: any); 42 | get data(): JoplinData; 43 | get clipboard(): JoplinClipboard; 44 | get imaging(): JoplinImaging; 45 | get window(): JoplinWindow; 46 | get plugins(): JoplinPlugins; 47 | get workspace(): JoplinWorkspace; 48 | get contentScripts(): JoplinContentScripts; 49 | /** 50 | * @ignore 51 | * 52 | * Not sure if it's the best way to hook into the app 53 | * so for now disable filters. 54 | */ 55 | get filters(): JoplinFilters; 56 | get commands(): JoplinCommands; 57 | get views(): JoplinViews; 58 | get interop(): JoplinInterop; 59 | get settings(): JoplinSettings; 60 | /** 61 | * It is not possible to bundle native packages with a plugin, because they 62 | * need to work cross-platforms. Instead access to certain useful native 63 | * packages is provided using this function. 64 | * 65 | * Currently these packages are available: 66 | * 67 | * - [sqlite3](https://www.npmjs.com/package/sqlite3) 68 | * - [fs-extra](https://www.npmjs.com/package/fs-extra) 69 | * 70 | * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/nativeModule) 71 | */ 72 | require(_path: string): any; 73 | versionInfo(): Promise; 74 | } 75 | -------------------------------------------------------------------------------- /api/JoplinClipboard.d.ts: -------------------------------------------------------------------------------- 1 | export default class JoplinClipboard { 2 | private electronClipboard_; 3 | private electronNativeImage_; 4 | constructor(electronClipboard: any, electronNativeImage: any); 5 | readText(): Promise; 6 | writeText(text: string): Promise; 7 | readHtml(): Promise; 8 | writeHtml(html: string): Promise; 9 | /** 10 | * Returns the image in [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) format. 11 | */ 12 | readImage(): Promise; 13 | /** 14 | * Takes an image in [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) format. 15 | */ 16 | writeImage(dataUrl: string): Promise; 17 | /** 18 | * Returns the list available formats (mime types). 19 | * 20 | * For example [ 'text/plain', 'text/html' ] 21 | */ 22 | availableFormats(): Promise; 23 | } 24 | -------------------------------------------------------------------------------- /api/JoplinCommands.d.ts: -------------------------------------------------------------------------------- 1 | import { Command } from './types'; 2 | /** 3 | * This class allows executing or registering new Joplin commands. Commands 4 | * can be executed or associated with 5 | * {@link JoplinViewsToolbarButtons | toolbar buttons} or 6 | * {@link JoplinViewsMenuItems | menu items}. 7 | * 8 | * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/register_command) 9 | * 10 | * ## Executing Joplin's internal commands 11 | * 12 | * It is also possible to execute internal Joplin's commands which, as of 13 | * now, are not well documented. You can find the list directly on GitHub 14 | * though at the following locations: 15 | * 16 | * * [Main screen commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/gui/MainScreen/commands) 17 | * * [Global commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/commands) 18 | * * [Editor commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.ts) 19 | * 20 | * To view what arguments are supported, you can open any of these files 21 | * and look at the `execute()` command. 22 | * 23 | * ## Executing editor commands 24 | * 25 | * There might be a situation where you want to invoke editor commands 26 | * without using a {@link JoplinContentScripts | contentScript}. For this 27 | * reason Joplin provides the built in `editor.execCommand` command. 28 | * 29 | * `editor.execCommand` should work with any core command in both the 30 | * [CodeMirror](https://codemirror.net/doc/manual.html#execCommand) and 31 | * [TinyMCE](https://www.tiny.cloud/docs/api/tinymce/tinymce.editorcommands/#execcommand) editors, 32 | * as well as most functions calls directly on a CodeMirror editor object (extensions). 33 | * 34 | * * [CodeMirror commands](https://codemirror.net/doc/manual.html#commands) 35 | * * [TinyMCE core editor commands](https://www.tiny.cloud/docs/advanced/editor-command-identifiers/#coreeditorcommands) 36 | * 37 | * `editor.execCommand` supports adding arguments for the commands. 38 | * 39 | * ```typescript 40 | * await joplin.commands.execute('editor.execCommand', { 41 | * name: 'madeUpCommand', // CodeMirror and TinyMCE 42 | * args: [], // CodeMirror and TinyMCE 43 | * ui: false, // TinyMCE only 44 | * value: '', // TinyMCE only 45 | * }); 46 | * ``` 47 | * 48 | * [View the example using the CodeMirror editor](https://github.com/laurent22/joplin/blob/dev/packages/app-cli/tests/support/plugins/codemirror_content_script/src/index.ts) 49 | * 50 | */ 51 | export default class JoplinCommands { 52 | /** 53 | * desktop Executes the given 54 | * command. 55 | * 56 | * The command can take any number of arguments, and the supported 57 | * arguments will vary based on the command. For custom commands, this 58 | * is the `args` passed to the `execute()` function. For built-in 59 | * commands, you can find the supported arguments by checking the links 60 | * above. 61 | * 62 | * ```typescript 63 | * // Create a new note in the current notebook: 64 | * await joplin.commands.execute('newNote'); 65 | * 66 | * // Create a new sub-notebook under the provided notebook 67 | * // Note: internally, notebooks are called "folders". 68 | * await joplin.commands.execute('newFolder', "SOME_FOLDER_ID"); 69 | * ``` 70 | */ 71 | execute(commandName: string, ...args: any[]): Promise; 72 | /** 73 | * desktop Registers a new command. 74 | * 75 | * ```typescript 76 | * // Register a new commmand called "testCommand1" 77 | * 78 | * await joplin.commands.register({ 79 | * name: 'testCommand1', 80 | * label: 'My Test Command 1', 81 | * iconName: 'fas fa-music', 82 | * execute: () => { 83 | * alert('Testing plugin command 1'); 84 | * }, 85 | * }); 86 | * ``` 87 | */ 88 | register(command: Command): Promise; 89 | } 90 | -------------------------------------------------------------------------------- /api/JoplinContentScripts.d.ts: -------------------------------------------------------------------------------- 1 | import Plugin from '../Plugin'; 2 | import { ContentScriptType } from './types'; 3 | export default class JoplinContentScripts { 4 | private plugin; 5 | constructor(plugin: Plugin); 6 | /** 7 | * Registers a new content script. Unlike regular plugin code, which runs in 8 | * a separate process, content scripts run within the main process code and 9 | * thus allow improved performances and more customisations in specific 10 | * cases. It can be used for example to load a Markdown or editor plugin. 11 | * 12 | * Note that registering a content script in itself will do nothing - it 13 | * will only be loaded in specific cases by the relevant app modules (eg. 14 | * the Markdown renderer or the code editor). So it is not a way to inject 15 | * and run arbitrary code in the app, which for safety and performance 16 | * reasons is not supported. 17 | * 18 | * The plugin generator provides a way to build any content script you might 19 | * want to package as well as its dependencies. See the [Plugin Generator 20 | * doc](https://github.com/laurent22/joplin/blob/dev/packages/generator-joplin/README.md) 21 | * for more information. 22 | * 23 | * * [View the renderer demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script) 24 | * * [View the editor demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/codemirror_content_script) 25 | * 26 | * See also the [postMessage demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages) 27 | * 28 | * @param type Defines how the script will be used. See the type definition for more information about each supported type. 29 | * @param id A unique ID for the content script. 30 | * @param scriptPath Must be a path relative to the plugin main script. For example, if your file content_script.js is next to your index.ts file, you would set `scriptPath` to `"./content_script.js`. 31 | */ 32 | register(type: ContentScriptType, id: string, scriptPath: string): Promise; 33 | /** 34 | * Listens to a messages sent from the content script using postMessage(). 35 | * See {@link ContentScriptType} for more information as well as the 36 | * [postMessage 37 | * demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages) 38 | */ 39 | onMessage(contentScriptId: string, callback: any): Promise; 40 | } 41 | -------------------------------------------------------------------------------- /api/JoplinData.d.ts: -------------------------------------------------------------------------------- 1 | import { ModelType } from '../../../BaseModel'; 2 | import Plugin from '../Plugin'; 3 | import { Path } from './types'; 4 | /** 5 | * This module provides access to the Joplin data API: https://joplinapp.org/help/api/references/rest_api 6 | * This is the main way to retrieve data, such as notes, notebooks, tags, etc. 7 | * or to update them or delete them. 8 | * 9 | * This is also what you would use to search notes, via the `search` endpoint. 10 | * 11 | * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/simple) 12 | * 13 | * 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. 14 | * And each method takes these parameters: 15 | * 16 | * * `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. 17 | * * `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. 18 | * * `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. 19 | * * `files`: (Optional) Used to create new resources and associate them with files. 20 | * 21 | * 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. 22 | * 23 | * For example: 24 | * 25 | * ```typescript 26 | * // Get a note ID, title and body 27 | * const noteId = 'some_note_id'; 28 | * const note = await joplin.data.get(['notes', noteId], { fields: ['id', 'title', 'body'] }); 29 | * 30 | * // Get all folders 31 | * const folders = await joplin.data.get(['folders']); 32 | * 33 | * // Set the note body 34 | * await joplin.data.put(['notes', noteId], null, { body: "New note body" }); 35 | * 36 | * // Create a new note under one of the folders 37 | * await joplin.data.post(['notes'], null, { body: "my new note", title: "some title", parent_id: folders[0].id }); 38 | * ``` 39 | */ 40 | export default class JoplinData { 41 | private api_; 42 | private pathSegmentRegex_; 43 | private plugin; 44 | constructor(plugin: Plugin); 45 | private serializeApiBody; 46 | private pathToString; 47 | get(path: Path, query?: any): Promise; 48 | post(path: Path, query?: any, body?: any, files?: any[]): Promise; 49 | put(path: Path, query?: any, body?: any, files?: any[]): Promise; 50 | delete(path: Path, query?: any): Promise; 51 | itemType(itemId: string): Promise; 52 | resourcePath(resourceId: string): Promise; 53 | /** 54 | * Gets an item user data. User data are key/value pairs. The `key` can be any 55 | * arbitrary string, while the `value` can be of any type supported by 56 | * [JSON.stringify](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#description) 57 | * 58 | * User data is synchronised across devices, and each value wil be merged based on their timestamp: 59 | * 60 | * - If value is modified by client 1, then modified by client 2, it will take the value from client 2 61 | * - If value is modified by client 1, then deleted by client 2, the value will be deleted after merge 62 | * - 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 63 | */ 64 | userDataGet(itemType: ModelType, itemId: string, key: string): Promise; 65 | /** 66 | * Sets a note user data. See {@link JoplinData.userDataGet} for more details. 67 | */ 68 | userDataSet(itemType: ModelType, itemId: string, key: string, value: T): Promise; 69 | /** 70 | * Deletes a note user data. See {@link JoplinData.userDataGet} for more details. 71 | */ 72 | userDataDelete(itemType: ModelType, itemId: string, key: string): Promise; 73 | } 74 | -------------------------------------------------------------------------------- /api/JoplinFilters.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @ignore 3 | * 4 | * Not sure if it's the best way to hook into the app 5 | * so for now disable filters. 6 | */ 7 | export default class JoplinFilters { 8 | on(name: string, callback: Function): Promise; 9 | off(name: string, callback: Function): Promise; 10 | } 11 | -------------------------------------------------------------------------------- /api/JoplinImaging.d.ts: -------------------------------------------------------------------------------- 1 | import { Rectangle } from './types'; 2 | export interface Implementation { 3 | nativeImage: any; 4 | } 5 | export interface CreateFromBufferOptions { 6 | width?: number; 7 | height?: number; 8 | scaleFactor?: number; 9 | } 10 | export interface ResizeOptions { 11 | width?: number; 12 | height?: number; 13 | quality?: 'good' | 'better' | 'best'; 14 | } 15 | export type Handle = string; 16 | /** 17 | * Provides imaging functions to resize or process images. You create an image 18 | * using one of the `createFrom` functions, then use the other functions to 19 | * process the image. 20 | * 21 | * Images are associated with a handle which is what will be available to the 22 | * plugin. Once you are done with an image, free it using the `free()` function. 23 | * 24 | * [View the 25 | * example](https://github.com/laurent22/joplin/blob/dev/packages/app-cli/tests/support/plugins/imaging/src/index.ts) 26 | * 27 | */ 28 | export default class JoplinImaging { 29 | private implementation_; 30 | private images_; 31 | constructor(implementation: Implementation); 32 | private createImageHandle; 33 | private imageByHandle; 34 | private cacheImage; 35 | createFromPath(filePath: string): Promise; 36 | createFromResource(resourceId: string): Promise; 37 | getSize(handle: Handle): Promise; 38 | resize(handle: Handle, options?: ResizeOptions): Promise; 39 | crop(handle: Handle, rectange: Rectangle): Promise; 40 | toPngFile(handle: Handle, filePath: string): Promise; 41 | /** 42 | * Quality is between 0 and 100 43 | */ 44 | toJpgFile(handle: Handle, filePath: string, quality?: number): Promise; 45 | private tempFilePath; 46 | /** 47 | * Creates a new Joplin resource from the image data. The image will be 48 | * first converted to a JPEG. 49 | */ 50 | toJpgResource(handle: Handle, resourceProps: any, quality?: number): Promise; 51 | /** 52 | * Creates a new Joplin resource from the image data. The image will be 53 | * first converted to a PNG. 54 | */ 55 | toPngResource(handle: Handle, resourceProps: any): Promise; 56 | /** 57 | * Image data is not automatically deleted by Joplin so make sure you call 58 | * this method on the handle once you are done. 59 | */ 60 | free(handle: Handle): Promise; 61 | } 62 | -------------------------------------------------------------------------------- /api/JoplinInterop.d.ts: -------------------------------------------------------------------------------- 1 | import { ExportModule, ImportModule } from './types'; 2 | /** 3 | * Provides a way to create modules to import external data into Joplin or to export notes into any arbitrary format. 4 | * 5 | * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/json_export) 6 | * 7 | * To implement an import or export module, you would simply define an object with various event handlers that are called 8 | * by the application during the import/export process. 9 | * 10 | * See the documentation of the [[ExportModule]] and [[ImportModule]] for more information. 11 | * 12 | * 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 13 | */ 14 | export default class JoplinInterop { 15 | registerExportModule(module: ExportModule): Promise; 16 | registerImportModule(module: ImportModule): Promise; 17 | } 18 | -------------------------------------------------------------------------------- /api/JoplinPlugins.d.ts: -------------------------------------------------------------------------------- 1 | import Plugin from '../Plugin'; 2 | import { ContentScriptType, Script } from './types'; 3 | /** 4 | * This class provides access to plugin-related features. 5 | */ 6 | export default class JoplinPlugins { 7 | private plugin; 8 | constructor(plugin: Plugin); 9 | /** 10 | * Registers a new plugin. This is the entry point when creating a plugin. You should pass a simple object with an `onStart` method to it. 11 | * That `onStart` method will be executed as soon as the plugin is loaded. 12 | * 13 | * ```typescript 14 | * joplin.plugins.register({ 15 | * onStart: async function() { 16 | * // Run your plugin code here 17 | * } 18 | * }); 19 | * ``` 20 | */ 21 | register(script: Script): Promise; 22 | /** 23 | * @deprecated Use joplin.contentScripts.register() 24 | */ 25 | registerContentScript(type: ContentScriptType, id: string, scriptPath: string): Promise; 26 | /** 27 | * Gets the plugin own data directory path. Use this to store any 28 | * plugin-related data. Unlike [[installationDir]], any data stored here 29 | * will be persisted. 30 | */ 31 | dataDir(): Promise; 32 | /** 33 | * Gets the plugin installation directory. This can be used to access any 34 | * asset that was packaged with the plugin. This directory should be 35 | * considered read-only because any data you store here might be deleted or 36 | * re-created at any time. To store new persistent data, use [[dataDir]]. 37 | */ 38 | installationDir(): Promise; 39 | /** 40 | * @deprecated Use joplin.require() 41 | */ 42 | require(_path: string): any; 43 | } 44 | -------------------------------------------------------------------------------- /api/JoplinSettings.d.ts: -------------------------------------------------------------------------------- 1 | import Plugin from '../Plugin'; 2 | import { SettingItem, SettingSection } from './types'; 3 | export interface ChangeEvent { 4 | /** 5 | * Setting keys that have been changed 6 | */ 7 | keys: string[]; 8 | } 9 | export type ChangeHandler = (event: ChangeEvent) => void; 10 | export declare const namespacedKey: (pluginId: string, key: string) => string; 11 | /** 12 | * 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. 13 | * 14 | * Settings are essentially key/value pairs. 15 | * 16 | * Note: Currently this API does **not** provide access to Joplin's built-in settings. This is by design as plugins that modify user settings could give unexpected results 17 | * 18 | * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/settings) 19 | */ 20 | export default class JoplinSettings { 21 | private plugin_; 22 | constructor(plugin: Plugin); 23 | /** 24 | * Registers new settings. 25 | * Note that registering a setting item is dynamic and will be gone next time Joplin starts. 26 | * What it means is that you need to register the setting every time the plugin starts (for example in the onStart event). 27 | * The setting value however will be preserved from one launch to the next so there is no risk that it will be lost even if for some 28 | * reason the plugin fails to start at some point. 29 | */ 30 | registerSettings(settings: Record): Promise; 31 | /** 32 | * @deprecated Use joplin.settings.registerSettings() 33 | * 34 | * Registers a new setting. 35 | */ 36 | registerSetting(key: string, settingItem: SettingItem): Promise; 37 | /** 38 | * Registers a new setting section. Like for registerSetting, it is dynamic and needs to be done every time the plugin starts. 39 | */ 40 | registerSection(name: string, section: SettingSection): Promise; 41 | /** 42 | * Gets a setting value (only applies to setting you registered from your plugin) 43 | */ 44 | value(key: string): Promise; 45 | /** 46 | * Sets a setting value (only applies to setting you registered from your plugin) 47 | */ 48 | setValue(key: string, value: any): Promise; 49 | /** 50 | * Gets a global setting value, including app-specific settings and those set by other plugins. 51 | * 52 | * The list of available settings is not documented yet, but can be found by looking at the source code: 53 | * 54 | * https://github.com/laurent22/joplin/blob/dev/packages/lib/models/Setting.ts#L142 55 | */ 56 | globalValue(key: string): Promise; 57 | /** 58 | * Called when one or multiple settings of your plugin have been changed. 59 | * - For performance reasons, this event is triggered with a delay. 60 | * - You will only get events for your own plugin settings. 61 | */ 62 | onChange(handler: ChangeHandler): Promise; 63 | } 64 | -------------------------------------------------------------------------------- /api/JoplinViews.d.ts: -------------------------------------------------------------------------------- 1 | import Plugin from '../Plugin'; 2 | import JoplinViewsDialogs from './JoplinViewsDialogs'; 3 | import JoplinViewsMenuItems from './JoplinViewsMenuItems'; 4 | import JoplinViewsMenus from './JoplinViewsMenus'; 5 | import JoplinViewsToolbarButtons from './JoplinViewsToolbarButtons'; 6 | import JoplinViewsPanels from './JoplinViewsPanels'; 7 | import JoplinViewsNoteList from './JoplinViewsNoteList'; 8 | /** 9 | * This namespace provides access to view-related services. 10 | * 11 | * 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. 12 | * 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. 13 | */ 14 | export default class JoplinViews { 15 | private store; 16 | private plugin; 17 | private panels_; 18 | private menuItems_; 19 | private menus_; 20 | private toolbarButtons_; 21 | private dialogs_; 22 | private noteList_; 23 | private implementation_; 24 | constructor(implementation: any, plugin: Plugin, store: any); 25 | get dialogs(): JoplinViewsDialogs; 26 | get panels(): JoplinViewsPanels; 27 | get menuItems(): JoplinViewsMenuItems; 28 | get menus(): JoplinViewsMenus; 29 | get toolbarButtons(): JoplinViewsToolbarButtons; 30 | get noteList(): JoplinViewsNoteList; 31 | } 32 | -------------------------------------------------------------------------------- /api/JoplinViewsDialogs.d.ts: -------------------------------------------------------------------------------- 1 | import Plugin from '../Plugin'; 2 | import { ButtonSpec, ViewHandle, DialogResult } from './types'; 3 | /** 4 | * Allows creating and managing dialogs. A dialog is modal window that 5 | * contains a webview and a row of buttons. You can update the 6 | * webview using the `setHtml` method. Dialogs are hidden by default and 7 | * you need to call `open()` to open them. Once the user clicks on a 8 | * button, the `open` call will return an object indicating what button was 9 | * clicked on. 10 | * 11 | * ## Retrieving form values 12 | * 13 | * If your HTML content included one or more forms, a `formData` object 14 | * will also be included with the key/value for each form. 15 | * 16 | * ## Special button IDs 17 | * 18 | * The following buttons IDs have a special meaning: 19 | * 20 | * - `ok`, `yes`, `submit`, `confirm`: They are considered "submit" buttons 21 | * - `cancel`, `no`, `reject`: They are considered "dismiss" buttons 22 | * 23 | * This information is used by the application to determine what action 24 | * should be done when the user presses "Enter" or "Escape" within the 25 | * dialog. If they press "Enter", the first "submit" button will be 26 | * automatically clicked. If they press "Escape" the first "dismiss" button 27 | * will be automatically clicked. 28 | * 29 | * [View the demo 30 | * plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/dialog) 31 | */ 32 | export default class JoplinViewsDialogs { 33 | private store; 34 | private plugin; 35 | private implementation_; 36 | constructor(implementation: any, plugin: Plugin, store: any); 37 | private controller; 38 | /** 39 | * Creates a new dialog 40 | */ 41 | create(id: string): Promise; 42 | /** 43 | * Displays a message box with OK/Cancel buttons. Returns the button index that was clicked - "0" for OK and "1" for "Cancel" 44 | */ 45 | showMessageBox(message: string): Promise; 46 | /** 47 | * Displays a dialog to select a file or a directory. Same options and 48 | * output as 49 | * https://www.electronjs.org/docs/latest/api/dialog#dialogshowopendialogbrowserwindow-options 50 | */ 51 | showOpenDialog(options: any): Promise; 52 | /** 53 | * Sets the dialog HTML content 54 | */ 55 | setHtml(handle: ViewHandle, html: string): Promise; 56 | /** 57 | * Adds and loads a new JS or CSS files into the dialog. 58 | */ 59 | addScript(handle: ViewHandle, scriptPath: string): Promise; 60 | /** 61 | * Sets the dialog buttons. 62 | */ 63 | setButtons(handle: ViewHandle, buttons: ButtonSpec[]): Promise; 64 | /** 65 | * Opens the dialog 66 | */ 67 | open(handle: ViewHandle): Promise; 68 | /** 69 | * Toggle on whether to fit the dialog size to the content or not. 70 | * When set to false, the dialog is set to 90vw and 80vh 71 | * @default true 72 | */ 73 | setFitToContent(handle: ViewHandle, status: boolean): Promise; 74 | } 75 | -------------------------------------------------------------------------------- /api/JoplinViewsMenuItems.d.ts: -------------------------------------------------------------------------------- 1 | import { CreateMenuItemOptions, MenuItemLocation } from './types'; 2 | import Plugin from '../Plugin'; 3 | /** 4 | * Allows creating and managing menu items. 5 | * 6 | * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/register_command) 7 | */ 8 | export default class JoplinViewsMenuItems { 9 | private store; 10 | private plugin; 11 | constructor(plugin: Plugin, store: any); 12 | /** 13 | * Creates a new menu item and associate it with the given command. You can specify under which menu the item should appear using the `location` parameter. 14 | */ 15 | create(id: string, commandName: string, location?: MenuItemLocation, options?: CreateMenuItemOptions): Promise; 16 | } 17 | -------------------------------------------------------------------------------- /api/JoplinViewsMenus.d.ts: -------------------------------------------------------------------------------- 1 | import { MenuItem, MenuItemLocation } from './types'; 2 | import Plugin from '../Plugin'; 3 | /** 4 | * Allows creating menus. 5 | * 6 | * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/menu) 7 | */ 8 | export default class JoplinViewsMenus { 9 | private store; 10 | private plugin; 11 | constructor(plugin: Plugin, store: any); 12 | private registerCommandAccelerators; 13 | /** 14 | * Creates a new menu from the provided menu items and place it at the given location. As of now, it is only possible to place the 15 | * menu as a sub-menu of the application build-in menus. 16 | */ 17 | create(id: string, label: string, menuItems: MenuItem[], location?: MenuItemLocation): Promise; 18 | } 19 | -------------------------------------------------------------------------------- /api/JoplinViewsNoteList.d.ts: -------------------------------------------------------------------------------- 1 | import { Store } from 'redux'; 2 | import Plugin from '../Plugin'; 3 | import { ListRenderer } from './noteListType'; 4 | /** 5 | * This API allows you to customise how each note in the note list is rendered. 6 | * The renderer you implement follows a unidirectional data flow. 7 | * 8 | * The app provides the required dependencies whenever a note is updated - you 9 | * process these dependencies, and return some props, which are then passed to 10 | * your template and rendered. See [[[ListRenderer]]] for a detailed description 11 | * of each property of the renderer. 12 | * 13 | * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/note_list_renderer) 14 | * 15 | * The default list renderer is implemented using the same API, so it worth checking it too: 16 | * 17 | * [Default list renderer](https://github.com/laurent22/joplin/tree/dev/packages/lib/services/noteList/defaultListRenderer.ts) 18 | */ 19 | export default class JoplinViewsNoteList { 20 | private plugin_; 21 | private store_; 22 | constructor(plugin: Plugin, store: Store); 23 | registerRenderer(renderer: ListRenderer): Promise; 24 | } 25 | -------------------------------------------------------------------------------- /api/JoplinViewsPanels.d.ts: -------------------------------------------------------------------------------- 1 | import Plugin from '../Plugin'; 2 | import { ViewHandle } from './types'; 3 | /** 4 | * Allows creating and managing view panels. View panels currently are 5 | * displayed at the right of the sidebar and allows displaying any HTML 6 | * content (within a webview) and update it in real-time. For example it 7 | * could be used to display a table of content for the active note, or 8 | * display various metadata or graph. 9 | * 10 | * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/toc) 11 | */ 12 | export default class JoplinViewsPanels { 13 | private store; 14 | private plugin; 15 | constructor(plugin: Plugin, store: any); 16 | private controller; 17 | /** 18 | * Creates a new panel 19 | */ 20 | create(id: string): Promise; 21 | /** 22 | * Sets the panel webview HTML 23 | */ 24 | setHtml(handle: ViewHandle, html: string): Promise; 25 | /** 26 | * Adds and loads a new JS or CSS files into the panel. 27 | */ 28 | addScript(handle: ViewHandle, scriptPath: string): Promise; 29 | /** 30 | * Called when a message is sent from the webview (using postMessage). 31 | * 32 | * To post a message from the webview to the plugin use: 33 | * 34 | * ```javascript 35 | * const response = await webviewApi.postMessage(message); 36 | * ``` 37 | * 38 | * - `message` can be any JavaScript object, string or number 39 | * - `response` is whatever was returned by the `onMessage` handler 40 | * 41 | * Using this mechanism, you can have two-way communication between the 42 | * plugin and webview. 43 | * 44 | * See the [postMessage 45 | * demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages) for more details. 46 | * 47 | */ 48 | onMessage(handle: ViewHandle, callback: Function): Promise; 49 | /** 50 | * Sends a message to the webview. 51 | * 52 | * The webview must have registered a message handler prior, otherwise the message is ignored. Use; 53 | * 54 | * ```javascript 55 | * webviewApi.onMessage((message) => { ... }); 56 | * ``` 57 | * 58 | * - `message` can be any JavaScript object, string or number 59 | * 60 | * The view API may have only one onMessage handler defined. 61 | * This method is fire and forget so no response is returned. 62 | * 63 | * It is particularly useful when the webview needs to react to events emitted by the plugin or the joplin api. 64 | */ 65 | postMessage(handle: ViewHandle, message: any): void; 66 | /** 67 | * Shows the panel 68 | */ 69 | show(handle: ViewHandle, show?: boolean): Promise; 70 | /** 71 | * Hides the panel 72 | */ 73 | hide(handle: ViewHandle): Promise; 74 | /** 75 | * Tells whether the panel is visible or not 76 | */ 77 | visible(handle: ViewHandle): Promise; 78 | } 79 | -------------------------------------------------------------------------------- /api/JoplinViewsToolbarButtons.d.ts: -------------------------------------------------------------------------------- 1 | import { ToolbarButtonLocation } from './types'; 2 | import Plugin from '../Plugin'; 3 | /** 4 | * Allows creating and managing toolbar buttons. 5 | * 6 | * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/register_command) 7 | */ 8 | export default class JoplinViewsToolbarButtons { 9 | private store; 10 | private plugin; 11 | constructor(plugin: Plugin, store: any); 12 | /** 13 | * Creates a new toolbar button and associate it with the given command. 14 | */ 15 | create(id: string, commandName: string, location: ToolbarButtonLocation): Promise; 16 | } 17 | -------------------------------------------------------------------------------- /api/JoplinWindow.d.ts: -------------------------------------------------------------------------------- 1 | import Plugin from '../Plugin'; 2 | export interface Implementation { 3 | injectCustomStyles(elementId: string, cssFilePath: string): Promise; 4 | } 5 | export default class JoplinWindow { 6 | private plugin_; 7 | private store_; 8 | private implementation_; 9 | constructor(implementation: Implementation, plugin: Plugin, store: any); 10 | /** 11 | * Loads a chrome CSS file. It will apply to the window UI elements, except 12 | * for the note viewer. It is the same as the "Custom stylesheet for 13 | * 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) 14 | * for an example. 15 | */ 16 | loadChromeCssFile(filePath: string): Promise; 17 | /** 18 | * Loads a note CSS file. It will apply to the note viewer, as well as any 19 | * exported or printed note. It is the same as the "Custom stylesheet for 20 | * rendered Markdown" setting. See the [Load CSS Demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/load_css) 21 | * for an example. 22 | */ 23 | loadNoteCssFile(filePath: string): Promise; 24 | } 25 | -------------------------------------------------------------------------------- /api/JoplinWorkspace.d.ts: -------------------------------------------------------------------------------- 1 | import { FolderEntity } from '../../database/types'; 2 | import { Disposable, MenuItem } from './types'; 3 | export interface EditContextMenuFilterObject { 4 | items: MenuItem[]; 5 | } 6 | type FilterHandler = (object: T) => Promise; 7 | declare enum ItemChangeEventType { 8 | Create = 1, 9 | Update = 2, 10 | Delete = 3 11 | } 12 | interface ItemChangeEvent { 13 | id: string; 14 | event: ItemChangeEventType; 15 | } 16 | interface SyncStartEvent { 17 | withErrors: boolean; 18 | } 19 | interface ResourceChangeEvent { 20 | id: string; 21 | } 22 | type ItemChangeHandler = (event: ItemChangeEvent) => void; 23 | type SyncStartHandler = (event: SyncStartEvent) => void; 24 | type ResourceChangeHandler = (event: ResourceChangeEvent) => void; 25 | /** 26 | * The workspace service provides access to all the parts of Joplin that 27 | * are being worked on - i.e. the currently selected notes or notebooks as 28 | * well as various related events, such as when a new note is selected, or 29 | * when the note content changes. 30 | * 31 | * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins) 32 | */ 33 | export default class JoplinWorkspace { 34 | private store; 35 | constructor(store: any); 36 | /** 37 | * Called when a new note or notes are selected. 38 | */ 39 | onNoteSelectionChange(callback: Function): Promise; 40 | /** 41 | * Called when the content of a note changes. 42 | * @deprecated Use `onNoteChange()` instead, which is reliably triggered whenever the note content, or any note property changes. 43 | */ 44 | onNoteContentChange(callback: Function): Promise; 45 | /** 46 | * Called when the content of the current note changes. 47 | */ 48 | onNoteChange(handler: ItemChangeHandler): Promise; 49 | /** 50 | * Called when a resource is changed. Currently this handled will not be 51 | * called when a resource is added or deleted. 52 | */ 53 | onResourceChange(handler: ResourceChangeHandler): Promise; 54 | /** 55 | * Called when an alarm associated with a to-do is triggered. 56 | */ 57 | onNoteAlarmTrigger(handler: Function): Promise; 58 | /** 59 | * Called when the synchronisation process is starting. 60 | */ 61 | onSyncStart(handler: SyncStartHandler): Promise; 62 | /** 63 | * Called when the synchronisation process has finished. 64 | */ 65 | onSyncComplete(callback: Function): Promise; 66 | /** 67 | * Called just before the editor context menu is about to open. Allows 68 | * adding items to it. 69 | */ 70 | filterEditorContextMenu(handler: FilterHandler): void; 71 | /** 72 | * Gets the currently selected note 73 | */ 74 | selectedNote(): Promise; 75 | /** 76 | * Gets the currently selected folder. In some cases, for example during 77 | * search or when viewing a tag, no folder is actually selected in the user 78 | * interface. In that case, that function would return the last selected 79 | * folder. 80 | */ 81 | selectedFolder(): Promise; 82 | /** 83 | * Gets the IDs of the selected notes (can be zero, one, or many). Use the data API to retrieve information about these notes. 84 | */ 85 | selectedNoteIds(): Promise; 86 | } 87 | export {}; 88 | -------------------------------------------------------------------------------- /api/index.ts: -------------------------------------------------------------------------------- 1 | import type Joplin from './Joplin'; 2 | 3 | declare const joplin: Joplin; 4 | 5 | export default joplin; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "joplin-plugin-enhancement", 3 | "version": "1.3.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", 7 | "update": "npm install -g generator-joplin && yo joplin --node-package-manager npm --update --force", 8 | "updateVersion": "webpack --env joplin-plugin-config=updateVersion" 9 | }, 10 | "license": "MIT", 11 | "keywords": [ 12 | "joplin-plugin", 13 | "live-preview" 14 | ], 15 | "files": [ 16 | "publish" 17 | ], 18 | "devDependencies": { 19 | "@types/codemirror": "^5.60.5", 20 | "codemirror": "^5.60.5", 21 | "@codemirror/view": "6.24.1", 22 | "@codemirror/state": "6.4.1", 23 | "@codemirror/language": "6.10.1", 24 | "@types/node": "^18.7.13", 25 | "chalk": "^4.1.0", 26 | "copy-webpack-plugin": "^11.0.0", 27 | "fs-extra": "^10.1.0", 28 | "glob": "^8.0.3", 29 | "tar": "^6.1.11", 30 | "ts-loader": "^9.3.1", 31 | "ts-node": "^10.7.0", 32 | "typescript": "^4.8.2", 33 | "webpack": "^5.74.0", 34 | "webpack-cli": "^4.10.0", 35 | "yargs": "^16.2.0", 36 | "@joplin/lib": "~2.9" 37 | }, 38 | "dependencies": { 39 | "dayjs": "^1.11.2", 40 | "highlight.js": "^11.6.0", 41 | "html-entities": "^2.3.3", 42 | "katex": "^0.16.0", 43 | "markdown-it": "^13.0.1", 44 | "markdown-it-mark": "^3.0.1", 45 | "mermaid": "10.6.1", 46 | "mime-types": "^2.1.35", 47 | "plantuml-encoder": "^1.4.0", 48 | "pseudocode": "^2.2.0", 49 | "showdown": "^2.1.0", 50 | "tippy.js": "^6.3.7", 51 | "ts-debounce": "^4.0.0" 52 | } 53 | } -------------------------------------------------------------------------------- /plugin.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "extraScripts": [ 3 | "driver/codemirror/tableFormatter/index.ts", 4 | 5 | "driver/markdownItRenderer/filePreview/index.ts", 6 | "driver/markdownItRenderer/image/index.ts", 7 | "driver/markdownItRenderer/pseudocode/index.ts", 8 | "driver/markdownItRenderer/quote/index.ts", 9 | 10 | "driver/markdownItRuler/image/index.ts", 11 | "driver/markdownItRuler/frontMatter/index.ts", 12 | 13 | "driver/codemirror/admonition/index.ts", 14 | "driver/codemirror/blockquote/index.ts", 15 | "driver/codemirror/searchReplace/index.ts", 16 | "driver/codemirror/focusMode/index.ts", 17 | "driver/codemirror/indentBorder/index.ts", 18 | "driver/codemirror/overlay/bulletList.ts", 19 | 20 | "driver/codemirror/index.ts" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /screenshot/admonition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/screenshot/admonition.png -------------------------------------------------------------------------------- /screenshot/codeblock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/screenshot/codeblock.png -------------------------------------------------------------------------------- /screenshot/colorfulBlockquote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/screenshot/colorfulBlockquote.png -------------------------------------------------------------------------------- /screenshot/enhancedQuote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/screenshot/enhancedQuote.png -------------------------------------------------------------------------------- /screenshot/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/screenshot/example.png -------------------------------------------------------------------------------- /screenshot/frontmatter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/screenshot/frontmatter.png -------------------------------------------------------------------------------- /screenshot/indentBorder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/screenshot/indentBorder.png -------------------------------------------------------------------------------- /screenshot/linkRender.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/screenshot/linkRender.png -------------------------------------------------------------------------------- /screenshot/mathRender.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/screenshot/mathRender.png -------------------------------------------------------------------------------- /screenshot/mermaidRender.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/screenshot/mermaidRender.png -------------------------------------------------------------------------------- /screenshot/pseudocode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/screenshot/pseudocode.png -------------------------------------------------------------------------------- /screenshot/quoteRender.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/screenshot/quoteRender.png -------------------------------------------------------------------------------- /screenshot/taskRender.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/screenshot/taskRender.png -------------------------------------------------------------------------------- /src/common.ts: -------------------------------------------------------------------------------- 1 | export const ENABLE_TABLE_FORMATTER = 'enableTableFormatter'; 2 | export const ENABLE_LOCAL_PDF_PREVIEW = 'enableLocalPDFPreview'; 3 | export const ENABLE_IMAGE_ENHANCEMENT = 'enableImageEnhancement'; 4 | export const ENABLE_QUICK_COMMANDS = 'enableQuickCommands'; 5 | export const ENABLE_PSEUDOCODE = 'enablePseudocode'; 6 | export const ENABLE_ADMONITION_CM_RENDER = 'enableAdmonitionCmRender'; 7 | export const ENABLE_FRONT_MATTER = 'enhancementEnableFrontMatter'; 8 | export const ENABLE_COLORFUL_QUOTE = 'enhancementEnableColorfulQuote'; 9 | export const ENABLE_LINK_FOLDER = 'enhancementEnableLinkFolder'; 10 | export const ENABLE_BLOCK_LINK_FOLDER = 'enhancementEnableBlockLinkFolder'; 11 | export const ENABLE_BLOCK_IMAGE_FOLDER = 'enhancementEnableBlockImageFolder'; 12 | export const ENABLE_BLOCK_IMAGE_CAPTION = 'enhancementEnableBlockImageCaption'; 13 | export const ENABLE_SEARCH_REPLACE = 'enhancementEnableSearchReplace'; 14 | export const ENABLE_INLINE_MARKER = 'enhancementEnableInlineMarker'; 15 | export const ENABLE_FOCUS_MODE = 'enhancementEnableFocusMode'; 16 | export const ENABLE_INDENT_BORDER = 'enhancementEnableIndentBorder'; 17 | export const ENABLE_TASK_RENDER = 'enhancementEnableTaskRender'; 18 | export const ENABLE_HEADER_HASH_RENDER = 'enhancementEnableHeaderHashRender'; 19 | export const ENABLE_TABLE_RENDER = 'enhancementEnableTableRender'; 20 | export const ENABLE_MATH_RENDER = 'enhancementEnableMathRender'; 21 | export const ENABLE_MERMAID_RENDER = 'enhancementEnableMermaidRender'; 22 | export const ENABLE_PLANTUML_RENDER = 'enhancementEnablePlantUmlRenderer'; 23 | export const ENABLE_CODEBLOCK_HL = 'enhancementEnableCodeBlockHighlight'; 24 | export const ENABLE_FORMATTING_BAR = 'enhancementEnableFormattingBar'; 25 | export const ENABLE_BULLET_LIST_RENDER = 'enhancementEnableBulletListRender'; 26 | export const ENABLE_LIST_NUMBER_AUTO_CORRECT = 'enhancementEnableListNumberAutoCorrect'; 27 | export const ENABLE_HORIZONTAL_LINE_RENDER = 'enhancementEnableHorizontalLineRender'; 28 | 29 | export class EnhancementConfig { 30 | public tableFormatter: boolean; 31 | public localPdfPreview: boolean; 32 | public imageEnhancement: boolean; 33 | public quickCommands: boolean; 34 | public pseudocode: boolean; 35 | public admonitionCmRender: boolean; 36 | public frontMatterRender: boolean; 37 | public colorfulQuote: boolean; 38 | public linkFolder: boolean; 39 | public blockLinkFolder: boolean; 40 | public blockImageFolder: boolean; 41 | public blockImageCaption: boolean; 42 | public searchReplace: boolean; 43 | public inlineMarker: boolean; 44 | public focusMode: boolean; 45 | public indentBorder: boolean; 46 | public taskCmRender: boolean; 47 | public headerHashRender: boolean; 48 | public tableCmRender: boolean; 49 | public mathCmRender: boolean; 50 | public mermaidCmRender: boolean; 51 | public codeBlockHL: boolean; 52 | public formattingBar: boolean; 53 | public dateFormat: string; 54 | public plantumlCmRender: boolean; 55 | public bulletListCmRender: boolean; 56 | public listNumberAutoCorrect: boolean; 57 | public horizontalLineRender: boolean; 58 | } 59 | 60 | 61 | export enum ContextMsgType { 62 | GET_SETTINGS, 63 | OPEN_URL, 64 | RESOURCE_PATH, 65 | SHORTCUT 66 | } 67 | 68 | 69 | export class ContextMsg { 70 | type: ContextMsgType; 71 | content: any; 72 | } 73 | -------------------------------------------------------------------------------- /src/driver/codemirror/admonition/admonition.css: -------------------------------------------------------------------------------- 1 | .CodeMirror-sizer { 2 | padding-left: 1em; 3 | } 4 | 5 | .CodeMirror-linebackground.admonition { 6 | border-left: 0.35em solid blue; 7 | background-color: transparent; 8 | margin-left: -0.8em; 9 | } 10 | 11 | .CodeMirror-linebackground.admonition.header { 12 | background-color: rgba(68,138,255,.1); 13 | } 14 | 15 | .cm-admonition-type, .cm-admonition-end { 16 | color: blue; 17 | } 18 | 19 | .cm-admonition-type.cm-warning, .cm-admonition-end.cm-warning { 20 | color: orange; 21 | } 22 | 23 | .admonition.header { 24 | border-radius: 0.25em 0.25em 0 0; 25 | } 26 | 27 | /* From joplin-plugin-admonition */ 28 | 29 | .admonition.header.note { 30 | background-color: rgba(68,138,255,.1); 31 | } 32 | 33 | .admonition.header.abstract { 34 | background-color: rgba(0,176,255,.1); 35 | } 36 | 37 | .admonition.header.info { 38 | background-color: rgba(0,184,212,.1); 39 | } 40 | 41 | .admonition.header.tip { 42 | background-color: rgba(0,191,165,.1); 43 | } 44 | 45 | .admonition.header.success { 46 | background-color: rgba(0,200,83,.1); 47 | } 48 | 49 | .admonition.header.question { 50 | background-color: rgba(100,221,23,.1); 51 | } 52 | 53 | .admonition.header.warning { 54 | background-color: rgba(255,145,0,.1); 55 | } 56 | 57 | .admonition.header.failure { 58 | background-color: rgba(255,82,82,.1); 59 | } 60 | 61 | .admonition.header.danger { 62 | background-color: rgba(255,23,68,.1); 63 | } 64 | 65 | .admonition.header.bug { 66 | background-color: rgba(245,0,87,.1); 67 | } 68 | 69 | .admonition.header.example { 70 | background-color: rgba(124,77,255,.1); 71 | } 72 | 73 | .admonition.header.quote { 74 | background-color: hsla(0,0%,62%,.1); 75 | } 76 | 77 | .admonition.note { 78 | border-color: #448aff; 79 | box-shadow: 3px 0px 0px #eee; 80 | } 81 | 82 | .cm-admonition-type.cm-note, .cm-admonition-end.cm-note { 83 | color: #448aff; 84 | } 85 | 86 | .admonition.abstract { 87 | border-color: #00b0ff; 88 | box-shadow: 3px 0px 0px #eee; 89 | } 90 | 91 | .cm-admonition-type.cm-abstract, .cm-admonition-end.cm-abstract { 92 | color: #00b0ff; 93 | } 94 | 95 | .admonition.info { 96 | border-color: #00b8d4; 97 | box-shadow: 3px 0px 0px #eee; 98 | } 99 | 100 | .cm-admonition-type.cm-info, .cm-admonition-end.cm-info { 101 | color: #00b8d4; 102 | } 103 | 104 | .admonition.tip { 105 | border-color: #00bfa5; 106 | box-shadow: 3px 0px 0px #eee; 107 | } 108 | 109 | .cm-admonition-type.cm-tip, .cm-admonition-end.cm-tip { 110 | color: #00bfa5; 111 | } 112 | 113 | .admonition.success { 114 | border-color: #00c853; 115 | box-shadow: 3px 0px 0px #eee; 116 | } 117 | 118 | .cm-admonition-type.cm-success, .cm-admonition-end.cm-success { 119 | color: #00c853; 120 | } 121 | 122 | .admonition.question { 123 | border-color: #64dd17; 124 | box-shadow: 3px 0px 0px #eee; 125 | } 126 | 127 | .cm-admonition-type.cm-question, .cm-admonition-end.cm-question { 128 | color: #64dd17; 129 | } 130 | 131 | .admonition.warning { 132 | border-color: #ff9100; 133 | box-shadow: 3px 0px 0px #eee; 134 | } 135 | 136 | .cm-admonition-type.cm-warning, .cm-admonition-end.cm-warning { 137 | color: #ff9100; 138 | } 139 | 140 | .admonition.failure { 141 | border-color: #ff5252; 142 | box-shadow: 3px 0px 0px #eee; 143 | } 144 | 145 | .cm-admonition-type.cm-failure, .cm-admonition-end.cm-failure { 146 | color: #ff5252; 147 | } 148 | 149 | .admonition.danger { 150 | border-color: #ff1744; 151 | box-shadow: 3px 0px 0px #eee; 152 | } 153 | 154 | .cm-admonition-type.cm-danger, .cm-admonition-end.cm-danger { 155 | color: #ff1744; 156 | } 157 | 158 | .admonition.bug { 159 | border-color: #f50057; 160 | box-shadow: 3px 0px 0px #eee; 161 | } 162 | 163 | .cm-admonition-type.cm-bug, .cm-admonition-end.cm-bug { 164 | color: #f50057; 165 | } 166 | 167 | .admonition.example { 168 | border-color: #7c4dff; 169 | box-shadow: 3px 0px 0px #eee; 170 | } 171 | 172 | .cm-admonition-type.cm-example, .cm-admonition-end.cm-example { 173 | color: #7c4dff; 174 | } 175 | 176 | .admonition.quote { 177 | border-color: #9e9e9e; 178 | box-shadow: 3px 0px 0px #eee; 179 | } 180 | 181 | .cm-admonition-type.cm-quote, .cm-admonition-end.cm-quote { 182 | color: #9e9e9e; 183 | } 184 | 185 | span.cm-katex-marker-open, span.cm-katex-marker-close { 186 | color: burlywood; 187 | } 188 | 189 | span.cm-overlay.cm-enhancement-image-size { 190 | color: gray; 191 | } 192 | 193 | .cm-overlay.cm-enhancement-katex-inline-math { 194 | color: gray !important; 195 | } 196 | 197 | .CodeMirror-lines pre.footnote.CodeMirror-line { 198 | font-size: smaller; 199 | opacity: 0.75 !important; 200 | padding-left: 2em !important; 201 | padding-bottom: 5px; 202 | } 203 | 204 | .cm-admonition-type, .cm-admonition-end { 205 | opacity: 0.35 !important; 206 | } 207 | -------------------------------------------------------------------------------- /src/driver/codemirror/blockquote/blockquote.css: -------------------------------------------------------------------------------- 1 | span.enhancement-folded-blockquotes-name, span.enhancement-folded-blockquotes-date { 2 | font-family: 'Font Awesome 5 Free' !important; 3 | color: var(--joplin-color-faded); 4 | } 5 | -------------------------------------------------------------------------------- /src/driver/codemirror/blockquote/index.ts: -------------------------------------------------------------------------------- 1 | import CMInlineMarkerHelper from "../../../utils/CMInlineMarkerHelper"; 2 | import {debounce} from "ts-debounce"; 3 | 4 | const ENHANCED_QUOTE_MARKER = 'enhancement-folded-blockquotes'; 5 | const ENHANCED_QUOTE_MARKER_NAME = 'enhancement-folded-blockquotes-name'; 6 | const ENHANCED_QUOTE_MARKER_DATE = 'enhancement-folded-blockquotes-date'; 7 | 8 | const regexList = [ 9 | /(?<=\[)color=(.*?)(?=\])/g, 10 | /(?<=\[)name=(.*?)(?=\])/g, 11 | /(?<=\[)date=(.*?)(?=\])/g, 12 | ] 13 | 14 | module.exports = { 15 | default: function (_context) { 16 | return { 17 | plugin: function (CodeMirror) { 18 | CodeMirror.defineOption("quoteFolder", [], async function (cm, val, old) { 19 | const mathMarkerHelper = new CMInlineMarkerHelper(_context, cm, regexList, function (match, regIndex: number, from, to, innerDomEleCopy, lastMatchFrom, lastMatchTo) { 20 | const markEl = document.createElement('span'); 21 | markEl.classList.add(ENHANCED_QUOTE_MARKER); 22 | switch (regIndex) { 23 | case 0: 24 | markEl.textContent = `■`; 25 | markEl.style.cssText = `color: ${match[1]}; font-size: large; vertical-align: middle;` 26 | break; 27 | case 1: 28 | markEl.classList.add('fas', 'fa-user', ENHANCED_QUOTE_MARKER_NAME); 29 | break; 30 | case 2: 31 | markEl.classList.add('fas', 'fa-clock', ENHANCED_QUOTE_MARKER_DATE); 32 | break; 33 | default: 34 | break; 35 | } 36 | if (regIndex !== 0) { 37 | markEl.style.cssText = 'color: darkgray;'; 38 | } 39 | return markEl; 40 | }, [ENHANCED_QUOTE_MARKER, ENHANCED_QUOTE_MARKER_NAME, ENHANCED_QUOTE_MARKER_DATE], function (line) { 41 | return line.trim().startsWith('>') 42 | }); 43 | }); 44 | }, 45 | codeMirrorOptions: {'quoteFolder': true}, 46 | assets: function () { 47 | return [ 48 | { 49 | name: "blockquote.css" 50 | } 51 | ]; 52 | } 53 | } 54 | }, 55 | } 56 | -------------------------------------------------------------------------------- /src/driver/codemirror/commands/index.ts: -------------------------------------------------------------------------------- 1 | import { isReadOnly } from "../../../utils/cm-utils"; 2 | 3 | export function initCommands(cm, CodeMirror) { 4 | const commandBridge = new CommandsBridge(cm); 5 | 6 | const addCommand = (name: string, command: ()=>void) => { 7 | // Work around a bug in CodeMirror.defineExtension on CodeMirror 6 -- 8 | // closing and opening settings can leave such extensions only registered once. 9 | // See https://github.com/laurent22/joplin/issues/10023 10 | if (cm.cm6) { 11 | CodeMirror.registerCommand(name, command); 12 | } else { 13 | CodeMirror.defineExtension(name, command); 14 | } 15 | }; 16 | 17 | addCommand('markdownHL1', commandBridge.hL1.bind(commandBridge)); 18 | addCommand('markdownHL2', commandBridge.hL2.bind(commandBridge)); 19 | addCommand('markdownHL3', commandBridge.hL3.bind(commandBridge)); 20 | addCommand('markdownHL4', commandBridge.hL4.bind(commandBridge)); 21 | addCommand('markdownHL5', commandBridge.hL5.bind(commandBridge)); 22 | addCommand('markdownHL6', commandBridge.hL6.bind(commandBridge)); 23 | addCommand('markdownHL7', commandBridge.hL7.bind(commandBridge)); 24 | } 25 | 26 | class CommandsBridge { 27 | constructor(private readonly cm) { 28 | } 29 | 30 | hL1() { 31 | this.hlWithColor('#ffd400'); 32 | } 33 | 34 | hL2() { 35 | this.hlWithColor('#ff6666'); 36 | } 37 | 38 | hL3() { 39 | this.hlWithColor('#5fb236'); 40 | } 41 | 42 | hL4() { 43 | this.hlWithColor('#2ea8e5'); 44 | } 45 | 46 | hL5() { 47 | this.hlWithColor('#a28ae5'); 48 | } 49 | 50 | hL6() { 51 | this.hlWithColor('#e56eee'); 52 | } 53 | 54 | hL7() { 55 | this.hlWithColor('#f19837'); 56 | } 57 | 58 | hlWithColor(color: string) { 59 | if (isReadOnly(this.cm)) { 60 | return; 61 | } 62 | markdownInline(this.cm, ``, '', 'mark') 63 | } 64 | } 65 | 66 | 67 | /** 68 | * Converts selection into a markdown inline element (or removes formatting) 69 | * 70 | * @param {CodeMirror.Editor} cm The CodeMirror instance 71 | * @param {string} pre The formatting mark before the element 72 | * @param {string} post The formatting mark behind the element 73 | */ 74 | function markdownInline (cm: CodeMirror.Editor, pre: string, post: string, tokentype?: string): void { 75 | // Is something selected? 76 | if (!cm.somethingSelected()) { 77 | // TODO: Check token type state at the cursor position to leave the 78 | // mode if already in the mode. 79 | // 80 | // FIXME(cm6): cm.getTokenAt is unsupported in CodeMirror 6. 81 | let currentToken = cm.getTokenAt?.(cm.getCursor())?.type 82 | if (tokentype !== undefined && currentToken !== null && currentToken?.includes(tokentype)) { // -- the tokentypes can be multiple (spell-error, e.g.) 83 | // We are, indeed, currently in this token. So let's check *how* 84 | // we are going to leave the state. 85 | let to = { 'line': cm.getCursor().line, 'ch': cm.getCursor().ch + post.length } 86 | if (cm.getRange(cm.getCursor(), to) === post) { 87 | cm.setCursor(to) 88 | } else { 89 | // No sign in sight -> insert it. Cursor will automatically move forward 90 | cm.replaceSelection(post) 91 | } 92 | } else { 93 | // Not in the mode -> simply do the standard. 94 | cm.replaceSelection(pre + '' + post, 'start') 95 | // Move cursor forward (to the middle of the insertion) 96 | const cur = cm.getCursor() 97 | cur.ch = cur.ch + pre.length 98 | cm.setCursor(cur) 99 | } 100 | return 101 | } 102 | 103 | // Build the regular expression by first escaping problematic characters 104 | let preregex = pre.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') 105 | let postregex = post.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') 106 | 107 | let re = new RegExp('^' + preregex + '.+?' + postregex + '$', 'g') 108 | 109 | const replacements = [] 110 | for (const selection of cm.getSelections()) { 111 | if (re.test(selection)) { 112 | // We got something so unformat. 113 | replacements.push(selection.substr(pre.length, selection.length - pre.length - post.length)) 114 | } else { 115 | // TODO: Check whether the user just selected the text itself and 116 | // not the formatting marks! 117 | 118 | // NOTE: Since the user can triple-click a line, that selection will 119 | // extend beyond the line. So check if the last char of selection is 120 | // a newline, and, if so, pluck that and push it after post. 121 | if (selection[selection.length - 1] === '\n') { 122 | replacements.push(pre + String(selection).substr(0, selection.length - 1) + post + '\n') 123 | } else { 124 | replacements.push(pre + selection + post) 125 | } 126 | } 127 | } 128 | 129 | // Replace with changes selections 130 | cm.replaceSelections(replacements, 'around') 131 | } 132 | -------------------------------------------------------------------------------- /src/driver/codemirror/common.ts: -------------------------------------------------------------------------------- 1 | export const enhancement_overlay_option = 'enhancement_overlay_option'; 2 | export const enhancement_mermaid_render = 'enhancement_mermaid_render'; 3 | export const enhancement_inline_marker = 'enhancement_inline_marker'; 4 | -------------------------------------------------------------------------------- /src/driver/codemirror/focusMode/focusModeStyle.css: -------------------------------------------------------------------------------- 1 | .CodeMirror-sizer { 2 | padding-left: 1em; 3 | } 4 | -------------------------------------------------------------------------------- /src/driver/codemirror/focusMode/index.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | default: function(_context) { 3 | return { 4 | plugin: function (CodeMirror) { }, 5 | assets: function() { 6 | return [ 7 | { 8 | name: 'focusModeStyle.css' 9 | } 10 | ]; 11 | } 12 | } 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /src/driver/codemirror/formattingBar/formattingBar.css: -------------------------------------------------------------------------------- 1 | .editor-formatting-bar { 2 | cursor: default; 3 | display: flex; 4 | background-color: rgba(51, 51,51, 0.85); 5 | border-radius: 4px; 6 | color: white; 7 | text-align: center; 8 | } 9 | 10 | .editor-formatting-bar .button { 11 | display: inline-block; 12 | padding: 4px 8px; 13 | width: 35px; 14 | height: 35px; 15 | line-height: 25px; 16 | transition: 0.3s all ease; 17 | } 18 | 19 | .editor-formatting-bar .button:first-child { 20 | border-top-left-radius: 4px; 21 | border-bottom-left-radius: 4px; 22 | } 23 | 24 | .editor-formatting-bar .button:last-child { 25 | border-top-right-radius: 4px; 26 | border-bottom-right-radius: 4px; 27 | } 28 | 29 | .editor-formatting-bar .button:hover { 30 | background-color: rgb(120, 120, 120); 31 | } 32 | 33 | .editor-formatting-bar i.fas { 34 | font-family: 'Font Awesome 5 Free' !important; 35 | } 36 | 37 | .editor-formatting-bar i.fas.color1 { 38 | color: #ffd400; 39 | } 40 | 41 | .editor-formatting-bar i.fas.color2 { 42 | color: #ff6666; 43 | } 44 | 45 | .editor-formatting-bar i.fas.color3 { 46 | color: #5fb236; 47 | } 48 | 49 | .editor-formatting-bar i.fas.color4 { 50 | color: #2ea8e5; 51 | } 52 | 53 | .editor-formatting-bar i.fas.color5 { 54 | color: #a28ae5; 55 | } 56 | 57 | .editor-formatting-bar i.fas.color6 { 58 | color: #e56eee; 59 | } 60 | 61 | .editor-formatting-bar i.fas.color7 { 62 | color: #f19837; 63 | } 64 | -------------------------------------------------------------------------------- /src/driver/codemirror/formattingBar/initFormattingBar.ts: -------------------------------------------------------------------------------- 1 | import { ContentScriptContext } from "api/types"; 2 | import v6FormattingBar from "./v6/formattingBar" 3 | import v5FormattingBar from "./v5/formattingBar"; 4 | 5 | const initFormattingBar = (context: ContentScriptContext, cm: any) => { 6 | if (cm.cm6) { 7 | cm.addExtension(v6FormattingBar(context)); 8 | } else { 9 | v5FormattingBar(context, cm); 10 | } 11 | }; 12 | 13 | export default initFormattingBar; -------------------------------------------------------------------------------- /src/driver/codemirror/indentBorder/indentBorder.css: -------------------------------------------------------------------------------- 1 | /* from Obsidian */ 2 | 3 | .cm-tab::before, .cm-null.cm-overlay.cm-rm-list-token::before { 4 | content: "\200B"; 5 | width: 1px; 6 | border-right: 1px solid lightgray; 7 | color: transparent; 8 | position: absolute; 9 | top: 0; 10 | bottom: 0; 11 | } 12 | 13 | /* Add a vertical bar to the left side of quote blocks so they match the viewer */ 14 | pre.cm-rm-blockquote.CodeMirror-line { 15 | border-left: 4px solid var(--joplin-code-border-color); 16 | opacity: 0.7; 17 | background-color: var(--joplin-background-color3); 18 | 19 | } 20 | 21 | pre.cm-rm-blockquote span.cm-quote + span.cm-quote { 22 | opacity: 1; 23 | } 24 | 25 | pre.cm-jn-code-block.CodeMirror-line { 26 | padding-left: 0.4em !important; 27 | line-height: initial !important; 28 | } 29 | 30 | /*.CodeMirror-selected {*/ 31 | /* background-color: transparent !important;*/ 32 | /*}*/ 33 | 34 | /*.CodeMirror-selectedtext {*/ 35 | /* background-color: rgba(179, 215, 255, 1) !important;*/ 36 | /*}*/ 37 | 38 | 39 | div.CodeMirror-activeline > pre > span > span.cm-rm-blockquote-token { 40 | opacity: 0.7 !important; 41 | } 42 | 43 | div:not(.CodeMirror-activeline) > pre > span > span.cm-rm-blockquote-token { 44 | opacity: 0 !important; 45 | } 46 | -------------------------------------------------------------------------------- /src/driver/codemirror/indentBorder/index.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | default: function(_context) { 3 | return { 4 | plugin: function (CodeMirror) { }, 5 | codeMirrorResources: [ 6 | 'addon/selection/mark-selection.js' 7 | ], 8 | codeMirrorOptions: { 9 | }, 10 | assets: function() { 11 | return [ 12 | { 13 | name: 'indentBorder.css' 14 | } 15 | ]; 16 | } 17 | } 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /src/driver/codemirror/inlineMarker/inlineMarkerStyle.css: -------------------------------------------------------------------------------- 1 | .cm-editor-marker-colons, 2 | .cm-editor-marker-keyword { 3 | color: orangered !important; 4 | font-weight: bold; 5 | } 6 | 7 | .cm-editor-marker-left-open, 8 | .cm-editor-marker-keyword, 9 | .cm-editor-marker-colons { 10 | background-color: #f5f6f8; 11 | } 12 | 13 | .cm-editor-marker-left-open { 14 | border-radius: 1em 0 0 1em; 15 | color: transparent !important; 16 | } 17 | 18 | .cm-editor-marker-right-close { 19 | border-radius: 0 1em 1em 0; 20 | color: transparent !important; 21 | } 22 | 23 | .cm-editor-marker-right-close, 24 | .cm-editor-marker-text { 25 | background-color: deepskyblue; 26 | } 27 | 28 | .cm-editor-marker-text { 29 | color: white !important; 30 | } 31 | -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/codeMarker.ts: -------------------------------------------------------------------------------- 1 | import {CMBlockMarkerHelperV2} from "../../../utils/CMBlockMarkerHelperV2"; 2 | import {CODE_BLOCK_END, CODE_BLOCK_START} from "./regexps"; 3 | 4 | export const ENHANCED_CODE_BLOCK_MARKER = 'enhancement-code-block-marker'; 5 | const NOT_RENDERED = 'paper|mermaid|pseudocode|plantuml'; 6 | 7 | const hljs = require('highlight.js'); 8 | 9 | 10 | export function createCodeBlockMarker(context, cm) { 11 | return new CMBlockMarkerHelperV2(cm, null, CODE_BLOCK_START, CODE_BLOCK_END, (beginMatch, endMatch, content, fromLine, toLine) => { 12 | const markEl = document.createElement('div'); 13 | 14 | markEl.innerHTML = hljs.highlight(content, {language: beginMatch[1]}).value; 15 | 16 | return markEl; 17 | }, () => { 18 | const span = document.createElement('span'); 19 | span.textContent = '===> Folded Code Block <==='; 20 | span.style.cssText = 'color: lightgray; font-size: smaller; font-style: italic;'; 21 | return span; 22 | }, ENHANCED_CODE_BLOCK_MARKER, true, false, (cm, line, match) => { 23 | if (match[1]) { 24 | return !NOT_RENDERED.includes(match[1].toLowerCase()) && hljs.getLanguage(match[1]); 25 | } else { 26 | return true; 27 | } 28 | }, (content, e) => { 29 | 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/default.min.css: -------------------------------------------------------------------------------- 1 | pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*! 2 | Theme: Agate 3 | Author: (c) Taufik Nurrohman 4 | Maintainer: @taufik-nurrohman 5 | Updated: 2021-04-24 6 | 7 | #333 8 | #62c8f3 9 | #7bd694 10 | #888 11 | #a2fca2 12 | #ade5fc 13 | #b8d8a2 14 | #c6b4f0 15 | #d36363 16 | #fc9b9b 17 | #fcc28c 18 | #ffa 19 | #fff 20 | */.hljs{background:#333;color:#fff}.hljs-doctag,.hljs-meta-keyword,.hljs-name,.hljs-strong{font-weight:700}.hljs-code,.hljs-emphasis{font-style:italic}.hljs-section,.hljs-tag{color:#62c8f3}.hljs-selector-class,.hljs-selector-id,.hljs-template-variable,.hljs-variable{color:#ade5fc}.hljs-meta-string,.hljs-string{color:#a2fca2}.hljs-attr,.hljs-quote,.hljs-selector-attr{color:#7bd694}.hljs-tag .hljs-attr{color:inherit}.hljs-attribute,.hljs-title,.hljs-type{color:#ffa}.hljs-number,.hljs-symbol{color:#d36363}.hljs-bullet,.hljs-template-tag{color:#b8d8a2}.hljs-built_in,.hljs-keyword,.hljs-literal,.hljs-selector-tag{color:#fcc28c}.hljs-code,.hljs-comment,.hljs-formula{color:#888}.hljs-link,.hljs-regexp,.hljs-selector-pseudo{color:#c6b4f0}.hljs-meta{color:#fc9b9b}.hljs-deletion{background:#fc9b9b;color:#333}.hljs-addition{background:#a2fca2;color:#333}.hljs-subst{color:#fff}.hljs a{color:inherit}.hljs a:focus,.hljs a:hover{color:inherit;text-decoration:underline}.hljs mark{background:#555;color:inherit} 21 | -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_AMS-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_AMS-Regular.ttf -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_AMS-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_AMS-Regular.woff -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_AMS-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_AMS-Regular.woff2 -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Caligraphic-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Caligraphic-Bold.ttf -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Caligraphic-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Caligraphic-Bold.woff -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Caligraphic-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Caligraphic-Bold.woff2 -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Caligraphic-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Caligraphic-Regular.ttf -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Caligraphic-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Caligraphic-Regular.woff -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Caligraphic-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Caligraphic-Regular.woff2 -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Fraktur-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Fraktur-Bold.ttf -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Fraktur-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Fraktur-Bold.woff -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Fraktur-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Fraktur-Bold.woff2 -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Fraktur-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Fraktur-Regular.ttf -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Fraktur-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Fraktur-Regular.woff -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Fraktur-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Fraktur-Regular.woff2 -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Main-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Main-Bold.ttf -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Main-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Main-Bold.woff -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Main-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Main-Bold.woff2 -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Main-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Main-BoldItalic.ttf -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Main-BoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Main-BoldItalic.woff -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Main-BoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Main-BoldItalic.woff2 -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Main-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Main-Italic.ttf -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Main-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Main-Italic.woff -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Main-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Main-Italic.woff2 -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Main-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Main-Regular.ttf -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Main-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Main-Regular.woff -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Main-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Main-Regular.woff2 -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Math-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Math-BoldItalic.ttf -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Math-BoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Math-BoldItalic.woff -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Math-BoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Math-BoldItalic.woff2 -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Math-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Math-Italic.ttf -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Math-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Math-Italic.woff -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Math-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Math-Italic.woff2 -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_SansSerif-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_SansSerif-Bold.ttf -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_SansSerif-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_SansSerif-Bold.woff -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_SansSerif-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_SansSerif-Bold.woff2 -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_SansSerif-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_SansSerif-Italic.ttf -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_SansSerif-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_SansSerif-Italic.woff -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_SansSerif-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_SansSerif-Italic.woff2 -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_SansSerif-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_SansSerif-Regular.ttf -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_SansSerif-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_SansSerif-Regular.woff -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_SansSerif-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_SansSerif-Regular.woff2 -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Script-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Script-Regular.ttf -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Script-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Script-Regular.woff -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Script-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Script-Regular.woff2 -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Size1-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Size1-Regular.ttf -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Size1-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Size1-Regular.woff -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Size1-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Size1-Regular.woff2 -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Size2-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Size2-Regular.ttf -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Size2-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Size2-Regular.woff -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Size2-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Size2-Regular.woff2 -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Size3-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Size3-Regular.ttf -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Size3-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Size3-Regular.woff -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Size3-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Size3-Regular.woff2 -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Size4-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Size4-Regular.ttf -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Size4-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Size4-Regular.woff -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Size4-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Size4-Regular.woff2 -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Typewriter-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Typewriter-Regular.ttf -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Typewriter-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Typewriter-Regular.woff -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/fonts/KaTeX_Typewriter-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeptemberHX/joplin-plugin-enhancement/69ddd42bf645feb982d2e1be7d0e33697a61b6ad/src/driver/codemirror/linkFolder/fonts/KaTeX_Typewriter-Regular.woff2 -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/footnoteMarker.ts: -------------------------------------------------------------------------------- 1 | import CMInlineMarkerHelperV2 from "../../../utils/CMInlineMarkerHelperV2"; 2 | import {INLINE_FOOTNOTE_REG} from "./regexps"; 3 | 4 | const ENHANCED_FOOTNOTE_MARKER = 'enhancement-footnote-marker'; 5 | const ENHANCED_FOOTNOTE_MARKER_TEXT = 'enhancement-footnote-marker-text'; 6 | 7 | 8 | export function createInlineFootnoteMarker(context, cm) { 9 | return new CMInlineMarkerHelperV2(cm, INLINE_FOOTNOTE_REG, function (match, from, to) { 10 | const markEl = document.createElement('span'); 11 | markEl.classList.add(ENHANCED_FOOTNOTE_MARKER); 12 | const textEl = document.createElement('span'); 13 | textEl.classList.add(ENHANCED_FOOTNOTE_MARKER_TEXT); 14 | textEl.textContent = match[2]; 15 | markEl.appendChild(textEl); 16 | 17 | const typesStr = cm.getTokenTypeAt(from); 18 | if (typesStr) { 19 | for (const typeStr of typesStr.split(' ')) { 20 | markEl.classList.add(`cm-${typeStr}`); 21 | } 22 | } 23 | return markEl; 24 | }, ENHANCED_FOOTNOTE_MARKER); 25 | } 26 | -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/horizontalLineMarker.ts: -------------------------------------------------------------------------------- 1 | import {CMBlockMarkerHelperV2} from "../../../utils/CMBlockMarkerHelperV2"; 2 | import {HORIZONTAL_LINE_REG} from "./regexps"; 3 | 4 | export const ENHANCED_HORIZONTAL_LINE_MARKER = 'enhancement-horizontal-line-marker'; 5 | 6 | 7 | export function createHorizontalLineMarker(context, cm) { 8 | return new CMBlockMarkerHelperV2(cm, null, HORIZONTAL_LINE_REG, null, (beginMatch, endMatch, content, fromLine, toLine) => { 9 | const hrEl = document.createElement('hr'); 10 | return hrEl; 11 | }, () => { 12 | const span = document.createElement('span'); 13 | span.textContent = '===> Folded Horizontal Line <==='; 14 | span.style.cssText = 'color: lightgray; font-size: smaller; font-style: italic;'; 15 | return span; 16 | }, ENHANCED_HORIZONTAL_LINE_MARKER, true, false, null, null, true); 17 | } 18 | -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/htmlTagRender.ts: -------------------------------------------------------------------------------- 1 | import CMInlineMarkerHelperV2 from "../../../utils/CMInlineMarkerHelperV2"; 2 | import {HTML_INS_REG, HTML_MARK_REG, HTML_TAG_STYLE_REG} from "./regexps"; 3 | import {renderStrToDom} from "../../../utils/string-render"; 4 | 5 | 6 | const ENHANCED_HTML_MARK_MARKER = 'enhancement-html-mark-marker'; 7 | const ENHANCED_HTML_INS_MARKER = 'enhancement-html-ins-marker'; 8 | 9 | 10 | export function createHtmlTagMarkRenderMarker(context, cm, renderBlock: boolean = false) { 11 | return new CMInlineMarkerHelperV2(cm, HTML_MARK_REG, function (match, from, to) { 12 | const markEl = document.createElement('mark'); 13 | markEl.classList.add(ENHANCED_HTML_MARK_MARKER); 14 | markEl.innerHTML = renderStrToDom(match[1]); 15 | 16 | const styleMatch = HTML_TAG_STYLE_REG.exec(match[0]); 17 | if (styleMatch) { 18 | let cssText = ''; 19 | for (const style of styleMatch[1].split(';')) { 20 | if (style.length > 0) { 21 | cssText += style + ' !important;'; 22 | } 23 | } 24 | 25 | markEl.style.cssText = cssText; 26 | } 27 | 28 | return markEl; 29 | }, ENHANCED_HTML_MARK_MARKER); 30 | } 31 | 32 | export function createHtmlTagInsRenderMarker(context, cm, renderBlock: boolean = false) { 33 | return new CMInlineMarkerHelperV2(cm, HTML_INS_REG, function (match, from, to) { 34 | const markEl = document.createElement('ins'); 35 | markEl.classList.add(ENHANCED_HTML_INS_MARKER); 36 | markEl.innerHTML = renderStrToDom(match[1]); 37 | 38 | const styleMatch = HTML_TAG_STYLE_REG.exec(match[0]); 39 | if (styleMatch) { 40 | let cssText = ''; 41 | for (const style of styleMatch[1].split(';')) { 42 | if (style.length > 0) { 43 | cssText += style + ' !important;'; 44 | } 45 | } 46 | 47 | markEl.style.cssText = cssText; 48 | } 49 | 50 | return markEl; 51 | }, ENHANCED_HTML_INS_MARKER); 52 | } 53 | -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/imageMarker.ts: -------------------------------------------------------------------------------- 1 | import CMInlineMarkerHelperV2 from "../../../utils/CMInlineMarkerHelperV2"; 2 | import {CMBlockMarkerHelperV2} from "../../../utils/CMBlockMarkerHelperV2"; 3 | import {BLOCK_IMAGE_REG, INLINE_IMAGE_REG} from "./regexps"; 4 | import {findLineWidgetAtLine} from "../../../utils/cm-utils"; 5 | import {ContextMsgType} from "../../../common"; 6 | import {renderStrToDom} from "../../../utils/string-render"; 7 | 8 | 9 | const ENHANCED_IMAGE_MARKER = 'enhancement-image-marker'; 10 | export const ENHANCED_BLOCK_IMAGE_MARKER = 'enhancement-block-image-marker'; 11 | 12 | const ENHANCED_IMAGE_MARKER_ICON = 'enhancement-image-marker-icon'; 13 | const ENHANCED_IMAGE_MARKER_TEXT = 'enhancement-image-marker-text'; 14 | const ENHANCED_IMAGE_SIZE_TEXT = 'enhancement-image-size-text'; 15 | 16 | export function createInlineImageMarker(context, cm, enableBlockImageRendering: boolean) { 17 | return new CMInlineMarkerHelperV2(cm, INLINE_IMAGE_REG, function (match, from, to) { 18 | const markEl = document.createElement('span'); 19 | 20 | markEl.classList.add(ENHANCED_IMAGE_MARKER); 21 | const iconEl = document.createElement('i'); 22 | iconEl.classList.add(ENHANCED_IMAGE_MARKER_ICON, 'fas', 'fa-image'); 23 | markEl.appendChild(iconEl); 24 | 25 | const textEl = document.createElement('span'); 26 | textEl.classList.add(ENHANCED_IMAGE_MARKER_TEXT); 27 | textEl.innerHTML = renderStrToDom(match[1]); 28 | markEl.appendChild(textEl); 29 | 30 | if (match[3]) { 31 | const sizeEl = document.createElement('span'); 32 | sizeEl.classList.add(ENHANCED_IMAGE_SIZE_TEXT); 33 | sizeEl.textContent = match[3].substr(1, match[3].length - 2); 34 | markEl.appendChild(sizeEl); 35 | } 36 | 37 | const typesStr = cm.getTokenTypeAt(from); 38 | if (typesStr) { 39 | for (const typeStr of typesStr.split(' ')) { 40 | markEl.classList.add(`cm-${typeStr}`); 41 | } 42 | } 43 | return markEl; 44 | }, ENHANCED_IMAGE_MARKER, function (line) { 45 | if (enableBlockImageRendering) { 46 | return !BLOCK_IMAGE_REG.test(line); 47 | } else { 48 | return true; 49 | } 50 | }); 51 | } 52 | 53 | export function createBlockImageMarker(context, cm, enableCaption: boolean) { 54 | return new CMBlockMarkerHelperV2(cm, null, BLOCK_IMAGE_REG, null, (beginMatch, endMatch, content, fromLine, toLine) => { 55 | const markEl = document.createElement('figure'); 56 | const imgEl = document.createElement('img'); 57 | 58 | let imgUrl = beginMatch[2] ? beginMatch[2] : ''; 59 | if (imgUrl.startsWith(':/')) { 60 | context.postMessage({ 61 | type: ContextMsgType.RESOURCE_PATH, 62 | content: beginMatch[2] 63 | }).then((path) => { 64 | imgEl.src = path; 65 | const lineWidget = findLineWidgetAtLine(cm, fromLine, ENHANCED_BLOCK_IMAGE_MARKER + '-line-widget'); 66 | if (lineWidget) { 67 | setTimeout(() => {lineWidget.changed()}, 50); 68 | } 69 | }) 70 | } else { 71 | imgEl.src = imgUrl; 72 | } 73 | markEl.appendChild(imgEl); 74 | 75 | if (enableCaption && beginMatch[1]) { 76 | const captionEl = document.createElement('figcaption'); 77 | captionEl.innerHTML = renderStrToDom(beginMatch[1]); 78 | markEl.appendChild(captionEl); 79 | } 80 | 81 | if (beginMatch[3]) { 82 | const widthMatch = /\{width=(\d+)(px|%|)\}/.exec(beginMatch[3]); 83 | if (widthMatch) { 84 | imgEl.style.width = widthMatch[1] + (widthMatch[2].length === 0 ? 'px' : widthMatch[2]); 85 | } 86 | } 87 | return markEl; 88 | }, () => { 89 | const span = document.createElement('span'); 90 | span.textContent = '===> Folded Image Block <==='; 91 | span.style.cssText = 'color: lightgray; font-size: smaller; font-style: italic;'; 92 | return span; 93 | }, ENHANCED_BLOCK_IMAGE_MARKER, true, false, null, (content, e) => { 94 | const match = BLOCK_IMAGE_REG.exec(content); 95 | context.postMessage({ 96 | type: ContextMsgType.OPEN_URL, 97 | content: match[2] 98 | }); 99 | }); 100 | } 101 | -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/mathMaker.ts: -------------------------------------------------------------------------------- 1 | import {CMBlockMarkerHelperV2} from "../../../utils/CMBlockMarkerHelperV2"; 2 | import katex from 'katex' 3 | import CMInlineMarkerHelperV2 from "../../../utils/CMInlineMarkerHelperV2"; 4 | 5 | export const ENHANCEMENT_MATH_BLOCK_SPAN_MARKER_CLASS = 'enhancement-math-block-marker'; 6 | 7 | export function createInlineMathMarker(context, cm) { 8 | return new CMInlineMarkerHelperV2(cm, /(? { 9 | const markEl = document.createElement('span'); 10 | katex.render(match[1], markEl, { throwOnError: false, displayMode: false, output: 'html' }) 11 | return markEl; 12 | }, 'enhancement-inline-math-marker'); 13 | } 14 | 15 | export function createBlockMathMarker(context, cm) { 16 | return new CMBlockMarkerHelperV2(cm, null, /^\s*\$\$\s*$/, /^\s*\$\$\s*$/, (beginMatch, endMatch, content: string, fromLine, toLine) => { 17 | let divElement = document.createElement("div"); 18 | let spanElement = document.createElement('span'); 19 | if (content.trim().length > 0) { 20 | let cCount = 0; 21 | for (let i = content.length - 1; i >= 0; i--) { 22 | if (content[i] === '\\') { 23 | cCount++; 24 | } else { 25 | break; 26 | } 27 | } 28 | 29 | katex.render(cCount % 2 !== 0 ? content.substring(0, content.length - 1) : content, 30 | spanElement, { throwOnError: false, strict: false, displayMode: true, output: 'html' }) 31 | } else { 32 | spanElement.textContent = 'Empty Math Block'; 33 | } 34 | divElement.appendChild(spanElement); 35 | divElement.style.cssText = 'text-align: center;' 36 | return divElement; 37 | }, () => { 38 | const span = document.createElement('span'); 39 | span.textContent = '===> Folded Math Block <==='; 40 | span.style.cssText = 'color: lightgray; font-size: smaller; font-style: italic;'; 41 | return span; 42 | },ENHANCEMENT_MATH_BLOCK_SPAN_MARKER_CLASS, true, true, (editor, line, match) => { 43 | const token = editor.getTokenTypeAt({line: line, ch: match.index}); 44 | return !token || !token.includes('jn-monospace'); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/plantumlMarker.ts: -------------------------------------------------------------------------------- 1 | import {CMBlockMarkerHelperV2} from "../../../utils/CMBlockMarkerHelperV2"; 2 | var plantumlEncoder = require('plantuml-encoder') 3 | 4 | export const ENHANCEMENT_PLANTUML_SPAN_MARKER_CLASS = 'enhancement-plantuml-block-marker'; 5 | 6 | 7 | export default function createPlantumlMarker(context, cm) { 8 | return new CMBlockMarkerHelperV2(cm, null, /^\s*```plantuml\s*$/, /^\s*```\s*$/, (beginMatch, endMatch, content: string, fromLine, toLine) => { 9 | let divElement = document.createElement("div"); 10 | let imgElement = document.createElement('img'); 11 | const encoded = plantumlEncoder.encode(content); 12 | imgElement.src = `http://www.plantuml.com/plantuml/svg/${encoded}`; 13 | divElement.appendChild(imgElement); 14 | divElement.style.cssText = 'text-align: center;' 15 | return divElement; 16 | }, () => { 17 | const span = document.createElement('span'); 18 | span.textContent = '===> Folded Plantuml Block <==='; 19 | span.style.cssText = 'color: lightgray; font-size: smaller; font-style: italic;'; 20 | return span; 21 | }, ENHANCEMENT_PLANTUML_SPAN_MARKER_CLASS, true, false); 22 | } 23 | -------------------------------------------------------------------------------- /src/driver/codemirror/linkFolder/regexps.ts: -------------------------------------------------------------------------------- 1 | export const INLINE_LINK_REG = /(??[ \t]*((['"])(.*?)\6[ \t]*)?\))$/; 9 | 10 | export const CODE_BLOCK_START = /^\s*```\s*(\S+)\s*$/; 11 | export const CODE_BLOCK_END = /^\s*```\s*$/; 12 | 13 | 14 | export const HTML_MARK_REG = /]*?>(.*?)<\/mark>/g; 15 | export const HTML_TAG_STYLE_REG = /style="(.*?)"/; 16 | 17 | export const HTML_INS_REG = /]*?>(.*?)<\/ins>/g; 18 | 19 | export const HORIZONTAL_LINE_REG = /^-{3,}$/g; 20 | -------------------------------------------------------------------------------- /src/driver/codemirror/listNumber/index.ts: -------------------------------------------------------------------------------- 1 | import {debounce} from "ts-debounce"; 2 | 3 | const listLineReg = /^\s*[\d|a-z]+\.\s/; 4 | const dotListLineReg = /^\s+[\*\-\+]\s/; 5 | const indentLineReg = /^\s+/; 6 | 7 | 8 | export function listNumberCorrector(context, cm: CodeMirror.Editor) { 9 | const fixListNumberDebounce = debounce((cm) => { 10 | fixListNumber(cm); 11 | }, 50); 12 | cm.on('cursorActivity', fixListNumberDebounce); 13 | } 14 | 15 | export function fixListNumber(cm: CodeMirror.Editor) { 16 | let currLineNumber = cm.getCursor().line; 17 | if (!currLineNumber) { 18 | return; 19 | } 20 | 21 | const currLine = cm.getLine(currLineNumber); 22 | if (!listLineReg.test(currLine) && !dotListLineReg.test(currLine)) { 23 | return; 24 | } 25 | 26 | // check list range starts 27 | let listFromLineN = currLineNumber; 28 | for (let lineN = currLineNumber - 1; lineN >= 0; --lineN) { 29 | const lineStr = cm.getLine(lineN); 30 | if (!lineStr) { 31 | break; 32 | } 33 | 34 | if (!listLineReg.test(lineStr) && !dotListLineReg.test(lineStr) && !indentLineReg.test(lineStr)) { 35 | break; 36 | } 37 | 38 | listFromLineN = lineN; 39 | } 40 | 41 | let listToLineN = currLineNumber; 42 | for (let lineN = currLineNumber + 1; lineN < cm.lineCount(); ++lineN) { 43 | const lineStr = cm.getLine(lineN); 44 | if (!lineStr) { 45 | break; 46 | } 47 | 48 | if (!listLineReg.test(lineStr) && !dotListLineReg.test(lineStr) && !indentLineReg.test(lineStr)) { 49 | break; 50 | } 51 | 52 | listToLineN = lineN; 53 | } 54 | // check list range ends 55 | 56 | let lastIndexMap = {0: 0}; 57 | let listIndexType = 1; // 1: number; 2: a b c 58 | let listIndexTypeMap = {}; 59 | let currIndent = 0; 60 | let currIndexN = 0; 61 | for (let lineN = listFromLineN; lineN <= listToLineN; ++lineN) { 62 | // ignore stuff in codeblocks 63 | const token = cm.getTokenTypeAt({line: lineN, ch: 0}); 64 | if (token && token.includes('jn-monospace')) { 65 | continue; 66 | } 67 | 68 | const lineStr = cm.getLine(lineN); 69 | const indent = lineStr.length - lineStr.trimStart().length; 70 | 71 | if (indent > currIndent) { 72 | currIndexN = 1; 73 | } else if (indent == currIndent) { 74 | currIndexN += 1; 75 | } else if (indent < currIndent) { 76 | if (indent in lastIndexMap) { 77 | currIndexN = lastIndexMap[indent] + 1; 78 | } else { 79 | currIndexN = 1; 80 | } 81 | 82 | for (let child of Object.keys(lastIndexMap)) { 83 | if (Number(child) > indent) { 84 | delete lastIndexMap[child]; 85 | } 86 | } 87 | 88 | for (let child of Object.keys(listIndexTypeMap)) { 89 | if (Number(child) > indent) { 90 | delete listIndexTypeMap[child]; 91 | } 92 | } 93 | } 94 | 95 | currIndent = indent; 96 | lastIndexMap[indent] = currIndexN; 97 | 98 | const regResult = listLineReg.exec(lineStr); 99 | 100 | if (!regResult) { 101 | continue; 102 | } 103 | 104 | const numberStr = regResult[0].substr(indent, regResult[0].length - indent); 105 | if (!(indent in listIndexTypeMap)) { 106 | if (/\d+\.\s/.test(numberStr)) { 107 | listIndexTypeMap[indent] = 1; 108 | } else if (/[a-z]+\.\s/.test(numberStr)) { 109 | listIndexTypeMap[indent] = 2; 110 | } else { 111 | listIndexTypeMap[indent] = 1; 112 | } 113 | } 114 | 115 | listIndexType = listIndexTypeMap[indent]; 116 | let replacement = ''; 117 | switch (listIndexType) { 118 | case 1: 119 | replacement = `${currIndexN}. `; 120 | break; 121 | case 2: 122 | replacement = `${convertToNumberingScheme(currIndexN)}. `; 123 | break; 124 | default: 125 | break; 126 | } 127 | 128 | if (numberStr == replacement) { 129 | continue; 130 | } 131 | 132 | cm.replaceRange(replacement, {'line': lineN, 'ch': indent}, {'line': lineN, 'ch': regResult[0].length}); 133 | } 134 | } 135 | 136 | function convertToNumberingScheme(number) { 137 | var baseChar = ("a").charCodeAt(0), 138 | letters = ""; 139 | 140 | do { 141 | number -= 1; 142 | letters = String.fromCharCode(baseChar + (number % 26)) + letters; 143 | number = (number / 26) >> 0; // quick `floor` 144 | } while(number > 0); 145 | 146 | return letters; 147 | } 148 | -------------------------------------------------------------------------------- /src/driver/codemirror/mermaidRender/index.ts: -------------------------------------------------------------------------------- 1 | import {CMBlockMarkerHelper} from "../../../utils/CMBlockMarkerHelper"; 2 | import mermaid from 'mermaid' 3 | import {LineHandle} from "codemirror"; 4 | 5 | const ENHANCEMENT_MERMAID_SPAN_MARKER_CLASS = 'enhancement-mermaid-block-marker'; 6 | const ENHANCEMENT_MERMAID_SPAN_MARKER_LINE_CLASS = 'enhancement-mermaid-block-marker-line'; 7 | 8 | // Initialise the mermaid API. 9 | mermaid.initialize({ startOnLoad: false }) 10 | 11 | export default function mermaidRender(cm) { 12 | // Block Katex Math Render 13 | new CMBlockMarkerHelper(cm, null, /^\s*```mermaid\s*$/, /^\s*```\s*$/, (beginMatch, endMatch, content, fromLine, toLine) => { 14 | // code from zettlr 15 | let svg = document.createElement('span') 16 | svg.innerText = "..." 17 | svg.classList.add('mermaid-chart') 18 | mermaid.render(`graphDivL${fromLine}-L${toLine}${Date.now()}`, content).then((renderResult) => { 19 | svg.innerHTML = renderResult.svg 20 | }, (err) => { 21 | svg.classList.add('error') 22 | // TODO: Localise! 23 | svg.innerText = `Could not render Graph:\n\n${err.message as string}` 24 | }) 25 | return svg; 26 | }, () => { 27 | const span = document.createElement('span'); 28 | span.textContent = '===> Folded Mermaid Code Block <==='; 29 | span.style.cssText = 'color: lightgray; font-size: smaller; font-style: italic;'; 30 | return span; 31 | },ENHANCEMENT_MERMAID_SPAN_MARKER_CLASS, true); 32 | 33 | cm.on('renderLine', (editor, line: LineHandle, element: Element) => { 34 | if (element.getElementsByClassName(ENHANCEMENT_MERMAID_SPAN_MARKER_CLASS).length > 0) { 35 | element.classList.add(ENHANCEMENT_MERMAID_SPAN_MARKER_LINE_CLASS); 36 | } 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /src/driver/codemirror/mermaidRender/mermaid.css: -------------------------------------------------------------------------------- 1 | .CodeMirror-sizer .mermaid-chart div, 2 | .CodeMirror-sizer .mermaid-chart span { 3 | font-family: "trebuchet ms",verdana,arial,sans-serif !important; 4 | } 5 | 6 | .CodeMirror-sizer .mermaid-chart svg { 7 | max-height: 300px; 8 | } 9 | 10 | .enhancement-mermaid-block-marker-line { 11 | display: none; 12 | } 13 | -------------------------------------------------------------------------------- /src/driver/codemirror/mode/index.ts: -------------------------------------------------------------------------------- 1 | export function initCodeMode(context, CodeMirror) { 2 | // FIXME(cm6): For now, not supported in CM6 3 | if (CodeMirror.cm6) { 4 | return; 5 | } 6 | 7 | CodeMirror.defineMode('pseudocode', function (config, modeConfig) { 8 | return CodeMirror.getMode(config, { name: 'stex', inMathMode: false }); 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /src/driver/codemirror/overlay/bullet-list.css: -------------------------------------------------------------------------------- 1 | .cm-variable-2.cm-rm-single-list-token, 2 | .cm-variable-3.cm-rm-single-list-token, 3 | .cm-keyword.cm-rm-single-list-token { 4 | visibility: hidden; 5 | position: relative; 6 | } 7 | 8 | .cm-variable-2.cm-rm-single-list-token:after, 9 | .cm-variable-3.cm-rm-single-list-token:after, 10 | .cm-keyword.cm-rm-single-list-token:after { 11 | visibility: visible; 12 | content: '\2022'; 13 | position: absolute; 14 | right: 0; 15 | } 16 | -------------------------------------------------------------------------------- /src/driver/codemirror/overlay/bulletList.ts: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | default: function (_context) { 4 | return { 5 | plugin: function (CodeMirror) { 6 | 7 | }, 8 | assets: function () { 9 | return [ 10 | { 11 | name: "bullet-list.css" 12 | } 13 | ]; 14 | } 15 | } 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /src/driver/codemirror/overlay/indent.ts: -------------------------------------------------------------------------------- 1 | // This module is modified from the CodeMirror indent wrap demo 2 | // https://codemirror.net/demo/indentwrap.html 3 | 4 | import {list_token_regex} from './index'; 5 | 6 | // These variables are cached when the plugin is loaded 7 | // This stores the width of a space in the current font 8 | let spaceWidth = 0; 9 | // This stores the width of a monospace character using the current monospace font 10 | let monoSpaceWidth = 0; 11 | // This stores the width of the > character in the current font 12 | let blockCharWidth = 0; 13 | 14 | // Must be called when the editor is mounted 15 | export function calculateSpaceWidth(cm: any) { 16 | spaceWidth = charWidth(cm, ' ', ''); 17 | monoSpaceWidth = charWidth(cm, ' ', 'cm-rm-monospace'); 18 | blockCharWidth = charWidth(cm, '>', ''); 19 | } 20 | 21 | // Adapted from codemirror/lib/codemirror.js 22 | function charWidth(cm: any, chr: string, cls: string) { 23 | let e = document.createElement('span'); 24 | if (cls) 25 | e.classList.add(cls); 26 | e.style.whiteSpace = "pre-wrap"; 27 | e.appendChild(document.createTextNode(chr.repeat(10))) 28 | 29 | const measure = cm.getWrapperElement().getElementsByClassName('CodeMirror-measure')[0]; 30 | if (measure.firstChild) 31 | measure.removeChild(measure.firstChild); 32 | 33 | measure.appendChild(e); 34 | 35 | const rect = e.getBoundingClientRect() 36 | const width = (rect.right - rect.left) / 10; 37 | 38 | return width || cm.defaultCharWidth(); 39 | 40 | } 41 | 42 | // Adapted from 43 | // https://github.com/codemirror/CodeMirror/blob/master/demo/indentwrap.html 44 | export function onRenderLine(cm: any, line: any, element: HTMLElement, CodeMirror: any) { 45 | 46 | const matches = line.text.match(list_token_regex); 47 | if (!matches) return; 48 | 49 | let off = CodeMirror.countColumn(line.text, matches[0].length, cm.getOption("tabSize")) * spaceWidth; 50 | 51 | // Special case handling for checkboxes with monospace enabled 52 | if (matches[0].indexOf('[') > 0) { 53 | // "- [ ] " is 6 characters 54 | off += monoSpaceWidth * 6 - spaceWidth * 6; 55 | } else if (matches[0].indexOf('>') >= 0) { 56 | off += blockCharWidth - spaceWidth; 57 | } 58 | 59 | element.style.textIndent = "-" + off + "px"; 60 | element.style.paddingLeft = off + "px"; 61 | } 62 | -------------------------------------------------------------------------------- /src/driver/codemirror/overlay/overlay.css: -------------------------------------------------------------------------------- 1 | .cm-rm-monospace { 2 | font-family: monospace !important; 3 | } 4 | .cm-rm-ins { 5 | text-decoration: underline; 6 | } 7 | .cm-rm-sub { 8 | vertical-align: sub; 9 | font-size: smaller; 10 | } 11 | /*.cm-rm-sup {*/ 12 | /* vertical-align: super;*/ 13 | /* font-size: smaller;*/ 14 | /*}*/ 15 | .cm-rm-highlight { 16 | background-color: var(--joplin-search-marker-background-color); 17 | color: var(--joplin-search-marker-color); 18 | } 19 | .CodeMirror-selectedtext.cm-rm-highlight { 20 | background-color: #e5d3ce; 21 | } 22 | /* Needed for the renderLine indent hack to work */ 23 | .CodeMirror pre > * { text-indent: 0px; } 24 | 25 | span.cm-overlay.cm-rm-todo-priority-1, 26 | span.cm-overlay.cm-rm-todo-priority-2, 27 | span.cm-overlay.cm-rm-todo-priority-3, 28 | span.cm-overlay.cm-rm-todo-priority-4 { 29 | padding: 1px 2px; 30 | border-radius: 4px; 31 | color: white !important; 32 | opacity: 0.75; 33 | } 34 | 35 | span.cm-overlay.cm-rm-todo-priority-1 { 36 | background: #e33e2f; 37 | } 38 | 39 | span.cm-overlay.cm-rm-todo-priority-2 { 40 | background: #f8d15f; 41 | } 42 | 43 | span.cm-overlay.cm-rm-todo-priority-3 { 44 | background: #549644; 45 | } 46 | 47 | span.cm-overlay.cm-rm-todo-priority-4 { 48 | background: #ececec; 49 | } 50 | 51 | span.cm-overlay.cm-rm-todo-date { 52 | background: #316cf4; 53 | padding: 1px 2px; 54 | border-radius: 4px; 55 | color: white !important; 56 | opacity: 0.75; 57 | } 58 | 59 | span.cm-overlay.cm-rm-todo-tag { 60 | background: #5dc7ec; 61 | padding: 1px 2px; 62 | border-radius: 4px; 63 | color: white !important; 64 | opacity: 0.75; 65 | } 66 | 67 | span.cm-overlay.cm-rm-todo-project { 68 | background: #f6c344; 69 | padding: 1px 2px; 70 | border-radius: 4px; 71 | color: white !important; 72 | opacity: 0.75; 73 | } 74 | -------------------------------------------------------------------------------- /src/driver/codemirror/quickCommands/DateHints.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | 3 | 4 | export function getDateHints(dateFormat: string) { 5 | return [ 6 | { 7 | text: dayjs().format(dateFormat), 8 | displayText: '/today', 9 | description: 'Today', 10 | inline: true 11 | }, 12 | { 13 | text: dayjs().format(`${dateFormat} HH:mm:ss`), 14 | displayText: '/now', 15 | description: 'Now', 16 | inline: true 17 | }, 18 | { 19 | text: dayjs().add(1, 'day').format(dateFormat), 20 | displayText: '/tomorrow', 21 | description: 'Tomorrow', 22 | inline: true 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /src/driver/codemirror/quickCommands/MermaidHints.ts: -------------------------------------------------------------------------------- 1 | import {Hint} from "./quickCommands"; 2 | 3 | const MermaidHints: Hint[] = [ 4 | { 5 | text: '```mermaid\n' + 6 | 'graph\n' + 7 | '\n' + 8 | '```', 9 | displayText: '/flowchart', 10 | description: 'Mermaid flowchart', 11 | inline: false 12 | }, 13 | { 14 | text: '```mermaid\n' + 15 | 'sequenceDiagram\n' + 16 | '\n' + 17 | '```', 18 | displayText: '/sequenceDiagram', 19 | description: 'Mermaid sequence diagram', 20 | inline: false 21 | }, 22 | { 23 | text: '```mermaid\n' + 24 | 'gantt\n' + 25 | '\n' + 26 | '```', 27 | displayText: '/gantt', 28 | description: 'Mermaid gantt diagram', 29 | inline: false 30 | }, 31 | { 32 | text: '```mermaid\n' + 33 | 'classDiagram\n' + 34 | '\n' + 35 | '```', 36 | displayText: '/classDiagram', 37 | description: 'Mermaid class diagram', 38 | inline: false 39 | }, 40 | { 41 | text: '```mermaid\n' + 42 | 'erDiagram\n' + 43 | '\n' + 44 | '```', 45 | displayText: '/erDiagram', 46 | description: 'Mermaid entity relationship diagram', 47 | inline: false 48 | }, 49 | { 50 | text: '```mermaid\n' + 51 | 'journey\n' + 52 | '\n' + 53 | '```', 54 | displayText: '/journey', 55 | description: 'Mermaid journey diagram', 56 | inline: false 57 | } 58 | ] 59 | 60 | export default MermaidHints; 61 | -------------------------------------------------------------------------------- /src/driver/codemirror/quickCommands/PlantumlHints.ts: -------------------------------------------------------------------------------- 1 | import { Hint } from "./quickCommands"; 2 | 3 | const sequanceDiagramText = () => { 4 | return '```plantuml\n' + 5 | '@startuml diagramName\n' + 6 | 'participant fullName as name\n' + 7 | 'participant another\n\n\n' + 8 | 9 | 'group group_name\n' + 10 | ' name -> another: some description\n' + 11 | ' another -> name: some description \n' + 12 | 'end\n' + 13 | '@enduml\n' + 14 | '```' 15 | }; 16 | 17 | 18 | const classDiagramText = () => { 19 | return '```plantuml\n' + 20 | '@startuml\n' + 21 | 'interface Store {\n' + 22 | ' List getProducts\n' + 23 | '}\n' + 24 | 'Store *-- Product\n\n' + 25 | 26 | 'class LintaoStore implements Store\n' + 27 | 'interface Product\n' + 28 | 'class Computer implements Product\n' + 29 | 'class Phone implements Product\n' + 30 | '@enduml\n' + 31 | '```' 32 | } 33 | 34 | const PlantumlHints: Hint[] = [ 35 | { 36 | text: sequanceDiagramText(), 37 | displayText: "/psequenceDiagram", 38 | description: 'Sequence diagram | plantuml', 39 | inline: false 40 | }, 41 | { 42 | text: classDiagramText(), 43 | displayText: "/pclassDiagramText", 44 | description: 'Class diagram | plantuml', 45 | inline: false 46 | } 47 | ]; 48 | 49 | export default PlantumlHints; 50 | 51 | -------------------------------------------------------------------------------- /src/driver/codemirror/quickCommands/ShortTypeHints.ts: -------------------------------------------------------------------------------- 1 | import { Hint } from "./quickCommands"; 2 | 3 | const ShortTypeHints: Hint[] = [ 4 | { 5 | text: '- [ ] ', 6 | displayText: '/task', 7 | description: 'Checkbox', 8 | inline: true 9 | }, 10 | { 11 | text: '``', 12 | displayText: '/inline', 13 | description: 'Inline Code', 14 | inline: true 15 | }, 16 | { 17 | text: '```\n' + 18 | '\n' + 19 | '```', 20 | displayText: '/code', 21 | description: 'Codeblock', 22 | inline: false 23 | } 24 | ] 25 | 26 | export default ShortTypeHints; 27 | -------------------------------------------------------------------------------- /src/driver/codemirror/quickCommands/quickCommands.css: -------------------------------------------------------------------------------- 1 | /* from https://github.com/ylc395/joplin-plugin-note-link-system/blob/main/src/driver/codeMirror/style.css */ 2 | 3 | .CodeMirror-hints { 4 | position: absolute; 5 | z-index: 10; 6 | overflow: hidden; 7 | list-style: none; 8 | 9 | margin: 0; 10 | padding: 2px; 11 | 12 | -webkit-box-shadow: 2px 3px 5px rgba(0, 0, 0, 0.2); 13 | -moz-box-shadow: 2px 3px 5px rgba(0, 0, 0, 0.2); 14 | box-shadow: 2px 3px 5px rgba(0, 0, 0, 0.2); 15 | border-radius: 3px; 16 | border: 1px solid silver; 17 | 18 | background: var(--joplin-background-color); 19 | font-size: 90%; 20 | font-family: monospace; 21 | 22 | max-height: 20em; 23 | overflow-y: auto; 24 | } 25 | 26 | .quick-commands-hint.CodeMirror-hint { 27 | margin: 0; 28 | padding: 0 4px; 29 | border-radius: 2px; 30 | white-space: pre; 31 | color: var(--joplin-color); 32 | cursor: pointer; 33 | } 34 | 35 | .quick-commands-hint.CodeMirror-hint-active { 36 | background: var(--joplin-raised-background-color); 37 | color: var(--joplin-raised-color); 38 | } 39 | 40 | .quick-commands-hint-path { 41 | margin-left: 1em; 42 | font-size: 0.8em; 43 | color: var(--joplin-color-faded); 44 | } 45 | -------------------------------------------------------------------------------- /src/driver/codemirror/searchReplace/index.ts: -------------------------------------------------------------------------------- 1 | import {searchReplace} from "./search"; 2 | 3 | module.exports = { 4 | default: function(_context) { 5 | return { 6 | plugin: function (CodeMirror) { 7 | searchReplace(CodeMirror); 8 | }, 9 | codeMirrorOptions: { 'searchbox': true }, 10 | codeMirrorResources: [ 11 | 'addon/search/searchcursor', 12 | 'addon/scroll/annotatescrollbar', 13 | 'addon/search/matchesonscrollbar' 14 | ], 15 | assets: function() { 16 | return []; 17 | } 18 | } 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /src/driver/codemirror/tableFormatter/index.ts: -------------------------------------------------------------------------------- 1 | import {navigateCell, insertColumn, insertRow, deleteColume} from "./tableCommands"; 2 | import TableFormatterBridge from "./tableFormatterBridge"; 3 | 4 | 5 | module.exports = { 6 | default: function(_context) { 7 | return { 8 | plugin: function (CodeMirror) { 9 | CodeMirror.defineOption("tableFormatter", [], async function(cm, val, old) { 10 | if (old && old != CodeMirror.Init) { 11 | cm.off('keyup', tabPressed); 12 | } 13 | cm.on('keyup', tabPressed); 14 | 15 | const commandBridge = new TableFormatterBridge(cm); 16 | CodeMirror.defineExtension('insertColumnLeft', commandBridge.insertColumnLeft.bind(commandBridge)); 17 | CodeMirror.defineExtension('insertColumnRight', commandBridge.insertColumnRight.bind(commandBridge)); 18 | CodeMirror.defineExtension('insertRowAbove', commandBridge.insertRowAbove.bind(commandBridge)); 19 | CodeMirror.defineExtension('insertRowBelow', commandBridge.insertRowBelow.bind(commandBridge)); 20 | CodeMirror.defineExtension('deleteColumn', commandBridge.deleteColumn.bind(commandBridge)); 21 | CodeMirror.defineExtension('alignColumns', commandBridge.alignColumnsCommand.bind(commandBridge)); 22 | }); 23 | 24 | function tabPressed(cm, event) { 25 | if (event.code === 'Tab' && !event.shiftKey) { 26 | navigateCell(cm, true, false); 27 | } else if (event.code === 'Tab' && event.shiftKey) { 28 | navigateCell(cm, true, true); 29 | } else if (event.shiftKey && event.ctrlKey && !event.metaKey) { 30 | switch (event.code) { 31 | case 'ArrowLeft': 32 | insertColumn(cm, true); 33 | break; 34 | case 'ArrowRight': 35 | insertColumn(cm, false); 36 | break; 37 | case 'ArrowUp': 38 | insertRow(cm, true); 39 | break; 40 | case 'ArrowDown': 41 | insertRow(cm, false); 42 | break; 43 | case 'Backspace': 44 | deleteColume(cm); 45 | break; 46 | default:break; 47 | } 48 | } 49 | } 50 | }, 51 | codeMirrorOptions: { 'tableFormatter': true }, 52 | assets: function() { 53 | return [ ]; 54 | } 55 | } 56 | }, 57 | } 58 | -------------------------------------------------------------------------------- /src/driver/codemirror/tableFormatter/markdownTableData.ts: -------------------------------------------------------------------------------- 1 | // implemented by takumisoft68 at https://github.com/takumisoft68/vscode-markdown-table/blob/master/src/markdownTableData.ts 2 | 3 | export default class MarkdownTableData { 4 | public readonly originalText: string; 5 | public readonly aligns: [string, string][]; 6 | public readonly columns: string[]; 7 | public readonly cells: string[][]; 8 | public readonly leftovers: string[]; 9 | public readonly indent: string; 10 | 11 | constructor(_text: string, _aligns: [string, string][], _columns: string[], _cells: string[][], _leftovers: string[], _indent: string) { 12 | this.originalText = _text; 13 | this.aligns = _aligns; 14 | this.columns = _columns; 15 | this.cells = _cells; 16 | this.leftovers = _leftovers; 17 | this.indent = _indent; 18 | } 19 | }; -------------------------------------------------------------------------------- /src/driver/codemirror/tableFormatter/markdownTableUtility.ts: -------------------------------------------------------------------------------- 1 | // implemented by takumisoft68 at https://github.com/takumisoft68/vscode-markdown-table/blob/master/src/markdownTableUtility.ts 2 | 3 | /** 4 | * 1行分の文字列を列データ配列に分解する 5 | * 指定した列数に満たない行は 埋め文字 で埋める 6 | * @param linestr 1行分の文字列 7 | * @param columnNum 列数 8 | * @param fillstr 埋め文字 9 | */ 10 | export function splitline(linestr: string, columnNum: number, fillstr: string = '') { 11 | // 先頭と末尾の|を削除 12 | linestr = linestr.trim(); 13 | if (linestr.startsWith('|')) { 14 | linestr = linestr.slice(1); 15 | } 16 | if (linestr.endsWith('|')) { 17 | linestr = linestr.slice(0, -1); 18 | } 19 | 20 | // |で分割 21 | let linedatas: string[] = []; 22 | let startindex = 0; 23 | let endindex = 0; 24 | let isEscaping = false; 25 | let isInInlineCode = false; 26 | for (let i = 0; i < linestr.length; ++i) { 27 | if (isEscaping) { 28 | // エスケープ文字の次の文字は|かどうか判定しない 29 | isEscaping = false; 30 | endindex++; 31 | continue; 32 | } 33 | 34 | const chara = linestr.charAt(i); 35 | if (chara === '\`') { 36 | // `の間はインラインコード 37 | isInInlineCode = !isInInlineCode; 38 | endindex++; 39 | continue; 40 | } 41 | if (isInInlineCode) { 42 | // インラインコード中は|かどうか判定しない 43 | endindex++; 44 | continue; 45 | } 46 | 47 | if (chara === '\\') { 48 | // \はエスケープ文字 49 | isEscaping = true; 50 | endindex++; 51 | continue; 52 | } 53 | 54 | if (chara !== '|') { 55 | // | 以外だったら継続 56 | endindex++; 57 | continue; 58 | } 59 | 60 | // | だったら分割 61 | let cellstr = linestr.slice(startindex, endindex); 62 | linedatas.push(cellstr); 63 | startindex = i + 1; 64 | endindex = i + 1; 65 | } 66 | linedatas.push(linestr.slice(startindex)); 67 | 68 | // データ数分を''で埋めておく 69 | let datas: string[] = new Array(columnNum).fill(fillstr); 70 | // 行文字列から取得したデータに置き換える 71 | for (let i = 0; i < linedatas.length; i++) { 72 | datas[i] = linedatas[i]; 73 | } 74 | return datas; 75 | }; 76 | 77 | 78 | 79 | // 半角文字は1文字、全角文字は2文字として文字数をカウントする 80 | export function getLen(str: string): number { 81 | let length = 0; 82 | for (let i = 0; i < str.length; i++) { 83 | let chp = str.codePointAt(i); 84 | if (chp === undefined) { 85 | continue; 86 | } 87 | let chr = chp as number; 88 | if (doesUse0Space(chr)) { 89 | length += 0; 90 | } 91 | else if (doesUse3Spaces(chr)) { 92 | length += 3; 93 | } 94 | else if (doesUse2Spaces(chr)) { 95 | // 全角文字の場合は2を加算 96 | length += 2; 97 | } 98 | else { 99 | //それ以外の文字の場合は1を加算 100 | length += 1; 101 | } 102 | 103 | let chc = str.charCodeAt(i); 104 | if (chc >= 0xD800 && chc <= 0xDBFF) { 105 | // サロゲートペアの時は1文字読み飛ばす 106 | i++; 107 | } 108 | 109 | // if( (chr >= 0x00 && chr <= 0x80) || 110 | // (chr >= 0xa0 && chr <= 0xff) || 111 | // (chr === 0xf8f0) || 112 | // (chr >= 0xff61 && chr <= 0xff9f) || 113 | // (chr >= 0xf8f1 && chr <= 0xf8f3)){ 114 | // //半角文字の場合は1を加算 115 | // length += 1; 116 | // }else{ 117 | // //それ以外の文字の場合は2を加算 118 | // length += 2; 119 | // } 120 | } 121 | //結果を返す 122 | return length; 123 | }; 124 | 125 | function doesUse0Space(charCode: number): boolean { 126 | if ((charCode === 0x02DE) || 127 | (charCode >= 0x0300 && charCode <= 0x036F) || 128 | (charCode >= 0x0483 && charCode <= 0x0487) || 129 | (charCode >= 0x0590 && charCode <= 0x05CF)) { 130 | return true; 131 | } 132 | return false; 133 | } 134 | 135 | function doesUse2Spaces(charCode: number): boolean { 136 | if ((charCode >= 0x2480 && charCode <= 0x24FF) || 137 | (charCode >= 0x2600 && charCode <= 0x27FF) || 138 | (charCode >= 0x2900 && charCode <= 0x2CFF) || 139 | (charCode >= 0x2E00 && charCode <= 0xFF60) || 140 | (charCode >= 0xFFA0)) { 141 | return true; 142 | } 143 | return false; 144 | } 145 | 146 | function doesUse3Spaces(charCode: number): boolean { 147 | if (charCode >= 0x1F300 && charCode <= 0x1FBFF) { 148 | return true; 149 | } 150 | return false; 151 | } 152 | -------------------------------------------------------------------------------- /src/driver/codemirror/tableFormatter/tableFormatterBridge.ts: -------------------------------------------------------------------------------- 1 | import {alignColumns, deleteColume, insertColumn, insertRow} from "./tableCommands"; 2 | 3 | export default class TableFormatterBridge { 4 | constructor(private readonly editor) {} 5 | private readonly doc = this.editor.getDoc(); 6 | insertColumnLeft() { 7 | insertColumn(this.editor, true); 8 | } 9 | 10 | insertColumnRight() { 11 | insertColumn(this.editor, false); 12 | } 13 | 14 | insertRowAbove() { 15 | insertRow(this.editor, true); 16 | } 17 | 18 | insertRowBelow() { 19 | insertRow(this.editor, false); 20 | } 21 | 22 | deleteColumn() { 23 | deleteColume(this.editor); 24 | } 25 | 26 | alignColumnsCommand(options) { 27 | alignColumns(this.editor, options); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/driver/codemirror/taskAndHeaderRenderer/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import { taskAndHeaderRender as v5TaskAndHeaderRenderer } from './v5'; 3 | import v6TaskAndHeaderRenderer from './v6'; 4 | 5 | const taskAndHeaderRenderer = (cm: any) => { 6 | if (cm.cm6) { 7 | cm.addExtension(v6TaskAndHeaderRenderer()); 8 | } else { 9 | v5TaskAndHeaderRenderer(cm); 10 | } 11 | }; 12 | 13 | export default taskAndHeaderRenderer; -------------------------------------------------------------------------------- /src/driver/codemirror/taskAndHeaderRenderer/taskRender.css: -------------------------------------------------------------------------------- 1 | /* For CodeMirror 6 */ 2 | input.cm-enhancement-checkbox { 3 | cursor: pointer; 4 | } 5 | 6 | /* from zettlr */ 7 | .CodeMirror-sizer input[type="checkbox"], 8 | input.cm-enhancement-checkbox { 9 | vertical-align: middle; 10 | margin: 4px 0.6em 4px 0px; 11 | width: 1.4em; 12 | height: 1.4em; 13 | appearance: none; 14 | border: 1px solid #acacac; 15 | border-radius: 1.4em; 16 | } 17 | 18 | /* from obsidian */ 19 | .CodeMirror-sizer input[type="checkbox"]:checked, 20 | input.cm-enhancement-checkbox:checked { 21 | background-color: dodgerblue; 22 | border: 1px solid transparent; 23 | background-position: 44% 55%; 24 | background-size: 70%; 25 | background-repeat: no-repeat; 26 | background-image: url('data:image/svg+xml; utf8, '); 27 | } 28 | 29 | .heading-tag { 30 | display: inline-block; 31 | cursor: pointer; 32 | border-radius: 3px; 33 | padding: 0 2px; 34 | transition: 0.2s all ease; 35 | color: var(--joplin-color); 36 | margin-left: -1.2em; 37 | } 38 | 39 | .heading-tag:hover { 40 | color: #787881; 41 | background-color: #dcdcdc; 42 | } 43 | 44 | div.application-menu { 45 | background-color: var(--joplin-background-color); 46 | border: 0.5px solid rgb(180, 180, 180); 47 | border-radius: 6px; 48 | max-width: 110px; 49 | position: fixed; 50 | overflow-y: auto; 51 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 52 | font-size: 14px; 53 | box-shadow: 5px 5px 15px 0px rgba(0, 0, 0, .3); 54 | min-width: 50px; 55 | } 56 | 57 | div.application-menu div.menu-item { 58 | height: 20px; 59 | line-height: 20px; 60 | overflow: hidden; 61 | margin: 5px; 62 | padding: 0 5px; 63 | display: grid; 64 | grid-template-columns: 20px auto 50px; 65 | grid-template-rows: 100%; 66 | grid-template-areas: "status label after"; 67 | } 68 | 69 | div.application-menu div.menu-item:not(.separator):not(.disabled):hover { 70 | background-color: var(--joplin-selected-color); 71 | border-radius: 4px; 72 | } 73 | 74 | div.application-menu div.menu-item div.status { 75 | grid-area: status; 76 | text-align: center; 77 | } 78 | 79 | div.application-menu div.menu-item div.after-element { 80 | grid-area: after; 81 | text-align: right; 82 | } 83 | 84 | div.application-menu div.menu-item div.label { 85 | grid-area: label; 86 | text-align: left; 87 | } 88 | 89 | div.application-menu div.menu-item.separator { 90 | height: 1px; 91 | border-bottom: 1px solid var(--joplin-divider-color); 92 | } 93 | 94 | .cm-enhancement-finished-task { 95 | text-decoration: line-through; 96 | opacity: 0.85; 97 | } 98 | 99 | .table-helper { 100 | border: 1px solid #e6e6e6; 101 | } 102 | 103 | .table-helper tr:nth-child(odd) { 104 | background-color: #f4f5f5; 105 | } 106 | 107 | table.table-helper td { 108 | padding: 6px; 109 | border: 1px solid #E6E6E6; 110 | } 111 | 112 | table.table-helper tr p { 113 | margin: 0; 114 | } 115 | -------------------------------------------------------------------------------- /src/driver/codemirror/taskAndHeaderRenderer/v5/index.ts: -------------------------------------------------------------------------------- 1 | import {markdownRenderTasks} from "./taskRender"; 2 | import {markdownRenderHTags} from "./render-h-tags"; 3 | import {markdownRenderTables} from "./render-tables"; 4 | import {debounce} from "ts-debounce"; 5 | 6 | 7 | export function taskAndHeaderRender(cm) { 8 | // While taskHandle is undefined, there's no task scheduled. Else, there is. 9 | let taskHandle: number|undefined 10 | 11 | const callback = function (cm: CodeMirror.Editor): void { 12 | if (taskHandle !== undefined) { 13 | return // Already a task registered 14 | } 15 | 16 | debounce(() => { 17 | renderElements(cm, true); 18 | }, 500)(); 19 | } 20 | 21 | cm.on('change', async function (cm, changeObjs) { 22 | if (changeObjs.origin === 'setValue') { 23 | renderElements(cm, false); 24 | } 25 | }); 26 | 27 | cm.on('cursorActivity', callback) 28 | cm.on('viewportChange', callback) // renderElements) 29 | cm.on('optionChange', callback) 30 | 31 | callback(cm); 32 | } 33 | 34 | function renderElements (cm: CodeMirror.Editor, viewPort: boolean): void { 35 | if (cm.state.enhancement) { 36 | if (cm.state.enhancement.settings.taskCmRender) { 37 | markdownRenderTasks(cm, viewPort); 38 | } 39 | 40 | if (cm.state.enhancement.settings.headerHashRender) { 41 | markdownRenderHTags(cm, viewPort); 42 | } 43 | 44 | if (cm.state.enhancement.settings.tableCmRender) { 45 | markdownRenderTables(cm); 46 | } 47 | } else { 48 | markdownRenderTasks(cm, viewPort); 49 | markdownRenderHTags(cm, viewPort); 50 | markdownRenderTables(cm); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/driver/codemirror/taskAndHeaderRenderer/v5/render-h-tags.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @ignore 3 | * BEGIN HEADER 4 | * 5 | * Contains: Heading tags Plugin 6 | * CVM-Role: CodeMirror Plugin 7 | * Maintainer: Hendrik Erz 8 | * License: GNU GPL v3 9 | * 10 | * Description: This plugin renders Bear-style heading indicators. 11 | * 12 | * END HEADER 13 | */ 14 | 15 | import CodeMirror, { commands } from 'codemirror' 16 | import { getHeadRE } from '../../../../utils/regular-expressions' 17 | import showPopupMenu from './window-register/application-menu-helper' 18 | import canRenderElement from '../../../../utils/can-render-element' 19 | 20 | const headRE = getHeadRE() 21 | 22 | export function markdownRenderHTags (cm: CodeMirror.Editor, viewPort: boolean): void { 23 | let match 24 | 25 | let fromLine = 0; 26 | let toLine = Math.min(60, cm.lineCount()); 27 | 28 | if (viewPort) { 29 | // We'll only renderer the viewport 30 | const viewport = cm.getViewport() 31 | fromLine = viewport.from; 32 | toLine = viewport.to; 33 | } 34 | for (let i = fromLine; i < toLine; i++) { 35 | if (cm.getModeAt({ 'line': i, 'ch': 0 }).name !== 'markdown') continue 36 | // Always reset lastIndex property, because test()-ing on regular 37 | // expressions advances it. 38 | headRE.lastIndex = 0 39 | 40 | // First get the line and test if the contents contain a link 41 | let line = cm.getLine(i) 42 | if ((match = headRE.exec(line)) == null) { 43 | continue 44 | } 45 | 46 | const headingLevel = match[1].length 47 | 48 | const curFrom = { line: i, ch: 0 } 49 | const curTo = { line: i, ch: headingLevel } 50 | 51 | if (!canRenderElement(cm, curFrom, curTo, !viewPort)) { 52 | continue 53 | } 54 | 55 | const hTagWrapper = document.createElement('div') 56 | hTagWrapper.className = 'heading-tag' 57 | 58 | const hTag = document.createElement('span') 59 | hTag.textContent = `h${headingLevel}` 60 | hTagWrapper.appendChild(hTag) 61 | 62 | const textMarker = cm.markText( 63 | curFrom, curTo, 64 | { 65 | 'clearOnEnter': true, 66 | 'replacedWith': hTagWrapper, 67 | 'inclusiveLeft': false, 68 | 'inclusiveRight': false 69 | } 70 | ) 71 | 72 | // Display a small menu to change the heading level on click 73 | hTagWrapper.onclick = (e) => { 74 | // Prevent bubbling because otherwise the context menu will be hidden 75 | // again immediately due to the event bubbling upwards to the window. 76 | e.stopPropagation() 77 | 78 | const items = [ 79 | { 80 | id: '1', 81 | label: '#', 82 | type: 'checkbox', 83 | enabled: !cm.isReadOnly(), 84 | checked: headingLevel === 1 85 | }, 86 | { 87 | id: '2', 88 | label: '##', 89 | type: 'checkbox', 90 | enabled: !cm.isReadOnly(), 91 | checked: headingLevel === 2 92 | }, 93 | { 94 | id: '3', 95 | label: '###', 96 | type: 'checkbox', 97 | enabled: !cm.isReadOnly(), 98 | checked: headingLevel === 3 99 | }, 100 | { 101 | id: '4', 102 | label: '####', 103 | type: 'checkbox', 104 | enabled: !cm.isReadOnly(), 105 | checked: headingLevel === 4 106 | }, 107 | { 108 | id: '5', 109 | label: '#####', 110 | type: 'checkbox', 111 | enabled: !cm.isReadOnly(), 112 | checked: headingLevel === 5 113 | }, 114 | { 115 | id: '6', 116 | label: '######', 117 | type: 'checkbox', 118 | enabled: !cm.isReadOnly(), 119 | checked: headingLevel === 6 120 | } 121 | ] 122 | 123 | const point = { x: e.clientX, y: e.clientY } 124 | showPopupMenu(point, items as any, (id) => { 125 | const newLevel = parseInt(id, 10) 126 | 127 | const markerRange = textMarker.find() 128 | if (markerRange === undefined) { 129 | return 130 | } 131 | 132 | // The heading might have changed position in the meantime 133 | const { from, to } = markerRange 134 | cm.replaceRange('#'.repeat(newLevel), from, to) 135 | textMarker.clear() 136 | // Programmatically trigger a cursor movement ... 137 | cm.setCursor({ line: from.line, ch: newLevel + 1 }) 138 | // ... and re-focus the editor 139 | cm.focus() 140 | }) 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/driver/codemirror/taskAndHeaderRenderer/v5/renderer/context.ts: -------------------------------------------------------------------------------- 1 | export interface CheckboxRadioItem { 2 | id: string 3 | label: string 4 | accelerator?: string 5 | type: 'checkbox'|'radio' 6 | enabled: boolean 7 | checked: boolean 8 | } 9 | 10 | export interface SeparatorItem { 11 | type: 'separator' 12 | } 13 | 14 | export interface SubmenuItem { 15 | id: string 16 | label: string 17 | type: 'submenu' 18 | enabled: boolean 19 | submenu: Array 20 | } 21 | 22 | export interface NormalItem { 23 | id: string 24 | label: string 25 | accelerator?: string 26 | type: 'normal' 27 | enabled: boolean 28 | } 29 | 30 | export type AnyMenuItem = CheckboxRadioItem | SeparatorItem | SubmenuItem | NormalItem 31 | 32 | // Any menu item w/o separators 33 | export type InteractiveMenuItem = CheckboxRadioItem | SubmenuItem | NormalItem 34 | 35 | export interface Rect { 36 | top: number 37 | left: number 38 | width: number 39 | height: number 40 | } 41 | 42 | export interface Point { 43 | x: number 44 | y: number 45 | } 46 | -------------------------------------------------------------------------------- /src/driver/codemirror/taskAndHeaderRenderer/v5/table-editor/build-grid.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @ignore 3 | * BEGIN HEADER 4 | * 5 | * Contains: TableHelper utility function 6 | * CVM-Role: Utility 7 | * Maintainer: Hendrik Erz 8 | * License: GNU GPL v3 9 | * 10 | * Description: Transforms an AST to a grid table. 11 | * Cf. https://pandoc.org/MANUAL.html#tables 12 | * 13 | * END HEADER 14 | */ 15 | 16 | import calculateColSizes from './calculate-col-sizes' 17 | import { ColAlignment } from './types' 18 | 19 | export default function buildGridTable (ast: string[][], colAlignment: ColAlignment[]): string { 20 | const colSizes = calculateColSizes(ast) 21 | let separatorRow = colSizes.map(elem => '-'.repeat(elem + 2)).join('+') 22 | separatorRow = '+' + separatorRow + '+\n' 23 | 24 | // First add a beginning row 25 | let markdownTable = separatorRow 26 | 27 | for (let i = 0; i < ast.length; i++) { 28 | for (let j = 0; j < ast[i].length; j++) { 29 | if (j === 0) markdownTable += '|' 30 | // Pad the text contents to fill up to the maximum chars 31 | switch (colAlignment[j]) { 32 | case 'left': 33 | case 'center': 34 | markdownTable += ` ${ast[i][j].padEnd(colSizes[j], ' ')} |` 35 | break 36 | case 'right': 37 | markdownTable += ` ${ast[i][j].padStart(colSizes[j], ' ')} |` 38 | break 39 | } 40 | } 41 | 42 | markdownTable += '\n' 43 | 44 | // First row is the header, so add a secondary row. 45 | if (i === 0) { 46 | markdownTable += '+' 47 | for (let k = 0; k < colSizes.length; k++) { 48 | // Respect the spaces left and right and account for alignment 49 | switch (colAlignment[k]) { 50 | case 'left': 51 | markdownTable += '='.repeat(colSizes[k] + 2) + '+' 52 | break 53 | case 'center': 54 | markdownTable += ':' + '='.repeat(colSizes[k]) + ':+' 55 | break 56 | case 'right': 57 | markdownTable += '='.repeat(colSizes[k] + 1) + ':+' 58 | break 59 | } 60 | } 61 | markdownTable += '\n' 62 | } else { 63 | // Add separators after each line 64 | markdownTable += separatorRow 65 | } 66 | } 67 | 68 | return markdownTable 69 | } 70 | -------------------------------------------------------------------------------- /src/driver/codemirror/taskAndHeaderRenderer/v5/table-editor/build-pipe.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @ignore 3 | * BEGIN HEADER 4 | * 5 | * Contains: TableHelper utility function 6 | * CVM-Role: Utility 7 | * Maintainer: Hendrik Erz 8 | * License: GNU GPL v3 9 | * 10 | * Description: This parser transforms pipe tables as specified 11 | * in the Pandoc manual into an AST and returns both 12 | * that and the column alignments. 13 | * Cf. https://pandoc.org/MANUAL.html#tables 14 | * 15 | * END HEADER 16 | */ 17 | 18 | import calculateColSizes from './calculate-col-sizes' 19 | import { ColAlignment } from './types' 20 | 21 | export default function buildPipeTable (ast: string[][], colAlignment: ColAlignment[]): string { 22 | const colSizes = calculateColSizes(ast) 23 | let markdownTable = '' 24 | // Now build from AST 25 | for (let i = 0; i < ast.length; i++) { 26 | for (let j = 0; j < ast[i].length; j++) { 27 | if (j === 0) markdownTable += '|' 28 | // Pad the text contents to fill up to the maximum chars 29 | switch (colAlignment[j]) { 30 | case 'left': 31 | case 'center': 32 | markdownTable += ` ${ast[i][j].padEnd(colSizes[j], ' ')} |` 33 | break 34 | case 'right': 35 | markdownTable += ` ${ast[i][j].padStart(colSizes[j], ' ')} |` 36 | break 37 | } 38 | } 39 | 40 | markdownTable += '\n' 41 | 42 | // First row is the header, so add a secondary row. 43 | if (i === 0) { 44 | markdownTable += '|' 45 | for (let k = 0; k < colSizes.length; k++) { 46 | // Respect the spaces left and right and account for alignment 47 | switch (colAlignment[k]) { 48 | case 'left': 49 | markdownTable += '-'.repeat(colSizes[k] + 2) + '|' 50 | break 51 | case 'center': 52 | markdownTable += ':' + '-'.repeat(colSizes[k]) + ':|' 53 | break 54 | case 'right': 55 | markdownTable += '-'.repeat(colSizes[k] + 1) + ':|' 56 | break 57 | } 58 | } 59 | markdownTable += '\n' 60 | } 61 | } 62 | 63 | return markdownTable 64 | } 65 | -------------------------------------------------------------------------------- /src/driver/codemirror/taskAndHeaderRenderer/v5/table-editor/build-simple.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @ignore 3 | * BEGIN HEADER 4 | * 5 | * Contains: TableHelper utility function 6 | * CVM-Role: Utility 7 | * Maintainer: Hendrik Erz 8 | * License: GNU GPL v3 9 | * 10 | * Description: This parser transforms an AST into a simple Table. 11 | * Cf. https://pandoc.org/MANUAL.html#tables 12 | * 13 | * END HEADER 14 | */ 15 | 16 | import calculateColSizes from './calculate-col-sizes' 17 | import { ColAlignment } from './types' 18 | 19 | export default function buildSimpleTable (ast: string[][], colAlignment: ColAlignment[]): string { 20 | const colSizes = calculateColSizes(ast) 21 | let markdownTable = '' 22 | // Now build from AST 23 | for (let i = 0; i < ast.length; i++) { 24 | for (let j = 0; j < ast[i].length; j++) { 25 | // Pad the text contents to fill up to the maximum chars 26 | let toFill = colSizes[j] - ast[i][j].length 27 | let start = Math.round(toFill / 2) 28 | let end = toFill - start 29 | start += 1 30 | switch (colAlignment[j]) { 31 | case 'left': 32 | markdownTable += ast[i][j].padEnd(colSizes[j] + 2, ' ') 33 | break 34 | case 'center': 35 | markdownTable += ' '.repeat(start) + ast[i][j] + ' '.repeat(end + 1) 36 | break 37 | case 'right': 38 | markdownTable += ast[i][j].padStart(colSizes[j] + 2, ' ') 39 | break 40 | } 41 | 42 | // Pad with one space 43 | if (j < ast[i].length - 1) markdownTable += ' ' 44 | } 45 | 46 | markdownTable += '\n' 47 | 48 | // First row is the header, so add a secondary row. 49 | if (i === 0) { 50 | let r = colSizes.map(elem => '-'.repeat(elem + 2)) 51 | markdownTable += r.join(' ') + '\n' 52 | } 53 | } 54 | 55 | return markdownTable 56 | } 57 | -------------------------------------------------------------------------------- /src/driver/codemirror/taskAndHeaderRenderer/v5/table-editor/calculate-col-sizes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @ignore 3 | * BEGIN HEADER 4 | * 5 | * Contains: Utility function 6 | * CVM-Role: Utility 7 | * Maintainer: Hendrik Erz 8 | * License: GNU GPL v3 9 | * 10 | * Description: This function takes a table AST and calculates, for each 11 | * column, the maximum amount of characters. 12 | * 13 | * END HEADER 14 | */ 15 | 16 | /** 17 | * Calculates the maximum size (characters) of each column of a table AST 18 | * 19 | * @param {string[][]} ast The AST 20 | * 21 | * @return {number[]} The maximum sizes for all columns 22 | */ 23 | export default function calculateColSizes (ast: string[][]): number[] { 24 | const sizes = [] 25 | for (let col = 0; col < ast[0].length; col++) { 26 | let colSize = 0 27 | for (let row = 0; row < ast.length; row++) { 28 | if (ast[row][col].length > colSize) { 29 | colSize = ast[row][col].length 30 | } 31 | } 32 | sizes.push(colSize) 33 | } 34 | return sizes 35 | } 36 | -------------------------------------------------------------------------------- /src/driver/codemirror/taskAndHeaderRenderer/v5/table-editor/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @ignore 3 | * BEGIN HEADER 4 | * 5 | * Contains: TableEditor 6 | * CVM-Role: Model 7 | * Maintainer: Hendrik Erz 8 | * License: GNU GPL v3 9 | * 10 | * Description: This class models Markdown tables using an internal AST and 11 | * enables easy WYSIWYG-style editing of tables. 12 | * 13 | * END HEADER 14 | */ 15 | 16 | import TableEditor from './table-editor' 17 | import parsePipeTable from './parse-pipe' 18 | import parseSimpleTable from './parse-simple' 19 | import parseGridTable from './parse-grid' 20 | import { TableEditorOptions } from './types' 21 | 22 | export default function fromMarkdown (markdownTable: string, potentialType: 'pipe'|'simple'|'grid' = 'pipe', hooks: TableEditorOptions = {}): TableEditor { 23 | let parsed 24 | switch (potentialType) { 25 | case 'simple': 26 | parsed = parseSimpleTable(markdownTable) 27 | break 28 | case 'grid': 29 | parsed = parseGridTable(markdownTable) 30 | break 31 | default: 32 | parsed = parsePipeTable(markdownTable) 33 | break 34 | } 35 | 36 | // Now parse the whole thing into the table editor. 37 | const editor = new TableEditor(parsed.ast, parsed.colAlignments, potentialType, hooks) 38 | 39 | return editor 40 | } 41 | -------------------------------------------------------------------------------- /src/driver/codemirror/taskAndHeaderRenderer/v5/table-editor/parse-grid.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @ignore 3 | * BEGIN HEADER 4 | * 5 | * Contains: TableHelper utility function 6 | * CVM-Role: Utility 7 | * Maintainer: Hendrik Erz 8 | * License: GNU GPL v3 9 | * 10 | * Description: This parser transforms grid tables as specified 11 | * in the Pandoc manual into an AST and returns both 12 | * that and the column alignments. 13 | * Cf. https://pandoc.org/MANUAL.html#tables 14 | * 15 | * END HEADER 16 | */ 17 | 18 | import { ParsedTable, ColAlignment } from './types' 19 | 20 | /** 21 | * Parses a grid table 22 | * @param {String|Array} A markdown table, either as line array or string 23 | * @returns {Object} An object with properties ast and colAlignments 24 | */ 25 | export default function parseGridTable (markdownTable: string|string[]): ParsedTable { 26 | // First remove whitespace from both sides of the table, e.g. in case 27 | // a trailing newline is present 28 | if (typeof markdownTable === 'string') markdownTable = markdownTable.trim() 29 | if (!Array.isArray(markdownTable)) markdownTable = markdownTable.split('\n') 30 | 31 | if (markdownTable.length === 0 || (markdownTable.length === 1 && markdownTable[0].trim() === '')) { 32 | throw new Error('MarkdownTable was empty!') 33 | } 34 | 35 | const ast: string[][] = [] 36 | const colAlignments: ColAlignment[] = [] // One-dimensional column alignments 37 | let numColumns = 0 // If there is an uneven number of columns, throw an error. 38 | 39 | // The speciality of grid tables is that they can contain the alignment info 40 | // either in the first row, or in the second separator row. So we have to find 41 | // the alignment prior to iterating over the rows. 42 | let headerRow = 0 43 | for (let i = 0; i < markdownTable.length; i++) { 44 | if (/^[:+= ]+$/.test(markdownTable[i])) { // "Real" headers have = instead of - 45 | headerRow = i 46 | break 47 | } 48 | } 49 | 50 | let headerContent: string|string[] = markdownTable[headerRow] 51 | 52 | headerContent = headerContent.split('+').slice(1, -1) 53 | for (let i = 0; i < headerContent.length; i++) { 54 | const col = headerContent[i].trim() 55 | if (col.startsWith(':') && col.endsWith(':')) { 56 | colAlignments[i] = 'center' 57 | } else if (col.endsWith(':')) { 58 | colAlignments[i] = 'right' 59 | } else { 60 | colAlignments[i] = 'left' 61 | } 62 | } 63 | 64 | // Now iterate over all table rows 65 | for (let i = 0; i < markdownTable.length; i++) { 66 | // There should not be empty lines in the table. 67 | // If so, this indicates an error in the render tables plugin! 68 | if (markdownTable[i].trim() === '') { 69 | throw new Error(`Line ${i} in the table was empty!`) 70 | } 71 | 72 | const row: string|string[] = markdownTable[i].trim() // Clean up whitespace 73 | if (row.startsWith('+')) { 74 | continue // Skip separation lines 75 | } 76 | 77 | if (!row[0].startsWith('|')) { 78 | throw new Error(`Malformed Markdown Table! Row ${i} did not start with + or |!`) 79 | } 80 | 81 | // Split to columns 82 | const cols = row.split('|').map(elem => elem.trim()).slice(1, -1) 83 | 84 | // First row determines the amount of columns expected 85 | if (numColumns === 0) { 86 | numColumns = cols.length // Basically: First row determines column count ... 87 | } else if (numColumns !== cols.length) { // ... subsequent rows check against this. 88 | throw new Error(`Malformed Markdown Table! Found ${row.length} columns on line ${i} (should be ${numColumns}).`) 89 | } 90 | 91 | // Add the whole row to the AST 92 | ast.push(cols) 93 | } 94 | 95 | if (ast.length === 0 || ast[0].length === 0) { 96 | // This AST does not look as it's supposed to -> abort 97 | throw new Error('Malformed Markdown Table! The parsed AST was empty.') 98 | } 99 | 100 | // Return the AST 101 | return { ast, colAlignments } 102 | } 103 | -------------------------------------------------------------------------------- /src/driver/codemirror/taskAndHeaderRenderer/v5/table-editor/parse-pipe.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @ignore 3 | * BEGIN HEADER 4 | * 5 | * Contains: TableHelper utility function 6 | * CVM-Role: Utility 7 | * Maintainer: Hendrik Erz 8 | * License: GNU GPL v3 9 | * 10 | * Description: This parser transforms pipe tables as specified 11 | * in the Pandoc manual into an AST and returns both 12 | * that and the column alignments. 13 | * Cf. https://pandoc.org/MANUAL.html#tables 14 | * 15 | * END HEADER 16 | */ 17 | 18 | import { ParsedTable, ColAlignment } from './types' 19 | 20 | /** 21 | * Parses a pipe table 22 | * @param {String|Array} A markdown table, either as line array or string 23 | * @returns {Object} An object with properties ast and colAlignments 24 | */ 25 | export default function parsePipeTable (markdownTable: string|string[]): ParsedTable { 26 | // First remove whitespace from both sides of the table, e.g. in case 27 | // a trailing newline is present 28 | if (typeof markdownTable === 'string') markdownTable = markdownTable.trim() 29 | if (!Array.isArray(markdownTable)) markdownTable = markdownTable.split('\n') 30 | 31 | if (markdownTable.length === 0) throw new Error('MarkdownTable was empty!') 32 | if (markdownTable.length === 1 && markdownTable[0].trim() === '') throw new Error('MarkdownTable was empty!') 33 | 34 | let ast = [] // Two-dimensional array 35 | let colAlignments: ColAlignment[] = [] // One-dimensional column alignments 36 | let numColumns = -1 // If there is an uneven number of columns, throw an error. 37 | 38 | // Now iterate over all table rows 39 | for (let i = 0; i < markdownTable.length; i++) { 40 | // There should not be empty lines in the table. 41 | // If so, this indicates an error in the render tables plugin! 42 | if (markdownTable[i].trim() === '') throw new Error(`Line ${i} in the table was empty!`) 43 | let row: string[]|string = markdownTable[i].trim() // Clean up whitespace 44 | if (/^[- :+|]+$/.test(row)) { 45 | // We have an alternative pipetable, separated with + instead of |, 46 | // so we have to replace all instances of + with | 47 | row = row.replace(/\+/g, '|') 48 | } 49 | 50 | // Split to columns 51 | row = row.split('|') 52 | 53 | // Now, expect that the first and last "column" are empty and remove them. 54 | // If they are not empty, we probably have a pipe table without surrounding 55 | // pipes. 56 | if (row[0].trim() === '') row.shift() 57 | if (row[row.length - 1].trim() === '') row.pop() 58 | 59 | // First row determines the amount of columns expected 60 | if (numColumns === -1) { 61 | numColumns = row.length // First row determines column count ... 62 | } 63 | 64 | if (numColumns !== row.length) { // ... subsequent rows check that length. 65 | throw new Error(`Malformed Markdown Table! Found ${row.length} columns on line ${i} (should be ${numColumns}).`) 66 | } 67 | 68 | // First test if we've got a header row. A header row is defined of 69 | // consisting of columns only containing dashes, colons and spaces. The 70 | // first column to break this rule means we don't have a valid header. 71 | let isHeader = true 72 | for (let col of row) { 73 | if (!/^[-: ]+$/.test(col) || col.trim() === '') { 74 | // Note we have to check for completely blank lines 75 | isHeader = false 76 | break 77 | } 78 | } 79 | 80 | // Parse the header 81 | if (isHeader) { 82 | for (let j = 0; j < row.length; j++) { 83 | let col = row[j].trim() 84 | if (col[0] === ':' && col[col.length - 1] === ':') { 85 | colAlignments[j] = 'center' 86 | } else if (col[col.length - 1] === ':') { 87 | colAlignments[j] = 'right' 88 | } else { 89 | colAlignments[j] = 'left' 90 | } 91 | } 92 | continue // We're done here -- don't add it to the AST 93 | } 94 | 95 | // We have a normal table row, so parse all columns 96 | let cols = [] 97 | for (let j = 0; j < row.length; j++) { 98 | cols.push(row[j].trim()) // Trim whitespaces 99 | } 100 | 101 | // Add the whole row to the AST 102 | ast.push(cols) 103 | } 104 | 105 | // If we have reached this stage, but no header row was found, 106 | // preset all column alignments with left 107 | if (colAlignments.length === 0) { 108 | for (let i = 0; i < numColumns; i++) { 109 | colAlignments.push('left') 110 | } 111 | } 112 | 113 | if (ast.length === 0 || ast[0].length === 0) { 114 | // This AST does not look as it's supposed to -> abort 115 | throw new Error('Malformed Markdown Table! The parsed AST was empty.') 116 | } 117 | 118 | // Return the AST 119 | return { 120 | 'ast': ast, 121 | 'colAlignments': colAlignments 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/driver/codemirror/taskAndHeaderRenderer/v5/table-editor/parse-simple.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @ignore 3 | * BEGIN HEADER 4 | * 5 | * Contains: TableHelper utility function 6 | * CVM-Role: Utility 7 | * Maintainer: Hendrik Erz 8 | * License: GNU GPL v3 9 | * 10 | * Description: This parser transforms simple tables as specified 11 | * in the Pandoc manual into an AST and returns both 12 | * that and the column alignments. 13 | * Cf. https://pandoc.org/MANUAL.html#tables 14 | * 15 | * END HEADER 16 | */ 17 | 18 | import { ParsedTable, ColAlignment } from './types' 19 | 20 | /** 21 | * Parses a "simple" table ("simple", because it's not that simple) 22 | * @param {String|Array} A markdown table, either as line array or string 23 | * @returns {Object} An object with properties ast and colAlignments 24 | */ 25 | export default function parseSimpleTable (markdownTable: string|string[]): ParsedTable { 26 | // Let's begin. First we need a line array. Make sure to strip potential 27 | // newlines from beginning and end, if applicable. Also perform some 28 | // initial sanity checks on the table. 29 | if (!Array.isArray(markdownTable)) { 30 | markdownTable = markdownTable.split('\n') 31 | } 32 | 33 | if (markdownTable.length === 0 || (markdownTable.length === 1 && markdownTable[0].trim() === '')) { 34 | throw new Error('MarkdownTable was empty!') 35 | } 36 | 37 | const ast: string[][] = [] 38 | const colAlignments: ColAlignment[] = [] 39 | const colSizes: number[] = [] // Yes, here we need the lengths of the columns 40 | const colPadding: number[] = [] // Also yes: We need to know the padding between cols. 41 | 42 | // First, we need to determine the headers and the column alignments. As the 43 | // columns can be separated by an arbitrary amount of spaces, we need to 44 | // gauge the length AND the indentation by looking either at the first line 45 | // OR at the second line, depending on which one consists solely of dashes 46 | // and spaces. 47 | let determineAlignWith = markdownTable[0] 48 | let determineLengthsWith = markdownTable[1] 49 | if (/^[- ]+$/.test(markdownTable[0])) { 50 | // We got a headless table, so we need to swap the lines. 51 | determineLengthsWith = markdownTable[0] 52 | determineAlignWith = markdownTable[1] 53 | 54 | // Bonus round: According to the manual (TM) a headless table needs to have 55 | // a solely dashed line at the bottom. 56 | if (!/^[- ]+$/.test(markdownTable[markdownTable.length - 1])) { 57 | throw new Error('Malformed Markdown Table: Did not find a finishing dashed line for the table.') 58 | } 59 | } 60 | 61 | // Additional sanity check: The table is not allowed to be padded. 62 | if (determineLengthsWith.startsWith(' ') || determineLengthsWith.endsWith(' ')) { 63 | throw new Error('The table is padded.') 64 | } 65 | 66 | // Now we first need the indent and column sizes 67 | // @ts-ignore 68 | for (const match of determineLengthsWith.matchAll(/-+| +/g)) { 69 | // This RegExp is able to extract single-character-substrings 70 | if (match[0].startsWith('-')) { 71 | colSizes.push(match[0].length) 72 | } else if (match[0].startsWith(' ')) { 73 | colPadding.push(match[0].length) 74 | } 75 | } 76 | 77 | // Now, determine the alignments. Do so by first splitting the respective 78 | // line according to the lengths and see where the whitespace is. 79 | let index = 0 80 | for (let i = 0; i < colSizes.length; i++) { 81 | colAlignments.push(determineAlignWith.substring(index, index + colSizes[i]) as any) 82 | index += colSizes[i] + colPadding[i] 83 | } 84 | 85 | // Transform the colAlignments containing real text into alignments 86 | for (let i = 0; i < colAlignments.length; i++) { 87 | let a = colAlignments[i] 88 | if (a.startsWith(' ') && a.endsWith(' ')) { 89 | colAlignments[i] = 'center' 90 | } else if (a.startsWith(' ')) { 91 | colAlignments[i] = 'right' 92 | } else { 93 | colAlignments[i] = 'left' 94 | } 95 | } 96 | 97 | // Now we should have column sizes and indentation sizes. 98 | // Next: Parse the full table according to these rules. 99 | for (const line of markdownTable) { 100 | if (/^[- ]+$/.test(line)) { 101 | continue // Heading row 102 | } 103 | 104 | const columns: string[] = [] 105 | let index = 0 106 | for (let i = 0; i < colSizes.length; i++) { 107 | columns.push(line.substring(index, index + colSizes[i]).trim()) 108 | index += colSizes[i] + colPadding[i] 109 | } 110 | ast.push(columns) 111 | } 112 | 113 | if (ast.length === 0 || ast[0].length === 0) { 114 | // This AST does not look as it's supposed to -> abort 115 | throw new Error('Malformed Markdown Table! The parsed AST was empty.') 116 | } 117 | 118 | // Return the AST 119 | return { ast, colAlignments } 120 | } 121 | -------------------------------------------------------------------------------- /src/driver/codemirror/taskAndHeaderRenderer/v5/table-editor/types.ts: -------------------------------------------------------------------------------- 1 | import TableEditor from './index' 2 | 3 | export type ColAlignment = 'center'|'left'|'right' 4 | 5 | export interface ParsedTable { 6 | ast: string[][] 7 | colAlignments: ColAlignment[] 8 | } 9 | 10 | export interface TableEditorOptions { 11 | /** 12 | * Describes the container for the Table element (either an Element or a querySelector) 13 | */ 14 | container?: HTMLElement|string 15 | /** 16 | * A callback that is fired whenever the TableEditor is unfocused 17 | * 18 | * @param {TableEditor} instance The TableEditor instance 19 | */ 20 | onBlur?: (instance: ReturnType) => void 21 | /** 22 | * A callback that is fired whenever the TableEditor's contents change 23 | * 24 | * @param {TableEditor} instance The TableEditor instance 25 | */ 26 | onChange?: (instance: ReturnType) => void 27 | /** 28 | * A callback that is fired whenever the user switches the cell of the table 29 | * 30 | * @param {TableEditor} instance The TableEditor instance 31 | */ 32 | onCellChange?: (instance: ReturnType) => void 33 | } 34 | -------------------------------------------------------------------------------- /src/driver/codemirror/taskAndHeaderRenderer/v5/taskRender.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @ignore 3 | * BEGIN HEADER 4 | * 5 | * Contains: Task rendering Plugin 6 | * CVM-Role: CodeMirror Plugin 7 | * Maintainer: Hendrik Erz 8 | * License: GNU GPL v3 9 | * 10 | * Description: This plugin renders task items and makes them checkable. 11 | * 12 | * END HEADER 13 | */ 14 | 15 | import CodeMirror from 'codemirror' 16 | import { getTaskRE } from '../../../../utils/regular-expressions' 17 | import canRenderElement from "../../../../utils/can-render-element"; 18 | 19 | const taskRE = getTaskRE() // Matches `- [ ]` and `- [x]` 20 | 21 | /** 22 | * Renders task list items 23 | * 24 | * @param {CodeMirror.Editor} cm The CodeMirror instance 25 | */ 26 | export function markdownRenderTasks(cm: CodeMirror.Editor, viewPort: boolean) { 27 | let match 28 | 29 | let fromLine = 0; 30 | let toLine = Math.min(60, cm.lineCount()); 31 | 32 | if (viewPort) { 33 | // We'll only renderer the viewport 34 | const viewport = cm.getViewport() 35 | fromLine = viewport.from; 36 | toLine = viewport.to; 37 | } 38 | for (let i = fromLine; i <= toLine; i++) { 39 | if (cm.getModeAt({ line: i, ch: 0 }).name !== 'markdown') continue 40 | // Always reset lastIndex property, because test()-ing on regular 41 | // expressions advances it. 42 | taskRE.lastIndex = 0 43 | 44 | // First get the line and test if the contents contain a link 45 | const line = cm.getLine(i) 46 | if ((match = taskRE.exec(line)) == null) { 47 | continue 48 | } 49 | 50 | const leadingSpaces = match[1].length ?? 0 51 | const curFrom = { line: i, ch: 0 + leadingSpaces } 52 | const curTo = { line: i, ch: 5 + leadingSpaces } 53 | 54 | if (!canRenderElement(cm, curFrom, curTo, !viewPort)) { 55 | continue 56 | } 57 | 58 | // Now we can renderer it finally. 59 | const checked = (match[3] === 'x') 60 | const listSign = match[2] // Save the sign +, -, or * for later 61 | 62 | const cbox = document.createElement('input') 63 | cbox.type = 'checkbox' 64 | if (checked) { 65 | cbox.checked = true 66 | } 67 | 68 | // If the CodeMirror instance is readOnly, disable the checkbox 69 | cbox.disabled = cm.isReadOnly() 70 | 71 | let textMarker = cm.markText( 72 | curFrom, curTo, 73 | { 74 | 'clearOnEnter': true, 75 | 'replacedWith': cbox, 76 | 'inclusiveLeft': false, 77 | 'inclusiveRight': false 78 | } 79 | ) 80 | 81 | // Clear the textmarker once it's hidden b/c we'd rather 82 | // re-renderer than having a wrong state associated with the marker 83 | textMarker.on('hide', () => { textMarker.clear() }) 84 | 85 | cbox.onclick = (e) => { 86 | const markerPosition = textMarker.find() 87 | if (cm.isReadOnly() || markerPosition === undefined) { 88 | return // Don't do anything 89 | } 90 | 91 | const { from, to } = markerPosition 92 | 93 | // Check or uncheck it (NOTE that cbox will already represent the NEW state) 94 | const newMark = (cbox.checked) ? 'x' : ' ' 95 | cm.replaceRange(`${listSign} [${newMark}]`, from, to) 96 | 97 | // ReplaceRange removes the marker, so we have to re-initiate it 98 | textMarker = cm.markText( 99 | from, to, 100 | { 101 | 'clearOnEnter': true, 102 | 'replacedWith': cbox, 103 | 'inclusiveLeft': false, 104 | 'inclusiveRight': false 105 | } 106 | ) 107 | } // END onclick 108 | 109 | // We need to listen to readOnly state changes to enable/disable checkboxes 110 | const updateHandler = (cm: CodeMirror.Editor, option: any): void => { 111 | if (!document.body.contains(cbox)) { 112 | // Remove the event listener again 113 | cm.off('optionChange', updateHandler) 114 | } 115 | 116 | cbox.disabled = cm.isReadOnly() 117 | } 118 | 119 | // Listen to option changes 120 | cm.on('optionChange', updateHandler) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/driver/codemirror/taskAndHeaderRenderer/v6/checklistPlugin.ts: -------------------------------------------------------------------------------- 1 | 2 | import { requireCodeMirrorLanguage, requireCodeMirrorView } from '../../../../utils/cm-dynamic-require'; 3 | import type { Range } from '@codemirror/state'; 4 | import { Decoration, DecorationSet, EditorView, ViewUpdate, WidgetType } from '@codemirror/view'; 5 | 6 | // See https://codemirror.net/examples/decoration/ 7 | 8 | const checklistPlugin = () => { 9 | const { ViewPlugin, WidgetType, Decoration } = requireCodeMirrorView(); 10 | const { syntaxTree } = requireCodeMirrorLanguage(); 11 | 12 | const checkboxClassName = 'cm-enhancement-checkbox'; 13 | 14 | class CheckboxWidget extends WidgetType { 15 | public constructor(private checked: boolean) { 16 | super(); 17 | } 18 | 19 | public override eq(other: WidgetType) { 20 | return (other instanceof CheckboxWidget) && this.checked === other.checked; 21 | } 22 | 23 | public override toDOM() { 24 | const container = document.createElement('span'); 25 | container.setAttribute('aria-hidden', 'true'); 26 | 27 | const input = document.createElement('input'); 28 | input.type = 'checkbox'; 29 | input.checked = this.checked; 30 | input.classList.add(checkboxClassName); 31 | 32 | container.appendChild(input); 33 | return container; 34 | } 35 | 36 | public override ignoreEvent(_event: Event) { 37 | // Allows toggling the checkbox on click. 38 | return false; 39 | } 40 | } 41 | 42 | const checkboxDecorationChecked = Decoration.replace({ 43 | widget: new CheckboxWidget(true), 44 | }); 45 | const checkboxDecorationUnchecked = Decoration.replace({ 46 | widget: new CheckboxWidget(false), 47 | }); 48 | 49 | const buildCheckboxDecorations = (view: EditorView) => { 50 | const decorations: Range[] = []; 51 | const cursor = view.state.selection.main.from; 52 | 53 | for (const { from, to } of view.visibleRanges) { 54 | syntaxTree(view.state).iterate({ 55 | from, to, 56 | enter: node => { 57 | if (node.name === 'TaskMarker') { 58 | // Generally, task markers are defined like this: 59 | // - [x] 60 | // CodeMirror doesn't include the "- " part in the TaskMarker syntax tree 61 | // node. As such, we increase the concealed size by 2 to include it: 62 | const beforeCheckbox = view.state.sliceDoc(node.from - 2, node.from); 63 | const from = beforeCheckbox === '- ' ? node.from - 2 : node.from; 64 | 65 | const containsCursor = cursor >= from && cursor <= node.to; 66 | if (!containsCursor) { 67 | const checkboxContent = view.state.sliceDoc(node.from, node.to); 68 | const isChecked = checkboxContent.toLowerCase().includes('x'); 69 | const decoration = isChecked ? checkboxDecorationChecked : checkboxDecorationUnchecked; 70 | 71 | decorations.push(decoration.range(from, node.to)); 72 | } 73 | } 74 | }, 75 | }); 76 | } 77 | return Decoration.set(decorations); 78 | }; 79 | 80 | // Toggles the checkbox just before `pos`. 81 | // `pos` should point to just after the checkbox. 82 | const toggleChecbox = (view: EditorView, pos: number) => { 83 | const before = view.state.sliceDoc(pos - 3, pos); 84 | let newContent = '[x]'; 85 | if (before.toLowerCase() === '[x]') { 86 | newContent = '[ ]'; 87 | } 88 | view.dispatch({ changes: { from: pos - 3, to: pos, insert: newContent }}); 89 | }; 90 | 91 | return ViewPlugin.fromClass(class { 92 | public decorations: DecorationSet; 93 | 94 | public constructor(view: EditorView) { 95 | this.decorations = buildCheckboxDecorations(view); 96 | } 97 | 98 | public update(update: ViewUpdate) { 99 | if (update.docChanged || update.viewportChanged || update.selectionSet) { 100 | this.decorations = buildCheckboxDecorations(update.view); 101 | } 102 | } 103 | }, { 104 | decorations: plugin => plugin.decorations, 105 | eventHandlers: { 106 | mousedown: (event, view) => { 107 | const target = event.target as HTMLElement; 108 | if (target.nodeName === 'INPUT' && target.classList.contains(checkboxClassName)) { 109 | toggleChecbox(view, view.posAtDOM((target))); 110 | return true; 111 | } 112 | }, 113 | } 114 | }); 115 | }; 116 | 117 | export default checklistPlugin; -------------------------------------------------------------------------------- /src/driver/codemirror/taskAndHeaderRenderer/v6/index.ts: -------------------------------------------------------------------------------- 1 | import checklistPlugin from './checklistPlugin'; 2 | 3 | const taskAndHeaderRenderer = () => { 4 | return [ 5 | checklistPlugin(), 6 | ]; 7 | }; 8 | 9 | export default taskAndHeaderRenderer; -------------------------------------------------------------------------------- /src/driver/markdownItRenderer/filePreview/filePreviewRenderer.ts: -------------------------------------------------------------------------------- 1 | import path = require("path"); 2 | var mime = require('mime-types') 3 | 4 | 5 | export function filePreviewRenderer(markdownIt, _options) { 6 | const defaultRender = markdownIt.renderer.rules.link_close || function (tokens, idx, options, env, self) { 7 | return self.renderToken(tokens, idx, options, env, self); 8 | }; 9 | 10 | markdownIt.renderer.rules.link_close = function (tokens, idx, options, env, self) { 11 | let result = defaultRender(tokens, idx, options, env, self); 12 | if (tokens.length < 2) { 13 | return result; 14 | } 15 | 16 | const token = tokens[0]; 17 | if (token.type !== 'link_open') { 18 | return result; 19 | } 20 | 21 | let link_path; 22 | if (!token.attrs) { 23 | return result; 24 | } 25 | 26 | for (let arr of token.attrs) { 27 | if (arr[0] === 'href') { 28 | link_path = arr[1]; 29 | } 30 | } 31 | 32 | if (link_path.startsWith('file:///')) { 33 | let file_extension = path.extname(link_path).toUpperCase(); 34 | switch (file_extension) { 35 | case '.PDF': 36 | return result + ``; 37 | } 38 | } 39 | return result 40 | }; 41 | } -------------------------------------------------------------------------------- /src/driver/markdownItRenderer/filePreview/index.ts: -------------------------------------------------------------------------------- 1 | import {filePreviewRenderer} from "./filePreviewRenderer"; 2 | 3 | export default function (context) { 4 | return { 5 | plugin: function (markdownIt, _options) { 6 | const pluginId = context.pluginId; 7 | 8 | filePreviewRenderer(markdownIt, _options); 9 | }, 10 | assets: function() { 11 | return [ 12 | ]; 13 | }, 14 | } 15 | } -------------------------------------------------------------------------------- /src/driver/markdownItRenderer/image/imageRenderer.ts: -------------------------------------------------------------------------------- 1 | export function imageRenderer(markdownIt, _options) { 2 | const defaultRender = markdownIt.renderer.rules.image || function (tokens, idx, options, env, self) { 3 | return self.renderToken(tokens, idx, options, env, self); 4 | }; 5 | 6 | markdownIt.renderer.rules.image = function (tokens, idx, options, env, self) { 7 | // console.log(tokens, idx); 8 | const token = tokens[idx]; 9 | let result = defaultRender(tokens, idx, options, env, self); 10 | 11 | let styleStr = ""; 12 | for (let attr of token.attrs) { 13 | if (attr[0] == 'alt' && attr[1].length > 0) { 14 | result = ` 15 | ${result} 16 |
${attr[1]}
17 | ` 18 | } else if (attr[0] == 'width') { 19 | styleStr += ` width="${attr[1]}" `; 20 | } else if (attr[0] == 'height') { 21 | styleStr += ` height="${attr[1]}" `; 22 | } 23 | } 24 | 25 | let imgTagRex = / 30 | ${result} 31 | 32 | `; 33 | return result 34 | } 35 | } -------------------------------------------------------------------------------- /src/driver/markdownItRenderer/image/index.ts: -------------------------------------------------------------------------------- 1 | import {imageRenderer} from "./imageRenderer"; 2 | 3 | export default function (context) { 4 | return { 5 | plugin: function (markdownIt, _options) { 6 | const pluginId = context.pluginId; 7 | 8 | imageRenderer(markdownIt, _options); 9 | }, 10 | assets: function() { 11 | return [ 12 | { name: 'markdownItPlugin.css' } 13 | ]; 14 | }, 15 | } 16 | } -------------------------------------------------------------------------------- /src/driver/markdownItRenderer/image/markdownItPlugin.css: -------------------------------------------------------------------------------- 1 | body { 2 | counter-set: figNo; 3 | } 4 | 5 | figure { 6 | text-align: center; 7 | counter-increment: figNo; 8 | } 9 | 10 | img { 11 | text-align: center; 12 | } 13 | 14 | figcaption { 15 | text-align: center; 16 | font-weight: bold; 17 | } 18 | 19 | figcaption::before { 20 | content: 'Fig. ' counter(figNo) ': '; 21 | } 22 | 23 | .mermaid { 24 | text-align: center; 25 | } 26 | 27 | -------------------------------------------------------------------------------- /src/driver/markdownItRenderer/pseudocode/index.ts: -------------------------------------------------------------------------------- 1 | import {pseudocodeFenceRenderer} from "./pseudocode"; 2 | 3 | var mime = require('mime-types') 4 | 5 | export default function (context) { 6 | return { 7 | plugin: function (markdownIt, _options) { 8 | const pluginId = context.pluginId; 9 | 10 | pseudocodeFenceRenderer(markdownIt, _options); 11 | }, 12 | assets: function() { 13 | return [ 14 | { name: 'pseudocode.min.css' } 15 | ]; 16 | }, 17 | } 18 | } -------------------------------------------------------------------------------- /src/driver/markdownItRenderer/pseudocode/pseudocode.min.css: -------------------------------------------------------------------------------- 1 | .ps-root{font-family:KaTeX_Main,'Times New Roman',Times,serif;font-size:1em;font-weight:100;-webkit-font-smoothing:antialiased!important}.ps-root .ps-algorithm{margin:.8em 0;border-top:3px solid #000;border-bottom:2px solid #000}.ps-root .ps-algorithm.with-caption>.ps-line:first-child{border-bottom:2px solid #000}.ps-root .katex{text-indent:0;font-size:1em}.ps-root .MathJax,.ps-root .MathJax_CHTML{text-indent:0;font-size:1em!important}.ps-root .ps-line{margin:0;padding:0;line-height:1.2}.ps-root .ps-funcname{font-family:KaTeX_Main,'Times New Roman',Times,serif;font-weight:400;font-variant:small-caps;font-style:normal;text-transform:none}.ps-root .ps-keyword{font-family:KaTeX_Main,'Times New Roman',Times,serif;font-weight:700;font-variant:normal;font-style:normal;text-transform:none}.ps-root .ps-comment{font-family:KaTeX_Main,'Times New Roman',Times,serif;font-weight:400;font-variant:normal;font-style:normal;text-transform:none}.ps-root .ps-linenum{font-size:.8em;line-height:1em;width:1.6em;text-align:right;display:inline-block;position:relative;padding-right:.3em}.ps-root .ps-algorithmic.with-linenum .ps-line.ps-code{text-indent:-1.6em}.ps-root .ps-algorithmic.with-linenum .ps-line.ps-code>span{text-indent:0} 2 | 3 | p.ps-line { 4 | margin: 0.25em !important; 5 | } 6 | 7 | .ps-algorithm { 8 | width: 400px; 9 | } -------------------------------------------------------------------------------- /src/driver/markdownItRenderer/pseudocode/pseudocode.ts: -------------------------------------------------------------------------------- 1 | import { renderToString } from 'pseudocode'; 2 | 3 | export function pseudocodeFenceRenderer(markdownIt, _options) { 4 | const defaultRender = markdownIt.renderer.rules.fence || function (tokens, idx, options, env, self) { 5 | return self.renderToken(tokens, idx, options, env, self); 6 | }; 7 | 8 | markdownIt.renderer.rules.fence = function (tokens, idx, options, env, self) { 9 | // console.log(tokens, idx); 10 | const token = tokens[idx]; 11 | if (token.info !== 'pseudocode') { 12 | return defaultRender(tokens, idx, options, env, self); 13 | } 14 | 15 | try { 16 | let result = renderToString(token.content, {lineNumber: true, captionCount: 1}); 17 | return result.replace(/Algorithm \d/, 'Algorithm'); 18 | } catch (e) { 19 | 20 | } finally { 21 | } 22 | 23 | return '

Errors in pseudocode

'; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/driver/markdownItRenderer/quote/index.ts: -------------------------------------------------------------------------------- 1 | import {quoteRenderer} from "./quoteRender"; 2 | 3 | export default function (context) { 4 | return { 5 | plugin: function (markdownIt, _options) { 6 | const pluginId = context.pluginId; 7 | 8 | quoteRenderer(markdownIt, _options); 9 | }, 10 | assets: function() { 11 | return [ 12 | { 13 | name: 'quoteRender.css' 14 | } 15 | ]; 16 | }, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/driver/markdownItRenderer/quote/quoteRender.css: -------------------------------------------------------------------------------- 1 | span.blockquote-name.blockquote-enhancement::before { 2 | content: '🖇 '; 3 | } 4 | 5 | span.blockquote-date.blockquote-enhancement::before { 6 | content: '🕰 '; 7 | } 8 | 9 | span.blockquote-name.blockquote-enhancement { 10 | padding-right: 1em; 11 | } 12 | 13 | small.blockquote-enhancement-wrap::before { 14 | content: '\2014 \00A0'; 15 | } 16 | 17 | small.blockquote-enhancement-wrap { 18 | background: lavender; 19 | padding: 2px 6px 2px 4px; 20 | border-radius: 6px; 21 | } 22 | 23 | blockquote > p { 24 | margin-top: 0; 25 | } 26 | -------------------------------------------------------------------------------- /src/driver/markdownItRenderer/quote/quoteRender.ts: -------------------------------------------------------------------------------- 1 | const colorRegex = /\[color=(.*?)\]/; 2 | const nameRegex = /\[name=(.*?)\]/; 3 | const dateRegex = /\[date=(.*?)\]/; 4 | 5 | 6 | export function quoteRenderer(markdownIt, _options) { 7 | const defaultRender = markdownIt.renderer.rules.blockquote_open || function (tokens, idx, options, env, self) { 8 | return self.renderToken(tokens, idx, options, env, self); 9 | }; 10 | 11 | markdownIt.renderer.rules.blockquote_open = function (tokens, idx, options, env, self) { 12 | const token = tokens[idx]; 13 | let name = null, date = null; 14 | for (let i = idx + 1; i < tokens.length; i++) { 15 | if (tokens[i].type === 'blockquote_close') { 16 | break; 17 | } 18 | 19 | const colorMatch = colorRegex.exec(tokens[i].content); 20 | const nameMatch = nameRegex.exec(tokens[i].content); 21 | const dateMatch = dateRegex.exec(tokens[i].content); 22 | 23 | if (colorMatch || nameMatch || dateMatch) { 24 | for (const child of tokens[i].children) { 25 | if (child.type !== 'text') { 26 | continue; 27 | } 28 | 29 | let realColorMatch = colorRegex.exec(child.content); 30 | if (realColorMatch) { 31 | child.content = child.content.replace(colorRegex, ''); 32 | if (!token.attrs) { 33 | token.attrs = []; 34 | } 35 | token.attrs.push(['style', `border-color:${realColorMatch[1]}`]); 36 | } 37 | 38 | let realNameMatch = nameRegex.exec(child.content); 39 | if (realNameMatch) { 40 | child.content = child.content.replace(nameRegex, ''); 41 | name = realNameMatch[1]; 42 | } 43 | 44 | let realDateMatch = dateRegex.exec(child.content); 45 | if (realDateMatch) { 46 | child.content = child.content.replace(dateRegex, ''); 47 | date = realDateMatch[1]; 48 | } 49 | } 50 | } 51 | } 52 | 53 | let result = defaultRender(tokens, idx, options, env, self); 54 | let appendix = ''; 55 | if (name) { 56 | appendix += `${name}`; 57 | } 58 | if (date) { 59 | appendix += `${date}`; 60 | } 61 | 62 | if (appendix.length > 0) { 63 | result += `${appendix}`; 64 | } 65 | 66 | return result; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/driver/markdownItRuler/image/utils.ts: -------------------------------------------------------------------------------- 1 | // Utilities 2 | // 3 | 'use strict'; 4 | 5 | // Hepler to unify [reference labels]. 6 | // 7 | function normalizeReference(str) { 8 | // Trim and collapse whitespace 9 | // 10 | str = str.trim().replace(/\s+/g, ' '); 11 | 12 | // In node v10 'ẞ'.toLowerCase() === 'Ṿ', which is presumed to be a bug 13 | // fixed in v12 (couldn't find any details). 14 | // 15 | // So treat this one as a special case 16 | // (remove this when node v10 is no longer supported). 17 | // 18 | if ('ẞ'.toLowerCase() === 'Ṿ') { 19 | str = str.replace(/ẞ/g, 'ß'); 20 | } 21 | 22 | // .toLowerCase().toUpperCase() should get rid of all differences 23 | // between letter variants. 24 | // 25 | // Simple .toLowerCase() doesn't normalize 125 code points correctly, 26 | // and .toUpperCase doesn't normalize 6 of them (list of exceptions: 27 | // İ, ϴ, ẞ, Ω, K, Å - those are already uppercased, but have differently 28 | // uppercased versions). 29 | // 30 | // Here's an example showing how it happens. Lets take greek letter omega: 31 | // uppercase U+0398 (Θ), U+03f4 (ϴ) and lowercase U+03b8 (θ), U+03d1 (ϑ) 32 | // 33 | // Unicode entries: 34 | // 0398;GREEK CAPITAL LETTER THETA;Lu;0;L;;;;;N;;;;03B8; 35 | // 03B8;GREEK SMALL LETTER THETA;Ll;0;L;;;;;N;;;0398;;0398 36 | // 03D1;GREEK THETA SYMBOL;Ll;0;L; 03B8;;;;N;GREEK SMALL LETTER SCRIPT THETA;;0398;;0398 37 | // 03F4;GREEK CAPITAL THETA SYMBOL;Lu;0;L; 0398;;;;N;;;;03B8; 38 | // 39 | // Case-insensitive comparison should treat all of them as equivalent. 40 | // 41 | // But .toLowerCase() doesn't change ϑ (it's already lowercase), 42 | // and .toUpperCase() doesn't change ϴ (already uppercase). 43 | // 44 | // Applying first lower then upper case normalizes any character: 45 | // '\u0398\u03f4\u03b8\u03d1'.toLowerCase().toUpperCase() === '\u0398\u0398\u0398\u0398' 46 | // 47 | // Note: this is equivalent to unicode case folding; unicode normalization 48 | // is a different step that is not required here. 49 | // 50 | // Final result should be uppercased, because it's later stored in an object 51 | // (this avoid a conflict with Object.prototype members, 52 | // most notably, `__proto__`) 53 | // 54 | return str.toLowerCase().toUpperCase(); 55 | } 56 | 57 | function isSpace(code) { 58 | switch (code) { 59 | case 0x09: 60 | case 0x20: 61 | return true; 62 | } 63 | return false; 64 | } 65 | 66 | exports.normalizeReference = normalizeReference; 67 | exports.isSpace = isSpace; 68 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 1, 3 | "id": "com.septemberhx.Joplin.Enhancement", 4 | "app_min_version": "2.7", 5 | "version": "1.3.1", 6 | "name": "Enhancement", 7 | "description": "Enhance the markdown editor with live preview for math, mermaid, link, image, and more. It also includes other features for markdown rendering.", 8 | "author": "SeptemberHX", 9 | "homepage_url": "https://github.com/SeptemberHX/joplin-plugin-enhancement.git", 10 | "repository_url": "https://github.com/SeptemberHX/joplin-plugin-enhancement.git", 11 | "keywords": ["Live Preview"], 12 | "categories": [] 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/CMInlineMarkerHelperV2.ts: -------------------------------------------------------------------------------- 1 | // implemented according to https://github.com/ylc395/joplin-plugin-note-link-system/blob/main/src/driver/codeMirror/UrlFolder.ts 2 | 3 | import { debounce } from "ts-debounce"; 4 | import clickAndClear from "./click-and-clear"; 5 | import {isRangeSelected} from "./cm-utils"; 6 | 7 | interface MarkerMatch { 8 | match; 9 | } 10 | 11 | export default class CMInlineMarkerHelperV2 { 12 | renderer: (match, from, to) => any; 13 | lineFilter: (line: string) => boolean; 14 | clicked: (match, e: MouseEvent) => void; 15 | regex; 16 | MARKER_CLASS_NAME: string; 17 | 18 | constructor(private readonly editor, regex, renderer, MARKER_CLASS_NAME, lineFilter?, clicked?) { 19 | this.regex = regex; 20 | this.renderer = renderer; 21 | this.lineFilter = lineFilter; 22 | this.clicked = clicked; 23 | this.MARKER_CLASS_NAME = MARKER_CLASS_NAME; 24 | } 25 | 26 | /** 27 | * This function should be in editor.operation() 28 | */ 29 | public process(afterSetValue: boolean = false) { 30 | const viewport = this.editor.getViewport() 31 | const doc = this.editor.getDoc(); 32 | const currentCursor = this.editor.getCursor(); 33 | 34 | let fromLine = viewport.from; 35 | let toLine = viewport.to; 36 | 37 | if (afterSetValue) { 38 | fromLine = 0; 39 | 40 | // improve user experience; 41 | // todo: decide whether use smaller number or not 42 | toLine = this.editor.lineCount(); 43 | } 44 | 45 | for (let lineNo = fromLine; lineNo < toLine; ++lineNo) { 46 | const line = doc.getLine(lineNo); 47 | if (!line) { 48 | continue; 49 | } 50 | 51 | if (this.lineFilter) { 52 | if (!this.lineFilter(line)) { 53 | continue; 54 | } 55 | } 56 | 57 | // to process the situations like ==Hello **World**== correctly 58 | // we need to get all match and process with specific orders 59 | let lineMatches: MarkerMatch[] = []; 60 | for (const match of line.matchAll(this.regex)) { 61 | // by default, we ignore inline code between ` and ``` 62 | const tokenType = this.editor.getTokenTypeAt({line: lineNo, ch: match.index}); 63 | if (tokenType && tokenType.includes('jn-monospace')) { 64 | continue; 65 | } 66 | lineMatches.push({ 67 | match: match 68 | }); 69 | } 70 | 71 | // we need to process from inside out, which means we need to process them in reverse order 72 | for (let currIndex = lineMatches.length - 1; currIndex >= 0; --currIndex) { 73 | const markerMatch = lineMatches[currIndex]; 74 | this.foldByMatch(doc, lineNo, markerMatch.match); 75 | } 76 | } 77 | } 78 | 79 | private foldByMatch(doc, lineNo, match) { 80 | if (match) { 81 | const cursor = this.editor.getCursor(); 82 | // not fold when it is folded ? 83 | this.editor.findMarksAt({line: lineNo, ch: match.index}).find((marker) => { 84 | if (marker.className === this.MARKER_CLASS_NAME) { 85 | marker.clear(); 86 | } 87 | }); 88 | 89 | const from = {line: lineNo, ch: match.index}; 90 | const to = {line: lineNo, ch: match.index + match[0].length}; 91 | 92 | let selected = isRangeSelected(from, to, this.editor); 93 | 94 | if (!selected) { 95 | // not fold when the cursor is in the block 96 | if (!(cursor.line === lineNo && cursor.ch >= from.ch && cursor.ch <= to.ch)) { 97 | const element = this.renderer(match, from, to); 98 | const textMarker = doc.markText( 99 | from, 100 | to, 101 | { 102 | replacedWith: element, 103 | className: this.MARKER_CLASS_NAME, // class name is not renderer in DOM 104 | clearOnEnter: true, 105 | inclusiveLeft: false, 106 | inclusiveRight: false 107 | }, 108 | ); 109 | 110 | element.onclick = (e) => { 111 | if (e.ctrlKey || e.metaKey) { 112 | e.preventDefault(); 113 | if (this.clicked) { 114 | this.clicked(match, e); 115 | } 116 | } else { 117 | clickAndClear(textMarker, this.editor)(e); 118 | } 119 | }; 120 | } 121 | } 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/utils/can-render-element.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @ignore 3 | * BEGIN HEADER 4 | * 5 | * Contains: canRenderElement 6 | * CVM-Role: Utility Function 7 | * Maintainer: Hendrik Erz 8 | * License: GNU GPL v3 9 | * 10 | * Description: This function can check if you can renderer a preview element 11 | * at the provided position 12 | * 13 | * END HEADER 14 | */ 15 | 16 | import { Editor, Position } from 'codemirror' 17 | 18 | /** 19 | * Given the provided positions, this function returns true if you are free to 20 | * renderer an element there 21 | * 22 | * @param {Editor} cm The editor instance 23 | * @param {Position} from The beginning position 24 | * @param {Position} to The ending position 25 | * 26 | * @param {Boolean} ignoreCursor Whether we need to check the cursor position 27 | * @return {boolean} Returns true if you can renderer an element there 28 | */ 29 | export default function canRenderElement (cm: Editor, from: Position, to: Position, ignoreCursor: boolean = false): boolean { 30 | // Check if the cursor is within the range 31 | if (!ignoreCursor) { 32 | const cursor = cm.getCursor('head') 33 | if (cursor.line === from.line && cursor.ch >= from.ch && cursor.ch <= to.ch) { 34 | return false 35 | } 36 | } 37 | 38 | // We can only have one marker at any given position at any given time 39 | if (cm.findMarks(from, to).length > 0) { 40 | return false 41 | } 42 | 43 | // We cannot renderer an element within a comment. This is the final check since 44 | // it is quite expensive to compute 45 | const tokenTypeBegin = cm.getTokenTypeAt(from) 46 | const tokenTypeEnd = cm.getTokenTypeAt(to) 47 | if (tokenTypeBegin?.includes('comment') || tokenTypeEnd?.includes('comment')) { 48 | return false 49 | } 50 | 51 | return true 52 | } 53 | -------------------------------------------------------------------------------- /src/utils/citeproc.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @ignore 3 | * BEGIN HEADER 4 | * 5 | * Contains: Citeproc Typings 6 | * CVM-Role: Types 7 | * Maintainer: Hendrik Erz 8 | * License: GNU GPL v3 9 | * 10 | * Description: Contains the types for Citeproc.js, since these are not (yet) 11 | * added to the types repository. 12 | * 13 | * END HEADER 14 | */ 15 | 16 | /** 17 | * Cite-items describe a specific reference to a bibliographic item. The fields 18 | * that a cite-item may contain depend on its context. In a citation, cite-items 19 | * listed as part of the citationItems array provide only pinpoint, descriptive, 20 | * and text-suppression fields. 21 | */ 22 | interface CiteItem { 23 | /** 24 | * The only required field: Contains a citation key with which the engine can 25 | * request the full bibliographic information from the registry. 26 | */ 27 | id: string 28 | /** 29 | * A string identifying a page number or other pinpoint location or range 30 | * within the resource. 31 | */ 32 | locator?: string 33 | /** 34 | * A label type, indicating whether the locator is to a page, a chapter, or 35 | * other subdivision of the target resource. Valid labels are defined in the 36 | * CSL specification. 37 | */ 38 | label?: string 39 | /** 40 | * If true, author names will not be included in the citation output for this 41 | * cite. 42 | */ 43 | 'suppress-author'?: boolean 44 | /** 45 | * If true, only the author name will be included in the citation output for 46 | * this cite – this optional parameter provides a means for certain demanding 47 | * styles that require the processor output to be divided between the main 48 | * text and a footnote. (See the section Partial suppression of citation 49 | * content under Running the Processor :: Dirty Tricks for more details.) 50 | */ 51 | 'author-only'?: boolean 52 | /** 53 | * A string to print before this cite item. 54 | */ 55 | prefix?: string 56 | /** 57 | * A string to print after this cite item. 58 | */ 59 | suffix?: string 60 | } 61 | -------------------------------------------------------------------------------- /src/utils/click-and-clear.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @ignore 3 | * BEGIN HEADER 4 | * 5 | * Contains: clickAndClear 6 | * CVM-Role: Utility function 7 | * Maintainer: Hendrik Erz 8 | * License: GNU GPL v3 9 | * 10 | * Description: A small helper function that is intended to be used with 11 | * CodeMirror rendering plugins. 12 | * 13 | * END HEADER 14 | */ 15 | 16 | import CodeMirror, { TextMarker } from 'codemirror' 17 | 18 | /** 19 | * Returns a callback that performs a "click and clear" operation on a rendered 20 | * textmarker, i.e. remove the marker and place the cursor precisely where the 21 | * user clicked 22 | * 23 | * @param {TextMarker} marker The text marker 24 | * @param {CodeMirror} cm The CodeMirror instance 25 | * 26 | * @return {Function} The callback 27 | */ 28 | export default function clickAndClear ( 29 | marker: TextMarker, 30 | cm: CodeMirror.Editor 31 | ): (e: MouseEvent) => void { 32 | return (e: MouseEvent) => { 33 | marker.clear() 34 | cm.setCursor(cm.coordsChar({ left: e.clientX, top: e.clientY })) 35 | cm.focus() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/utils/cm-dynamic-require.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Because this plugin supports both CodeMirror 5 and CodeMirror 6, 4 | * we need to be able to dynamic-require some packages that are only available 5 | * in CodeMirror 6. 6 | * 7 | * Normal imports fail in older versions of Joplin where these libraries aren't 8 | * available and perhaps in some cases while using CodeMirror 5. 9 | */ 10 | 11 | import type * as CodeMirrorView from '@codemirror/view'; 12 | import type * as CodeMirrorState from '@codemirror/state'; 13 | import type * as CodeMirrorLanguage from '@codemirror/language'; 14 | 15 | export function requireCodeMirrorView() { 16 | return require('@codemirror/view') as typeof CodeMirrorView; 17 | } 18 | 19 | export function requireCodeMirrorState() { 20 | return require('@codemirror/state') as typeof CodeMirrorState; 21 | } 22 | 23 | export function requireCodeMirrorLanguage() { 24 | return require('@codemirror/language') as typeof CodeMirrorLanguage; 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/cm-utils.ts: -------------------------------------------------------------------------------- 1 | import type { EditorView } from "@codemirror/view"; 2 | import {Editor, Position} from "codemirror"; 3 | 4 | export function isRangeSelected(from: Position, to: Position, cm: Editor): boolean { 5 | let selected = false; 6 | for (let selection of cm.getDoc().listSelections()) { 7 | if (from.line < selection.from().line || (from.line === selection.from().line && from.ch < selection.from().ch) 8 | || to.line > selection.to().line || (to.line === selection.to().line && to.ch > selection.to().ch)) { 9 | ; 10 | } else { 11 | selected = true; 12 | break; 13 | } 14 | } 15 | return selected; 16 | } 17 | 18 | export function isCursorOutRange(cursorPos: Position, from: Position, to: Position) { 19 | return cursorPos.line < from.line || cursorPos.line > to.line 20 | || (cursorPos.line === from.line && cursorPos.ch < from.ch) 21 | || (cursorPos.line === to.line && cursorPos.ch > to.ch); 22 | } 23 | 24 | export function findLineWidgetAtLine(editor: Editor, lineNumber: number, className: string) { 25 | // check whether there exists rendered line widget 26 | const line = editor.lineInfo(lineNumber); 27 | if (line.widgets) { 28 | for (const wid of line.widgets) { 29 | if (wid.className === className) { 30 | return wid; 31 | } 32 | } 33 | } 34 | return null; 35 | } 36 | 37 | export function isReadOnly(editor: any) { 38 | if (editor.cm6) { 39 | return (editor.editor as EditorView).state.readOnly; 40 | } else { 41 | return (editor as Editor).isReadOnly(); 42 | } 43 | } -------------------------------------------------------------------------------- /src/utils/md-to-html.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @ignore 3 | * BEGIN HEADER 4 | * 5 | * Contains: md2html function 6 | * CVM-Role: Utility function 7 | * Maintainer: Hendrik Erz 8 | * License: GNU GPL v3 9 | * 10 | * Description: md2html converts a Markdown string to valid HTML 11 | * 12 | * END HEADER 13 | */ 14 | 15 | import { Converter, ShowdownExtension } from 'showdown' 16 | import extractCitations from './extract-citations' 17 | 18 | type CitationCallback = (items: CiteItem[], composite: boolean) => string|undefined 19 | 20 | /** 21 | * md2html converts the given Markdown to HTML, optionally making any link 22 | * "renderer" safe, which means that those links will be opened using 23 | * electron shell to prevent them overriding the content of the window. 24 | * 25 | * @param {string} markdown The input Markdown text 26 | * 27 | * @return {string} The final HTML string 28 | */ 29 | export function getConverter (citationCallback?: CitationCallback): (markdown: string) => string { 30 | // If the caller did not provide a citation callback we'll create a polyfill 31 | // that will simply return an empty string, effectively stripping any citations 32 | if (citationCallback === undefined) { 33 | citationCallback = function (items, composite) { return '' } 34 | } 35 | 36 | // Spin up a showdown converter 37 | const showdownConverter = new Converter({ 38 | strikethrough: true, 39 | tables: true, 40 | omitExtraWLInCodeBlocks: true, 41 | tasklists: true, 42 | requireSpaceBeforeHeadingText: true, 43 | ghMentions: false, 44 | extensions: [makeCitationPlugin(citationCallback)] 45 | }) 46 | 47 | showdownConverter.setFlavor('github') 48 | 49 | return function (markdown: string): string { 50 | return showdownConverter.makeHtml(markdown) 51 | } 52 | } 53 | 54 | /** 55 | * Extension for showdown.js which parses citations using our citeproc provider. 56 | * 57 | * @return {any} The showdown extension 58 | */ 59 | function makeCitationPlugin (citationCallback: CitationCallback): () => ShowdownExtension { 60 | return function (): ShowdownExtension { 61 | return { 62 | type: 'lang', 63 | filter: function (text, converter, options) { 64 | // First, extract all citations ... 65 | const allCitations = extractCitations(text) 66 | // ... and retrieve the rendered ones from the citeproc provider 67 | const finalCitations = allCitations.map((elem) => { 68 | return citationCallback(elem.citations, elem.composite) ?? text 69 | }) 70 | 71 | // Now get the citations to be replaced 72 | const toBeReplaced = allCitations.map(citation => { 73 | return text.substring(citation.from, citation.to - citation.from) 74 | }) 75 | 76 | // Finally, replace every citation with its designated replacement 77 | for (let i = 0; i < allCitations.length; i++) { 78 | text = text.replace(toBeReplaced[i], finalCitations[i]) 79 | } 80 | 81 | // Now return the text 82 | return text 83 | } 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /src/utils/reg.ts: -------------------------------------------------------------------------------- 1 | export function exec(query: RegExp, stream: any) { 2 | query.lastIndex = stream.pos; 3 | return query.exec(stream.string); 4 | } 5 | -------------------------------------------------------------------------------- /src/utils/string-render.ts: -------------------------------------------------------------------------------- 1 | var md = require('markdown-it')() 2 | .use(require('markdown-it-mark')); 3 | 4 | /* 5 | * Use markdown-it until joplin provides APIs for string rendering 6 | */ 7 | export function renderStrToDom(str: string) { 8 | return md.renderInline(str); 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "module": "commonjs", 5 | "target": "es2015", 6 | "jsx": "react", 7 | "allowJs": true, 8 | "baseUrl": ".", 9 | "esModuleInterop": true, 10 | } 11 | } 12 | --------------------------------------------------------------------------------