├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github └── workflows │ └── release.yml ├── .gitignore ├── .npmrc ├── .whitesource ├── LICENSE ├── README.md ├── esbuild.config.mjs ├── images ├── default-command.gif └── remove-empty-links.gif ├── manifest.json ├── package-lock.json ├── package.json ├── src ├── main.ts ├── remark-ctor.ts ├── remark-whitespace-reducer.js ├── settings.ts ├── transform.ts └── transforms.ts ├── tsconfig.json ├── version-bump.mjs └── versions.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = spaces 9 | indent_size = 4 10 | tab_width = 4 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | npm node_modules 2 | build -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { "node": true }, 5 | "plugins": [ 6 | "@typescript-eslint" 7 | ], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "parserOptions": { 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "no-unused-vars": "off", 18 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], 19 | "@typescript-eslint/ban-ts-comment": "off", 20 | "no-prototype-builtins": "off", 21 | "@typescript-eslint/no-empty-function": "off" 22 | } 23 | } -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Obsidian plugin 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | env: 9 | PLUGIN_NAME: advanced-paste 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Use Node.js 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: "16.x" 21 | 22 | - name: Build 23 | id: build 24 | run: | 25 | npm install 26 | npm run build 27 | mkdir ${{ env.PLUGIN_NAME }} 28 | cp main.js manifest.json ${{ env.PLUGIN_NAME }} 29 | zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }} 30 | ls 31 | echo "::set-output name=tag_name::$(git tag --sort version:refname | tail -n 1)" 32 | 33 | - name: Create Release 34 | id: create_release 35 | uses: actions/create-release@v1 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | VERSION: ${{ github.ref }} 39 | with: 40 | tag_name: ${{ github.ref }} 41 | release_name: ${{ github.ref }} 42 | draft: false 43 | prerelease: false 44 | 45 | - name: Upload zip file 46 | id: upload-zip 47 | uses: actions/upload-release-asset@v1 48 | env: 49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 50 | with: 51 | upload_url: ${{ steps.create_release.outputs.upload_url }} 52 | asset_path: ./${{ env.PLUGIN_NAME }}.zip 53 | asset_name: ${{ env.PLUGIN_NAME }}-${{ steps.build.outputs.tag_name }}.zip 54 | asset_content_type: application/zip 55 | 56 | - name: Upload main.js 57 | id: upload-main 58 | uses: actions/upload-release-asset@v1 59 | env: 60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | with: 62 | upload_url: ${{ steps.create_release.outputs.upload_url }} 63 | asset_path: ./main.js 64 | asset_name: main.js 65 | asset_content_type: text/javascript 66 | 67 | - name: Upload manifest.json 68 | id: upload-manifest 69 | uses: actions/upload-release-asset@v1 70 | env: 71 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 72 | with: 73 | upload_url: ${{ steps.create_release.outputs.upload_url }} 74 | asset_path: ./manifest.json 75 | asset_name: manifest.json 76 | asset_content_type: application/json 77 | 78 | # - name: Upload styles.css 79 | # id: upload-css 80 | # uses: actions/upload-release-asset@v1 81 | # env: 82 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 83 | # with: 84 | # upload_url: ${{ steps.create_release.outputs.upload_url }} 85 | # asset_path: ./styles.css 86 | # asset_name: styles.css 87 | # asset_content_type: text/css -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # Intellij 5 | *.iml 6 | .idea 7 | 8 | # npm 9 | node_modules 10 | 11 | # Don't include the compiled main.js file in the repo. 12 | # They should be uploaded to GitHub releases instead. 13 | main.js 14 | 15 | # Exclude sourcemaps 16 | *.map 17 | 18 | # obsidian 19 | data.json 20 | 21 | # Exclude macOS Finder (System Explorer) View States 22 | .DS_Store 23 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /.whitesource: -------------------------------------------------------------------------------- 1 | { 2 | "scanSettings": { 3 | "baseBranches": [] 4 | }, 5 | "checkRunSettings": { 6 | "vulnerableCheckRunConclusionLevel": "failure", 7 | "displayMode": "diff", 8 | "useMendCheckNames": true 9 | }, 10 | "issueSettings": { 11 | "minSeverityLevel": "LOW", 12 | "issueType": "DEPENDENCY" 13 | } 14 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Levi Zim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # !! Project Archived, feel free to fork 2 | # !! I am moving away from obsidian to open source alternatives 3 | 4 | # Advanced Paste for Obsidian 5 | 6 | [![Sponsor](https://img.shields.io/badge/sponsor-via%20paypal-blue)](https://www.paypal.com/paypalme/tokxxt) 7 | ![License](https://img.shields.io/github/license/kxxt/obsidian-advanced-paste) 8 | ![GitHub manifest version](https://img.shields.io/github/manifest-json/v/kxxt/obsidian-advanced-paste) 9 | 10 | This plugin provides advanced paste commands and enables you to create custom transforms for pasting. 11 | 12 | Usage: Assign a hotkey to the command that you want to use and then press the hotkey to paste the content. 13 | 14 | Personally, I prefer to assign Alt+V to [`Smart Join`](#smart-join) 15 | and Alt+Shift+V to [`Remove Blank Lines`](#remove-blank-lines). 16 | 17 | > **Warning** 18 | > Never add untrusted scripts to the script directory BECAUSE IT MIGHT DESTROY YOUR VAULT OR WORSE! 19 | 20 | **You need to disable and re-enable this plugin in order to apply the changes to the script directory.** 21 | 22 | # Features 23 | 24 | ## Default 25 | 26 | This plugin provides a default transform that is better than Obsidian built-in. You can disable it in the plugin settings 27 | (You need to restart obsidian to apply the changes). 28 | 29 | It is compatible with the [auto link title plugin](https://github.com/zolrath/obsidian-auto-link-title) but might not be compatible with some other plugins 30 | that mess with the clipboard. 31 | 32 | **If you have bind Ctrl+V to this command previously, you 33 | should unbind it because the underlying mechanism has changed.** 34 | 35 | The default transform will convert html to markdown using [turndown](https://github.com/mixmark-io/turndown) 36 | and [turndown-plugin-gfm](https://github.com/mixmark-io/turndown-plugin-gfm). And it will remove empty headings and links. 37 | 38 | ![Showcase](images/remove-empty-links.gif) 39 | 40 | ### TODO 41 | 42 | - [ ] try to eliminate the extra blank lines in the converted markdown. 43 | 44 | ## Smart Join 45 | 46 | This command trims all the lines and join them by a space before pasting. It will automatically join the words that are broken by hyphens. 47 | 48 | This command is especially useful when pasting from a PDF file. 49 | 50 | ## Join Lines 51 | 52 | This command joins all the lines without trimming them before pasting. 53 | 54 | ## Remove Blank Lines 55 | 56 | This command removes all the blank lines before pasting. 57 | 58 | This command is useful when you copy something from a web page and you find that there are too many blank lines when directly pasting into obsidian. 59 | 60 | ## Raw HTML 61 | 62 | This command pastes the raw HTML of the copied content. 63 | 64 | ## Custom Transforms 65 | 66 | You can define your own custom transform by writing JavaScript. 67 | 68 | 1. Set your script directory in the settings of this plugin 69 | (Or stick to the default settings). 70 | 2. Create a JavaScript source file(`*.js` or `*.mjs`) in the script directory. 71 | (You can't do it in obsidian. You need an editor like VSCode) 72 | 3. Edit the JavaScript file to add your custom transform(s). 73 | 4. Disable this plugin and re-enable it to apply your changes. 74 | 5. Now you can find your custom transform in command platte and assign a hotkey to it. 75 | 76 | # Creating Custom Transforms 77 | 78 | The JavaScript source file will be imported as an ES Module. 79 | 80 | To create a transform, you need to create an exported function: 81 | 82 | ```javascript 83 | export function myTransform(input) { 84 | return input; 85 | } 86 | ``` 87 | 88 | The function name (in start case) will become the name of your custom transform. 89 | 90 | You can write multiple transforms in a single JavaScript source file. 91 | 92 | By default, the input to your transform is the text in the clipboard. 93 | To support other MIME types, you can set the `type` field of your transform to `"blob"`: 94 | 95 | ```javascript 96 | export async function myBlobTransform(input) { 97 | if (!input.types.includes("text/html")) { 98 | return { kind: "err", value: "No html found in clipboard!" }; 99 | } 100 | const html = await input.getType("text/html"); 101 | return html.text(); 102 | } 103 | myBlobTransform.type = "blob"; 104 | ``` 105 | 106 | This way, the input to your transform is a 107 | [`ClipboardItem`](https://developer.mozilla.org/en-US/docs/Web/API/ClipboardItem) 108 | instead of a `string`. 109 | 110 | Your transform function should return either a `string` or a `TransformResult` 111 | (Of course, you can also return a `Promise` that resolves to one of them). 112 | 113 | A `TransformResult` is a discriminated union. Its kind can only be `ok` or `err`. 114 | When you return an `ok` variant from your transform function, 115 | the `value` field should be the string of your final transformation result. 116 | When you return an `err` variant, a notice, in which the `value` field will be displayed 117 | as an error to the end user, will show up. 118 | 119 | ```javascript 120 | { kind: "ok", value: "string" } 121 | { kind: "err", value: "An error occurred!" } 122 | ``` 123 | 124 | ## Advanced: Utilities 125 | 126 | The transform function can take an optional second parameter `utils` which is an object containing some useful helpers. 127 | 128 | Currently, the [turndown service](https://github.com/mixmark-io/turndown) is provided as a utility. You can call `turndown.turndown` to convert html to markdown. 129 | 130 | You can call `saveAttachment` to save a blob to the vault. The function signature is: 131 | 132 | ```typescript 133 | saveAttachment: (name: string, ext: string, data: ArrayBuffer) => 134 | Promise; 135 | ``` 136 | 137 | If you need more information about or control over the editor state, the [Obsidian `editor` object](https://docs.obsidian.md/Plugins/Editor/Editor) is provided as a utility. 138 | 139 | `lodash`, `moment.js` and `mime` are also provided as utilities. Check out the following example: 140 | 141 | ```javascript 142 | export async function myTransform( 143 | input, 144 | { turndown, editor, _, moment, mime, saveAttachment } 145 | ) { 146 | if (input.types.includes("text/html")) { 147 | const html = await input.getType("text/html"); 148 | return turndown.turndown(await html.text()); 149 | } 150 | const text = await input.getType("text/plain"); 151 | return text.text(); 152 | } 153 | myTransform.type = "blob"; 154 | ``` 155 | 156 | `remark` and `remark` plugins are also provided as utilities, which enables you to transform a Markdown AST. Check out the following example that removes all the images before pasting: 157 | 158 | ```javascript 159 | export async function noImage( 160 | input, 161 | { 162 | turndown, 163 | remark: { 164 | remark, 165 | remarkGfm, 166 | remarkMath, 167 | unistUtilVisit: { visit, SKIP }, 168 | }, 169 | } 170 | ) { 171 | if (input.types.includes("text/html")) { 172 | const html = await input.getType("text/html"); 173 | const md = turndown.turndown(await html.text()); 174 | return remark() 175 | .use(remarkGfm) 176 | .use(remarkMath) 177 | .use(() => (tree, file) => { 178 | visit(tree, "image", (node, index, parent) => { 179 | parent.children.splice(index, 1); 180 | return [SKIP, index]; 181 | }); 182 | }) 183 | .processSync(md) 184 | .toString(); 185 | } 186 | return { kind: "err", value: "No html found in clipboard!" }; 187 | } 188 | noImage.type = "blob"; 189 | ``` 190 | 191 | For now, the following remark related utilities are provided: 192 | 193 | ```typescript 194 | remark: { 195 | unified: typeof import("unified").unified; 196 | remark: typeof import("remark").remark; 197 | remarkGfm: typeof import("remark-gfm").default; 198 | remarkMath: typeof import("remark-math").default; 199 | remarkParse: typeof import("remark-parse").default; 200 | remarkStringify: typeof import("remark-stringify").default; 201 | unistUtilVisit: typeof import("unist-util-visit"); 202 | unistUtilIs: typeof import("unist-util-is"); 203 | } 204 | ``` 205 | 206 | If you need more utilities, feel free to open an issue or submit a PR. 207 | -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import process from "process"; 3 | import builtins from "builtin-modules"; 4 | 5 | const banner = `/* 6 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 7 | if you want to view the source, please visit the github repository of this plugin 8 | */ 9 | `; 10 | 11 | const prod = process.argv[2] === "production"; 12 | 13 | const context = await esbuild.context({ 14 | banner: { 15 | js: banner, 16 | }, 17 | entryPoints: ["src/main.ts"], 18 | bundle: true, 19 | external: [ 20 | "obsidian", 21 | "electron", 22 | "@codemirror/autocomplete", 23 | "@codemirror/collab", 24 | "@codemirror/commands", 25 | "@codemirror/language", 26 | "@codemirror/lint", 27 | "@codemirror/search", 28 | "@codemirror/state", 29 | "@codemirror/view", 30 | "@lezer/common", 31 | "@lezer/highlight", 32 | "@lezer/lr", 33 | ...builtins, 34 | ], 35 | format: "cjs", 36 | target: "es2018", 37 | logLevel: "info", 38 | sourcemap: prod ? false : "inline", 39 | treeShaking: true, 40 | outfile: "main.js", 41 | }); 42 | 43 | if (!prod) await context.watch(); 44 | else { 45 | await context.rebuild(); 46 | await context.dispose(); 47 | } 48 | -------------------------------------------------------------------------------- /images/default-command.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kxxt/obsidian-advanced-paste/f71552f569fec633b0d5c8a5cc7c2cc435c2374d/images/default-command.gif -------------------------------------------------------------------------------- /images/remove-empty-links.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kxxt/obsidian-advanced-paste/f71552f569fec633b0d5c8a5cc7c2cc435c2374d/images/remove-empty-links.gif -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "advanced-paste", 3 | "name": "Advanced Paste", 4 | "version": "2.7.0", 5 | "minAppVersion": "0.15.0", 6 | "description": "This plugin provides advanced paste commands and enables you to create custom transforms for pasting.", 7 | "author": "kxxt", 8 | "authorUrl": "https://www.kxxt.dev", 9 | "fundingUrl": "https://www.paypal.com/paypalme/tokxxt", 10 | "isDesktopOnly": false 11 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "advanced-paste", 3 | "version": "2.7.0", 4 | "description": "This plugin provides advanced paste commands and enables you to create custom transforms for pasting.", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "node esbuild.config.mjs", 8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", 9 | "version": "node version-bump.mjs && git add manifest.json versions.json" 10 | }, 11 | "keywords": [ 12 | "obsidian" 13 | ], 14 | "author": "kxxt", 15 | "license": "MIT", 16 | "devDependencies": { 17 | "@types/lodash": "^4.14.200", 18 | "@types/mime": "^3.0.3", 19 | "@types/moment": "^2.13.0", 20 | "@types/node": "^20.8.7", 21 | "@types/turndown": "^5.0.3", 22 | "@typescript-eslint/eslint-plugin": "6.8.0", 23 | "@typescript-eslint/parser": "6.8.0", 24 | "builtin-modules": "3.3.0", 25 | "esbuild": "0.19.5", 26 | "obsidian": "latest", 27 | "tslib": "2.6.2", 28 | "typescript": "5.2.2" 29 | }, 30 | "dependencies": { 31 | "lodash": "^4.17.21", 32 | "mime": "^3.0.0", 33 | "moment": "^2.29.4", 34 | "obsidian-community-lib": "^2.0.2", 35 | "remark": "^15.0.1", 36 | "remark-gfm": "^4.0.0", 37 | "remark-math": "^6.0.0", 38 | "remark-parse": "^11.0.0", 39 | "remark-stringify": "^11.0.0", 40 | "turndown": "^7.1.2", 41 | "turndown-plugin-gfm": "^1.0.2", 42 | "unified": "^11.0.3", 43 | "unist-util-is": "^6.0.0", 44 | "unist-util-visit": "^5.0.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Editor, 3 | MarkdownFileInfo, 4 | MarkdownView, 5 | Notice, 6 | Plugin, 7 | TFile, 8 | TFolder, 9 | Vault, 10 | } from "obsidian"; 11 | import transformsWrapper from "./transforms"; 12 | import * as _ from "lodash"; 13 | import { 14 | Transform, 15 | TransformUtils, 16 | TransformUtilsBase, 17 | err, 18 | ok, 19 | } from "./transform"; 20 | import TurnDownService from "turndown"; 21 | import TurndownService from "turndown"; 22 | import { getAvailablePathForAttachments } from "obsidian-community-lib"; 23 | import mime from "mime"; 24 | import moment from "moment"; 25 | import { 26 | AdvancedPasteSettingTab, 27 | AdvancedPasteSettings, 28 | DEFAULT_SETTINGS, 29 | } from "./settings"; 30 | import { unified } from "unified"; 31 | import { remark } from "remark"; 32 | import remarkMath from "remark-math"; 33 | import remarkGfm from "remark-gfm"; 34 | import remarkParse from "remark-parse"; 35 | import remarkStringify from "remark-stringify"; 36 | import * as unistUtilVisit from "unist-util-visit"; 37 | import * as unistUtilIs from "unist-util-is"; 38 | import remarkCtor from "./remark-ctor"; 39 | 40 | // No types for this plugin, so we have to use require 41 | // eslint-disable-next-line @typescript-eslint/no-var-requires 42 | const { gfm } = require("turndown-plugin-gfm"); 43 | 44 | const AUTO_LINK_TITLE_REGEX = 45 | /^(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})$/i; 46 | 47 | function initTurnDown(options: TurndownService.Options): TurnDownService { 48 | const turndown = new TurndownService(options); 49 | turndown.use(gfm); 50 | return turndown; 51 | } 52 | 53 | async function executePaste( 54 | transform: Transform, 55 | utilsBase: TransformUtilsBase, 56 | vault: Vault, 57 | withinEvent: boolean, 58 | editor: Editor, 59 | view: MarkdownFileInfo 60 | ): Promise { 61 | let result; 62 | const file = view.file; 63 | if (file == null) { 64 | new Notice("Advanced paste: Can't determine active file!"); 65 | console.log(view); 66 | throw new Error( 67 | "Advanced paste: Can't determine active file!, view is" 68 | ); 69 | } 70 | const utils: TransformUtils = { 71 | ...utilsBase, 72 | async saveAttachment(name, ext, data) { 73 | const path = await getAvailablePathForAttachments(name, ext, file); 74 | return vault.createBinary(path, data); 75 | }, 76 | editor, 77 | }; 78 | const internalParams = { shouldHandleImagePasting: !withinEvent }; 79 | try { 80 | if (transform.type == "text") { 81 | const input = await navigator.clipboard.readText(); 82 | result = transform.transform(input, utils, internalParams); 83 | } else if (transform.type == "blob") { 84 | const inputs = await navigator.clipboard.read(); 85 | if (inputs.length > 0) { 86 | result = transform.transform(inputs[0], utils, internalParams); 87 | } else new Notice("Nothing to paste!"); 88 | } else { 89 | throw new Error("Unsupported input type"); 90 | } 91 | } catch (e) { 92 | if ( 93 | e instanceof DOMException && 94 | e.message == "No valid data on clipboard." 95 | ) { 96 | return null; 97 | } 98 | throw e; 99 | } 100 | const resultStringHandler = (str: string) => { 101 | if (!withinEvent) editor.replaceSelection(str); 102 | return str; 103 | }; 104 | result = await Promise.resolve(result); 105 | if (typeof result == "string") return resultStringHandler(result); 106 | else if (result?.kind == "ok") { 107 | return resultStringHandler(result.value); 108 | } else { 109 | new Notice(result?.value ?? "An error occurred in Advanced Paste."); 110 | } 111 | return null; 112 | } 113 | 114 | export default class AdvancedPastePlugin extends Plugin { 115 | settings: AdvancedPasteSettings; 116 | utils: TransformUtilsBase; 117 | 118 | registerTransform( 119 | transformId: string, 120 | transform: Transform, 121 | transformName: null | string = null 122 | ) { 123 | this.addCommand({ 124 | id: transformId, 125 | name: transformName ?? _.startCase(transformId), 126 | editorCallback: _.partial( 127 | executePaste, 128 | transform, 129 | this.utils, 130 | this.app.vault, 131 | false 132 | ), 133 | }); 134 | } 135 | 136 | async saveAttachment( 137 | name: string, 138 | ext: string, 139 | data: ArrayBuffer, 140 | sourceFile: TFile 141 | ) { 142 | const path = await getAvailablePathForAttachments( 143 | name, 144 | ext, 145 | sourceFile 146 | ); 147 | return this.app.vault.createBinary(path, data); 148 | } 149 | 150 | async getClipboardData() { 151 | const data = await navigator.clipboard.read(); 152 | if (data.length == 0) return null; 153 | return data[0]; 154 | } 155 | 156 | async handleImagePaste(input: ClipboardItem, sourceFile: TFile) { 157 | for (const type of input.types) { 158 | if (type.startsWith("image/")) { 159 | const blob = await input.getType(type); 160 | const ext = mime.getExtension(type); 161 | if (!ext) { 162 | return err( 163 | `Failed to save attachment: Could not determine extension for mime type ${type}` 164 | ); 165 | } 166 | const name = `Pasted Image ${moment().format( 167 | "YYYYMMDDHHmmss" 168 | )}`; 169 | await this.saveAttachment( 170 | name, 171 | ext, 172 | await blob.arrayBuffer(), 173 | sourceFile 174 | ); 175 | return ok(`![[${name}.${ext}]]`); 176 | } else if (type == "text/plain") { 177 | const blob = await input.getType(type); 178 | const text = await blob.text(); 179 | if (text.match(/^file:\/\/.+$/)) { 180 | try { 181 | // eslint-disable-next-line @typescript-eslint/no-var-requires 182 | const fs = require("fs").promises; 183 | const path = decodeURIComponent(text).replace( 184 | /^file:\/\//, 185 | "" 186 | ); 187 | const mimeType = mime.getType(path); 188 | if (!mimeType || !mimeType.startsWith("image/")) 189 | throw new Error("Not an image file!"); 190 | const buffer = await fs.readFile(path); 191 | const attachmentName = `Pasted Image ${moment().format( 192 | "YYYYMMDDHHmmss" 193 | )}`; 194 | const ext = mime.getExtension(mimeType); 195 | if (!ext) 196 | throw new Error( 197 | `No extension for mime type ${mimeType}` 198 | ); 199 | await this.saveAttachment( 200 | attachmentName, 201 | ext, 202 | buffer, 203 | sourceFile 204 | ); 205 | return ok(`![[${attachmentName}.${ext}]]`); 206 | } catch (e) { 207 | // 1. On mobile platform 208 | // 2. Failed to resolve/copy file 209 | console.log( 210 | `Advanced paste: can't interpret ${text} as an image`, 211 | e 212 | ); 213 | } 214 | } 215 | } 216 | } 217 | return null; 218 | } 219 | 220 | async defaultPasteCommand( 221 | evt: ClipboardEvent | null, 222 | editor: Editor, 223 | info: MarkdownView | MarkdownFileInfo 224 | ) { 225 | const isManuallyTriggered = evt == null; // Not triggered by Ctrl+V 226 | if ( 227 | !isManuallyTriggered && 228 | (evt.clipboardData?.getData("application/x-advpaste-tag") == 229 | "tag" || 230 | AUTO_LINK_TITLE_REGEX.test( 231 | evt.clipboardData?.getData("text/plain") ?? "" 232 | )) 233 | ) { 234 | // 1. Event was triggered by us, don't handle it again 235 | // 2. url, let obsidian-auto-link-title handle it 236 | return; 237 | } 238 | let html; 239 | if (isManuallyTriggered) { 240 | const items = await navigator.clipboard.read(); 241 | if (info.file) { 242 | // Try to handle image paste first 243 | const res = await this.handleImagePaste(items[0], info.file); 244 | if (res != null) { 245 | if (res.kind === "ok") { 246 | editor.replaceSelection(res.value); 247 | } else { 248 | new Notice(res.value); 249 | return; 250 | } 251 | } 252 | } 253 | if (items.length == 0 || !items[0].types.includes("text/html")) 254 | return; 255 | const blob = await items[0].getType("text/html"); 256 | html = await blob.text(); 257 | } else { 258 | // Let obsidian handle image paste, do not handle it ourselves 259 | if ( 260 | evt.clipboardData?.types.some( 261 | (x) => x == "Files" || x.startsWith("image/") 262 | ) 263 | ) 264 | return; 265 | html = evt.clipboardData?.getData("text/html"); 266 | } 267 | if (html) { 268 | evt?.preventDefault(); 269 | evt?.stopPropagation(); 270 | const md = this.utils.turndown.turndown(html); 271 | const processed = await remarkCtor(this.settings).process(md); 272 | const dat = new DataTransfer(); 273 | dat.setData("text/html", `
${processed}
`); 274 | dat.setData("application/x-advpaste-tag", "tag"); 275 | const e = new ClipboardEvent("paste", { 276 | clipboardData: dat, 277 | }); 278 | // console.log(info); 279 | const clipboardMgr = (this.app.workspace.activeEditor as any) 280 | ._children[0].clipboardManager; 281 | // console.log(clipboardMgr); 282 | clipboardMgr.handlePaste(e, editor, info); 283 | } 284 | } 285 | 286 | async onload() { 287 | await this.loadSettings(); 288 | this.utils = { 289 | turndown: initTurnDown(this.settings.turndown), 290 | mime, 291 | _, 292 | moment, 293 | remark: { 294 | unified, 295 | remark, 296 | remarkMath, 297 | remarkGfm, 298 | unistUtilVisit, 299 | unistUtilIs, 300 | remarkParse, 301 | remarkStringify, 302 | }, 303 | }; 304 | const transforms = transformsWrapper({ vault: this.app.vault }); 305 | for (const transformId in transforms) { 306 | const transform = transforms[transformId]; 307 | this.registerTransform(transformId, transform); 308 | } 309 | const vault = this.app.vault; 310 | const { scriptDir = DEFAULT_SETTINGS.scriptDir } = this.settings; 311 | // Wait for vault to be loaded 312 | this.app.workspace.onLayoutReady(async () => { 313 | const fileOrFolder = vault.getAbstractFileByPath(scriptDir); 314 | if (fileOrFolder instanceof TFolder) { 315 | const scriptFolder = fileOrFolder; 316 | const entries = await scriptFolder.children; 317 | for (const entry of entries) { 318 | let module; 319 | if ( 320 | entry instanceof TFile && 321 | (entry.name.endsWith(".js") || 322 | entry.name.endsWith(".mjs")) 323 | ) { 324 | console.log( 325 | `Advanced Paste: Loading script ${entry.name}` 326 | ); 327 | try { 328 | module = await import( 329 | "data:text/javascript," + 330 | (await vault.read(entry)) 331 | ); 332 | } catch (e) { 333 | new Notice( 334 | `Advanced Paste failed to load script: ${entry}\nPlease check your script!` 335 | ); 336 | console.error("Advanced Paste Script Error:", e); 337 | } 338 | } 339 | if (!module) continue; 340 | for (const prop of Object.getOwnPropertyNames(module)) { 341 | const obj = module[prop]; 342 | if (typeof obj == "function") { 343 | const { type = "text" } = obj; 344 | const transform = { type, transform: obj }; 345 | this.registerTransform( 346 | `custom-${prop}`, 347 | transform, 348 | _.startCase(prop) 349 | ); 350 | } 351 | } 352 | } 353 | } 354 | this.addCommand({ 355 | id: "default", 356 | name: "Default", 357 | editorCallback: (editor, info) => { 358 | this.defaultPasteCommand(null, editor, info); 359 | }, 360 | }); 361 | if (this.settings.enhanceDefaultPaste) { 362 | this.app.workspace.on( 363 | "editor-paste", 364 | this.defaultPasteCommand.bind(this) 365 | ); 366 | } 367 | }); 368 | this.addCommand({ 369 | id: `advpaste-debug`, 370 | name: "Dump Clipboard to Console", 371 | editorCallback: async (editor: Editor, view: MarkdownView) => { 372 | const contents = await navigator.clipboard.read(); 373 | console.log(contents); 374 | }, 375 | }); 376 | // This adds a settings tab so the user can configure various aspects of the plugin 377 | this.addSettingTab(new AdvancedPasteSettingTab(this.app, this)); 378 | console.info(`${this.manifest.name} loaded!`); 379 | } 380 | 381 | onunload() {} 382 | 383 | async loadSettings() { 384 | this.settings = Object.assign( 385 | {}, 386 | DEFAULT_SETTINGS, 387 | await this.loadData() 388 | ); 389 | } 390 | 391 | async saveSettings() { 392 | this.utils.turndown = initTurnDown(this.settings.turndown); 393 | await this.saveData(this.settings); 394 | } 395 | } 396 | -------------------------------------------------------------------------------- /src/remark-ctor.ts: -------------------------------------------------------------------------------- 1 | import remarkGfm from "remark-gfm"; 2 | import remarkMath from "remark-math"; 3 | import remarkParse from "remark-parse"; 4 | import remarkStringify from "remark-stringify"; 5 | import { unified } from "unified"; 6 | import remarkWhitespaceReducer from "./remark-whitespace-reducer"; 7 | import { AdvancedPasteSettings } from "./settings"; 8 | 9 | export default function remarkCtor(pluginSettings: AdvancedPasteSettings) { 10 | return ( 11 | unified() 12 | .use(remarkParse) 13 | .use(remarkGfm) 14 | .use(remarkMath) 15 | .use(remarkWhitespaceReducer) 16 | // @ts-expect-error 17 | .use(remarkStringify, { 18 | bullet: pluginSettings.turndown.bulletListMarker ?? "-", 19 | fence: pluginSettings.turndown.fence?.[0] ?? "`", 20 | setext: pluginSettings.turndown.headingStyle == "setext", 21 | strong: pluginSettings.turndown.strongDelimiter?.[0] ?? "*", 22 | emphasis: pluginSettings.turndown.emDelimiter?.[0] ?? "*", 23 | }) 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/remark-whitespace-reducer.js: -------------------------------------------------------------------------------- 1 | import { is } from "unist-util-is"; 2 | import { visit, SKIP } from "unist-util-visit"; 3 | 4 | export default function remarkWhitespaceReducer() { 5 | return (tree, file) => { 6 | visit(tree, ["link", "heading"], (node, index, parent) => { 7 | // console.log(node); 8 | if ( 9 | is(node, "heading") && 10 | (node.children.length === 0 || // A heading without children 11 | (node.children.length === 1 && 12 | is(node.children[0], "link") && 13 | node.children[0].children.length === 0)) // A heading with a single child that is an empty link 14 | ) { 15 | console.log("removing empty heading"); 16 | parent.children.splice(index, 1); 17 | return [SKIP, index]; 18 | } else if (is(node, "link") && node.children.length == 0) { 19 | console.log("removing empty link"); 20 | parent.children.splice(index, 1); 21 | return [SKIP, index]; 22 | } 23 | }); 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | import { App, PluginSettingTab, Setting } from "obsidian"; 2 | import TurndownService from "turndown"; 3 | import TurnDownService from "turndown"; 4 | import AdvancedPastePlugin from "./main"; 5 | 6 | export interface AdvancedPasteSettings { 7 | scriptDir: string; 8 | turndown: TurnDownService.Options; 9 | enhanceDefaultPaste: boolean; 10 | // autoLinkTitleRegex: RegExp; 11 | } 12 | 13 | const DEFAULT_SETTINGS: AdvancedPasteSettings = { 14 | scriptDir: "advpaste", 15 | turndown: { 16 | headingStyle: "atx", 17 | hr: "* * *", 18 | bulletListMarker: "-", 19 | codeBlockStyle: "fenced", 20 | fence: "```", 21 | emDelimiter: "*", 22 | strongDelimiter: "**", 23 | linkStyle: "inlined", 24 | linkReferenceStyle: "full", 25 | // preformattedCode: false, 26 | }, 27 | enhanceDefaultPaste: false, 28 | // autoLinkTitleRegex: 29 | }; 30 | 31 | export { DEFAULT_SETTINGS }; 32 | 33 | export class AdvancedPasteSettingTab extends PluginSettingTab { 34 | plugin: AdvancedPastePlugin; 35 | 36 | constructor(app: App, plugin: AdvancedPastePlugin) { 37 | super(app, plugin); 38 | this.plugin = plugin; 39 | } 40 | 41 | display(): void { 42 | const { containerEl } = this; 43 | 44 | containerEl.empty(); 45 | const warning = containerEl.createEl("h2", { 46 | text: "Never add untrusted scripts to the script directory BECAUSE IT MIGHT DESTROY YOUR VAULT OR WORSE!", 47 | }); 48 | warning.style.color = "red"; 49 | containerEl.createEl("h2", { 50 | text: "You need to disable and re-enable this plugin in order to apply the changes to the script directory", 51 | }); 52 | const hint = containerEl.createEl("h2", { 53 | text: "Please unbind Ctrl+V if you previously bind it to advanced paste's default paste command. Use the `Enhanced Ctrl+V` setting instead.", 54 | }); 55 | hint.style.color = "orange"; 56 | new Setting(containerEl) 57 | .setName("Enhanced Ctrl+V") 58 | .setDesc( 59 | "Enhance the default Ctrl+V behavior. You need to restart Obsidian for the changes to take effect." 60 | ) 61 | .addToggle((toggle) => { 62 | toggle.setValue(this.plugin.settings.enhanceDefaultPaste); 63 | toggle.onChange(async (value) => { 64 | this.plugin.settings.enhanceDefaultPaste = value; 65 | await this.plugin.saveSettings(); 66 | }); 67 | }); 68 | new Setting(containerEl) 69 | .setName("Script Directory") 70 | .setDesc("Directory for custom transforms.") 71 | .addText((text) => 72 | text 73 | .setPlaceholder("advpaste") 74 | .setValue(this.plugin.settings.scriptDir) 75 | .onChange(async (value) => { 76 | this.plugin.settings.scriptDir = value; 77 | await this.plugin.saveSettings(); 78 | }) 79 | ); 80 | // new Setting(containerEl) 81 | // .setName("Auto Link Title Regex") 82 | // .setDesc("The regex used in the auto link title plugin.") 83 | // .addText((text) => 84 | // text 85 | // .setValue(this.plugin.settings.autoLinkTitleRegex.source) 86 | // .onChange(async (value) => { 87 | // this.plugin.settings.autoLinkTitleRegex = new RegExp( 88 | // value 89 | // ); 90 | // await this.plugin.saveSettings(); 91 | // }) 92 | // ); 93 | containerEl.createEl("h2", { 94 | text: "Turndown Settings", 95 | }); 96 | containerEl.createEl("p", { 97 | text: "Turndown is a library that converts HTML to Markdown. Some transforms in this plugin use it. You can configure it here.", 98 | }); 99 | new Setting(containerEl) 100 | .setName("Heading Style") 101 | .setDesc("atx for `# heading`, setext for line under `heading`") 102 | .addDropdown((dropdown) => { 103 | dropdown.addOption("atx", "atx"); 104 | dropdown.addOption("setext", "setext"); 105 | dropdown.addOption("", "turndown default"); 106 | dropdown.setValue( 107 | this.plugin.settings.turndown.headingStyle ?? "" 108 | ); 109 | dropdown.onChange(async (value) => { 110 | this.plugin.settings.turndown.headingStyle = 111 | value === "" 112 | ? undefined 113 | : (value as TurndownService.Options["headingStyle"]); 114 | await this.plugin.saveSettings(); 115 | }); 116 | }); 117 | new Setting(containerEl) 118 | .setName("Bullet List Marker") 119 | .addText((text) => { 120 | text.setPlaceholder("-") 121 | .setValue( 122 | this.plugin.settings.turndown.bulletListMarker ?? "-" 123 | ) 124 | .onChange(async (value) => { 125 | this.plugin.settings.turndown.bulletListMarker = 126 | value as TurndownService.Options["bulletListMarker"]; 127 | await this.plugin.saveSettings(); 128 | }); 129 | }); 130 | new Setting(containerEl) 131 | .setName("Code Block Style") 132 | .addDropdown((dropdown) => { 133 | dropdown.addOption("fenced", "fenced"); 134 | dropdown.addOption("indented", "indented"); 135 | dropdown.addOption("", "turndown default"); 136 | dropdown.setValue( 137 | this.plugin.settings.turndown.codeBlockStyle ?? "" 138 | ); 139 | dropdown.onChange(async (value) => { 140 | this.plugin.settings.turndown.codeBlockStyle = 141 | value === "" 142 | ? undefined 143 | : (value as TurndownService.Options["codeBlockStyle"]); 144 | await this.plugin.saveSettings(); 145 | }); 146 | }); 147 | new Setting(containerEl) 148 | .setName("Code Block Fence Style") 149 | .addDropdown((dropdown) => { 150 | dropdown.addOption("```", "```"); 151 | dropdown.addOption("~~~", "~~~"); 152 | dropdown.addOption("", "turndown default"); 153 | dropdown.setValue(this.plugin.settings.turndown.fence ?? ""); 154 | dropdown.onChange(async (value) => { 155 | this.plugin.settings.turndown.fence = 156 | value === "" 157 | ? undefined 158 | : (value as TurndownService.Options["fence"]); 159 | await this.plugin.saveSettings(); 160 | }); 161 | }); 162 | new Setting(containerEl) 163 | .setName("Emphasis Style") 164 | .addDropdown((dropdown) => { 165 | dropdown.addOption("*", "asterisk"); 166 | dropdown.addOption("_", "underscore"); 167 | dropdown.addOption("", "turndown default"); 168 | dropdown.setValue( 169 | this.plugin.settings.turndown.emDelimiter ?? "" 170 | ); 171 | dropdown.onChange(async (value) => { 172 | this.plugin.settings.turndown.emDelimiter = 173 | value === "" 174 | ? undefined 175 | : (value as TurndownService.Options["emDelimiter"]); 176 | await this.plugin.saveSettings(); 177 | }); 178 | }); 179 | new Setting(containerEl) 180 | .setName("Strong Style") 181 | .addDropdown((dropdown) => { 182 | dropdown.addOption("**", "asterisk"); 183 | dropdown.addOption("__", "underscore"); 184 | dropdown.addOption("", "turndown default"); 185 | dropdown.setValue( 186 | this.plugin.settings.turndown.strongDelimiter ?? "" 187 | ); 188 | dropdown.onChange(async (value) => { 189 | this.plugin.settings.turndown.strongDelimiter = 190 | value === "" 191 | ? undefined 192 | : (value as TurndownService.Options["strongDelimiter"]); 193 | await this.plugin.saveSettings(); 194 | }); 195 | }); 196 | new Setting(containerEl) 197 | .setName("Link Style") 198 | .addDropdown((dropdown) => { 199 | dropdown.addOption("inlined", "inlined"); 200 | dropdown.addOption("referenced", "referenced"); 201 | dropdown.addOption("", "turndown default"); 202 | dropdown.setValue( 203 | this.plugin.settings.turndown.linkStyle ?? "" 204 | ); 205 | dropdown.onChange(async (value) => { 206 | this.plugin.settings.turndown.linkStyle = 207 | value === "" 208 | ? undefined 209 | : (value as TurndownService.Options["linkStyle"]); 210 | await this.plugin.saveSettings(); 211 | }); 212 | }); 213 | new Setting(containerEl) 214 | .setName("Link Reference Style") 215 | .addDropdown((dropdown) => { 216 | dropdown.addOption("full", "full"); 217 | dropdown.addOption("collapsed", "collapsed"); 218 | dropdown.addOption("shortcut", "shortcut"); 219 | dropdown.addOption("", "turndown default"); 220 | dropdown.setValue( 221 | this.plugin.settings.turndown.linkReferenceStyle ?? "" 222 | ); 223 | dropdown.onChange(async (value) => { 224 | this.plugin.settings.turndown.linkReferenceStyle = 225 | value === "" 226 | ? undefined 227 | : (value as TurndownService.Options["linkReferenceStyle"]); 228 | await this.plugin.saveSettings(); 229 | }); 230 | }); 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/transform.ts: -------------------------------------------------------------------------------- 1 | import { Editor, TFile } from "obsidian"; 2 | import TurndownService from "turndown"; 3 | export type TransformType = "text" | "blob"; 4 | 5 | interface Ok { 6 | kind: "ok"; 7 | value: TValue; 8 | } 9 | 10 | interface Err { 11 | kind: "err"; 12 | value: TError; 13 | } 14 | 15 | export function ok(value: TValue): Ok { 16 | return { kind: "ok", value }; 17 | } 18 | export function err(value: TValue): Err { 19 | return { kind: "err", value }; 20 | } 21 | 22 | export type TransformResult = Ok | Err; 23 | 24 | export interface TransformUtilsBase { 25 | turndown: TurndownService; 26 | mime: typeof import("mime"); 27 | _: typeof import("lodash"); 28 | moment: typeof import("moment"); 29 | remark: { 30 | unified: typeof import("unified").unified; 31 | remark: typeof import("remark").remark; 32 | remarkGfm: typeof import("remark-gfm").default; 33 | remarkMath: typeof import("remark-math").default; 34 | remarkParse: typeof import("remark-parse").default; 35 | remarkStringify: typeof import("remark-stringify").default; 36 | unistUtilVisit: typeof import("unist-util-visit"); 37 | unistUtilIs: typeof import("unist-util-is"); 38 | }; 39 | } 40 | 41 | export interface TransformUtils extends TransformUtilsBase { 42 | saveAttachment: ( 43 | name: string, 44 | ext: string, 45 | data: ArrayBuffer 46 | ) => Promise; 47 | editor: Editor; 48 | } 49 | 50 | export interface AdvpasteInternalParams { 51 | shouldHandleImagePasting: boolean; 52 | } 53 | 54 | export type TransformFunction = ( 55 | input: string | ClipboardItem, 56 | utils: TransformUtils, 57 | internal: AdvpasteInternalParams 58 | ) => TransformResult | string; 59 | 60 | export type TransformOutput = 61 | | string 62 | | Promise 63 | | TransformResult 64 | | Promise; 65 | 66 | export interface BlobTransform { 67 | type: "blob"; 68 | transform: ( 69 | input: ClipboardItem, 70 | utils: TransformUtils, 71 | internal: AdvpasteInternalParams 72 | ) => TransformOutput; 73 | } 74 | 75 | export interface TextTransform { 76 | type: "text"; 77 | transform: ( 78 | input: string, 79 | utils: TransformUtils, 80 | internal: AdvpasteInternalParams 81 | ) => TransformOutput; 82 | } 83 | 84 | export type Transform = BlobTransform | TextTransform; 85 | 86 | export interface Transforms { 87 | [id: string]: Transform; 88 | } 89 | -------------------------------------------------------------------------------- /src/transforms.ts: -------------------------------------------------------------------------------- 1 | import { ok, err, Transforms } from "./transform"; 2 | import { Vault } from "obsidian"; 3 | 4 | function privilegedWrapper({ vault }: { vault: Vault }): Transforms { 5 | return { 6 | smartJoin: { 7 | type: "text", 8 | transform(text: string) { 9 | return ok( 10 | text 11 | .split("\n") 12 | .map((x) => x.trim()) 13 | .reduce((acc, cur, idx) => { 14 | return acc.endsWith("-") 15 | ? `${acc.slice(0, -1)}${cur}` 16 | : cur !== "" 17 | ? `${acc} ${cur}` 18 | : `${acc}\n`; 19 | }) 20 | ); 21 | }, 22 | }, 23 | joinLines: { 24 | type: "text", 25 | transform(text: string) { 26 | return ok(text.split("\n").join("")); 27 | }, 28 | }, 29 | removeBlankLines: { 30 | type: "text", 31 | transform(text) { 32 | return ok( 33 | text 34 | .split("\n") 35 | .filter((x) => x.trim() !== "") 36 | .join("\n") 37 | ); 38 | }, 39 | }, 40 | rawHTML: { 41 | type: "blob", 42 | async transform(input) { 43 | if (!input.types.includes("text/html")) { 44 | return err("No html found in clipboard!"); 45 | } 46 | const html = await input.getType("text/html"); 47 | return ok(await html.text()); 48 | }, 49 | }, 50 | }; 51 | } 52 | 53 | export default privilegedWrapper; 54 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "module": "ESNext", 7 | "target": "ES6", 8 | "allowJs": true, 9 | "noImplicitAny": true, 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "isolatedModules": true, 13 | "strictNullChecks": true, 14 | "lib": ["DOM", "ES5", "ES6", "ES7"], 15 | "allowSyntheticDefaultImports": true 16 | }, 17 | "include": ["**/*.ts", "src/remark-whitespace-reducer.js"] 18 | } 19 | -------------------------------------------------------------------------------- /version-bump.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | 3 | const targetVersion = process.env.npm_package_version; 4 | 5 | // read minAppVersion from manifest.json and bump version to target version 6 | let manifest = JSON.parse(readFileSync("manifest.json", "utf8")); 7 | const { minAppVersion } = manifest; 8 | manifest.version = targetVersion; 9 | writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); 10 | 11 | // update versions.json with target version and minAppVersion from manifest.json 12 | let versions = JSON.parse(readFileSync("versions.json", "utf8")); 13 | versions[targetVersion] = minAppVersion; 14 | writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); 15 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.0.0": "0.15.0", 3 | "1.1.0": "0.15.0", 4 | "2.0.0": "0.15.0", 5 | "2.0.1": "0.15.0", 6 | "2.0.2": "0.15.0", 7 | "2.0.3": "0.15.0", 8 | "2.1.0": "0.15.0", 9 | "2.2.0": "0.15.0", 10 | "2.2.1": "0.15.0", 11 | "2.3.0": "0.15.0", 12 | "2.3.1": "0.15.0", 13 | "2.4.0": "0.15.0", 14 | "2.5.0": "0.15.0", 15 | "2.5.1": "0.15.0", 16 | "2.6.0": "0.15.0", 17 | "2.7.0": "0.15.0" 18 | } --------------------------------------------------------------------------------