├── .gitattributes ├── .gitconfig ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE ├── README.ja.md ├── README.md ├── README.zh-cn.md ├── README.zh-tw.md ├── asserts ├── crlf.js ├── icon.png ├── linux.sh ├── mac.applescript ├── pc.ps1 ├── rtf.ps1 ├── rtf.sh └── sample.js ├── package-lock.json ├── package.json ├── package.nls.json ├── package.nls.zh-cn.json ├── package.nls.zh-tw.json ├── pnpm-lock.yaml ├── src ├── extension.ts ├── i18n │ ├── index.ts │ ├── locale.ts │ ├── locale.zh-cn.ts │ └── locale.zh-tw.ts ├── lib │ ├── azure.ts │ ├── cloudflare.ts │ ├── cloudinary.ts │ ├── coding.ts │ ├── dataurl.ts │ ├── define.ts │ ├── github.d.ts │ ├── github.ts │ ├── imgur.ts │ ├── local.ts │ ├── qiniu.ts │ ├── s3.ts │ ├── sftp.ts │ ├── sm.ms.ts │ ├── upload.d.ts │ ├── uploads.ts │ ├── upyun.ts │ └── utils.ts └── test │ ├── extension.test.ts │ └── index.ts ├── tsconfig.json ├── tslint.json └── typings.d.ts /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behavior to automatically normalize line endings. 2 | * text=auto 3 | 4 | -------------------------------------------------------------------------------- /.gitconfig: -------------------------------------------------------------------------------- 1 | [core] 2 | autocrlf = false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .vscode-test/ 4 | *.vsix 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "runtimeExecutable": "${execPath}", 13 | "args": [ 14 | "--extensionDevelopmentPath=${workspaceFolder}" 15 | ], 16 | "outFiles": [ 17 | "${workspaceFolder}/out/**/*.js" 18 | ], 19 | "preLaunchTask": "npm: watch" 20 | }, 21 | { 22 | "name": "Extension Tests", 23 | "type": "extensionHost", 24 | "request": "launch", 25 | "runtimeExecutable": "${execPath}", 26 | "args": [ 27 | "--extensionDevelopmentPath=${workspaceFolder}", 28 | "--extensionTestsPath=${workspaceFolder}/out/test" 29 | ], 30 | "outFiles": [ 31 | "${workspaceFolder}/out/test/**/*.js" 32 | ], 33 | "preLaunchTask": "npm: watch" 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off", 11 | "GitCommitPlugin.ShowEmoji": true, 12 | "editor.tabSize": 4, 13 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | src/** 5 | .gitignore 6 | vsc-extension-quickstart.md 7 | **/tsconfig.json 8 | **/tslint.json 9 | **/*.map 10 | **/*.ts 11 | asserts/crlf.js -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to the "markdown-image" extension will be documented in this file. 3 | 4 | ## [1.1.49] - 2025-02-13 5 | 6 | ### Fixed 7 | - Fixed bug of format code by DIY. 8 | 9 | ## [1.1.48] - 2025-02-11 10 | 11 | ### Fixed 12 | - Fixed transparent background turning black when copy png Image on paste. 13 | 14 | ## [1.1.47] - 2025-02-08 15 | 16 | ### Fixed 17 | - Fixed multiple files could not be copied and pasted on Mac. 18 | - Fixed pasted rich text on Windows had garbled characters. 19 | 20 | ## [1.1.46] - 2025-02-07 21 | 22 | ### Fixed 23 | - Reverted changes from version `1.1.45` to fix the issue where the background of a copied PNG image turns black when pasted. 24 | 25 | ## [1.1.45] - 2025-02-07 26 | 27 | ### Fixed 28 | - Fixed transparent background turning black when copy png Image on paste. 29 | 30 | ### Updated 31 | - Support auto activate the extension when the markdown file is opened. 32 | 33 | ## [1.1.44] - 2025-02-06 34 | 35 | ### Fixed 36 | - Fixed the issue where disabling Encode caused addresses with spaces to fail to preview correctly. 37 | 38 | ### Added 39 | - Added a new setting `markdown-image.base.codeFormat` to support setting the image code format. 40 | 41 | ### Updated 42 | - Updated the setting `markdown-image.base.codeType` to add the `DIY` option, supporting custom image code formats. 43 | - Updated the setting `markdown-image.base.altFormat` to add support for the `${prompt}` variable. 44 | 45 | ## [1.1.43] - 2024-07-08 46 | 47 | ### Fixed 48 | - Fixed the issue where the `path` variable still had incomplete backslashes replaced. 49 | 50 | ## [1.1.42] - 2024-07-01 51 | 52 | ### Fixed 53 | - Fixed the issue where the `path` variable is not in Unix format when running at Microsoft Windows. 54 | 55 | ## [1.1.41] - 2024-03-14 56 | 57 | ### Added 58 | - Added a new setting item `markdown-image.s3.config` to config s3 client. 59 | 60 | ### Fixed 61 | - Fixed the s3 cdn url generate wrong. 62 | 63 | ## [1.1.40] - 2024-03-11 64 | 65 | ### Added 66 | - Added a new setting item `markdown-image.s3.cdn` to set the s3 cdn url. 67 | 68 | ## [1.1.39] - 2024-03-06 69 | 70 | ### Added 71 | - Add `Content-Type` to s3 upload method. 72 | 73 | ## [1.1.38] - 2024-02-24 74 | 75 | ### Added 76 | - Supports Azure Blob Storage Passwordless authentication method. 77 | 78 | ## [1.1.37] - 2024-02-17 79 | 80 | ### Added 81 | - Add support for Azure Storage. 82 | 83 | ## [1.1.36] - 2023-08-22 84 | 85 | ### Fixed 86 | - Fixed the issue where the placeholder `filename` was not using the value calculated by `fileNameFormat`. 87 | 88 | ## [1.1.35] - 2023-06-30 89 | 90 | ### Fixed 91 | - Fixed the issue where only the first occurrence of the same variable for `fileNameFormat` was effective. 92 | 93 | ## [1.1.34] - 2023-06-28 94 | 95 | ### Added 96 | - Added a new setting item `markdown-image.base.altFormat` to set the placeholder for image code. 97 | 98 | ## [1.1.33] - 2023-06-04 99 | 100 | ### Added 101 | - Added a new setting item `markdown-image.base.uploadMethods` to support concurrent uploads with multiple upload methods. 102 | - Added a new setting item `markdown-image.base.fileFormat` to set the file format for saving clipboard images. 103 | - Supported pasting images into unconventional editing windows. 104 | 105 | ### Deprecated 106 | - Removed the deprecated Coding image upload method. 107 | 108 | ## [1.1.30] - 2023-05-15 109 | 110 | ### Added 111 | - Added new setting `markdown-image.github.httpProxy` to settting GitHub http proxy. 112 | 113 | ## [1.1.29] - 2023-05-02 114 | 115 | ### Fixed 116 | - Fix failed to get image from clipboard on deepin. 117 | 118 | ## [1.1.28] - 2023-04-10 119 | 120 | ### Added 121 | - Support group setting. 122 | - Add support for remote mode. 123 | - Add SFTP upload method. 124 | - Includes the following new settings: 125 | - `markdown-image.sftp.host` 126 | - `markdown-image.sftp.port` 127 | - `markdown-image.sftp.username` 128 | - `markdown-image.sftp.password` 129 | - `markdown-image.sftp.privateKeyPath` 130 | - `markdown-image.sftp.path` 131 | - `markdown-image.sftp.referencePath` 132 | 133 | ## [1.1.27] - 2023-04-07 134 | 135 | ### Added 136 | - Add support for S3 compatible providers. 137 | - Includes the following new settings: 138 | - `markdown-image.s3.endpoint` 139 | - `markdown-image.s3.region` 140 | - `markdown-image.s3.bucketName` 141 | - `markdown-image.s3.accessKeyId` 142 | - `markdown-image.s3.secretAccessKey` 143 | 144 | ## [1.1.26] - 2022-12-09 145 | ### Added 146 | - Add support for Upyun. 147 | - Includes the following new settings: 148 | * `markdown-image.upyun.bucket` 149 | * `markdown-image.upyun.domain` 150 | * `markdown-image.upyun.operator` 151 | * `markdown-image.upyun.password` 152 | * `markdown-image.upyun.path` 153 | * `markdown-image.upyun.link` 154 | 155 | ## [1.1.25] - 2022-12-09 156 | ### Fixed 157 | - Fixed image url of GitHub CDN encode wrong when enable `markdown-image.base.urlEncode` . 158 | 159 | ## [1.1.24] - 2022-12-06 160 | ### Fixed 161 | - Fixed some bug of the option `markdown-image.local.referencePath`. 162 | - Fixed the `alt` count of image restart when restarted extension. 163 | 164 | ## [1.1.23] - 2022-11-30 165 | ### Added 166 | - Added new setting `markdown-image.github.cdn` to settting GitHub CDN Format Address. 167 | 168 | ## [1.1.22] - 2022-05-10 169 | ### Fixed 170 | - Fixed upload wrong path in GitHub Mode. 171 | 172 | ## [1.1.21] - 2022-05-09 173 | ### Fixed 174 | - Fixed the problem of uploading to GitHub when the file path contains Chinese. 175 | 176 | ## [1.1.20] - 2022-04-08 177 | - Fixed bug of the setting start with `/` on `local.referencePath`. 178 | 179 | ## [1.1.19] - 2022-02-23 180 | ### Fixed 181 | - Fixed local mode always save to disk root path. 182 | 183 | ## [1.1.18] - 2022-02-22 184 | ### Fixed 185 | - Fixed local mode can't use absolute path. 186 | 187 | ## [1.1.17] - 2022-01-03 188 | ### Added 189 | - Add support for Cloudflare Images. 190 | - Includes the following new settings: 191 | * `markdown-image.cloudflare.accountId` 192 | * `markdown-image.cloudflare.apiToken` 193 | 194 | ## [1.1.16] - 2021-12-06 195 | ### Added 196 | - Added to support upload image to github repository. 197 | 198 | ## [1.1.15] - 2021-11-24 199 | ### Added 200 | - Added file format variable `prompt`. Makes it possible to enter a custom name through an input prompt when pasting the image. 201 | 202 | ## [1.1.14] - 2021-09-06 203 | ### Updated 204 | - Update Coding Picbed Package to fixed upload to coding error. 205 | 206 | ## [1.1.13] - 2021-08-20 207 | ### Added 208 | - Added new setting `markdown-image.local.referencePath` to support DIY reference path in markdown file. 209 | 210 | ## [1.1.12] - 2021-08-04 211 | ### Added 212 | - Added to support paste image in Jupyter file. 213 | 214 | ## [1.1.11] - 2021-07-08 215 | ### Fixed 216 | - Updated Cloudinary CDN to use the `markdown-image.base.fileNameFormat` setting. The extension will check for existing files and will prompt to overwrite if necessary. 217 | 218 | ## [1.1.10] - 2021-07-05 219 | ### Added 220 | - Added support for Cloudinary CDN 221 | - Includes the following new settings: 222 | * `markdown-image.cloudinary.cloudName` 223 | * `markdown-image.cloudinary.apiKey` 224 | * `markdown-image.cloudinary.apiSecret` 225 | * `markdown-image.cloudinary.folder` 226 | 227 | ## [1.1.9] - 2021-05-17 228 | ### Added 229 | - Added setting options `markdown-image.base.codeType` and `markdown-image.base.imageWidth` use to set the maximum image width. 230 | 231 | ## [1.1.8] - 2021-04-25 232 | ### Fixed 233 | - Fixed vscode caches the `DIY` path code, causing a question that cannot be changed immediately. 234 | 235 | ## [1.1.7] - 2021-04-16 236 | ### Added 237 | - Added a option to switch whether url encode. 238 | 239 | ## [1.1.6] - 2021-04-13 240 | ### Fixed 241 | - Fixed extension log level. 242 | ### Changed 243 | - Update action after replace file. 244 | 245 | ## [1.1.5] - 2021-03-29 246 | ### Fixed 247 | - Fixed `Data URL` Setting Description. 248 | 249 | ## [1.1.4] - 2021-03-26 250 | ### Added 251 | - Added upload method `Data URL`. 252 | ### Fixed 253 | - Fixed paste multiple documents invalid. 254 | 255 | ## [1.1.3] - 2021-03-26 256 | ### Fixed 257 | - Fixed the filename format variable `${path}` were uploaded to the `Coding` failure in the Windows. 258 | 259 | ## [1.1.2] - 2021-03-26 260 | ### Fixed 261 | - Fixed the path contains Chinese prompts cannot be found when pasting the copied picture again. 😂 262 | 263 | ## [1.1.1] - 2021-03-16 264 | ### Fixed 265 | - Fixed the path contains Chinese prompts cannot be found when pasting the copied picture. 266 | 267 | ## [1.1.0] - 2021-02-04 268 | ### Added 269 | - Added Beta feature, support paste rich text. (Only supports Windows) 270 | 271 | ## [1.0.14] - 2021-01-29 272 | ### Fixed 273 | - Fixed incompatibility with Windows 7. 274 | 275 | ## [1.0.13] - 2021-01-29 276 | ### Fixed 277 | - Fixed getting the path resolution error of the clipboard picture in Windows. 278 | 279 | ## [1.0.12] - 2021-01-21 280 | ### Added 281 | - Added file name variable `${path}`: "The path of the Markdown file being edited relative to the root directory." 282 | 283 | ## [1.0.11] - 2021-01-06 284 | ### Fixed 285 | - Fixed api url of `sm.ms`. 286 | - Fixed the file name with spaces cannot be found in Linux. 287 | ### Added 288 | - Added prompt without token in `sm.ms`. 289 | 290 | ## [1.0.10] - 2020-12-06 291 | ### Fixed 292 | - Fix the variable `$mdname` does not remove the extension name of `md`. 293 | 294 | ## [1.0.9] - 2020-10-19 295 | ### Fixed 296 | - Fixed the problem of getting wrong date and hour in filename formatting. 297 | 298 | ## [1.0.8] - 2020-07-22 299 | ### Added 300 | - Added mdx support. 301 | 302 | ## [1.0.7] - 2020-07-21 303 | ### Fixed 304 | - Fixed launch extension home page failed at MacOS and Linux. 305 | - Fixed the colon of the picture address is incorrectly encode. 306 | 307 | ## [1.0.6] - 2020-07-16 308 | ### Fixed 309 | - Fixed the date variable has not consider the time zone. 310 | 311 | ## [1.0.5] - 2020-07-01 312 | ### Fixed 313 | - Fixed file name include space could not preview. 314 | - Fixed random variable not work. 315 | 316 | ## [1.0.4] - 2020-05-18 317 | ### Updated 318 | - Update sponsored links. 319 | 320 | ## [1.0.3] - 2020-05-15 321 | ### Added 322 | - Add sponsored links. 323 | 324 | ## [1.0.2] - 2020-05-09 325 | ### Fixed 326 | - Fixed replace notice bug. 327 | 328 | ## [1.0.1] - 2020-05-09 329 | ### Fixed 330 | - Fixed Readme. 331 | 332 | ## [1.0.0] - 2020-05-09 333 | - Finish First Version. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Hancel Lin 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.ja.md: -------------------------------------------------------------------------------- 1 | [English Readme](README.md) / [简体中文说明](README.zh-cn.md) / [繁體中文說明](README.zh-tw.md) / 日本語説明 2 | 3 | # Markdown Image 4 | 5 | Markdownで画像を簡単に挿入できる拡張機能です。ローカルまたはサードパーティのCDNサービスに画像を保存することをサポートしています。 6 | 7 | ❤ [Sponsor me](https://www.paypal.me/imlinhanchao) / [赞助开发者](http://sponsor.hancel.org) 8 | 9 | ## Features 10 | 11 | 1. 画像ファイルのコピーやスクリーンショットのペーストが可能。ショートカットキーは `Alt + Shift + V`、または右クリックメニューの「Paste Image」。 12 | 2. 自動的にMarkdownコードを生成して挿入します。 13 | 3. `Imgur`、`Qiniu`、`SM.MS`、`Cloudflare`、`Cloudinary`、`S3`、`Azure Storage`などのCDNサービスをサポートし、設定可能です。デフォルトはローカルで、Markdownファイルがあるフォルダを開く必要があります。 14 | 4. 独自コードを設定して画像をアップロードすることも可能です。 15 | 5. Windows、MacOS、Linuxをサポートしています。 16 | 17 | ## Requirements 18 | 19 | Linux環境では xclip をインストールする必要があります。 20 | 21 | Ubuntu: 22 | 23 | ```bash 24 | sudo apt install xclip 25 | ``` 26 | 27 | CentOS: 28 | 29 | ```bash 30 | sudo yum install epel-release.noarch 31 | sudo yum install xclip 32 | ``` 33 | 34 | ## Notice 35 | 36 | Remote Modeで使用する場合は、次のように `remote.extensionKind` を設定してください: 37 | 38 | ```json 39 | "remote.extensionKind": { 40 | "hancel.markdown-image": [ 41 | "ui" 42 | ] 43 | } 44 | ``` 45 | 46 | また、リモート環境の作業スペースに画像を保存したい場合は、`SFTP` のアップロード方式を使用する必要があります。`Local` はRemote Modeでは使用できません。 47 | 48 | ## Extension Settings 49 | 50 | ### Base Settings 51 | 52 | * `markdown-image.base.uploadMethod`: 画像のアップロード方法を指定します。ローカルまたは他の画像CDNサービスへアップロードします。 53 | * `markdown-image.base.uploadMethods`: 複数のアップロード方式を並行して使うための設定です。ここに設定したアップロード結果はMarkdownファイルに挿入されません。 54 | * `markdown-image.base.fileNameFormat`: アップロード時のファイル名形式。`Imgur` と `SM.MS` ではサポートされていません。利用できる変数については設定項目を参照してください。 55 | * `markdown-image.base.codeType`: 画像コードの形式。`` タグまたはMarkdown記法を設定できます。 56 | * `markdown-image.base.codeFormat`: `markdown-image.base.codeType` を `DIY` に設定した場合の挿入コードの書式。 57 | * `markdown-image.base.imageWidth`: 画像の最大幅。画像がこの幅より大きい場合は、この値に合わせられます。0に設定するとサイズ変更を行いません。 58 | * `markdown-image.base.urlEncode`: 画像URLをURLエンコードするかどうかを設定します。 59 | 60 | ### Local Settings 61 | 62 | * `markdown-image.local.path`: ローカルに保存する画像ディレクトリ(存在しない場合は自動で作成)。 63 | * `markdown-image.local.referencePath`: Markdown内での参照パス形式(ファイル名は含まない)。空の場合は相対パスを使用します。ここで `#markdown-image.base.fileNameFormat#` の変数が使用可能です。例: `/images/${YY}-${MM}/` 64 | 65 | ### GitHub Settings 66 | 67 | * `markdown-image.github.path`: リポジトリ内に画像をアップロードするディレクトリ(存在しない場合は自動で作成)。リポジトリは事前に初期化しておく必要があります。 68 | * `markdown-image.github.token`: GitHubの個人[アクセストークン](https://github.com/settings/tokens)。 69 | * `markdown-image.github.repository`: GitHubリポジトリ。例: `https://github.com/username/repository` 70 | * `markdown-image.github.branch`: 画像を保存するGitHubリポジトリのブランチ。 71 | * `markdown-image.github.cdn`: 使用するGitHub CDNアドレスの形式。`${username}` は `markdown-image.github.repository` のユーザー名、`${repository}` はリポジトリ名、`${branch}` は `markdown-image.github.branch` の値、`${filepath}` はリポジトリ内のアップロードパスを表します。 72 | * `markdown-image.github.httpProxy`: HTTPプロキシを介してGitHubに接続する場合の設定。 73 | 74 | ### Imgur Settings 75 | 76 | * `markdown-image.imgur.clientId`: Imgurで登録したClient ID。[こちら](https://api.imgur.com/oauth2/addclient)で登録可能です。 77 | * `markdown-image.imgur.httpProxy`: HTTPプロキシを介してImgurに接続する場合の設定。 78 | 79 | ### SM.MS Settings 80 | 81 | * `markdown-image.sm_ms.token`: SM.MSのAPIトークン(オプション)。アカウント登録後、[API Token](https://sm.ms/home/apitoken)ページで秘密トークンを生成可能です。 82 | 83 | ### Qiniu Settings 84 | 85 | * `markdown-image.qiniu.accessKey`: アカウントのAccess Key。 86 | * `markdown-image.qiniu.secretKey`: アカウントのSecret Key。 87 | * `markdown-image.qiniu.bucket`: ストレージ名。 88 | * `markdown-image.qiniu.domain`: ストレージのバインドドメイン。 89 | * `markdown-image.qiniu.zone`: ストレージのゾーン。 90 | 91 | ### Upyun Settings 92 | 93 | * `markdown-image.upyun.bucket`: アップロードするストレージ名。 94 | * `markdown-image.upyun.domain`: ストレージ名にバインドされたドメイン。 95 | * `markdown-image.upyun.operator`: Upyunのオペレーター名。 96 | * `markdown-image.upyun.password`: Upyunオペレーターのパスワード。 97 | * `markdown-image.upyun.path`: 画像を保存するパス。 98 | * `markdown-image.upyun.link`: Upyunに接続するリンク。 99 | 100 | ### Cloudinary Settings 101 | 102 | これらの値はCloudinaryのダッシュボードから確認できます 103 | 104 | * `markdown-image.cloudinary.cloudName`: アカウント名。 105 | * `markdown-image.cloudinary.apiKey`: アカウントのAPIキー。 106 | * `markdown-image.cloudinary.apiSecret`: アカウントのAPIシークレット。 107 | * `markdown-image.cloudinary.folder`: 画像をアップロードするフォルダ。 108 | 109 | ### Cloudflare Settings 110 | 111 | これらの値はCloudflareのダッシュボードで確認できます 112 | 113 | * `markdown-image.cloudflare.accountId`: アカウントID。 114 | * `markdown-image.cloudflare.apiToken`: Cloudflare ImagesのAPIトークン。 115 | 116 | ### S3 Settings 117 | 118 | これらの値はS3サービスプロバイダのダッシュボードで確認できます 119 | 120 | * `markdown-image.s3.endpoint`: S3 APIのエンドポイント。バケット設定から取得可能です。 121 | * `markdown-image.s3.region`: S3バケットのリージョン。バケット設定から取得可能です。 122 | * `markdown-image.s3.bucketName`: 画像をアップロードするS3バケット名。バケットへのアクセスはパブリックである必要があります。 123 | * `markdown-image.s3.accessKeyId`: S3 APIのAccess Key ID。 124 | * `markdown-image.s3.secretAccessKey`: S3のシークレットアクセスキー。 125 | * `markdown-image.s3.cdn`: S3のCDNアドレス。`${bucket}` `${region}` `${pathname}` `${filepath}`などの変数を使用できます。例: `https://${bucket}.${region}.s3.amazonaws.com/${pathname}/${filepath}` 126 | 127 | ### SFTP Settings 128 | 129 | * `markdown-image.sftp.host`: リモートサーバーのホスト名。 130 | * `markdown-image.sftp.port`: リモートサーバーのSSHポート。 131 | * `markdown-image.sftp.username`: リモートサーバーのユーザー名。 132 | * `markdown-image.sftp.password`: リモートサーバーのパスワード。 133 | * `markdown-image.sftp.privateKeyPath`: リモートサーバーにアクセスするための秘密鍵パス。 134 | * `markdown-image.sftp.path`: リモート環境に保存する画像ディレクトリ(存在しない場合は自動で作成)。注意: ここでは変数は使用できません。`#markdown-image.base.fileNameFormat#` の変数はここでは使えません。 135 | * `markdown-image.sftp.referencePath`: Markdown内での参照パス形式(ファイル名は含まない)。空の場合は相対パスを使用。`#markdown-image.base.fileNameFormat#` の変数が使用可能です。例: `/images/${YY}-${MM}/` 136 | 137 | ### Azure Storage Settings 138 | 139 | * `markdown-image.azure.authenticationMethod`: Azure Blob Storageアカウントに対する認証方法を指定します。デフォルトは `Passwordless` です。[詳細はこちら](https://learn.microsoft.com/en-us/azure/storage/blobs/storage-quickstart-blobs-nodejs?tabs=managed-identity%2Croles-azure-portal%2Csign-in-azure-cli#authenticate-to-azure-and-authorize-access-to-blob-data)。 140 | * `markdown-image.azure.accountName`: Azure Blob Storageアカウント名。 141 | * `markdown-image.azure.connectionString`: Azure Storageアカウントの接続文字列。 142 | * `markdown-image.azure.container`: 画像をアップロードするコンテナー名。 143 | 144 | ### DIY Settings 145 | 146 | * `markdown-image.DIY.path`: 独自に記述するコードのパス。ここには `async function (filePath:string, savePath:string, markdownPath:string):string` をエクスポートする関数を含む必要があります。 147 | 例: 148 | ```javascript 149 | const path = require("path"); 150 | module.exports = async function (filePath, savePath, markdownPath) { 151 | // 画像アクセス用のリンクを返す 152 | return path.relative(path.dirname(markdownPath), filePath); 153 | }; 154 | ``` 155 | 156 | ### Others 157 | 158 | * [GitHub](https://github.com/imlinhanchao/vsc-markdown-image) 159 | * [VSCode Market](https://marketplace.visualstudio.com/items?itemName=hancel.markdown-image) 160 | * アイコン: [www.flaticon.com](https://www.flaticon.com/) の [Good Ware](https://www.flaticon.com/authors/good-ware) より作成 161 | 162 | **Enjoy!** 163 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | English Readme / [简体中文说明](README.zh-cn.md) / [繁體中文說明](README.zh-tw.md) / [日本語説明](README.ja.md) 2 | 3 | # Markdown Image 4 | 5 | An extension for conveniently inserting pictures in Markdown, which supports storing pictures in local or third-party CDN service. 6 | 7 | ❤ [Sponsor me](https://www.paypal.me/imlinhanchao) / [赞助开发者](http://sponsor.hancel.org) 8 | 9 | ## Features 10 | 11 | 1. Copy image files or paste screenshots, Shortcut key `Alt + Shift + V`, or right-click menu `Paste Image`. 12 | 2. Automatically generate Markdown code insertion. 13 | 3. Configurable to support `Imgur`, `Qiniu`, `SM.MS`, `Cloudflare`, `Cloudinary`, `S3`, `Azure Storage` and other CDN service. The default is local, you need to open the folder where the Markdown file is located. 14 | 4. You can also customize the code to upload pictures. 15 | 5. Support Windows, MacOS, Linux. 16 | 17 | ## Requirements 18 | 19 | Linux users must install xclip. 20 | 21 | Ubuntu 22 | 23 | ```bash 24 | sudo apt install xclip 25 | ``` 26 | 27 | CentOS 28 | 29 | ```bash 30 | sudo yum install epel-release.noarch 31 | sudo yum install xclip 32 | ``` 33 | 34 | ## Notice 35 | 36 | If you want to use in the Remote Mode, please set `remote.extensionKind` like this: 37 | 38 | ```json 39 | "remote.extensionKind": { 40 | "hancel.markdown-image": [ 41 | "ui" 42 | ] 43 | } 44 | ``` 45 | 46 | And if you want to save image in your remote workplace, you must use `SFTP` upload method. `Local` couldn't use in Remote Mode. 47 | 48 | ## Extension Settings 49 | 50 | ### Base Settings 51 | 52 | - `markdown-image.base.uploadMethod`: Method to upload pictures. To the local or another picture CDN service. 53 | - `markdown-image.base.uploadMethods`: The upload method for parallel upload. The upload results of the upload method set here will not be inserted into the Markdown file. 54 | - `markdown-image.base.fileNameFormat`: The filename format for upload. Not Support in `Imgur` and `SM.MS`. You can use some variables. You can find more in setting. 55 | - `markdown-image.base.codeType`: The type of image code, you can set to `` tag or markdown 56 | - `markdown-image.base.codeFormat`: Insert code format, effective when `markdown-image.base.codeType` is set to `DIY`. 57 | - `markdown-image.base.imageWidth`: The maximum width of the image, if the image is greater than this width, the width is set to this value. Set to 0 means not change. 58 | - `markdown-image.base.urlEncode`: Whether URL encode for the url of image. 59 | 60 | ### Local Settings 61 | 62 | - `markdown-image.local.path`: Picture storage directory that in the local (automatically created if it does not exist). 63 | - `markdown-image.local.referencePath`: The reference path format in markdown(not include file name). Empty means use relative path. You can use variable of `#markdown-image.base.fileNameFormat#` in here. For example: `/images/${YY}-${MM}/` 64 | 65 | ### GitHub Settings 66 | 67 | - `markdown-image.github.path`: Picture upload directory that in the repository (automatically created if it does not exist). The repository must initialization first. 68 | - `markdown-image.github.token`: GitHub person [access token](https://github.com/settings/tokens). 69 | - `markdown-image.github.repository`: GitHub repository, for example: `https://github.com/username/repository` 70 | - `markdown-image.github.branch`: GitHub repository branch to save. 71 | - `markdown-image.github.cdn`: The github cdn address format to be used, `${username}` is the username of `markdown-image.github.repository`, and `${repository}` is the repository name. `${branch}` is the value of `markdown-image.github.branch`. `${filepath}` is the upload path in repository. 72 | - `markdown-image.github.httpProxy`: Connect to Github via http proxy. 73 | 74 | ### Imgur Settings 75 | 76 | - `markdown-image.imgur.clientId`: The client id registered with imgur. You can registed it at [here](https://api.imgur.com/oauth2/addclient). 77 | - `markdown-image.imgur.httpProxy`: Connect to Imgur via http proxy. 78 | 79 | ### SM.MS Settings 80 | 81 | - `markdown-image.sm_ms.token`: SM.MS API token (Options). You can register an account and then visit [API Token](https://sm.ms/home/apitoken) Page to generate secret token. 82 | 83 | ### Qiniu Settings 84 | 85 | - `markdown-image.qiniu.accessKey`: The Access Key of account. 86 | - `markdown-image.qiniu.secretKey`: The Secret Key of account. 87 | - `markdown-image.qiniu.bucket`: The storage name. 88 | - `markdown-image.qiniu.domain`: Bound domain name of storage. 89 | - `markdown-image.qiniu.zone`: Zone of storage. 90 | 91 | ### Upyun Settings 92 | 93 | - `markdown-image.upyun.bucket`: Storge name of upload. 94 | - `markdown-image.upyun.domain`: Domain bind with storge name. 95 | - `markdown-image.upyun.operator`: Operator of upyun. 96 | - `markdown-image.upyun.password`: Password of upyun operator. 97 | - `markdown-image.upyun.path`: The path that img store. 98 | - `markdown-image.upyun.link`: The link that connect to upyun. 99 | 100 | ### Cloudinary Settings 101 | 102 | These values can be found on your Cloudinary Dashboard 103 | 104 | - `markdown-image.cloudinary.cloudName`: Your user account name. 105 | - `markdown-image.cloudinary.apiKey`: API key for your account. 106 | - `markdown-image.cloudinary.apiSecret`: API secret for your account. 107 | - `markdown-image.cloudinary.folder`: Folder to upload the image to. 108 | 109 | ### Cloudflare Settings 110 | 111 | These values can be found on your Cloudflare dashboard 112 | 113 | - `markdown-image.cloudflare.accountId`: Your account ID. 114 | - `markdown-image.cloudflare.apiToken`: Cloudflare Images API token. 115 | 116 | ### S3 Settings 117 | 118 | These values can be found in your S3 service provider dashboard 119 | 120 | - `markdown-image.s3.endpoint`: The endpoint for S3 API, can be obtained from bucket setting. 121 | - `markdown-image.s3.region`: The region for the S3 bucket, can be obtained from bucket setting. 122 | - `markdown-image.s3.bucketName`: The name of the S3 bucket to upload images to. Access to the bucket should be public. 123 | - `markdown-image.s3.accessKeyId`: Your S3 API access key ID. 124 | - `markdown-image.s3.secretAccessKey`: Your S3 secret access key. 125 | - `markdown-image.s3.cdn`: Your S3 CDN Url. You can use variable `${bucket}` `${region}` `${pathname}` and `${filepath}`. For example: `https://${bucket}.${region}.s3.amazonaws.com/${pathname}/${filepath}`. 126 | 127 | ### SFTP Settings 128 | 129 | - `markdown-image.sftp.host`: The host of the remote server. 130 | - `markdown-image.sftp.port`: The ssh port of the remote server. 131 | - `markdown-image.sftp.username`: The username of the remote server. 132 | - `markdown-image.sftp.password`: The password of the remote server. 133 | - `markdown-image.sftp.privateKeyPath`: The private key path of the remote server. 134 | - `markdown-image.sftp.path`: Picture storage directory that in the remote (automatically created if it does not exist). Notice: You can't use variable in here. You can use variable in `#markdown-image.base.fileNameFormat#`. 135 | - `markdown-image.sftp.referencePath`: The reference path format in markdown(not include file name). Empty means use relative path. You can use variable of `#markdown-image.base.fileNameFormat#` in here. For example: `/images/${YY}-${MM}/` 136 | 137 | ### Azure Storage Settings 138 | - `markdown-image.azure.authenticationMethod`: The authentication method to use for the Azure Blob Storage account. The default is `Passwordless`. You can obtain more information from [here](https://learn.microsoft.com/en-us/azure/storage/blobs/storage-quickstart-blobs-nodejs?tabs=managed-identity%2Croles-azure-portal%2Csign-in-azure-cli#authenticate-to-azure-and-authorize-access-to-blob-data). 139 | - `markdown-image.azure.accountName`: Your Azure Blob Storage account. 140 | - `markdown-image.azure.connectionString`: The connection string of the Azure Storage account. 141 | - `markdown-image.azure.container`: The name of the container to upload images to. 142 | 143 | ### DIY Settings 144 | 145 | - `markdown-image.DIY.path`: The Code Path what you write. Your code must exports a function as `async function (filePath:string, savePath:string, markdownPath:string):string`. 146 | For example: 147 | ```javascript 148 | const path = require("path"); 149 | module.exports = async function (filePath, savePath, markdownPath) { 150 | // Return a picture access link 151 | return path.relative(path.dirname(markdownPath), filePath); 152 | }; 153 | ``` 154 | 155 | ### Others 156 | 157 | - [GitHub](https://github.com/imlinhanchao/vsc-markdown-image) 158 | - [VSCode Market](https://marketplace.visualstudio.com/items?itemName=hancel.markdown-image) 159 | - Icons made by [Good Ware](https://www.flaticon.com/authors/good-ware) from [www.flaticon.com](https://www.flaticon.com/) 160 | 161 | **Enjoy!** 162 | -------------------------------------------------------------------------------- /README.zh-cn.md: -------------------------------------------------------------------------------- 1 | [English Readme](README.md) / 简体中文说明 / [繁體中文說明](README.zh-tw.md) / [日本語説明](README.ja.md) 2 | 3 | # Markdown Image 4 | 5 | 一个用于方便的在 Markdown 中插入图片的扩展,支持将图片存放在本地或第三方的图床或对象存储。 6 | 7 | ❤ [Sponsor me](https://www.paypal.me/imlinhanchao) / [赞助开发者](http://sponsor.hancel.org) 8 | 9 | ## 功能 10 | 11 | 1. 可复制图片文件或截图粘贴,快捷键 `Alt + Shift + V`,或右键菜单 `粘贴图片`。 12 | 2. 自动生成 Markdown 代码插入。 13 | 3. 可配置支持 `Imgur`,`七牛`,`SM.MS`,`Coding` 等图床。默认为本地,需打开 Markdown 文件所在文件夹。 14 | 4. 也可以自定义代码实现图片上传。 15 | 5. 支援 Windows,MacOS,Linux。 16 | 17 | ## 依赖要求 18 | 19 | Windows 和 MacOS 用户可直接使用,Linux 用户须安装 xclip. 20 | 21 | Ubuntu 22 | 23 | ```bash 24 | sudo apt install xclip 25 | ``` 26 | 27 | CentOS 28 | 29 | ```bash 30 | sudo yum install epel-release.noarch 31 | sudo yum install xclip 32 | ``` 33 | 34 | ## 注意事项 35 | 36 | 如果你要在 Remote 模式中使用,请设置 `remote.extensionKind` 如下: 37 | 38 | ```json 39 | "remote.extensionKind": { 40 | "hancel.markdown-image": [ 41 | "ui" 42 | ] 43 | } 44 | ``` 45 | 46 | 而且,如果要将图片保存在远程工作目录中,则必须使用 `SFTP` 上传方法,`Local` 方法无法在 Remote 模式中使用。 47 | 48 | ## 扩展设置项目 49 | 50 | ### 基本设置项目 51 | 52 | - `markdown-image.base.uploadMethod`: 上传图片的方式,根据不同的方式,须在设置不同的项目。 53 | - `markdown-image.base.uploadMethods`: 并行上传的上传方法。此处设置的上传方法的上传结果,将不会被插入到 Markdown 文件中。 54 | - `markdown-image.base.fileNameFormat`: 图片文件命名格式化字符串。支持多种变量做格式化,可同时配置文件夹格式,具体见设置。 55 | - `markdown-image.base.codeType`: 插入代码的类型, 你可以设置为使用 `` 标签, Markdown或自定义。 56 | - `markdown-image.base.codeFormat`: 插入代码的格式代码,当 `markdown-image.base.codeType` 设置为 `DIY` 时生效。 57 | - `markdown-image.base.imageWidth`: 图片的最大宽度,若图片大于这个宽度,则会设置宽度为该值。设置为 0 则表示不设置。 58 | - `markdown-image.base.urlEncode`: 是否对图像的 URL 编码。 59 | 60 | ### Local 设置项目 61 | 62 | - `markdown-image.local.path`: 图片本地存放路径,支持相对路径,相对于所粘贴的 Markdown 文件。`/` 表示打开的文件夹根目录。若路径不存在,将会自动创建。 63 | - `markdown-image.local.referencePath`: Markdown 中的图片的引用路径格式(不包含文件名)。留空表示使用相对路径。 你可以使用 `#markdown-image.base.fileNameFormat#` 中的所有变量。例如:`/images/${YY}-${MM}/` 64 | 65 | ### GitHub Settings 66 | 67 | - `markdown-image.github.path`: 仓库中的图片保存目录(如果不存在,则自动创建)。 68 | - `markdown-image.github.token`: GitHub 的个人[访问令牌](https://github.com/settings/tokens),用于访问仓库,上传图片。 69 | - `markdown-image.github.repository`: 所要上传的目的仓库,比如:`https://github.com/username/repository/`。仓库必须要先初始化。 70 | - `markdown-image.github.branch`: 要存放图片的仓库分支。 71 | - `markdown-image.github.cdn`: 要使用的 CDN 地址格式,`${username}` 表示上传仓库的用户名,`${repository}` 表示上传的仓库,`${branch}` 表示上传的分支,`${filepath}` 表示上传的仓库目录与文件名。 72 | - `markdown-image.github.httpProxy` : 设置访问Github的 HTTP 代理。 73 | 74 | ### Imgur 设置项目 75 | 76 | - `markdown-image.imgur.clientId`: 在 `imgur` 注册的 `Client Id`。您可以在[这儿](https://api.imgur.com/oauth2/addclient)注册。 77 | - `markdown-image.imgur.httpProxy`: 设置访问的 HTTP 代理。 78 | 79 | ### SM.MS 设置项目 80 | 81 | - `markdown-image.sm_ms.token`: SM.MS Secret Token。您可以注册一个帐户,然后访问 [API Access](https://sm.ms/home/apitoken) 页面生成。 82 | 83 | ### 七牛设置项目 84 | 85 | - `markdown-image.qiniu.accessKey`: 七牛账户的 Access Key。 86 | - `markdown-image.qiniu.secretKey`: 七牛账户的 Secret Key。 87 | - `markdown-image.qiniu.bucket`: 七牛的对象存储空间名。 88 | - `markdown-image.qiniu.domain`: 七牛空间绑定的域名。 89 | - `markdown-image.qiniu.zone`: 七牛空间的存储区域。 90 | 91 | ### 又拍云设置项目 92 | 93 | - `markdown-image.upyun.bucket`: 又拍云的云存储服务名称。 94 | - `markdown-image.upyun.domain`: 又拍云云存储服务绑定的域名。 95 | - `markdown-image.upyun.operator`: 又拍云的操作员名。 96 | - `markdown-image.upyun.password`: 又拍云的操作员密码。 97 | - `markdown-image.upyun.path`: 又拍云图片存储路径。 98 | - `markdown-image.upyun.link`: 又拍云链接线路。 99 | 100 | ### Cloudinary 设置项目 101 | 102 | 这些值可以在 Cloudinary Dashboard 上找到 103 | 104 | - `markdown-image.cloudinary.cloudName`: 你的帐户名称。 105 | - `markdown-image.cloudinary.apiKey`: 你的帐户 API key。 106 | - `markdown-image.cloudinary.apiSecret`: 你的用户帐户 API secret。 107 | - `markdown-image.cloudinary.folder`: 图像上传文件夹。 108 | 109 | ### Cloudflare 设置项目 110 | 111 | 这些值可以在 CloudFlare Dashboard 上找到 112 | 113 | - `markdown-image.cloudflare.accountId`: 你的帐户名称 114 | - `markdown-image.cloudflare.apiToken`: Cloudflare API 令牌。 115 | 116 | ### S3 设置项目 117 | 118 | 这些值可以在 S3 服务 Dashboard 上找到 119 | 120 | - `markdown-image.s3.endpoint`: 你的 S3 API 端点,是从存储桶设置或 dashboard 获得的。 121 | - `markdown-image.s3.region`: 你的 S3 存储桶区域,是从存储桶设置中获得的。 122 | - `markdown-image.s3.bucketName`: 你的 S3 存储桶名称。存储桶访问权限应该是公开的。 123 | - `markdown-image.s3.accessKeyId`: 你的 S3 access key ID。 124 | - `markdown-image.s3.secretAccessKey`: 你的 S3 secret access key。 125 | - `markdown-image.s3.cdn`: 设置你的 S3 CDN URL。你可以使用变量 `${bucket}` `${region}` `${pathname}` and `${filepath}`。比如:`https://${bucket}.${region}.s3.amazonaws.com/${pathname}/${filepath}`。 126 | 127 | ### SFTP 设置项目 128 | 129 | - `markdown-image.sftp.host`: 远程服务器地址。 130 | - `markdown-image.sftp.port`: SSH 服务端口。 131 | - `markdown-image.sftp.username`: 远程用户名。 132 | - `markdown-image.sftp.password`: SSH 密码。 133 | - `markdown-image.sftp.privateKeyPath`: 远程私钥文件路径。 134 | - `markdown-image.sftp.path`: 远程服务器的图片存储目录(如果不存在,则自动创建)。支持相对路径,相对于所粘贴的 Markdown 文件。 `/` 表示打开的文件夹根目录。注意:您不能在此处使用变量。您可以在 `#markdown-image.base.fileNameFormat#` 中使用变量。 135 | - `markdown-image.sftp.referencePath`: Markdown 中的图片的引用路径格式(不包含文件名)。留空表示使用相对路径。 你可以使用 `#markdown-image.base.fileNameFormat#` 中的所有变量。例如:`/images/${YY}-${MM}/` 136 | 137 | ### Azure Storage 设置项目 138 | 139 | - `markdown-image.azure.authenticationMethod`: 用于 Azure Blob 存储帐户的身份验证方法。默认值为 `Passwordless`。您可以从[这里](https://learn.microsoft.com/zh-cn/azure/storage/blobs/storage-quickstart-blobs-nodejs?tabs=managed-identity%2Croles-azure-portal%2Csign-in-azure-cli#authenticate-to-azure-and-authorize-access-to-blob-data)获取更多信息。 140 | - `markdown-image.azure.accountName`: 您的 Azure Blob 存储帐户。 141 | - `markdown-image.azure.connectionString`: 你的 Azure 存储连接字符串。 142 | - `markdown-image.azure.container`: 你的 Azure 存储容器名称。 143 | 144 | ### 自定义设置项目 145 | 146 | - `markdown-image.DIY.path`: 你写的代码的路径。 你的代码必须 exports 一个像 `async function (filePath:string, savePath:string, markdownPath:string):string` 的函数。 147 | 比如: 148 | ```javascript 149 | const path = require('path'); 150 | module.exports = async function(filePath, savePath, markdownPath) { 151 | // Return a picture access link 152 | return path.relative(path.dirname(markdownPath), filePath); 153 | } 154 | ``` 155 | 156 | ### 其他 157 | 158 | * [GitHub](https://github.com/imlinhanchao/vsc-markdown-image) 159 | * [VSCode Market](https://marketplace.visualstudio.com/items?itemName=hancel.markdown-image) 160 | * 图标来自 [www.flaticon.com](https://www.flaticon.com/) 的 [Good Ware](https://www.flaticon.com/authors/good-ware) 161 | 162 | **Enjoy!** 163 | -------------------------------------------------------------------------------- /README.zh-tw.md: -------------------------------------------------------------------------------- 1 | [English Readme](README.md) / [简体中文说明](README.zh-cn.md) / 繁體中文說明 / [日本語説明](README.ja.md) 2 | 3 | # Markdown Image 4 | 5 | 一個用於方便的在 Markdown 中插入圖片的擴展,支持將圖片存放在本地或第三方的圖床或對象存儲。 6 | 7 | ❤ [Sponsor me](https://www.paypal.me/imlinhanchao) / [贊助開發者](http://sponsor.hancel.org) 8 | 9 | ## 功能 10 | 11 | 1. 可複製圖片文件或截圖粘貼,快捷键 `Alt + Shift + V`,或右键菜单 `粘贴图片`。 12 | 2. 自動生成 Markdown 代碼插入。 13 | 3. 可配置支持 `Imgur`,`七牛`,`SM.MS`,`Coding` 等圖床。默認為本地,需打開 Markdown 文件所在文件夾。 14 | 4. 也可以自定義代碼實現圖片上傳。 15 | 5. 支援 Windows,MacOS,Linux。 16 | 17 | ## 依赖要求 18 | 19 | Windows 和 MacOS 用戶可直接使用,Linux 用戶須安裝 xclip. 20 | 21 | Ubuntu 22 | 23 | ```bash 24 | sudo apt install xclip 25 | ``` 26 | 27 | CentOS 28 | 29 | ```bash 30 | sudo yum install epel-release.noarch 31 | sudo yum install xclip 32 | ``` 33 | 34 | ## 注意事項 35 | 36 | 如果你要在 Remote 模式中使用,請設置 `remote.extensionKind` 如下: 37 | 38 | ```json 39 | "remote.extensionKind": { 40 | "hancel.markdown-image": [ 41 | "ui" 42 | ] 43 | } 44 | ``` 45 | 46 | 而且,如果要將圖像保存在遠程工作目录中,則必須使用 `SFTP` 上傳方法,`Local` 方法無法在 Remote 模式中使用。 47 | 48 | ## 擴展設置項目 49 | 50 | ### 基本設置項目 51 | 52 | - `markdown-image.base.uploadMethod`: 上傳圖片的方式,根據不同的方式,須在設置不同的項目。 53 | - `markdown-image.base.uploadMethods`: 平行上傳的上傳方法。 此處設置的上傳方法的上傳結果,將不會被插入到 Markdown 文件中。 54 | - `markdown-image.base.fileNameFormat`: 圖片文件命名格式化字符串。支持多種變量做格式化,可同時配置文件夾格式,具體見設置。 55 | - `markdown-image.base.codeType`: 插入代碼的類型, 你可以設置為使用 `` 標籤或 Markdown。 56 | - `markdown-image.base.codeFormat`: 插入代碼的格式代碼,當 `markdown-image.base.codeType` 設置為 `DIY` 時生效。 57 | - `markdown-image.base.imageWidth`: 圖片的最大寬度,若圖片大於這個寬度,則會設置寬度為該值。設置為 0 則表示不設置。 58 | - `markdown-image.base.urlEncode`: 是否對圖像的 URL 編碼。 59 | 60 | ### Local 设置项目 61 | 62 | - `markdown-image.local.path`: 圖片本地存放路徑,支持相對路徑,相對於所粘貼的 Markdown 文件。 `/` 表示打開的文件夾根目錄。若路徑不存在,將會自動創建。 63 | - `markdown-image.local.referencePath`: Markdown 中的圖片的引用路徑格式(不包含文件名)。留空表示使用相對路徑。你可以使用 `#markdown-image.base.fileNameFormat#` 中的所有變量。例如:`/images/${YY}-${MM}/` 64 | 65 | ### GitHub Settings 66 | 67 | - `markdown-image.github.path`: 倉庫中的圖片保存目錄(如果不存在,則自動創建)。 68 | - `markdown-image.github.token`: Git Hub 的個人[訪問令牌](https://github. com/settings/tokens),用於訪問倉庫,上傳圖片。 69 | - `markdown-image.github.repository`: 所要上傳的目的倉庫,比如:`https://github.com/username/repository/`。倉庫必須要先初始化。 70 | - `markdown-image.github.branch`: 要存放圖片的倉庫分支。 71 | - `markdown-image.github.cdn`: 要使用的 CDN 地址格式,`${username}` 表示上傳倉庫的用戶名,`${repository}` 表示上傳的倉庫,`${branch}` 表示上傳的分支,`${filepath}` 表示上傳的倉庫目錄與文件名。 72 | - `markdown-image.github.httpProxy` : 設置訪問Github的 HTTP 代理。 73 | 74 | ### Imgur 設置項目 75 | 76 | - `markdown-image.imgur.clientId`: 在 `imgur` 註冊的 `Client Id`。您可以在[這裡](https://api.imgur.com/oauth2/addclient)註冊。 77 | - `markdown-image.imgur.httpProxy`: 設置訪問的 HTTP 代理。 78 | 79 | ### SM.MS 設置項目 80 | 81 | - `markdown-image.sm_ms.token`: SM.MS Secret Token。您可以註冊一個帳戶,然後訪問 [API Access](https://sm.ms/home/apitoken) 頁面生成。 82 | 83 | ### 七牛設置項目 84 | 85 | - `markdown-image.qiniu.accessKey`: 七牛賬戶的 Access Key。 86 | - `markdown-image.qiniu.secretKey`: 七牛賬戶的 Secret Key。 87 | - `markdown-image.qiniu.bucket`: 七牛的對象存儲空間名。 88 | - `markdown-image.qiniu.domain`: 七牛空间绑定的域名。 89 | - `markdown-image.qiniu.zone`: 七牛空間的存儲區域。 90 | 91 | ### 又拍云设置项目 92 | 93 | - `markdown-image.upyun.bucket`: 又拍雲的雲存儲服務名稱。 94 | - `markdown-image.upyun.domain`: 又拍雲雲存儲服務綁定的域名。 95 | - `markdown-image.upyun.operator`: 又拍雲的操作員名。 96 | - `markdown-image.upyun.password`: 又拍雲的操作員密碼。 97 | - `markdown-image.upyun.path`: 又拍雲圖片存儲路徑。 98 | - `markdown-image.upyun.link`: 又拍雲鏈接線路。 99 | 100 | ### Cloudinary 設置項目 101 | 102 | 這些值可以在 Cloudinary Dashboard 上找到 103 | 104 | - `markdown-image.cloudinary.cloudName`: 你的帳戶名稱。 105 | - `markdown-image.cloudinary.apiKey`: 你的帳戶 API key。 106 | - `markdown-image.cloudinary.apiSecret`: 你的帳戶 API secret。 107 | - `markdown-image.cloudinary.folder`: 圖像上傳文件夾。 108 | 109 | ### Cloudflare 設置項目 110 | 111 | 這些值可以在 CloudFlare Dashboard 上找到 112 | 113 | - `markdown-image.cloudflare.accountId`: 你的帳戶名稱。 114 | - `markdown-image.cloudflare.apiToken`: Cloudflare API 令牌. 115 | 116 | ### S3 設置項目 117 | 118 | 這些值可以在 S3 服務 Dashboard 上找到 119 | 120 | - `markdown-image.s3.endpoint`: 你的 S3 API 端點,是從存儲桶設置或 dashboard 獲得的。 121 | - `markdown-image.s3.region`: 你的 S3 存儲桶區域,是從存儲桶設置中獲得的。 122 | - `markdown-image.s3.bucketName`: 你的 S3 存儲桶名稱。存儲桶訪問權限應該是公開的。 123 | - `markdown-image.s3.accessKeyId`: 你的 S3 access key ID。 124 | - `markdown-image.s3.secretAccessKey`: 你的 S3 secret access key。 125 | - `markdown-image.s3.cdn`: 設定你的 S3 CDN URL。你可以使用變數 `${bucket}` `${region}` `${pathname}` and `${filepath}`。比如:`https://${bucket}.${region}.s3.amazonaws.com/${pathname}/${filepath}`。 126 | 127 | ### SFTP 設置項目 128 | 129 | - `markdown-image.sftp.host`: 遠端伺服器地址。 130 | - `markdown-image.sftp.port`: SSH 服務端口。 131 | - `markdown-image.sftp.username`: 遠端賬戶。 132 | - `markdown-image.sftp.password`: SSH 密碼。 133 | - `markdown-image.sftp.privateKeyPath`: 遠端私鑰文件路徑。 134 | - `markdown-image.sftp.path`: 遠端伺服器的圖片存儲目錄(如果不存在,則自動創建)。支持相對路徑,相對於所粘貼的 Markdown 文件。 `/` 表示打開的文件夾根目錄。注意:您不能在此處使用變量。您可以在 `#markdown-image.base.fileNameFormat#` 中使用變量。 135 | - `markdown-image.sftp.referencePath`: Markdown 中的圖片的引用路徑格式(不包含文件名)。留空表示使用相對路徑。你可以使用 `#markdown-image.base.fileNameFormat#` 中的所有變量。例如:`/images/${YY}-${MM}/` 136 | 137 | ### Azure Storage 設置項目 138 | 139 | - `markdown-image.azure.authenticationMethod`: 用於 Azure Blob 存儲帳戶的身份驗證方法。 默認值為 `Passwordless`。 您可以從[這裡](https://learn.microsoft.com/zh-tw/azure/storage/blobs/storage-quickstart-blobs-nodejs?tabs=managed-identity%2Croles-azure-portal%2Csign-in-azure-cli#authenticate-to-azure-and-authorize-access-to-blob-data)獲取更多信息。 140 | - `markdown-image.azure.accountName`: 你的 Azure Blob 存儲帳戶名。 141 | - `markdown-image.azure.connectionString`: 你的 Azure 存儲連接字符串。 142 | - `markdown-image.azure.container`: 你的 Azure 存儲容器名稱。 143 | 144 | ### 自定義設置項目 145 | 146 | - `markdown-image.DIY.path`: 你寫的代碼的路徑。你的代碼必須 exports 一個像 `async function (filePath:string, savePath:string, markdownPath:string):string` 的函數。 147 | 比如: 148 | ```javascript 149 | const path = require('path'); 150 | module.exports = async function(filePath, savePath, markdownPath) { 151 | // Return a picture access link 152 | return path.relative(path.dirname(markdownPath), filePath); 153 | } 154 | ``` 155 | 156 | ### 其他 157 | 158 | * [GitHub](https://github.com/imlinhanchao/vsc-markdown-image) 159 | * [VSCode Market](https://marketplace.visualstudio.com/items?itemName=hancel.markdown-image) 160 | * 圖標來自 [www.flaticon.com](https://www.flaticon.com/) 的 [Good Ware](https://www.flaticon.com/authors/good-ware) 161 | 162 | **Enjoy!** 163 | -------------------------------------------------------------------------------- /asserts/crlf.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const filePath = path.join(__dirname, 'linux.sh'); 5 | 6 | fs.readFile(filePath, 'utf8', (err, data) => { 7 | if (err) { 8 | console.error('Read file failded:', err); 9 | return; 10 | } 11 | 12 | const result = data.replace(/\r\n/g, '\n'); 13 | 14 | fs.writeFile(filePath, result, 'utf8', (err) => { 15 | if (err) { 16 | console.error('Write file failed:', err); 17 | return; 18 | } 19 | console.log('linux.sh: CR LF => LF'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /asserts/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imlinhanchao/vsc-markdown-image/93d7e222f3816e9a17568f2a4145631fa861b686/asserts/icon.png -------------------------------------------------------------------------------- /asserts/linux.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # require xclip(see http://stackoverflow.com/questions/592620/check-if-a-program-exists-from-a-bash-script/677212#677212) 4 | command -v xclip >/dev/null 2>&1 || { 5 | echo >&1 "no xclip" 6 | exit 1 7 | } 8 | 9 | # write image in clipboard to file (see http://unix.stackexchange.com/questions/145131/copy-image-from-clipboard-to-file) 10 | xclip -selection clipboard -target image/png -o >$1 11 | if [ $? -eq 0 ]; then 12 | echo $1 13 | else 14 | content=$(xclip -selection c -o) 15 | echo $content 16 | fi 17 | -------------------------------------------------------------------------------- /asserts/mac.applescript: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imlinhanchao/vsc-markdown-image/93d7e222f3816e9a17568f2a4145631fa861b686/asserts/mac.applescript -------------------------------------------------------------------------------- /asserts/pc.ps1: -------------------------------------------------------------------------------- 1 | param($imagePath) 2 | 3 | # Adapted from https://github.com/octan3/img-clipboard-dump/blob/master/dump-clipboard-png.ps1 4 | chcp 65001 | Out-Null 5 | 6 | Add-Type -Assembly PresentationCore 7 | 8 | $image = [System.Windows.Clipboard]::GetImage() 9 | if (-not $image) { 10 | $fileDropList = [System.Windows.Clipboard]::GetFileDropList() 11 | if (-not $fileDropList) { 12 | Write-Host "no image in clipboard" 13 | Exit 1 14 | } 15 | Write-Host $fileDropList 16 | Exit 0 17 | } 18 | 19 | if (-not $imagePath) { 20 | Write-Host "image path unspecified" 21 | Exit 1 22 | } 23 | 24 | function Is-Bgra32AndMissingAlpha { 25 | param([System.Windows.Media.Imaging.BitmapSource] $source) 26 | 27 | if ($source.Format -ne [System.Windows.Media.PixelFormats]::Bgra32) { 28 | return $false 29 | } 30 | 31 | $stride = $source.PixelWidth * 4 32 | $pixelData = New-Object byte[] ($stride * $source.PixelHeight) 33 | 34 | try { 35 | $source.CopyPixels($pixelData, $stride, 0) 36 | } catch { 37 | return $false 38 | } 39 | 40 | for ($i = 0; $i -lt $pixelData.Length; $i += 4) { 41 | if ($pixelData[$i + 3] -gt 0) { 42 | return $false 43 | } 44 | } 45 | 46 | return $true 47 | } 48 | 49 | $targetFormat = if (-not @( 50 | [System.Windows.Media.PixelFormats]::Bgra32, 51 | [System.Windows.Media.PixelFormats]::Pbgra32, 52 | [System.Windows.Media.PixelFormats]::Rgba64, 53 | [System.Windows.Media.PixelFormats]::Prgba64, 54 | [System.Windows.Media.PixelFormats]::Rgba128Float 55 | ) -contains $image.Format) { 56 | [System.Windows.Media.PixelFormats]::Rgb24 57 | } elseif (Is-Bgra32AndMissingAlpha -source $image) { 58 | [System.Windows.Media.PixelFormats]::Rgb24 59 | } else { 60 | [System.Windows.Media.PixelFormats]::Bgra32 61 | } 62 | 63 | $fcb = New-Object Windows.Media.Imaging.FormatConvertedBitmap($image, $targetFormat, $null, 0) 64 | $stream = [System.IO.File]::Open($imagePath, "Create") 65 | $encoder = New-Object Windows.Media.Imaging.PngBitmapEncoder 66 | $encoder.Frames.Add([System.Windows.Media.Imaging.BitmapFrame]::Create($fcb)) | Out-Null 67 | $encoder.Save($stream) | Out-Null 68 | $stream.Dispose() | Out-Null 69 | 70 | Write-Host $imagePath 71 | -------------------------------------------------------------------------------- /asserts/rtf.ps1: -------------------------------------------------------------------------------- 1 | Add-Type -Assembly PresentationCore 2 | 3 | ($html = [Windows.Clipboard]::GetData([Windows.DataFormats]::Html)) | out-null 4 | 5 | [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 6 | [Console]::WriteLine($html) 7 | 8 | -------------------------------------------------------------------------------- /asserts/rtf.sh: -------------------------------------------------------------------------------- 1 | command -v xclip >/dev/null 2>&1 || { echo >&1 "no xclip"; exit 1; } 2 | 3 | xclip -selection clipboard -o -t text/html 4 | -------------------------------------------------------------------------------- /asserts/sample.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = async function(filePath, savePath, markdownPath) { 4 | // Return a picture access link 5 | return path.relative(path.dirname(markdownPath), filePath); 6 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "markdown-image", 3 | "displayName": "Markdown Image", 4 | "description": "Easy to insert a image to markdown", 5 | "version": "1.1.49", 6 | "publisher": "hancel", 7 | "engines": { 8 | "vscode": "^1.62.0" 9 | }, 10 | "keywords": [ 11 | "Picture", 12 | "cdn", 13 | "markdown", 14 | "imgur", 15 | "七牛", 16 | "sm.ms", 17 | "helper" 18 | ], 19 | "icon": "asserts/icon.png", 20 | "license": "MIT", 21 | "categories": [ 22 | "Other" 23 | ], 24 | "activationEvents": [ 25 | "onLanguage:markdown" 26 | ], 27 | "repository": "https://github.com/imlinhanchao/vsc-markdown-image", 28 | "main": "./out/extension", 29 | "contributes": { 30 | "commands": [ 31 | { 32 | "command": "markdown-image.paste", 33 | "category": "Markdown Image", 34 | "title": "%markdown-image.command.paste%" 35 | }, 36 | { 37 | "command": "markdown-image.config", 38 | "category": "Markdown Image", 39 | "title": "%markdown-image.command.config%" 40 | }, 41 | { 42 | "command": "markdown-image.paste-rich-text", 43 | "category": "Markdown Image", 44 | "title": "%markdown-image.command.paste-rich-text%" 45 | } 46 | ], 47 | "keybindings": [ 48 | { 49 | "command": "markdown-image.paste", 50 | "key": "alt+shift+v", 51 | "mac": "alt+shift+v", 52 | "when": "editorLangId == markdown || editorLangId == mdx || resourceExtname == .ipynb" 53 | } 54 | ], 55 | "menus": { 56 | "editor/context": [ 57 | { 58 | "command": "markdown-image.paste", 59 | "when": "editorLangId == markdown || editorLangId == mdx || resourceExtname == .ipynb", 60 | "group": "9_cutcopypaste@4" 61 | }, 62 | { 63 | "command": "markdown-image.paste-rich-text", 64 | "when": "editorLangId == markdown || editorLangId == mdx || resourceExtname == .ipynb", 65 | "group": "9_cutcopypaste@5" 66 | } 67 | ] 68 | }, 69 | "configuration": [ 70 | { 71 | "properties": { 72 | "markdown-image.base.codeType": { 73 | "default": "Markdown", 74 | "enum": [ 75 | "Markdown", 76 | "HTML", 77 | "DIY" 78 | ], 79 | "enumDescriptions": [ 80 | "%markdown-image.base.codeType.Markdown%", 81 | "%markdown-image.base.codeType.HTML%", 82 | "%markdown-image.base.codeType.DIY%" 83 | ], 84 | "markdownDescription": "%markdown-image.base.codeType%", 85 | "order": 2, 86 | "type": "string" 87 | }, 88 | "markdown-image.base.codeFormat": { 89 | "default": "![${alt}](${src})", 90 | "markdownDescription": "%markdown-image.base.codeFormat%", 91 | "order": 3, 92 | "type": "string" 93 | }, 94 | "markdown-image.base.fileNameFormat": { 95 | "default": "${hash}", 96 | "markdownDescription": "%markdown-image.base.fileNameFormat%", 97 | "order": 5, 98 | "pattern": "^[^:*?<>|]+$", 99 | "type": "string" 100 | }, 101 | "markdown-image.base.altFormat": { 102 | "default": "%markdown-image.base.altFormat.default%", 103 | "markdownDescription": "%markdown-image.base.altFormat%", 104 | "order": 6, 105 | "type": "string" 106 | }, 107 | "markdown-image.base.imageWidth": { 108 | "default": 0, 109 | "markdownDescription": "%markdown-image.base.imageWidth%", 110 | "order": 4, 111 | "type": "number" 112 | }, 113 | "markdown-image.base.uploadMethod": { 114 | "default": "Local", 115 | "enum": [ 116 | "Local", 117 | "GitHub", 118 | "Imgur", 119 | "SM.MS", 120 | "Data URL", 121 | "%markdown-image.qiniu%", 122 | "%markdown-image.upyun%", 123 | "Cloudinary", 124 | "Cloudflare", 125 | "S3", 126 | "SFTP", 127 | "Azure Storage", 128 | "%markdown-image.DIY%" 129 | ], 130 | "enumDescriptions": [ 131 | "%markdown-image.base.uploadMethod.Local%", 132 | "%markdown-image.base.uploadMethod.GitHub%", 133 | "%markdown-image.base.uploadMethod.Imgur%", 134 | "%markdown-image.base.uploadMethod.SM.MS%", 135 | "%markdown-image.base.uploadMethod.DataURL%", 136 | "%markdown-image.base.uploadMethod.Qiniu%", 137 | "%markdown-image.base.uploadMethod.Upyun%", 138 | "%markdown-image.base.uploadMethod.Cloudinary%", 139 | "%markdown-image.base.uploadMethod.Cloudflare%", 140 | "%markdown-image.base.uploadMethod.S3%", 141 | "%markdown-image.base.uploadMethod.SFTP%", 142 | "%markdown-image.base.uploadMethod.Azure%", 143 | "%markdown-image.base.uploadMethod.DIY%" 144 | ], 145 | "markdownDescription": "%markdown-image.base.uploadMethod%", 146 | "order": 0, 147 | "type": "string" 148 | }, 149 | "markdown-image.base.uploadMethods": { 150 | "default": [], 151 | "markdownDescription": "%markdown-image.base.uploadMethods%", 152 | "order": 1, 153 | "type": "array", 154 | "items": { 155 | "type": "string", 156 | "enum": [ 157 | "Local", 158 | "GitHub", 159 | "Imgur", 160 | "SM.MS", 161 | "Data URL", 162 | "%markdown-image.qiniu%", 163 | "%markdown-image.upyun%", 164 | "Cloudinary", 165 | "Cloudflare", 166 | "S3", 167 | "SFTP", 168 | "Azure Storage", 169 | "%markdown-image.DIY%" 170 | ] 171 | } 172 | }, 173 | "markdown-image.base.urlEncode": { 174 | "default": true, 175 | "markdownDescription": "%markdown-image.base.urlEncode%", 176 | "order": 7, 177 | "type": "boolean" 178 | }, 179 | "markdown-image.base.fileFormat": { 180 | "default": "png", 181 | "markdownDescription": "%markdown-image.base.fileFormat%", 182 | "enum": [ 183 | "png", 184 | "jpg" 185 | ], 186 | "order": 8, 187 | "type": "string" 188 | } 189 | }, 190 | "title": "Base" 191 | }, 192 | { 193 | "properties": { 194 | "markdown-image.local.path": { 195 | "default": "/images", 196 | "markdownDescription": "%markdown-image.local.path%", 197 | "order": 0, 198 | "type": "string" 199 | }, 200 | "markdown-image.local.referencePath": { 201 | "default": "", 202 | "markdownDescription": "%markdown-image.local.referencePath%", 203 | "order": 1, 204 | "type": "string" 205 | } 206 | }, 207 | "title": "Local" 208 | }, 209 | { 210 | "properties": { 211 | "markdown-image.github.branch": { 212 | "default": "master", 213 | "markdownDescription": "%markdown-image.github.branch%", 214 | "order": 3, 215 | "type": "string" 216 | }, 217 | "markdown-image.github.cdn": { 218 | "default": "https://cdn.jsdelivr.net/gh/${username}/${repository}@${branch}/${filepath}", 219 | "markdownDescription": "%markdown-image.github.cdn%", 220 | "order": 4, 221 | "type": "string" 222 | }, 223 | "markdown-image.github.path": { 224 | "default": "/", 225 | "markdownDescription": "%markdown-image.github.path%", 226 | "order": 0, 227 | "type": "string" 228 | }, 229 | "markdown-image.github.repository": { 230 | "default": "", 231 | "markdownDescription": "%markdown-image.github.repository%", 232 | "order": 2, 233 | "pattern": "^(https://github.com/[^/]*?/[^/]*?/*|)$", 234 | "type": "string" 235 | }, 236 | "markdown-image.github.token": { 237 | "default": "", 238 | "markdownDescription": "%markdown-image.github.token%", 239 | "order": 1, 240 | "type": "string" 241 | }, 242 | "markdown-image.github.httpProxy": { 243 | "default": "", 244 | "markdownDescription": "%markdown-image.github.httpProxy%", 245 | "order": 5, 246 | "type": "string" 247 | } 248 | }, 249 | "title": "GitHub" 250 | }, 251 | { 252 | "properties": { 253 | "markdown-image.imgur.clientId": { 254 | "default": "", 255 | "markdownDescription": "%markdown-image.imgur.clientId%", 256 | "order": 0, 257 | "type": "string" 258 | }, 259 | "markdown-image.imgur.httpProxy": { 260 | "default": "", 261 | "markdownDescription": "%markdown-image.imgur.httpProxy%", 262 | "order": 1, 263 | "type": "string" 264 | } 265 | }, 266 | "title": "Imgur" 267 | }, 268 | { 269 | "properties": { 270 | "markdown-image.sm_ms.token": { 271 | "default": "", 272 | "markdownDescription": "%markdown-image.sm_ms.token%", 273 | "order": 0, 274 | "type": "string" 275 | } 276 | }, 277 | "title": "SM.MS" 278 | }, 279 | { 280 | "properties": { 281 | "markdown-image.qiniu.accessKey": { 282 | "default": "", 283 | "markdownDescription": "%markdown-image.qiniu.accessKey%", 284 | "order": 0, 285 | "type": "string" 286 | }, 287 | "markdown-image.qiniu.bucket": { 288 | "default": "", 289 | "markdownDescription": "%markdown-image.qiniu.bucket%", 290 | "order": 2, 291 | "type": "string" 292 | }, 293 | "markdown-image.qiniu.domain": { 294 | "default": "", 295 | "markdownDescription": "%markdown-image.qiniu.domain%", 296 | "order": 3, 297 | "pattern": "^(http(s|)://[a-zA-Z0-9][-\\w]{0,62}(.[a-zA-Z0-9][-\\w]{0,62})+.|)?", 298 | "type": "string" 299 | }, 300 | "markdown-image.qiniu.secretKey": { 301 | "default": "", 302 | "markdownDescription": "%markdown-image.qiniu.secretKey%", 303 | "order": 1, 304 | "type": "string" 305 | }, 306 | "markdown-image.qiniu.zone": { 307 | "default": "East China", 308 | "enum": [ 309 | "%markdown-image.qiniu.east%", 310 | "%markdown-image.qiniu.north%", 311 | "%markdown-image.qiniu.south%", 312 | "%markdown-image.qiniu.na%", 313 | "%markdown-image.qiniu.sa%" 314 | ], 315 | "markdownDescription": "%markdown-image.qiniu.zone%", 316 | "order": 4, 317 | "type": "string" 318 | } 319 | }, 320 | "title": "Qiniu" 321 | }, 322 | { 323 | "properties": { 324 | "markdown-image.upyun.bucket": { 325 | "default": "", 326 | "markdownDescription": "%markdown-image.upyun.bucket%", 327 | "order": 0, 328 | "type": "string" 329 | }, 330 | "markdown-image.upyun.domain": { 331 | "default": "", 332 | "markdownDescription": "%markdown-image.upyun.domain%", 333 | "order": 1, 334 | "pattern": "^(http(s|)://[a-zA-Z0-9][-\\w]{0,62}(.[a-zA-Z0-9][-\\w]{0,62})+.|)?", 335 | "type": "string" 336 | }, 337 | "markdown-image.upyun.link": { 338 | "default": "%markdown-image.upyun.smart%", 339 | "enum": [ 340 | "%markdown-image.upyun.smart%", 341 | "%markdown-image.upyun.telecom%", 342 | "%markdown-image.upyun.unicom%", 343 | "%markdown-image.upyun.mobile%" 344 | ], 345 | "markdownDescription": "%markdown-image.upyun.link%", 346 | "order": 5, 347 | "type": "string" 348 | }, 349 | "markdown-image.upyun.operator": { 350 | "default": "", 351 | "markdownDescription": "%markdown-image.upyun.operator%", 352 | "order": 2, 353 | "type": "string" 354 | }, 355 | "markdown-image.upyun.password": { 356 | "default": "", 357 | "markdownDescription": "%markdown-image.upyun.password%", 358 | "order": 3, 359 | "type": "string" 360 | }, 361 | "markdown-image.upyun.path": { 362 | "default": "", 363 | "markdownDescription": "%markdown-image.upyun.path%", 364 | "order": 4, 365 | "type": "string" 366 | } 367 | }, 368 | "title": "Upyun" 369 | }, 370 | { 371 | "properties": { 372 | "markdown-image.cloudinary.apiKey": { 373 | "default": "", 374 | "markdownDescription": "%markdown-image.cloudinary.apiKey%", 375 | "order": 1, 376 | "type": "string" 377 | }, 378 | "markdown-image.cloudinary.apiSecret": { 379 | "default": "", 380 | "markdownDescription": "%markdown-image.cloudinary.apiSecret%", 381 | "order": 2, 382 | "type": "string" 383 | }, 384 | "markdown-image.cloudinary.cloudName": { 385 | "default": "", 386 | "markdownDescription": "%markdown-image.cloudinary.cloudName%", 387 | "order": 0, 388 | "type": "string" 389 | }, 390 | "markdown-image.cloudinary.folder": { 391 | "default": "", 392 | "markdownDescription": "%markdown-image.cloudinary.folder%", 393 | "order": 3, 394 | "type": "string" 395 | } 396 | }, 397 | "title": "Cloudinary" 398 | }, 399 | { 400 | "properties": { 401 | "markdown-image.cloudflare.accountId": { 402 | "default": "", 403 | "markdownDescription": "%markdown-image.cloudflare.accountId%", 404 | "order": 0, 405 | "type": "string" 406 | }, 407 | "markdown-image.cloudflare.apiToken": { 408 | "default": "", 409 | "markdownDescription": "%markdown-image.cloudflare.apiToken%", 410 | "order": 1, 411 | "type": "string" 412 | } 413 | }, 414 | "title": "Cloudflare" 415 | }, 416 | { 417 | "properties": { 418 | "markdown-image.s3.accessKeyId": { 419 | "default": "", 420 | "markdownDescription": "%markdown-image.s3.accessKeyId%", 421 | "order": 3, 422 | "type": "string" 423 | }, 424 | "markdown-image.s3.bucketName": { 425 | "default": "", 426 | "markdownDescription": "%markdown-image.s3.bucketName%", 427 | "order": 2, 428 | "type": "string" 429 | }, 430 | "markdown-image.s3.endpoint": { 431 | "default": "", 432 | "markdownDescription": "%markdown-image.s3.endpoint%", 433 | "order": 0, 434 | "type": "string" 435 | }, 436 | "markdown-image.s3.region": { 437 | "default": "", 438 | "markdownDescription": "%markdown-image.s3.region%", 439 | "order": 1, 440 | "type": "string" 441 | }, 442 | "markdown-image.s3.secretAccessKey": { 443 | "default": "", 444 | "markdownDescription": "%markdown-image.s3.secretAccessKey%", 445 | "order": 4, 446 | "type": "string" 447 | }, 448 | "markdown-image.s3.cdn": { 449 | "default": "", 450 | "markdownDescription": "%markdown-image.s3.cdn%", 451 | "order": 5, 452 | "type": "string" 453 | }, 454 | "markdown-image.s3.config": { 455 | "markdownDescription": "%markdown-image.s3.config%", 456 | "order": 6, 457 | "type": "object" 458 | } 459 | }, 460 | "title": "S3" 461 | }, 462 | { 463 | "properties": { 464 | "markdown-image.sftp.host": { 465 | "default": "", 466 | "markdownDescription": "%markdown-image.sftp.host%", 467 | "order": 0, 468 | "type": "string" 469 | }, 470 | "markdown-image.sftp.port": { 471 | "default": 22, 472 | "markdownDescription": "%markdown-image.sftp.port%", 473 | "order": 1, 474 | "type": "number" 475 | }, 476 | "markdown-image.sftp.username": { 477 | "default": "", 478 | "markdownDescription": "%markdown-image.sftp.username%", 479 | "order": 2, 480 | "type": "string" 481 | }, 482 | "markdown-image.sftp.password": { 483 | "default": "", 484 | "markdownDescription": "%markdown-image.sftp.password%", 485 | "order": 3, 486 | "type": "string" 487 | }, 488 | "markdown-image.sftp.privateKeyPath": { 489 | "default": "", 490 | "markdownDescription": "%markdown-image.sftp.privateKeyPath%", 491 | "order": 4, 492 | "type": "string" 493 | }, 494 | "markdown-image.sftp.path": { 495 | "default": "/images", 496 | "markdownDescription": "%markdown-image.sftp.path%", 497 | "order": 5, 498 | "type": "string" 499 | }, 500 | "markdown-image.sftp.referencePath": { 501 | "default": "", 502 | "markdownDescription": "%markdown-image.sftp.referencePath%", 503 | "order": 6, 504 | "type": "string" 505 | } 506 | }, 507 | "title": "SFTP" 508 | }, 509 | { 510 | "properties": { 511 | "markdown-image.azure.authenticationMethod": { 512 | "default": "Passwordless", 513 | "markdownDescription": "%markdown-image.azure.authenticationMethod%", 514 | "enum": [ 515 | "Passwordless", 516 | "Connection String" 517 | ], 518 | "enumDescriptions": [ 519 | "%markdown-image.azure.authenticationMethod.Passwordless%", 520 | "%markdown-image.azure.authenticationMethod.ConnectionString%" 521 | ], 522 | "order": 0, 523 | "type": "string" 524 | }, 525 | "markdown-image.azure.accountName": { 526 | "default": "", 527 | "markdownDescription": "%markdown-image.azure.accountName%", 528 | "order": 1, 529 | "type": "string" 530 | }, 531 | "markdown-image.azure.connectionString": { 532 | "default": "", 533 | "markdownDescription": "%markdown-image.azure.connect-string%", 534 | "order": 2, 535 | "type": "string" 536 | }, 537 | "markdown-image.azure.container": { 538 | "default": "", 539 | "markdownDescription": "%markdown-image.azure.container%", 540 | "order": 3, 541 | "type": "string" 542 | } 543 | }, 544 | "title": "Azure Storage" 545 | }, 546 | { 547 | "properties": { 548 | "markdown-image.DIY.path": { 549 | "default": "", 550 | "markdownDescription": "%markdown-image.DIY.path%", 551 | "order": 0, 552 | "type": "string" 553 | } 554 | }, 555 | "title": "DIY" 556 | } 557 | ] 558 | }, 559 | "scripts": { 560 | "vscode:prepublish": "npm run compile", 561 | "compile": "node asserts/crlf.js && tsc -p ./", 562 | "watch": "tsc -watch -p ./", 563 | "postinstall": "node ./node_modules/vscode/bin/install", 564 | "test": "npm run compile && node ./node_modules/vscode/bin/test" 565 | }, 566 | "devDependencies": { 567 | "@types/mime": "^3.0.4", 568 | "@types/mocha": "^2.2.42", 569 | "@types/node": "^8.10.25", 570 | "@types/node-fetch": "^2.5.12", 571 | "@types/turndown": "^5.0.0", 572 | "@types/ssh2-sftp-client": "^9.0.0", 573 | "tslint": "^5.8.0", 574 | "typescript": "4.1", 575 | "vscode": "^1.1.25" 576 | }, 577 | "dependencies": { 578 | "@aws-sdk/client-s3": "^3.304.0", 579 | "@aws-sdk/s3-request-presigner": "^3.304.0", 580 | "@azure/identity": "^4.0.1", 581 | "@azure/storage-blob": "^12.17.0", 582 | "cloudinary": "^1.26.1", 583 | "coding-picbed": "^0.0.13", 584 | "form-data": "^3.0.1", 585 | "github-picbed": "^0.0.7", 586 | "got": "^10.7.0", 587 | "https-proxy-agent": "^4.0.0", 588 | "image-size": "^1.0.0", 589 | "mime": "^3.0.0", 590 | "node-fetch": "^2.6.6", 591 | "png-to-jpeg": "^1.0.1", 592 | "qiniu": "^7.3.1", 593 | "ssh2-sftp-client": "^9.0.4", 594 | "turndown": "^7.0.0", 595 | "turndown-plugin-gfm": "^1.0.2", 596 | "upyun": "^3.4.6", 597 | "vscode-nls": "^4.1.2" 598 | } 599 | } 600 | -------------------------------------------------------------------------------- /package.nls.json: -------------------------------------------------------------------------------- 1 | { 2 | "markdown-image.title": "Markdown Image", 3 | "markdown-image.local": "Local", 4 | "markdown-image.qiniu": "Qiniu", 5 | "markdown-image.upyun": "Upyun", 6 | "markdown-image.DIY": "DIY", 7 | "markdown-image.Cloudinary": "Cloudinary", 8 | "markdown-image.Cloudflare": "Cloudflare", 9 | "markdown-image.command.paste": "Paste Image", 10 | "markdown-image.command.config": "Markdown Image Config Setting", 11 | "markdown-image.command.paste-rich-text": "Paste Rich Text (Beta)", 12 | "markdown-image.base.uploadMethod": "Method to upload pictures. To the local or another picture CDN service.\n\n- Local [Options](#markdown-image.local.path)\n\n- GitHub [Options](#markdown-image.github.branch)\n\n- Imgur [Options](#markdown-image.imgur.clientId)\n\n- SM.MS [Options](#markdown-image.sm_ms.token)\n\n- Qiniu [Options](#markdown-image.qiniu.accessKey)\n\n- Upyun [Options](#markdown-image.upyun.bucket)\n\n- DIY [Options](#markdown-image.DIY.path)\n\n- Cloudflare [Options](#markdown-image.cloudflare.accountId)\n\n- Cloudinary [Options](#markdown-image.cloudinary.apiKey)\n\n- S3 [Options](#markdown-image.s3.endpoint)", 13 | "markdown-image.base.uploadMethod.Local": "Upload the image to the project directory. You can use markdown-image.local.path sets the relative path for upload.", 14 | "markdown-image.base.uploadMethod.Coding": "Upload the image to git repository in Coding.net . You can configure the repository through markdown-image.coding.repository. You need to configure the Token to access the repository through markdown-image.coding.token.", 15 | "markdown-image.base.uploadMethod.GitHub": "Upload the image to git repository in GitHub.com . You can configure the repository through markdown-image.github.repository. You need to configure the Token to access the repository through markdown-image.github.token.", 16 | "markdown-image.base.uploadMethod.Imgur": "Upload the image to Imgur. You can configure `Client Id` through markdown-image.imgur.clientId.", 17 | "markdown-image.base.uploadMethod.SM.MS": "Upload the image to sm.ms. You can configure `Token` through markdown-image.sm_ms.token if you have an account.", 18 | "markdown-image.base.uploadMethod.DataURL": "Turn the picture into the DATA URL insert.", 19 | "markdown-image.base.uploadMethod.Qiniu": "Upload the image to qiniu.com.", 20 | "markdown-image.base.uploadMethod.Upyun": "Upload the image to upyun.com.", 21 | "markdown-image.base.uploadMethod.DIY": "You can define your code used to upload. You need to configure the code path through markdown-image.DIY.path", 22 | "markdown-image.base.uploadMethod.Cloudinary": "Upload the image to Cloudinary. You can configure `Cloud Name` through markdown-image.cloudinary.cloudName.", 23 | "markdown-image.base.uploadMethod.S3": "Upload the image to any S3 API compatible server, for example, AWS S3, MinIO, Backblaze B2, etc.", 24 | "markdown-image.base.uploadMethod.Cloudflare": "Upload the image to Cloudflare Image. You can configure `accountId` through markdown-image.cloudflare.accountId.", 25 | "markdown-image.base.uploadMethod.SFTP": "Upload the image to SFTP server. You can configure the server through markdown-image.sftp.host.", 26 | "markdown-image.base.uploadMethod.Azure": "Upload the image to Azure Blob Storage. You can configure the connection string through markdown-image.azure.connect-string.", 27 | "markdown-image.base.uploadMethods": "Multiple upload services are used for concurrent uploading. The default is empty. The list should contain the selectable values of the `#markdown-image.base.uploadMethod#` option mentioned above. The upload results from the configured upload methods will not be inserted into the Markdown file.", 28 | "markdown-image.base.fileNameFormat": "The filenname and path format string for upload. Not Support in `Imgur` and `SM.MS`. You can use some variables: \n\n- `${filename}`: The original filename. \n- `${mdname}`: The name of the Markdown file being edited. \n- `${path}`: The path of the Markdown file being edited relative to the root directory. \n- `${hash}`: The sha256 hash of image. \n- `${timestamp}`: The timestamp of upload time. \n- `${YY}`: The Year \n- `${MM}`:The Month \n- `${DD}`: The Day \n- `${hh}`: The 12-hour clock \n- `${HH}`: The 24-hour clock \n- `${mm}`: The minutes \n- `${ss}`: The seconds \n- `${mss}`: The milliseconds \n- `${rand,number}`: A random number, for example: `${rand,100}`. It will generate random numbers from 0 to 99 \n- `${prompt}`: Makes it possible to enter a custom name through an input prompt when pasting the image.", 29 | "markdown-image.base.urlEncode": "Whether URL encode for the url of image.", 30 | "markdown-image.base.fileFormat": "The Clipboard Image File Format. You can use `png` or `jpg`.", 31 | "markdown-image.base.codeType": "The type of image code", 32 | "markdown-image.base.codeType.Markdown": "Markdown Code as: ![image](./image/file/path.jpg)", 33 | "markdown-image.base.codeType.HTML": "HTML Code as: \"image\"", 34 | "markdown-image.base.codeType.DIY": "Custom image code, you need to configure the code format through `#markdown-image.base.codeFormat#`.", 35 | "markdown-image.base.codeFormat": "The format of custom image code, need to set `#markdown-image.base.codeType#` to `DIY`. You can use `${src}` and `${alt}` variables, which represent the image address and default placeholder (defined by `#markdown-image.base.altFormat#`). For example: `![${alt}](${src})`. You can also use all variables in `#markdown-image.base.fileNameFormat#`.", 36 | "markdown-image.base.imageWidth": "The maximum width of the image, if the image is greater than this width, the width is set to this value. Set to `0` means not change.", 37 | "markdown-image.base.altFormat": "Placeholder for Markdown code. You have some variables available: \n\n- ${filename}: The original filename of the image. \n- ${mdname}: The name of the Markdown file being edited (without the extension). \n- ${hash}: The sha256 hash of the image. \n- ${timestamp}: The timestamp of the upload time. \n- ${YY}: The year \n- ${MM}: The month \n- ${DD}: The date \n- ${hh}: The hour in 12-hour format \n- ${HH}: The hour in 24-hour format \n- ${mm}: The minute \n- ${ss}: The second \n- ${mss}: The millisecond \n- ${rand,number}: Random number, for example: ${rand,100}. It will generate a random number from 0 to 99. \n- ${index}: The index of the image pasted into the file by the plugin.\n- `${prompt}`: enter a custom name through an input prompt.", 38 | "markdown-image.base.altFormat.default": "picture ${index}", 39 | "markdown-image.local.path": "Picture storage directory that in the local (automatically created if it does not exist). Notice: You can't use variable in here. You can use variable in `#markdown-image.base.fileNameFormat#`.", 40 | "markdown-image.local.referencePath": "The reference path format in markdown(not include file name). Empty means use relative path. You can use variable of `#markdown-image.base.fileNameFormat#` in here. For example: `/images/${YY}-${MM}/`", 41 | "markdown-image.coding.path": "Picture upload directory that in the repository (automatically created if it does not exist). The repository must initialization first.", 42 | "markdown-image.coding.token": "Coding person [access token](https://help.coding.net/docs/member/tokens.html).", 43 | "markdown-image.coding.repository": "Coding repository, for example: `https://coding-demo.coding.net/p/coding-demo/d/coding-demo/git`", 44 | "markdown-image.github.path": "Picture upload directory that in the repository (automatically created if it does not exist). The repository must initialization first.", 45 | "markdown-image.github.token": "GitHub person [access token](https://github.com/settings/tokens).", 46 | "markdown-image.github.repository": "GitHub repository, for example: `https://github.com/username/repository`", 47 | "markdown-image.github.branch": "GitHub repository branch to save.", 48 | "markdown-image.github.cdn": "The github cdn address format to be used, `${username}` is the username of `#markdown-image.github.repository#`, and `${repository}` is the repository name. `${branch}` is the value of `#markdown-image.github.branch#`. `${filepath}` is the upload path in repository.", 49 | "markdown-image.github.httpProxy": "Connect to Github via http proxy.", 50 | "markdown-image.imgur.clientId": "The client id registered with imgur. You can registed it at [here](https://api.imgur.com/oauth2/addclient).", 51 | "markdown-image.imgur.httpProxy": "Connect to Imgur via http proxy.", 52 | "markdown-image.sm_ms.token": "SM.MS API token (Options). You can register an account and then visit [API Token](https://smms.app/home/apitoken) Page to generate secret token.", 53 | "markdown-image.qiniu.accessKey": "AccessKey of upload.", 54 | "markdown-image.qiniu.secretKey": "SecretKey of upload", 55 | "markdown-image.qiniu.bucket": "Storge name of upload", 56 | "markdown-image.qiniu.domain": "Domain bind with storge name", 57 | "markdown-image.qiniu.zone": "Zone of storge", 58 | "markdown-image.qiniu.east": "East China", 59 | "markdown-image.qiniu.north": "North China", 60 | "markdown-image.qiniu.south": "South China", 61 | "markdown-image.qiniu.na": "North America", 62 | "markdown-image.qiniu.sa": "Southeast Asia", 63 | "markdown-image.upyun.bucket": "Storge name of upload", 64 | "markdown-image.upyun.domain": "Domain bind with storge name", 65 | "markdown-image.upyun.operator": "Operator of upyun", 66 | "markdown-image.upyun.password": "Password of upyun operator", 67 | "markdown-image.upyun.path": "The path that img store", 68 | "markdown-image.upyun.link": "The link that connect to upyun", 69 | "markdown-image.upyun.smart": "Smart choose: v0.api.upyun.com", 70 | "markdown-image.upyun.telecom": "China Telecom: v1.api.upyun.com", 71 | "markdown-image.upyun.unicom": "China Unicom: v2.api.upyun.com", 72 | "markdown-image.upyun.mobile": "China Mobile: v3.api.upyun.com", 73 | "markdown-image.DIY.path": "The Code File Path. You can write a Node.js code file to upload, and fill in the file path to here. Your code must exports a function as `async function (filePath:string, savePath:string, markdownPath:string):string`.\n\nFor example: \n\n ```javascript\nconst path = require('path');\nmodule.exports = async function(filePath, savePath, markdownPath) {\n\t// Return a picture access link\n\treturn path.relative(path.dirname(markdownPath), filePath); \n}\n```\nThe arguments are :\n- `filePath`: The absolute path of the file. \n- `savePath`: The path of the saved file generate according to `#markdown-image.base.fileNameFormat#`. \n- `markdownPath`: The path of markdown file being edited.", 74 | "markdown-image.cloudinary.cloudName": "Your user account name.", 75 | "markdown-image.cloudinary.apiKey": "API key for your account.", 76 | "markdown-image.cloudinary.apiSecret": "API secret for your account.", 77 | "markdown-image.cloudinary.folder": "Folder to upload the image to.", 78 | "markdown-image.cloudflare.accountId": "Your Cloudflare account ID.", 79 | "markdown-image.cloudflare.apiToken": "You Clouflare Image API token.", 80 | "markdown-image.s3.endpoint": "Your S3 API endpoint obtained from bucket setting or dashboard. It should include protocol as well, e.g. `http/https`.", 81 | "markdown-image.s3.region": "Your S3 bucket region obtained from the bucket setting.", 82 | "markdown-image.s3.bucketName": "Your S3 bucket name.", 83 | "markdown-image.s3.accessKeyId": "Your S3 access key ID.", 84 | "markdown-image.s3.secretAccessKey": "Your S3 secret access key.", 85 | "markdown-image.s3.cdn": "Your S3 CDN Url. You can use variable `${bucket}` `${region}` `${pathname}` and `${filepath}`. For example: `https://${bucket}.${region}.s3.amazonaws.com/${pathname}/${filepath}`.", 86 | "markdown-image.s3.config": "The other configuration of the S3Client. You can get all options in [here](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3/).", 87 | "markdown-image.sftp.host": "The host of the remote server.", 88 | "markdown-image.sftp.port": "The ssh port of the remote server.", 89 | "markdown-image.sftp.username": "The username of the remote server.", 90 | "markdown-image.sftp.password": "The password of the remote server.", 91 | "markdown-image.sftp.privateKeyPath": "The private key path of the remote server.", 92 | "markdown-image.sftp.path": "Picture storage directory that in the remote (automatically created if it does not exist). Notice: You can't use variable in here. You can use variable in `#markdown-image.base.fileNameFormat#`.", 93 | "markdown-image.sftp.referencePath": "The reference path format in markdown(not include file name). Empty means use relative path. You can use variable of `#markdown-image.base.fileNameFormat#` in here. For example: `/images/${YY}-${MM}/`", 94 | "markdown-image.azure.authenticationMethod": "The authentication method to use for the Azure Blob Storage account. The default is `Passwordless`. You can obtain more information from [here](https://learn.microsoft.com/en-us/azure/storage/blobs/storage-quickstart-blobs-nodejs?tabs=managed-identity%2Croles-azure-portal%2Csign-in-azure-cli#authenticate-to-azure-and-authorize-access-to-blob-data) .", 95 | "markdown-image.azure.authenticationMethod.Passwordless": "Use the account name to authenticate. You must first execute `az login` to log in to Azure CLI.", 96 | "markdown-image.azure.authenticationMethod.ConnectionString": "Use the connection string to authenticate.", 97 | "markdown-image.azure.accountName": "Your Azure Blob Storage account.", 98 | "markdown-image.azure.connect-string": "The connection string of your Azure Blob Storage account.", 99 | "markdown-image.azure.container": "The container name of your Azure Blob Storage account." 100 | } 101 | -------------------------------------------------------------------------------- /package.nls.zh-cn.json: -------------------------------------------------------------------------------- 1 | { 2 | "markdown-image.title": "Markdown 图片", 3 | "markdown-image.local": "本地", 4 | "markdown-image.qiniu": "七牛", 5 | "markdown-image.upyun": "又拍云", 6 | "markdown-image.DIY": "自定义", 7 | "markdown-image.Cloudinary": "Cloudinary", 8 | "markdown-image.command.paste": "粘贴图片", 9 | "markdown-image.command.config": "Markdown 图片配置", 10 | "markdown-image.command.paste-rich-text": "粘贴富文本 (Beta)", 11 | "markdown-image.base.uploadMethod": "上传图片的方式。到本地或其他图片CDN服务。\n\n- 本地 [设置项目](#markdown-image.local.path)\n\n- GitHub [设置项目](#markdown-image.github.branch)\n\n- Imgur [设置项目](#markdown-image.imgur.clientId)\n\n- SM.MS [设置项目](#markdown-image.sm_ms.token)\n\n- 七牛 [设置项目](#markdown-image.qiniu.accessKey)\n\n- 又拍云 [设置项目](#markdown-image.upyun.bucket)\n\n- 自定义 [设置项目](#markdown-image.DIY.path)\n\n- Cloudinary [设置项目](#markdown-image.cloudinary.apiKey)\n\n- Cloudflare [设置项目](#markdown-image.cloudflare.accountId)\n\n- S3 [设置项目](#markdown-image.s3.endpoint)", 12 | "markdown-image.base.uploadMethod.Local": "将图像上传到项目目录。您可以使用 markdown-image.local.path 设置上传的相对路径。", 13 | "markdown-image.base.uploadMethod.Coding": "将图像上传到 Coding.net 中的 Git 存储库。您可以通过 markdown-image.coding.repository 配置存储库. 您必须通过 markdown-image.coding.token 配置个人访问令牌访问存储库。", 14 | "markdown-image.base.uploadMethod.GitHub": "将图像上传到 GitHub.com 中的 Git 存储库。您可以通过 markdown-image.github.repository 配置存储库. 您必须通过 markdown-image.github.token 配置个人访问令牌访问存储库。", 15 | "markdown-image.base.uploadMethod.Imgur": "将图像上传到 Imgur 。您可以通过 markdown-image.imgur.clientId 配置`Client Id`。", 16 | "markdown-image.base.uploadMethod.SM.MS": "将图像上传到 sm.ms。 您如果有一个帐户,可以通过 markdown-image.sm_ms.token 配置 `Secret Token` 。", 17 | "markdown-image.base.uploadMethod.DataURL": "将图片转为Data URL 插入.", 18 | "markdown-image.base.uploadMethod.Qiniu": "将图片上传到七牛对象存储,你有很多项目需要配置。", 19 | "markdown-image.base.uploadMethod.Upyun": "将图片上传到又拍云对象存储,你有很多项目需要配置。", 20 | "markdown-image.base.uploadMethod.DIY": "您可以自定义用于上传的代码。您需要通过 markdown-image.DIY.path 配置代码路径。", 21 | "markdown-image.base.uploadMethod.Cloudinary": "上传图片到 Cloudinary。你有很多项目需要配置。", 22 | "markdown-image.base.uploadMethod.Cloudflare": "上传图片到 Cloudflare。你有很多项目需要配置。", 23 | "markdown-image.base.uploadMethod.S3": "上传图片到 S3。你有很多项目需要配置。", 24 | "markdown-image.base.uploadMethod.SFTP": "上传图片到 SFTP。你有很多项目需要配置。", 25 | "markdown-image.base.uploadMethod.Azure": "上传图片到 Azure。", 26 | "markdown-image.base.uploadMethods": "可用于并行上传的上传方法,默认为空,列表内容为上面 `#markdown-image.base.uploadMethod#` 选项的可选值。此处设置的上传方法的上传结果,将不会被插入到 Markdown 文件中。", 27 | "markdown-image.base.fileNameFormat": "上传的文件名与路径的格式字符串。不支持 `Imgur` 和 `SM.MS`。 你有一些变量可以使用: \n\n- `${filename}`: 图片原始的文件名。 \n- `${mdname}`: 正在编辑的 Markdown 文件的名称(不含扩展名)。 \n- `${path}`: 正在编辑的 Markdown 文件相对于根目录的路径。 \n- `${hash}`: 图像的 sha256 哈希。 \n- `${timestamp}`: 上传时间的时间戳。 \n- `${YY}`: 年份 \n- `${MM}`: 月份 \n- `${DD}`: 日期 \n- `${hh}`: 12小时制小时 \n- `${HH}`: 24小时制小时 \n- `${mm}`: 分 \n- `${ss}`: 秒 \n- `${mss}`: 毫秒 \n- `${rand,number}`: 随机数, 比如:`${rand,100}`. 它将生成从 0 到 99 的随机数。\n- `${prompt}`: 可以在粘贴图像时通过输入框输入自定义名称。", 28 | "markdown-image.base.urlEncode": "是否对图像的 URL 编码。", 29 | "markdown-image.base.fileFormat": "剪贴板图像文件格式。您可以使用`png`或`jpg`。", 30 | "markdown-image.base.codeType": "插入代码的类型", 31 | "markdown-image.base.codeType.Markdown": "Markdown 代码,比如:![image](./image/file/path.jpg)", 32 | "markdown-image.base.codeType.HTML": "HTML 代码,比如:\"image\"", 33 | "markdown-image.base.codeType.DIY": "自定义插入代码,你需要通过 `#markdown-image.base.codeFormat#` 配置代码格式。", 34 | "markdown-image.base.codeFormat": "自定义插入代码的格式,需设置 `#markdown-image.base.codeType#` 为 `DIY`。你可以使用 `${src}` 和 `${alt}` 变量,分别表示图片地址与默认占位符(由 `#markdown-image.base.altFormat#` 定义)。比如:`![${alt}](${src})`。也可以使用 `#markdown-image.base.fileNameFormat#` 中的所有变量。", 35 | "markdown-image.base.imageWidth": "图片的最大宽度,若图片大于这个宽度,则会设置宽度为该值。设置为 0 则表示不设置。", 36 | "markdown-image.base.altFormat": "Markdown 代码的占位符。你有一些变量可以使用: \n\n- `${filename}`: 图片原始的文件名。 \n- `${mdname}`: 正在编辑的 Markdown 文件的名称(不含扩展名)。 \n- `${hash}`: 图像的 sha256 哈希。 \n- `${timestamp}`: 上传时间的时间戳。 \n- `${YY}`: 年份 \n- `${MM}`: 月份 \n- `${DD}`: 日期 \n- `${hh}`: 12小时制小时 \n- `${HH}`: 24小时制小时 \n- `${mm}`: 分 \n- `${ss}`: 秒 \n- `${mss}`: 毫秒 \n- `${rand,number}`: 随机数, 比如:`${rand,100}`. 它将生成从 0 到 99 的随机数。\n- `${index}`: 文件通过插件粘贴的图片 index。\n- `${prompt}`: 通过输入框输入自定义名称。", 37 | "markdown-image.base.altFormat.default": "图 ${index}", 38 | "markdown-image.local.path": "本地的图片存储目录(如果不存在,则自动创建)。支持相对路径,相对于所粘贴的 Markdown 文件。 `/` 表示打开的文件夹根目录。注意:您不能在此处使用变量。您可以在 `#markdown-image.base.fileNameFormat#` 中使用变量。", 39 | "markdown-image.local.referencePath": "Markdown 中的图片的引用路径格式(不包含文件名)。留空表示使用相对路径。 你可以使用 `#markdown-image.base.fileNameFormat#` 中的所有变量。例如:`/images/${YY}-${MM}/`", 40 | "markdown-image.coding.path": "仓库中的图片保存目录(如果不存在,则自动创建)。", 41 | "markdown-image.coding.token": "Coding 的个人[访问令牌](https://help.coding.net/docs/member/tokens.html),用于访问仓库,上传图片。", 42 | "markdown-image.coding.repository": "所要上传的目的仓库,比如:`https://coding-demo.coding.net/p/coding-demo/d/coding-demo/git`。仓库必须要先初始化。", 43 | "markdown-image.github.path": "仓库中的图片保存目录(如果不存在,则自动创建)。仓库需要先初始化。", 44 | "markdown-image.github.token": "GitHub 的个人[访问令牌](https://github.com/settings/tokens),用于访问仓库,上传图片。", 45 | "markdown-image.github.repository": "所要上传的目的仓库,比如:`https://github.com/username/repository/`。仓库必须要先初始化。", 46 | "markdown-image.github.branch": "要存放图片的仓库分支。", 47 | "markdown-image.github.cdn": "要使用的 CDN 地址格式,${username} 表示上传仓库的用户名,${repository} 表示上传的仓库,${branch} 表示上传的分支,${filepath} 表示上传的仓库目录与文件名。", 48 | "markdown-image.github.httpProxy": "通过 HTTP 代理连接到 Github", 49 | "markdown-image.imgur.clientId": "在 `imgur` 注册的 `Client Id`。您可以在[这儿](https://api.imgur.com/oauth2/addclient)注册。", 50 | "markdown-image.imgur.httpProxy": "通过 HTTP 代理连接到 Imgur。", 51 | "markdown-image.sm_ms.token": "SM.MS Secret Token。您可以注册一个帐户,然后访问 [API Access](https://smms.app/home/apitoken) 页面以生成。", 52 | "markdown-image.qiniu.accessKey": "七牛账户的 Access Key。", 53 | "markdown-image.qiniu.secretKey": "七牛账户的 Secret Key。", 54 | "markdown-image.qiniu.bucket": "七牛的对象存储空间名。", 55 | "markdown-image.qiniu.domain": "七牛空间绑定的域名。", 56 | "markdown-image.qiniu.zone": "七牛空间的存储区域。", 57 | "markdown-image.qiniu.east": "华东", 58 | "markdown-image.qiniu.north": "华北", 59 | "markdown-image.qiniu.south": "华南", 60 | "markdown-image.qiniu.na": "北美", 61 | "markdown-image.qiniu.sa": "东南亚", 62 | "markdown-image.upyun.bucket": "又拍云的云存储服务名称。", 63 | "markdown-image.upyun.domain": "又拍云云存储服务绑定的域名。", 64 | "markdown-image.upyun.operator": "又拍云的操作员名。", 65 | "markdown-image.upyun.password": "又拍云的操作员密码。", 66 | "markdown-image.upyun.path": "又拍云图片存储路径。", 67 | "markdown-image.upyun.link": "又拍云链接线路。", 68 | "markdown-image.upyun.smart": "智能选路(推荐):v0.api.upyun.com", 69 | "markdown-image.upyun.telecom": "电信线路:v1.api.upyun.com", 70 | "markdown-image.upyun.unicom": "联通(网通)线路:v2.api.upyun.com", 71 | "markdown-image.upyun.mobile": "移动(铁通)线路:v3.api.upyun.com", 72 | "markdown-image.DIY.path": "你写的代码的绝对路径。你可以写一个 Node.js 代码文件用于上传,并在此处填写文件路径。你的代码必须 exports 一个像 `async function (filePath:string, savePath:string, markdownPath:string):string` 的函数。\n\n比如:\n\n ```javascript\nconst path = require('path');\nmodule.exports = async function(filePath, savePath, markdownPath) {\n\t// Return a picture access link\n\treturn path.relative(path.dirname(markdownPath), filePath); \n}\n```\n函数参数分别是:\n- `filePath`: 文件的绝对路径。 \n- `savePath`: 保存的文件的路径,根据 `#markdown-image.base.fileNameFormat#` 生成。 \n- `markdownPath`: 正在编辑的 markdown 文件的路径。", 73 | "markdown-image.cloudinary.cloudName": "你的 Cloudinary 用户名", 74 | "markdown-image.cloudinary.apiKey": "你的 Cloudinary 账户 API key", 75 | "markdown-image.cloudinary.apiSecret": "你的 Cloudinary 账户 API secret", 76 | "markdown-image.cloudinary.folder": "你所要上传到的文件夹", 77 | "markdown-image.cloudflare.accountId": "你的帐户名称", 78 | "markdown-image.cloudflare.apiToken": "Cloudflare API 令牌", 79 | "markdown-image.s3.endpoint": "你的 S3 API 端点,是从存储桶设置或 dashboard 获得的。应包括协议,比如:`http/https`。", 80 | "markdown-image.s3.region": "你的 S3 存储桶区域,是从存储桶设置中获得的。", 81 | "markdown-image.s3.bucketName": "你的 S3 存储桶名称。", 82 | "markdown-image.s3.accessKeyId": "你的 S3 access key ID。", 83 | "markdown-image.s3.secretAccessKey": "你的 S3 secret access key。", 84 | "markdown-image.s3.cdn": "设置你的 S3 CDN URL。你可以使用变量 `${bucket}` `${region}` `${pathname}` and `${filepath}`。比如:`https://${bucket}.${region}.s3.amazonaws.com/${pathname}/${filepath}`。", 85 | "markdown-image.s3.config": "S3Client 的其它配置项目。你可以在[此处](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3/)中获得所有选项。", 86 | "markdown-image.sftp.host": "远程服务器地址。", 87 | "markdown-image.sftp.port": "SSH 服务端口。", 88 | "markdown-image.sftp.username": "远程用户名。", 89 | "markdown-image.sftp.password": "SSH 密码。", 90 | "markdown-image.sftp.privateKeyPath": "远程私钥文件路径。", 91 | "markdown-image.sftp.path": "远程服务器的图片存储目录(如果不存在,则自动创建)。支持相对路径,相对于所粘贴的 Markdown 文件。 `/` 表示打开的文件夹根目录。注意:您不能在此处使用变量。您可以在 `#markdown-image.base.fileNameFormat#` 中使用变量。", 92 | "markdown-image.sftp.referencePath": "Markdown 中的图片的引用路径格式(不包含文件名)。留空表示使用相对路径。 你可以使用 `#markdown-image.base.fileNameFormat#` 中的所有变量。例如:`/images/${YY}-${MM}/`", 93 | "markdown-image.azure.authenticationMethod": "用于 Azure Blob 存储帐户的身份验证方法。默认值为 `Passwordless`。您可以从[这里](https://learn.microsoft.com/zh-cn/azure/storage/blobs/storage-quickstart-blobs-nodejs?tabs=managed-identity%2Croles-azure-portal%2Csign-in-azure-cli#authenticate-to-azure-and-authorize-access-to-blob-data)获取更多信息。", 94 | "markdown-image.azure.authenticationMethod.Passwordless": "使用帐户名称进行身份验证。您必须首先执行 `az login` 登录到 Azure CLI。", 95 | "markdown-image.azure.authenticationMethod.ConnectionString": "使用连接字符串进行身份验证。", 96 | "markdown-image.azure.accountName": "Azure Blob 存储帐户。", 97 | "markdown-image.azure.connect-string": "Azure 存储连接字符串。", 98 | "markdown-image.azure.container": "Azure 存储容器名称。" 99 | } 100 | -------------------------------------------------------------------------------- /package.nls.zh-tw.json: -------------------------------------------------------------------------------- 1 | { 2 | "markdown-image.title": "Markdown 圖片", 3 | "markdown-image.local": "本地", 4 | "markdown-image.qiniu": "七牛", 5 | "markdown-image.upyun": "又拍雲", 6 | "markdown-image.DIY": "自定義", 7 | "markdown-image.Cloudinary": "Cloudinary", 8 | "markdown-image.command.paste": "粘貼圖片", 9 | "markdown-image.command.config": "Markdown 圖片配置", 10 | "markdown-image.command.paste-rich-text": "粘貼富文本 (Beta)", 11 | "markdown-image.base.uploadMethod": "上傳圖片的方式。到本地或其他圖片 CDN 服務。\n\n- 本地 [設置項目](#markdown-image.local.path)\n\n- GitHub [設置項目](#markdown-image.github.branch)\n\n- Imgur [設置項目](#markdown-image.imgur.clientId)\n\n- SM.MS [設置項目](#markdown-image.sm_ms.token)\n\n- 七牛 [設置項目](#markdown-image.qiniu.accessKey)\n\n- 又拍雲 [設置項目](#markdown-image.upyun.bucket)\n\n- 自定義 [設置項目](#markdown-image.DIY.path)\n\n- Cloudinary [設置項目](#markdown-image.cloudinary.apiKey)\n\n- Cloudflare [設置項目](#markdown-image.cloudflare.accountId)\n\n- S3 [設置項目](#markdown-image.s3.endpoint)", 12 | "markdown-image.base.uploadMethod.Local": "將圖像上傳到項目目錄。您可以使用 markdown-image.local.path 設置上傳的相對路徑。", 13 | "markdown-image.base.uploadMethod.Coding": "將圖像上傳到 Coding.net 中的 Git 存儲庫。您可以通過markdown-image.coding.repository 配置存儲庫. 您必須通過 markdown-image.coding.token 配置個人訪問令牌訪問存儲庫。", 14 | "markdown-image.base.uploadMethod.Imgur": "將圖像上傳到 Imgur。您可以通過 markdown-image.imgur.clientId 配置`Client Id`。", 15 | "markdown-image.base.uploadMethod.SM.MS": "將圖像上傳到 sm.ms 。 您如果有一個帳戶,可以通過 markdown-image.sm_ms.token 配置 `Secret Token` 。", 16 | "markdown-image.base.uploadMethod.DataURL": "將圖片轉爲Data URL 插入.", 17 | "markdown-image.base.uploadMethod.Qiniu": "將圖片上傳到七牛對象存儲,你有很多項目需要配置。", 18 | "markdown-image.base.uploadMethod.Upyun": "將圖片上傳到又拍雲對象存儲,你有很多項目需要配置。", 19 | "markdown-image.base.uploadMethod.DIY": "您可以自定義用於上傳的代碼。您需要通過 markdown-image.DIY.path 配置代碼路徑。", 20 | "markdown-image.base.uploadMethod.Cloudinary": "將圖片上傳到 Cloudinary。您需要配置 Cloudinary 的用戶名、API key 和 API secret。", 21 | "markdown-image.base.uploadMethod.Cloudflare": "將圖片上傳到 Cloudflare Workers KV。您需要配置 Cloudflare 的帳戶名和 API 令牌。", 22 | "markdown-image.base.uploadMethod.S3": "將圖片上傳到 Amazon S3。您需要配置 S3 的 API 端點、區域、存儲桶名、Access Key ID 和 Secret Access Key", 23 | "markdown-image.base.uploadMethod.SFTP": "將圖片上傳到 SFTP 服務器。您需要配置 SFTP 服務器地址、端口、賬戶、密碼或私鑰文件路徑。", 24 | "markdown-image.base.uploadMethod.Azure": "將圖片上傳到 Azure Blob 存儲。您需要配置 Azure 存儲帳戶連接字符串和容器名。", 25 | "markdown-image.base.fileNameFormat": "上傳的檔案名稱與路徑的格式字串。不支援 Imgur 和 SM.MS。您可以使用以下變數:\n\n- ${filename}:圖片原始的檔案名稱。\n- ${mdname}:正在編輯的 Markdown 檔案的名稱(不包含副檔名)。\n- ${path}:正在編輯的 Markdown 檔案相對於根目錄的路徑。\n- ${hash}:圖片的 sha256 雜湊。\n- ${timestamp}:上傳時間的時間戳記。\n- ${YY}:年份\n- ${MM}:月份\n- ${DD}:日期\n- ${hh}:12 小時制的小時\n- ${HH}:24 小時制的小時\n- ${mm}:分鐘\n- ${ss}:秒\n- ${mss}:毫秒\n- ${rand,number}:隨機數,例如:${rand,100}。它將生成從 0 到 99 的隨機數。\n- ${prompt}:在貼上圖片時可通過輸入框輸入自訂名稱。", 26 | "markdown-image.base.uploadMethods": "可用於並行上傳的多個上傳方法。默認為空,列表應包含上述提到的 `#markdown-image.base.uploadMethod#` 選項的可選值。配置的上傳方法的上傳結果將不會插入到 Markdown 文件中。", 27 | "markdown-image.base.urlEncode": "是否對圖像的 URL 編碼。", 28 | "markdown-image.base.fileFormat": "剪貼板圖像文件格式。 您可以使用`png`或`jpg`。", 29 | "markdown-image.base.codeType": "插入代碼的類型", 30 | "markdown-image.base.codeType.Markdown": "Markdown 代碼,比如:![image](./image/file/path.jpg)", 31 | "markdown-image.base.codeType.HTML": "HTML 代碼,比如:\"image\"", 32 | "markdown-image.base.codeType.DIY": "自定義插入代碼,你需要通過 `#markdown-image.base.codeFormat#` 配置代碼格式。", 33 | "markdown-image.base.codeFormat": "自定義插入代碼的格式,需設置 `#markdown-image.base.codeType#` 爲 `DIY`。你可以使用 `${src}` 和 `${alt}` 變量,分別表示圖片地址與默認佔位符(由 `#markdown-image.base.altFormat#` 定義)。比如:`![${alt}](${src})`。也可以使用 `#markdown-image.base.fileNameFormat#` 中的所有變量。", 34 | "markdown-image.base.imageWidth": "圖片的最大寬度,若圖片大於這個寬度,則會設置寬度為該值。設置為 0 則表示不設置。", 35 | "markdown-image.base.altFormat": "Markdown 代碼的佔位符。您可以使用以下變量:\n\n- `${filename}`:圖片原始的檔案名稱。\n- `${mdname}`:正在編輯的 Markdown 檔案的名稱(不包含副檔名)。\n- `${hash}`:圖像的 sha256 雜湊。\n- `${timestamp}`:上傳時間的時間戳。\n- `${YY}`:年份\n- `${MM}`:月份\n- `${DD}`:日期\n- `${hh}`:12 小時制的小時\n- `${HH}`:24 小時制的小時\n- `${mm}`:分鐘\n- `${ss}`:秒\n- `${mss}`:毫秒\n- `${rand,number}`:隨機數,例如:`${rand,100}`。它將生成從 0 到 99 的隨機數。\n- `${index}`:插件粘貼到文件中的圖片索引。\n- `${prompt}`: 通過輸入框輸入自定義名稱。", 36 | "markdown-image.base.altFormat.default": "圖 ${index}", 37 | "markdown-image.local.path": "本地的圖片存儲目錄(如果不存在,則自動創建)。支持相對路徑,相對於所粘貼的 Markdown 文件。`/` 表示打開的文件夾根目錄。注意:您不能在此處使用變量。您可以在 `#markdown-image.base.fileNameFormat#` 中使用變量。", 38 | "markdown-image.local.referencePath": "Markdown 中的圖片的引用路徑格式(不包含文件名)。留空表示使用相對路徑。你可以使用 `#markdown-image.base.fileNameFormat#` 中的所有變量。例如:`/images/${YY}-${MM}/`", 39 | "markdown-image.coding.path": "倉庫中的圖片保存目錄(如果不存在,則自動創建)。", 40 | "markdown-image.coding.token": "Coding 的個人[訪問令牌](https://help.coding.net/docs/member/tokens.html),用於訪問倉庫,上傳圖片。", 41 | "markdown-image.coding.repository": "所要上傳的目的倉庫,比如:`https://coding-demo.coding.net/p/coding-demo/d/coding-demo/git`。倉庫必須要先初始化。", 42 | "markdown-image.github.path": "倉庫中的圖片保存目錄(如果不存在,則自動創建)。", 43 | "markdown-image.github.token": "GitHub 的個人[訪問令牌](https://github.com/settings/tokens),用於訪問倉庫,上傳圖片。", 44 | "markdown-image.github.repository": "所要上傳的目的倉庫,比如:`https://github.com/username/repository/`。倉庫必須要先初始化。 ", 45 | "markdown-image.github.branch": "要存放圖片的倉庫分支。", 46 | "markdown-image.github.cdn": "要使用的 CDN 地址格式,${username} 表示上傳倉庫的用戶名,${repository} 表示上傳的倉庫,${branch} 表示上傳的分支,${filepath} 表示上傳的倉庫目錄與文件名。", 47 | "markdown-image.github.httpProxy": "通過 HTTP 代理連接到 Github。", 48 | "markdown-image.imgur.clientId": "在 `imgur` 註冊的 `Client Id`。您可以在[這兒](https://api.imgur.com/oauth2/addclient)註冊。", 49 | "markdown-image.imgur.httpProxy": "通過 HTTP 代理連接到 Imgur。", 50 | "markdown-image.sm_ms.token": "SM.MS Secret Token。您可以註冊一個帳戶,然後訪問 [API Access](https://smms.app/home/apitoken) 頁面生成。", 51 | "markdown-image.qiniu.accessKey": "七牛賬戶的 Access Key。", 52 | "markdown-image.qiniu.secretKey": "七牛賬戶的 Secret Key。", 53 | "markdown-image.qiniu.bucket": "七牛的對象存儲空間名。", 54 | "markdown-image.qiniu.domain": "七牛空間綁定的域名。", 55 | "markdown-image.qiniu.zone": "七牛空間的存儲區域。", 56 | "markdown-image.qiniu.east": "華東", 57 | "markdown-image.qiniu.north": "華北", 58 | "markdown-image.qiniu.south": "華南", 59 | "markdown-image.qiniu.na": "北美", 60 | "markdown-image.qiniu.sa": "東南亞", 61 | "markdown-image.upyun.bucket": "又拍雲的雲存儲服務名稱。", 62 | "markdown-image.upyun.domain": "又拍雲雲存儲服務綁定的域名。", 63 | "markdown-image.upyun.operator": "又拍雲的操作員名。", 64 | "markdown-image.upyun.password": "又拍雲的操作員密碼。", 65 | "markdown-image.upyun.path": "又拍雲圖片存儲路徑。", 66 | "markdown-image.upyun.link": "又拍雲鏈接線路。", 67 | "markdown-image.upyun.smart": "智能選路(推薦):v0.api.upyun.com", 68 | "markdown-image.upyun.telecom": "電信線路:v1.api.upyun.com", 69 | "markdown-image.upyun.unicom": "聯通(網通)線路:v2.api.upyun.com", 70 | "markdown-image.upyun.mobile": "移動(鐵通)線路:v3.api.upyun.com", 71 | "markdown-image.DIY.path": "你寫的代碼的絕對路徑。你可以寫一個 Node.js 代碼文件用於上傳,並在此處填寫文件路徑。你的代碼必須 exports 一個像 `async function (filePath:string, savePath:string, markdownPath:string):string` 的函數。\n \n比如:\n\n ```javascript\nconst path = require('path');\nmodule.exports = async function(filePath, savePath, markdownPath) {\n\t// Return a picture access link\n\treturn path.relative(path.dirname(markdownPath), filePath); \n}\n```\n函數參數分別是:\n- `filePath`: 文件的絕對路徑。 \n- `savePath` : 保存的文件的路徑,根據 `#markdown-image.base.fileNameFormat#` 生成。 \n- `markdownPath`: 正在編輯的 markdown 文件的路徑。", 72 | "markdown-image.cloudinary.cloudName": "你的 Cloudinary 用戶名", 73 | "markdown-image.cloudinary.apiKey": "你的 Cloudinary 賬戶 API key", 74 | "markdown-image.cloudinary.apiSecret": "你的 Cloudinary 賬戶 API secret", 75 | "markdown-image.cloudinary.folder": "你所要上傳到的文件夾", 76 | "markdown-image.cloudflare.accountId": "你的帳戶名稱", 77 | "markdown-image.cloudflare.apiToken": "Cloudflare API 令牌", 78 | "markdown-image.s3.endpoint": "你的 S3 API 端點,是从存儲桶設置或 dashboard 獲得的。應包括協議,比如:`http/https`。", 79 | "markdown-image.s3.region": "你的 S3 存儲桶區域,是從存儲桶設置中獲得的。", 80 | "markdown-image.s3.bucketName": "你的 S3 存儲桶名稱。", 81 | "markdown-image.s3.accessKeyId": "你的 S3 access key ID.", 82 | "markdown-image.s3.secretAccessKey": "你的 S3 secret access key.", 83 | "markdown-image.s3.cdn": "設定你的 S3 CDN URL。你可以使用變數 `${bucket}` `${region}` `${pathname}` and `${filepath}`。比如:`https://${bucket}.${region}.s3.amazonaws.com/${pathname}/${filepath}`。", 84 | "markdown-image.s3.config": "S3Client 的其它配置項目。你可以在[此處](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3/)中获得所有选项。", 85 | "markdown-image.sftp.host": "遠端伺服器地址。", 86 | "markdown-image.sftp.port": "SSH 服務端口。", 87 | "markdown-image.sftp.username": "遠端賬戶。", 88 | "markdown-image.sftp.password": "SSH 密碼。", 89 | "markdown-image.sftp.privateKeyPath": "遠端私鑰文件路徑。", 90 | "markdown-image.sftp.path": "遠端伺服器的圖片存儲目錄(如果不存在,則自動創建)。支持相對路徑,相對於所粘貼的 Markdown 文件。 `/` 表示打開的文件夾根目錄。注意:您不能在此處使用變量。您可以在 `#markdown-image.base.fileNameFormat#` 中使用變量。", 91 | "markdown-image.sftp.referencePath": "Markdown 中的圖片的引用路徑格式(不包含文件名)。留空表示使用相對路徑。你可以使用 `#markdown-image.base.fileNameFormat#` 中的所有變量。例如:`/images/${YY}-${MM}/`", 92 | "markdown-image.azure.authenticationMethod": "用於 Azure Blob 存儲帳戶的身份驗證方法。 默認值為 `Passwordless`。 您可以從[這裡](https://learn.microsoft.com/zh-tw/azure/storage/blobs/storage-quickstart-blobs-nodejs?tabs=managed-identity%2Croles-azure-portal%2Csign-in-azure-cli#authenticate-to-azure-and-authorize-access-to-blob-data)獲取更多信息。", 93 | "markdown-image.azure.authenticationMethod.Passwordless": "使用帳戶名稱進行身份驗證。 您必須首先執行 `az login` 登錄到 Azure CLI。", 94 | "markdown-image.azure.authenticationMethod.ConnectionString": "使用連接字符串進行身份驗證。", 95 | "markdown-image.azure.accountName": "Azure Blob 存儲帳戶名。", 96 | "markdown-image.azure.connect-string": "Azure 存儲帳戶連接字符串。", 97 | "markdown-image.azure.container": "Azure 存儲容器名。" 98 | } 99 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // The module 'vscode' contains the VS Code extensibility API 3 | // Import the module and reference it with the alias vscode in your code below 4 | import * as vscode from 'vscode'; 5 | import * as path from 'path'; 6 | import utils from './lib/utils'; 7 | import { locale as $l } from './lib/utils'; 8 | import { imageSize } from 'image-size'; 9 | 10 | function getUploadInstance(config: any) { 11 | let uploads: Upload[] = []; 12 | 13 | // Get All Upload Methods 14 | let uploadMethods = config.base.uploadMethods || []; 15 | uploadMethods.unshift(config.base.uploadMethod); 16 | uploadMethods = Array.from(new Set(uploadMethods)); 17 | 18 | // Load All Upload Method Instance 19 | for (const t of uploadMethods) { 20 | const upload = utils.getUpload(t, config) 21 | if (upload) uploads.push(upload); 22 | } 23 | 24 | return uploads; 25 | } 26 | 27 | export function activate(context: vscode.ExtensionContext) { 28 | let config = utils.getConfig(); 29 | let uploads: Upload[] = getUploadInstance(config); 30 | 31 | let pasteCommand = vscode.commands.registerCommand('markdown-image.paste', async () => { 32 | let stop = () => {}; 33 | try { 34 | stop = utils.showProgress($l['uploading']); 35 | 36 | let editor = vscode.window.activeTextEditor; 37 | let selections = utils.getSelections(); 38 | let savePath = utils.getTmpFolder(); 39 | savePath = path.resolve(savePath, `pic_${new Date().getTime()}.png`); 40 | let images = await utils.getPasteImage(savePath); 41 | images = images.filter(img => ['.jpg', '.jpeg', '.gif', '.bmp', '.png', '.webp', '.svg'].find(ext => img.endsWith(ext))); 42 | if (config.base.fileFormat == 'jpg' && images.length === 1 && images[0] == savePath) { 43 | images[0] = await utils.convertImage(images[0]) 44 | } 45 | 46 | let urls = [], maxWidth = []; 47 | for (let i = 0; i < images.length; i++) { 48 | let width = imageSize(images[i]).width || 0; 49 | maxWidth.push(config.base.imageWidth < width ? config.base.imageWidth : 0); 50 | let name = config.base.fileNameFormat ? await utils.formatName(config.base.fileNameFormat, images[i], savePath === images[i]) + (images[i] ? path.extname(images[i]) : '.png') : images[i]; 51 | console.debug(`Uploading ${images[i]} to ${name}.`); 52 | 53 | let p; 54 | for (let j = 0; j < uploads.length; j++) { 55 | let upload = uploads[j]; 56 | const result = await upload.upload(images[i], name); 57 | if (j == 0) p = result; 58 | } 59 | images[i] = name; 60 | if(p) { urls.push(p); } 61 | } 62 | 63 | let insertCode = '', insertTag = ''; 64 | for (let i = 0; i < urls.length; i++) { 65 | let selection = await utils.getAlt(config.base.altFormat, images[i], context); 66 | if (selections?.length === 1 && editor?.document.getText(selections[0])) { 67 | selection = `${editor?.document.getText(selections[0])} ${i + 1}`; 68 | } 69 | else if(selections?.[i] && editor?.document.getText(selections[i])) 70 | { 71 | selection = selections?.[i] && editor?.document.getText(selections[i]); 72 | } 73 | 74 | if (config.base.uploadMethod !== 'Data URL') { 75 | if(config.base.urlEncode) { 76 | urls[i] = encodeURIComponent(urls[i].toString()).replace(/%5C/g, '\\').replace(/%2F/g, '/').replace(/%3A/g, ':').replace(/%40/g, '@'); 77 | } else { 78 | urls[i] = urls[i].replaceAll(' ', '%20'); 79 | } 80 | let text = await utils.formatCode(urls[i], selection, maxWidth[i], config.base.codeType, config.base.codeFormat || '![${alt}](${src})'); 81 | if (selections?.[i] && selections?.length > 1) { 82 | await utils.editorEdit(selections[i], text); 83 | } 84 | else { 85 | insertCode += text; 86 | } 87 | } 88 | else 89 | { 90 | let tag = new Date().getTime().toString(); 91 | let text = await utils.formatCode(tag, selection, maxWidth[i], 'Markdown', ''); 92 | tag = `\n[${tag}]: ${urls[i]}`; 93 | if (selections?.[i] && selections?.length > 1) { 94 | await utils.insertToEnd(tag); 95 | await utils.editorEdit(selections[i], text); 96 | } 97 | else { 98 | insertCode += text; 99 | insertTag += tag; 100 | } 101 | } 102 | } 103 | 104 | if (insertCode) { 105 | let pos = editor?.selection.active; 106 | if (config.base.uploadMethod === 'Data URL') { 107 | await utils.insertToEnd(insertTag); 108 | } else if (pos) { 109 | await utils.editorEdit(pos, insertCode); 110 | } else { 111 | vscode.env.clipboard.writeText(insertCode); 112 | setTimeout(() => { 113 | vscode.commands.executeCommand("editor.action.clipboardPasteAction"); 114 | }, 100); 115 | } 116 | } 117 | 118 | utils.noticeComment(context); 119 | } catch (error) { 120 | let e = error as Error; 121 | vscode.window.showErrorMessage(`${$l['something_wrong']}${e.message in $l ? ($l)[e.message] : e.message}\n${e.toString()}`); 122 | } 123 | 124 | stop(); 125 | }); 126 | 127 | context.subscriptions.push(pasteCommand); 128 | 129 | let configCommand = vscode.commands.registerCommand('markdown-image.config', () => { 130 | vscode.commands.executeCommand('workbench.action.openSettings', 'markdown-image' ); 131 | }); 132 | 133 | context.subscriptions.push(configCommand); 134 | 135 | let richTextCommand = vscode.commands.registerCommand('markdown-image.paste-rich-text', async () => { 136 | let stop = () => {}; 137 | try { 138 | let editor = vscode.window.activeTextEditor; 139 | let text = await utils.getRichText(); 140 | 141 | if(text) { 142 | utils.editorEdit(editor?.selection, utils.html2Markdown(text)); 143 | } 144 | } catch (error) { 145 | let e = error as Error; 146 | vscode.window.showErrorMessage(`${$l['something_wrong']}${e.message}\n${e.toString()}`); 147 | } 148 | 149 | stop(); 150 | }); 151 | 152 | context.subscriptions.push(richTextCommand); 153 | 154 | vscode.workspace.onDidChangeConfiguration(function(event) { 155 | config = utils.getConfig(); 156 | uploads = getUploadInstance(config); 157 | }); 158 | } 159 | 160 | // this method is called when your extension is deactivated 161 | export function deactivate() { 162 | } 163 | -------------------------------------------------------------------------------- /src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as i18n from './locale'; 3 | 4 | export default () => { 5 | let locale = i18n.default; 6 | let lang = vscode.env.language; 7 | let langLocale: any = null; 8 | 9 | try { 10 | langLocale = i18n.$[lang]; 11 | } catch (error) { 12 | lang = lang.split('-')[0]; 13 | } 14 | 15 | try { 16 | langLocale = i18n.$[lang]; 17 | } catch (error) { } 18 | 19 | if (langLocale) { locale = Object.assign(locale, langLocale); } 20 | 21 | return locale; 22 | }; -------------------------------------------------------------------------------- /src/i18n/locale.ts: -------------------------------------------------------------------------------- 1 | import zh_cn from './locale.zh-cn'; 2 | import zh_tw from './locale.zh-tw'; 3 | 4 | export default { 5 | config_failed: "Config Failed: ", 6 | upload_failed: "Upload File Failed: ", 7 | save_failed: "Save File Failed: ", 8 | something_wrong: "Something was wrong: ", 9 | prompt_name_component: "Please enter a file name. The full name will be: ", 10 | user_did_not_answer_prompt: "User did not answer prompt", 11 | open_with_folder: "Please Open the project of the file with folder.", 12 | replace_or_no: "The file was exists. Would you replace it?", 13 | Yes: "Yes", 14 | No: "No", 15 | 'qiniu.east': "East China", 16 | 'qiniu.north': "North China", 17 | 'qiniu.south': "South China", 18 | 'qiniu.na': "North America", 19 | 'qiniu.sa': "Southeast Asia", 20 | 'upyun.smart': "Smart choose: v0.api.upyun.com", 21 | 'upyun.telecom': "China Telecom: v1.api.upyun.com", 22 | 'upyun.unicom': "China Unicom: v2.api.upyun.com", 23 | 'upyun.mobile': "China Mobile: v3.api.upyun.com", 24 | 'smms.token-miss': 'Please setting sm.ms token first.', 25 | named_paste: "Name the picture you pasted (don't include extname, it's will be replace the ${filename} in the format).", 26 | uploading: "Uploading...", 27 | picture: "picture", 28 | install_xclip: "You need to install xclip command first.", 29 | powershell_not_found: "The powershell command is not in you PATH environment variables. Please add it and retry.", 30 | "like.extension": "Do you like this extension? Hope to get your kindly favorable comments.", 31 | "like.ok": "Yes", 32 | "like.no": "No", 33 | "like.later": "Remind later", 34 | sftp_not_host: "You must config the ssh host.", 35 | sftp_not_port: "You must config the ssh port.", 36 | sftp_not_username: "You must config the ssh username.", 37 | sftp_not_password: "You must config a ssh password or private key file path.", 38 | sftp_connect_failed: "Connect to the ssh server failed: ", 39 | connection_string_empty: "The connection string is empty.", 40 | account_name_empty: "The account name is empty.", 41 | }; 42 | 43 | let $: any = { 'zh-cn': zh_cn, 'zh-tw': zh_tw }; 44 | export { $ }; -------------------------------------------------------------------------------- /src/i18n/locale.zh-cn.ts: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | config_failed: "配置失败了:", 4 | upload_failed: "上传文件失败了:", 5 | save_failed: "保存文件失败了:", 6 | something_wrong: "糟糕!出现一个我没想到的错误:", 7 | open_with_folder: "请使用打开文件夹的方式编辑文件。", 8 | replace_or_no: "这个文件已存在。要把它替换掉吗?", 9 | Yes: "可以", 10 | No: "不要", 11 | 'qiniu.east': "华东", 12 | 'qiniu.north': "华北", 13 | 'qiniu.south': "华南", 14 | 'qiniu.na': "北美", 15 | 'qiniu.sa': "东南亚", 16 | 'upyun.smart': "智能选路(推荐):v0.api.upyun.com", 17 | 'upyun.telecom': "电信线路:v1.api.upyun.com", 18 | 'upyun.unicom': "联通(网通)线路:v2.api.upyun.com", 19 | 'upyun.mobile': "移动(铁通)线路:v3.api.upyun.com", 20 | 'smms.token-miss': '请先设置 sm.ms token。', 21 | named_paste: "给你粘贴的图片起个名字(不需要包含后缀名,它将替换掉格式字符串中的 ${filename})。", 22 | uploading: "正在上传...", 23 | picture: "图", 24 | install_xclip: "您需要先安装 xclip 命令。", 25 | powershell_not_found: "Powershell 不在你的 PATH 环境变量中。请添加把他加进去之后重试。", 26 | "like.extension": "您喜欢这个扩展吗?给我一个好评吧!", 27 | "like.ok": "好啊", 28 | "like.no": "不要", 29 | "like.later": "等等再说", 30 | sftp_not_host: "您必须配置远程服务器地址。", 31 | sftp_not_port: "您必须配置 SSH 端口。", 32 | sftp_not_username: "您必须配置SSH 用户名。", 33 | sftp_not_password: "您必须配置 SSH 密码或私钥文件路径。", 34 | sftp_connect_failed: "连接到 SSH 服务器失败:", 35 | connection_string_empty: "连接字符串为空。", 36 | account_name_empty: "账户名为空。", 37 | }; -------------------------------------------------------------------------------- /src/i18n/locale.zh-tw.ts: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | config_failed: "配置失敗了:", 4 | upload_failed: "上傳文件失敗了:", 5 | save_failed: "保存文件失敗了:", 6 | something_wrong: "糟糕!出現一個我沒想到的錯誤:", 7 | open_with_folder: "請使用打開文件夾的方式編輯文件。", 8 | replace_or_no: "這個文件已存在。要把它替換掉嗎?", 9 | Yes: "可以", 10 | No: "不要", 11 | "qiniu.east": "華東", 12 | "qiniu.north": "華北", 13 | "qiniu.south": "華南", 14 | "qiniu.na": "北美", 15 | "qiniu.sa": "東南亞", 16 | 'upyun.smart': "智能選路(推薦):v0.api.upyun.com", 17 | 'upyun.telecom': "電信線路:v1.api.upyun.com", 18 | 'upyun.unicom': "聯通(網通)線路:v2.api.upyun.com", 19 | 'upyun.mobile': "移動(鐵通)線路:v3.api.upyun.com", 20 | 'smms.token-miss': '請先設置 sm.ms token。', 21 | named_paste: "給你粘貼的圖片起個名字(不需要包含後綴名,它將替換掉格式字符串中的 ${filename})。", 22 | uploading: "正在上傳...", 23 | picture: "圖", 24 | install_xclip: "您需要先安裝 xclip 命令。", 25 | powershell_not_found: "Powershell 不在你的 PATH 環境變量中。請添加把他加進去之後重試。", 26 | "like.extension": "您喜歡這個擴展嗎?給我一個好評吧!", 27 | "like.ok": "好啊", 28 | "like.no": "不要", 29 | "like.later": "等等再説", 30 | sftp_not_host: "您必須配置遠端伺服器地址。", 31 | sftp_not_port: "您必須配置 SSH 端口。", 32 | sftp_not_username: "您必須配置 SSH 賬戶。", 33 | sftp_not_password: "您必須配置 SSH 密碼或私鑰文件路徑。", 34 | sftp_connect_failed: "連接到 SSH 伺服器失敗:", 35 | connection_string_empty: "連接字符串為空。", 36 | account_name_empty: "賬戶名為空。", 37 | }; -------------------------------------------------------------------------------- /src/lib/azure.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { locale as $l } from './utils'; 3 | import { BlobServiceClient } from "@azure/storage-blob"; 4 | import { DefaultAzureCredential } from '@azure/identity'; 5 | 6 | class Azure implements Upload 7 | { 8 | config: Config; 9 | lastTimestamp: number; 10 | constructor(config: Config) { 11 | this.config = config; 12 | this.lastTimestamp = 0; 13 | } 14 | 15 | async reconfig(config: Config) { 16 | this.config = config; 17 | this.lastTimestamp = 0; 18 | } 19 | 20 | async upload(filePath: string, savePath: string): Promise { 21 | try { 22 | const { authenticationMethod, connectionString, accountName, container } = this.config.azure; 23 | let authMethod = authenticationMethod; 24 | if (connectionString !== '' && accountName === '') { 25 | authMethod = 'connectionString'; 26 | } 27 | else if (connectionString === '' && accountName !== '') { 28 | authMethod = 'Passwordless'; 29 | } 30 | if (authMethod === 'connectionString' && connectionString === '') { 31 | vscode.window.showInformationMessage($l['connection_string_empty']); 32 | return null; 33 | } 34 | if (authMethod === 'Passwordless' && accountName === '') { 35 | vscode.window.showInformationMessage($l['account_name_empty']); 36 | return null; 37 | } 38 | 39 | let blobServiceClient: BlobServiceClient; 40 | if (authMethod === 'connectionString') { 41 | blobServiceClient = BlobServiceClient.fromConnectionString( 42 | connectionString 43 | ); 44 | } else { 45 | blobServiceClient = new BlobServiceClient( 46 | `https://${accountName}.blob.core.windows.net`, 47 | new DefaultAzureCredential() 48 | ); 49 | } 50 | 51 | const containerClient = blobServiceClient.getContainerClient(container); 52 | const blockBlobClient = containerClient.getBlockBlobClient(savePath); 53 | await blockBlobClient.uploadFile(filePath); 54 | 55 | return blockBlobClient.url; 56 | } 57 | catch(error) { 58 | let e = error as Error; 59 | vscode.window.showInformationMessage(`${$l['upload_failed']}${e.message}`); 60 | return null; 61 | } 62 | } 63 | } 64 | 65 | export default Azure; -------------------------------------------------------------------------------- /src/lib/cloudflare.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as fs from 'fs'; 3 | import fetch from 'node-fetch'; 4 | import * as FormData from 'form-data'; 5 | import { locale as $l } from './utils'; 6 | 7 | const cloudflareUrl = (accountId: string) => 8 | `https://api.cloudflare.com/client/v4/accounts/${accountId}/images/v1`; 9 | 10 | class Cloudflare implements Upload { 11 | config: Config; 12 | constructor(config: Config) { 13 | this.config = config; 14 | } 15 | 16 | async getSavePath(filePath: string) { 17 | return filePath; 18 | } 19 | 20 | async reconfig(config: Config) { 21 | this.config = config; 22 | } 23 | 24 | async upload(filePath: string, savePath: string): Promise { 25 | try { 26 | const result = await this.doUpload(filePath, savePath); 27 | if (result.success) { 28 | return result.result.variants[0]; 29 | } 30 | throw new Error(result.errors[0].message); 31 | } catch (error) { 32 | let e = error as Error; 33 | vscode.window.showInformationMessage( 34 | `${$l['upload_failed']}${e.message}\n${e.stack}` 35 | ); 36 | return null; 37 | } 38 | } 39 | 40 | async doUpload(filePath: string, savePath: string): Promise { 41 | const { accountId, apiToken } = this.config.cloudflare; 42 | const formData = new FormData(); 43 | formData.append('file', fs.createReadStream(filePath), { 44 | filepath: savePath, 45 | }); 46 | const response = await fetch(cloudflareUrl(accountId), { 47 | method: 'POST', 48 | body: formData, 49 | headers: { 50 | authorization: `Bearer ${apiToken}`, 51 | }, 52 | }); 53 | return response.json(); 54 | } 55 | } 56 | 57 | export default Cloudflare; 58 | -------------------------------------------------------------------------------- /src/lib/cloudinary.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import * as path from "path"; 3 | import utils, { locale as $l } from "./utils"; 4 | import { UploadApiResponse, v2 as cloudinary } from "cloudinary"; 5 | 6 | class Cloudinary implements Upload { 7 | config: Config; 8 | constructor(config: Config) { 9 | this.config = config; 10 | } 11 | 12 | async getSavePath(filePath: string) { 13 | return filePath; 14 | } 15 | 16 | async reconfig(config: Config) { 17 | this.config = config; 18 | } 19 | 20 | async upload(filePath: string, savePath: string): Promise { 21 | try { 22 | let result = await this.doUpload(filePath, savePath, { 23 | overwrite: false, 24 | }); 25 | if (result.existing) { 26 | const replace = await utils.confirm($l["replace_or_no"], [ 27 | $l["Yes"], 28 | $l["No"], 29 | ]); 30 | if (replace === $l["No"]) { 31 | return null; 32 | } 33 | // yes, so re-upload with overwrite: true 34 | result = await this.doUpload(filePath, savePath, { overwrite: true }); 35 | } 36 | return result.secure_url; 37 | } catch (error) { 38 | let e = error as Error; 39 | vscode.window.showInformationMessage( 40 | `${$l["upload_failed"]}${e.message}`, 41 | ); 42 | return null; 43 | } 44 | } 45 | 46 | async doUpload( 47 | filePath: string, 48 | savePath: string, 49 | options: { overwrite: boolean }, 50 | ): Promise { 51 | cloudinary.config({ 52 | cloud_name: this.config.cloudinary.cloudName, 53 | api_key: this.config.cloudinary.apiKey, 54 | api_secret: this.config.cloudinary.apiSecret, 55 | }); 56 | 57 | const folder = path.join( 58 | this.config.cloudinary.folder, 59 | path.dirname(savePath), 60 | ); 61 | const filename = path.basename(savePath); 62 | 63 | return cloudinary.uploader.upload(filePath, { 64 | folder: folder, 65 | filename_override: filename, 66 | use_filename: true, 67 | unique_filename: false, 68 | overwrite: options.overwrite, 69 | fetch_format: "auto", 70 | quality: "auto", 71 | }); 72 | } 73 | } 74 | 75 | export default Cloudinary; 76 | -------------------------------------------------------------------------------- /src/lib/coding.ts: -------------------------------------------------------------------------------- 1 | import utils from './utils'; 2 | import { locale as $l } from './utils'; 3 | import * as vscode from 'vscode'; 4 | import { Coding as CodingPicbed } from 'coding-picbed'; 5 | 6 | class Coding implements Upload 7 | { 8 | config: Config; 9 | static coding: any; 10 | constructor(config: Config) { 11 | if (!Coding.coding) { Coding.coding = new CodingPicbed(); } 12 | if(!Coding.coding.lastconfig || 13 | Coding.coding.lastconfig.token !== config.coding.token || 14 | Coding.coding.lastconfig.repository !== config.coding.repository 15 | ) { 16 | this.reconfig(config); 17 | } 18 | this.config = config; 19 | } 20 | 21 | async reconfig(config: Config) { 22 | try { 23 | this.config = config; 24 | Coding.coding.lastconfig = config.coding; 25 | await Coding.coding.config({ token: config.coding.token, repository: config.coding.repository }); 26 | } catch (error) { 27 | let e = error as Error; 28 | vscode.window.showErrorMessage(`${$l['config_failed']}${e.message}`); 29 | } 30 | } 31 | 32 | async upload(filePath: string, savePath: string): Promise { 33 | try { 34 | while (!Coding.coding.isInitialized()) { await utils.sleep(100); } 35 | 36 | let saveFolder = this.config.coding.path; 37 | let data = await Coding.coding.upload(filePath, saveFolder.replace(/\\/g, '/'), savePath.replace(/\\/g, '/')); 38 | 39 | return data.urls[0].replace('http:', 'https:'); 40 | } catch (error) { 41 | let e = error as Error; 42 | vscode.window.showInformationMessage(`${$l['upload_failed']}${e.message}`); 43 | return null; 44 | } 45 | } 46 | } 47 | 48 | export default Coding; -------------------------------------------------------------------------------- /src/lib/dataurl.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import * as vscode from 'vscode'; 4 | import { locale as $l } from './utils'; 5 | 6 | class Local implements Upload 7 | { 8 | config: Config; 9 | constructor(config: Config) { 10 | this.config = config; 11 | } 12 | 13 | async reconfig(config: Config) { 14 | this.config = config; 15 | } 16 | 17 | async upload(filePath: string, savePath: string): Promise { 18 | try { 19 | return 'data:image/' + path.extname(filePath).substr(1) + ';base64,' + Buffer.from(fs.readFileSync(filePath)).toString('base64'); 20 | } 21 | catch(error) { 22 | let e = error as Error; 23 | vscode.window.showInformationMessage(`${$l['save_failed']}${e.message}`); 24 | return null; 25 | } 26 | } 27 | } 28 | 29 | export default Local; -------------------------------------------------------------------------------- /src/lib/define.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import utils from './utils'; 3 | import { locale as $l } from './utils'; 4 | 5 | class Define implements Upload 6 | { 7 | config: Config; 8 | constructor(config: Config) { 9 | this.config = config; 10 | } 11 | 12 | async reconfig(config: Config) { 13 | this.config = config; 14 | } 15 | 16 | async upload(filePath: string, savePath: string): Promise { 17 | try { 18 | delete require.cache[this.config.DIY.path]; 19 | let define = require(this.config.DIY.path); 20 | return await define(filePath, savePath, utils.getCurrentFilePath()); 21 | } 22 | catch(error) { 23 | let e = error as Error 24 | vscode.window.showInformationMessage(`${$l['upload_failed']}${e.message}`); 25 | return null; 26 | } 27 | } 28 | } 29 | 30 | export default Define; -------------------------------------------------------------------------------- /src/lib/github.d.ts: -------------------------------------------------------------------------------- 1 | export function GitHubPic(options: any): any 2 | -------------------------------------------------------------------------------- /src/lib/github.ts: -------------------------------------------------------------------------------- 1 | import utils from './utils'; 2 | import { locale as $l } from './utils'; 3 | import * as path from 'path'; 4 | import * as vscode from 'vscode'; 5 | import * as GitHubPic from 'github-picbed'; 6 | 7 | class GitHub implements Upload 8 | { 9 | config: Config; 10 | static github: any; 11 | constructor(config: Config) { 12 | if (!GitHub.github) { GitHub.github = GitHubPic(config.github); } 13 | if(!GitHub.github.lastconfig || 14 | GitHub.github.lastconfig.token !== config.github.token || 15 | GitHub.github.lastconfig.branch !== config.github.branch || 16 | GitHub.github.lastconfig.repository !== config.github.repository 17 | ) { 18 | this.reconfig(config); 19 | } 20 | this.config = config; 21 | } 22 | 23 | async reconfig(config: Config) { 24 | try { 25 | this.config = config; 26 | GitHub.github.lastconfig = config.github; 27 | await GitHub.github.config(config.github); 28 | } catch (error) { 29 | let e = error as Error; 30 | vscode.window.showErrorMessage(`${$l['config_failed']}${e.message}`); 31 | } 32 | } 33 | 34 | async upload(filePath: string, savePath: string): Promise { 35 | try { 36 | while (!GitHub.github.isInitialized()) { await utils.sleep(100); } 37 | 38 | savePath = path.join(this.config.github.path, savePath).replace(/\\/g, '/').replace(/^\/|\/$/, ''); 39 | let data = await GitHub.github.upload({ data: filePath, filename: savePath }); 40 | let options = GitHub.github.options; 41 | 42 | return this.config.github.cdn 43 | .replace(/\${username}/g, options.username) 44 | .replace(/\${repository}/g, options.repository) 45 | .replace(/\${branch}/g, options.branch) 46 | .replace(/\${filepath}/g, data.filename) 47 | 48 | } catch (error) { 49 | let e = error as Error; 50 | vscode.window.showErrorMessage(`${$l['upload_failed']}${e.message}`); 51 | return null; 52 | } 53 | } 54 | } 55 | 56 | export default GitHub; -------------------------------------------------------------------------------- /src/lib/imgur.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as HttpsProxyAgent from 'https-proxy-agent'; 3 | import got from 'got'; 4 | import * as formData from 'form-data'; 5 | import * as fs from 'fs'; 6 | import * as path from 'path'; 7 | import { locale as $l } from './utils'; 8 | 9 | class Imgur implements Upload 10 | { 11 | config: Config; 12 | constructor(config: Config) { 13 | this.config = config; 14 | } 15 | 16 | async getSavePath(filePath: string) { 17 | return filePath; 18 | } 19 | 20 | async reconfig(config: Config) { 21 | this.config = config; 22 | } 23 | 24 | async upload(filePath: string, savePath: string): Promise { 25 | try { 26 | let tunnel = this.config.imgur.httpProxy ? new HttpsProxyAgent(this.config.imgur.httpProxy) : undefined; 27 | 28 | const form = new formData(); 29 | form.append('name', path.basename(filePath)); 30 | form.append('type', 'file'); 31 | form.append('image', fs.createReadStream(filePath)); 32 | let rsp = await got.post('https://api.imgur.com/3/image', { 33 | headers: { 34 | 'Authorization': `Client-ID ${this.config.imgur.clientId}` 35 | }, 36 | body: form, 37 | agent: tunnel 38 | }); 39 | 40 | return JSON.parse(rsp.body).data.link; 41 | } 42 | catch(error) { 43 | let e = error as Error; 44 | vscode.window.showInformationMessage(`${$l['upload_failed']}${e.message}`); 45 | return null; 46 | } 47 | } 48 | } 49 | 50 | export default Imgur; -------------------------------------------------------------------------------- /src/lib/local.ts: -------------------------------------------------------------------------------- 1 | import utils from './utils'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | import * as vscode from 'vscode'; 5 | import { locale as $l } from './utils'; 6 | 7 | class Local implements Upload 8 | { 9 | config: Config; 10 | constructor(config: Config) { 11 | this.config = config; 12 | } 13 | 14 | async reconfig(config: Config) { 15 | this.config = config; 16 | } 17 | 18 | async upload(filePath: string, savePath: string): Promise { 19 | try { 20 | if (!utils.getCurrentRoot()) { 21 | vscode.window.showInformationMessage($l['open_with_folder']); 22 | return null; 23 | } 24 | 25 | let saveFolder = this.config.local.path.startsWith('/') ? 26 | path.join(utils.getCurrentRoot(), this.config.local.path) : 27 | path.resolve(path.dirname(utils.getCurrentFilePath()), this.config.local.path); 28 | 29 | console.debug(`Create Project Upload Folder.`); 30 | 31 | savePath = path.resolve(saveFolder, savePath); 32 | saveFolder = path.dirname(savePath); 33 | 34 | if (!fs.existsSync(saveFolder)) { 35 | utils.mkdirs(saveFolder); 36 | } 37 | 38 | if (fs.existsSync(savePath) && 39 | (await utils.confirm($l['replace_or_no'], [$l['Yes'], $l['No']])) === $l['No']) { 40 | return savePath; 41 | } 42 | fs.copyFileSync(filePath, savePath); 43 | 44 | if(this.config.local.referencePath === '') { 45 | return path.relative(path.dirname(utils.getCurrentFilePath()), savePath).replace(/\\/g, '/'); 46 | } 47 | 48 | return await utils.formatName(this.config.local.referencePath, savePath, false) 49 | .then(data => data + path.basename(savePath)) 50 | } 51 | catch(error) { 52 | let e = error as Error; 53 | vscode.window.showInformationMessage(`${$l['save_failed']}${e.message}`); 54 | return null; 55 | } 56 | } 57 | } 58 | 59 | export default Local; -------------------------------------------------------------------------------- /src/lib/qiniu.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | import * as url from 'url'; 5 | import utils from './utils'; 6 | import { locale as $l } from './utils'; 7 | import * as qiniu from 'qiniu'; 8 | 9 | class Qiniu implements Upload 10 | { 11 | config: Config; 12 | token: string; 13 | lastTimestamp: number; 14 | constructor(config: Config) { 15 | this.config = config; 16 | this.token = ''; 17 | this.lastTimestamp = 0; 18 | } 19 | 20 | async reconfig(config: Config) { 21 | this.config = config; 22 | this.token = ''; 23 | this.lastTimestamp = 0; 24 | } 25 | 26 | async upload(filePath: string, savePath: string): Promise { 27 | try { 28 | let key = savePath.replace(/\\/g, '/') || (utils.hash(fs.readFileSync(filePath)) + path.extname(filePath)); 29 | let token = this.getToken(key); 30 | let config: qiniu.conf.ConfigOptions = new qiniu.conf.Config(); 31 | switch(this.config.qiniu.zone) { 32 | case $l['qiniu.east']: config.zone = qiniu.zone.Zone_z0; break; 33 | case $l['qiniu.north']: config.zone = qiniu.zone.Zone_z1; break; 34 | case $l['qiniu.south']: config.zone = qiniu.zone.Zone_z2; break; 35 | case $l['qiniu.na']: config.zone = qiniu.zone.Zone_na0; break; 36 | case $l['qiniu.sa']: config.zone = qiniu.zone.Zone_as0; break; 37 | } 38 | 39 | let formUploader = new qiniu.form_up.FormUploader(config); 40 | let putExtra = new qiniu.form_up.PutExtra(); 41 | 42 | let upload = () : Promise => { 43 | return new Promise((resolve, reject) => { 44 | formUploader.putFile(token, key, filePath, putExtra, (respErr, respBody, respInfo) => { 45 | if (respErr) { 46 | reject(respErr); 47 | } 48 | if (respInfo.statusCode === 200) { 49 | console.debug(respBody); 50 | resolve(url.resolve(this.config.qiniu.domain, key)); 51 | } else { 52 | console.debug(respInfo.statusCode); 53 | console.debug(respBody); 54 | reject(new Error(respBody.error)); 55 | } 56 | }); 57 | }); 58 | }; 59 | 60 | return await upload(); 61 | } 62 | catch(error) { 63 | let e = error as Error; 64 | vscode.window.showInformationMessage(`${$l['upload_failed']}${e.message}`); 65 | return null; 66 | } 67 | } 68 | 69 | getToken(filename: string): string { 70 | let mac = new qiniu.auth.digest.Mac(this.config.qiniu.accessKey, this.config.qiniu.secretKey); 71 | let putPolicy = new qiniu.rs.PutPolicy({ 72 | scope: this.config.qiniu.bucket + ':' + filename, 73 | }); 74 | return putPolicy.uploadToken(mac); 75 | } 76 | } 77 | 78 | export default Qiniu; -------------------------------------------------------------------------------- /src/lib/s3.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import * as mime from "mime"; 3 | import { readFileSync } from "fs"; 4 | import { 5 | S3Client, 6 | PutObjectCommand, 7 | GetObjectCommand, 8 | } from "@aws-sdk/client-s3"; 9 | import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; 10 | 11 | import { locale as $l } from "./utils"; 12 | 13 | class S3 implements Upload { 14 | config: Config; 15 | s3Client: S3Client; 16 | 17 | constructor(config: Config) { 18 | this.config = config; 19 | const { endpoint, region, accessKeyId, secretAccessKey, config: cfg } = this.config.s3; 20 | this.s3Client = new S3Client({ 21 | ...cfg, 22 | endpoint: endpoint, 23 | region: region, 24 | credentials: { 25 | accessKeyId: accessKeyId, 26 | secretAccessKey: secretAccessKey, 27 | }, 28 | }); 29 | } 30 | 31 | async getSavePath(filePath: string) { 32 | return filePath; 33 | } 34 | 35 | async reconfig(config: Config) { 36 | this.config = config; 37 | } 38 | 39 | sanitizeURL = (url: string): string => { 40 | /* 41 | * preSignedURLs generated by @aws-sdk are for sharing resources only for a limited time 42 | * but we are going to get the permanent URL by removing the authorization information 43 | * from the URL. This will give us the direct link to the image in S3 bucket. 44 | * Another way would be concatenating the bucket name with region, host and 45 | * image key but it is not reliable if we are going to support multiple S3 46 | * compatible providers. 47 | */ 48 | const decodedURL = decodeURIComponent(url); 49 | const urlObject = new URL(decodedURL); 50 | urlObject.search = ""; 51 | urlObject.hash = ""; 52 | return urlObject.toString(); 53 | }; 54 | 55 | async upload(filePath: string, savePath: string): Promise { 56 | try { 57 | await this.doUpload(filePath, savePath); 58 | 59 | const { bucketName } = this.config.s3; 60 | const getObjectCommand = new GetObjectCommand({ 61 | Bucket: bucketName, 62 | Key: savePath, 63 | }); 64 | const encodedURL = await getSignedUrl(this.s3Client, getObjectCommand); 65 | 66 | if (!this.config.s3.cdn) { 67 | return this.sanitizeURL(encodedURL); 68 | } 69 | 70 | const decodedURL = decodeURIComponent(encodedURL); 71 | const urlObject = new URL(decodedURL); 72 | 73 | return this.config.s3.cdn 74 | .replace(/\${bucket}/g, this.config.s3.bucketName) 75 | .replace(/\${region}/g, this.config.s3.region) 76 | .replace(/\${pathname}/g, urlObject.pathname) 77 | .replace(/\${filepath}/g, savePath); 78 | } catch (error) { 79 | let e = error as Error; 80 | vscode.window.showInformationMessage( 81 | `${$l["upload_failed"]}${e.message}\n${e.stack}` 82 | ); 83 | return null; 84 | } 85 | } 86 | 87 | doUpload(filePath: string, savePath: string): Promise { 88 | const body = readFileSync(filePath); 89 | 90 | const { bucketName } = this.config.s3; 91 | const putObjectCommand = new PutObjectCommand({ 92 | Bucket: bucketName, 93 | Body: body, 94 | Key: savePath, 95 | ContentType: mime.getType(filePath) || "application/octet-stream", 96 | }); 97 | 98 | return this.s3Client.send(putObjectCommand); 99 | } 100 | } 101 | 102 | export default S3; 103 | -------------------------------------------------------------------------------- /src/lib/sftp.ts: -------------------------------------------------------------------------------- 1 | import utils from './utils'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | import * as vscode from 'vscode'; 5 | import { locale as $l } from './utils'; 6 | import * as SFTP from 'ssh2-sftp-client'; 7 | 8 | class Sftp implements Upload { 9 | config: Config; 10 | constructor(config: Config) { 11 | this.config = config; 12 | } 13 | 14 | async reconfig (config: Config) { 15 | this.config = config; 16 | } 17 | 18 | async upload (filePath: string, savePath: string): Promise { 19 | try { 20 | if (!utils.getCurrentRoot()) { 21 | vscode.window.showInformationMessage($l['open_with_folder']); 22 | return null; 23 | } 24 | 25 | if (!this.config.sftp.host) { 26 | vscode.window.showInformationMessage($l['sftp_not_host']); 27 | return null; 28 | } 29 | 30 | if (!this.config.sftp.port) { 31 | vscode.window.showInformationMessage($l['sftp_not_port']); 32 | return null; 33 | } 34 | 35 | if (!this.config.sftp.username) { 36 | vscode.window.showInformationMessage($l['sftp_not_username']); 37 | return null; 38 | } 39 | 40 | const sftpConfig: SFTP.ConnectOptions = { 41 | host: this.config.sftp.host, 42 | port: this.config.sftp.port, 43 | username: this.config.sftp.username, 44 | }; 45 | 46 | if (this.config.sftp.password) { 47 | sftpConfig.password = this.config.sftp.password; 48 | } else if (fs.existsSync(this.config.sftp.privateKeyPath)) { 49 | sftpConfig.privateKey = fs.readFileSync(this.config.sftp.privateKeyPath).toString(); 50 | } else { 51 | vscode.window.showInformationMessage($l['sftp_not_password']); 52 | return null; 53 | } 54 | 55 | const sftp = new SFTP(); 56 | 57 | try { 58 | await sftp.connect(sftpConfig) 59 | } catch (error) { 60 | let e = error as Error; 61 | vscode.window.showErrorMessage(`${$l['sftp_connect_failed']}${e.message}`); 62 | return null; 63 | } 64 | 65 | let saveFolder = this.config.sftp.path.startsWith('/') ? 66 | path.join(utils.getCurrentRoot(), this.config.sftp.path) : 67 | path.join(path.dirname(utils.getCurrentFilePath()), this.config.sftp.path); 68 | 69 | savePath = path.join(saveFolder, savePath).replace(/\\/g, '/'); 70 | saveFolder = path.dirname(savePath); 71 | 72 | if (!await sftp.exists(saveFolder)) { 73 | await sftp.mkdir(saveFolder); 74 | } 75 | 76 | if (await sftp.exists(savePath) && 77 | (await utils.confirm($l['replace_or_no'], [$l['Yes'], $l['No']])) === $l['No']) { 78 | return savePath; 79 | } 80 | await sftp.put(fs.createReadStream(filePath), savePath); 81 | 82 | sftp.end(); 83 | 84 | if (this.config.local.referencePath === '') { 85 | return path.relative(path.dirname(utils.getCurrentFilePath()), savePath).replace(/\\/g, '/'); 86 | } 87 | 88 | return await utils.formatName(this.config.local.referencePath, savePath, false) 89 | .then(data => data + path.basename(savePath)) 90 | } 91 | catch (error) { 92 | let e = error as Error; 93 | vscode.window.showInformationMessage(`${$l['save_failed']}${e.message}`); 94 | return null; 95 | } 96 | } 97 | } 98 | 99 | export default Sftp; -------------------------------------------------------------------------------- /src/lib/sm.ms.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import got from 'got'; 3 | import * as formData from 'form-data'; 4 | import * as fs from 'fs'; 5 | import { locale as $l } from './utils'; 6 | 7 | class Imgur implements Upload 8 | { 9 | config: Config; 10 | constructor(config: Config) { 11 | this.config = config; 12 | } 13 | 14 | async getSavePath(filePath: string) { 15 | return filePath; 16 | } 17 | 18 | async reconfig(config: Config) { 19 | this.config = config; 20 | } 21 | 22 | async upload(filePath: string, savePath: string): Promise { 23 | try { 24 | const form = new formData(); 25 | form.append('smfile', fs.createReadStream(filePath)); 26 | if(!this.config.sm_ms.token) { 27 | vscode.window.showInformationMessage(`${$l['smms.token-miss']}`); 28 | return null; 29 | } 30 | let headers = { 31 | Authorization: `Basic ${this.config.sm_ms.token}` 32 | }; 33 | let rsp = await got.post('https://smms.app/api/v2/upload', { 34 | body: form, 35 | headers 36 | }); 37 | 38 | let body = JSON.parse(rsp.body); 39 | return body.images || body.data.url; 40 | } 41 | catch(error) { 42 | let e = error as Error; 43 | vscode.window.showInformationMessage(`${$l['upload_failed']}${e.message}`); 44 | return null; 45 | } 46 | } 47 | } 48 | 49 | export default Imgur; -------------------------------------------------------------------------------- /src/lib/upload.d.ts: -------------------------------------------------------------------------------- 1 | interface Upload 2 | { 3 | config: Config; 4 | upload(filePath: string, savePath: string): Promise; 5 | reconfig(config: Config): Promise; 6 | } -------------------------------------------------------------------------------- /src/lib/uploads.ts: -------------------------------------------------------------------------------- 1 | import Local from './local'; 2 | import Coding from './coding'; 3 | import GitHub from './github'; 4 | import Imgur from './imgur'; 5 | import SM_MS from './sm.ms'; 6 | import Qiniu from './qiniu'; 7 | import Upyun from './upyun'; 8 | import DataUrl from './dataurl'; 9 | import Define from './define'; 10 | import Cloudinary from './cloudinary'; 11 | import Cloudflare from './cloudflare'; 12 | import S3 from './s3'; 13 | import Sftp from './sftp'; 14 | import Azure from './azure'; 15 | 16 | export default { 17 | Local, Coding, GitHub, Imgur, SM_MS, Qiniu, DataUrl, Define, Cloudinary, Cloudflare, Upyun, S3, Sftp, Azure 18 | }; -------------------------------------------------------------------------------- /src/lib/upyun.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as path from 'path'; 3 | import { locale as $l } from './utils'; 4 | import * as fs from 'fs'; 5 | import * as url from 'url'; 6 | const upyun = require('upyun'); 7 | 8 | class Upyun implements Upload 9 | { 10 | config: Config; 11 | client: any; 12 | lastTimestamp: number; 13 | constructor(config: Config) { 14 | this.config = config; 15 | this.client = null; 16 | this.lastTimestamp = 0; 17 | } 18 | 19 | async reconfig(config: Config) { 20 | this.config = config; 21 | this.client = null; 22 | this.lastTimestamp = 0; 23 | } 24 | 25 | async upload(filePath: string, savePath: string): Promise { 26 | try{ 27 | savePath = path.join(this.config.upyun.path, savePath).replace(/\\/g, '/').replace(/^\/|\/$/, ''); 28 | let link = "v0.api.upyun.com"; 29 | switch(this.config.upyun.link) { 30 | case $l['upyun.smart']: link = "v0.api.upyun.com"; break; 31 | case $l['upyun.telecom']: link = "v1.api.upyun.com"; break; 32 | case $l['upyun.unicom']: link = "v2.api.upyun.com"; break; 33 | case $l['upyun.mobile']: link = "v3.api.upyun.com"; break; 34 | } 35 | let client = this.getClient( 36 | this.config.upyun.bucket, 37 | this.config.upyun.operator, 38 | this.config.upyun.password, 39 | link); 40 | let file = fs.readFileSync(filePath) 41 | 42 | let upload = () : Promise => { 43 | return new Promise((resolve, reject) => { 44 | client.putFile(savePath, file).then( (res: any) => { 45 | console.debug(res); 46 | if(res){ 47 | resolve(url.resolve(this.config.upyun.domain, savePath)); 48 | }else{ 49 | reject(); 50 | } 51 | }); 52 | }); 53 | }; 54 | return await upload(); 55 | }catch(error) { 56 | let e = error as Error; 57 | vscode.window.showInformationMessage(`${$l['upload_failed']}${e.message}`); 58 | return null; 59 | } 60 | } 61 | 62 | 63 | getClient(bucket: string, operator: string, password: string, domain: string){ 64 | var options = { 65 | domain: domain 66 | }; 67 | return new upyun.Client(new upyun.Service(bucket, operator, password),options); 68 | } 69 | 70 | 71 | } 72 | 73 | export default Upyun; -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { spawn, exec } from 'child_process'; 3 | import { tmpdir } from 'os'; 4 | import * as fs from 'fs'; 5 | import * as path from 'path'; 6 | import * as packages from '../../package.json'; 7 | import * as crypto from 'crypto'; 8 | import Uploads from './uploads'; 9 | import i18n from '../i18n/index'; 10 | import * as TurndownService from 'turndown'; 11 | import { gfm } from 'turndown-plugin-gfm'; 12 | import * as pngToJpeg from 'png-to-jpeg'; 13 | 14 | let pkg = packages as any; 15 | let locale = i18n(); 16 | 17 | function getUpload (uploadMethod: string, config: any): Upload | null { 18 | switch (uploadMethod) { 19 | case 'Local': return new Uploads.Local(config); 20 | case 'Coding': return new Uploads.Coding(config); 21 | case 'GitHub': return new Uploads.GitHub(config); 22 | case 'Imgur': return new Uploads.Imgur(config); 23 | case 'SM.MS': return new Uploads.SM_MS(config); 24 | case 'Data URL': return new Uploads.DataUrl(config); 25 | case 'Qiniu': return new Uploads.Qiniu(config); 26 | case 'DIY': return new Uploads.Define(config); 27 | case '本地': return new Uploads.Local(config); 28 | case '七牛': return new Uploads.Qiniu(config); 29 | case 'Upyun': return new Uploads.Upyun(config); 30 | case '又拍云': return new Uploads.Upyun(config); 31 | case '又拍雲': return new Uploads.Upyun(config); 32 | case '自定义': return new Uploads.Define(config); 33 | case '自定義': return new Uploads.Define(config); 34 | case 'Cloudinary': return new Uploads.Cloudinary(config); 35 | case 'Cloudflare': return new Uploads.Cloudflare(config); 36 | case 'S3': return new Uploads.S3(config); 37 | case 'SFTP': return new Uploads.Sftp(config); 38 | case 'Azure Storage': return new Uploads.Azure(config); 39 | } 40 | return null; 41 | } 42 | 43 | function showProgress (message: string) { 44 | let show = true; 45 | function stopProgress () { 46 | show = false; 47 | } 48 | 49 | vscode.window.withProgress({ 50 | location: vscode.ProgressLocation.Window, 51 | title: message, 52 | cancellable: false 53 | }, (progress, token) => { 54 | return new Promise(resolve => { 55 | let timer = setInterval(() => { 56 | if (show) { return; } 57 | clearInterval(timer); 58 | resolve(show); 59 | }, 100); 60 | }); 61 | }); 62 | 63 | return stopProgress; 64 | } 65 | 66 | function editorEdit (selection: vscode.Selection | vscode.Position | undefined | null, text: string): Promise { 67 | return new Promise((resolve, reject) => { 68 | vscode.window.activeTextEditor?.edit(editBuilder => { 69 | if (selection) { 70 | editBuilder.replace(selection, text); 71 | } 72 | }).then(resolve); 73 | }); 74 | } 75 | 76 | function insertToEnd (text: string): Promise { 77 | return new Promise((resolve, reject) => { 78 | let linenumber = vscode.window.activeTextEditor?.document.lineCount || 1; 79 | let pos = vscode.window.activeTextEditor?.document.lineAt(linenumber - 1).range.end || new vscode.Position(0, 0); 80 | vscode.window.activeTextEditor?.edit(editBuilder => { 81 | editBuilder.insert(pos, text); 82 | }).then(resolve); 83 | }); 84 | } 85 | 86 | function getSelections (): vscode.Selection[] | null { 87 | let editor = vscode.window.activeTextEditor; 88 | if (!editor) { 89 | return null; // No open text editor 90 | } 91 | 92 | let selections = editor.selections; 93 | return selections; 94 | } 95 | 96 | async function formatCode(filePath: string, selection: string, maxWidth: number, codeType: string, format: string) { 97 | switch (codeType) { 98 | case "Markdown": 99 | return `![${selection}](${filePath}${maxWidth > 0 ? ` =${maxWidth}x` : ''}) \n`; 100 | 101 | case "DIY": 102 | let code = await formatName(format, filePath, false); 103 | code = code.replaceAll('${src}', filePath); 104 | code = code.replaceAll('${alt}', selection); 105 | return code; 106 | 107 | default: 108 | return `${selection} 0 ? `width="${maxWidth}" ` : ''}/> \n`; 109 | } 110 | } 111 | 112 | async function variableGetter (variable: string, 113 | { filePath, isPaste, match, context, saveName }: 114 | { 115 | filePath: string, 116 | isPaste: boolean, 117 | match?: string, 118 | context?: vscode.ExtensionContext, 119 | saveName?: string; 120 | }): Promise { 121 | switch (variable) { 122 | case 'filename': { 123 | return !isPaste ? path.basename(filePath, path.extname(filePath)) : 124 | (path.basename((await prompt(locale['named_paste'], path.basename(filePath, '.png'))) || '') || ''); 125 | } 126 | case 'mdname': { 127 | return path.basename(getCurrentFilePath(), path.extname(getCurrentFilePath())); 128 | } 129 | case 'path': { 130 | return path.dirname(getCurrentFilePath()).replace(getCurrentRoot(), '').slice(1).replace(/\\/g, '/'); 131 | } 132 | case 'hash': { 133 | return hash(fs.readFileSync(filePath)); 134 | } 135 | case 'timestramp': // spell mistake 136 | case 'timestamp': { 137 | return new Date().getTime().toString(); 138 | } 139 | case 'YY': { 140 | return new Date().getFullYear().toString(); 141 | } 142 | case 'MM': { 143 | return (new Date().getMonth() + 1).toString().padStart(2, '0'); 144 | } 145 | case 'DD': { 146 | return (new Date().getDate()).toString().padStart(2, '0'); 147 | } 148 | case 'hh': { 149 | let hours = new Date().getHours(); 150 | return (hours > 12 ? hours - 12 : hours).toString().padStart(2, '0'); 151 | } 152 | case 'HH': { 153 | return new Date().getHours().toString().padStart(2, '0'); 154 | } 155 | case 'mm': { 156 | return new Date().getMinutes().toString().padStart(2, '0'); 157 | } 158 | case 'ss': { 159 | return new Date().getSeconds().toString().padStart(2, '0'); 160 | } 161 | case 'mss': { 162 | return new Date().getMilliseconds().toString().padStart(3, '0'); 163 | } 164 | case 'rand,(\\d+)': { 165 | let numberMat = match!.match(/\d+/); 166 | if (!numberMat) { return ''; } 167 | let n = parseInt(numberMat[0]); 168 | return parseInt((Math.random() * n).toString()).toString(); 169 | } 170 | case 'prompt': { 171 | let promptInput = await vscode.window.showInputBox({ prompt: `${locale['prompt_name_component']}${saveName || ''}` }); 172 | // make sure that promptInput is a filename-safe string 173 | promptInput = promptInput?.replace(/[:*?<>|]/g, '').trim(); 174 | if (promptInput) 175 | return promptInput; 176 | else 177 | throw Error(locale['user_did_not_answer_prompt']); 178 | } 179 | case 'index': { 180 | const fsPath = getCurrentFilePath(); 181 | let lastIndex = parseInt(context!.globalState.get(fsPath) || '0') 182 | context!.globalState.update(fsPath, `${lastIndex + 1}`); 183 | return lastIndex.toString(); 184 | } 185 | } 186 | return ''; 187 | } 188 | 189 | async function formatName (format: string, filePath: string, isPaste: boolean): Promise { 190 | let saveName = format; 191 | let variables = [ 192 | 'filename', 'mdname', 'path', 'hash', 'timestramp', 'timestamp', 'YY', 'MM', 'DD', 'HH', 'hh', 'mm', 'ss', 'mss', 'rand,(\\d+)', 'prompt' 193 | ]; 194 | for (let i = 0; i < variables.length; i++) { 195 | let reg = new RegExp(`\\$\\{${variables[i]}\\}`, 'g'); 196 | let mat = format.match(reg); 197 | if (!mat) { continue; } 198 | if (variables[i] === 'rand,(\\d+)') { 199 | for (let j = 0; j < mat.length; j++) { 200 | const data = await variableGetter(variables[i], { filePath, isPaste, match: mat[j] }); 201 | if (!data) continue; 202 | saveName = saveName.replaceAll(mat[j], data); 203 | } 204 | } else { 205 | const data = await variableGetter(variables[i], { filePath, isPaste, saveName }); 206 | if (!data) continue; 207 | saveName = saveName.replaceAll(mat[0], data); 208 | } 209 | } 210 | 211 | return saveName; 212 | } 213 | 214 | async function getAlt (format: string, filePath: string, context: vscode.ExtensionContext) { 215 | let alt = format; 216 | let variables = [ 217 | 'filename', 'mdname', 'path', 'hash', 'timestamp', 'YY', 'MM', 'DD', 'HH', 'hh', 'mm', 'ss', 'mss', 'rand,(\\d+)', 'index', 'prompt' 218 | ]; 219 | 220 | for (let i = 0; i < variables.length; i++) { 221 | let reg = new RegExp(`\\$\\{${variables[i]}\\}`, 'g'); 222 | let mat = format.match(reg); 223 | if (!mat) { continue; } 224 | if (variables[i] === 'rand,(\\d+)') { 225 | for (let j = 0; j < mat.length; j++) { 226 | const data = await variableGetter(variables[i], { filePath, isPaste: false, match: mat[j] }); 227 | if (!data) continue; 228 | alt = alt.replaceAll(mat[j], data); 229 | } 230 | } else { 231 | const data = await variableGetter(variables[i], { filePath, isPaste: false, context }); 232 | if (!data) continue; 233 | alt = alt.replaceAll(mat[0], data); 234 | } 235 | } 236 | return alt; 237 | } 238 | 239 | function mkdirs (dirname: string) { 240 | //console.debug(dirname); 241 | if (fs.existsSync(dirname)) { 242 | return true; 243 | } else { 244 | if (mkdirs(path.dirname(dirname))) { 245 | fs.mkdirSync(dirname); 246 | return true; 247 | } 248 | } 249 | } 250 | 251 | function html2Markdown (data: string): string { 252 | let turndownService = new TurndownService({ codeBlockStyle: 'fenced', headingStyle: 'atx', }); 253 | turndownService.use(gfm); 254 | return turndownService.turndown(data); 255 | } 256 | 257 | function getConfig () { 258 | const configurations: { properties: object, title: string }[] = pkg.contributes.configuration; 259 | const keys: string[] = configurations.reduce((acc, config) => acc.concat(Object.keys(config.properties)), [] as string[]) 260 | 261 | let values: Config = {}; 262 | function toVal (str: string, val: string | undefined, cfg: Config): string | Config { 263 | let keys = str.split('.'); 264 | if (keys.length === 1) { 265 | cfg[keys[0]] = val; 266 | } else { 267 | cfg[keys[0]] = toVal(keys.slice(1).join('.'), val, cfg[keys[0]] || {}); 268 | } 269 | return cfg; 270 | } 271 | keys.forEach(k => toVal(k.split('.').slice(1).join('.'), vscode.workspace.getConfiguration().get(k), values)); 272 | return values; 273 | } 274 | 275 | function getPasteImage (imagePath: string): Promise { 276 | return new Promise((resolve, reject) => { 277 | if (!imagePath) { return; } 278 | 279 | let platform = process.platform; 280 | if (platform === 'win32') { 281 | // Windows 282 | const scriptPath = path.join(__dirname, '..', '..', 'asserts/pc.ps1'); 283 | 284 | let command = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"; 285 | let powershellExisted = fs.existsSync(command); 286 | let output = ''; 287 | if (!powershellExisted) { 288 | command = "powershell"; 289 | } 290 | 291 | const powershell = spawn(command, [ 292 | '-noprofile', 293 | '-noninteractive', 294 | '-nologo', 295 | '-sta', 296 | '-executionpolicy', 'unrestricted', 297 | '-windowstyle', 'hidden', 298 | '-file', scriptPath, 299 | imagePath 300 | ]); 301 | // the powershell can't auto exit in windows 7 . 302 | let timer = setTimeout(() => powershell.kill(), 8000); 303 | 304 | powershell.on('error', (e: any) => { 305 | if (e.code === 'ENOENT') { 306 | vscode.window.showErrorMessage(locale['powershell_not_found']); 307 | } else { 308 | vscode.window.showErrorMessage(e); 309 | } 310 | }); 311 | 312 | powershell.on('exit', function (code, signal) { 313 | // console.debug('exit', code, signal); 314 | }); 315 | powershell.stdout.on('data', (data) => { 316 | output += data.toString(); 317 | clearTimeout(timer); 318 | timer = setTimeout(() => powershell.kill(), 2000); 319 | }); 320 | powershell.on('close', (code) => { 321 | resolve(output.trim().split('\n').map(i => i.trim())); 322 | clearTimeout(timer); 323 | }); 324 | } 325 | else if (platform === 'darwin') { 326 | // Mac 327 | let scriptPath = path.join(__dirname, '..', '..', 'asserts/mac.applescript'); 328 | 329 | let ascript = spawn('osascript', [scriptPath, imagePath]); 330 | ascript.on('error', (e: any) => { 331 | vscode.window.showErrorMessage(e); 332 | }); 333 | ascript.on('exit', (code, signal) => { 334 | // console.debug('exit', code, signal); 335 | }); 336 | ascript.stdout.on('data', (data) => { 337 | resolve(data.toString().trim().split('\n')); 338 | }); 339 | } else { 340 | // Linux 341 | 342 | let scriptPath = path.join(__dirname, '..', '..', 'asserts/linux.sh'); 343 | 344 | let ascript = spawn('sh', [scriptPath, imagePath]); 345 | ascript.on('error', (e: any) => { 346 | vscode.window.showErrorMessage(e); 347 | }); 348 | ascript.on('exit', (code, signal) => { 349 | // console.debug('exit',code,signal); 350 | }); 351 | ascript.stdout.on('data', (data) => { 352 | let result = data.toString().trim(); 353 | if (result === "no xclip") { 354 | vscode.window.showInformationMessage(locale['install_xclip']); 355 | return; 356 | } 357 | let match = decodeURI(result).trim().match(/((\/[^\/]+)+\/[^\/]*?\.(jpg|jpeg|gif|bmp|png))/g); 358 | resolve(match || []); 359 | }); 360 | } 361 | }); 362 | } 363 | 364 | function getRichText (): Promise { 365 | return new Promise((resolve, reject) => { 366 | let platform = process.platform; 367 | if (platform === 'win32') { 368 | // Windows 369 | const scriptPath = path.join(__dirname, '..', '..', 'asserts/rtf.ps1'); 370 | 371 | let command = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"; 372 | let powershellExisted = fs.existsSync(command); 373 | let output = ''; 374 | if (!powershellExisted) { 375 | command = "powershell"; 376 | } 377 | 378 | const powershell = spawn(command, [ 379 | '-noprofile', 380 | '-noninteractive', 381 | '-nologo', 382 | '-sta', 383 | '-executionpolicy', 'unrestricted', 384 | '-windowstyle', 'hidden', 385 | '-file', scriptPath, 386 | ]); 387 | // the powershell can't auto exit in windows 7 . 388 | let timer = setTimeout(() => powershell.kill(), 8000); 389 | let result = ''; 390 | powershell.on('error', (e: any) => { 391 | if (e.code === 'ENOENT') { 392 | vscode.window.showErrorMessage(locale['powershell_not_found']); 393 | } else { 394 | vscode.window.showErrorMessage(e); 395 | } 396 | }); 397 | 398 | powershell.on('exit', function (code, signal) { 399 | // console.debug('exit', code, signal); 400 | }); 401 | powershell.stdout.on('data', (data) => { 402 | result += data.toString(); 403 | clearTimeout(timer); 404 | timer = setTimeout(() => powershell.kill(), 8000); 405 | }); 406 | powershell.on('close', (code) => { 407 | output = (result.match(/[\s\S]*<\/body>/g) || [''])[0]; 408 | resolve(output.replace(/<\/*body>/g, '').trim()); 409 | }); 410 | } 411 | else if (platform === 'darwin') { 412 | // Mac 413 | vscode.window.showInformationMessage('Not support in macos yet.'); 414 | resolve(''); 415 | } else { 416 | // Linux 417 | 418 | let scriptPath = path.join(__dirname, '..', '..', 'asserts/rtf.sh'); 419 | let result = ''; 420 | let ascript = spawn('sh', [scriptPath]); 421 | ascript.on('error', (e: any) => { 422 | vscode.window.showErrorMessage(e); 423 | }); 424 | ascript.on('exit', (code, signal) => { 425 | if (result === "no xclip") { 426 | vscode.window.showInformationMessage(locale['install_xclip']); 427 | return; 428 | } 429 | resolve(result); 430 | }); 431 | ascript.stdout.on('data', (data) => { 432 | result += data.toString().trim(); 433 | }); 434 | } 435 | }); 436 | } 437 | 438 | function getCurrentRoot (): string { 439 | const editor = vscode.window.activeTextEditor; 440 | if (!editor || !vscode.workspace.workspaceFolders || vscode.workspace.workspaceFolders.length < 1) { return ''; } 441 | const resource = editor.document.uri; 442 | if (resource.scheme == 'vscode-notebook-cell') { 443 | let filePath = resource.fsPath; 444 | let root = vscode.workspace.workspaceFolders.find(f => filePath.indexOf(f.uri.fsPath) >= 0); 445 | if (root) return root.uri.fsPath; 446 | else return ''; 447 | } 448 | if (resource.scheme !== 'file' && resource.scheme !== 'vscode-remote') { return ''; } 449 | const folder = vscode.workspace.getWorkspaceFolder(resource); 450 | if (!folder) { return ''; } 451 | return folder.uri.fsPath; 452 | } 453 | 454 | function getCurrentFilePath (): string { 455 | const editor = vscode.window.activeTextEditor; 456 | if (!editor || !vscode.workspace.workspaceFolders || vscode.workspace.workspaceFolders.length < 1) { return ''; } 457 | return editor.document.uri.fsPath; 458 | } 459 | 460 | function getTmpFolder () { 461 | let savePath = path.join(tmpdir(), pkg.name); 462 | if (!fs.existsSync(savePath)) { fs.mkdirSync(savePath); } 463 | return savePath; 464 | } 465 | 466 | function convertImage (imagePath: string): Promise { 467 | return new Promise((resolve, reject) => { 468 | pngToJpeg({ quality: 90 })(fs.readFileSync(imagePath)).then((output: Buffer) => { 469 | const newImage = path.join(path.dirname(imagePath), path.basename(imagePath, path.extname(imagePath)) + '.jpg'); 470 | fs.writeFileSync(newImage, output); 471 | resolve(newImage); 472 | fs.unlinkSync(imagePath); 473 | }) 474 | }); 475 | } 476 | 477 | function sleep (time: number) { 478 | return new Promise((resolve) => setTimeout(resolve, time)); 479 | } 480 | 481 | function confirm (message: string, options: string[]): Promise { 482 | return new Promise((resolve, reject) => { 483 | return vscode.window.showInformationMessage(message, ...options).then(resolve); 484 | }); 485 | } 486 | 487 | function prompt (message: string, defaultVal: string = ''): Promise { 488 | return new Promise((resolve, reject) => { 489 | return vscode.window.showInputBox({ 490 | value: defaultVal, 491 | prompt: message 492 | }).then(resolve); 493 | }); 494 | } 495 | 496 | function hash (buffer: Buffer): string { 497 | let sha256 = crypto.createHash('sha256'); 498 | let hash = sha256.update(buffer).digest('hex'); 499 | return hash; 500 | } 501 | 502 | function getOpenCmd (): string { 503 | let cmd = 'start'; 504 | if (process.platform === 'win32') { 505 | cmd = 'start'; 506 | } else if (process.platform === 'linux') { 507 | cmd = 'xdg-open'; 508 | } else if (process.platform === 'darwin') { 509 | cmd = 'open'; 510 | } 511 | return cmd; 512 | } 513 | 514 | function noticeComment (context: vscode.ExtensionContext) { 515 | let notice = context.globalState.get('notice'); 516 | let usetimes: number = context.globalState.get('usetimes') || 0; 517 | if (!notice && usetimes > 100) { 518 | confirm(locale['like.extension'], [locale['like.ok'], locale['like.no'], locale['like.later']]) 519 | .then((option) => { 520 | switch (option) { 521 | case locale['like.ok']: 522 | exec(`${getOpenCmd()} https://marketplace.visualstudio.com/items?itemName=hancel.markdown-image`); 523 | context.globalState.update('notice', true); 524 | break; 525 | case locale['like.no']: 526 | context.globalState.update('notice', true); 527 | break; 528 | case locale['like.later']: 529 | usetimes = 50; 530 | context.globalState.update('usetimes', usetimes); 531 | context.globalState.update('notice', false); 532 | break; 533 | } 534 | }) 535 | .catch(e => console.debug(e)); 536 | } else if (!notice) { 537 | context.globalState.update('usetimes', ++usetimes); 538 | } 539 | } 540 | 541 | export default { 542 | getUpload, 543 | showProgress, 544 | editorEdit, 545 | insertToEnd, 546 | formatCode, 547 | formatName, 548 | getAlt, 549 | mkdirs, 550 | html2Markdown, 551 | getConfig, 552 | getSelections, 553 | getPasteImage, 554 | getRichText, 555 | getCurrentRoot, 556 | getCurrentFilePath, 557 | getTmpFolder, 558 | convertImage, 559 | noticeComment, 560 | sleep, 561 | confirm, 562 | prompt, 563 | hash, 564 | }; 565 | export { locale }; 566 | -------------------------------------------------------------------------------- /src/test/extension.test.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Note: This example test is leveraging the Mocha test framework. 3 | // Please refer to their documentation on https://mochajs.org/ for help. 4 | // 5 | 6 | // The module 'assert' provides assertion methods from node 7 | import * as assert from 'assert'; 8 | 9 | // You can import and use all API from the 'vscode' module 10 | // as well as import your extension to test it 11 | // import * as vscode from 'vscode'; 12 | // import * as myExtension from '../extension'; 13 | 14 | // Defines a Mocha test suite to group tests of similar kind together 15 | suite("Extension Tests", function () { 16 | 17 | // Defines a Mocha unit test 18 | test("Something 1", function() { 19 | assert.equal(-1, [1, 2, 3].indexOf(5)); 20 | assert.equal(-1, [1, 2, 3].indexOf(0)); 21 | }); 22 | }); -------------------------------------------------------------------------------- /src/test/index.ts: -------------------------------------------------------------------------------- 1 | // 2 | // PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING 3 | // 4 | // This file is providing the test runner to use when running extension tests. 5 | // By default the test runner in use is Mocha based. 6 | // 7 | // You can provide your own test runner if you want to override it by exporting 8 | // a function run(testRoot: string, clb: (error:Error) => void) that the extension 9 | // host can call to run the tests. The test runner is expected to use console.debug 10 | // to report the results back to the caller. When the tests are finished, return 11 | // a possible error to the callback or null if none. 12 | 13 | import * as testRunner from 'vscode/lib/testrunner'; 14 | 15 | // You can directly control Mocha options by uncommenting the following lines 16 | // See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info 17 | testRunner.configure({ 18 | ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) 19 | useColors: true // colored output from test results 20 | }); 21 | 22 | module.exports = testRunner; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": [ 7 | "es6", 8 | "dom" 9 | ], 10 | "sourceMap": true, 11 | "rootDir": "src", 12 | /* Strict Type-Checking Option */ 13 | "strict": true, /* enable all strict type-checking options */ 14 | /* Additional Checks */ 15 | "noUnusedLocals": true /* Report errors on unused locals. */ 16 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 17 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 18 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 19 | }, 20 | "exclude": [ 21 | "node_modules", 22 | ".vscode-test" 23 | ] 24 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-string-throw": true, 4 | "no-unused-expression": true, 5 | "no-duplicate-variable": true, 6 | "curly": true, 7 | "class-name": true, 8 | "semicolon": [ 9 | true, 10 | "always" 11 | ], 12 | "triple-equals": true 13 | }, 14 | "exclude": [ 15 | "node_modules/**/*.ts" 16 | ], 17 | "defaultSeverity": "warning" 18 | } -------------------------------------------------------------------------------- /typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.json" { 2 | const value: any; 3 | export default value; 4 | } 5 | 6 | declare type Config = { 7 | [key: string]: any; 8 | } 9 | 10 | declare module 'coding-picbed' { 11 | const Coding: any; 12 | } 13 | 14 | declare module 'github-picbed'; 15 | 16 | declare module 'turndown-plugin-gfm' { 17 | export function gfm(turndownService: any) : void; 18 | export function highlightedCodeBlock(turndownService: any) : void; 19 | export function strikethrough(turndownService: any) : void; 20 | export function tables(turndownService: any) : void; 21 | export function taskListItems(turndownService: any) : void; 22 | } 23 | 24 | declare module 'png-to-jpeg'; --------------------------------------------------------------------------------