├── .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 | [](https://www.paypal.com/paypalme/tokxxt)
7 | 
8 | 
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 | 
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 | }
--------------------------------------------------------------------------------