Repository: yzhang-gh/vscode-markdown Branch: master Commit: 414d6e33c62a Files: 88 Total size: 461.5 KB Directory structure: gitextract_9glk66aa/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── Bug.md │ │ ├── Feature.md │ │ └── config.yml │ └── workflows/ │ ├── main.yml │ └── test.yml ├── .gitignore ├── .vscode/ │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build/ │ ├── build.js │ ├── compilation.js │ ├── duplicate-changelog.js │ ├── logger.js │ └── tsconfig.json ├── donate.md ├── media/ │ └── checkbox.css ├── package.json ├── package.nls.ja.json ├── package.nls.json ├── package.nls.ru-ru.json ├── package.nls.zh-cn.json ├── package.nls.zh-tw.json ├── src/ │ ├── IDisposable.ts │ ├── completion.ts │ ├── configuration/ │ │ ├── fallback.ts │ │ ├── manager.ts │ │ └── model.ts │ ├── contract/ │ │ ├── LanguageIdentifier.ts │ │ ├── MarkdownSpec.ts │ │ ├── README.md │ │ ├── SlugifyMode.ts │ │ └── VisualStudioCodeLocaleId.ts │ ├── editor-context-service/ │ │ ├── context-service-in-fenced-code-block.ts │ │ ├── context-service-in-list.ts │ │ ├── context-service-in-math-env.ts │ │ ├── i-context-service.ts │ │ └── manager.ts │ ├── extension.ts │ ├── formatting.ts │ ├── listEditing.ts │ ├── markdown-it-plugin-provider.ts │ ├── markdownEngine.ts │ ├── markdownExtensions.ts │ ├── nls/ │ │ ├── README.md │ │ ├── index.ts │ │ └── resolveResource.ts │ ├── preview.ts │ ├── print.ts │ ├── syntaxDecorations.ts │ ├── tableFormatter.ts │ ├── test/ │ │ ├── runTest.ts │ │ └── suite/ │ │ ├── index.ts │ │ ├── integration/ │ │ │ ├── blockquoteEditing.test.ts │ │ │ ├── formatting.test.ts │ │ │ ├── listEditing.fallback.test.ts │ │ │ ├── listEditing.test.ts │ │ │ ├── listRenumbering.test.ts │ │ │ ├── tableFormatter.test.ts │ │ │ └── toc.test.ts │ │ ├── unit/ │ │ │ ├── linksRecognition.test.ts │ │ │ └── slugify.test.ts │ │ └── util/ │ │ ├── configuration.ts │ │ └── generic.ts │ ├── theming/ │ │ ├── constant.ts │ │ ├── decorationManager.ts │ │ └── decorationWorkerRegistry.ts │ ├── toc.ts │ ├── util/ │ │ ├── contextCheck.ts │ │ ├── generic.ts │ │ ├── katex-funcs.ts │ │ ├── lazy.ts │ │ └── slugify.ts │ └── zola-slug/ │ ├── .gitignore │ ├── Cargo.toml │ ├── LICENSE │ └── src/ │ └── lib.rs ├── syntaxes/ │ ├── katex.tmLanguage.json │ ├── math_display.markdown.tmLanguage.json │ └── math_inline.markdown.tmLanguage.json ├── test/ │ └── test.md ├── tools/ │ └── set-welcome-message.js ├── tsconfig.base.json ├── tsconfig.json └── webpack.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/Bug.md ================================================ --- name: '🐛 Bug report' about: 'Report errors or unexpected behavior.' --- ### What's the problem ### What's the expected result ### How to reproduce 1. ... 2. ... ### Other information ================================================ FILE: .github/ISSUE_TEMPLATE/Feature.md ================================================ --- name: '✨ Feature request' about: 'Suggest a new feature or improvement.' --- ### Proposal ### Other information ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: '🔒 Security issue' url: 'https://github.com/yzhang-gh' about: 'Please email @yzhang-gh.' ================================================ FILE: .github/workflows/main.yml ================================================ name: CI # Build VSIX packages in production and debug mode. # Run tests against debug build. # # This workflow is designed to be run on pushing to master branch. on: push: branches: [master] paths: - ".github/workflows/*.yml" - "src/**" - "syntaxes/**" - ".*" - "package*.json" - "tsconfig.json" - "webpack.*" - "!**.md" workflow_dispatch: repository_dispatch: types: ["package"] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: "Setup Node.js environment" uses: actions/setup-node@v4 with: node-version: "20" cache: "npm" - name: Setup Rust Toolchain for GitHub CI uses: actions-rust-lang/setup-rust-toolchain@v1.8.0 - name: Install wasm-pack run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh - name: "Install dependencies" run: | npm run build-wasm npm ci # Our prepublish script runs in production mode by default. # We should run tests against debug build to make error messages as useful as possible. - name: "Build release" run: | npx vsce package --pre-release export NODE_ENV='development' npx vsce package --pre-release --out debug.vsix - name: Test uses: GabrielBB/xvfb-action@v1.0 with: run: npm test - name: "Rust test" run: | cd ./src/zola-slug/ cargo test --release - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: Markdown-All-in-One-${{ github.sha }} path: ./*.vsix # If the run failed, npm log may help to diagnose. - name: "(debug) Upload npm log" if: ${{ !success() }} uses: actions/upload-artifact@v4 with: name: "npm-debug-log" path: "~/.npm/_logs" ================================================ FILE: .github/workflows/test.yml ================================================ name: Test # Run tests only. # # This workflow is designed to be run on PR to validate commits. # It can be seen as a short version of `main.yml`. on: pull_request: branches: ["master"] # Sometimes we want to test a few minor changes. # To avoid triggering the workflow twice, # this is only enabled for branch names that are very unlikely to be a PR head. push: branches: ["dev/-/**"] paths: - "src/**" - "package-lock.json" - "!**.md" workflow_dispatch: repository_dispatch: types: ["test"] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: "Setup Node.js environment" uses: actions/setup-node@v4 with: node-version: "20" cache: "npm" - name: Setup Rust Toolchain for GitHub CI uses: actions-rust-lang/setup-rust-toolchain@v1.8.0 - name: Install wasm-pack run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh - name: "Install dependencies" run: | npm run build-wasm npm ci - name: "Build debug" run: | export NODE_ENV='development' npx vsce package --pre-release --out debug.vsix - name: Test uses: GabrielBB/xvfb-action@v1.0 with: run: npm test - name: "Rust test" run: | cd ./src/zola-slug/ cargo test --release - name: "(debug) Upload npm log" if: ${{ !success() }} uses: actions/upload-artifact@v4 with: name: "npm-debug-log" path: "~/.npm/_logs" ================================================ FILE: .gitignore ================================================ out node_modules *.vsix ROADMAP.md *.log /.vscode-test dist tmp # The welcome materials /welcome/** /changes.md ================================================ FILE: .vscode/launch.json ================================================ // A launch configuration that compiles the extension and then opens it inside a new window // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 { "version": "0.2.0", "configurations": [ { "name": "Extension", "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", "args": [ "--extensionDevelopmentPath=${workspaceFolder}" ], "outFiles": [ "${workspaceFolder}/dist/**/*.js" ], "preLaunchTask": "npm: dev-build" }, { "name": "Extension Tests", "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", "args": [ "--disable-extensions", "--user-data-dir=tmp", "--extensionDevelopmentPath=${workspaceFolder}", "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" ], "outFiles": [ "${workspaceFolder}/out/test/**/*.js" ], "preLaunchTask": "npm: pretest" } ] } ================================================ FILE: .vscode/settings.json ================================================ // Place your settings in this file to overwrite default and user settings. { "files.exclude": { "out": false // set this to true to hide the "out" folder with the compiled JS files }, "search.exclude": { "out": true // set this to false to include "out" folder in search results }, // Turn off tsc task auto detection since we have the necessary tasks as npm scripts "typescript.tsc.autoDetect": "off" } ================================================ FILE: .vscode/tasks.json ================================================ // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format // "$tsc-watch" definition: https://github.com/microsoft/vscode/blob/main/extensions/typescript-language-features/package.json { "version": "2.0.0", "tasks": [ { "label": "Dev - Compile", "detail": "Fast compilation for development time.", "type": "npm", "script": "dev-compile", "problemMatcher": "$tsc-watch", "isBackground": true, "presentation": { "reveal": "never" }, "group": { "kind": "build", "isDefault": true } } ] } ================================================ FILE: .vscodeignore ================================================ # Important: The patterns are relative to the location of the vscodeignore file. # To help understand and edit, patterns should match the whole path whenever possible. # Development tools, intermediate build results, and temporary files .vscode-test/ build/** out/ test/** tools/ **/*.map # Source files node_modules/** src/ **/*.ts !node_modules/katex/dist/fonts/*.woff2 !node_modules/katex/dist/katex.min.css !node_modules/markdown-it-github-alerts/styles/*.css # Configuration files .github/ .vscode/ .gitignore **/tsconfig*.json package-json.lock webpack.config.js # The welcome flag file welcome/WELCOMED # VS Code maps local image paths to GitHub paths images !images/Markdown-mark.png ================================================ FILE: CHANGELOG.md ================================================ ## 3.6.3 (2025.03.09) ### New - [TOC] Add a new slugify method for Zola ([#1419](https://github.com/yzhang-gh/vscode-markdown/pull/1419)). Thanks, [hill (@float3)](https://github.com/float3). - [i18n] Add zh-tw translations ([#1499](https://github.com/yzhang-gh/vscode-markdown/pull/1499)). Thanks, [Will 保哥 (@doggy8088)](https://github.com/doggy8088). ### Fixes - Fix keybinding conflict with GitHub Copilot NES ([#1498](https://github.com/yzhang-gh/vscode-markdown/pull/1498)). Thanks, [Will 保哥 (@doggy8088)](https://github.com/doggy8088). ### Others - [TOC] Update slugify method for Azure DevOps ([#1383](https://github.com/yzhang-gh/vscode-markdown/pull/1383)). Thanks, [Levi Richards (@PHLUNK)](https://github.com/PHLUNK). - [GFM] Make `markdown-it-github-alerts` case insensitive ([#1389](https://github.com/yzhang-gh/vscode-markdown/pull/1389)). Thanks, [PraZ (@prazdevs)](https://github.com/prazdevs). - Support the Eclipse Theia IDE ([#1442](https://github.com/yzhang-gh/vscode-markdown/pull/1442)). Thanks, [Max Jakobitsch (@madmini)](https://github.com/madmini). ## 3.6.0/3.6.1/3.6.2 (2024.01.08) ### New - [Completion] Add an option `completion.enabled` which allows to re-enable the path completion provided by this extension ([#1258](https://github.com/yzhang-gh/vscode-markdown/issues/1258)). - [Export] Add a right-click menu entry for the command `printToHtml` ([#1278](https://github.com/yzhang-gh/vscode-markdown/pull/1278)). Thanks, [Dihong Luo (@Andy-Dihong-Luo)](https://github.com/Andy-Dihong-Luo). - [Export] Add an option `print.pureHtml` ([#1310](https://github.com/yzhang-gh/vscode-markdown/issues/1310)). - [Table formatter] Add range format support ([#1361](https://github.com/yzhang-gh/vscode-markdown/pull/1361)). Thanks, [PeaceShi (@peaceshi)](https://github.com/peaceshi). - [List continuation] New `onEnterKey` logic so as to align with other editors ([#1364](https://github.com/yzhang-gh/vscode-markdown/pull/1364)). Thanks, [@HughLink](https://github.com/HughLink). - Add action buttons (e.g. toggle bold, italic) to the editor toolbar, you can enable it with a new option `showActionButtons` ([#1358](https://github.com/yzhang-gh/vscode-markdown/issues/1358)). Thanks, [PeaceShi (@peaceshi)](https://github.com/peaceshi). - Support [GFM alerts](https://github.com/orgs/community/discussions/16925) ([#1350](https://github.com/yzhang-gh/vscode-markdown/issues/1350)). Thanks, [PraZ (@prazdevs)](https://github.com/prazdevs). ### Fixes - Fix `when` conditions of the `closePreview` commands/keybindings ([#1263](https://github.com/yzhang-gh/vscode-markdown/issues/1263)). - Remove the default key binding Alt+c on Mac ([#1285](https://github.com/yzhang-gh/vscode-markdown/issues/1285)). - [Auto preview] Fix conditions (Jupyter Notebook, VS Code comment widget) of the `autoShowPreviewToSide` feature ([#1288](https://github.com/yzhang-gh/vscode-markdown/pull/1288), thanks, [Dihong Luo (@Andy-Dihong-Luo)](https://github.com/Andy-Dihong-Luo)), ([#1342](https://github.com/yzhang-gh/vscode-markdown/issues/1342)). - [Export] No long convert `.md` to `.html` if the links are GitHub urls ([#1324](https://github.com/yzhang-gh/vscode-markdown/issues/1324)). - [Export] Now correctly convert `.md#anchor` type links ([#1347](https://github.com/yzhang-gh/vscode-markdown/issues/1347)). - [List continuation] Check option `editor.acceptSuggestionOnEnter` ([#1367](https://github.com/yzhang-gh/vscode-markdown/issues/1367)). - **v3.6.2 (2024.01.15)** Fix "GFM alerts" exported HTML styles ([#1386](https://github.com/yzhang-gh/vscode-markdown/issues/1386)). ### Others - [i18n] Update Chinese translations ([#1286](https://github.com/yzhang-gh/vscode-markdown/pull/1286)). Thanks, [Dihong Luo (@Andy-Dihong-Luo)](https://github.com/Andy-Dihong-Luo). ## 3.5.1 (2023.03.26) ### Fixes - Quarto support, [#618](https://github.com/yzhang-gh/vscode-markdown/issues/618) follow-up ([#1199](https://github.com/yzhang-gh/vscode-markdown/pull/1199)). Thanks, [Dongdong Kong (@kongdd)](https://github.com/kongdd). - [List renumbering] Incorrect second-level list renumbering on line delete ([#1155](https://github.com/yzhang-gh/vscode-markdown/issues/1155)). - [Toggle list] A bug in multi-line case ([#1203](https://github.com/yzhang-gh/vscode-markdown/issues/1203)). - [HTML] A bug that generates duplicated heading ids ([#1232](https://github.com/yzhang-gh/vscode-markdown/issues/1232)). ### Others - [i18n] Add Russian translations ([#1201](https://github.com/yzhang-gh/vscode-markdown/pull/1201)). Thanks, [Sergey Romanov (@Serhioromano)](https://github.com/Serhioromano). - Fix the Shields build status badge ([#1215](https://github.com/yzhang-gh/vscode-markdown/pull/1215)). Thanks, [James H (@hughesjs)](https://github.com/hughesjs). - Remove extra spaces when pasting links on selected text ([#1245](https://github.com/yzhang-gh/vscode-markdown/pull/1245)). Thanks, [auh (@fanlushuai)](https://github.com/fanlushuai). - [Math] KaTeX v0.16.4 - [Completion] Disable path completions as VS Code now has built-in support. - [Completion] Always exclude `.git` from completions. ## 3.5.0 (2022.11.20) ### New - [TOC] Use `` to omit a certain section from the table of contents. `` is still supported for backward compatibility ([#1118](https://github.com/yzhang-gh/vscode-markdown/issues/1118)). - [List continuation] The continuation of task list should now has the same behavior as other editors ([#1138](https://github.com/yzhang-gh/vscode-markdown/pull/1138)). Thanks, [@yy0931](https://github.com/yy0931). - [List] New option `list.toggle.candidate-markers` to custom list markers when you use command `Toggle list` ([#1145](https://github.com/yzhang-gh/vscode-markdown/pull/1145)). Thanks, [@petergithub](https://github.com/petergithub). - Add a new option `bold.indicator` so you can use either `**` or `__` for bold text ([#1174](https://github.com/yzhang-gh/vscode-markdown/pull/1174)). Thanks, [@krsche](https://github.com/krsche) and [Samuel Weinhardt (@samuel-weinhardt)](https://github.com/samuel-weinhardt). - [R Markdown] Add support for R Markdown and a new option `extraLangIds` which accepts only `rmd` ~~and `qmd`~~ for now. ([#618](https://github.com/yzhang-gh/vscode-markdown/issues/618)). Thank [Dongdong Kong (@kongdd)](https://github.com/kongdd) for [#1198](https://github.com/yzhang-gh/vscode-markdown/pull/1198). - [GFM task lists] Checkboxes are now visually enabled but but not clickable ([#1189](https://github.com/yzhang-gh/vscode-markdown/pull/1189)). Thanks, [Ian Holst (@ianholst)](https://github.com/ianholst). ### Fixes - Update word pattern for code spans and strikethrough ([#1130](https://github.com/yzhang-gh/vscode-markdown/pull/1130)). Thanks, [@Yarakashi-Kikohshi](https://github.com/Yarakashi-Kikohshi). - [Syntax decorations] of code spans ([#1134](https://github.com/yzhang-gh/vscode-markdown/pull/1134), [#1135](https://github.com/yzhang-gh/vscode-markdown/pull/1135)). Thanks, [@yy0931](https://github.com/yy0931). - [Ordered list renumbering] An issue with sub-list ([#1155](https://github.com/yzhang-gh/vscode-markdown/issues/1155)). - [HTML] Remove `` comment in the title of the exported HTML ([#1175](https://github.com/yzhang-gh/vscode-markdown/issues/1175)). ### Others - Code refactoring ([#1119](https://github.com/yzhang-gh/vscode-markdown/pull/1119)). Thanks, [@Lemmingh](https://github.com/Lemmingh). - [Math] Update math function completions. - Add custom context key for `onTabKey/onShiftTabKey` key binding. There should be less key binding conflicts in the future ([#1075](https://github.com/yzhang-gh/vscode-markdown/pull/1075)). Thanks, [@takumisoft68](https://github.com/takumisoft68). - Better blockquote continuation ([#1183](https://github.com/yzhang-gh/vscode-markdown/issues/1183)). - Reduce extension size by removing unused images ([#1161](https://github.com/yzhang-gh/vscode-markdown/issues/1161)). Thanks, [Kid (@kidonng)](https://github.com/kidonng). ## 3.4.3 (2022.4.24) ### Fixes - [Math] mhchem support ([#1116](https://github.com/yzhang-gh/vscode-markdown/issues/1116)). - VS Code freezes because of the new word pattern ([#1117](https://github.com/yzhang-gh/vscode-markdown/issues/1117)). ## 3.4.1 (2022.4.17) **Update 3.4.2**: fix dependencies. ### Breaking Changes - [Table formatter] Now you need to escape the pipe character (`|`) inside table cells, even if it is in a code span. This behavior follows the [GFM spec](https://github.github.com/gfm/#example-200). ([#24](https://github.com/yzhang-gh/vscode-markdown/issues/24)) ### New - [Auto completion] Add `.webp` files to the image path suggestions ([#934](https://github.com/yzhang-gh/vscode-markdown/issues/934)). ### Fixes - Resolve Tab conflict when `inlineSuggestionVisible`, e.g., GitHub Copilot ([#665](https://github.com/yzhang-gh/vscode-markdown/issues/665), [#1011](https://github.com/yzhang-gh/vscode-markdown/issues/1011)). - Multi-cursor list editing ([#829](https://github.com/yzhang-gh/vscode-markdown/issues/829), [#926](https://github.com/yzhang-gh/vscode-markdown/issues/926)). - You can now add section numbers larger than 99 ([#852](https://github.com/yzhang-gh/vscode-markdown/issues/852)). - Resolve keybinding conflict Alt + Shift + Up/Down on Linux ([#857](https://github.com/yzhang-gh/vscode-markdown/issues/857)). - TOC with a heading ending with literal `#` ([#867](https://github.com/yzhang-gh/vscode-markdown/issues/867)). - Load extension-contributed scripts asynchronously ([#956](https://github.com/yzhang-gh/vscode-markdown/pull/956)). Thanks, [Jay Park (@phoihos)](https://github.com/phoihos). - The internal command `_wrapBy` ignores the `after` argument ([#1051](https://github.com/yzhang-gh/vscode-markdown/pull/1051)). Thanks, [@King-of-Infinite-Space](https://github.com/King-of-Infinite-Space). - Set `vscode-dark` class when exporting to HTML with dark theme ([#1091](https://github.com/yzhang-gh/vscode-markdown/pull/1091)). Thanks, [Raphael Sander (@raphaelsander)](https://github.com/raphaelsander). ### Others - Code health ([#869](https://github.com/yzhang-gh/vscode-markdown/pull/869)). Thanks to [@Lemmingh](https://github.com/Lemmingh). - (Temporary fix) The `toggleMath` issue with blockquotes ([#907](https://github.com/yzhang-gh/vscode-markdown/issues/907)). - Update Japanese translations ([#909](https://github.com/yzhang-gh/vscode-markdown/pull/909)). Thanks, [にせ十字 (@falsecross)](https://github.com/falsecross). - [Math] Upgrade KaTeX ([#943](https://github.com/yzhang-gh/vscode-markdown/issues/943)). - The `togglePreview` command has been replaced by `closePreview` ([`05fb1af`](https://github.com/yzhang-gh/vscode-markdown/commit/05fb1af27150fa8c1c271fc03533d28787ea25d1)). - Enable virtual workspaces support (limited functionality) ([#948](https://github.com/yzhang-gh/vscode-markdown/pull/948), [#996](https://github.com/yzhang-gh/vscode-markdown/pull/996)) - Update Markdown word pattern ([#1092](https://github.com/yzhang-gh/vscode-markdown/issues/1092)). - A few documentation improvements. --- ### 3.4.0 (2020.11.14) - **New**: New TOC slugify mode `azureDevops` ([#802](https://github.com/yzhang-gh/vscode-markdown/issues/802)). - **Fix**: Math syntax highlight ([#346](https://github.com/yzhang-gh/vscode-markdown/issues/346)). - **Fix**: Color of the inline code span outline (now using `editor.selectionBackground`) ([#734](https://github.com/yzhang-gh/vscode-markdown/issues/734)). - **Fix**: Broken TOC links if headings have emoji ([#792](https://github.com/yzhang-gh/vscode-markdown/issues/792)). - **Fix**: Catch exception of other Markdown extensions ([#834](https://github.com/yzhang-gh/vscode-markdown/issues/834)). - **Fix**: Compatibility with [Markdown Preview Github Styling](https://marketplace.visualstudio.com/items?itemName=bierner.markdown-preview-github-styles). - **Other**: Many documentation improvements. - **Other**: Automatically build `debug.vsix` using GitHub Actions ([#837](https://github.com/yzhang-gh/vscode-markdown/pull/837)). --- ### 3.3.0 (2020.08.31) - **New**: You can now use `` comment to specify a title of the exported HTML ([#506](https://github.com/yzhang-gh/vscode-markdown/issues/506)). - **New**: Added a `toc.slugifyMode` option `gitea` ([#763](https://github.com/yzhang-gh/vscode-markdown/pull/763)). Thanks, [Rodolphe Houdas (@rodolpheh)](https://github.com/rodolpheh). - **New**: Option `completion.respectVscodeSearchExclude` controlling where to look for file path completions ([#789](https://github.com/yzhang-gh/vscode-markdown/issues/789)). - **Fix**: Failure of `toggleList` command if there are more than 9 list items ([#776](https://github.com/yzhang-gh/vscode-markdown/issues/776)). - **Fix**: Command `togglePreviewToSide` wouldn't close the preview tab ([#780](https://github.com/yzhang-gh/vscode-markdown/pull/780)). Thanks, [Anton Tuchkov (@Technik-J)](https://github.com/Technik-J). - **Fix**: `- - -` was wrongly treated as a list item ([#785](https://github.com/yzhang-gh/vscode-markdown/issues/785)). - **Other**: Activate extension on command `printToHtmlBatch` ([#769](https://github.com/yzhang-gh/vscode-markdown/issues/769)). --- ### 3.2.0 (2020.07.25) - **New**: [Batch export] command "print document**s** to HTML" ([#594](https://github.com/yzhang-gh/vscode-markdown/issues/594), [#747](https://github.com/yzhang-gh/vscode-markdown/issues/747)). - **Fix**: [HTML export] escape spaces in image path ([#731](https://github.com/yzhang-gh/vscode-markdown/issues/731)). - **Fix**: [TOC] headings with LaTeX ([#732](https://github.com/yzhang-gh/vscode-markdown/issues/732)). - **Other**: A lot of `README` improvements. --- ### 3.1.0 (2020.06.20) - **New**: Option `print.includeVscodeStylesheets` ([#726](https://github.com/yzhang-gh/vscode-markdown/pull/726)). Thanks, [Gluck_S (@Glucksistemi)](https://github.com/Glucksistemi). - **New**: Option `syntax.decorationFileSizeLimit` ([#728](https://github.com/yzhang-gh/vscode-markdown/pull/728)). Thanks, [Rohit Patnaik (@quanticle)](https://github.com/quanticle). - **Fix**: Wrong mime type of SVG in exported HTML ([#694](https://github.com/yzhang-gh/vscode-markdown/issues/694)). - **Fix**: Heading numbering issues ([#695](https://github.com/yzhang-gh/vscode-markdown/issues/695), [#701](https://github.com/yzhang-gh/vscode-markdown/issues/701)). - **Fix**: TOC issues ([#555](https://github.com/yzhang-gh/vscode-markdown/issues/555), [#699](https://github.com/yzhang-gh/vscode-markdown/issues/699), [#706](https://github.com/yzhang-gh/vscode-markdown/issues/706)) - **Fix**: Counterintuitive behavior of command `checkTaskList` ([#700](https://github.com/yzhang-gh/vscode-markdown/issues/700)). - **Other**: `README` improvements ([#709](https://github.com/yzhang-gh/vscode-markdown/pull/709)). Thanks, [Quang Vu (@vhquang)](https://github.com/vhquang). --- ### 3.0.0 (2020.05.24) #### Highlights

section numbers

markdown extensions

- **Breaking change**: Replace `toc.githubCompatibility` with `toc.slugifyMode`. Now GitLab-style TOC is supported ([#660](https://github.com/yzhang-gh/vscode-markdown/pull/660)). Thanks, [@BeeeWall](https://github.com/BeeeWall). - **New**: Command to add/update/remove numbering to headings ([#457](https://github.com/yzhang-gh/vscode-markdown/issues/457), [#555](https://github.com/yzhang-gh/vscode-markdown/issues/555)). - **New**: Automatically include other installed Markdown plugins when exporting Markdown to HTML ([#658](https://github.com/yzhang-gh/vscode-markdown/pull/658)). Thanks, [qiqiworld (@1354092549)](https://github.com/1354092549). - **New**: The links to `.md` files will be renamed to `.html` in the exported HTML ([#667](https://github.com/yzhang-gh/vscode-markdown/issues/667)). - **Fix**: Properly handle Markdown syntax in TOC entries ([#654](https://github.com/yzhang-gh/vscode-markdown/pull/654)). - **Fix**: An issue with `workspaceFolders` ([#666](https://github.com/yzhang-gh/vscode-markdown/issues/666)). - **Fix**: Slugify function `github` should downcase also non-Latin characters ([#670](https://github.com/yzhang-gh/vscode-markdown/pull/670)). Thanks, [lesha (@lesha-co)](https://github.com/lesha-co). - **Fix**: TOC issues ([#675](https://github.com/yzhang-gh/vscode-markdown/issues/675), [#683](https://github.com/yzhang-gh/vscode-markdown/issues/683)). - **Fix**: Table formatter fails if there are two identical tables ([#682](https://github.com/yzhang-gh/vscode-markdown/issues/682)). - **Fix**: CJ**K** characters in Markdown Tables ([#685](https://github.com/yzhang-gh/vscode-markdown/issues/685)). - **Other**: Expose `wrapBy` function ([#663](https://github.com/yzhang-gh/vscode-markdown/issues/663)). - **Other**: `README` improvements ([#681](https://github.com/yzhang-gh/vscode-markdown/pull/681)). Thanks, [Kaspar (@casaper)](https://github.com/casaper). --- ### 2.8.0 (2020.04.10) - **New**: Path auto-completion now respects option `search.exclude` ([#614](https://github.com/yzhang-gh/vscode-markdown/issues/614)). - **New**: Suggest `katex.macros` in math environments ([#633](https://github.com/yzhang-gh/vscode-markdown/pull/633)). Thanks, [Y. Ding (@yd278)](https://github.com/yd278). - **New**: Option `math.enabled`. - **Fix**: Escape spaces in file path completions ([#590](https://github.com/yzhang-gh/vscode-markdown/pull/590)). Thanks, [Tomoki Aonuma (@uasi)](https://github.com/uasi). - **Fix**: TOC issues ([#593](https://github.com/yzhang-gh/vscode-markdown/issues/593), [#603](https://github.com/yzhang-gh/vscode-markdown/issues/603), [#629](https://github.com/yzhang-gh/vscode-markdown/issues/629)). - **Fix**: Table formatter for Thai characters ([#602](https://github.com/yzhang-gh/vscode-markdown/pull/602)). Thanks, [Nutchanon Ninyawee (@CircleOnCircles)](https://github.com/CircleOnCircles). - **Fix**: Single column table formatting ([#604](https://github.com/yzhang-gh/vscode-markdown/pull/604)). Thanks, [@chnicholas](https://github.com/chnicholas). - **Fix**: Issues with option `omitFromToc` ([#644](https://github.com/yzhang-gh/vscode-markdown/issues/644)). - **Other**: Added Japanese translation ([#608](https://github.com/yzhang-gh/vscode-markdown/pull/608)). Thanks, [にせ十字 (@falsecross)](https://github.com/falsecross). - **Other**: Upgraded KaTeX. - **Other**: Moved from AppVeyor to GitHub Actions. Thank [雪松 (@yxs)](https://github.com/yxs) for the CI badge. --- ### 2.7.0 (2020.01.11) - **New**: Option `omittedFromToc` ([#580](https://github.com/yzhang-gh/vscode-markdown/pull/580)). Thanks, [Dorian Marchal (@dorian-marchal)](https://github.com/dorian-marchal). - **Fix**: Don't continue list item in math environment ([#574](https://github.com/yzhang-gh/vscode-markdown/issues/574)). - **Fix**: HTML entities in TOC ([#575](https://github.com/yzhang-gh/vscode-markdown/issues/575)). - **Fix**: User-defined KaTeX macros weren't included in the exported HTML ([#579](https://github.com/yzhang-gh/vscode-markdown/issues/579)). - **Fix**: Strange HTML tags in the generated TOC ([#585](https://github.com/yzhang-gh/vscode-markdown/issues/585)). - **Fix**: Use `%20` for space in URL ([#589](https://github.com/yzhang-gh/vscode-markdown/issues/589)). - **Other**: Update keybindings ([#571](https://github.com/yzhang-gh/vscode-markdown/issues/571)). - **Other**: Disable decorations for large files (threshold 128 KB → 50 KB) ([#578](https://github.com/yzhang-gh/vscode-markdown/issues/578)). --- ### 2.6.1 (2019.12.12) - **Fix**: Strange HTML tags in TOC ([#567](https://github.com/yzhang-gh/vscode-markdown/issues/567)). --- ### 2.6.0 (2019.12.08) - **New**: Support `` above a heading ([#495](https://github.com/yzhang-gh/vscode-markdown/issues/495)). - **New**: Support `` above a list ([#525](https://github.com/yzhang-gh/vscode-markdown/issues/525)). - **New**: Option `print.theme` ([#534](https://github.com/yzhang-gh/vscode-markdown/issues/534)). - **New**: Command "toggle code block" ([#551](https://github.com/yzhang-gh/vscode-markdown/pull/551)). Thanks, [@axiqia](https://github.com/axiqia). - **New**: Support image path completions for HTML `img` tags. - **New**: Include [Markdown Footnotes](https://marketplace.visualstudio.com/items?itemName=bierner.markdown-footnotes) in exported HTML if you have that extension installed ([#212](https://github.com/yzhang-gh/vscode-markdown/issues/212)). - **Fix**: TOC links ([#494](https://github.com/yzhang-gh/vscode-markdown/issues/494), [#515](https://github.com/yzhang-gh/vscode-markdown/issues/515) and [#550](https://github.com/yzhang-gh/vscode-markdown/issues/550)). - **Fix**: No longer convert images paths with data URIs ([#539](https://github.com/yzhang-gh/vscode-markdown/pull/539)). Thanks, [@leapwill](https://github.com/leapwill). - **Fix**: Unexpected ordered list marker updating ([#546](https://github.com/yzhang-gh/vscode-markdown/pull/546)). Thanks, [Alper Cugun (@alper)](https://github.com/alper). - **Fix**: Shift + Tab never outdents ([#561](https://github.com/yzhang-gh/vscode-markdown/issues/561)). - **Other**: Update `README` with high-resolution images. --- ### 2.5.0/2.5.1 (2019.10.12) - **New**: File path completions ([#497](https://github.com/yzhang-gh/vscode-markdown/pull/497)). Thanks, [@linsui](https://github.com/linsui). - **New**: Toggle multiple checkboxes ([#513](https://github.com/yzhang-gh/vscode-markdown/pull/513)). Thanks, [@GeorchW](https://github.com/GeorchW). - **New**: Option `print.validateUrls` ([#517](https://github.com/yzhang-gh/vscode-markdown/pull/517)). Thanks, [Olmo Maldonado (@ibolmo)](https://github.com/ibolmo). - **New**: Add KaTeX mhchem extension ([#521](https://github.com/yzhang-gh/vscode-markdown/pull/521)). Thanks, [Balthild Ires (@balthild)](https://github.com/balthild). - **New**: Option `completion.root` ([#526](https://github.com/yzhang-gh/vscode-markdown/issues/526)). - **Fix**: Cannot recognize indented headings ([#508](https://github.com/yzhang-gh/vscode-markdown/issues/508)). - **Fix**: TOC and code blocks ([#532](https://github.com/yzhang-gh/vscode-markdown/issues/532)). - **Other**: New logo with white background ([#498](https://github.com/yzhang-gh/vscode-markdown/issues/498)). - **Other**: Remove obsolete HTML attributes ([#499](https://github.com/yzhang-gh/vscode-markdown/issues/499)). - **Other**: Use light theme in exported HTML ([#529](https://github.com/yzhang-gh/vscode-markdown/issues/529)). --- ### 2.4.1/2.4.2 (2019.07.21) - **New**: Option `toc.downcaseLink` (default: `true`) ([#476](https://github.com/yzhang-gh/vscode-markdown/issues/476)). - **Fix**: KaTeX macros ([#473](https://github.com/yzhang-gh/vscode-markdown/pull/473)). Thanks, [Pierre (@PierreMarchand20)](https://github.com/PierreMarchand20). - **Fix**: Ignore headings in comments ([#462](https://github.com/yzhang-gh/vscode-markdown/issues/462)). - **Fix**: Magic comment `` was ignored ([#490](https://github.com/yzhang-gh/vscode-markdown/issues/490)). - **Other**: Improve performance for large documents --- ### 2.4.0 (2019.06.16) - **New**: Command `toggleList` (*Note: no default keybinding assigned*) ([#237](https://github.com/yzhang-gh/vscode-markdown/issues/237), [#307](https://github.com/yzhang-gh/vscode-markdown/issues/307)). ![toggle list](images/gifs/toggle-list.gif) - **New**: Support KaTeX macros ([#426](https://github.com/yzhang-gh/vscode-markdown/issues/426)). Thanks, [Pierre (@PierreMarchand20)](https://github.com/PierreMarchand20). - **Fix**: Image paths ([#415](https://github.com/yzhang-gh/vscode-markdown/issues/415)). - **Fix**: Fenced code block checking ([#434](https://github.com/yzhang-gh/vscode-markdown/issues/434)). - **Other**: Don't downcase the TOC links ([#312](https://github.com/yzhang-gh/vscode-markdown/issues/312)). Thanks, [Scott Meesseman (@spmeesseman)](https://github.com/spmeesseman). - **Other**: Command `toggleMath` now cycles through `|` -> `$|$` -> `$$\n|\n$$` -> `$$ | $$` ([#421](https://github.com/yzhang-gh/vscode-markdown/issues/421#issuecomment-493747064)). Thanks, [Li Yiming (@upupming)](https://github.com/upupming). - **Other**: Don't include KaTeX stylesheets in the exported HTML if no math ([#430](https://github.com/yzhang-gh/vscode-markdown/issues/430)). - **Other**: Upgrade KaTeX ([#446](https://github.com/yzhang-gh/vscode-markdown/issues/446)). - **Other**: Better math completions ([PR#470](https://github.com/yzhang-gh/vscode-markdown/pull/470), [PR#471](https://github.com/yzhang-gh/vscode-markdown/pull/471)). --- ### 2.3.1 (2019.04.29) - **Fix**: Option `markdown.extension.print.onFileSave` not respected ([#432](https://github.com/yzhang-gh/vscode-markdown/issues/432)). --- ### 2.3.0 (2019.04.28) - **New** Prefer unused links for reference link label completions ([#414](https://github.com/yzhang-gh/vscode-markdown/issues/414)). Thanks, [Chris (@alshain)](https://github.com/alshain). - **New**: Option `markdown.extension.print.onFileSave` ([#417](https://github.com/yzhang-gh/vscode-markdown/issues/417)). Thanks, [Li Yiming (@upupming)](https://github.com/upupming). - **New**: Autocompletion for heading links ([#419](https://github.com/yzhang-gh/vscode-markdown/issues/419)). Thanks again, [Chris (@alshain)](https://github.com/alshain). - **Fix**: Syntax decorations ([#390](https://github.com/yzhang-gh/vscode-markdown/issues/390)). - **Fix**: Table formatter ([#408](https://github.com/yzhang-gh/vscode-markdown/issues/408)). - **Fix**: Delete space rather than outdent list when there are two or more spaces on Backspace ([#410](https://github.com/yzhang-gh/vscode-markdown/issues/410)). - **Fix**: Image paths in exported HTML ([#415](https://github.com/yzhang-gh/vscode-markdown/issues/415), [#429](https://github.com/yzhang-gh/vscode-markdown/issues/429)). - **Fix**: TOC and fenced code blocks ([#425](https://github.com/yzhang-gh/vscode-markdown/issues/425)). - **Other**: Sort KaTeX functions (lowercase first) ([#413](https://github.com/yzhang-gh/vscode-markdown/issues/413)). - **Other**: Update KaTeX supported functions ([#416](https://github.com/yzhang-gh/vscode-markdown/issues/416)). Thanks again, [Li Yiming (@upupming)](https://github.com/upupming). --- ### 2.2.0 (2019.03.24) - **Fix**: Better syntax decorations ([#390](https://github.com/yzhang-gh/vscode-markdown/issues/390), [#393](https://github.com/yzhang-gh/vscode-markdown/issues/393)). - **Fix**: Recognize relative path of `markdown.styles` when exporting to HTML ([#394](https://github.com/yzhang-gh/vscode-markdown/issues/394)). - **Other**: Unregister formatter when being disabled ([#395](https://github.com/yzhang-gh/vscode-markdown/issues/395)). - **Other**: Better URL regexp ([#397](https://github.com/yzhang-gh/vscode-markdown/issues/397)). Thanks, [Igor (@Ovsyanka)](https://github.com/Ovsyanka). - **Other**: Remove default `alt + s` keybinding for macOS ([#404](https://github.com/yzhang-gh/vscode-markdown/issues/404)). - **Other**: webpack! --- ### 2.1.1 (2019.03.05) - **Fix**: Table format ([#381](https://github.com/yzhang-gh/vscode-markdown/issues/381)). - **Fix**: Unexpected link creation on pasting ([#382](https://github.com/yzhang-gh/vscode-markdown/issues/382)). - **Fix**: Image path encoding when printing ([#385](https://github.com/yzhang-gh/vscode-markdown/issues/385)). --- ### 2.1.0 (2019.02.16) - **New**: Paste link on selected text ([#20](https://github.com/yzhang-gh/vscode-markdown/issues/20)). ![paste](images/gifs/paste.gif) - **New**: Multi-cursor support ([#33](https://github.com/yzhang-gh/vscode-markdown/issues/33)). ![multi-cursor](images/gifs/multi-cursor.gif) - **New**: Auto-complete for reference link IDs ([#366](https://github.com/yzhang-gh/vscode-markdown/issues/366)). ![suggest ref link](images/gifs/suggest-ref-link.png) - **Fix**: Conflict with `editor.tabCompletion` setting ([#367](https://github.com/yzhang-gh/vscode-markdown/issues/367)). - **Other**: Added ways to buy me a coffee 😉 ([PayPal](https://www.paypal.me/2yzhang), [Alipay or WeChat](donate.md)). --- ### 2.0.0 (2019.01.19) 🎂🎂 This extension is 2 years old! - **New**: Option `markdown.extension.list.indentationSize` ([#344](https://github.com/yzhang-gh/vscode-markdown/issues/344)). - `adaptive`: use 2 spaces indentation for unordered lists, 3 for ordered lists. - `inherit`: respect the tab size setting of current file. - **New**: Copy math as TeX command in exported HTML ([#358](https://github.com/yzhang-gh/vscode-markdown/issues/358)). - **Fix**: Many performance issue ([#181](https://github.com/yzhang-gh/vscode-markdown/issues/181), [#323](https://github.com/yzhang-gh/vscode-markdown/issues/323)). - **Fix**: Fake heading in YAML front matter ([#343](https://github.com/yzhang-gh/vscode-markdown/issues/343)). - **Fix**: Math function `\neq` rendering ([#252](https://github.com/yzhang-gh/vscode-markdown/issues/252), [#349](https://github.com/yzhang-gh/vscode-markdown/issues/349)). - **Fix**: Keybinding for checking/unchecking task list ([#361](https://github.com/yzhang-gh/vscode-markdown/issues/361)). - **Fix**: Backspace conflicts with Vim extension ([#362](https://github.com/yzhang-gh/vscode-markdown/issues/362)). - **Fix**: GFM table syntax ([#316](https://github.com/yzhang-gh/vscode-markdown/issues/316)). Thanks a lot, [Li Yiming (@upupming)](https://github.com/upupming). --- ### 1.8.0 (2018.12.08) - **New**: Option `markdown.extension.toc.tabSize`, default `auto`. Thanks, [Maël Valais (@maelvalais)](https://github.com/maelvalais). - **New**: Adaptive indentation size on Tab/Backspace key ([#155](https://github.com/yzhang-gh/vscode-markdown/issues/155), [#241](https://github.com/yzhang-gh/vscode-markdown/issues/241)). - **New**: Better alignment of cells within tables ([#341](https://github.com/yzhang-gh/vscode-markdown/issues/341)). Thanks, [Sriram Krishna (@k-sriram)](https://github.com/k-sriram). - **Fix**: Support setext headings in TOC ([#284](https://github.com/yzhang-gh/vscode-markdown/issues/284), [#311](https://github.com/yzhang-gh/vscode-markdown/issues/311)). - **Fix**: Markdown preview stylesheets priority (VSCode base styles < VSCode preview settings < Custom stylesheets) ([#329](https://github.com/yzhang-gh/vscode-markdown/issues/329)). - **Fix**: Math completions for untitled document ([#326](https://github.com/yzhang-gh/vscode-markdown/issues/326)). - **Fix**: Image completions ([#330](https://github.com/yzhang-gh/vscode-markdown/issues/330)). - **Other**: Use `cmd` instead of `ctrl` for some keybindings on Mac ([#334](https://github.com/yzhang-gh/vscode-markdown/issues/334)). --- ### 1.7.0 (2018.10.27) - **New**: Math syntax highlight ([#254](https://github.com/yzhang-gh/vscode-markdown/issues/254)). Many thanks, [@linsui](https://github.com/linsui). - **Fix**: `imgToBase64` option doesn't apply to relative image paths ([#266](https://github.com/yzhang-gh/vscode-markdown/issues/266)). - **Fix**: TOC generation error `Cannot read property '1' of null` ([#275](https://github.com/yzhang-gh/vscode-markdown/issues/275)). - **Fix**: Escape HTML markup in code blocks ([#285](https://github.com/yzhang-gh/vscode-markdown/issues/285)). - **Fix**: Fix false positive TOC detection ([#304](https://github.com/yzhang-gh/vscode-markdown/issues/304)). - **Other**: Generate HTML with `title` field ([#280](https://github.com/yzhang-gh/vscode-markdown/issues/280)). - **Other**: Upgrade `KaTeX` to `v0.10.0-rc.1` --- ### 1.6.3 (2018.10.24) - **Fix**: Table formatter --- ### 1.6.1 (2018.09.10), 1.6.2 (2018.09.19) - **Fix**: for VSCode v1.28.0-insider (and again) - **Other**: Remove outline view feature --- ### 1.6.0 (2018.07.22) - **New**: Add Chinese language support ([#240](https://github.com/yzhang-gh/vscode-markdown/issues/240)). Thanks, [@linsui](https://github.com/linsui). - **Fix**: Some minor bugs ([#205](https://github.com/yzhang-gh/vscode-markdown/issues/205), [#223](https://github.com/yzhang-gh/vscode-markdown/issues/223), [#231](https://github.com/yzhang-gh/vscode-markdown/issues/231)). Thanks, [Tom Bresson (@tombresson)](https://github.com/tombresson) for #231. - **Other**: More math completions (in fact, all KaTeX function) ([#219](https://github.com/yzhang-gh/vscode-markdown/issues/219)). --- ### 1.5.1 (2018.06.29) - **Fix**: Handle activation error for vscode earlier than v1.24.0. --- ### 1.5.0 (2018.06.24) - **New**: Additional syntax decorations (for strikethrough, code span etc.) and a new plain theme ([#185](https://github.com/yzhang-gh/vscode-markdown/issues/185)). - **New**: Show image preview along with path intellisense ([#188](https://github.com/yzhang-gh/vscode-markdown/issues/188)). - **Fix**: Multi-line task list indentation ([#203](https://github.com/yzhang-gh/vscode-markdown/issues/203)). - **Fix**: Add unique ids to duplicate headings (only when `githubCompatibility` is `true`) ([#211](https://github.com/yzhang-gh/vscode-markdown/issues/211)). - **Other**: Upgrade KaTeX version ([#196](https://github.com/yzhang-gh/vscode-markdown/issues/196)). ![v1.5.0 release note](images/v1.5.0.png) --- ### 1.4.0 (2018.05.20) - **New**: Auto completions! Images paths and math commands - **New**: Use comment `` to omit specific heading in TOC ([#177](https://github.com/yzhang-gh/vscode-markdown/issues/177)). - **New**: Option `print.imgToBase64`, encoding images into HTML file ([#73](https://github.com/yzhang-gh/vscode-markdown/issues/73)). Thanks, [Eric Yancey Dauenhauer (@ericyd)](https://github.com/ericyd). - **Fix**: Regression on table formatting ([#171](https://github.com/yzhang-gh/vscode-markdown/issues/171)). Thanks, [Stefan Zi (@StefanZi)](https://github.com/StefanZi). - **Fix**: Problem of losing track of TOC after editing the first heading ([#48](https://github.com/yzhang-gh/vscode-markdown/issues/48)). - **Other**: Remove `quickStylingMode` option. (It's default behavior now) - **Other**: Provide latest CI build ([here](https://ci.appveyor.com/project/yzhang-gh/vscode-markdown/build/artifacts)). --- ### 1.3.0 (2018.05.06) - **New**: Automatically fix list markers when editing ordered list ([#32](https://github.com/yzhang-gh/vscode-markdown/issues/32), [#104](https://github.com/yzhang-gh/vscode-markdown/issues/104), [#154](https://github.com/yzhang-gh/vscode-markdown/issues/154)). Thanks, [Eric Yancey Dauenhauer (@ericyd)](https://github.com/ericyd) - **New**: Keyboard shortcut for toggling math environment (Ctrl + M) ([#165](https://github.com/yzhang-gh/vscode-markdown/issues/165)) - **New**: Command `toggleUnorderedList`, switching between non-list, - , * and + ([#145](https://github.com/yzhang-gh/vscode-markdown/issues/145)) - **Fix**: Tables inside list item will be also formatted now ([#107](https://github.com/yzhang-gh/vscode-markdown/issues/107)). Thanks, [Stefan Zi (@StefanZi)](https://github.com/StefanZi) - **Fix**: Keybinding (Ctrl + K V) conflicts with command `workbench.action.terminal.clear` ([#161](https://github.com/yzhang-gh/vscode-markdown/issues/161)) - **Other**: Handle Japanese characters when formatting tables ([#153](https://github.com/yzhang-gh/vscode-markdown/issues/153)). Thanks, [Matsuyanagi (@Matsuyanagi)](https://github.com/Matsuyanagi) - **Other**: Smartly set collapse states when showing outline view ([#149](https://github.com/yzhang-gh/vscode-markdown/issues/149)) #### List Renumbering ![list renumbering](images/gifs/list-renumbering.gif) #### Keyboard Shortcut for Toggling Math Environment ![math toggle](images/gifs/math-toggle.gif) #### Toggle Unordered List (assign your desired key binding to `markdown.extension.editing.toggleUnorderedList` first) ![toggle unordered list](images/gifs/toggle-unordered-list.gif) --- ### 1.2.0 (2018.04.20) - **New**: Math rendering! (supported in both vscode preview and exported HTML) ([#106](https://github.com/yzhang-gh/vscode-markdown/issues/106)) - **New**: Option `toc.githubCompatibility` (in place of removed `toc.encodeUri` and `toc.toLowerCase`) - **Fix**: Replace underscore with dash when slugifying ([#147](https://github.com/yzhang-gh/vscode-markdown/issues/147)) - **Other**: Add default keybinding Alt + S to command `toggleStrikethrough` ([#91](https://github.com/yzhang-gh/vscode-markdown/issues/91)) --- ### 1.1.2 (2018.04.04) - **New**: Option `toc.toLowerCase` determining whether or not lowercasing TOC anchors ([#136](https://github.com/yzhang-gh/vscode-markdown/issues/136), [#137](https://github.com/yzhang-gh/vscode-markdown/issues/137). Thanks, [Владислав Люминарский (@Vladislav-Lyuminarskiy)](https://github.com/Vladislav-Lyuminarskiy)) - **Fix**: Handle relative CSS paths in `markdown.styles` setting when printing ([#113](https://github.com/yzhang-gh/vscode-markdown/issues/113)) - **Fix**: TOC now works better with ordered list ([#130](https://github.com/yzhang-gh/vscode-markdown/issues/130), [#131](https://github.com/yzhang-gh/vscode-markdown/issues/131)) - **Fix**: Keybinding conflict between `togglePreview` and `paste` on Linux ([#134](https://github.com/yzhang-gh/vscode-markdown/issues/134)) - **Fix**: Reveal cursor after editing list in case it is out of view ([#138](https://github.com/yzhang-gh/vscode-markdown/issues/138)) --- ### 1.1.1 (2018.03.24) - **New**: Override default "Open Preview" keybinding with "Toggle Preview". Now you can close preview use the same keybinding. ([#86](https://github.com/yzhang-gh/vscode-markdown/issues/86)) - **Fix**: No outline if first-level headiing is missing ([#120](https://github.com/yzhang-gh/vscode-markdown/issues/120)) - **Fix**: List does not continue if a list item starts with URL ([#122](https://github.com/yzhang-gh/vscode-markdown/issues/122)) - **Fix**: `print.absoluteImgPath` option doesn't take effect on some image tags ([#124](https://github.com/yzhang-gh/vscode-markdown/issues/124)) - **Fix**: A bug when formatting table ([#128](https://github.com/yzhang-gh/vscode-markdown/issues/128)) --- ### 1.1.0 (2018.03.08) - **New**: Option `toc.encodeUri` ([#90](https://github.com/yzhang-gh/vscode-markdown/issues/90), [#98](https://github.com/yzhang-gh/vscode-markdown/issues/98)) - **Fix**: TOC detection ([#85](https://github.com/yzhang-gh/vscode-markdown/issues/85), [#102](https://github.com/yzhang-gh/vscode-markdown/issues/102)) - **Fix**: Wrong HTML output path if you are editing `.MD` file ([#105](https://github.com/yzhang-gh/vscode-markdown/issues/105)) ### 1.0.5 (2018.02.01) - **Fix**: Option `markdown.extension.print.absoluteImgPath` doesn't work ([#84](https://github.com/yzhang-gh/vscode-markdown/issues/84)) ### 1.0.4 (2018.01.29) - **Fix**: TOC entries that contain links do not generate correctly ([#83](https://github.com/yzhang-gh/vscode-markdown/issues/83)) ### 1.0.3 (2018.01.23) - **New**: Option `markdown.extension.print.absoluteImgPath` ([#81](https://github.com/yzhang-gh/vscode-markdown/issues/81)) ### 1.0.2 (2018.01.15) - **Fix**: Anchors in exported HTML ([#78](https://github.com/yzhang-gh/vscode-markdown/issues/78)) ### 1.0.1 (2018.01.12) - **Fix**: Conditions to show outline ([#60](https://github.com/yzhang-gh/vscode-markdown/issues/60)) - **Fix**: Respect `insertSpaces` and `tabSize` options of current file when generating TOC ([#77](https://github.com/yzhang-gh/vscode-markdown/issues/77)) ### 1.0.0 (2018.01.05) - **New**: Update outline view on save ([#68](https://github.com/yzhang-gh/vscode-markdown/issues/68)) - **New**: Option `markdown.extension.toc.unorderedList.marker` ([#74](https://github.com/yzhang-gh/vscode-markdown/issues/74)) - **Change**: Use Ctrl + Shift + [ (or ]) to change heading level in Mac ([#71](https://github.com/yzhang-gh/vscode-markdown/issues/71)) - **Fix**: Some fixes you might not notice ### 0.11.2 (2017.11.23) - **New**: Option `markdown.extension.tableFormatter.enabled` ([#51](https://github.com/yzhang-gh/vscode-markdown/issues/51)) - **Fix**: Show outline only when current doc is Markdown ([#40](https://github.com/yzhang-gh/vscode-markdown/issues/40)) - **Fix**: Now option `editor.tabCompletion` is correctly handled ([#55](https://github.com/yzhang-gh/vscode-markdown/issues/55)) - **Fix**: Now if you export Markdown to HTML, all CSS will be embedded rather than referred ([#57](https://github.com/yzhang-gh/vscode-markdown/issues/57)) ### 0.11.1 (2017.11.02) - **New**: Use Tab/Backspace key to indent/outdent task list ([#50](https://github.com/yzhang-gh/vscode-markdown/issues/50)) ### 0.11.0 (2017.10.18) - **New**: Support GFM task lists (checkbox) - Press Alt + C to check/uncheck a task list item - **New**: Add new setting `markdown.extension.showExplorer` to control whether to show outline view in the explorer panel (Thank you, [Ali Karbassi (@karbassi)](https://github.com/karbassi), [PR#44](https://github.com/yzhang-gh/vscode-markdown/pull/44)) - **Preview**: Print to HTML/PDF (work in progress) ### 0.10.3 (2017.09.30) - **New**: Support GFM checkbox when continuing list item ([#38](https://github.com/yzhang-gh/vscode-markdown/issues/38)) - **Fix**: Unexpected deletion of list marker when deleting leading spaces of a list item ([#39](https://github.com/yzhang-gh/vscode-markdown/issues/39)) ### Patches - **v0.10.2**: Fix `toc == null` - **v0.10.1**: Update readme ### 0.10.0 (2017.09.24) - **New**: Outline view ([#36](https://github.com/yzhang-gh/vscode-markdown/issues/36)) - **New**: Toggle strikethrough `~~` with the keybinding you like `markdown.extension.editing.toggleStrikethrough` ([#35](https://github.com/yzhang-gh/vscode-markdown/issues/35)) - **Fix**: Update TOC on save ### 0.9.0 (2017.09.11) - **New**: Multi-cursor support ([#33](https://github.com/yzhang-gh/vscode-markdown/issues/33)) - **Fix**: Support setext heading syntax on TOC generation ([#30](https://github.com/yzhang-gh/vscode-markdown/issues/30)) - **Fix**: Remove backticks in generated TOC link ([#29](https://github.com/yzhang-gh/vscode-markdown/issues/29)) ### 0.8.3 (2017.08.17) - **Fix**: Respect indentation rules ([#9](https://github.com/yzhang-gh/vscode-markdown/issues/9)) - **Fix**: Handle escaped pipe when formatting GFM table ([#28](https://github.com/yzhang-gh/vscode-markdown/issues/28)) ### 0.8.2 (2017.08.07) - **Fix**: Handle Chinese characters when formatting table ([#26](https://github.com/yzhang-gh/vscode-markdown/issues/26)) - **Fix**: Use the same slugify function with vscode when creating table of contents ([#27](https://github.com/yzhang-gh/vscode-markdown/issues/27)) ### 0.8.1 (2017.07.30) - **New**: Support more than 9 list items and some improvements. Thank you [@rbolsius](https://github.com/rbolsius) - **Fix**: Wrong formatting when table contains `|` ([#24](https://github.com/yzhang-gh/vscode-markdown/issues/24)) ### 0.8.0 (2017.07.26) - **New**: New setting `markdown.extension.quickStyling`. Quick styling (toggle bold/italic without selecting words) (default `false`) - **New**: New setting `markdown.extension.italic.indicator` (`*` or `_`) - **New**: New setting `markdown.extension.toc.levels` controlling the range of TOC levels (syntax `x..y`, default `1..6`) - **Other**: Add unit tests and continuous integration (Appveyor) ### 0.7.6/7 (2017.07.18/20) - **Fix**: Fix again (activation events). Finally go back to the legacy activation events (not fancy but robust). ### 0.7.5 (2017.07.15) - **Fix**: Cannot activate extension when no folder is opened ([#14](https://github.com/yzhang-gh/vscode-markdown/issues/14)) ### 0.7.4 (2017.07.14) - **Fix**: Fix activation events ([#12](https://github.com/yzhang-gh/vscode-markdown/issues/12)) ### 0.7.3 (2017.07.11) - **Fix**: Chinese TOC ([#11](https://github.com/yzhang-gh/vscode-markdown/issues/11)) ### 0.7.2 (2017.06.30) - **Fix**: Adopt normal Enter, Tab and Backspace behaviors in fenced code blocks ([#8](https://github.com/yzhang-gh/vscode-markdown/issues/8)) - **Fix**: Unexpected list continuing ### 0.7.1 (2017.06.24) - **Fix**: Better TOC detection rules ([#7](https://github.com/yzhang-gh/vscode-markdown/issues/7)) ### 0.7.0 (2017.06.10) - **New**: GFM table formatter - **New**: Add shortcuts for code spans (Ctrl + `) - **New**: Remove empty list item when pressing Enter ### 0.6.2 (2017.06.07) - **Other**: Add marketplace badges; Improve documentation ### 0.6.1 (2017.05.23) - **Fix**: Ctrl + Enter won't break current line now - **Other**: Move word completion feature to a standalone extension [Dictionary Completion](https://marketplace.visualstudio.com/items?itemName=yzhang.dictionary-completion) ### 0.6.0 (2017.05.15) - **New**: Edit lists with Enter, Tab and Backspace ### 0.5.2 (2017.04.17) - Rollback ### 0.5.1 (2017.04.16) - ~~**New**: Automatic close Markdown preview when change editor~~ ### 0.5.0 (2017.04.13) - **New**: New shortcut behavior to let cursor jump out of **bold** or *italic* block Thanks, [Zach Kirkland (@zkirkland)](https://github.com/zkirkland) ### 0.4.4 (2017.03.27) - **New**: Suggest capitalized words - **Other**: More words ### 0.4.3 - **Fix**: Word completion, handle `,`, `.`, ... ### 0.4.2 - **Other**: Word completion, more words, more accurate ### 0.4.1 - **Fix**: Typo ### 0.4.0 (2017.02.23) - **New**: Word completion for frequently used words - **New**: Continue quote block `>` ### 0.3.0 (2017.02.08) - ~~**New**: Print your Markdown to PDF~~ (Need more tests for the installation of required library) - **New**: At the end of a list item, pressing Enter will automatically insert the new list item bullet - Blank list item won't be continued - (Planed: Pressing Tab on the blank list item will indent it) (Help wanted) - **Fix**: LF and CRLF in TOC - **Other**: Override `blockComment` (`` to <!-- ,  -->) ### 0.2.0 (2017.01.05) - **New**: Automatically show preview to side when opening a Markdown file - **New**: Option for plain text TOC ### 0.1.0 - **New**: Keyboard shortcuts (toggle bold, italic, heading) - **New**: Table of contents (create, update) - Options (depth, orderedList, updateOnSave) ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2017 张宇 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Markdown Support for Visual Studio Code [![version](https://img.shields.io/vscode-marketplace/v/yzhang.markdown-all-in-one.svg?style=flat-square&label=vscode%20marketplace)](https://marketplace.visualstudio.com/items?itemName=yzhang.markdown-all-in-one) [![installs](https://img.shields.io/vscode-marketplace/d/yzhang.markdown-all-in-one.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=yzhang.markdown-all-in-one) [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/yzhang-gh/vscode-markdown/main.yml?style=flat-square&branch=master)](https://github.com/yzhang-gh/vscode-markdown/actions) [![GitHub stars](https://img.shields.io/github/stars/yzhang-gh/vscode-markdown.svg?style=flat-square&label=github%20stars)](https://github.com/yzhang-gh/vscode-markdown) [![GitHub Contributors](https://img.shields.io/github/contributors/yzhang-gh/vscode-markdown.svg?style=flat-square)](https://github.com/yzhang-gh/vscode-markdown/graphs/contributors) All you need for Markdown (keyboard shortcuts, table of contents, auto preview and more). ***Note***: VS Code has basic Markdown support out-of-the-box (e.g, **Markdown preview**), please see the [official documentation](https://code.visualstudio.com/docs/languages/markdown) for more information. **Table of Contents** - [Features](#features) - [Keyboard shortcuts](#keyboard-shortcuts) - [Table of contents](#table-of-contents) - [List editing](#list-editing) - [Print Markdown to HTML](#print-markdown-to-html) - [GitHub Flavored Markdown](#github-flavored-markdown) - [Math](#math) - [Auto completions](#auto-completions) - [Others](#others) - [Available Commands](#available-commands) - [Keyboard Shortcuts](#keyboard-shortcuts-1) - [Supported Settings](#supported-settings) - [FAQ](#faq) - [Q: Error "command 'markdown.extension.onXXXKey' not found"](#q-error-command-markdownextensiononxxxkey-not-found) - [Q: Which Markdown syntax is supported?](#q-which-markdown-syntax-is-supported) - [Q: This extension has overridden some of my key bindings (e.g. Ctrl + B, Alt + C)](#q-this-extension-has-overridden-some-of-my-key-bindings-eg-ctrl--b-alt--c) - [Q: The extension is unresponsive, causing lag etc. (performance issues)](#q-the-extension-is-unresponsive-causing-lag-etc-performance-issues) - [Changelog](#changelog) - [Latest Development Build](#latest-development-build) - [Contributing](#contributing) - [Related](#related) ## Features ### Keyboard shortcuts

toggle bold gif
(Typo: multiple words)

check task list

See full key binding list in the [keyboard shortcuts](#keyboard-shortcuts-1) section ### Table of contents

toc

- Run command "**Create Table of Contents**" (in the [VS Code Command Palette](https://code.visualstudio.com/docs/getstarted/userinterface#_command-palette)) to insert a new table of contents. - The TOC is **automatically updated** on file save by default. To disable, please change the `toc.updateOnSave` option. - The **indentation type (tab or spaces)** of TOC can be configured per file. Find the setting in the right bottom corner of VS Code's status bar. ***Note***: Be sure to also check the `list.indentationSize` option. - To make TOC **compatible with GitHub or GitLab**, set option `slugifyMode` accordingly - Three ways to **control which headings are present** in the TOC:
Click to expand 1. Add `` at the end of a heading to ignore it in TOC\ (It can also be placed above a heading) 2. Use `toc.levels` setting. 3. You can also use the `toc.omittedFromToc` setting to omit some headings (and their subheadings) from TOC: ```js // In your settings.json "markdown.extension.toc.omittedFromToc": { // Use a path relative to your workspace. "README.md": [ "# Introduction", "## Also omitted", ], // Or an absolute path for standalone files. "/home/foo/Documents/todo-list.md": [ "## Shame list (I'll never do these)", ] } ``` ***Note***: - Setext headings (underlined with `===` or `---`) can also be omitted, just put their `# ` and `## ` versions in the setting, respectively. - When omitting heading, **make sure headings within a document are unique**. Duplicate headings may lead to unpredictable behavior.
- Easily add/update/remove **section numbering** section numbers - *In case you are seeing **unexpected TOC recognition**, you can add a `` comment above the list*. ### List editing

on enter key

on tab/backspace key

fix ordered list markers

***Note***: By default, this extension tries to determine indentation size for different lists according to [CommonMark Spec](https://spec.commonmark.org/0.29/#list-items). If you prefer to use a fixed tab size, please change the `list.indentationSize` setting. ### Print Markdown to HTML - Commands `Markdown: Print current document to HTML` and `Markdown: Print documents to HTML` (batch mode) - **Compatible** with other installed Markdown plugins (e.g. [Markdown Footnotes](https://marketplace.visualstudio.com/items?itemName=bierner.markdown-footnotes)). The exported HTML should look the same as inside VS Code (except for a few theme colors due to the limitations of APIs). - Use comment `` (in the first line) to specify a title of the exported HTML. - Plain links to `.md` files will be converted to `.html`. - It's recommended to print the exported HTML to PDF with browser (e.g. Chrome) if you want to share your documents with others. ### GitHub Flavored Markdown - Table formatter

table formatter

***Note***: The key binding is Ctrl + Shift + I on Linux. See [Visual Studio Code Key Bindings](https://code.visualstudio.com/docs/getstarted/keybindings#_keyboard-shortcuts-reference). - Task lists ### Math

math

Please use [Markdown+Math](https://marketplace.visualstudio.com/items?itemName=goessner.mdmath) for dedicated math support. Be sure to disable `math.enabled` option of this extension. ### Auto completions Tip: also support the option `completion.root` - Images/Files (respects option `search.exclude`)

image completions

- Math functions (including option `katex.macros`)

math completions

- Reference links

reference links

### Others - Paste link on selected text

paste link

- Add "Close Preview" keybinding, which allows you to close the preview tab using the same keybinding of "Open Preview" (Ctrl + Shift + V or Ctrl + K V). ## Available Commands - Markdown All in One: Create Table of Contents - Markdown All in One: Update Table of Contents - Markdown All in One: Add/Update section numbers - Markdown All in One: Remove section numbers - Markdown All in One: Toggle code span - Markdown All in One: Toggle code block - Markdown All in One: Print current document to HTML - Markdown All in One: Print documents to HTML - Markdown All in One: Toggle math environment - Markdown All in One: Toggle list - It will cycle through list markers (by default `-`, `*`, `+`, `1.` and `1)`, which can be changed with option `list.toggle.candidate-markers`). ## Keyboard Shortcuts
Table | Key | Command | | ---------------------------------------------------------------- | -------------------------------- | | Ctrl/Cmd + B | Toggle bold | | Ctrl/Cmd + I | Toggle italic | | Alt+S (on Windows) | Toggle strikethrough1 | | Ctrl + Shift + ] | Toggle heading (uplevel) | | Ctrl + Shift + [ | Toggle heading (downlevel) | | Ctrl/Cmd + M | Toggle math environment | | Alt + C | Check/Uncheck task list item | | Ctrl/Cmd + Shift + V | Toggle preview | | Ctrl/Cmd + K V | Toggle preview to side | 1. If the cursor is on a list/task item without selection, strikethrough will be added to the whole item (line)
## Supported Settings
Table | Name | Default | Description | | ---------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------ | | `markdown.extension.completion.respectVscodeSearchExclude` | `true` | Whether to consider `search.exclude` option when providing file path completions | | `markdown.extension.completion.root` | | Root folder when providing file path completions (It takes effect when the path starts with `/`) | | `markdown.extension.italic.indicator` | `*` | Use `*` or `_` to wrap italic text | | `markdown.extension.bold.indicator` | `**` | Use `**` or `__` to wrap bold text | | `markdown.extension.katex.macros` | `{}` | KaTeX macros e.g. `{ "\\name": "expansion", ... }` | | `markdown.extension.list.indentationSize` | `adaptive` | Use different indentation size for ordered and unordered list | | `markdown.extension.list.toggle.candidate-markers` | `[ "-", "*", "+", "1.", "1)" ]` | Use a array for toggle ordered list marker e.g. `["*", "1."]` | | `markdown.extension.orderedList.autoRenumber` | `true` | Auto fix list markers as you edits | | `markdown.extension.orderedList.marker` | `ordered` | Or `one`: always use `1.` as ordered list marker | | `markdown.extension.preview.autoShowPreviewToSide` | `false` | Automatically show preview when opening a Markdown file. | | `markdown.extension.print.absoluteImgPath` | `true` | Convert image path to absolute path | | `markdown.extension.print.imgToBase64` | `false` | Convert images to base64 when printing to HTML | | `markdown.extension.print.includeVscodeStylesheets` | `true` | Whether to include VS Code's default styles | | `markdown.extension.print.onFileSave` | `false` | Print to HTML on file save | | `markdown.extension.print.theme` | `light` | Theme of the exported HTML | | `markdown.extension.print.validateUrls` | `true` | Enable/disable URL validation when printing | | `markdown.extension.syntax.decorations` | `true` | Add decorations to ~~strikethrough~~ and `code span` | | `markdown.extension.syntax.decorationFileSizeLimit` | 50000 | Don't render syntax decorations if a file is larger than this size (in byte/B) | | `markdown.extension.syntax.plainTheme` | `false` | A distraction-free theme | | `markdown.extension.tableFormatter.enabled` | `true` | Enable GFM table formatter | | `markdown.extension.toc.slugifyMode` | `github` | Slugify mode for TOC link generation (`vscode`, `github`, `gitlab` or `gitea`) | | `markdown.extension.toc.omittedFromToc` | `{}` | Lists of headings to omit by project file (e.g. `{ "README.md": ["# Introduction"] }`) | | `markdown.extension.toc.levels` | `1..6` | Control the heading levels to show in the table of contents. | | `markdown.extension.toc.orderedList` | `false` | Use ordered list in the table of contents. | | `markdown.extension.toc.plaintext` | `false` | Just plain text. | | `markdown.extension.toc.unorderedList.marker` | `-` | Use `-`, `*` or `+` in the table of contents (for unordered list) | | `markdown.extension.toc.updateOnSave` | `true` | Automatically update the table of contents on save. |
## FAQ #### Q: Error "command 'markdown.extension.onXXXKey' not found" - In most cases, it is because VS Code **needs a few seconds to load** this extension when you open a Markdown file *for the first time*. (You will see a message "Activating Extensions..." on the status bar.) - If you still see this "command not found" error after waiting for a long time, please try to **restart** VS Code. If needed, **reinstall** this extension: 1. Uninstall this extension. 2. **Close and restart VS Code. (important!)** 3. Reinstall this extension. - If it doesn't help, feel free to open a new issue on [GitHub](https://github.com/yzhang-gh/vscode-markdown/issues/new/choose). It would be better if you can report any suspicious error information to us: It's usually in VS Code's menubar **Help** > **Toggle Developer Tools** > **Console**. - (As a last resort, you may choose to delete `onXXXKey` keys through [VS Code's Keyboard Shortcuts editor](https://code.visualstudio.com/docs/getstarted/keybindings) if you do not need the [list editing feature](https://github.com/yzhang-gh/vscode-markdown#list-editing) at all.) #### Q: Which Markdown syntax is supported? - [CommonMark](https://spec.commonmark.org/) - [Tables](https://help.github.com/articles/organizing-information-with-tables/), [strikethrough](https://help.github.com/articles/basic-writing-and-formatting-syntax/#styling-text) and [task lists](https://docs.github.com/en/github/writing-on-github/basic-writing-and-formatting-syntax#task-lists) (from GitHub Flavored Markdown) - [Math support](https://github.com/waylonflinn/markdown-it-katex#syntax) (from KaTeX) - [Front matter](https://github.com/ParkSB/markdown-it-front-matter#valid-front-matter) For other Markdown syntax, you need to install the corresponding extensions from VS Code marketplace (e.g. [Mermaid diagram](https://marketplace.visualstudio.com/items?itemName=bierner.markdown-mermaid), [emoji](https://marketplace.visualstudio.com/items?itemName=bierner.markdown-emoji), [footnotes](https://marketplace.visualstudio.com/items?itemName=bierner.markdown-footnotes) and [superscript](https://marketplace.visualstudio.com/items?itemName=DevHawk.markdown-sup)). Once installed, they will take effect in VS Code and also the exported HTML file. #### Q: This extension has overridden some of my key bindings (e.g. Ctrl + B, Alt + C) You can easily manage key bindings with [VS Code's **Keyboard Shortcuts** editor](https://code.visualstudio.com/docs/getstarted/keybindings). (Commands provided by this extension have prefix `markdown.extension`.) #### Q: The extension is unresponsive, causing lag etc. (performance issues) From experience, there is *a good chance* that the performance issues are caused by *other extensions* (e.g., some spell checker extensions). This can be verified if you try again with all other extensions disabled (execute `Developer: Reload with Extensions Disabled` or `Extensions: Disable All Installed Extensions for this Workspace` in the VS Code command Palette) and then enable this extension. To find out the root cause, you can install our [development build](#latest-development-build) (`debug.vsix`) and create a CPU profile following this official [instruction](https://github.com/microsoft/vscode/wiki/Performance-Issues#profile-the-running-extensions) from the VS Code. And then please open a GitHub issue with that profile (`.cpuprofile.txt`) attached. ## Changelog See [CHANGELOG](CHANGELOG.md) for more information. ## Latest Development Build Download it [here](https://github.com/yzhang-gh/vscode-markdown/actions/workflows/main.yml?query=event%3Apush+is%3Asuccess), please click the latest passing event to download artifacts. There are two versions: `markdown-all-in-one-*.vsix` is the regular build, while `debug.vsix` is used to create a verbose CPU profile. To install, execute `Extensions: Install from VSIX...` in the VS Code Command Palette (`ctrl + shift + p`) ## Contributing - File bugs, feature requests in [GitHub Issues](https://github.com/yzhang-gh/vscode-markdown/issues). - Leave a review on [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=yzhang.markdown-all-in-one#review-details). - Buy me a coffee ☕ (via [PayPal](https://www.paypal.me/2yzhang), [Alipay or WeChat](donate.md)). Special thanks to the collaborator [@Lemmingh](https://github.com/Lemmingh) and all other [contributors](https://github.com/yzhang-gh/vscode-markdown/graphs/contributors). [![](https://sourcerer.io/fame/yzhang-gh/yzhang-gh/vscode-markdown/images/0)](https://sourcerer.io/fame/yzhang-gh/yzhang-gh/vscode-markdown/links/0)[![](https://sourcerer.io/fame/yzhang-gh/yzhang-gh/vscode-markdown/images/1)](https://sourcerer.io/fame/yzhang-gh/yzhang-gh/vscode-markdown/links/1)[![](https://sourcerer.io/fame/yzhang-gh/yzhang-gh/vscode-markdown/images/2)](https://sourcerer.io/fame/yzhang-gh/yzhang-gh/vscode-markdown/links/2)[![](https://sourcerer.io/fame/yzhang-gh/yzhang-gh/vscode-markdown/images/3)](https://sourcerer.io/fame/yzhang-gh/yzhang-gh/vscode-markdown/links/3)[![](https://sourcerer.io/fame/yzhang-gh/yzhang-gh/vscode-markdown/images/4)](https://sourcerer.io/fame/yzhang-gh/yzhang-gh/vscode-markdown/links/4)[![](https://sourcerer.io/fame/yzhang-gh/yzhang-gh/vscode-markdown/images/5)](https://sourcerer.io/fame/yzhang-gh/yzhang-gh/vscode-markdown/links/5)[![](https://sourcerer.io/fame/yzhang-gh/yzhang-gh/vscode-markdown/images/6)](https://sourcerer.io/fame/yzhang-gh/yzhang-gh/vscode-markdown/links/6)[![](https://sourcerer.io/fame/yzhang-gh/yzhang-gh/vscode-markdown/images/7)](https://sourcerer.io/fame/yzhang-gh/yzhang-gh/vscode-markdown/links/7) --- ## Related [More extensions of mine](https://marketplace.visualstudio.com/publishers/yzhang) ================================================ FILE: build/build.js ================================================ "use strict"; const { createLogger } = require("./logger.js"); const logger = createLogger("Build"); logger.log("Started."); // This is not necessarily the last line of the log, as others may also register on the event. process.on("exit", (code) => logger.log(`${code ? "Failed" : "Passing"}.`)); require("./compilation.js").run(); require("./duplicate-changelog.js").run(); ================================================ FILE: build/compilation.js ================================================ // # Notes // // We adjust the configurations according to OS environment variables. // The `mode` reflects the "NODE_ENV", which is a convention established by Express. // Source map (`devtool`) is expensive and not needed on CI. // // The compilation starts when calling `webpack()` with a callback. // It is async, sequential. The callback is invoked only once. // https://webpack.js.org/api/node/#multicompiler "use strict"; const webpack = require("webpack"); const { createLogger } = require("./logger.js"); const logger = createLogger("Compilation"); /** * @type {boolean} * @see {@link https://docs.github.com/en/actions/reference/environment-variables} * @see {@link https://github.com/actions/runner/blob/main/src/Runner.Sdk/ProcessInvoker.cs} */ const Env_Is_Ci = process.env["CI"] === "true" || process.env["GITHUB_ACTIONS"] === "true"; /** * Only distinguish "development" or not. * @type {"development" | "production"} */ const Env_Mode = process.env["NODE_ENV"] === "development" ? "development" : "production"; /** * @type {webpack.StatsOptions} */ const Stats_Options = { all: false, assets: true, children: true, errors: true, errorsCount: true, outputPath: true, timings: true, version: true, warnings: true, warningsCount: true, }; /** * @param {webpack.StatsError} e */ const formatCompilationError = (e) => ` ERROR @ ${e.moduleName} (${e.loc}) ${e.message} `; /** * @param {webpack.StatsAsset} a */ const formatAssetInfo = (a) => `\ ${a.type} : ${a.name ?? "[no name]"} \ : ${a.size} bytes \ ${a.emitted ? "[emitted]" : a.comparedForEmit ? "[compared for emit]" : ""}\ `; /** * Collects and formats stats summary by pre-order traversal. * The stats tree should be relatively small in real world. * @param {webpack.StatsCompilation} i - The beginning node. */ const formatStatsInfo = (i) => { let r = i.name ? ` STATS @ ${i.name} ${i.assets?.map(formatAssetInfo).join("\n") ?? "No asset."} Compiled in ${i.time} ms. Errors: ${i.errorsCount}. Warnings: ${i.warningsCount}. ` : ""; if (i.children?.length) { for (const c of i.children) { r += formatStatsInfo(c); } } return r; }; const run = () => { logger.log(`Started. Mode: ${Env_Mode}. CI: ${Env_Is_Ci}.`); const configs = require("../webpack.config.js"); for (const c of configs) { c.mode = Env_Mode; if (Env_Is_Ci) { c.devtool = false; } } webpack(configs, (err, stats) => { // `!stats` is just to satisfy type-checking. if (err || !stats) { throw err; } /** @type {Required} */ // @ts-ignore Too hard to check type. Please debug to inspect it. const info = stats.toJson(Stats_Options); logger.append(`webpack ${info.version}`, true, true); // All errors. Treat warning as error. if (info.errorsCount || info.warningsCount) { logger.append([...info.errors, ...info.warnings].map(formatCompilationError).join("\n")); logger.append(` Errors: ${info.errorsCount}. Warnings: ${info.warningsCount}. `); } // Summary of each configuration. logger.append(formatStatsInfo(info)); logger.flush(); if (info.errorsCount) { throw new Error("{Compilation} Failed."); } logger.log("Passing."); }); }; module.exports = Object.freeze({ run }); ================================================ FILE: build/duplicate-changelog.js ================================================ // # Notes // // vsce will modify `README.md` and `CHANGELOG.md` during packaging. // Thus, we create the `changes.md` for ours to consume. // Due to relative paths in the file, it has to be under the project root. "use strict"; const fs = require("fs"); const path = require("path"); const { createLogger } = require("./logger.js"); const logger = createLogger("Duplicate Changelog"); const run = () => { logger.log("Started."); const projectRoot = path.resolve(__dirname, ".."); const src = path.resolve(projectRoot, "CHANGELOG.md"); const dest = path.resolve(projectRoot, "changes.md"); logger.log(`\nFrom: ${src}\nTo: ${dest}`); fs.copyFileSync(src, dest); logger.log("Passing."); }; module.exports = Object.freeze({ run }); ================================================ FILE: build/logger.js ================================================ "use strict"; class Logger { /** * The logger name. Read-only. * @type {string} */ #name; /** * The output buffer. * @type {string[]} */ #buffer; /** * @param {string} name - Human-readable name of the logger. */ constructor(name) { this.#name = name; this.#buffer = []; } get name() { return this.#name; } /** * @param {string} message * @param {boolean} withName * @param {boolean} withTime */ #format(message, withName, withTime) { // https://2ality.com/2011/10/string-concatenation.html let result = ""; if (withTime) { result += `[${new Date().toISOString()}] `; } if (withName) { result += `{${this.#name}} `; } result += message; return result; } /** * Adds a message to the log, which will show on a manual flush. * A Line Feed will be appended automatically. * @param {string} message * @param {boolean} withName - `true` to prepend the logger name. * @param {boolean} withTime - `true` to prepend timestamp. */ append(message, withName = false, withTime = false) { this.#buffer.push(this.#format(message, withName, withTime)); } /** * Flushes the output buffer. */ flush() { if (this.#buffer.length) { console.log(this.#buffer.join("\n")); } this.#buffer.length = 0; } /** * Writes a message to the console immediately, always with timestamp and the logger name. * @param {string} message */ log(message) { console.log(this.#format(message, true, true)); } } /** * @param {string} label - The logger name representing this output channel. */ const createLogger = (label) => { const logger = new Logger(label); process.on("exit", () => logger.flush()); return logger; }; // Sort in alphabetical order. module.exports = Object.freeze({ createLogger, }); ================================================ FILE: build/tsconfig.json ================================================ { "extends": "../tsconfig.base.json", "compilerOptions": { "module": "CommonJS", "target": "ES2020", "lib": ["ES2020"], "allowJs": true, "checkJs": true, "noEmit": true } } ================================================ FILE: donate.md ================================================ # Buy Me a Coffee ☕ Thank you! | Alipay | WeChat Pay | | :------------------------------: | :------------------------------------: | | ![alipay](images/pay/Alipay.png) | ![wechatpay](images/pay/WeChatPay.png) | ================================================ FILE: media/checkbox.css ================================================ .task-list-item { list-style-type: none; } .task-list-item-checkbox { margin-left: -20px; vertical-align: middle; pointer-events: none; } ================================================ FILE: package.json ================================================ { "name": "markdown-all-in-one", "displayName": "%ext.displayName%", "description": "%ext.description%", "icon": "images/Markdown-mark.png", "version": "3.6.3", "publisher": "yzhang", "engines": { "vscode": "^1.77.0" }, "categories": [ "Programming Languages", "Formatters", "Other" ], "keywords": [ "markdown" ], "bugs": { "url": "https://github.com/yzhang-gh/vscode-markdown/issues" }, "repository": { "type": "git", "url": "https://github.com/yzhang-gh/vscode-markdown" }, "license": "MIT", "activationEvents": [ "onLanguage:markdown", "onLanguage:rmd", "onLanguage:quarto", "workspaceContains:README.md" ], "main": "./dist/node/main.js", "contributes": { "colors": [ { "id": "markdown.extension.editor.codeSpan.background", "description": "Background color of code spans in the Markdown editor.", "defaults": { "dark": "#00000000", "light": "#00000000", "highContrast": "#00000000" } }, { "id": "markdown.extension.editor.codeSpan.border", "description": "Border color of code spans in the Markdown editor.", "defaults": { "dark": "editor.selectionBackground", "light": "editor.selectionBackground", "highContrast": "editor.selectionBackground" } }, { "id": "markdown.extension.editor.formattingMark.foreground", "description": "Color of formatting marks (paragraphs, hard line breaks, links, etc.) in the Markdown editor.", "defaults": { "dark": "editorWhitespace.foreground", "light": "editorWhitespace.foreground", "highContrast": "diffEditor.insertedTextBorder" } }, { "id": "markdown.extension.editor.trailingSpace.background", "description": "Background color of trailing space (U+0020) characters in the Markdown editor.", "defaults": { "dark": "diffEditor.diagonalFill", "light": "diffEditor.diagonalFill", "highContrast": "editorWhitespace.foreground" } } ], "commands": [ { "command": "markdown.extension.toc.create", "enablement": "editorLangId =~ /^markdown$|^rmd$|^quarto$/", "title": "%command.toc.create.title%", "category": "Markdown All in One" }, { "command": "markdown.extension.toc.update", "enablement": "editorLangId =~ /^markdown$|^rmd$|^quarto$/", "title": "%command.toc.update.title%", "category": "Markdown All in One" }, { "command": "markdown.extension.toc.addSecNumbers", "enablement": "editorLangId =~ /^markdown$|^rmd$|^quarto$/", "title": "%command.toc.addSecNumbers.title%", "category": "Markdown All in One" }, { "command": "markdown.extension.toc.removeSecNumbers", "enablement": "editorLangId =~ /^markdown$|^rmd$|^quarto$/", "title": "%command.toc.removeSecNumbers.title%", "category": "Markdown All in One" }, { "command": "markdown.extension.printToHtml", "enablement": "editorLangId =~ /^markdown$|^rmd$|^quarto$/", "title": "%command.printToHtml.title%", "category": "Markdown All in One" }, { "command": "markdown.extension.printToHtmlBatch", "enablement": "workspaceFolderCount >= 1", "title": "%command.printToHtmlBatch.title%", "category": "Markdown All in One" }, { "command": "markdown.extension.editing.toggleCodeSpan", "enablement": "editorLangId =~ /^markdown$|^rmd$|^quarto$/", "title": "%command.editing.toggleCodeSpan.title%", "icon": "$(code)", "category": "Markdown All in One" }, { "command": "markdown.extension.editing.toggleMath", "enablement": "editorLangId =~ /^markdown$|^rmd$|^quarto$/", "title": "%command.editing.toggleMath.title%", "category": "Markdown All in One" }, { "command": "markdown.extension.editing.toggleMathReverse", "enablement": "editorLangId =~ /^markdown$|^rmd$|^quarto$/", "title": "%command.editing.toggleMathReverse.title%", "category": "Markdown All in One" }, { "command": "markdown.extension.editing.toggleList", "enablement": "editorLangId =~ /^markdown$|^rmd$|^quarto$/", "title": "%command.editing.toggleList.title%", "icon": "$(list-unordered)", "category": "Markdown All in One" }, { "command": "markdown.extension.editing.toggleCodeBlock", "enablement": "editorLangId =~ /^markdown$|^rmd$|^quarto$/", "title": "%command.editing.toggleCodeBlock.title%", "category": "Markdown All in One" }, { "command": "markdown.extension.editing.toggleBold", "enablement": "editorLangId =~ /^markdown$|^rmd$|^quarto$/", "title": "%command.editing.toggleBold%", "icon": "$(bold)", "category": "Markdown All in One" }, { "command": "markdown.extension.editing.toggleItalic", "enablement": "editorLangId =~ /^markdown$|^rmd$|^quarto$/", "title": "%command.editing.toggleItalic%", "icon": "$(italic)", "category": "Markdown All in One" }, { "command": "markdown.extension.editing.toggleStrikethrough", "enablement": "editorLangId =~ /^markdown$|^rmd$|^quarto$/", "title": "%command.editing.toggleStrikethrough%", "category": "Markdown All in One" }, { "command": "markdown.extension.checkTaskList", "enablement": "editorLangId =~ /^markdown$|^rmd$|^quarto$/", "title": "%command.checkTaskList%", "icon": "$(tasklist)", "category": "Markdown All in One" } ], "menus": { "editor/context": [ { "command": "markdown.extension.printToHtml", "when": "editorLangId =~ /^markdown$|^rmd$|^quarto$/", "group": "markdown.print@1" }, { "command": "markdown.extension.printToHtmlBatch", "when": "editorLangId =~ /^markdown$|^rmd$|^quarto$/ && workspaceFolderCount >= 1", "group": "markdown.print@2" } ], "editor/title": [ { "when": "editorLangId =~ /^markdown$|^rmd$|^quarto$/ && config.markdown.extension.showActionButtons", "command": "markdown.extension.editing.toggleBold", "group": "navigation@1" }, { "when": "editorLangId =~ /^markdown$|^rmd$|^quarto$/ && config.markdown.extension.showActionButtons", "command": "markdown.extension.editing.toggleItalic", "group": "navigation@2" }, { "when": "editorLangId =~ /^markdown$|^rmd$|^quarto$/ && config.markdown.extension.showActionButtons", "command": "markdown.extension.editing.toggleCodeSpan", "group": "navigation@3" }, { "when": "editorLangId =~ /^markdown$|^rmd$|^quarto$/ && config.markdown.extension.showActionButtons", "command": "markdown.extension.editing.toggleList", "group": "navigation@4" }, { "when": "editorLangId =~ /^markdown$|^rmd$|^quarto$/ && config.markdown.extension.showActionButtons", "command": "markdown.extension.checkTaskList", "group": "navigation@5" } ] }, "keybindings": [ { "command": "markdown.extension.editing.toggleBold", "key": "ctrl+b", "mac": "cmd+b", "when": "editorTextFocus && !editorReadonly && editorLangId =~ /^markdown$|^rmd$|^quarto$/" }, { "command": "markdown.extension.editing.toggleItalic", "key": "ctrl+i", "mac": "cmd+i", "when": "editorTextFocus && !editorReadonly && editorLangId =~ /^markdown$|^rmd$|^quarto$/" }, { "command": "markdown.extension.editing.toggleStrikethrough", "key": "alt+s", "when": "editorTextFocus && !editorReadonly && editorLangId =~ /^markdown$|^rmd$|^quarto$/ && !isMac" }, { "command": "markdown.extension.editing.toggleHeadingUp", "key": "ctrl+shift+]", "mac": "ctrl+shift+]", "when": "editorTextFocus && !editorReadonly && editorLangId =~ /^markdown$|^rmd$|^quarto$/" }, { "command": "markdown.extension.editing.toggleHeadingDown", "key": "ctrl+shift+[", "mac": "ctrl+shift+[", "when": "editorTextFocus && !editorReadonly && editorLangId =~ /^markdown$|^rmd$|^quarto$/" }, { "command": "markdown.extension.editing.toggleMath", "key": "ctrl+m", "mac": "cmd+m", "when": "editorTextFocus && !editorReadonly && editorLangId =~ /^markdown$|^rmd$|^quarto$/" }, { "command": "markdown.extension.onEnterKey", "key": "enter", "when": "editorTextFocus && !editorReadonly && editorLangId =~ /^markdown$|^rmd$|^quarto$/ && (!suggestWidgetVisible || config.editor.acceptSuggestionOnEnter == 'off') && !editorHasMultipleSelections && vim.mode != 'Normal' && vim.mode != 'Visual' && vim.mode != 'VisualBlock' && vim.mode != 'VisualLine' && vim.mode != 'SearchInProgressMode' && vim.mode != 'CommandlineInProgress' && vim.mode != 'Replace' && vim.mode != 'EasyMotionMode' && vim.mode != 'EasyMotionInputMode' && vim.mode != 'SurroundInputMode' && !markdown.extension.editor.cursor.inFencedCodeBlock && !markdown.extension.editor.cursor.inMathEnv" }, { "command": "markdown.extension.onCtrlEnterKey", "key": "ctrl+enter", "mac": "cmd+enter", "when": "editorTextFocus && !editorReadonly && editorLangId =~ /^markdown$|^rmd$|^quarto$/ && (!suggestWidgetVisible || config.editor.acceptSuggestionOnEnter == 'off') && !editorHasMultipleSelections && !markdown.extension.editor.cursor.inFencedCodeBlock && !markdown.extension.editor.cursor.inMathEnv" }, { "command": "markdown.extension.onShiftEnterKey", "key": "shift+enter", "when": "editorTextFocus && !editorReadonly && editorLangId =~ /^markdown$|^rmd$|^quarto$/ && (!suggestWidgetVisible || config.editor.acceptSuggestionOnEnter == 'off') && !editorHasMultipleSelections && !markdown.extension.editor.cursor.inFencedCodeBlock && !markdown.extension.editor.cursor.inMathEnv" }, { "command": "markdown.extension.onTabKey", "key": "tab", "when": "editorTextFocus && !editorReadonly && editorLangId =~ /^markdown$|^rmd$|^quarto$/ && !suggestWidgetVisible && !inlineSuggestionVisible && !editorHasMultipleSelections && !editorTabMovesFocus && !inSnippetMode && !hasSnippetCompletions && !hasOtherSuggestions && markdown.extension.editor.cursor.inList && !markdown.extension.editor.cursor.inFencedCodeBlock && !markdown.extension.editor.cursor.inMathEnv && !tabShouldJumpToInlineEdit && !tabShouldAcceptInlineEdit" }, { "command": "markdown.extension.onShiftTabKey", "key": "shift+tab", "when": "editorTextFocus && !editorReadonly && editorLangId =~ /^markdown$|^rmd$|^quarto$/ && !suggestWidgetVisible && !editorHasMultipleSelections && !editorTabMovesFocus && !inSnippetMode && !hasSnippetCompletions && !hasOtherSuggestions && markdown.extension.editor.cursor.inList && !markdown.extension.editor.cursor.inFencedCodeBlock && !markdown.extension.editor.cursor.inMathEnv" }, { "command": "markdown.extension.onBackspaceKey", "key": "backspace", "when": "editorTextFocus && !editorReadonly && editorLangId =~ /^markdown$|^rmd$|^quarto$/ && !suggestWidgetVisible && !editorHasMultipleSelections && vim.mode != 'Normal' && vim.mode != 'Visual' && vim.mode != 'VisualBlock' && vim.mode != 'VisualLine' && vim.mode != 'SearchInProgressMode' && vim.mode != 'CommandlineInProgress' && vim.mode != 'Replace' && vim.mode != 'EasyMotionMode' && vim.mode != 'EasyMotionInputMode' && vim.mode != 'SurroundInputMode' && !markdown.extension.editor.cursor.inFencedCodeBlock && !markdown.extension.editor.cursor.inMathEnv" }, { "command": "markdown.extension.onMoveLineUp", "key": "alt+up", "when": "editorTextFocus && !editorReadonly && editorLangId =~ /^markdown$|^rmd$|^quarto$/ && !suggestWidgetVisible" }, { "command": "markdown.extension.onMoveLineDown", "key": "alt+down", "when": "editorTextFocus && !editorReadonly && editorLangId =~ /^markdown$|^rmd$|^quarto$/ && !suggestWidgetVisible" }, { "command": "markdown.extension.onCopyLineUp", "win": "shift+alt+up", "mac": "shift+alt+up", "linux": "ctrl+shift+alt+up", "when": "editorTextFocus && !editorReadonly && editorLangId =~ /^markdown$|^rmd$|^quarto$/ && !suggestWidgetVisible" }, { "command": "markdown.extension.onCopyLineDown", "win": "shift+alt+down", "mac": "shift+alt+down", "linux": "ctrl+shift+alt+down", "when": "editorTextFocus && !editorReadonly && editorLangId =~ /^markdown$|^rmd$|^quarto$/ && !suggestWidgetVisible" }, { "command": "markdown.extension.onIndentLines", "key": "ctrl+]", "mac": "cmd+]", "when": "editorTextFocus && editorLangId =~ /^markdown$|^rmd$|^quarto$/ && !suggestWidgetVisible" }, { "command": "markdown.extension.onOutdentLines", "key": "ctrl+[", "mac": "cmd+[", "when": "editorTextFocus && editorLangId =~ /^markdown$|^rmd$|^quarto$/ && !suggestWidgetVisible" }, { "command": "markdown.extension.checkTaskList", "key": "alt+c", "when": "editorTextFocus && editorLangId =~ /^markdown$|^rmd$|^quarto$/ && !isMac" }, { "command": "markdown.extension.closePreview", "key": "ctrl+shift+v", "mac": "cmd+shift+v", "when": "activeWebviewPanelId == 'markdown.preview'" }, { "command": "markdown.extension.closePreview", "key": "ctrl+k v", "mac": "cmd+k v", "when": "activeWebviewPanelId == 'markdown.preview'" }, { "command": "markdown.extension.editing.paste", "key": "ctrl+v", "mac": "cmd+v", "when": "editorTextFocus && editorLangId =~ /^markdown$|^rmd$|^quarto$/ && editorHasSelection" } ], "configuration": { "type": "object", "title": "%config.title%", "properties": { "markdown.extension.completion.enabled": { "type": "boolean", "default": false, "description": "%config.completion.enabled%", "scope": "resource" }, "markdown.extension.completion.respectVscodeSearchExclude": { "type": "boolean", "default": true, "markdownDescription": "%config.completion.respectVscodeSearchExclude%", "scope": "resource" }, "markdown.extension.completion.root": { "type": "string", "default": "", "description": "%config.completion.root%", "scope": "resource" }, "markdown.extension.italic.indicator": { "type": "string", "default": "*", "markdownDescription": "%config.italic.indicator.description%", "enum": [ "*", "_" ] }, "markdown.extension.bold.indicator": { "type": "string", "default": "**", "markdownDescription": "%config.bold.indicator.description%", "enum": [ "**", "__" ] }, "markdown.extension.katex.macros": { "type": "object", "default": {}, "description": "%config.katex.macros.description%" }, "markdown.extension.list.indentationSize": { "type": "string", "enum": [ "adaptive", "inherit" ], "markdownEnumDescriptions": [ "%config.list.indentationSize.enumDescriptions.adaptive%", "%config.list.indentationSize.enumDescriptions.inherit%" ], "default": "adaptive", "markdownDescription": "%config.list.indentationSize.description%", "scope": "resource" }, "markdown.extension.list.toggle.candidate-markers": { "type": "array", "default": [ "-", "*", "+", "1.", "1)" ], "items": { "enum": [ "-", "*", "+", "1.", "1)" ] }, "minItems": 1, "maxItems": 5, "uniqueItems": true, "description": "%config.list.toggle.candidate-markers.description%" }, "markdown.extension.math.enabled": { "type": "boolean", "default": true, "description": "%config.math.enabled%" }, "markdown.extension.orderedList.autoRenumber": { "type": "boolean", "default": true, "description": "%config.orderedList.autoRenumber.description%" }, "markdown.extension.orderedList.marker": { "type": "string", "default": "ordered", "description": "%config.orderedList.marker.description%", "enum": [ "one", "ordered" ], "markdownEnumDescriptions": [ "%config.orderedList.marker.enumDescriptions.one%", "%config.orderedList.marker.enumDescriptions.ordered%" ] }, "markdown.extension.preview.autoShowPreviewToSide": { "type": "boolean", "default": false, "description": "%config.preview.autoShowPreviewToSide.description%" }, "markdown.extension.print.absoluteImgPath": { "type": "boolean", "default": true, "description": "%config.print.absoluteImgPath.description%", "scope": "resource" }, "markdown.extension.print.imgToBase64": { "type": "boolean", "default": false, "description": "%config.print.imgToBase64.description%", "scope": "resource" }, "markdown.extension.print.includeVscodeStylesheets": { "type": "boolean", "default": true, "description": "%config.print.includeVscodeStylesheets%" }, "markdown.extension.print.onFileSave": { "type": "boolean", "default": false, "description": "%config.print.onFileSave.description%", "scope": "resource" }, "markdown.extension.print.pureHtml": { "type": "boolean", "default": false, "description": "%config.print.pureHtml.description%", "scope": "resource" }, "markdown.extension.print.theme": { "type": "string", "default": "light", "enum": [ "light", "dark" ], "description": "%config.print.theme%", "scope": "resource" }, "markdown.extension.print.validateUrls": { "type": "boolean", "default": true, "description": "%config.print.validateUrls.description%" }, "markdown.extension.showActionButtons": { "type": "boolean", "default": false, "description": "%config.showActionButtons.description%" }, "markdown.extension.syntax.decorations": { "type": "boolean", "default": null, "markdownDeprecationMessage": "%config.syntax.decorations.description%" }, "markdown.extension.syntax.decorationFileSizeLimit": { "type": "number", "default": 50000, "description": "%config.syntax.decorationFileSizeLimit.description%" }, "markdown.extension.syntax.plainTheme": { "type": "boolean", "default": false, "markdownDescription": "%config.syntax.plainTheme.description%" }, "markdown.extension.tableFormatter.enabled": { "type": "boolean", "default": true, "markdownDescription": "%config.tableFormatter.enabled.description%" }, "markdown.extension.tableFormatter.normalizeIndentation": { "type": "boolean", "default": false, "markdownDescription": "%config.tableFormatter.normalizeIndentation.description%" }, "markdown.extension.tableFormatter.delimiterRowNoPadding": { "type": "boolean", "default": false, "markdownDescription": "%config.tableFormatter.delimiterRowNoPadding.description%" }, "markdown.extension.theming.decoration.renderCodeSpan": { "type": "boolean", "default": true, "markdownDescription": "%config.theming.decoration.renderCodeSpan.description%", "scope": "application" }, "markdown.extension.theming.decoration.renderHardLineBreak": { "type": "boolean", "default": false, "markdownDescription": "%config.theming.decoration.renderHardLineBreak.description%", "scope": "application" }, "markdown.extension.theming.decoration.renderLink": { "type": "boolean", "default": false, "markdownDescription": "%config.theming.decoration.renderLink.description%", "scope": "application" }, "markdown.extension.theming.decoration.renderParagraph": { "type": "boolean", "default": false, "markdownDescription": "%config.theming.decoration.renderParagraph.description%", "scope": "application" }, "markdown.extension.theming.decoration.renderStrikethrough": { "type": "boolean", "default": true, "markdownDescription": "%config.theming.decoration.renderStrikethrough.description%", "scope": "application" }, "markdown.extension.theming.decoration.renderTrailingSpace": { "type": "boolean", "default": false, "markdownDescription": "%config.theming.decoration.renderTrailingSpace.description%", "scope": "application" }, "markdown.extension.toc.levels": { "type": "string", "default": "1..6", "markdownDescription": "%config.toc.levels.description%", "pattern": "^[1-6]\\.\\.[1-6]$" }, "markdown.extension.toc.omittedFromToc": { "type": "object", "default": {}, "description": "%config.toc.omittedFromToc.description%" }, "markdown.extension.toc.orderedList": { "type": "boolean", "default": false, "description": "%config.toc.orderedList.description%" }, "markdown.extension.toc.plaintext": { "type": "boolean", "default": false, "description": "%config.toc.plaintext.description%" }, "markdown.extension.toc.slugifyMode": { "type": "string", "default": "github", "markdownDescription": "%config.toc.slugifyMode.description%", "enum": [ "github", "azureDevops", "bitbucket-cloud", "gitea", "gitlab", "vscode", "zola" ], "enumDescriptions": [ "GitHub", "Azure DevOps", "Bitbucket Cloud", "Gitea", "GitLab", "Visual Studio Code", "Zola" ] }, "markdown.extension.toc.unorderedList.marker": { "type": "string", "default": "-", "markdownDescription": "%config.toc.unorderedList.marker.description%", "enum": [ "-", "*", "+" ] }, "markdown.extension.toc.updateOnSave": { "type": "boolean", "default": true, "description": "%config.toc.updateOnSave.description%" }, "markdown.extension.extraLangIds": { "type": "array", "default": [], "items": { "enum": [ "rmd", "quarto" ] }, "description": "%config.extraLangIds.description%" } } }, "markdown.markdownItPlugins": true, "markdown.previewStyles": [ "./media/checkbox.css", "./node_modules/katex/dist/katex.min.css", "./node_modules/markdown-it-github-alerts/styles/github-colors-light.css", "./node_modules/markdown-it-github-alerts/styles/github-colors-dark-media.css", "./node_modules/markdown-it-github-alerts/styles/github-base.css" ], "grammars": [ { "scopeName": "markdown.math_display", "path": "./syntaxes/math_display.markdown.tmLanguage.json", "injectTo": [ "text.html.markdown" ] }, { "scopeName": "markdown.math_inline", "path": "./syntaxes/math_inline.markdown.tmLanguage.json", "injectTo": [ "text.html.markdown" ] }, { "scopeName": "text.katex", "path": "./syntaxes/katex.tmLanguage.json" } ] }, "capabilities": { "virtualWorkspaces": { "supported": "limited", "description": "In virtual workspaces, some features may not work well." } }, "scripts": { "vscode:prepublish": "npm run build", "build": "npm run build-wasm && node ./build/build.js", "build-wasm": "cd ./src/zola-slug && wasm-pack build --release", "dev-build": "webpack --mode development", "dev-compile": "tsc --build --watch --verbose", "pretest": "tsc --build", "test": "node ./out/test/runTest.js" }, "dependencies": { "@neilsustc/markdown-it-katex": "^1.0.0", "entities": "^3.0.1", "grapheme-splitter": "^1.0.4", "highlight.js": "^11.5.1", "image-size": "^0.9.3", "katex": "^0.16.4", "markdown-it": "^13.0.2", "markdown-it-github-alerts": "^0.1.2", "markdown-it-task-lists": "^2.1.1", "string-similarity": "^4.0.4", "zola-slug": "file:./src/zola-slug/pkg" }, "devDependencies": { "@types/glob": "^7.2.0", "@types/katex": "^0.14.0", "@types/markdown-it": "^13.0.7", "@types/mocha": "^9.1.0", "@types/node": "~14.18.13", "@types/string-similarity": "^4.0.0", "@types/vscode": "~1.63.2", "@vscode/test-electron": "^1.6.2", "@vscode/vsce": "^2.26.1", "glob": "^7.2.0", "mocha": "^9.2.2", "ts-loader": "^9.2.8", "typescript": "~4.5.5", "webpack": "^5.91.0", "webpack-cli": "^4.9.2" } } ================================================ FILE: package.nls.ja.json ================================================ { "ext.displayName": "Markdown All in One", "ext.description": "する事はMarkdownを書くだけでいいのです(キーボードショートカット、目次、自動プレビューなど)", "command.toc.create.title": "目次(TOC)の作成", "command.toc.update.title": "目次(TOC)の更新", "command.toc.addSecNumbers.title": "セクション番号を追加/更新", "command.toc.removeSecNumbers.title": "セクション番号の除去", "command.printToHtml.title": "現在のドキュメントをHTMLで出力", "command.printToHtmlBatch.title": "ドキュメントをHTMLで出力(ソースフォルダを選択)", "command.editing.toggleCodeSpan.title": "インラインコード構文のトグル", "command.editing.toggleMath.title": "数式環境のトグル", "command.editing.toggleMathReverse.title": "数式環境のトグル(逆順)", "command.editing.toggleList.title": "リスト構文のトグル", "command.editing.toggleCodeBlock.title": "コードブロック構文のトグル", "config.title": "Markdown All in One", "config.completion.respectVscodeSearchExclude": "VS Codeの `#search.exclude#` 設定を使用して、オートコンプリートからファイルを除外する(`node_modules` や、`bower_components` や、`*.code-search` は**常に除外**され、このオプションの影響を受けません)", "config.completion.root": "Pathを自動補完する際のルートフォルダ", "config.italic.indicator.description": "斜体テキストの囲いに `*` と `_` のどちらを使用するか", "config.bold.indicator.description": "太字の囲み文字に `**` と `__` のどちらを使用するか", "config.katex.macros.description": "ユーザ定義のKaTeXマクロ", "config.list.indentationSize.description": "リスト構文のインデント形式(TOCの生成にも影響します)\n\nリスト構文のコンテキストによって異なったインデント幅を使用するか、VS Codeにおけるタブのサイズに従うかどうか", "config.list.indentationSize.enumDescriptions.adaptive": "コンテキストに応じたインデント幅を使用します。**サブ項目をその親の項目に左揃えで配置**させます。例:\n\n```markdown\n- 親項目\n - サブ項目\n\n1. 親項目\n 1. サブ項目\n\n10. マーカーが長い親項目\n 1. サブ項目\n```", "config.list.indentationSize.enumDescriptions.inherit": "現在のドキュメントに設定されているタブのサイズを使用します(ステータスバーを参照)。例(`tabSize: 4`):\n\n```markdown\n- 親項目\n - サブ項目\n\n1. 親項目\n 1. サブ項目\n\n10. マーカーが長い親項目\n 1. サブ項目\n```", "config.math.enabled": "基本的な数式のサポートを有効化(KaTeXを使用)", "config.orderedList.autoRenumber.description": "順序付きリストマーカーの自動修正", "config.orderedList.marker.description": "順序付きリストのマーカー", "config.orderedList.marker.enumDescriptions.one": "順序付きリストのマーカーとして、常に `1.` を使用します。", "config.orderedList.marker.enumDescriptions.ordered": "順序付きリストマーカーとして、増加する連番を使用します。", "config.preview.autoShowPreviewToSide.description": "自動でプレビューを横に表示する", "config.print.absoluteImgPath.description": "画像Pathを絶対Pathに変換する", "config.print.imgToBase64.description": "HTML出力時に画像をbase64へ変換する", "config.print.includeVscodeStylesheets": "VS Code の基本的な Markdown スタイルを埋め込み、出力された HTML が VS Code 上での表示と同じ見た目になるようにする", "config.print.onFileSave.description": "ファイル保存時に現在のドキュメントをHTMLへ出力する", "config.print.theme": "出力時のHTMLテーマ。コードブロック構文にのみ影響します。", "config.print.validateUrls.description": "出力時のURL検証の有効化/無効化", "config.syntax.decorations.description": "(**非推奨**)代わりに `#markdown.extension.theming.decoration.renderCodeSpan#` を使用してください。詳細は、 を参照。", "config.syntax.decorationFileSizeLimit.description": "ファイルが指定のサイズよりも大きい場合(バイト / B単位)、シンタックス装飾はレンダリングされません。", "config.syntax.plainTheme.description": "(**実験的な機能**)問題の報告はこちらへ:", "config.tableFormatter.enabled.description": "[GitHub Flavored Markdown](https://github.github.com/gfm/)のテーブルフォーマッターを有効化", "config.tableFormatter.normalizeIndentation.description": "設定されているタブのサイズに最も近い倍数となるよう、テーブルのインデントを正規化する", "config.theming.decoration.renderCodeSpan.description": "[インラインコード](https://spec.commonmark.org/0.29/#code-spans)の周囲に境界線を適用する", "config.theming.decoration.renderHardLineBreak.description": "(**実験的な機能**)", "config.theming.decoration.renderLink.description": "(**実験的な機能**)", "config.theming.decoration.renderParagraph.description": "(**実験的な機能**)", "config.theming.decoration.renderStrikethrough.description": "[取り消し線](https://github.github.com/gfm/#strikethrough-extension-)の中央に線を表示する", "config.theming.decoration.renderTrailingSpace.description": "[行(Line)](https://spec.commonmark.org/0.29/#line)末尾にある空白文字(U+0020)の背景をシェードする", "config.toc.levels.description": "目次(TOC)における階層の範囲。 `x..y` と使用すれば、階層 `x` ~ `y` となります。", "config.toc.omittedFromToc.description": "プロジェクトファイルの目次(TOC)で除外する見出しの一覧。\n例:\n{ \"README.md\": [\"# Introduction\"] }", "config.toc.orderedList.description": "目次(TOC)に順序付きリストを使用する:\n1. ...\n2. ...", "config.toc.plaintext.description": "目次(TOC)にプレーンテキスト(リンクなし)を使用する", "config.toc.slugifyMode.description": "見出しIDの生成方法。これは、**目次(TOC)**、**コード補完**、**出力**における**見出しへのリンク**へ影響します。", "config.toc.unorderedList.marker.description": "目次(TOC)に `-` 、`*` 、`+` のどれを使用するか(**順序なし**リストの場合)。", "config.toc.updateOnSave.description": "保存時に目次(TOC)を自動更新する", "ui.exporting.messageCustomCssNotFound": "'{0}' が見つかりません。", "ui.exporting.messageExportingInProgress": "{1} 出力中: '{0}'", "ui.exporting.messageRevertingToImagePaths": "base64エンコードを画像Pathに戻す。", "ui.general.messageNoValidMarkdownFile": "有効な Markdown ファイルがありません。", "ui.general.messageUnableToReadFile": "ファイルを読み取れません: '{0}'", "ui.welcome.buttonDismiss": "却下", "ui.welcome.buttonOpenLocal": "表示する" } ================================================ FILE: package.nls.json ================================================ { "ext.displayName": "Markdown All in One", "ext.description": "All you need to write Markdown (keyboard shortcuts, table of contents, auto preview and more)", "command.toc.create.title": "Create Table of Contents", "command.toc.update.title": "Update Table of Contents", "command.toc.addSecNumbers.title": "Add/Update section numbers", "command.toc.removeSecNumbers.title": "Remove section numbers", "command.printToHtml.title": "Print current document to HTML", "command.printToHtmlBatch.title": "Print documents to HTML (select a source folder)", "command.editing.toggleCodeSpan.title": "Toggle code span", "command.editing.toggleMath.title": "Toggle math environment", "command.editing.toggleMathReverse.title": "Toggle math environment (in reverse order)", "command.editing.toggleList.title": "Toggle list", "command.editing.toggleCodeBlock.title": "Toggle code block", "command.editing.toggleBold": "Toggle Bold", "command.editing.toggleItalic": "Toggle Italic", "command.editing.toggleStrikethrough": "Toggle Strikethrough", "command.checkTaskList": "Toggle TaskList", "config.title": "Markdown All in One", "config.completion.enabled": "Whether to enable auto-completion.", "config.completion.respectVscodeSearchExclude": "Whether to exclude files from auto-completion using VS Code's `#search.exclude#` setting. (`node_modules`, `bower_components` and `*.code-search` are **always excluded**, not affected by this option.)", "config.completion.root": "The root folder for path auto-completion.", "config.italic.indicator.description": "Use `*` or `_` to wrap italic text.", "config.bold.indicator.description": "Use `**` or `__` to wrap bold text.", "config.katex.macros.description": "User-defined KaTeX macros.", "config.list.indentationSize.description": "List indentation scheme. (Also affects TOC generation.)\n\nWhether to use different indentation sizes on different list contexts or stick to VS Code's tab size.", "config.list.indentationSize.enumDescriptions.adaptive": "Adaptive indentation size according to the context, trying to **left align the sublist with its parent's content**. For example:\n\n```markdown\n- Parent\n - Sublist\n\n1. Parent\n 1. Sublist\n\n10. Parent with longer marker\n 1. Sublist\n```", "config.list.indentationSize.enumDescriptions.inherit": "Use the configured tab size of the current document (see the status bar). For example (with `tabSize: 4`):\n\n```markdown\n- Parent\n - Sublist\n\n1. Parent\n 1. Sublist\n\n10. Parent with longer marker\n 1. Sublist\n```", "config.list.toggle.candidate-markers.description": "List candidate markers. It will cycle through those markers", "config.math.enabled": "Enable basic math support (Powered by KaTeX).", "config.orderedList.autoRenumber.description": "Auto fix ordered list markers.", "config.orderedList.marker.description": "Ordered list marker.", "config.orderedList.marker.enumDescriptions.one": "Always use `1.` as ordered list marker.", "config.orderedList.marker.enumDescriptions.ordered": "Use increasing numbers as ordered list marker.", "config.preview.autoShowPreviewToSide.description": "Auto show preview to side.", "config.print.absoluteImgPath.description": "Convert image path to absolute path.", "config.print.imgToBase64.description": "Convert images to base64 when printing to HTML.", "config.print.includeVscodeStylesheets": "Include VS Code's basic Markdown styles so that the exported HTML looks similar as inside VS Code.", "config.print.onFileSave.description": "Print current document to HTML when file is saved.", "config.print.pureHtml.description": "Print current document to pure HTML (without any stylesheets).", "config.print.theme": "Theme of the exported HTML. Only affects code blocks.", "config.print.validateUrls.description": "Enable/disable URL validation when printing.", "config.showActionButtons.description": "Show buttons (e.g. toggle bold, italic) on the editor toolbar.", "config.syntax.decorations.description": "(**Deprecated**) Use `#markdown.extension.theming.decoration.renderCodeSpan#` instead. See for details.", "config.syntax.decorationFileSizeLimit.description": "If a file is larger than this size (in byte/B), we won't attempt to render syntax decorations.", "config.syntax.plainTheme.description": "(**Experimental**) Report issue at .", "config.tableFormatter.enabled.description": "Enable [GitHub Flavored Markdown](https://github.github.com/gfm/) table formatter.", "config.tableFormatter.normalizeIndentation.description": "Normalize table indentation to closest multiple of configured editor tab size.", "config.tableFormatter.delimiterRowNoPadding.description": "Don't add padding to the delimiter row.", "config.theming.decoration.renderCodeSpan.description": "Apply a border around a [code span](https://spec.commonmark.org/0.29/#code-spans).", "config.theming.decoration.renderHardLineBreak.description": "(**Experimental**)", "config.theming.decoration.renderLink.description": "(**Experimental**)", "config.theming.decoration.renderParagraph.description": "(**Experimental**)", "config.theming.decoration.renderStrikethrough.description": "Show a line through the middle of a [strikethrough](https://github.github.com/gfm/#strikethrough-extension-).", "config.theming.decoration.renderTrailingSpace.description": "Shade the background of trailing space (U+0020) characters on a [line](https://spec.commonmark.org/0.29/#line).", "config.toc.levels.description": "Range of levels for table of contents. Use `x..y` for level `x` to `y`.", "config.toc.omittedFromToc.description": "Lists of headings to omit by project file.\nExample:\n{ \"README.md\": [\"# Introduction\"] }", "config.toc.orderedList.description": "Use ordered list, that is:\n1. ...\n2. ...", "config.toc.plaintext.description": "Just plain text TOC, no links.", "config.toc.slugifyMode.description": "The method to generate heading ID. This affects **links to headings** in **TOC**, **code completion**, and **printing**.", "config.toc.unorderedList.marker.description": "Use `-`, `*`, or `+` in the table of contents (for **unordered** list).", "config.toc.updateOnSave.description": "Auto update TOC on save.", "config.extraLangIds.description": "List of extra supported languages (e.g., rmd, quarto), default [].", "ui.exporting.messageCustomCssNotFound": "Custom CSS '{0}' not found.", "ui.exporting.messageExportingInProgress": "Printing '{0}' to {1} ...", "ui.exporting.messageRevertingToImagePaths": "Reverting to image paths instead of base64 encoding.", "ui.general.messageNoValidMarkdownFile": "No valid Markdown file.", "ui.general.messageUnableToReadFile": "Unable to read file '{0}'.", "ui.welcome.buttonDismiss": "Dismiss", "ui.welcome.buttonOpenLocal": "Read" } ================================================ FILE: package.nls.ru-ru.json ================================================ { "ext.displayName": "Markdown Все в Одном", "ext.description": "Все что вам нужно для редактирования документов Markdown (Быстрые клавиши, содержание, предпросмотр и много другого)", "command.toc.create.title": "Создать содержание", "command.toc.update.title": "ОБновить содержание", "command.toc.addSecNumbers.title": "Добавить/Обновить номер раздела", "command.toc.removeSecNumbers.title": "Удалить номер раздела", "command.printToHtml.title": "Конвертировать текущий документ в HTML", "command.printToHtmlBatch.title": "Конвертировать документ в HTML (выбрать исходную папку)", "command.editing.toggleCodeSpan.title": "Переключить код в строке", "command.editing.toggleMath.title": "Переключить math environment", "command.editing.toggleMathReverse.title": "Переключить math environment (in reverse order)", "command.editing.toggleList.title": "Переключить список", "command.editing.toggleCodeBlock.title": "Переключить блок кода", "config.title": "Markdown Все в Одном", "config.completion.respectVscodeSearchExclude": "Нужно ли исключить файлы из поиска для авто-заполнения используя настройку VS Code `#search.exclude#`. (`node_modules`, `bower_components` и `*.code-search` **всегда исключены** и ни как не затронуты этим действием.)", "config.completion.root": "Корневая папка для авто-заполнения путей.", "config.italic.indicator.description": "Используйте `*` или `_` создать наклонный текст.", "config.bold.indicator.description": "Используйте `**` или `__` создать жирный текст.", "config.katex.macros.description": "Пользовательский KaTeX macros.", "config.list.indentationSize.description": "Схема отступов для списков. (Так же применяется для генерации содержания (TOC))\n\nНужно ли использовать другой размер отступа, на разных списках или использовать настройки VS Code's для размера отступа.", "config.list.indentationSize.enumDescriptions.adaptive": "Адаптивный размер отступа согласно контекста, попытка **Выровнять подсписок в лево относительно родителя**. Например:\n\n```markdown\n- Родитель\n - Подсписок\n\n1. Родитель\n 1. Подсписок\n\n10. Родитель с более длинным текстом\n 1. Подсписок\n```", "config.list.indentationSize.enumDescriptions.inherit": "Использовать размер табуляции текущего документа (смотрите в статусной строке). Например (с `tabSize: 4`):\n\n```markdown\n- Родитель\n - Подсписок\n\n1. Родитель\n 1. Подсписок\n\n10. Родитель wс более длинным текстом\n 1. Подсписок\n```", "config.list.toggle.candidate-markers.description": "List candidate markers. It will cycle through those markers", "config.math.enabled": "Активировать поддержку базовой математике (При поддержке KaTeX).", "config.orderedList.autoRenumber.description": "Автоматически исправлять маркеры сортированного списка.", "config.orderedList.marker.description": "Сортированный список.", "config.orderedList.marker.enumDescriptions.one": "Всегда использовать `1.` как маркер сортированного списка.", "config.orderedList.marker.enumDescriptions.ordered": "Увеличивать нумерацию сортированного списка.", "config.preview.autoShowPreviewToSide.description": "Автоматически открыть предпросмотр с боку.", "config.print.absoluteImgPath.description": "Изменить пути к картинкам на абсолютные.", "config.print.imgToBase64.description": "Конвертировать картинка в Base64 при генерации HTML.", "config.print.includeVscodeStylesheets": "Использовать базовый стиль VS Code's для Markdown, чтобы экспорт в HTML выглядел так же как и предпросмотр в VS Code.", "config.print.onFileSave.description": "Сохранить версию в HTML каждый раз когда файл markdown сохранен.", "config.print.theme": "Тема для экспорта в HTML. Применяется только к блокам кода.", "config.print.validateUrls.description": "Активировать/отключить проверку URL при экспорте.", "config.syntax.decorations.description": "(**Устарело**) Используйте `#markdown.extension.theming.decoration.renderCodeSpan#`. Смотрите для подробностей.", "config.syntax.decorationFileSizeLimit.description": "Если файл больше указного размер (в байтах), мы не будем стараться отрисовать подсветку синтаксиса.", "config.syntax.plainTheme.description": "(**Эксперимент**) Сообщите о проблеме .", "config.tableFormatter.enabled.description": "Активировать форматирование таблиц [GitHub Flavored Markdown](https://github.github.com/gfm/).", "config.tableFormatter.normalizeIndentation.description": "Нормализовать отступы таблиц к ближайшему от настроенного размера табуляции.", "config.tableFormatter.delimiterRowNoPadding.description": "Не добавлять отступы к строке разделителю.", "config.theming.decoration.renderCodeSpan.description": "Нарисовать границы вокруг [code span](https://spec.commonmark.org/0.29/#code-spans).", "config.theming.decoration.renderHardLineBreak.description": "(**Эксперимент**)", "config.theming.decoration.renderLink.description": "(**Эксперимент**)", "config.theming.decoration.renderParagraph.description": "(**Эксперимент**)", "config.theming.decoration.renderStrikethrough.description": "Показать линии через центр [зачеркнуто](https://github.github.com/gfm/#strikethrough-extension-).", "config.theming.decoration.renderTrailingSpace.description": "Подсветить пробел (U+0020) в конце строки [line](https://spec.commonmark.org/0.29/#line).", "config.toc.levels.description": "Диапазон уровней для списка содержания. Используйте `x..y` для уровней от `x` до `y`.", "config.toc.omittedFromToc.description": "Список заголовков игнорировать при генерации содержания всего проекта.\nНапример:\n{ \"README.md\": [\"# Вступление\"] }", "config.toc.orderedList.description": "Сортированный список, как:\n1. ...\n2. ...", "config.toc.plaintext.description": "Просто содержание (TOC), без ссылок.", "config.toc.slugifyMode.description": "Метод генерации ID заголовков. Применяется к **ссылкам на заголовки** в **содержании**, **автозаполнение**, и **печати**.", "config.toc.unorderedList.marker.description": "Используйте `-`, `*`, или `+` в содержании (ТОС) (для **несортированного** списка).", "config.toc.updateOnSave.description": "Автоматически обновить содержание (ТОС) при сохранении.", "config.extraLangIds.description": "Список дополнительно поддерживаемых языков (например: rmd, qmd), по умолчанию [].", "ui.exporting.messageCustomCssNotFound": "Пользовательский CSS '{0}' не найден.", "ui.exporting.messageExportingInProgress": "Печатаем '{0}' в {1} ...", "ui.exporting.messageRevertingToImagePaths": "Вернуть путь к картинке вместо кодирования base64.", "ui.general.messageNoValidMarkdownFile": "Это файл не Markdown.", "ui.general.messageUnableToReadFile": "Не могу прочитать файл '{0}'.", "ui.welcome.buttonDismiss": "Отменить", "ui.welcome.buttonOpenLocal": "Прочтено" } ================================================ FILE: package.nls.zh-cn.json ================================================ { "ext.displayName": "Markdown All in One", "ext.description": "使用 Markdown 所需要的一切(快捷键,目录,自动预览以及更多功能)", "command.toc.create.title": "创建目录", "command.toc.update.title": "更新目录", "command.toc.addSecNumbers.title": "添加/更新章节序号", "command.toc.removeSecNumbers.title": "删除章节序号", "command.printToHtml.title": "将当前文档打印为 HTML", "command.printToHtmlBatch.title": "批量打印文档为 HTML(选择文件夹)", "command.editing.toggleCodeSpan.title": "触发代码块", "command.editing.toggleMath.title": "触发数学环境", "command.editing.toggleMathReverse.title": "触发数学环境(反向)", "command.editing.toggleList.title": "触发列表", "command.editing.toggleCodeBlock.title": "触发代码块", "config.title": "Markdown All in One", "config.completion.enabled": "是否启用自动补全。", "config.completion.respectVscodeSearchExclude": "进行自动补全时是否考虑 VS Code 的 `#search.exclude#` 设置。(`node_modules`,`bower_components` 和 `*.code-search` 将**总是被排除**,不受此选项影响。)", "config.completion.root": "路径自动补全的根路径。", "config.italic.indicator.description": "使用 `*` 或 `_` 包围斜体文本。", "config.bold.indicator.description": "用 `**` 或 `__` 来括住粗体字。", "config.katex.macros.description": "自定义 KaTeX 宏。", "config.list.indentationSize.description": "Markdown 列表的缩进大小(包括目录列表)。", "config.list.indentationSize.enumDescriptions.adaptive": "参考 **CommonMark Spec**,根据上下文判断缩进大小,并尝试**将子级的左端对齐父级的内容的左端**。比如:\n\n```markdown\n- Parent\n - Sublist\n\n1. Parent\n 1. Sublist\n\n10. Parent with longer marker\n 1. Sublist\n```", "config.list.indentationSize.enumDescriptions.inherit": "使用当前文档设置的缩进量(请查看 VS Code 状态栏)。比如:(`tabSize: 4`)\n\n```markdown\n- Parent\n - Sublist\n\n1. Parent\n 1. Sublist\n\n10. Parent with longer marker\n 1. Sublist\n```", "config.list.toggle.candidate-markers.description": "可选的列表标记,调整时将循环使用这些标记", "config.math.enabled": "启用基本的数学支持(由 KaTeX 提供)。", "config.orderedList.autoRenumber.description": "自动更正有序列表的标号。", "config.orderedList.marker.description": "有序列表标记。", "config.orderedList.marker.enumDescriptions.one": "总是使用 `1.` 作为有序列表标记。", "config.orderedList.marker.enumDescriptions.ordered": "使用递增数字作为有序列表标记。", "config.preview.autoShowPreviewToSide.description": "自动在另一栏显示预览。", "config.print.absoluteImgPath.description": "将图片路径转换为绝对路径。", "config.print.imgToBase64.description": "在打印为 HTML 时将图片转换为 base64。", "config.print.includeVscodeStylesheets": "输出 HTML 时引用 VS Code 自带的 Markdown 样式,使其与 VS Code 中的预览保持接近。", "config.print.onFileSave.description": "Markdown 文档保存后自动打印为 HTML。", "config.print.theme": "输出的 HTML 的样式主题(只影响代码块)。", "config.print.validateUrls.description": "启用/禁用打印 HTML 时的 URL 验证。", "config.syntax.decorations.description": "(**已弃用**)改用 `#markdown.extension.theming.decoration.renderCodeSpan#`。请在 查看细节。", "config.syntax.decorationFileSizeLimit.description": "如果文件大于这个尺寸(byte/B), 我们不再渲染语法装饰器。", "config.syntax.plainTheme.description": "(**实验性**)请在 报告问题。", "config.tableFormatter.enabled.description": "启用 [GitHub Flavored Markdown](https://github.github.com/gfm/) 表格格式化。", "config.tableFormatter.normalizeIndentation.description": "使位于列表内的表格的缩进长度为制表符长度(`tabSize`)。", "config.tableFormatter.delimiterRowNoPadding.description": "使表格的分隔线 `---` 填满整个单元格。", "config.theming.decoration.renderCodeSpan.description": "在[行内代码 (code span)](https://spec.commonmark.org/0.29/#code-spans) 周围显示边框。", "config.theming.decoration.renderHardLineBreak.description": "(**实验性**)", "config.theming.decoration.renderLink.description": "(**实验性**)", "config.theming.decoration.renderParagraph.description": "(**实验性**)", "config.theming.decoration.renderStrikethrough.description": "显示[删除线 (strikethrough)](https://github.github.com/gfm/#strikethrough-extension-)。", "config.theming.decoration.renderTrailingSpace.description": "为[行](https://spec.commonmark.org/0.29/#line)末端的空格 (U+0020) 字符添加底纹背景。", "config.toc.levels.description": "目录级别的范围。例如 `2..5` 表示在目录中只包含 2 到 5 级标题。", "config.toc.omittedFromToc.description": "在指定文件的目录中省略这些标题。\n示例:\n{ \"README.md\": [\"# Introduction\"] }", "config.toc.orderedList.description": "使用有序列表,即:\n1. ...\n2. ...", "config.toc.plaintext.description": "使用纯文本目录。", "config.toc.slugifyMode.description": "生成标题 ID 的方法。该设置影响**目录**、**代码自动补全**、**打印**中**指向标题的链接**。", "config.toc.unorderedList.marker.description": "在目录中使用 `-`,`*` 或 `+` (仅对于**无序**列表)。", "config.toc.updateOnSave.description": "保存时自动更新目录。", "config.extraLangIds.description": "支持的其他语言列表(如rmd, quarto),默认为空。", "ui.exporting.messageCustomCssNotFound": "自定义样式 '{0}' 未找到。", "ui.exporting.messageExportingInProgress": "将 '{0}' 打印到 {1} …", "ui.exporting.messageRevertingToImagePaths": "已使用图像路径而不是 base64 编码。", "ui.general.messageNoValidMarkdownFile": "未选中有效的 Markdown 文件。", "ui.general.messageUnableToReadFile": "无法读取文件 '{0}'。", "ui.welcome.buttonDismiss": "忽略", "ui.welcome.buttonOpenLocal": "阅读" } ================================================ FILE: package.nls.zh-tw.json ================================================ { "ext.displayName": "Markdown All in One", "ext.description": "使用 Markdown 所需的一切(快捷鍵、目錄、自動預覽以及更多功能)", "command.toc.create.title": "建立目錄", "command.toc.update.title": "更新目錄", "command.toc.addSecNumbers.title": "新增/更新章節編號", "command.toc.removeSecNumbers.title": "移除章節編號", "command.printToHtml.title": "將當前文件列印為 HTML", "command.printToHtmlBatch.title": "批次列印文件為 HTML(選擇來源資料夾)", "command.editing.toggleCodeSpan.title": "觸發程式碼片段", "command.editing.toggleMath.title": "觸發數學環境", "command.editing.toggleMathReverse.title": "觸發數學環境(反向)", "command.editing.toggleList.title": "觸發清單", "command.editing.toggleCodeBlock.title": "觸發程式碼區塊", "command.editing.toggleBold": "觸發粗體", "command.editing.toggleItalic": "觸發斜體", "command.editing.toggleStrikethrough": "觸發刪除線", "command.checkTaskList": "觸發任務清單", "config.title": "Markdown All in One", "config.completion.enabled": "是否啟用自動補全。", "config.completion.respectVscodeSearchExclude": "進行自動補全時是否考慮 VS Code 的 `#search.exclude#` 設定。(`node_modules`、`bower_components` 和 `*.code-search` 將**始終被排除**,不受此選項影響。)", "config.completion.root": "路徑自動補全的根資料夾。", "config.italic.indicator.description": "使用 `*` 或 `_` 包圍斜體文字。", "config.bold.indicator.description": "使用 `**` 或 `__` 包圍粗體文字。", "config.katex.macros.description": "自訂 KaTeX 巨集。", "config.list.indentationSize.description": "清單縮排方案。(也影響目錄生成。)\n\n是否根據不同清單上下文使用不同的縮排大小,或遵循 VS Code 的製表符大小。", "config.list.indentationSize.enumDescriptions.adaptive": "根據上下文自適應縮排大小,嘗試**將子清單的左端對齊父層內容的左端**。例如:\n\n```markdown\n- 父層\n - 子清單\n\n1. 父層\n 1. 子清單\n\n10. 標記較長的父層\n 1. 子清單\n```", "config.list.indentationSize.enumDescriptions.inherit": "使用當前文件設定的製表符大小(請查看狀態列)。例如(`tabSize: 4`):\n\n```markdown\n- 父層\n - 子清單\n\n1. 父層\n 1. 子清單\n\n10. 標記較長的父層\n 1. 子清單\n```", "config.list.toggle.candidate-markers.description": "清單候選標記,將循環使用這些標記。", "config.math.enabled": "啟用基本的數學支援(由 KaTeX 提供)。", "config.orderedList.autoRenumber.description": "自動修正有序清單標記。", "config.orderedList.marker.description": "有序清單標記。", "config.orderedList.marker.enumDescriptions.one": "始終使用 `1.` 作為有序清單標記。", "config.orderedList.marker.enumDescriptions.ordered": "使用遞增數字作為有序清單標記。", "config.preview.autoShowPreviewToSide.description": "自動在側邊顯示預覽。", "config.print.absoluteImgPath.description": "將圖片路徑轉換為絕對路徑。", "config.print.imgToBase64.description": "在列印為 HTML 時將圖片轉換為 base64。", "config.print.includeVscodeStylesheets": "輸出 HTML 時引用 VS Code 自帶的 Markdown 樣式,使其與 VS Code 中的預覽保持接近。", "config.print.onFileSave.description": "文件儲存時自動將當前文件列印為 HTML。", "config.print.pureHtml.description": "將當前文件列印為純 HTML(不含任何樣式表)。", "config.print.theme": "輸出的 HTML 樣式主題(僅影響程式碼區塊)。", "config.print.validateUrls.description": "啟用/停用列印時的 URL 驗證。", "config.showActionButtons.description": "在編輯器工具列上顯示按鈕(例如觸發粗體、斜體)。", "config.syntax.decorations.description": "(**已棄用**)改用 `#markdown.extension.theming.decoration.renderCodeSpan#`。請在 查看詳細資訊。", "config.syntax.decorationFileSizeLimit.description": "如果文件大於此尺寸(byte/B),我們不再渲染語法裝飾器。", "config.syntax.plainTheme.description": "(**實驗性**)請在 回報問題。", "config.tableFormatter.enabled.description": "啟用 [GitHub Flavored Markdown](https://github.github.com/gfm/) 表格格式化。", "config.tableFormatter.normalizeIndentation.description": "將表格縮排標準化為最接近編輯器設定的製表符大小的倍數。", "config.tableFormatter.delimiterRowNoPadding.description": "不為分隔行添加填充。", "config.theming.decoration.renderCodeSpan.description": "在[行內程式碼 (code span)](https://spec.commonmark.org/0.29/#code-spans) 周圍顯示邊框。", "config.theming.decoration.renderHardLineBreak.description": "(**實驗性**)", "config.theming.decoration.renderLink.description": "(**實驗性**)", "config.theming.decoration.renderParagraph.description": "(**實驗性**)", "config.theming.decoration.renderStrikethrough.description": "在[刪除線 (strikethrough)](https://github.github.com/gfm/#strikethrough-extension-) 中間顯示一條線。", "config.theming.decoration.renderTrailingSpace.description": "為[行](https://spec.commonmark.org/0.29/#line)末端的空格(U+0020)字元添加底紋背景。", "config.toc.levels.description": "目錄的級別範圍。使用 `x..y` 表示從級別 `x` 到 `y`。", "config.toc.omittedFromToc.description": "按專案文件省略的標題清單。\n範例:\n{ \"README.md\": [\"# 簡介\"] }", "config.toc.orderedList.description": "使用有序清單,即:\n1. ...\n2. ...", "config.toc.plaintext.description": "純文字目錄,不含連結。", "config.toc.slugifyMode.description": "產生標題 ID 的方法。此設定影響**目錄**、**程式碼自動補全**和**列印**中的**標題連結**。", "config.toc.unorderedList.marker.description": "在目錄中使用 `-`、`*` 或 `+`(僅適用於**無序**清單)。", "config.toc.updateOnSave.description": "儲存時自動更新目錄。", "config.extraLangIds.description": "支援的額外語言清單(例如 rmd、quarto),預設為 []。", "ui.exporting.messageCustomCssNotFound": "自訂 CSS '{0}' 未找到。", "ui.exporting.messageExportingInProgress": "將 '{0}' 列印到 {1} ...", "ui.exporting.messageRevertingToImagePaths": "已使用圖片路徑而非 base64 編碼。", "ui.general.messageNoValidMarkdownFile": "無有效的 Markdown 文件。", "ui.general.messageUnableToReadFile": "無法讀取文件 '{0}'。", "ui.welcome.buttonDismiss": "忽略", "ui.welcome.buttonOpenLocal": "閱讀" } ================================================ FILE: src/IDisposable.ts ================================================ "use strict"; /** * @see * @see * @see */ export default interface IDisposable { /** * Performs application-defined tasks associated with freeing, releasing, or resetting resources. */ dispose(): any; } ================================================ FILE: src/completion.ts ================================================ 'use strict' import * as fs from 'fs'; import sizeOf from 'image-size'; import * as path from 'path'; import { CancellationToken, CompletionContext, CompletionItem, CompletionItemKind, CompletionItemProvider, CompletionList, ExtensionContext, languages, MarkdownString, Position, Range, SnippetString, TextDocument, workspace } from 'vscode'; import { configManager } from "./configuration/manager"; import { getAllTocEntry, IHeading } from './toc'; import { mathEnvCheck } from "./util/contextCheck"; import { Document_Selector_Markdown } from './util/generic'; import * as katexFuncs from './util/katex-funcs'; export function activate(context: ExtensionContext) { context.subscriptions.push(languages.registerCompletionItemProvider(Document_Selector_Markdown, new MdCompletionItemProvider(), '(', '\\', '/', '[', '#')); } interface IReferenceDefinition { label: string; usageCount: number; } class MdCompletionItemProvider implements CompletionItemProvider { readonly RXlookbehind = String.raw`(?<=(^[>]? {0,3}\[[ \t\r\n\f\v]*))`; // newline, not quoted, max 3 spaces, open [ readonly RXlinklabel = String.raw`(?([^\]]|(\\\]))*)`; // string for linklabel, allows for /] in linklabel readonly RXlink = String.raw`(?((<[^>]*>)|([^< \t\r\n\f\v]+)))`; // link either or mylink readonly RXlinktitle = String.raw`(?[ \t\r\n\f\v]+(("([^"]|(\\"))*")|('([^']|(\\'))*')))?$)`; // optional linktitle in "" or '' readonly RXlookahead = String.raw`(?=(\]:[ \t\r\n\f\v]*` // close linklabel with ]: + this.RXlink + this.RXlinktitle + String.raw`)`; // end regex readonly RXflags = String.raw`mg`; // multiline & global // This pattern matches linklabels in link references definitions: [linklabel]: link "link title" readonly Link_Label_Pattern = new RegExp(this.RXlookbehind + this.RXlinklabel + this.RXlookahead, this.RXflags); mathCompletions: CompletionItem[]; readonly EXCLUDE_GLOB: string; constructor() { // \cmd let c1 = Array.from(new Set( [ ...katexFuncs.delimiters0, ...katexFuncs.delimeterSizing0, ...katexFuncs.greekLetters0, ...katexFuncs.otherLetters0, ...katexFuncs.spacing0, ...katexFuncs.verticalLayout0, ...katexFuncs.logicAndSetTheory0, ...katexFuncs.macros0, ...katexFuncs.bigOperators0, ...katexFuncs.binaryOperators0, ...katexFuncs.binomialCoefficients0, ...katexFuncs.fractions0, ...katexFuncs.mathOperators0, ...katexFuncs.relations0, ...katexFuncs.negatedRelations0, ...katexFuncs.arrows0, ...katexFuncs.font0, ...katexFuncs.size0, ...katexFuncs.style0, ...katexFuncs.symbolsAndPunctuation0, ...katexFuncs.debugging0 ] )).map(cmd => { let item = new CompletionItem('\\' + cmd, CompletionItemKind.Function); item.insertText = cmd; return item; }); // \cmd{$1} let c2 = Array.from(new Set( [ ...katexFuncs.accents1, ...katexFuncs.annotation1, ...katexFuncs.verticalLayout1, ...katexFuncs.overlap1, ...katexFuncs.spacing1, ...katexFuncs.logicAndSetTheory1, ...katexFuncs.mathOperators1, ...katexFuncs.sqrt1, ...katexFuncs.extensibleArrows1, ...katexFuncs.font1, ...katexFuncs.braketNotation1, ...katexFuncs.classAssignment1 ] )).map(cmd => { let item = new CompletionItem('\\' + cmd, CompletionItemKind.Function); item.insertText = new SnippetString(`${cmd}\{$1\}`); return item; }); // \cmd{$1}{$2} let c3 = Array.from(new Set( [ ...katexFuncs.verticalLayout2, ...katexFuncs.binomialCoefficients2, ...katexFuncs.fractions2, ...katexFuncs.color2 ] )).map(cmd => { let item = new CompletionItem('\\' + cmd, CompletionItemKind.Function); item.insertText = new SnippetString(`${cmd}\{$1\}\{$2\}`); return item; }); let envSnippet = new CompletionItem('\\begin', CompletionItemKind.Snippet); envSnippet.insertText = new SnippetString('begin{${1|' + katexFuncs.envs.join(',') + '|}}\n\t$2\n\\end{$1}'); // Pretend to support multi-workspacefolders const folder = workspace.workspaceFolders?.[0]?.uri; // Import macros from configurations const configMacros = configManager.get("katex.macros", folder); var macroItems: CompletionItem[] = []; for (const [cmd, expansion] of Object.entries(configMacros)) { let item = new CompletionItem(cmd, CompletionItemKind.Function); // Find the number of arguments in the expansion let numArgs = 0; for (let i = 1; i < 10; i++) { if (!expansion.includes(`#${i}`)) { numArgs = i - 1; break; } } item.insertText = new SnippetString(cmd.slice(1) + [...Array(numArgs).keys()].map(i => `\{$${i + 1}\}`).join("")); macroItems.push(item); } this.mathCompletions = [...c1, ...c2, ...c3, envSnippet, ...macroItems]; // Sort for (const item of this.mathCompletions) { const label = typeof item.label === "string" ? item.label : item.label.label; item.sortText = label.replace(/[a-zA-Z]/g, (c) => { if (/[a-z]/.test(c)) { return `0${c}`; } else { return `1${c.toLowerCase()}`; } }); } const Always_Exclude = ["**/node_modules", "**/bower_components", "**/*.code-search", "**/.git"]; const excludePatterns = new Set(Always_Exclude); if (configManager.get("completion.respectVscodeSearchExclude", folder)) { // `search.exclude` is currently not implemented in Theia IDE (which is mostly compatible with VSCode extensions) // fallback to `files.exclude` (in VSCode, `search.exclude` inherits from `files.exclude`) or an empty list // see https://github.com/eclipse-theia/theia/issues/13823 const vscodeSearchExclude = configManager.getByAbsolute<object>("search.exclude", folder) ?? configManager.getByAbsolute<object>("search.exclude", folder) ?? {}; for (const [pattern, enabled] of Object.entries(vscodeSearchExclude)) { if (enabled) { excludePatterns.add(pattern); } } } this.EXCLUDE_GLOB = "{" + Array.from(excludePatterns).join(",") + "}"; } async provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken, _context: CompletionContext): Promise<CompletionItem[] | CompletionList<CompletionItem> | undefined> { const lineTextBefore = document.lineAt(position.line).text.substring(0, position.character); const lineTextAfter = document.lineAt(position.line).text.substring(position.character); let matches; matches = lineTextBefore.match(/\\+$/); // Math functions // ============== if ( // ends with an odd number of backslashes (matches = lineTextBefore.match(/\\+$/)) !== null && matches[0].length % 2 !== 0 ) { if (mathEnvCheck(document, position) === "") { return []; } else { return this.mathCompletions; } } // Reference link labels // ===================== // e.g. [linklabel]: link "link title" if (/\[[^\[\]]*$/.test(lineTextBefore)) { return this.completeRefLinks(document, lineTextBefore, position, token); } const enabled = workspace.getConfiguration('markdown.extension.completion', document.uri).get<boolean>('enabled', false); if (!enabled) { return []; } // Image paths // =========== if (/!\[[^\]]*?\]\([^\)]*$/.test(lineTextBefore) || /<img [^>]*src="[^"]*$/.test(lineTextBefore)) { return this.completeImgPaths(document, lineTextBefore); } // Links to heading // ================ if ( /\[[^\[\]]*?\]\(#[^#\)]*$/.test(lineTextBefore) || /^>? {0,3}\[[^\[\]]+?\]\:[ \t\f\v]*#[^#]*$/.test(lineTextBefore) // /\[[^\]]*\]\((\S*)#[^\)]*$/.test(lineTextBefore) // `[](url#anchor|` Link with anchor. // || /\[[^\]]*\]\:\s?(\S*)#$/.test(lineTextBefore) // `[]: url#anchor|` Link reference definition with anchor. ) { return this.completeLinksToHeading(document, position, lineTextBefore, lineTextAfter); } // File paths // ========== // should be after `completeLinksToHeading` if (/\[[^\[\]]*?\](?:(?:\([^\)]*)|(?:\:[ \t\f\v]*\S*))$/.test(lineTextBefore)) { return this.completeFilePaths(lineTextBefore, document); } return []; } private completeImgPaths(document: TextDocument, lineTextBefore: string) { if (workspace.getWorkspaceFolder(document.uri) === undefined) return []; //// 🤔 better name? let typedDir: string; if (/!\[[^\]]*?\]\([^\)]*$/.test(lineTextBefore)) { //// `![](dir_here|)` typedDir = lineTextBefore.substr(lineTextBefore.lastIndexOf('](') + 2); } else { //// `<img src="dir_here|">` typedDir = lineTextBefore.substr(lineTextBefore.lastIndexOf('="') + 2); } const basePath = getBasepath(document, typedDir); const isRootedPath = typedDir.startsWith('/'); return workspace.findFiles('**/*.{png,jpg,jpeg,svg,gif,webp}', this.EXCLUDE_GLOB).then(uris => { const items: CompletionItem[] = []; for (const imgUri of uris) { const label = path.relative(basePath, imgUri.fsPath).replace(/\\/g, '/'); if (isRootedPath && label.startsWith("..")) { continue; } let item = new CompletionItem(label.replace(/ /g, '%20'), CompletionItemKind.File); items.push(item); //// Add image preview let dimensions: { width: number; height: number; }; try { // @ts-ignore Deprecated. dimensions = sizeOf(imgUri.fsPath); } catch (error) { console.error(error); continue; } const maxWidth = 318; if (dimensions.width > maxWidth) { dimensions.height = Number(dimensions.height * maxWidth / dimensions.width); dimensions.width = maxWidth; } item.documentation = new MarkdownString(`![${label}](${imgUri.fsPath.replace(/ /g, '%20')}|width=${dimensions.width},height=${dimensions.height})`); item.sortText = label.replace(/\./g, '{'); } return items; }); } private completeRefLinks(document: TextDocument, lineTextBefore: string, position: Position, token: CancellationToken) { // TODO: may be extracted to a seperate function and used for all completions in the future. const docText = document.getText(); /** * NormalizedLabel (upper case) -> IReferenceDefinition */ const refDefinitions = new Map<string, IReferenceDefinition>(); for (const match of docText.matchAll(this.Link_Label_Pattern)) { // Remove leading and trailing whitespace characters. const label = match[0].replace(/^[ \t\r\n\f\v]+/, '').replace(/[ \t\r\n\f\v]+$/, ''); // For case-insensitive comparison. const normalizedLabel = label.toUpperCase(); // The one that comes first in the document is used. if (!refDefinitions.has(normalizedLabel)) { refDefinitions.set(normalizedLabel, { label, // Preserve original case in result. usageCount: 0, }); } } if (refDefinitions.size === 0 || token.isCancellationRequested) { return; } // A confusing feature from #414. Not sure how to get it work. const docLines = docText.split(/\r?\n/); for (const crtLine of docLines) { // Match something that may be a reference link. const pattern = /\[([^\[\]]+?)\](?![(:\[])/g; for (const match of crtLine.matchAll(pattern)) { const label = match[1]; const record = refDefinitions.get(label.toUpperCase()); if (record) { record.usageCount++; } } } let startIndex = lineTextBefore.lastIndexOf('['); const range = new Range(position.with({ character: startIndex + 1 }), position); if (token.isCancellationRequested) { return; } const completionItems = Array.from<IReferenceDefinition, CompletionItem>(refDefinitions.values(), ref => { const label = ref.label; const item = new CompletionItem(label, CompletionItemKind.Reference); const usages = ref.usageCount; item.documentation = new MarkdownString(label); item.detail = usages === 1 ? `1 usage` : `${usages} usages`; // Prefer unused items. <https://github.com/yzhang-gh/vscode-markdown/pull/414#discussion_r272807189> item.sortText = usages === 0 ? `0-${label}` : `1-${label}`; item.range = range; return item; }); return completionItems } private completeLinksToHeading(document: TextDocument, position: Position, lineTextBefore: string, lineTextAfter: string) { let startIndex = lineTextBefore.lastIndexOf('#') - 1; let isLinkRefDefinition = /^>? {0,3}\[[^\[\]]+?\]\:[ \t\f\v]*#[^#]*$/.test(lineTextBefore); // The same as the 2nd conditon above. let endPosition = position; let addClosingParen = false; if (/^([^\) ]+\s*|^\s*)\)/.test(lineTextAfter)) { // try to detect if user wants to replace a link (i.e. matching closing paren and ) // Either: ... <CURSOR> something <whitespace> ) // or: ... <CURSOR> <whitespace> ) // or: ... <CURSOR> ) (endPosition assignment is a no-op for this case) // in every case, we want to remove all characters after the cursor and before that first closing paren endPosition = position.with({ character: + endPosition.character + lineTextAfter.indexOf(')') }); } else { // If no closing paren is found, replace all trailing non-white-space chars and add a closing paren // distance to first non-whitespace or EOL const toReplace = (lineTextAfter.search(/(?<=^\S+)(\s|$)/)); endPosition = position.with({ character: + endPosition.character + toReplace }); if (!isLinkRefDefinition) { addClosingParen = true; } } const range = new Range(position.with({ character: startIndex + 1 }), endPosition); return new Promise<CompletionItem[]>((res, _) => { const toc: readonly Readonly<IHeading>[] = getAllTocEntry(document, { respectMagicCommentOmit: false, respectProjectLevelOmit: false }); const headingCompletions = toc.map<CompletionItem>(heading => { const item = new CompletionItem('#' + heading.slug, CompletionItemKind.Reference); if (addClosingParen) { item.insertText = item.label + ')'; } item.documentation = heading.rawContent; item.range = range; return item; }); res(headingCompletions); }); } private async completeFilePaths(lineTextBefore: string, document: TextDocument) { const typedDir = lineTextBefore.match(/(?<=((?:\]\()|(?:\]\:))[ \t\f\v]*)\S*$/)![0]; const basePath = getBasepath(document, typedDir); const isRootedPath = typedDir.startsWith('/'); const files = await workspace.findFiles("**/*", this.EXCLUDE_GLOB); const items: CompletionItem[] = []; for (const uri of files) { const label = path.relative(basePath, uri.fsPath).replace(/\\/g, "/").replace(/ /g, "%20"); if (isRootedPath && label.startsWith("..")) { continue; } const item = new CompletionItem(label, CompletionItemKind.File); item.sortText = label.replace(/\./g, "{"); items.push(item); } return items; } } /** * @param doc * @param dir The dir already typed in the src field, e.g. `[alt text](dir_here|)` */ function getBasepath(doc: TextDocument, dir: string): string { if (dir.includes('/')) { dir = dir.substr(0, dir.lastIndexOf('/') + 1); } else { dir = ''; } let root = workspace.getWorkspaceFolder(doc.uri)!.uri.fsPath; const rootFolder = workspace.getConfiguration('markdown.extension.completion', doc.uri).get<string>('root', ''); if (rootFolder.length > 0 && fs.existsSync(path.join(root, rootFolder))) { root = path.join(root, rootFolder); } const basePath = path.join( dir.startsWith('/') ? root : path.dirname(doc.uri.fsPath), dir ); return basePath; } ================================================ FILE: src/configuration/fallback.ts ================================================ import * as vscode from "vscode"; import { IConfigurationFallbackMap } from "./manager"; import { IConfigurationKeyTypeMap } from "./model"; /** * Configuration keys that are no longer supported, * and will be removed in the next major version. */ export const Deprecated_Keys = Object.freeze<string>([ "syntax.decorations", // ]); export const Fallback_Map = Object.freeze<IConfigurationFallbackMap<IConfigurationKeyTypeMap>>({ "theming.decoration.renderCodeSpan": (scope): boolean => { const config = vscode.workspace.getConfiguration("markdown.extension", scope); const old = config.get<boolean | null>("syntax.decorations"); if (old === null || old === undefined) { return config.get<boolean>("theming.decoration.renderCodeSpan")!; } else { return old; } }, "theming.decoration.renderStrikethrough": (scope): boolean => { const config = vscode.workspace.getConfiguration("markdown.extension", scope); const old = config.get<boolean | null>("syntax.decorations"); if (old === null || old === undefined) { return config.get<boolean>("theming.decoration.renderStrikethrough")!; } else { return old; } }, }); ================================================ FILE: src/configuration/manager.ts ================================================ import * as vscode from "vscode"; import type IDisposable from "../IDisposable"; import { Deprecated_Keys, Fallback_Map } from "./fallback"; export type IConfigurationFallbackMap<M> = { [key in keyof M]?: (scope?: vscode.ConfigurationScope) => M[key] }; export interface IConfigurationService<M> extends IDisposable { /** * Gets the value that an our own key denotes. * @param key The configuration key. * @param scope The scope, for which the configuration is asked. */ get<K extends keyof M>(key: K, scope?: vscode.ConfigurationScope): M[K]; /** * Gets the value that an absolute identifier denotes. * @param section The dot-separated identifier (usually a setting ID). * @param scope The scope, for which the configuration is asked. */ getByAbsolute<T>(section: string, scope?: vscode.ConfigurationScope): T | undefined; } /** * This is currently just a proxy that helps mapping our configuration keys. */ class ConfigurationManager<M> implements IConfigurationService<M> { private readonly _fallback: Readonly<IConfigurationFallbackMap<M>>; constructor(fallback: IConfigurationFallbackMap<M>, deprecatedKeys: readonly string[]) { this._fallback = Object.isFrozen(fallback) ? fallback : Object.freeze({ ...fallback }); this.showWarning(deprecatedKeys); } public dispose(): void { } /** * Shows an error message for each deprecated key, to help user migrate. * This is async to avoid blocking instance creation. */ private async showWarning(deprecatedKeys: readonly string[]): Promise<void> { for (const key of deprecatedKeys) { const value = vscode.workspace.getConfiguration("markdown.extension").get(key); if (value !== undefined && value !== null) { // We are not able to localize this string for now. // Our NLS module needs to be configured before using, which is done in the extension entry point. // This module may be directly or indirectly imported by the entry point. // Thus, this module may be loaded before the NLS module is available. vscode.window.showErrorMessage(`The setting 'markdown.extension.${key}' has been deprecated.`); } } } public get<K extends keyof M>(key: K, scope?: vscode.ConfigurationScope): M[K] { const fallback = this._fallback[key]; if (fallback) { return fallback(scope); } else { return vscode.workspace.getConfiguration("markdown.extension", scope).get<M[K]>(key as string)!; } } public getByAbsolute<T>(section: string, scope?: vscode.ConfigurationScope): T | undefined { if (section.startsWith("markdown.extension.")) { return this.get(section.slice(19) as any, scope) as any; } else { return vscode.workspace.getConfiguration(undefined, scope).get<T>(section); } } } export const configManager = new ConfigurationManager(Fallback_Map, Deprecated_Keys); ================================================ FILE: src/configuration/model.ts ================================================ import type { MarkdownBulletListMarker, MarkdownEmphasisIndicator, MarkdownStrongEmphasisIndicator } from "../contract/MarkdownSpec"; import type { SlugifyMode } from "../contract/SlugifyMode"; /** * A map from our configuration keys to the corresponding type definitions. * These keys are relative to `markdown.extension`. * Should keep in sync with `package.json`. */ export interface IConfigurationKeyTypeMap { "completion.enabled": string; "completion.respectVscodeSearchExclude": boolean; "completion.root": string; "italic.indicator": MarkdownEmphasisIndicator; "bold.indicator": MarkdownStrongEmphasisIndicator; /** * A collection of custom macros. * @see {@link https://katex.org/docs/options.html} */ "katex.macros": { [key: string]: string }; "list.indentationSize": "adaptive" | "inherit"; "math.enabled": boolean; "orderedList.autoRenumber": boolean; "orderedList.marker": "one" | "ordered"; "preview.autoShowPreviewToSide": boolean; "print.absoluteImgPath": boolean; "print.imgToBase64": boolean; "print.includeVscodeStylesheets": boolean; "print.onFileSave": boolean; "print.theme": "dark" | "light"; "print.validateUrls": boolean; /** To be superseded. */ "syntax.decorationFileSizeLimit": number; /** To be superseded. */ "syntax.plainTheme": boolean; "tableFormatter.enabled": boolean; "tableFormatter.normalizeIndentation": boolean; "tableFormatter.delimiterRowNoPadding": boolean; /** Formerly "syntax.decorations" */ "theming.decoration.renderCodeSpan": boolean; "theming.decoration.renderHardLineBreak": boolean; "theming.decoration.renderLink": boolean; "theming.decoration.renderParagraph": boolean; /** Formerly "syntax.decorations" */ "theming.decoration.renderStrikethrough": boolean; "theming.decoration.renderTrailingSpace": boolean; "toc.levels": string; /** To be superseded. */ "toc.omittedFromToc": { [path: string]: string[] }; "toc.orderedList": boolean; "toc.plaintext": boolean; "toc.slugifyMode": SlugifyMode; "toc.unorderedList.marker": MarkdownBulletListMarker; "toc.updateOnSave": boolean; } /** * Configuration keys that this product contributes. * These keys are relative to `markdown.extension`. */ export type IConfigurationKnownKey = keyof IConfigurationKeyTypeMap; ================================================ FILE: src/contract/LanguageIdentifier.ts ================================================ "use strict"; /** * Well-known language identifiers. * @see <https://code.visualstudio.com/docs/languages/identifiers> */ const enum LanguageIdentifier { Html = "html", Json = "json", Markdown = "markdown", PlainText = "plaintext", } export default LanguageIdentifier; ================================================ FILE: src/contract/MarkdownSpec.ts ================================================ "use strict"; // The name of types here begins with `Markdown`. /** * CommonMark bullet list marker. * https://spec.commonmark.org/0.29/#list-items */ export const enum MarkdownBulletListMarker { Asterisk = "*", Hyphen = "-", Plus = "+", } /** * CommonMark emphasis indicator. * https://spec.commonmark.org/0.29/#emphasis-and-strong-emphasis */ export const enum MarkdownEmphasisIndicator { Asterisk = "*", Underscore = "_", } /** * CommonMark strong emphasis indicator. * https://spec.commonmark.org/0.29/#emphasis-and-strong-emphasis */ export const enum MarkdownStrongEmphasisIndicator { Asterisk = "**", Underscore = "__", } /** * The heading level allowed by the CommonMark Spec. * https://spec.commonmark.org/0.29/#atx-headings */ export type MarkdownHeadingLevel = 1 | 2 | 3 | 4 | 5 | 6; ================================================ FILE: src/contract/README.md ================================================ # Top-level contracts and constants ## Conventions ### General Very few things are allowed to be under this directory. They are not scoped to a few specific modules, instead, must be globally recognized and used across the whole product, and well-known outside our codebase. Currently, here are: * Well-known constants. * Public API definitions, aka public contracts. ### Naming * The name of files, types, enum members, constants must match the [StrictPascalCase](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/naming-convention.md#format) format. * Names here are globally uniquely recognized, that is, once a name is assigned to a type here, no other identifier can have the same name in our codebase. * If a file holds only one type, then it may only provide the [default export](https://www.typescriptlang.org/docs/handbook/modules.html#default-exports), and the type must be of the same name as the file. * If a file holds multiple types, then the types must be under the same topic, which is the file name. ### Organization * Each file must be a module. * Only the following are allowed: * [Const enum](https://www.typescriptlang.org/docs/handbook/enums.html#const-enums). * [Interface](https://www.typescriptlang.org/docs/handbook/interfaces.html). * [Literal](https://www.typescriptlang.org/docs/handbook/literal-types.html). * [Type alias](https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-aliases). * Sort in alphabetical order whenever possible. ================================================ FILE: src/contract/SlugifyMode.ts ================================================ /** * Slugify mode. */ export const enum SlugifyMode { /** Azure DevOps */ AzureDevOps = "azureDevops", /** Bitbucket Cloud */ BitbucketCloud = "bitbucket-cloud", /** Gitea */ Gitea = "gitea", /** GitHub */ GitHub = "github", /** GitLab */ GitLab = "gitlab", /** Visual Studio Code */ VisualStudioCode = "vscode", /** Zola */ Zola = "zola", } export default SlugifyMode; ================================================ FILE: src/contract/VisualStudioCodeLocaleId.ts ================================================ "use strict"; /** * Visual Studio Code Locale ID. * @see <https://code.visualstudio.com/docs/getstarted/locales#_available-locales> * @see <https://github.com/microsoft/vscode-loc> */ const enum VisualStudioCodeLocaleId { Bulgarian = "bg", ChineseSimplified = "zh-cn", ChineseTraditional = "zh-tw", Czech = "cs", English = "en", French = "fr", German = "de", Hungarian = "hu", Italian = "it", Japanese = "ja", Korean = "ko", PortugueseBrazil = "pt-br", Russian = "ru", Spanish = "es", Turkish = "tr", } export default VisualStudioCodeLocaleId; ================================================ FILE: src/editor-context-service/context-service-in-fenced-code-block.ts ================================================ 'use strict' import { ExtensionContext, Position, TextDocument, window } from 'vscode'; import { AbsContextService } from "./i-context-service"; import { isInFencedCodeBlock } from "../util/contextCheck"; export class ContextServiceEditorInFencedCodeBlock extends AbsContextService { public contextName: string = "markdown.extension.editor.cursor.inFencedCodeBlock"; public onActivate(_context: ExtensionContext) { // set initial state of context this.setState(false); } public dispose(): void { } public onDidChangeActiveTextEditor(document: TextDocument, cursorPos: Position) { this.updateContextState(document, cursorPos); } public onDidChangeTextEditorSelection(document: TextDocument, cursorPos: Position) { this.updateContextState(document, cursorPos); } private updateContextState(document: TextDocument, cursorPos: Position) { if (isInFencedCodeBlock(document, cursorPos.line)) { this.setState(true); } else { this.setState(false); } return; } } ================================================ FILE: src/editor-context-service/context-service-in-list.ts ================================================ 'use strict' import { ExtensionContext, Position, TextDocument, window } from 'vscode'; import { AbsContextService } from "./i-context-service"; export class ContextServiceEditorInList extends AbsContextService { public contextName: string = "markdown.extension.editor.cursor.inList"; public onActivate(_context: ExtensionContext) { // set initial state of context this.setState(false); } public dispose(): void { } public onDidChangeActiveTextEditor(document: TextDocument, cursorPos: Position) { this.updateContextState(document, cursorPos); } public onDidChangeTextEditorSelection(document: TextDocument, cursorPos: Position) { this.updateContextState(document, cursorPos); } private updateContextState(document: TextDocument, cursorPos: Position) { let lineText = document.lineAt(cursorPos.line).text; let inList = /^\s*([-+*]|[0-9]+[.)]) +(\[[ x]\] +)?/.test(lineText); if (inList) { this.setState(true); } else { this.setState(false); } return; } } ================================================ FILE: src/editor-context-service/context-service-in-math-env.ts ================================================ 'use strict' import { ExtensionContext, Position, TextDocument, window } from 'vscode'; import { AbsContextService } from "./i-context-service"; import { mathEnvCheck } from "../util/contextCheck"; export class ContextServiceEditorInMathEn extends AbsContextService { public contextName: string = "markdown.extension.editor.cursor.inMathEnv"; public onActivate(_context: ExtensionContext) { // set initial state of context this.setState(false); } public dispose(): void { } public onDidChangeActiveTextEditor(document: TextDocument, cursorPos: Position) { this.updateContextState(document, cursorPos); } public onDidChangeTextEditorSelection(document: TextDocument, cursorPos: Position) { this.updateContextState(document, cursorPos); } private updateContextState(document: TextDocument, cursorPos: Position) { if (mathEnvCheck(document, cursorPos)) { this.setState(true); } else { this.setState(false); } return; } } ================================================ FILE: src/editor-context-service/i-context-service.ts ================================================ 'use strict' import { commands, ExtensionContext, Position, TextDocument } from 'vscode'; import type IDisposable from "../IDisposable"; interface IContextService extends IDisposable { onActivate(context: ExtensionContext): void; /** * handler of onDidChangeActiveTextEditor * implement this method to handle that event to update context state */ onDidChangeActiveTextEditor(document: TextDocument, cursorPos: Position): void; /** * handler of onDidChangeTextEditorSelection * implement this method to handle that event to update context state */ onDidChangeTextEditorSelection(document: TextDocument, cursorPos: Position): void; } export abstract class AbsContextService implements IContextService { public abstract readonly contextName: string; /** * activate context service * @param context ExtensionContext */ public abstract onActivate(context: ExtensionContext): void; public abstract dispose(): void; /** * default handler of onDidChangeActiveTextEditor, do nothing. * override this method to handle that event to update context state. */ public abstract onDidChangeActiveTextEditor(document: TextDocument, cursorPos: Position): void; /** * default handler of onDidChangeTextEditorSelection, do nothing. * override this method to handle that event to update context state. */ public abstract onDidChangeTextEditorSelection(document: TextDocument, cursorPos: Position): void; /** * set state of context */ protected setState(state: any) { commands.executeCommand('setContext', this.contextName, state); } } ================================================ FILE: src/editor-context-service/manager.ts ================================================ 'use strict' import type IDisposable from "../IDisposable"; import { ExtensionContext, window } from 'vscode'; import { AbsContextService } from "./i-context-service"; import { ContextServiceEditorInList } from "./context-service-in-list"; import { ContextServiceEditorInFencedCodeBlock } from "./context-service-in-fenced-code-block"; import { ContextServiceEditorInMathEn } from "./context-service-in-math-env"; export class ContextServiceManager implements IDisposable { private readonly contextServices: Array<AbsContextService> = []; public constructor() { // push context services this.contextServices.push(new ContextServiceEditorInList()); this.contextServices.push(new ContextServiceEditorInFencedCodeBlock()); this.contextServices.push(new ContextServiceEditorInMathEn()); } public activate(context: ExtensionContext) { for (const service of this.contextServices) { service.onActivate(context); } // subscribe update handler for context context.subscriptions.push( window.onDidChangeActiveTextEditor(() => this.onDidChangeActiveTextEditor()), window.onDidChangeTextEditorSelection(() => this.onDidChangeTextEditorSelection()) ); // initialize context state this.onDidChangeActiveTextEditor(); } public dispose(): void { while (this.contextServices.length > 0) { const service = this.contextServices.pop(); service!.dispose(); } } private onDidChangeActiveTextEditor() { const editor = window.activeTextEditor; if (editor === undefined) { return; } const cursorPos = editor.selection.start; const document = editor.document; for (const service of this.contextServices) { service.onDidChangeActiveTextEditor(document, cursorPos); } } private onDidChangeTextEditorSelection() { const editor = window.activeTextEditor; if (editor === undefined) { return; } const cursorPos = editor.selection.start; const document = editor.document; for (const service of this.contextServices) { service.onDidChangeTextEditorSelection(document, cursorPos); } } } export const contextServiceManager = new ContextServiceManager(); ================================================ FILE: src/extension.ts ================================================ 'use strict'; import { ExtensionContext, languages, Uri, window, workspace } from 'vscode'; import { configManager } from "./configuration/manager"; import { contextServiceManager } from "./editor-context-service/manager" import { decorationManager } from "./theming/decorationManager"; import * as completion from './completion'; import * as formatting from './formatting'; import * as listEditing from './listEditing'; import { commonMarkEngine, MarkdownIt, mdEngine } from "./markdownEngine"; import { extendMarkdownIt } from "./markdown-it-plugin-provider"; import { config as configNls, localize } from './nls'; import resolveResource from "./nls/resolveResource"; import * as preview from './preview'; import * as print from './print'; import * as tableFormatter from './tableFormatter'; import * as toc from './toc'; import { importZolaSlug } from './util/slugify'; export function activate(context: ExtensionContext) { configNls({ extensionContext: context }); context.subscriptions.push( configManager, contextServiceManager, decorationManager, commonMarkEngine, mdEngine ); // wasm modules need to be imported asynchronously (or any modules relying on them synchronously need to be imported asynchronously) importZolaSlug().then(() => { // we need to wait for the wasm module to be loaded before we can use it, it should only take a few milliseconds // if we move the activateMdExt function outside of this promise, slugify might be called before the wasm module has loaded which will cause it to fail activateMdExt(context); }); return { extendMarkdownIt }; } function activateMdExt(context: ExtensionContext) { // Context services contextServiceManager.activate(context); // Override `Enter`, `Tab` and `Backspace` keys listEditing.activate(context); // Shortcuts formatting.activate(context); // Toc toc.activate(context); // Images paths and math commands completions completion.activate(context); // Print to PDF print.activate(context); // Table formatter tableFormatter.activate(context); // Auto show preview to side preview.activate(context); // Allow `*` in word pattern for quick styling (toggle bold/italic without selection) // original https://github.com/microsoft/vscode/blob/3e5c7e2c570a729e664253baceaf443b69e82da6/extensions/markdown-basics/language-configuration.json#L55 languages.setLanguageConfiguration('markdown', { wordPattern: /([*_]{1,2}|~~|`+)?[\p{Alphabetic}\p{Number}\p{Nonspacing_Mark}]+(_+[\p{Alphabetic}\p{Number}\p{Nonspacing_Mark}]+)*\1/gu }); showWelcome(context); } /** * Shows a welcome message on first time startup. */ async function showWelcome(context: ExtensionContext): Promise<void> { const welcomeDirUri = Uri.joinPath(context.extensionUri, "welcome"); // The directory for an extension is recreated every time VS Code installs it. // Thus, we only need to read and write an empty flag file there. // If the file exists, then it's not the first time, and we don't need to do anything. const flagFileUri = Uri.joinPath(welcomeDirUri, "WELCOMED"); try { await workspace.fs.stat(flagFileUri); return; } catch { workspace.fs.writeFile(flagFileUri, new Uint8Array()).then(() => { }, () => { }); } // The existence of welcome materials depends on build options we set during pre-publish. // If any condition is not met, then we don't need to do anything. try { // Confirm the message is valid. // `locale` should be a string. But here we keep it `any` to suppress type checking. const locale: any = JSON.parse(process.env.VSCODE_NLS_CONFIG as string).locale; const welcomeMessageFileUri = Uri.file(resolveResource(welcomeDirUri.fsPath, "", ".txt", [locale, "en"], "")![0]); const msgWelcome = Buffer.from(await workspace.fs.readFile(welcomeMessageFileUri)).toString("utf8"); if (/^\s*$/.test(msgWelcome) || /\p{C}/u.test(msgWelcome)) { return; } // Confirm the file exists. const changelogFileUri = Uri.joinPath(context.extensionUri, "changes.md"); await workspace.fs.stat(changelogFileUri); const btnDismiss = localize("ui.welcome.buttonDismiss"); const btnOpenLocal = localize("ui.welcome.buttonOpenLocal"); window.showInformationMessage(msgWelcome, btnOpenLocal, btnDismiss).then(selection => { switch (selection) { case btnOpenLocal: workspace.openTextDocument(changelogFileUri).then(window.showTextDocument); return; } }); } catch { } } export function deactivate() { } ================================================ FILE: src/formatting.ts ================================================ 'use strict'; import { commands, env, ExtensionContext, Position, Range, Selection, SnippetString, TextDocument, TextEditor, window, workspace, WorkspaceEdit } from 'vscode'; import { fixMarker } from './listEditing'; export function activate(context: ExtensionContext) { context.subscriptions.push( commands.registerCommand('markdown.extension.editing.toggleBold', () => toggleEmphasis(EmphasisType.BOLD)), commands.registerCommand('markdown.extension.editing.toggleItalic', () => toggleEmphasis(EmphasisType.ITALIC)), commands.registerCommand('markdown.extension.editing.toggleCodeSpan', toggleCodeSpan), commands.registerCommand('markdown.extension.editing.toggleStrikethrough', toggleStrikethrough), commands.registerCommand('markdown.extension.editing.toggleMath', () => toggleMath(transTable)), commands.registerCommand('markdown.extension.editing.toggleMathReverse', () => toggleMath(reverseTransTable)), commands.registerCommand('markdown.extension.editing.toggleHeadingUp', toggleHeadingUp), commands.registerCommand('markdown.extension.editing.toggleHeadingDown', toggleHeadingDown), commands.registerCommand('markdown.extension.editing.toggleList', toggleList), commands.registerCommand('markdown.extension.editing.toggleCodeBlock', toggleCodeBlock), commands.registerCommand('markdown.extension.editing.paste', paste), commands.registerCommand('markdown.extension.editing._wrapBy', args => styleByWrapping(args['before'], args['after'])) ); } /** * Here we store Regexp to check if the text is the single link. */ const singleLinkRegex: RegExp = createLinkRegex(); // Return Promise because need to chain operations in unit tests enum EmphasisType { ITALIC, BOLD } function toggleEmphasis(type: EmphasisType) { let indicator = workspace.getConfiguration('markdown.extension.' + EmphasisType[type].toLowerCase()).get<string>('indicator')!; return styleByWrapping(indicator); } function toggleCodeSpan() { return styleByWrapping('`'); } function toggleCodeBlock() { const editor = window.activeTextEditor!; return editor.insertSnippet(new SnippetString('```$0\n$TM_SELECTED_TEXT\n```')); } function toggleStrikethrough() { return styleByWrapping('~~'); } async function toggleHeadingUp() { const editor = window.activeTextEditor!; let lineIndex = editor.selection.active.line; let lineText = editor.document.lineAt(lineIndex).text; return await editor.edit((editBuilder) => { if (!lineText.startsWith('#')) { // Not a heading editBuilder.insert(new Position(lineIndex, 0), '# '); } else if (!lineText.startsWith('######')) { // Already a heading (but not level 6) editBuilder.insert(new Position(lineIndex, 0), '#'); } }); } function toggleHeadingDown() { const editor = window.activeTextEditor!; let lineIndex = editor.selection.active.line; let lineText = editor.document.lineAt(lineIndex).text; editor.edit((editBuilder) => { if (lineText.startsWith('# ')) { // Heading level 1 editBuilder.delete(new Range(new Position(lineIndex, 0), new Position(lineIndex, 2))); } else if (lineText.startsWith('#')) { // Heading (but not level 1) editBuilder.delete(new Range(new Position(lineIndex, 0), new Position(lineIndex, 1))); } }); } enum MathBlockState { // State 1: not in any others states NONE, // State 2: $|$ INLINE, // State 3: $$ | $$ SINGLE_DISPLAYED, // State 4: // $$ // | // $$ MULTI_DISPLAYED } function getMathState(editor: TextEditor, cursor: Position): MathBlockState { if (getContext(editor, cursor, '$', '$') === '$|$') { return MathBlockState.INLINE; } else if (getContext(editor, cursor, '$$ ', ' $$') === '$$ | $$') { return MathBlockState.SINGLE_DISPLAYED; } else if ( editor.document.lineAt(cursor.line).text === '' && cursor.line > 0 && editor.document.lineAt(cursor.line - 1).text.endsWith('$$') && cursor.line < editor.document.lineCount - 1 && editor.document.lineAt(cursor.line + 1).text.startsWith('$$') ) { return MathBlockState.MULTI_DISPLAYED } else { return MathBlockState.NONE; } } /** * Modify the document, change from `oldMathBlockState` to `newMathBlockState`. * @param editor * @param cursor * @param oldMathBlockState * @param newMathBlockState */ function setMathState(editor: TextEditor, cursor: Position, oldMathBlockState: MathBlockState, newMathBlockState: MathBlockState) { // Step 1: Delete old math block. editor.edit(editBuilder => { let rangeToBeDeleted: Range switch (oldMathBlockState) { case MathBlockState.NONE: rangeToBeDeleted = new Range(cursor, cursor); break; case MathBlockState.INLINE: rangeToBeDeleted = new Range(new Position(cursor.line, cursor.character - 1), new Position(cursor.line, cursor.character + 1)); break; case MathBlockState.SINGLE_DISPLAYED: rangeToBeDeleted = new Range(new Position(cursor.line, cursor.character - 3), new Position(cursor.line, cursor.character + 3)); break; case MathBlockState.MULTI_DISPLAYED: const startCharIndex = editor.document.lineAt(cursor.line - 1).text.length - 2; rangeToBeDeleted = new Range(new Position(cursor.line - 1, startCharIndex), new Position(cursor.line + 1, 2)); break; } editBuilder.delete(rangeToBeDeleted) }).then(() => { // Step 2: Insert new math block. editor.edit(editBuilder => { let newCursor = editor.selection.active; let stringToBeInserted: string switch (newMathBlockState) { case MathBlockState.NONE: stringToBeInserted = '' break; case MathBlockState.INLINE: stringToBeInserted = '$$' break; case MathBlockState.SINGLE_DISPLAYED: stringToBeInserted = '$$ $$' break; case MathBlockState.MULTI_DISPLAYED: stringToBeInserted = '$$\n\n$$' break; } editBuilder.insert(newCursor, stringToBeInserted); }).then(() => { // Step 3: Move cursor to the middle. let newCursor = editor.selection.active; let newPosition: Position; switch (newMathBlockState) { case MathBlockState.NONE: newPosition = newCursor break; case MathBlockState.INLINE: newPosition = newCursor.with(newCursor.line, newCursor.character - 1) break; case MathBlockState.SINGLE_DISPLAYED: newPosition = newCursor.with(newCursor.line, newCursor.character - 3) break; case MathBlockState.MULTI_DISPLAYED: newPosition = newCursor.with(newCursor.line - 1, 0) break; } editor.selection = new Selection(newPosition, newPosition); }) }); } const transTable = [ MathBlockState.NONE, MathBlockState.INLINE, MathBlockState.MULTI_DISPLAYED, MathBlockState.SINGLE_DISPLAYED ]; const reverseTransTable = new Array<MathBlockState>(...transTable).reverse(); function toggleMath(transTable: MathBlockState[]) { const editor = window.activeTextEditor!; if (!editor.selection.isEmpty) return; let cursor = editor.selection.active; let oldMathBlockState = getMathState(editor, cursor) let currentStateIndex = transTable.indexOf(oldMathBlockState); setMathState(editor, cursor, oldMathBlockState, transTable[(currentStateIndex + 1) % transTable.length]) } function toggleList() { const editor = window.activeTextEditor!; const doc = editor.document; let batchEdit = new WorkspaceEdit(); for (const selection of editor.selections) { if (selection.isEmpty) { toggleListSingleLine(doc, selection.active.line, batchEdit); } else { for (let i = selection.start.line; i <= selection.end.line; i++) { toggleListSingleLine(doc, i, batchEdit); } } } return workspace.applyEdit(batchEdit).then(() => fixMarker(editor)); } function toggleListSingleLine(doc: TextDocument, line: number, wsEdit: WorkspaceEdit) { const lineText = doc.lineAt(line).text; const indentation = lineText.trim().length === 0 ? lineText.length : lineText.indexOf(lineText.trim()); const lineTextContent = lineText.slice(indentation); const currentMarker = getCurrentListStart(lineTextContent); const nextMarker = getNextListStart(currentMarker); // 1. delete current list marker wsEdit.delete(doc.uri, new Range(line, indentation, line, getMarkerEndCharacter(currentMarker, lineText))); // 2. insert next list marker if (nextMarker !== ListMarker.EMPTY) wsEdit.insert(doc.uri, new Position(line, indentation), nextMarker); } /** * List candidate markers enum */ enum ListMarker { EMPTY = "", DASH = "- ", STAR = "* ", PLUS = "+ ", NUM = "1. ", NUM_CLOSING_PARETHESES = "1) " } function getListMarker(listMarker: string): ListMarker { if ("- " === listMarker) { return ListMarker.DASH; } else if ("* " === listMarker) { return ListMarker.STAR; } else if ("+ " === listMarker) { return ListMarker.PLUS; } else if ("1. " === listMarker) { return ListMarker.NUM; } else if ("1) " === listMarker) { return ListMarker.NUM_CLOSING_PARETHESES; } else { return ListMarker.EMPTY; } } const listMarkerSimpleListStart = [ListMarker.DASH, ListMarker.STAR, ListMarker.PLUS] const listMarkerDefaultMarkerArray = [ListMarker.DASH, ListMarker.STAR, ListMarker.PLUS, ListMarker.NUM, ListMarker.NUM_CLOSING_PARETHESES] const listMarkerNumRegex = /^\d+\. /; const listMarkerNumClosingParethesesRegex = /^\d+\) /; function getMarkerEndCharacter(currentMarker: ListMarker, lineText: string): number { const indentation = lineText.trim().length === 0 ? lineText.length : lineText.indexOf(lineText.trim()); const lineTextContent = lineText.slice(indentation); let endCharacter = indentation; if (listMarkerSimpleListStart.includes(currentMarker)) { // `- `, `* `, `+ ` endCharacter += 2; } else if (listMarkerNumRegex.test(lineTextContent)) { // number const lenOfDigits = /^(\d+)\./.exec(lineText.trim())![1].length; endCharacter += lenOfDigits + 2; } else if (listMarkerNumClosingParethesesRegex.test(lineTextContent)) { // number with ) const lenOfDigits = /^(\d+)\)/.exec(lineText.trim())![1].length; endCharacter += lenOfDigits + 2; } return endCharacter; } /** * get list start marker */ function getCurrentListStart(lineTextContent: string): ListMarker { if (lineTextContent.startsWith(ListMarker.DASH)) { return ListMarker.DASH; } else if (lineTextContent.startsWith(ListMarker.STAR)) { return ListMarker.STAR; } else if (lineTextContent.startsWith(ListMarker.PLUS)) { return ListMarker.PLUS; } else if (listMarkerNumRegex.test(lineTextContent)) { return ListMarker.NUM; } else if (listMarkerNumClosingParethesesRegex.test(lineTextContent)) { return ListMarker.NUM_CLOSING_PARETHESES; } else { return ListMarker.EMPTY; } } /** * get next candidate marker from configArray */ function getNextListStart(current: ListMarker): ListMarker { const configArray = getCandidateMarkers(); let next = configArray[0]; const index = configArray.indexOf(current); if (index >= 0 && index < configArray.length - 1) next = configArray[index + 1]; return next; } /** * get candidate markers array from configuration */ function getCandidateMarkers(): ListMarker[] { // read configArray from configuration and append space let configArray = workspace.getConfiguration('markdown.extension.list.toggle').get<string[]>('candidate-markers'); if (!(configArray instanceof Array)) return listMarkerDefaultMarkerArray; // append a space after trim, markers must end with a space and remove unknown markers let listMarkerArray = configArray.map((e) => getListMarker(e + " ")).filter((e) => listMarkerDefaultMarkerArray.includes(e)); // push empty in the configArray for init status without list marker listMarkerArray.push(ListMarker.EMPTY); return listMarkerArray; } async function paste() { const editor = window.activeTextEditor!; const selection = editor.selection; if (selection.isSingleLine && !isSingleLink(editor.document.getText(selection))) { const text = await env.clipboard.readText(); const textTrimmed = text.trim(); if (isSingleLink(textTrimmed)) { return commands.executeCommand("editor.action.insertSnippet", { "snippet": `[$TM_SELECTED_TEXT$0](${textTrimmed})` }); } } return commands.executeCommand("editor.action.clipboardPasteAction"); } /** * Creates Regexp to check if the text is a link (further detailes in the isSingleLink() documentation). * * @return Regexp */ function createLinkRegex(): RegExp { // unicode letters range(must not be a raw string) const ul = '\\u00a1-\\uffff'; // IP patterns const ipv4_re = '(?:25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}'; const ipv6_re = '\\[[0-9a-f:\\.]+\\]'; // simple regex (in django it is validated additionally) // Host patterns const hostname_re = '[a-z' + ul + '0-9](?:[a-z' + ul + '0-9-]{0,61}[a-z' + ul + '0-9])?'; // Max length for domain name labels is 63 characters per RFC 1034 sec. 3.1 const domain_re = '(?:\\.(?!-)[a-z' + ul + '0-9-]{1,63}(?<!-))*'; const tld_re = '' + '\\.' // dot + '(?!-)' // can't start with a dash + '(?:[a-z' + ul + '-]{2,63}' // domain label + '|xn--[a-z0-9]{1,59})' // or punycode label + '(?<!-)' // can't end with a dash + '\\.?' // may have a trailing dot ; const host_re = '(' + hostname_re + domain_re + tld_re + '|localhost)'; const pattern = '' + '^(?:[a-z0-9\\.\\-\\+]*)://' // scheme is not validated (in django it is validated additionally) + '(?:[^\\s:@/]+(?::[^\\s:@/]*)?@)?' // user: pass authentication + '(?:' + ipv4_re + '|' + ipv6_re + '|' + host_re + ')' + '(?::\\d{2,5})?' // port + '(?:[/?#][^\\s]*)?' // resource path + '$' // end of string ; return new RegExp(pattern, 'i'); } /** * Checks if the string is a link. The list of link examples you can see in the tests file * `test/linksRecognition.test.ts`. This code ported from django's * [URLValidator](https://github.com/django/django/blob/2.2b1/django/core/validators.py#L74) with some simplifyings. * * @param text string to check * * @return boolean */ export function isSingleLink(text: string): boolean { return singleLinkRegex.test(text); } // Read PR #1052 before touching this please! function styleByWrapping(startPattern: string, endPattern = startPattern) { const editor = window.activeTextEditor!; let selections = editor.selections; let batchEdit = new WorkspaceEdit(); let shifts: [Position, number][] = []; let newSelections: Selection[] = selections.slice(); for (const [i, selection] of selections.entries()) { let cursorPos = selection.active; const shift = shifts.map(([pos, s]) => (selection.start.line == pos.line && selection.start.character >= pos.character) ? s : 0) .reduce((a, b) => a + b, 0); if (selection.isEmpty) { const context = getContext(editor, cursorPos, startPattern, endPattern); // No selected text if ( startPattern === endPattern && ["**", "*", "__", "_"].includes(startPattern) && context === `${startPattern}text|${endPattern}` ) { // `**text|**` to `**text**|` let newCursorPos = cursorPos.with({ character: cursorPos.character + shift + endPattern.length }); newSelections[i] = new Selection(newCursorPos, newCursorPos); continue; } else if (context === `${startPattern}|${endPattern}`) { // `**|**` to `|` let start = cursorPos.with({ character: cursorPos.character - startPattern.length }); let end = cursorPos.with({ character: cursorPos.character + endPattern.length }); wrapRange(editor, batchEdit, shifts, newSelections, i, shift, cursorPos, new Range(start, end), false, startPattern, endPattern); } else { // Select word under cursor let wordRange = editor.document.getWordRangeAtPosition(cursorPos); if (wordRange == undefined) { wordRange = selection; } // One special case: toggle strikethrough in task list const currentTextLine = editor.document.lineAt(cursorPos.line); if (startPattern === '~~' && /^\s*[\*\+\-] (\[[ x]\] )? */g.test(currentTextLine.text)) { wordRange = currentTextLine.range.with(new Position(cursorPos.line, currentTextLine.text.match(/^\s*[\*\+\-] (\[[ x]\] )? */g)![0].length)); } wrapRange(editor, batchEdit, shifts, newSelections, i, shift, cursorPos, wordRange, false, startPattern, endPattern); } } else { // Text selected wrapRange(editor, batchEdit, shifts, newSelections, i, shift, cursorPos, selection, true, startPattern, endPattern); } } return workspace.applyEdit(batchEdit).then(() => { editor.selections = newSelections; }); } /** * Add or remove `startPattern`/`endPattern` according to the context * @param editor * @param options The undo/redo behavior * @param cursor cursor position * @param range range to be replaced * @param isSelected is this range selected * @param startPtn * @param endPtn */ function wrapRange(editor: TextEditor, wsEdit: WorkspaceEdit, shifts: [Position, number][], newSelections: Selection[], i: number, shift: number, cursor: Position, range: Range, isSelected: boolean, startPtn: string, endPtn: string) { let text = editor.document.getText(range); const prevSelection = newSelections[i]; const ptnLength = (startPtn + endPtn).length; let newCursorPos = cursor.with({ character: cursor.character + shift }); let newSelection: Selection; if (isWrapped(text, startPtn, endPtn)) { // remove start/end patterns from range wsEdit.replace(editor.document.uri, range, text.substr(startPtn.length, text.length - ptnLength)); shifts.push([range.end, -ptnLength]); // Fix cursor position if (!isSelected) { if (!range.isEmpty) { // means quick styling if (cursor.character == range.end.character) { newCursorPos = cursor.with({ character: cursor.character + shift - ptnLength }); } else { newCursorPos = cursor.with({ character: cursor.character + shift - startPtn.length }); } } else { // means `**|**` -> `|` newCursorPos = cursor.with({ character: cursor.character + shift + startPtn.length }); } newSelection = new Selection(newCursorPos, newCursorPos); } else { newSelection = new Selection( prevSelection.start.with({ character: prevSelection.start.character + shift }), prevSelection.end.with({ character: prevSelection.end.character + shift - ptnLength }) ); } } else { // add start/end patterns around range wsEdit.replace(editor.document.uri, range, startPtn + text + endPtn); shifts.push([range.end, ptnLength]); // Fix cursor position if (!isSelected) { if (!range.isEmpty) { // means quick styling if (cursor.character == range.end.character) { newCursorPos = cursor.with({ character: cursor.character + shift + ptnLength }); } else { newCursorPos = cursor.with({ character: cursor.character + shift + startPtn.length }); } } else { // means `|` -> `**|**` newCursorPos = cursor.with({ character: cursor.character + shift + startPtn.length }); } newSelection = new Selection(newCursorPos, newCursorPos); } else { newSelection = new Selection( prevSelection.start.with({ character: prevSelection.start.character + shift }), prevSelection.end.with({ character: prevSelection.end.character + shift + ptnLength }) ); } } newSelections[i] = newSelection; } function isWrapped(text: string, startPattern: string, endPattern: string): boolean { return text.startsWith(startPattern) && text.endsWith(endPattern); } function getContext(editor: TextEditor, cursorPos: Position, startPattern: string, endPattern: string): string { let startPositionCharacter = cursorPos.character - startPattern.length; let endPositionCharacter = cursorPos.character + endPattern.length; if (startPositionCharacter < 0) { startPositionCharacter = 0; } let leftText = editor.document.getText(new Range(cursorPos.line, startPositionCharacter, cursorPos.line, cursorPos.character)); let rightText = editor.document.getText(new Range(cursorPos.line, cursorPos.character, cursorPos.line, endPositionCharacter)); if (rightText == endPattern) { if (leftText == startPattern) { return `${startPattern}|${endPattern}`; } else { return `${startPattern}text|${endPattern}`; } } return '|'; } ================================================ FILE: src/listEditing.ts ================================================ import { commands, ExtensionContext, Position, Range, Selection, TextEditor, window, workspace, WorkspaceEdit } from 'vscode'; import { isInFencedCodeBlock, mathEnvCheck } from "./util/contextCheck"; type IModifier = "ctrl" | "shift"; export function activate(context: ExtensionContext) { context.subscriptions.push( commands.registerCommand('markdown.extension.onEnterKey', onEnterKey), commands.registerCommand('markdown.extension.onCtrlEnterKey', () => { return onEnterKey('ctrl'); }), commands.registerCommand('markdown.extension.onShiftEnterKey', () => { return onEnterKey('shift'); }), commands.registerCommand('markdown.extension.onTabKey', onTabKey), commands.registerCommand('markdown.extension.onShiftTabKey', () => { return onTabKey('shift'); }), commands.registerCommand('markdown.extension.onBackspaceKey', onBackspaceKey), commands.registerCommand('markdown.extension.checkTaskList', checkTaskList), commands.registerCommand('markdown.extension.onMoveLineDown', onMoveLineDown), commands.registerCommand('markdown.extension.onMoveLineUp', onMoveLineUp), commands.registerCommand('markdown.extension.onCopyLineDown', onCopyLineDown), commands.registerCommand('markdown.extension.onCopyLineUp', onCopyLineUp), commands.registerCommand('markdown.extension.onIndentLines', onIndentLines), commands.registerCommand('markdown.extension.onOutdentLines', onOutdentLines) ); } // The commands here are only bound to keys with `when` clause containing `editorTextFocus && !editorReadonly`. (package.json) // So we don't need to check whether `activeTextEditor` returns `undefined` in most cases. function onEnterKey(modifiers?: IModifier) { const editor = window.activeTextEditor!; let cursorPos: Position = editor.selection.active; let line = editor.document.lineAt(cursorPos.line); let textBeforeCursor = line.text.substring(0, cursorPos.character); let textAfterCursor = line.text.substring(cursorPos.character); let lineBreakPos = cursorPos; if (modifiers == 'ctrl') { lineBreakPos = line.range.end; } if (modifiers == 'shift') { return asNormal(editor, 'enter', modifiers); } //// This is a possibility that the current line is a thematic break `<hr>` (GitHub #785) const lineTextNoSpace = line.text.replace(/\s/g, ''); if (lineTextNoSpace.length > 2 && ( lineTextNoSpace.replace(/\-/g, '').length === 0 || lineTextNoSpace.replace(/\*/g, '').length === 0 ) ) { return asNormal(editor, 'enter', modifiers); } //// If it's an empty list item, remove it if ( /^([-+*]|[0-9]+[.)])( +\[[ x]\])?$/.test(textBeforeCursor.trim()) // It is a (task) list item && textAfterCursor.trim().length == 0 // It is empty ) { if (/^\s+([-+*]|[0-9]+[.)]) +(\[[ x]\] )?$/.test(textBeforeCursor)) { // It is not a top-level list item, outdent it return outdent(editor).then(() => fixMarker(editor)); } else if (/^([-+*]|[0-9]+[.)]) $/.test(textBeforeCursor)) { // It is a general list item, delete the list marker return deleteRange(editor, new Range(cursorPos.with({ character: 0 }), cursorPos)).then(() => fixMarker(editor)); } else if (/^([-+*]|[0-9]+[.)]) +(\[[ x]\] )$/.test(textBeforeCursor)) { // It is a task list item, delete the checkbox return deleteRange(editor, new Range(cursorPos.with({ character: textBeforeCursor.length - 4 }), cursorPos)).then(() => fixMarker(editor)); } else { return asNormal(editor, 'enter', modifiers); } } let matches: RegExpExecArray | null; if (/^> /.test(textBeforeCursor)) { // Block quotes // Case 1: ending a blockquote if: const isEmptyArrowLine = line.text.replace(/[ \t]+$/, '') === '>'; if (isEmptyArrowLine) { if (cursorPos.line === 0) { // it is an empty '>' line and also the first line of the document return editor.edit(editorBuilder => { editorBuilder.replace(new Range(new Position(0, 0), new Position(cursorPos.line, cursorPos.character)), ''); }).then(() => { editor.revealRange(editor.selection) }); } else { // there have been 2 consecutive empty `>` lines const prevLineText = editor.document.lineAt(cursorPos.line - 1).text; if (prevLineText.replace(/[ \t]+$/, '') === '>') { return editor.edit(editorBuilder => { editorBuilder.replace(new Range(new Position(cursorPos.line - 1, 0), new Position(cursorPos.line, cursorPos.character)), '\n'); }).then(() => { editor.revealRange(editor.selection) }); } } } // Case 2: `>` continuation return editor.edit(editBuilder => { if (isEmptyArrowLine) { const startPos = new Position(cursorPos.line, line.text.trim().length); editBuilder.delete(new Range(startPos, line.range.end)); lineBreakPos = startPos; } editBuilder.insert(lineBreakPos, `\n> `); }).then(() => { // Fix cursor position if (modifiers == 'ctrl' && !cursorPos.isEqual(lineBreakPos)) { let newCursorPos = cursorPos.with(line.lineNumber + 1, 2); editor.selection = new Selection(newCursorPos, newCursorPos); } }).then(() => { editor.revealRange(editor.selection) }); } else if ((matches = /^((\s*[-+*] +)(\[[ x]\] +)?)/.exec(textBeforeCursor)) !== null) { // satisfy compiler's null check const match0 = matches[0]; const match1 = matches[1]; const match2 = matches[2]; const match3 = matches[3]; // Unordered list return editor.edit(editBuilder => { if ( match3 && // If it is a task list item and match0 === textBeforeCursor && // the cursor is right after the checkbox "- [x] |item1" modifiers !== 'ctrl' ) { // Move the task list item to the next line // - [x] |item1 // ↓ // - [ ] // - [x] |item1 editBuilder.replace(new Range(cursorPos.line, match2.length + 1, cursorPos.line, match2.length + 2), " "); editBuilder.insert(lineBreakPos, `\n${match1}`); } else { // Insert "- [ ]" // - [ ] item1| // ↓ // - [ ] item1 // - [ ] | editBuilder.insert(lineBreakPos, `\n${match1.replace('[x]', '[ ]')}`); } }).then(() => { // Fix cursor position if (modifiers == 'ctrl' && !cursorPos.isEqual(lineBreakPos)) { let newCursorPos = cursorPos.with(line.lineNumber + 1, matches![1].length); editor.selection = new Selection(newCursorPos, newCursorPos); } }).then(() => { editor.revealRange(editor.selection) }); } else if ((matches = /^(\s*)([0-9]+)([.)])( +)((\[[ x]\] +)?)/.exec(textBeforeCursor)) !== null) { // Ordered list let config = workspace.getConfiguration('markdown.extension.orderedList').get<string>('marker'); let marker = '1'; let leadingSpace = matches[1]; let previousMarker = matches[2]; let delimiter = matches[3]; let trailingSpace = matches[4]; let gfmCheckbox = matches[5].replace('[x]', '[ ]'); let textIndent = (previousMarker + delimiter + trailingSpace).length; if (config == 'ordered') { marker = String(Number(previousMarker) + 1); } // Add enough trailing spaces so that the text is aligned with the previous list item, but always keep at least one space trailingSpace = " ".repeat(Math.max(1, textIndent - (marker + delimiter).length)); const toBeAdded = leadingSpace + marker + delimiter + trailingSpace + gfmCheckbox; return editor.edit( editBuilder => { editBuilder.insert(lineBreakPos, `\n${toBeAdded}`); }, { undoStopBefore: true, undoStopAfter: false } ).then(() => { // Fix cursor position if (modifiers == 'ctrl' && !cursorPos.isEqual(lineBreakPos)) { let newCursorPos = cursorPos.with(line.lineNumber + 1, toBeAdded.length); editor.selection = new Selection(newCursorPos, newCursorPos); } }).then(() => fixMarker(editor)).then(() => { editor.revealRange(editor.selection); }); } else { return asNormal(editor, 'enter', modifiers); } } function onTabKey(modifiers?: IModifier) { const editor = window.activeTextEditor!; let cursorPos = editor.selection.start; let lineText = editor.document.lineAt(cursorPos.line).text; let match = /^\s*([-+*]|[0-9]+[.)]) +(\[[ x]\] +)?/.exec(lineText); if ( match && ( modifiers === 'shift' || !editor.selection.isEmpty || editor.selection.isEmpty && cursorPos.character <= match[0].length ) ) { if (modifiers === 'shift') { return outdent(editor).then(() => fixMarker(editor)); } else { return indent(editor).then(() => fixMarker(editor)); } } else { return asNormal(editor, 'tab', modifiers); } } function onBackspaceKey() { const editor = window.activeTextEditor!; let cursor = editor.selection.active; let document = editor.document; let textBeforeCursor = document.lineAt(cursor.line).text.substr(0, cursor.character); if (!editor.selection.isEmpty) { return asNormal(editor, 'backspace').then(() => fixMarker(editor)); } else if (/^\s+([-+*]|[0-9]+[.)]) $/.test(textBeforeCursor)) { // e.g. textBeforeCursor === ` - `, ` 1. ` return outdent(editor).then(() => fixMarker(editor)); } else if (/^([-+*]|[0-9]+[.)]) $/.test(textBeforeCursor)) { // e.g. textBeforeCursor === `- `, `1. ` return editor.edit(editBuilder => { editBuilder.replace(new Range(cursor.with({ character: 0 }), cursor), ' '.repeat(textBeforeCursor.length)) }).then(() => fixMarker(editor)); } else if (/^\s*([-+*]|[0-9]+[.)]) +(\[[ x]\] )$/.test(textBeforeCursor)) { // e.g. textBeforeCursor === `- [ ]`, `1. [x]`, ` - [x]` return deleteRange(editor, new Range(cursor.with({ character: textBeforeCursor.length - 4 }), cursor)).then(() => fixMarker(editor)); } else { return asNormal(editor, 'backspace'); } } function asNormal(editor: TextEditor, key: "backspace" | "enter" | "tab", modifiers?: IModifier) { switch (key) { case 'enter': if (modifiers === 'ctrl') { return commands.executeCommand('editor.action.insertLineAfter'); } else { return commands.executeCommand('type', { source: 'keyboard', text: '\n' }); } case 'tab': if (modifiers === 'shift') { return commands.executeCommand('editor.action.outdentLines'); } else if ( editor.selection.isEmpty && workspace.getConfiguration('emmet').get<boolean>('triggerExpansionOnTab') ) { return commands.executeCommand('editor.emmet.action.expandAbbreviation'); } else { return commands.executeCommand('tab'); } case 'backspace': return commands.executeCommand('deleteLeft'); } } /** * If * * 1. it is not the first line * 2. there is a Markdown list item before this line * * then indent the current line to align with the previous list item. */ function indent(editor: TextEditor) { if (workspace.getConfiguration("markdown.extension.list", editor.document.uri).get<string>("indentationSize") === "adaptive") { try { const selection = editor.selection; const indentationSize = tryDetermineIndentationSize(editor, selection.start.line, editor.document.lineAt(selection.start.line).firstNonWhitespaceCharacterIndex); let edit = new WorkspaceEdit() for (let i = selection.start.line; i <= selection.end.line; i++) { if (i === selection.end.line && !selection.isEmpty && selection.end.character === 0) { break; } if (editor.document.lineAt(i).text.length !== 0) { edit.insert(editor.document.uri, new Position(i, 0), ' '.repeat(indentationSize)); } } return workspace.applyEdit(edit); } catch (error) { } } return commands.executeCommand('editor.action.indentLines'); } /** * Similar to `indent`-function */ function outdent(editor: TextEditor) { if (workspace.getConfiguration("markdown.extension.list", editor.document.uri).get<string>("indentationSize") === "adaptive") { try { const selection = editor.selection; const indentationSize = tryDetermineIndentationSize(editor, selection.start.line, editor.document.lineAt(selection.start.line).firstNonWhitespaceCharacterIndex); let edit = new WorkspaceEdit() for (let i = selection.start.line; i <= selection.end.line; i++) { if (i === selection.end.line && !selection.isEmpty && selection.end.character === 0) { break; } const lineText = editor.document.lineAt(i).text; let maxOutdentSize: number; if (lineText.trim().length === 0) { maxOutdentSize = lineText.length; } else { maxOutdentSize = editor.document.lineAt(i).firstNonWhitespaceCharacterIndex; } if (maxOutdentSize > 0) { edit.delete(editor.document.uri, new Range(i, 0, i, Math.min(indentationSize, maxOutdentSize))); } } return workspace.applyEdit(edit); } catch (error) { } } return commands.executeCommand('editor.action.outdentLines'); } function tryDetermineIndentationSize(editor: TextEditor, line: number, currentIndentation: number) { while (--line >= 0) { const lineText = editor.document.lineAt(line).text; let matches; if ((matches = /^(\s*)(([-+*]|[0-9]+[.)]) +)(\[[ x]\] +)?/.exec(lineText)) !== null) { if (matches[1].length <= currentIndentation) { return matches[2].length; } } } throw "No previous Markdown list item"; } /** * Returns the line index of the next ordered list item starting from the specified line. * * @param line * Defaults to the beginning of the current primary selection (`editor.selection.start.line`) * in order to find the first marker following either the cursor or the entire selected range. */ function findNextMarkerLineNumber(editor: TextEditor, line = editor.selection.start.line): number { while (line < editor.document.lineCount) { const lineText = editor.document.lineAt(line).text; if (lineText.startsWith('#')) { // Don't go searching past any headings return -1; } if (/^\s*[0-9]+[.)] +/.exec(lineText) !== null) { return line; } line++; } return -1; } /** * Looks for the previous ordered list marker at the same indentation level * and returns the marker number that should follow it. * * @param currentIndentation treat tabs as if they were replaced by spaces with a tab stop of 4 characters * * @returns the fixed marker number */ function lookUpwardForMarker(editor: TextEditor, line: number, currentIndentation: number): number { let prevLine = line; while (--prevLine >= 0) { const prevLineText = editor.document.lineAt(prevLine).text.replace(/\t/g, ' '); let matches; if ((matches = /^(\s*)(([0-9]+)[.)] +)/.exec(prevLineText)) !== null) { // The previous line has an ordered list marker const prevLeadingSpace: string = matches[1]; const prevMarker = matches[3]; if (currentIndentation < prevLeadingSpace.length) { // yet to find a sibling item continue; } else if ( currentIndentation >= prevLeadingSpace.length && currentIndentation <= (prevLeadingSpace + prevMarker).length ) { // found a sibling item return Number(prevMarker) + 1; } else if (currentIndentation > (prevLeadingSpace + prevMarker).length) { // found a parent item return 1; } else { // not possible } } else if ((matches = /^(\s*)([-+*] +)/.exec(prevLineText)) !== null) { // The previous line has an unordered list marker const prevLeadingSpace: string = matches[1]; if (currentIndentation >= prevLeadingSpace.length) { // stop finding break; } } else if ((matches = /^(\s*)\S/.exec(prevLineText)) !== null) { // The previous line doesn't have a list marker if (matches[1].length < 3) { // no enough indentation for a list item break; } } } return 1; } /** * Fix ordered list marker *iteratively* starting from current line */ export function fixMarker(editor: TextEditor, line?: number): Thenable<unknown> | void { if (!workspace.getConfiguration('markdown.extension.orderedList').get<boolean>('autoRenumber')) return; if (workspace.getConfiguration('markdown.extension.orderedList').get<string>('marker') == 'one') return; if (line === undefined) { line = findNextMarkerLineNumber(editor); } if (line < 0 || line >= editor.document.lineCount) { return; } let currentLineText = editor.document.lineAt(line).text; let matches; if ((matches = /^(\s*)([0-9]+)([.)])( +)/.exec(currentLineText)) !== null) { // ordered list let leadingSpace = matches[1]; let marker = matches[2]; let delimiter = matches[3]; let trailingSpace = matches[4]; let fixedMarker = lookUpwardForMarker(editor, line, leadingSpace.replace(/\t/g, ' ').length); let listIndent = marker.length + delimiter.length + trailingSpace.length; let fixedMarkerString = String(fixedMarker); return editor.edit( // fix the marker (current line) editBuilder => { if (marker === fixedMarkerString) { return; } // Add enough trailing spaces so that the text is still aligned at the same indentation level as it was previously, but always keep at least one space fixedMarkerString += delimiter + " ".repeat(Math.max(1, listIndent - (fixedMarkerString + delimiter).length)); editBuilder.replace(new Range(line!, leadingSpace.length, line!, leadingSpace.length + listIndent), fixedMarkerString); }, { undoStopBefore: false, undoStopAfter: false } ).then(() => { let nextLine = line! + 1; while (editor.document.lineCount > nextLine) { const nextLineText = editor.document.lineAt(nextLine).text; if (/^\s*[0-9]+[.)] +/.test(nextLineText)) { return fixMarker(editor, nextLine); } else if ( editor.document.lineAt(nextLine - 1).isEmptyOrWhitespace // This line is a block && !nextLineText.startsWith(" ".repeat(3)) // and doesn't have enough indentation && !nextLineText.startsWith("\t") // so terminates the current list. ) { return; } else { nextLine++; } } }); } } function deleteRange(editor: TextEditor, range: Range): Thenable<boolean> { return editor.edit( editBuilder => { editBuilder.delete(range); }, // We will enable undoStop after fixing markers { undoStopBefore: true, undoStopAfter: false } ); } function checkTaskList(): Thenable<unknown> | void { // - Look into selections for lines that could be checked/unchecked. // - The first matching line dictates the new state for all further lines. // - I.e. if the first line is unchecked, only other unchecked lines will // be considered, and vice versa. const editor = window.activeTextEditor!; const uncheckedRegex = /^(\s*([-+*]|[0-9]+[.)]) +\[) \]/ const checkedRegex = /^(\s*([-+*]|[0-9]+[.)]) +\[)x\]/ let toBeToggled: Position[] = [] // all spots that have an "[x]" resp. "[ ]" which should be toggled let newState: boolean | undefined = undefined // true = "x", false = " ", undefined = no matching lines // go through all touched lines of all selections. for (const selection of editor.selections) { for (let i = selection.start.line; i <= selection.end.line; i++) { const line = editor.document.lineAt(i); const lineStart = line.range.start; if (!selection.isSingleLine && (selection.start.isEqual(line.range.end) || selection.end.isEqual(line.range.start))) { continue; } let matches: RegExpExecArray | null; if ( (matches = uncheckedRegex.exec(line.text)) && newState !== false ) { toBeToggled.push(lineStart.with({ character: matches[1].length })); newState = true; } else if ( (matches = checkedRegex.exec(line.text)) && newState !== true ) { toBeToggled.push(lineStart.with({ character: matches[1].length })); newState = false; } } } if (newState !== undefined) { const newChar = newState ? 'x' : ' '; return editor.edit(editBuilder => { for (const pos of toBeToggled) { let range = new Range(pos, pos.with({ character: pos.character + 1 })); editBuilder.replace(range, newChar); } }); } } function onMoveLineUp() { const editor = window.activeTextEditor!; return commands.executeCommand('editor.action.moveLinesUpAction') .then(() => fixMarker(editor)); } function onMoveLineDown() { const editor = window.activeTextEditor!; return commands.executeCommand('editor.action.moveLinesDownAction') .then(() => fixMarker(editor, findNextMarkerLineNumber(editor, editor.selection.start.line - 1))); } function onCopyLineUp() { const editor = window.activeTextEditor!; return commands.executeCommand('editor.action.copyLinesUpAction') .then(() => fixMarker(editor)); } function onCopyLineDown() { const editor = window.activeTextEditor!; return commands.executeCommand('editor.action.copyLinesDownAction') .then(() => fixMarker(editor)); } function onIndentLines() { const editor = window.activeTextEditor!; return indent(editor).then(() => fixMarker(editor)); } function onOutdentLines() { const editor = window.activeTextEditor!; return outdent(editor).then(() => fixMarker(editor)); } export function deactivate() { } ================================================ FILE: src/markdown-it-plugin-provider.ts ================================================ import type { KatexOptions } from "katex"; import MarkdownIt = require("markdown-it"); import { configManager } from "./configuration/manager"; const katexOptions: KatexOptions = { throwOnError: false }; /** * https://code.visualstudio.com/api/extension-guides/markdown-extension#adding-support-for-new-syntax-with-markdownit-plugins */ export function extendMarkdownIt(md: MarkdownIt): MarkdownIt { md.use(require("markdown-it-task-lists"), {enabled: true}); md.use(require("markdown-it-github-alerts"), { matchCaseSensitive: false }) if (configManager.get("math.enabled")) { // We need side effects. (#521) require("katex/contrib/mhchem"); // Deep copy, as KaTeX needs a normal mutable object. <https://katex.org/docs/options.html> const macros: KatexOptions["macros"] = JSON.parse(JSON.stringify(configManager.get("katex.macros"))); if (Object.keys(macros).length === 0) { delete katexOptions["macros"]; } else { katexOptions["macros"] = macros; } md.use(require("@neilsustc/markdown-it-katex"), katexOptions); } return md; } ================================================ FILE: src/markdownEngine.ts ================================================ //// <https://github.com/microsoft/vscode/blob/master/extensions/markdown-language-features/src/markdownEngine.ts> import * as vscode from "vscode"; import MarkdownIt = require("markdown-it"); import Token = require("markdown-it/lib/token"); import type IDisposable from "./IDisposable"; import { slugify } from "./util/slugify"; import { getMarkdownContributionProvider } from './markdownExtensions'; import { extendMarkdownIt } from "./markdown-it-plugin-provider"; import { isMdDocument } from "./util/generic"; // To help consumers. export type { MarkdownIt, Token }; /** * Represents the parsing result of a document. * An instance of this kind should be **shallow immutable**. */ export interface DocumentToken { readonly document: vscode.TextDocument; /** * The markdown-it environment sandbox. */ readonly env: object; /** * The list of markdown-it block tokens. */ readonly tokens: readonly Token[]; /** * The document version number when parsing it. */ readonly version: number; } interface IMarkdownEngine extends IDisposable { /** * Parses the document. */ getDocumentToken(document: vscode.TextDocument): DocumentToken | Thenable<DocumentToken>; /** * Gets the markdown-it instance that this engine holds asynchronously. */ getEngine(): Thenable<MarkdownIt>; } export interface IDynamicMarkdownEngine extends IMarkdownEngine { getDocumentToken(document: vscode.TextDocument): Thenable<DocumentToken>; } export interface IStaticMarkdownEngine extends IMarkdownEngine { /** * The markdown-it instance that this engine holds. */ readonly engine: MarkdownIt; getDocumentToken(document: vscode.TextDocument): DocumentToken; /** * This is for interface consistency. * As a static engine, it is recommended to read the `engine` property. */ getEngine(): Thenable<MarkdownIt>; } /** * A strict CommonMark only engine powered by `markdown-it`. */ class CommonMarkEngine implements IStaticMarkdownEngine { private readonly _disposables: vscode.Disposable[]; private readonly _documentTokenCache = new Map<vscode.TextDocument, DocumentToken>(); private readonly _engine: MarkdownIt; public get engine(): MarkdownIt { return this._engine; } constructor() { this._engine = new MarkdownIt('commonmark'); this._disposables = [ vscode.workspace.onDidCloseTextDocument(document => { if (isMdDocument(document)) { this._documentTokenCache.delete(document); } }), ]; } public dispose(): void { // Unsubscribe event listeners. for (const disposable of this._disposables) { disposable.dispose(); } this._disposables.length = 0; } public getDocumentToken(document: vscode.TextDocument): DocumentToken { // It's safe to be sync. // In the worst case, concurrent calls lead to run `parse()` multiple times. // Only performance regression. No data corruption. const cache = this._documentTokenCache.get(document); if (cache && cache.version === document.version) { return cache; } else { const env = Object.create(null); const result: DocumentToken = { document, env, // Read the version before parsing, in case the document changes, // so that we won't declare an old result as a new one. version: document.version, tokens: this._engine.parse(document.getText(), env), }; this._documentTokenCache.set(document, result); return result; } } public async getEngine() { return this._engine; } } class MarkdownEngine implements IDynamicMarkdownEngine { private readonly _disposables: vscode.Disposable[]; private readonly _documentTokenCache = new Map<vscode.TextDocument, DocumentToken>(); private _engine: MarkdownIt | undefined; /** * This is used by `addNamedHeaders()`, and reset on each call to `render()`. */ private _slugCount = new Map<string, number>(); public readonly contributionsProvider = getMarkdownContributionProvider(); constructor() { this._disposables = [ vscode.workspace.onDidCloseTextDocument(document => { if (isMdDocument(document)) { this._documentTokenCache.delete(document); } }), this.contributionsProvider.onDidChangeContributions(() => { this.newEngine().then((engine) => { this._engine = engine; }); }), ]; // Initialize an engine. this.newEngine().then((engine) => { this._engine = engine; }); } public dispose(): void { // Unsubscribe event listeners. for (const disposable of this._disposables) { disposable.dispose(); } this._disposables.length = 0; } public async getDocumentToken(document: vscode.TextDocument): Promise<DocumentToken> { const cache = this._documentTokenCache.get(document); if (cache && cache.version === document.version) { return cache; } else { const env = Object.create(null); const engine = await this.getEngine(); const result: DocumentToken = { document, env, version: document.version, tokens: engine.parse(document.getText(), env), }; this._documentTokenCache.set(document, result); return result; } } public async getEngine() { if (!this._engine) { this._engine = await this.newEngine(); } return this._engine; } private async newEngine() { let md: MarkdownIt; const hljs: typeof import("highlight.js").default = require("highlight.js"); md = new MarkdownIt({ html: true, highlight: (str: string, lang?: string) => { if (lang && (lang = normalizeHighlightLang(lang)) && hljs.getLanguage(lang)) { try { return hljs.highlight(str, { language: lang, ignoreIllegals: true }).value; } catch { } } return ""; // Signal to markdown-it itself to handle it. } }); // contributions provided by this extension must be processed specially, // since this extension may not finish activation when creating an engine. extendMarkdownIt(md); if (!vscode.workspace.getConfiguration('markdown.extension.print').get<boolean>('validateUrls', true)) { md.validateLink = () => true; } this.addNamedHeaders(md); for (const contribute of this.contributionsProvider.contributions) { if (!contribute.extendMarkdownIt) { continue; } // Skip the third-party Markdown extension, if it is broken or crashes. try { md = await contribute.extendMarkdownIt(md); } catch (err) { // Use the multiple object overload, so that the console can output the error object in its own way, which usually keeps more details than `toString`. console.warn(`[yzhang.markdown-all-in-one]:\nSkipped Markdown extension: ${contribute.extensionId}\nReason:`, err); } } return md; } public async render(text: string, config: vscode.WorkspaceConfiguration): Promise<string> { const md: MarkdownIt = await this.getEngine(); md.set({ breaks: config.get<boolean>('breaks', false), linkify: config.get<boolean>('linkify', true) }); this._slugCount.clear(); return md.render(text); } /** * Tweak the render rule for headings, to set anchor ID. */ private addNamedHeaders(md: MarkdownIt): void { const originalHeadingOpen = md.renderer.rules.heading_open; // Arrow function ensures that `this` is inherited from `addNamedHeaders`, // so that we won't need `bind`, and save memory a little. md.renderer.rules.heading_open = (tokens, idx, options, env, self) => { const raw = tokens[idx + 1].content; let slug = slugify(raw, { env }); let lastCount = this._slugCount.get(slug); if (lastCount !== undefined) { lastCount++; this._slugCount.set(slug, lastCount); slug += '-' + lastCount; } else { this._slugCount.set(slug, 0); } tokens[idx].attrs = [...(tokens[idx].attrs || []), ["id", slug]]; if (originalHeadingOpen) { return originalHeadingOpen(tokens, idx, options, env, self); } else { return self.renderToken(tokens, idx, options); } }; } } /** * Tries to convert the identifier to a language name supported by Highlight.js. * * @see {@link https://github.com/highlightjs/highlight.js/blob/main/SUPPORTED_LANGUAGES.md} */ function normalizeHighlightLang(lang: string): string { switch (lang && lang.toLowerCase()) { case 'tsx': case 'typescriptreact': return 'jsx'; case 'json5': case 'jsonc': return 'json'; case 'c#': case 'csharp': return 'cs'; default: return lang; } } /** * This engine dynamically refreshes in the same way as VS Code's built-in Markdown preview. */ export const mdEngine = new MarkdownEngine(); /** * A strict CommonMark only engine instance. */ export const commonMarkEngine = new CommonMarkEngine(); ================================================ FILE: src/markdownExtensions.ts ================================================ // Reference to https://github.com/microsoft/vscode/blob/master/extensions/markdown-language-features/src/markdownExtensions.ts // Note: // Not all extensions are implemented correctly. // Thus, we need to check redundantly when loading their contributions, typically in `resolveMarkdownContribution()`. import * as vscode from "vscode"; import MarkdownIt = require("markdown-it"); import { Lazy } from "./util/lazy"; /** * Represents a VS Code extension with Markdown contribution. * * @see {@link https://code.visualstudio.com/api/extension-guides/markdown-extension} * @see {@link https://code.visualstudio.com/api/references/extension-manifest} */ export interface IVscodeMarkdownExtension extends vscode.Extension<{ readonly extendMarkdownIt?: (md: MarkdownIt) => MarkdownIt; }> { readonly packageJSON: { readonly name: string; readonly version: string; readonly publisher: string; readonly engines: { readonly [engine: string]: string }; readonly contributes?: { /** * `true` when the extension should provide `extendMarkdownIt()`. */ readonly "markdown.markdownItPlugins"?: boolean; /** * A list of JavaScript files relative to the extension's root directory. */ readonly "markdown.previewScripts"?: readonly string[]; /** * A list of CSS files relative to the extension's root directory. */ readonly "markdown.previewStyles"?: readonly string[]; }; }; } /** * Represents the Markdown contribution from one VS Code extension. */ export interface IMarkdownContribution { readonly extensionId: string; readonly extensionUri: vscode.Uri; readonly extendMarkdownIt?: undefined | ((md: MarkdownIt) => Promise<MarkdownIt>); readonly previewScripts?: undefined | readonly vscode.Uri[]; readonly previewStyles?: undefined | readonly vscode.Uri[]; } /** * Represents a function that transforms `IMarkdownContribution`. */ export interface IFuncMarkdownContributionTransformer { /** * @returns `original` when no need to transform. Otherwise, a shallow copy with properties tweaked. */ (original: IMarkdownContribution): IMarkdownContribution; } /** * Extracts and wraps `extendMarkdownIt()` from the extension. * * @returns A function that will activate the extension and invoke its `extendMarkdownIt()`. */ function getContributedMarkdownItPlugin(extension: IVscodeMarkdownExtension): (md: MarkdownIt) => Promise<MarkdownIt> { return async (md) => { const exports = await extension.activate(); if (exports && exports.extendMarkdownIt) { return exports.extendMarkdownIt(md); } return md; }; } /** * Resolves absolute Uris of paths from the extension. * * @param paths The list of paths relative to the extension's root directory. * * @returns A list of resolved absolute Uris. * `undefined` indicates error. */ function resolveExtensionResourceUris( extension: vscode.Extension<unknown>, paths: readonly string[] ): vscode.Uri[] | undefined { try { return paths.map((path) => vscode.Uri.joinPath(extension.extensionUri, path)); } catch { return undefined; // Discard the extension. } } /** * Resolves the Markdown contribution from the VS Code extension. * * This function extracts and wraps the contribution without validating the underlying resources. */ function resolveMarkdownContribution(extension: IVscodeMarkdownExtension): IMarkdownContribution | undefined { const contributes = extension.packageJSON && extension.packageJSON.contributes; if (!contributes) { return; } const extendMarkdownIt = contributes["markdown.markdownItPlugins"] ? getContributedMarkdownItPlugin(extension) : undefined; const previewScripts = contributes["markdown.previewScripts"] && contributes["markdown.previewScripts"].length ? resolveExtensionResourceUris(extension, contributes["markdown.previewScripts"]) : undefined; const previewStyles = contributes["markdown.previewStyles"] && contributes["markdown.previewStyles"].length ? resolveExtensionResourceUris(extension, contributes["markdown.previewStyles"]) : undefined; if (!extendMarkdownIt && !previewScripts && !previewStyles) { return; } return { extensionId: extension.id, extensionUri: extension.extensionUri, extendMarkdownIt, previewScripts, previewStyles, }; } /** * Skip these extensions! */ const Extension_Blacklist: ReadonlySet<string> = new Set<string>([ "vscode.markdown-language-features", // Deadlock. "yzhang.markdown-all-in-one", // This. ]); /** * The contributions from these extensions can not be utilized directly. * * `ID -> Transformer` */ const Extension_Special_Treatment: ReadonlyMap<string, IFuncMarkdownContributionTransformer> = new Map([ ["vscode.markdown-math", (original) => ({ ...original, previewStyles: undefined })], // Its CSS is not portable. (#986) ]); /** * Represents a VS Code extension Markdown contribution provider. */ export interface IMarkdownContributionProvider extends vscode.Disposable { // This is theoretically long-running, and should be a `getContributions()` method. // But we're not motivated to rename it for now. readonly contributions: ReadonlyArray<IMarkdownContribution>; readonly onDidChangeContributions: vscode.Event<this>; } class MarkdownContributionProvider implements IMarkdownContributionProvider { private readonly _onDidChangeContributions = new vscode.EventEmitter<this>(); protected readonly _disposables: vscode.Disposable[] = []; private _cachedContributions: ReadonlyArray<IMarkdownContribution> | undefined = undefined; private _isDisposed = false; public readonly onDidChangeContributions = this._onDidChangeContributions.event; public constructor() { this._disposables.push( this._onDidChangeContributions, vscode.extensions.onDidChange(() => { // `contributions` will rebuild the cache. this._cachedContributions = undefined; this._onDidChangeContributions.fire(this); }) ); } public dispose(): void { if (this._isDisposed) { return; } for (const item of this._disposables) { item.dispose(); } this._disposables.length = 0; this._isDisposed = true; } public get contributions() { if (!this._cachedContributions) { this._cachedContributions = vscode.extensions.all.reduce<IMarkdownContribution[]>((result, extension) => { if (Extension_Blacklist.has(extension.id)) { return result; } const c = resolveMarkdownContribution(extension); if (!c) { return result; } const t = Extension_Special_Treatment.get(extension.id); result.push(t ? t(c) : c); return result; }, []); } return this._cachedContributions; } } const defaultProvider = new Lazy(() => new MarkdownContributionProvider()); export function getMarkdownContributionProvider(): IMarkdownContributionProvider { return defaultProvider.value; } ================================================ FILE: src/nls/README.md ================================================ # National Language Support (NLS) This module follows a pattern similar to [vscode-nls](https://github.com/microsoft/vscode-nls). ================================================ FILE: src/nls/index.ts ================================================ "use strict"; import * as fs from "fs"; import * as vscode from "vscode"; import VisualStudioCodeLocaleId from "../contract/VisualStudioCodeLocaleId"; import resolveResource from "./resolveResource"; export type Primitive = string | number | bigint | boolean | symbol | undefined | null; export interface IConfigOption { extensionContext: vscode.ExtensionContext; locale?: VisualStudioCodeLocaleId; } export interface IFuncLocalize { /** * @param key The key of the format string of the message in the bundle. * @param args An array of objects to format. */ (key: string, ...args: Primitive[]): string; } interface IInternalOption { /** * Indicates whether the extension is **not** running in development mode. * The same as `ExtensionContext.extensionMode !== Development`. */ cacheResolution: boolean; /** * The same as `ExtensionContext.extensionPath`. */ extensionPath: string; /** * The default locale. * This is internally treated as an arbitrary string. */ locale: VisualStudioCodeLocaleId | undefined; } interface INlsBundle { [key: string]: string; } // https://github.com/microsoft/vscode-nls/blob/9fd18e6777276ebeb68ddf314ec2459abc6e3f4f/src/node/main.ts#L36-L46 // https://github.com/microsoft/vscode/blob/dad5d39eb0a251a726388f547e8dc85cd96a184d/src/vs/base/node/languagePacks.d.ts interface IVscodeNlsConfig { locale: string; availableLanguages: { [pack: string]: string; }; } //#region Utility function readJsonFile<T = any>(path: string): T { return JSON.parse(fs.readFileSync(path, "utf8")); } //#endregion Utility //#region Private // Why `Object.create(null)`: // Once constructed, this is used as a readonly dictionary (map). // It is performance-sensitive, and should not be affected by the outside. // Besides, `Object.prototype` might collide with our keys. const resolvedBundle: INlsBundle = Object.create(null); /** * Internal options. * Will be initialized in `config()`. */ const options: IInternalOption = Object.create(null); /** * Updates the in-memory NLS bundle. * @param locales An array of locale IDs. The default locale will be appended. */ function cacheBundle(locales: VisualStudioCodeLocaleId[] = []): void { if (options.locale) { locales.push(options.locale); // Fallback. } // * We always provide `package.nls.json`. // * Reverse the return value, so that we can build a bundle with nice fallback by a simple loop. const files = resolveResource(options.extensionPath, "package.nls", "json", locales)!.reverse() as readonly string[]; for (const path of files) { try { Object.assign<typeof resolvedBundle, typeof resolvedBundle>(resolvedBundle, readJsonFile<INlsBundle>(path)); } catch (error) { console.error(error); // Log, and ignore the bundle. } } } /** * @param message A composite format string. * @param args An array of objects to format. */ function format(message: string, ...args: Primitive[]): string { if (args.length === 0) { return message; } else { return message.replace(/\{(0|[1-9]\d*?)\}/g, (match: string, index: string): string => { // `index` is zero-based. return args.length > +index ? String(args[+index]) : match; }); } } //#endregion Private //#region Public export const localize: IFuncLocalize = function (key: string, ...args: Primitive[]): string { if (options.cacheResolution) { const msg: string | undefined = resolvedBundle[key]; return msg === undefined ? "[" + key + "]" : format(msg, ...args); } else { // When in development mode, hot reload, and reveal the key. cacheBundle(); const msg: string | undefined = resolvedBundle[key]; return msg === undefined ? "[" + key + "]" : "[" + key.substring(key.lastIndexOf(".") + 1) + "] " + format(msg, ...args); } }; /** * Configures the NLS module. * * You should only call it **once** in the application entry point. */ export function config(opts: IConfigOption) { if (opts.locale) { options.locale = opts.locale; } else { try { const vscodeOptions = JSON.parse(process.env.VSCODE_NLS_CONFIG as string) as IVscodeNlsConfig; options.locale = vscodeOptions.locale as any; } catch (error) { // Log, but do nothing else, in case VS Code suddenly changes their mind, or we are not in VS Code. console.error(error); } } options.extensionPath = opts.extensionContext.extensionPath; options.cacheResolution = opts.extensionContext.extensionMode !== vscode.ExtensionMode.Development; // Load and freeze the cache when not in development mode. if (options.cacheResolution) { cacheBundle(); Object.freeze(resolvedBundle); } return localize; } //#endregion Public ================================================ FILE: src/nls/resolveResource.ts ================================================ "use strict"; import * as fs from "fs"; import * as path from "path"; import type VisualStudioCodeLocaleId from "../contract/VisualStudioCodeLocaleId"; /** * Finds localized resources that match the given pattern under the directory. * * ### Remarks * * Comparison is case-**sensitive** (SameValueZero). * * When an exact match cannot be found, this function performs fallback as per RFC 4647 Lookup. * * Make sure the directory does not change. * Call this function as **few** as possible. * This function may scan the directory thoroughly, thus is very **expensive**. * * ### Exceptions * * * The path to the directory is not absolute. * * The directory does not exist. * * Read permission is not granted. * * @param directory The **absolute** file system path to the directory that Node.js can recognizes, including UNC on Windows. * @param baseName The string that the file name begins with. * @param suffix The string that the file name ends with. * @param locales The locale IDs that can be inserted between `baseName` and `suffix`. Sorted by priority, from high to low. * @param separator The string to use when joining `baseName`, `locale`, `suffix` together. Defaults to `.` (U+002E). * @returns An array of absolute paths to matched files, sorted by priority, from high to low. Or `undefined` when no match. * @example * // Entries under directory `/tmp`: * // Directory f.nls.zh-cn.json/ * // File f.nls.json * // File f.nls.zh.json * * resolveResource("/tmp", "f.nls", "json", ["ja", "zh-cn"]); * * // Returns: * ["/tmp/f.nls.zh.json", "/tmp/f.nls.json"]; */ export default function resolveResource( directory: string, baseName: string, suffix: string, locales: VisualStudioCodeLocaleId[], separator: string = ".", ): string[] | undefined { if (!path.isAbsolute(directory)) { throw new Error("The directory must be an absolute file system path."); } // Throw an exception, if we do not have permission, or the directory does not exist. const files: readonly string[] = fs.readdirSync(directory, { withFileTypes: true }).reduce<string[]>((res, crt) => { if (crt.isFile()) { res.push(crt.name); } return res; }, []); const result: string[] = []; let splitIndex: number; for (let loc of locales as string[]) { while (true) { const fileName = baseName + separator + loc + separator + suffix; const resolvedPath = path.resolve(directory, fileName); if (!result.includes(resolvedPath) && files.includes(fileName)) { result.push(resolvedPath); } // Fallback according to RFC 4647 section 3.4. Although they are different systems, algorithms are common. splitIndex = loc.lastIndexOf("-"); if (splitIndex > 0) { loc = loc.slice(0, splitIndex); } else { break; } } } // Fallback. The use of block is to keep the function scope clean. { const fileName = baseName + separator + suffix; const resolvedPath = path.resolve(directory, fileName); // As long as parameters are legal, this `resolvedPath` won't have been in `result`. Thus, only test `fileName`. if (files.includes(fileName)) { result.push(resolvedPath); } } return result.length === 0 ? undefined : result; } ================================================ FILE: src/preview.ts ================================================ import * as vscode from "vscode"; // These are dedicated objects for the auto preview. let debounceHandle: ReturnType<typeof setTimeout> | undefined; let lastDoc: vscode.TextDocument | undefined; const d0 = Object.freeze<vscode.Disposable & { _disposables: vscode.Disposable[] }>({ _disposables: [], dispose: function () { for (const item of this._disposables) { item.dispose(); } this._disposables.length = 0; if (debounceHandle) { clearTimeout(debounceHandle); debounceHandle = undefined; } lastDoc = undefined; }, }); export function activate(context: vscode.ExtensionContext) { // Register auto preview. And try showing preview on activation. const d1 = vscode.workspace.onDidChangeConfiguration((e) => { if (e.affectsConfiguration("markdown.extension.preview")) { if (vscode.workspace.getConfiguration("markdown.extension.preview").get<boolean>("autoShowPreviewToSide")) { registerAutoPreview(); } else { d0.dispose(); } } }); if (vscode.workspace.getConfiguration("markdown.extension.preview").get<boolean>("autoShowPreviewToSide")) { registerAutoPreview(); triggerAutoPreview(vscode.window.activeTextEditor); } // `markdown.extension.closePreview` is just a wrapper for the `workbench.action.closeActiveEditor` command. // We introduce it to avoid confusing users in UI. // "Toggle preview" is achieved by contributing key bindings that very carefully match VS Code's default values. // https://github.com/yzhang-gh/vscode-markdown/pull/780 const d2 = vscode.commands.registerCommand("markdown.extension.closePreview", () => { return vscode.commands.executeCommand("workbench.action.closeActiveEditor"); }); // Keep code tidy. context.subscriptions.push(d1, d2, d0); } function registerAutoPreview() { d0._disposables.push(vscode.window.onDidChangeActiveTextEditor((editor) => triggerAutoPreview(editor))); } // VS Code dispatches a series of DidChangeActiveTextEditor events when moving tabs between groups, we don't want most of them. function triggerAutoPreview(editor: vscode.TextEditor | undefined): void { // GitHub issues #1282, #1342 const markdownRe = /(\.md|\.markdown)$/i if (!(editor && editor.document.uri.scheme === 'file' && markdownRe.test(editor.document.fileName))) { return; } if (debounceHandle) { clearTimeout(debounceHandle); debounceHandle = undefined; } // Usually, a user only wants to trigger preview when the currently and last viewed documents are not the same. const doc = editor.document; if (doc !== lastDoc) { lastDoc = doc; debounceHandle = setTimeout(() => autoPreviewToSide(editor), 100); } } /** * Shows preview for the editor. */ async function autoPreviewToSide(editor: vscode.TextEditor) { if (editor.document.isClosed) { return; } // Call `vscode.markdown-language-features`. await vscode.commands.executeCommand("markdown.showPreviewToSide"); // Wait, as VS Code won't respond when it just opened a preview. await new Promise((resolve) => setTimeout(resolve, 100)); // VS Code 1.62 appears to make progress in https://github.com/microsoft/vscode/issues/9526 // Thus, we must request the text editor directly with known view column (if available). await vscode.window.showTextDocument(editor.document, editor.viewColumn); } ================================================ FILE: src/print.ts ================================================ 'use strict'; import * as fs from "fs"; import * as path from 'path'; import { commands, ExtensionContext, TextDocument, Uri, window, workspace } from 'vscode'; import { encodeHTML } from 'entities'; import { localize } from './nls'; import { mdEngine } from "./markdownEngine"; import { isMdDocument } from "./util/generic"; let thisContext: ExtensionContext; export function activate(context: ExtensionContext) { thisContext = context; context.subscriptions.push( commands.registerCommand('markdown.extension.printToHtml', () => { print('html'); }), commands.registerCommand('markdown.extension.printToHtmlBatch', () => { batchPrint(); }), workspace.onDidSaveTextDocument(onDidSave) ); } export function deactivate() { } function onDidSave(doc: TextDocument) { if ( doc.languageId === 'markdown' && workspace.getConfiguration('markdown.extension.print', doc.uri).get<boolean>('onFileSave') ) { print('html'); } } async function print(type: string, uri?: Uri, outFolder?: string) { const editor = window.activeTextEditor; if (!editor || !isMdDocument(editor?.document)) { window.showErrorMessage(localize("ui.general.messageNoValidMarkdownFile")); return; } const doc = uri ? await workspace.openTextDocument(uri) : editor.document; if (doc.isDirty || doc.isUntitled) { doc.save(); } const statusBarMessage = window.setStatusBarMessage("$(sync~spin) " + localize("ui.exporting.messageExportingInProgress", path.basename(doc.fileName), type.toUpperCase())); if (outFolder && !fs.existsSync(outFolder)) { fs.mkdirSync(outFolder, { recursive: true }); } /** * Modified from <https://github.com/Microsoft/vscode/tree/master/extensions/markdown> * src/previewContentProvider MDDocumentContentProvider provideTextDocumentContent */ let outPath = outFolder ? path.join(outFolder, path.basename(doc.fileName)) : doc.fileName; outPath = outPath.replace(/\.\w+?$/, `.${type}`); outPath = outPath.replace(/^([cdefghij]):\\/, function (_, p1: string) { return `${p1.toUpperCase()}:\\`; // Capitalize drive letter }); if (!outPath.endsWith(`.${type}`)) { outPath += `.${type}`; } //// Determine document title. // 1. If the document begins with a comment like `<!-- title: Document Title -->`, use it. Empty title is not allow here. (GitHub #506) // 2. Else, find the first ATX heading, and use it. const firstLineText = doc.lineAt(0).text; // The lazy quantifier and `trim()` can avoid mistakenly capturing cases like: // <!-- title:-->--> // <!-- title: --> --> let m = /^<!-- title:(.*?)-->/.exec(firstLineText); let title: string | undefined = m === null ? undefined : m[1].trim(); // Empty string is also falsy. if (!title) { // Editors treat `\r\n`, `\n`, and `\r` as EOL. // Since we don't care about line numbers, a simple alternation is enough and slightly faster. title = doc.getText().split(/\n|\r/g).find(lineText => lineText.startsWith('#') && /^#{1,6} /.test(lineText)); if (title) { title = title.replace(/<!--(.*?)-->/g, ''); title = title.trim().replace(/^#+/, '').replace(/#+$/, '').trim(); } } //// Render body HTML. let body: string = await mdEngine.render(doc.getText(), workspace.getConfiguration('markdown.preview', doc.uri)); //// Image paths const config = workspace.getConfiguration('markdown.extension', doc.uri); const configToBase64 = config.get<boolean>('print.imgToBase64'); const configAbsPath = config.get<boolean>('print.absoluteImgPath'); const imgTagRegex = /(<img[^>]+src=")([^"]+)("[^>]*>)/g; // Match '<img...src="..."...>' if (configToBase64) { body = body.replace(imgTagRegex, function (_, p1, p2, p3) { if (p2.startsWith('http') || p2.startsWith('data:')) { return _; } const imgSrc = relToAbsPath(doc.uri, p2); try { let imgExt = path.extname(imgSrc).slice(1); if (imgExt === "jpg") { imgExt = "jpeg"; } else if (imgExt === "svg") { imgExt += "+xml"; } const file = fs.readFileSync(imgSrc.replace(/%20/g, '\ ')).toString('base64'); return `${p1}data:image/${imgExt};base64,${file}${p3}`; } catch (e) { window.showWarningMessage(localize("ui.general.messageUnableToReadFile", imgSrc) + ` ${localize("ui.exporting.messageRevertingToImagePaths")} (${doc.fileName})`); } if (configAbsPath) { return `${p1}file:///${imgSrc}${p3}`; } else { return _; } }); } else if (configAbsPath) { body = body.replace(imgTagRegex, function (_, p1, p2, p3) { if (p2.startsWith('http') || p2.startsWith('data:')) { return _; } const imgSrc = relToAbsPath(doc.uri, p2); // Absolute paths need `file:///` but relative paths don't return `${p1}file:///${imgSrc}${p3}`; }); } //// Convert `.md` links to `.html` by default (#667, #1324, #1347) const hrefRegex = /(<a[^>]+href=")([^"]+)("[^>]*>)/g; // Match '<a...href="..."...>' body = body.replace(hrefRegex, function (_, g1, g2, g3) { if ((g2.endsWith('.md') || g2.includes('.md#')) && !(g2.includes('github.com') && g2.includes('blob'))) { return `${g1}${g2.replace(/\.md$/, '.html').replace(/\.md#/, '.html#')}${g3}`; } else { return _; } }); const hasMath = hasMathEnv(doc.getText()); const extensionStyles = await getPreviewExtensionStyles(); const extensionScripts = await getPreviewExtensionScripts(); const includeVscodeStyles = config.get<boolean>('print.includeVscodeStylesheets')!; const pureHtml = config.get<boolean>('print.pureHtml')!; const themeKind = config.get<string>('print.theme'); const themeClass = themeKind === 'light' ? 'vscode-light' : themeKind === 'dark' ? 'vscode-dark' : ''; let html = body; if (!pureHtml) { html = `<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>${title ? encodeHTML(title) : ''} ${extensionStyles} ${getStyles(doc.uri, hasMath, includeVscodeStyles)} ${body} ${hasMath ? '' : ''} ${extensionScripts} `; } switch (type) { case 'html': fs.writeFile(outPath, html, 'utf-8', function (err) { if (err) { console.log(err); } }); break; case 'pdf': break; } // Hold the message for extra 500ms, in case the operation finished very fast. setTimeout(() => statusBarMessage.dispose(), 500); } function batchPrint() { const doc = window.activeTextEditor?.document; // @ts-ignore Needs refactoring. const root = workspace.getWorkspaceFolder(doc.uri).uri; window.showOpenDialog({ defaultUri: root, openLabel: 'Select source folder', canSelectFiles: false, canSelectFolders: true }).then(uris => { if (uris && uris.length > 0) { const selectedPath = uris[0].fsPath; const relPath = path.relative(root.fsPath, selectedPath); if (relPath.startsWith('..')) { window.showErrorMessage('Cannot use a path outside the current folder'); return; } workspace.findFiles((relPath.length > 0 ? relPath + '/' : '') + '**/*.{md}', '{**/node_modules,**/bower_components,**/*.code-search}').then(uris => { window.showInputBox({ value: selectedPath + path.sep + 'out', valueSelection: [selectedPath.length + 1, selectedPath.length + 4], prompt: 'Please specify an output folder' }).then(outFolder => { uris.forEach(uri => { print('html', uri, path.join(outFolder!, path.relative(selectedPath, path.dirname(uri.fsPath)))); }); }); }); } }); } function hasMathEnv(text: string) { // I'm lazy return text.includes('$'); } function wrapWithStyleTag(src: string) { if (src.startsWith('http')) { return ``; } else { return ``; } } function readCss(fileName: string) { try { return fs.readFileSync(fileName).toString(); } catch (error) { // https://nodejs.org/docs/latest-v12.x/api/errors.html#errors_class_systemerror window.showWarningMessage(localize("ui.exporting.messageCustomCssNotFound", (error as NodeJS.ErrnoException).path)); return ''; } } function getStyles(uri: Uri, hasMathEnv: boolean, includeVscodeStyles: boolean) { const katexCss = ''; const markdownCss = ''; const highlightCss = ''; const copyTeXCss = ''; const baseCssPaths = [ 'media/checkbox.css', "node_modules/markdown-it-github-alerts/styles/github-colors-light.css", "node_modules/markdown-it-github-alerts/styles/github-colors-dark-media.css", 'node_modules/markdown-it-github-alerts/styles/github-base.css' ].map(s => thisContext.asAbsolutePath(s)); const customCssPaths = getCustomStyleSheets(uri); return `${hasMathEnv ? katexCss + '\n' + copyTeXCss : ''} ${includeVscodeStyles ? markdownCss + '\n' + highlightCss + '\n' + getPreviewSettingStyles() : ''} ${baseCssPaths.map(cssSrc => wrapWithStyleTag(cssSrc)).join('\n')} ${customCssPaths.map(cssSrc => wrapWithStyleTag(cssSrc)).join('\n')}`; } function getCustomStyleSheets(resource: Uri): string[] { const styles = workspace.getConfiguration('markdown', resource)['styles']; if (styles && Array.isArray(styles) && styles.length > 0) { const root = workspace.getWorkspaceFolder(resource); return styles.map(s => { if (!s || s.startsWith('http') || path.isAbsolute(s)) { return s; } if (root) { return path.join(root.uri.fsPath, s); } else { // Otherwise look relative to the markdown file return path.join(path.dirname(resource.fsPath), s); } }); } return []; } function relToAbsPath(resource: Uri, href: string): string { if (!href || href.startsWith('http') || path.isAbsolute(href)) { return href; } // Otherwise look relative to the markdown file return path.join(path.dirname(resource.fsPath), href); } function getPreviewSettingStyles(): string { const previewSettings = workspace.getConfiguration('markdown')['preview']; if (!previewSettings) { return ''; } const { fontFamily, fontSize, lineHeight } = previewSettings; return ``; } async function getPreviewExtensionStyles() { var result = ""; return result; } async function getPreviewExtensionScripts() { var result = ""; for (const contribute of mdEngine.contributionsProvider.contributions) { if (!contribute.previewScripts || !contribute.previewScripts.length) { continue; } for (const scriptFile of contribute.previewScripts) { result += `\n`; } } return result; } ================================================ FILE: src/syntaxDecorations.ts ================================================ // This module is deprecated. // No update will land here. // It is superseded by `src/theming`, etc. "use strict"; import * as vscode from "vscode"; import { isInFencedCodeBlock, mathEnvCheck } from "./util/contextCheck"; //#region Constant const enum DecorationType { baseColor, gray, lightBlue, orange, } const decorationStyles: Readonly>> = { [DecorationType.baseColor]: { "dark": { "color": "#EEFFFF" }, "light": { "color": "000000" }, }, [DecorationType.gray]: { "rangeBehavior": 1, "dark": { "color": "#636363" }, "light": { "color": "#CCC" }, }, [DecorationType.lightBlue]: { "color": "#4080D0", }, [DecorationType.orange]: { "color": "#D2B640", }, }; const regexDecorTypeMappingPlainTheme: ReadonlyArray<[RegExp, ReadonlyArray]> = [ // [alt](link) [ /(^|[^!])(\[)([^\]\r\n]*?(?!\].*?\[)[^\[\r\n]*?)(\]\(.+?\))/, [undefined, DecorationType.gray, DecorationType.lightBlue, DecorationType.gray] ], // ![alt](link) [ /(\!\[)([^\]\r\n]*?(?!\].*?\[)[^\[\r\n]*?)(\]\(.+?\))/, [DecorationType.gray, DecorationType.orange, DecorationType.gray] ], // `code` [ /(?\/\?\s].*?[^\*\`\!\@\#\%\^\&\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s])(\*)/, [DecorationType.gray, DecorationType.baseColor, DecorationType.gray] ], // _italic_ [ /(_)([^\*\`\!\@\#\%\^\&\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s].*?[^\*\`\!\@\#\%\^\&\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s])(_)/, [DecorationType.gray, DecorationType.baseColor, DecorationType.gray] ], // **bold** [ /(\*\*)([^\*\`\!\@\#\%\^\&\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s].*?[^\*\`\!\@\#\%\^\&\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s])(\*\*)/, [DecorationType.gray, DecorationType.baseColor, DecorationType.gray] ], ]; //#endregion Constant /** * Decoration type instances **currently in use**. */ const decorationHandles = new Map(); const decors = new Map(); export function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.workspace.onDidChangeConfiguration(event => { if (event.affectsConfiguration("markdown.extension.syntax.plainTheme")) { triggerUpdateDecorations(vscode.window.activeTextEditor); } }), vscode.window.onDidChangeActiveTextEditor(triggerUpdateDecorations), vscode.workspace.onDidChangeTextDocument(event => { const editor = vscode.window.activeTextEditor; if (editor && event.document === editor.document) { triggerUpdateDecorations(editor); } }), ); triggerUpdateDecorations(vscode.window.activeTextEditor); } var timeout: NodeJS.Timeout | undefined; // Debounce. function triggerUpdateDecorations(editor: vscode.TextEditor | undefined) { if (!editor || editor.document.languageId !== "markdown") { return; } if (timeout) { clearTimeout(timeout); } timeout = setTimeout(() => updateDecorations(editor), 200); } /** * Clears decorations in the text editor. */ function clearDecorations(editor: vscode.TextEditor) { for (const handle of decorationHandles.values()) { editor.setDecorations(handle, []); } } function updateDecorations(editor: vscode.TextEditor) { // Remove everything if it's disabled. if (!vscode.workspace.getConfiguration().get("markdown.extension.syntax.plainTheme", false)) { for (const handle of decorationHandles.values()) { handle.dispose(); } decorationHandles.clear(); return; } const doc = editor.document; if (doc.getText().length * 1.5 > vscode.workspace.getConfiguration().get("markdown.extension.syntax.decorationFileSizeLimit")!) { clearDecorations(editor); // In case the editor is still visible. return; } // Reset decoration collection. decors.clear(); for (const typeName of Object.keys(decorationStyles)) { decors.set(+typeName, []); } // Analyze. doc.getText().split(/\r?\n/g).forEach((lineText, lineNum) => { // For each line if (isInFencedCodeBlock(doc, lineNum)) { return; } // Issue #412 // Trick. Match `[alt](link)` and `![alt](link)` first and remember those greyed out ranges const noDecorRanges: [number, number][] = []; for (const [re, types] of regexDecorTypeMappingPlainTheme) { const regex = new RegExp(re, "g"); for (const match of lineText.matchAll(regex)) { let startIndex = match.index!; if (noDecorRanges.some(r => (startIndex > r[0] && startIndex < r[1]) || (startIndex + match[0].length > r[0] && startIndex + match[0].length < r[1]) )) { continue; } for (let i = 0; i < types.length; i++) { //// Skip if in math environment (See `completion.ts`) if (mathEnvCheck(doc, new vscode.Position(lineNum, startIndex)) !== "") { break; } const typeName = types[i]; const caughtGroup = match[i + 1]; if (typeName === DecorationType.gray && caughtGroup.length > 2) { noDecorRanges.push([startIndex, startIndex + caughtGroup.length]); } const range = new vscode.Range(lineNum, startIndex, lineNum, startIndex + caughtGroup.length); startIndex += caughtGroup.length; //// Needed for `[alt](link)` rule. And must appear after `startIndex += caughtGroup.length;` if (!typeName) { continue; } // We've created these arrays at the beginning of the function. decors.get(typeName)!.push(range); } } } }); // Apply decorations. for (const [typeName, ranges] of decors) { let handle = decorationHandles.get(typeName); // Create a new decoration type instance if needed. if (!handle) { handle = vscode.window.createTextEditorDecorationType(decorationStyles[typeName]); decorationHandles.set(typeName, handle); } editor.setDecorations(handle, ranges); } } ================================================ FILE: src/tableFormatter.ts ================================================ 'use strict'; // https://github.github.com/gfm/#tables-extension- import * as vscode from "vscode"; import { configManager } from "./configuration/manager"; import { Document_Selector_Markdown } from "./util/generic"; //// This module can only be referenced with ECMAScript imports/exports by turning on the 'esModuleInterop' flag and referencing its default export. // import { GraphemeSplitter } from 'grapheme-splitter'; import GraphemeSplitter = require('grapheme-splitter'); const splitter = new GraphemeSplitter(); interface ITableRange { text: string; offset: number; range: vscode.Range; } // Dedicated objects for managing the formatter. const d0 = Object.freeze({ _disposables: [], dispose: function () { for (const item of this._disposables) { item.dispose(); } this._disposables.length = 0; }, }); const registerFormatter = () => { if (configManager.get("tableFormatter.enabled")) { d0._disposables.push(vscode.languages.registerDocumentFormattingEditProvider(Document_Selector_Markdown, new MarkdownDocumentFormatter())); d0._disposables.push(vscode.languages.registerDocumentRangeFormattingEditProvider(Document_Selector_Markdown, new MarkdownDocumentRangeFormattingEditProvider())); } else { d0.dispose(); } } export function activate(context: vscode.ExtensionContext) { const d1 = vscode.workspace.onDidChangeConfiguration((event) => { if (event.affectsConfiguration("markdown.extension.tableFormatter.enabled")) { registerFormatter(); } }); registerFormatter(); context.subscriptions.push(d1, d0); } enum ColumnAlignment { None, Left, Center, Right } class MarkdownDocumentFormatter implements vscode.DocumentFormattingEditProvider { provideDocumentFormattingEdits(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken) { const tables = this.detectTables(document); if (!tables || token.isCancellationRequested) { return; } const edits: vscode.TextEdit[] = tables.map( (target) => new vscode.TextEdit(target.range, this.formatTable(target, document, options)) ); return edits; } protected detectTables(document: vscode.TextDocument): ITableRange[] | undefined { const text = document.getText(); const lineBreak = String.raw`\r?\n`; const contentLine = String.raw`\|?.*\|.*\|?`; const leftSideHyphenComponent = String.raw`(?:\|? *:?-+:? *\|)`; const middleHyphenComponent = String.raw`(?: *:?-+:? *\|)*`; const rightSideHyphenComponent = String.raw`(?: *:?-+:? *\|?)`; const multiColumnHyphenLine = leftSideHyphenComponent + middleHyphenComponent + rightSideHyphenComponent; //// GitHub issue #431 const singleColumnHyphenLine = String.raw`(?:\| *:?-+:? *\|)`; const hyphenLine = String.raw`[ \t]*(?:${multiColumnHyphenLine}|${singleColumnHyphenLine})[ \t]*`; const tableRegex = new RegExp(contentLine + lineBreak + hyphenLine + '(?:' + lineBreak + contentLine + ')*', 'g'); const result: ITableRange[] = Array.from( text.matchAll(tableRegex), (item): ITableRange => ({ text: item[0], offset: item.index!, range: new vscode.Range( document.positionAt(item.index!), document.positionAt(item.index! + item[0].length) ), }) ); return result.length ? result : undefined; } /** * Return the indentation of a table as a string of spaces by reading it from the first line. * In case of `markdown.extension.table.normalizeIndentation` is `enabled` it is rounded to the closest multiple of * the configured `tabSize`. */ private getTableIndentation(text: string, options: vscode.FormattingOptions) { let doNormalize = configManager.get("tableFormatter.normalizeIndentation"); let indentRegex = new RegExp(/^(\s*)\S/u); let match = text.match(indentRegex); let spacesInFirstLine = match?.[1].length ?? 0; let tabStops = Math.round(spacesInFirstLine / options.tabSize); let spaces = doNormalize ? " ".repeat(options.tabSize * tabStops) : " ".repeat(spacesInFirstLine); return spaces; } protected formatTable(target: ITableRange, doc: vscode.TextDocument, options: vscode.FormattingOptions) { // The following operations require the Unicode Normalization Form C (NFC). const text = target.text.normalize(); const delimiterRowIndex = 1; const delimiterRowNoPadding = configManager.get('tableFormatter.delimiterRowNoPadding'); const indentation = this.getTableIndentation(text, options); const rowsNoIndentPattern = new RegExp(/^\s*(\S.*)$/gum); const rows: string[] = Array.from(text.matchAll(rowsNoIndentPattern), (match) => match[1].trim()); // Desired "visual" width of each column (the length of the longest cell in each column), **without padding** const colWidth: number[] = []; // Alignment of each column const colAlign: ColumnAlignment[] = []; // Regex to extract cell content. // GitHub #24 const fieldRegExp = new RegExp(/((\\\||[^\|])*)\|/gu); // https://www.ling.upenn.edu/courses/Spring_2003/ling538/UnicodeRanges.html const cjkRegex = /[\u3000-\u9fff\uac00-\ud7af\uff01-\uff60]/g; const lines = rows.map((row, iRow) => { // Normalize if (row.startsWith('|')) { row = row.slice(1); } if (!row.endsWith('|')) { row = row + '|'; } // Parse cells in the current row let values = []; let iCol = 0; for (const field of row.matchAll(fieldRegExp)) { let cell = field[1].trim(); values.push(cell); // Ignore the length of delimiter-line before we normalize it if (iRow === delimiterRowIndex) { continue; } // Calculate the desired "visual" column width. // The following notes help to understand the precondition for our calculation. // They don't reflect how text layout engines really work. // For more information, please consult UAX #11. // A grapheme cluster may comprise multiple Unicode code points. // One CJK grapheme consists of one CJK code point, in NFC. // In typical fixed-width typesetting without ligature, one grapheme is finally mapped to one glyph. // Such a glyph is usually the same width as an ASCII letter, but a CJK glyph is twice. const graphemeCount = splitter.countGraphemes(cell); const cjkPoints = cell.match(cjkRegex); const width = graphemeCount + (cjkPoints?.length ?? 0); colWidth[iCol] = Math.max(colWidth[iCol] || 0, width); iCol++; } return values; }); // Normalize the num of hyphen according to the desired column length lines[delimiterRowIndex] = lines[delimiterRowIndex].map((cell, iCol) => { if (/:-+:/.test(cell)) { // :---: colAlign[iCol] = ColumnAlignment.Center; // Update the lower bound of visual `colWidth` (without padding) based on the column alignment specification colWidth[iCol] = Math.max(colWidth[iCol], delimiterRowNoPadding ? 5 - 2 : 5); // The length of all `-`, `:` chars in this delimiter cell const specWidth = delimiterRowNoPadding ? colWidth[iCol] + 2 : colWidth[iCol]; return ':' + '-'.repeat(specWidth - 2) + ':'; } else if (/:-+/.test(cell)) { // :--- colAlign[iCol] = ColumnAlignment.Left; colWidth[iCol] = Math.max(colWidth[iCol], delimiterRowNoPadding ? 4 - 2 : 4); const specWidth = delimiterRowNoPadding ? colWidth[iCol] + 2 : colWidth[iCol]; return ':' + '-'.repeat(specWidth - 1); } else if (/-+:/.test(cell)) { // ---: colAlign[iCol] = ColumnAlignment.Right; colWidth[iCol] = Math.max(colWidth[iCol], delimiterRowNoPadding ? 4 - 2 : 4); const specWidth = delimiterRowNoPadding ? colWidth[iCol] + 2 : colWidth[iCol]; return '-'.repeat(specWidth - 1) + ':'; } else { // --- colAlign[iCol] = ColumnAlignment.None; colWidth[iCol] = Math.max(colWidth[iCol], delimiterRowNoPadding ? 3 - 2 : 3); const specWidth = delimiterRowNoPadding ? colWidth[iCol] + 2 : colWidth[iCol]; return '-'.repeat(specWidth); } }); return lines.map((row, iRow) => { if (iRow === delimiterRowIndex && delimiterRowNoPadding) { return indentation + '|' + row.join('|') + '|'; } let cells = row.map((cell, iCol) => { const visualWidth = colWidth[iCol]; let jsLength = splitter.splitGraphemes(cell + ' '.repeat(visualWidth)).slice(0, visualWidth).join('').length; const cjkPoints = cell.match(cjkRegex); if (cjkPoints) { jsLength -= cjkPoints.length; } return this.alignText(cell, colAlign[iCol], jsLength); }); return indentation + '| ' + cells.join(' | ') + ' |'; }).join(doc.eol === vscode.EndOfLine.LF ? '\n' : '\r\n'); } private alignText(text: string, align: ColumnAlignment, length: number) { if (align === ColumnAlignment.Center && length > text.length) { return (' '.repeat(Math.floor((length - text.length) / 2)) + text + ' '.repeat(length)).slice(0, length); } else if (align === ColumnAlignment.Right) { return (' '.repeat(length) + text).slice(-length); } else { return (text + ' '.repeat(length)).slice(0, length); } } } class MarkdownDocumentRangeFormattingEditProvider extends MarkdownDocumentFormatter implements vscode.DocumentRangeFormattingEditProvider { provideDocumentRangeFormattingEdits(document: vscode.TextDocument, range: vscode.Range, options: vscode.FormattingOptions, token: vscode.CancellationToken) { const tables = this.detectTables(document); if (!tables || token.isCancellationRequested) { return; } const selectedTables = new Array(); tables.forEach((table) => { if (range.contains(table.range)) { selectedTables.push(table); } }); const edits: vscode.TextEdit[] = selectedTables.map((target) => { return new vscode.TextEdit( target.range, this.formatTable(target, document, options) ); }); return edits; } } ================================================ FILE: src/test/runTest.ts ================================================ import * as path from 'path'; import { runTests } from '@vscode/test-electron'; async function main() { try { // The folder containing the Extension Manifest package.json // Passed to `--extensionDevelopmentPath` const extensionDevelopmentPath = path.resolve(__dirname, '../../'); // The path to test runner // Passed to --extensionTestsPath const extensionTestsPath = path.resolve(__dirname, './suite/index'); // Download VS Code, unzip it and run the integration test await runTests({ extensionDevelopmentPath, extensionTestsPath }); } catch (err) { console.error('Failed to run tests'); process.exit(1); } } main(); ================================================ FILE: src/test/suite/index.ts ================================================ import * as path from 'path'; import * as Mocha from 'mocha'; import * as glob from 'glob'; import { resetConfiguration } from "./util/configuration"; import { openDocument, sleep, Test_Md_File_Path } from "./util/generic"; export async function run(): Promise { // Let VS Code load the test workspace. await openDocument(Test_Md_File_Path); await sleep(2000); await resetConfiguration(); // Create the mocha test const mocha = new Mocha({ color: true, ui: 'tdd', }); // Load the test suite. const testSuiteRoot = path.resolve(__dirname); const globOptions: glob.IOptions = { cwd: testSuiteRoot }; const unitTests = glob.sync("unit/**/*.test.js", globOptions); const integrationTests = glob.sync("integration/**/*.test.js", globOptions); unitTests.forEach(f => mocha.addFile(path.resolve(testSuiteRoot, f))); // Run unit tests first. integrationTests.forEach(f => mocha.addFile(path.resolve(testSuiteRoot, f))); // Run tests. return new Promise((resolve, reject): void => { try { mocha.run(failures => { // Ensure the control returns only after tests finished. if (failures > 0) { reject(new Error(`${failures} tests failed.`)); } resolve(); }); } catch (err) { console.error(err); // https://github.com/microsoft/vscode/issues/80757 throw err; } }); } ================================================ FILE: src/test/suite/integration/blockquoteEditing.test.ts ================================================ import { Selection } from 'vscode'; import { resetConfiguration } from "../util/configuration"; import { testCommand } from "../util/generic"; suite("Block quote editing.", () => { suiteSetup(async () => { await resetConfiguration(); }); suiteTeardown(async () => { await resetConfiguration(); }); test("Enter key. Continue a block quote", () => { return testCommand('markdown.extension.onEnterKey', [ '> item1' ], new Selection(0, 7, 0, 7), [ '> item1', '> ' ], new Selection(1, 2, 1, 2)); }); test("Enter key. Still continue a block quote", () => { return testCommand('markdown.extension.onEnterKey', [ '> item1', '> ' ], new Selection(1, 2, 1, 2), [ '> item1', '>', '> ' ], new Selection(2, 2, 2, 2)); }); test("Enter key. Finish a block quote", () => { return testCommand('markdown.extension.onEnterKey', [ '> item1', '> ', '> ' ], new Selection(2, 2, 2, 2), [ '> item1', '', '' ], new Selection(2, 0, 2, 0)); }); test("Enter key. Finish a block quote (corner case)", () => { return testCommand('markdown.extension.onEnterKey', [ '> ' ], new Selection(0, 2, 0, 2), [ '' ], new Selection(0, 0, 0, 0)); }); }); ================================================ FILE: src/test/suite/integration/formatting.test.ts ================================================ import { Selection, env } from "vscode"; import { resetConfiguration, updateConfiguration } from "../util/configuration"; import { testCommand } from "../util/generic"; suite("Formatting.", () => { suiteSetup(async () => { await resetConfiguration(); }); suiteTeardown(async () => { await resetConfiguration(); }); test("Toggle bold. `text |` -> `text **|**`", () => { return testCommand('markdown.extension.editing.toggleBold', ['text '], new Selection(0, 5, 0, 5), ['text ****'], new Selection(0, 7, 0, 7) ); }); test("Toggle bold. `text **|**` -> `text |`", () => { return testCommand('markdown.extension.editing.toggleBold', ['text ****'], new Selection(0, 7, 0, 7), ['text '], new Selection(0, 5, 0, 5) ); }); test("Toggle bold. `text**|**` -> `text|`", () => { return testCommand('markdown.extension.editing.toggleBold', ['text****'], new Selection(0, 6, 0, 6), ['text'], new Selection(0, 4, 0, 4) ); }); test("Toggle bold. `**text|**` -> `**text**|`", () => { return testCommand('markdown.extension.editing.toggleBold', ['**text**'], new Selection(0, 6, 0, 6), ['**text**'], new Selection(0, 8, 0, 8) ); }); test("Toggle bold. `text|` -> `**text**|`", () => { return testCommand('markdown.extension.editing.toggleBold', ['text'], new Selection(0, 4, 0, 4), ['**text**'], new Selection(0, 8, 0, 8) ); }); test("Toggle bold. `te|xt` -> `**te|xt**`", () => { return testCommand('markdown.extension.editing.toggleBold', ['text'], new Selection(0, 2, 0, 2), ['**text**'], new Selection(0, 4, 0, 4) ); }); test("Toggle bold. `**text**|` -> `text|`", () => { return testCommand('markdown.extension.editing.toggleBold', ['**text**'], new Selection(0, 8, 0, 8), ['text'], new Selection(0, 4, 0, 4) ); }); test("Toggle bold. `**te|xt**` -> `te|xt`", () => { return testCommand('markdown.extension.editing.toggleBold', ['**text**'], new Selection(0, 4, 0, 4), ['text'], new Selection(0, 2, 0, 2) ); }); test("Toggle bold. With selection. Toggle on", () => { return testCommand('markdown.extension.editing.toggleBold', ['text'], new Selection(0, 0, 0, 4), ['**text**'], new Selection(0, 0, 0, 8) ); }); test("Toggle bold. With selection. Toggle off", () => { return testCommand('markdown.extension.editing.toggleBold', ['**text**'], new Selection(0, 0, 0, 8), ['text'], new Selection(0, 0, 0, 4) ); }); test("Toggle bold. Use `__`", async () => { await updateConfiguration({ config: [["markdown.extension.bold.indicator", "__"]] }); await testCommand('markdown.extension.editing.toggleBold', ['text'], new Selection(0, 0, 0, 4), ['__text__'], new Selection(0, 0, 0, 8) ); await resetConfiguration(); }); test("Toggle italic. Use `*`", () => { return testCommand('markdown.extension.editing.toggleItalic', ['text'], new Selection(0, 0, 0, 4), ['*text*'], new Selection(0, 0, 0, 6) ); }); test("Toggle italic. Use `_`", async () => { await updateConfiguration({ config: [["markdown.extension.italic.indicator", "_"]] }); await testCommand('markdown.extension.editing.toggleItalic', ['text'], new Selection(0, 0, 0, 4), ['_text_'], new Selection(0, 0, 0, 6) ); await resetConfiguration(); }); test("Toggle strikethrough. `text|` -> `~~text~~|`", () => { return testCommand('markdown.extension.editing.toggleStrikethrough', ['text'], new Selection(0, 4, 0, 4), ['~~text~~'], new Selection(0, 8, 0, 8) ); }); test("Toggle strikethrough. List item", () => { return testCommand('markdown.extension.editing.toggleStrikethrough', ['- text text'], new Selection(0, 11, 0, 11), ['- ~~text text~~'], new Selection(0, 15, 0, 15) ); }); test("Toggle strikethrough. Task list item", () => { return testCommand('markdown.extension.editing.toggleStrikethrough', ['- [ ] text text'], new Selection(0, 15, 0, 15), ['- [ ] ~~text text~~'], new Selection(0, 19, 0, 19) ); }); // disclaimer: I am not sure about this code. Looks like it works fine, but I am not fully understand how it works underneath. test("Paste link on selected text. `|text|` -> `[text|](link)`", async () => { const link = 'http://just.a.link'; await env.clipboard.writeText(link); return testCommand('markdown.extension.editing.paste', ['text'], new Selection(0, 0, 0, 4), ['[text](' + link + ')'], new Selection(0, 5, 0, 5) ); }); }); ================================================ FILE: src/test/suite/integration/listEditing.fallback.test.ts ================================================ import { Selection } from "vscode"; import { resetConfiguration } from "../util/configuration"; import { testCommand } from "../util/generic"; suite("No list editing.", () => { suiteSetup(async () => { await resetConfiguration(); }); suiteTeardown(async () => { await resetConfiguration(); }); test("Backspace key: '- |'", () => { return testCommand('markdown.extension.onBackspaceKey', [ '- item1' ], new Selection(0, 3, 0, 3), [ '- item1' ], new Selection(0, 2, 0, 2)); }); test("Backspace key: ' - |'", () => { return testCommand('markdown.extension.onBackspaceKey', [ ' - item1' ], new Selection(0, 5, 0, 5), [ ' - item1' ], new Selection(0, 4, 0, 4)); }); test("Backspace key: '- [ ] |'", () => { return testCommand('markdown.extension.onBackspaceKey', [ '- [ ] item1' ], new Selection(0, 7, 0, 7), [ '- [ ] item1' ], new Selection(0, 6, 0, 6)); }); test("Shift tab key: ' text'", () => { return testCommand('markdown.extension.onShiftTabKey', [ ' text' ], new Selection(0, 5, 0, 5), [ 'text' ], new Selection(0, 1, 0, 1)); }); }); ================================================ FILE: src/test/suite/integration/listEditing.test.ts ================================================ import { Selection } from "vscode"; import { resetConfiguration } from "../util/configuration"; import { testCommand } from "../util/generic"; suite("List editing.", () => { suiteSetup(async () => { await resetConfiguration(); }); suiteTeardown(async () => { await resetConfiguration(); }); test("Enter key. Continue list item. '- item1|'", () => { return testCommand('markdown.extension.onEnterKey', [ '- item1' ], new Selection(0, 7, 0, 7), [ '- item1', '- ' ], new Selection(1, 2, 1, 2)); }); test("Enter key. Continue list item. '- |item1'", () => { return testCommand('markdown.extension.onEnterKey', [ '- item1' ], new Selection(0, 2, 0, 2), [ '- ', '- item1' ], new Selection(1, 2, 1, 2)); }); test("Enter key. Don't continue empty list item", () => { return testCommand('markdown.extension.onEnterKey', [ '- item1', '- ' ], new Selection(1, 2, 1, 2), [ '- item1', '', ], new Selection(1, 0, 1, 0)); }); test("Enter key. Outdent empty list item until it is top-level", () => { return testCommand('markdown.extension.onEnterKey', [ '- item1', ' - ' ], new Selection(1, 6, 1, 6), [ '- item1', '- ' ], new Selection(1, 2, 1, 2)); }); test("Enter key. List marker `*`", () => { return testCommand('markdown.extension.onEnterKey', [ '* item1'], new Selection(0, 7, 0, 7), [ '* item1', '* ' ], new Selection(1, 2, 1, 2)); }); test("Enter key. Continue GFM checkbox item. '- [ ] item1|'", () => { return testCommand('markdown.extension.onEnterKey', [ '- [ ] item1' ], new Selection(0, 11, 0, 11), [ '- [ ] item1', '- [ ] ' ], new Selection(1, 6, 1, 6)); }); test("Enter key. Continue GFM checkbox item. '- [x] |item1'", () => { return testCommand('markdown.extension.onEnterKey', [ '- [x] item1' ], new Selection(0, 6, 0, 6), [ '- [ ] ', '- [x] item1' ], new Selection(1, 6, 1, 6)); }); test("Ctrl+Enter key. Continue GFM checkbox item. '- [x] |item1'", () => { return testCommand('markdown.extension.onCtrlEnterKey', [ '- [x] item1' ], new Selection(0, 6, 0, 6), [ '- [x] item1', '- [ ] ' ], new Selection(1, 6, 1, 6)); }) test("Enter key. Keep list item text indentation. '1. item1|'", () => { return testCommand('markdown.extension.onEnterKey', [ '1. item1' ], new Selection(0, 9, 0, 9), [ '1. item1', '2. ' ], new Selection(1, 4, 1, 4)); }); test("Enter key. Keep list item text indentation. '9. item9|'", () => { return testCommand('markdown.extension.onEnterKey', [ '9. item9' ], new Selection(0, 9, 0, 9), [ '9. item9', '10. ' ], new Selection(1, 4, 1, 4)); }); test("Enter key. '- [test]|'. #122", () => { return testCommand('markdown.extension.onEnterKey', [ '- [test]' ], new Selection(0, 8, 0, 8), [ '- [test]', '- ' ], new Selection(1, 2, 1, 2)); }); test("Enter key. '> |'", () => { return testCommand('markdown.extension.onEnterKey', [ '> test' ], new Selection(0, 6, 0, 6), [ '> test', '> ' ], new Selection(1, 2, 1, 2)); }); test("Backspace key: '- |'", () => { return testCommand('markdown.extension.onBackspaceKey', [ '- item1' ], new Selection(0, 2, 0, 2), [ ' item1' ], new Selection(0, 2, 0, 2)); }); test("Backspace key: '- [ ] |'", () => { return testCommand('markdown.extension.onBackspaceKey', [ '- [ ] item1' ], new Selection(0, 6, 0, 6), [ '- item1' ], new Selection(0, 2, 0, 2)); }); test("Backspace key: ' - [ ] |'", () => { return testCommand('markdown.extension.onBackspaceKey', [ ' - [ ] item1' ], new Selection(0, 8, 0, 8), [ ' - item1' ], new Selection(0, 4, 0, 4)); }); test("Tab key. 1: '- |'", () => { return testCommand('markdown.extension.onTabKey', [ '- item1' ], new Selection(0, 2, 0, 2), [ ' - item1' ], new Selection(0, 6, 0, 6)); }); test("Tab key. 2: '- |'", () => { return testCommand('markdown.extension.onTabKey', [ '- item1' ], new Selection(0, 3, 0, 3), [ ' - item1' ], new Selection(0, 7, 0, 7)); }); test("Tab key. 3: '- [ ] |'", () => { return testCommand('markdown.extension.onTabKey', [ '- [ ] item1' ], new Selection(0, 6, 0, 6), [ ' - [ ] item1' ], new Selection(0, 10, 0, 10)); }); test("List toggle. 1: Check single line", () => { return testCommand('markdown.extension.checkTaskList', [ '- [ ] test' ], new Selection(0, 0, 0, 0), [ '- [x] test' ], new Selection(0, 0, 0, 0), ); }); test("List toggle. 2: Check multiple lines", () => { return testCommand('markdown.extension.checkTaskList', [ '- [ ] test', '- [ ] test', '- [ ] test', ], new Selection(0, 0, 1, 1), [ '- [x] test', '- [x] test', '- [ ] test', ], new Selection(0, 0, 1, 1), ); }); test("List toggle. 3: Ignore already unchecked lines when unchecking", () => { return testCommand('markdown.extension.checkTaskList', [ '- [x] test', '- [ ] test', '- [x] test', ], new Selection(0, 0, 2, 1), [ '- [ ] test', '- [ ] test', '- [ ] test', ], new Selection(0, 0, 2, 1), ); }); test("List toggle. 4: Only touch lines that has selections", () => { return testCommand('markdown.extension.checkTaskList', [ '- [ ] test', '- [ ] test', '- [ ] test', '- [ ] test', ], new Selection(0, 10, 3, 0), [ '- [ ] test', '- [x] test', '- [x] test', '- [ ] test', ], new Selection(0, 10, 3, 0), ); }); }); ================================================ FILE: src/test/suite/integration/listRenumbering.test.ts ================================================ import { Selection } from "vscode"; import { resetConfiguration } from "../util/configuration"; import { testCommand } from "../util/generic"; suite("Ordered list renumbering.", () => { suiteSetup(async () => { await resetConfiguration(); }); suiteTeardown(async () => { await resetConfiguration(); }); test("Enter key. Fix ordered marker", () => { return testCommand('markdown.extension.onEnterKey', [ '1. one', '2. two' ], new Selection(0, 6, 0, 6), [ '1. one', '2. ', '3. two' ], new Selection(1, 3, 1, 3)); }); test("Backspace key. Fix ordered marker. 1", () => { return testCommand('markdown.extension.onBackspaceKey', [ ' 1. item1' ], new Selection(0, 7, 0, 7), [ '1. item1' ], new Selection(0, 3, 0, 3)); }); test("Backspace key. Fix ordered marker. 2", () => { return testCommand('markdown.extension.onBackspaceKey', [ '1. item1', ' 5. item2' ], new Selection(1, 6, 1, 6), [ '1. item1', '2. item2' ], new Selection(1, 3, 1, 3)); }); test("Backspace key. Fix ordered marker. 3", () => { return testCommand('markdown.extension.onBackspaceKey', [ '1. item1', ' 1. item1-1', ' 2. item1-2', ' 3. item1-3', ' 4. item1-4' ], new Selection(3, 6, 3, 6), [ '1. item1', ' 1. item1-1', ' 2. item1-2', '2. item1-3', ' 1. item1-4' ], new Selection(3, 3, 3, 3)); }); test("Backspace key. Fix ordered marker. 4: Multi-line list item", () => { return testCommand('markdown.extension.onBackspaceKey', [ '1. item1', ' 1. item1-1', ' item1-2', ' 2. item1-3', ' 3. item1-4' ], new Selection(3, 6, 3, 6), [ '1. item1', ' 1. item1-1', ' item1-2', '2. item1-3', ' 1. item1-4' ], new Selection(3, 3, 3, 3)); }); test("Backspace key. Fix ordered marker. 5: Selection range", () => { return testCommand('markdown.extension.onBackspaceKey', [ '1. item1', '2. item2', ' 1. item1-1', ' 2. item1-2', ' 3. item1-3', '3. item3' ], new Selection(1, 0, 3, 0), [ '1. item1', ' 1. item1-2', ' 2. item1-3', '2. item3' ], new Selection(1, 0, 1, 0)); }); test("Backspace key. GitHub#411", () => { return testCommand('markdown.extension.onBackspaceKey', [ '1. one', '2. ', '', '# Heading', '', '3. three' ], new Selection(1, 3, 1, 3), [ '1. one', ' ', '', '# Heading', '', '3. three' ], new Selection(1, 3, 1, 3)); }); test("Backspace key. GitHub#1155 (tab indented list)", () => { return testCommand('markdown.extension.onBackspaceKey', [ '1. Item to be deleted', '2. First level 1', ' 1. Second level 1', ' 2. Second level 2', '3. First level 2', ' 1. Second level 1', ' 2. Second level 2' ], new Selection(0, 0, 1, 0), [ '1. First level 1', ' 1. Second level 1', ' 2. Second level 2', '2. First level 2', ' 1. Second level 1', ' 2. Second level 2' ], new Selection(0, 0, 0, 0)); }); test("Tab key. Fix ordered marker. 1", () => { return testCommand('markdown.extension.onTabKey', [ '2. item1' ], new Selection(0, 3, 0, 3), [ ' 1. item1' ], new Selection(0, 7, 0, 7)); }); test("Tab key. Fix ordered marker. 2", () => { return testCommand('markdown.extension.onTabKey', [ '2. [ ] item1' ], new Selection(0, 7, 0, 7), [ ' 1. [ ] item1' ], new Selection(0, 11, 0, 11)); }); test("Tab key. Fix ordered marker. 3", () => { return testCommand('markdown.extension.onTabKey', [ '1. test', ' 1. test', ' 2. test', '2. test', ' 1. test' ], new Selection(3, 3, 3, 3), [ '1. test', ' 1. test', ' 2. test', ' 3. test', ' 4. test' ], new Selection(3, 6, 3, 6)); }); test("Tab key. Fix ordered marker. 4: Multi-line list item", () => { return testCommand('markdown.extension.onTabKey', [ '1. test', ' 1. test', ' test', '2. test', ' 1. test' ], new Selection(3, 3, 3, 3), [ '1. test', ' 1. test', ' test', ' 2. test', ' 3. test' ], new Selection(3, 6, 3, 6)); }); test("Tab key. Fix ordered marker. 5: Selection range", () => { return testCommand('markdown.extension.onTabKey', [ '1. test', '2. test', ' 1. test', ' 2. test', '3. test' ], new Selection(1, 0, 3, 0), [ '1. test', ' 1. test', ' 1. test', ' 2. test', '2. test' ], // Should have been (1, 0, 3, 0) if we want to accurately mimic `editor.action.indentLines` new Selection(1, 3, 3, 0)); }); test("Move Line Up. 1: '2. |'", () => { return testCommand('markdown.extension.onMoveLineUp', [ '1. item1', '2. item2' ], new Selection(1, 3, 1, 3), [ '1. item2', '2. item1' ], new Selection(0, 3, 0, 3)); }); test("Move Line Up. 2: '2. |'", () => { return testCommand('markdown.extension.onMoveLineUp', [ '1. item1', '2. item2' ], new Selection(1, 3, 1, 3), [ '1. item2', '2. item1' ], new Selection(0, 3, 0, 3)); }); test("Move Line Up. 3: '2. [ ] |'", () => { return testCommand('markdown.extension.onMoveLineUp', [ '1. [ ] item1', '2. [ ] item2' ], new Selection(1, 0, 1, 0), [ '1. [ ] item2', '2. [ ] item1' ], new Selection(0, 0, 0, 0)); }); test("Move Line Down. 1: '1. |'", () => { return testCommand('markdown.extension.onMoveLineDown', [ '1. item1', '2. item2' ], new Selection(0, 3, 0, 3), [ '1. item2', '2. item1' ], new Selection(1, 3, 1, 3)); }); test("Move Line Down. 2: '1. |'", () => { return testCommand('markdown.extension.onMoveLineDown', [ '1. item1', '2. item2' ], new Selection(0, 3, 0, 3), [ '1. item2', '2. item1' ], new Selection(1, 3, 1, 3)); }); test("Move Line Down. 3: '1. [ ] |'", () => { return testCommand('markdown.extension.onMoveLineDown', [ '1. [ ] item1', '2. [ ] item2' ], new Selection(0, 0, 0, 0), [ '1. [ ] item2', '2. [ ] item1' ], new Selection(1, 0, 1, 0)); }); test("Copy Line Up. 1: '2. |'", () => { return testCommand('markdown.extension.onCopyLineUp', [ '1. item1', '2. item2' ], new Selection(1, 3, 1, 3), [ '1. item1', '2. item2', '3. item2' ], new Selection(1, 3, 1, 3)); }); test("Copy Line Up. 2: '2. |'", () => { return testCommand('markdown.extension.onCopyLineUp', [ '1. item1', '2. item2' ], new Selection(1, 3, 1, 3), [ '1. item1', '2. item2', '3. item2' ], new Selection(1, 3, 1, 3)); }); test("Copy Line Up. 3: '2. [ ] |'", () => { return testCommand('markdown.extension.onCopyLineUp', [ '1. [ ] item1', '2. [x] item2' ], new Selection(1, 0, 1, 0), [ '1. [ ] item1', '2. [x] item2', '3. [x] item2' ], new Selection(1, 0, 1, 0)); }); test("Copy Line Down. 1: '1. |'", () => { return testCommand('markdown.extension.onCopyLineDown', [ '1. item1', '2. item2' ], new Selection(0, 3, 0, 3), [ '1. item1', '2. item1', '3. item2' ], new Selection(1, 3, 1, 3)); }); test("Copy Line Down. 2: '1. |'", () => { return testCommand('markdown.extension.onCopyLineDown', [ '1. item1', '2. item2' ], new Selection(0, 3, 0, 3), [ '1. item1', '2. item1', '3. item2' ], new Selection(1, 3, 1, 3)); }); test("Copy Line Down. 3: '1. [ ] |'", () => { return testCommand('markdown.extension.onCopyLineDown', [ '1. [x] item1', '2. [ ] item2' ], new Selection(0, 0, 0, 0), [ '1. [x] item1', '2. [x] item1', '3. [ ] item2' ], new Selection(1, 0, 1, 0)); }); test("Indent Lines. 1: No selection range", () => { return testCommand('markdown.extension.onIndentLines', [ '1. test', '2. test', ' 1. test', '3. test' ], new Selection(1, 5, 1, 5), [ '1. test', ' 1. test', ' 2. test', '2. test' ], new Selection(1, 8, 1, 8)); }); test("Indent Lines. 2: Selection range", () => { return testCommand('markdown.extension.onIndentLines', [ '1. test', '2. test', '3. test', ' 1. test', '4. test' ], new Selection(1, 0, 3, 0), [ '1. test', ' 1. test', ' 2. test', ' 3. test', '2. test' ], // Should have been (1, 0, 3, 0) if we want to accurately mimic `editor.action.indentLines` new Selection(1, 3, 3, 0)); }); test("Outdent Lines. 1: No selection range", () => { return testCommand('markdown.extension.onOutdentLines', [ '1. test', ' 1. test', ' 2. test', '2. test' ], new Selection(1, 0, 1, 0), [ '1. test', '2. test', ' 1. test', '3. test' ], new Selection(1, 0, 1, 0)); }); test("Outdent Lines. 2: Selection range", () => { return testCommand('markdown.extension.onOutdentLines', [ '1. test', ' 1. test', ' 2. test', ' 3. test', '2. test' ], new Selection(1, 0, 3, 0), [ '1. test', '2. test', '3. test', ' 1. test', '4. test' ], new Selection(1, 0, 3, 0)); }); }); ================================================ FILE: src/test/suite/integration/tableFormatter.test.ts ================================================ import { Selection } from "vscode"; import { resetConfiguration, updateConfiguration } from "../util/configuration"; import { testCommand } from "../util/generic"; suite("Table formatter.", () => { suiteSetup(async () => { await resetConfiguration(); }); suiteTeardown(async () => { await resetConfiguration(); }); test("Normal", () => { return testCommand('editor.action.formatDocument', [ '| a | b |', '| --- | --- |', '| c | d |' ], new Selection(0, 0, 0, 0), [ '| a | b |', '| --- | --- |', '| c | d |' ], new Selection(0, 0, 0, 0)); }); test("Normal, without leading and trailing pipes", () => { return testCommand('editor.action.formatDocument', [ '', 'a |b', '---| ---', 'c|de' ], new Selection(0, 0, 0, 0), [ '', '| a | b |', '| --- | --- |', '| c | de |' ], new Selection(0, 0, 0, 0)); }); test("Plain pipes should always be cell separators", () => { return testCommand('editor.action.formatDocument', [ '| a | b |', '| --- | --- |', '| c `a|b` | d |' ], new Selection(0, 0, 0, 0), [ '| a | b |', '| ---- | --- |', '| c `a | b` | d |' ], new Selection(0, 0, 0, 0)); }); // https://github.github.com/gfm/#example-200 test(String.raw`Contains escaped pipes '\|'`, () => { return testCommand('editor.action.formatDocument', [ '| a | b |', '| --- | --- |', '| c `a\\|b` | d |', '| c **a\\|b** | d |' ], new Selection(0, 0, 0, 0), [ '| a | b |', '| ---------- | --- |', '| c `a\\|b` | d |', '| c **a\\|b** | d |' ], new Selection(0, 0, 0, 0)); }); test("CJK characters", () => { return testCommand('editor.action.formatDocument', [ '| a | b |', '| --- | --- |', '| c 中文 | d |' ], new Selection(0, 0, 0, 0), [ '| a | b |', '| ------ | --- |', '| c 中文 | d |' ], new Selection(0, 0, 0, 0)); }); test("Not table", () => { return testCommand('editor.action.formatDocument', [ 'a | b', '---' ], new Selection(0, 0, 0, 0), [ 'a | b', '---' ], new Selection(0, 0, 0, 0)); }); test("Indented table, belongs to a list item", () => { return testCommand('editor.action.formatDocument', [ '1. A list', ' | a | b |', ' | --- | --- |', ' | c | d |' ], new Selection(0, 0, 0, 0), [ '1. A list', ' | a | b |', ' | --- | --- |', ' | c | d |' ], new Selection(0, 0, 0, 0)); }); test("Mixed-indented table (no normalization)", () => { return testCommand('editor.action.formatDocument', [ ' | a | b |', ' | --- | --- |', ' | c | d |' ], new Selection(0, 0, 0, 0), [ ' | a | b |', ' | --- | --- |', ' | c | d |' ], new Selection(0, 0, 0, 0)); }); // This is definitely WRONG. It may produce an indented code block! // test("Mixed-indented table (normalization)", async () => { // await updateConfiguration({ config: [["markdown.extension.tableFormatter.normalizeIndentation", true]] }); // await testCommand('editor.action.formatDocument', // [ // ' | a | b |', // ' | --- | --- |', // ' | c | d |' // ], // new Selection(0, 0, 0, 0), // [ // ' | a | b |', // ' | --- | --- |', // ' | c | d |' // ], // new Selection(0, 0, 0, 0) // ); // await resetConfiguration(); // }); test("Mixed ugly table", () => { return testCommand('editor.action.formatDocument', [ '| a | b | c ', ' --- | --- | :---:', ' c | d | e |' ], new Selection(0, 0, 0, 0), [ '| a | b | c |', '| --- | --- | :---: |', '| c | d | e |' ], new Selection(0, 0, 0, 0)); }); test("Alignment and padding within cells", () => { return testCommand('editor.action.formatDocument', [ '| Column L | Column C | Column R |', '| ---- | :----: | ----: |', '| c | d | e |', '| fg | hi | jk |' ], new Selection(0, 0, 0, 0), [ '| Column L | Column C | Column R |', '| -------- | :------: | -------: |', '| c | d | e |', '| fg | hi | jk |' ], new Selection(0, 0, 0, 0)); }); test("Contains escaped pipes '\\|' in last data cell", () => { return testCommand('editor.action.formatDocument', [ '', // Changing the first expected char somehow crashes the selection logic and the test fails 'a|b', '---|---', 'c|d\\|e' ], new Selection(0, 0, 0, 0), [ '', '| a | b |', '| --- | ---- |', '| c | d\\|e |' ], new Selection(0, 0, 0, 0)); }); test("Reduced width table", () => { return testCommand('editor.action.formatDocument', [ '| a | b |', '| ------- | ---- |', '| c | d |' ], new Selection(0, 0, 0, 0), [ '| a | b |', '| --- | --- |', '| c | d |' ], new Selection(0, 0, 0, 0)); }); test("Empty cell with nothing between pipes (#381)", () => { return testCommand('editor.action.formatDocument', [ '| a | b | c |', '| --- | --- | --- |', '| a || c |' ], new Selection(0, 0, 0, 0), [ '| a | b | c |', '| --- | --- | --- |', '| a | | c |' ], new Selection(0, 0, 0, 0)); }); test("CTL: Thai", () => { return testCommand('editor.action.formatDocument', [ '| คุณครู | รั้วริม | ไอ้หนูน้อย |', '| --- | --- | --- |', '| Teacher | The border | kids |' ], new Selection(0, 0, 0, 0), [ '| คุณครู | รั้วริม | ไอ้หนูน้อย |', '| ------- | ---------- | ------- |', '| Teacher | The border | kids |' ], new Selection(0, 0, 0, 0)); }); test("Left-aligned single column table (#431)", () => { return testCommand('editor.action.formatDocument', [ '| h |', '| --- |', '| a |' ], new Selection(0, 0, 0, 0), [ '| h |', '| --- |', '| a |' ], new Selection(0, 0, 0, 0)); }); test("Centre-aligned single column table (#431)", () => { return testCommand('editor.action.formatDocument', [ '| h |', '| :---: |', '| a |' ], new Selection(0, 0, 0, 0), [ '| h |', '| :---: |', '| a |' ], new Selection(0, 0, 0, 0)); }); test("Right-aligned single column table (#431)", () => { return testCommand('editor.action.formatDocument', [ '| h |', '| ---: |', '| a |' ], new Selection(0, 0, 0, 0), [ '| h |', '| ---: |', '| a |' ], new Selection(0, 0, 0, 0)); }); test("Delimiter row without padding", async () => { await updateConfiguration({ config: [["markdown.extension.tableFormatter.delimiterRowNoPadding", true]] }); await testCommand('editor.action.formatDocument', [ '| a | b | c | d |', '| --- | :--- | ---: | :---: |', '| w | x | y | z |' ], new Selection(0, 0, 0, 0), [ '| a | b | c | d |', '|---|:---|---:|:---:|', '| w | x | y | z |' ], new Selection(0,0,0,0)); await resetConfiguration(); }); test("Delimiter row without padding, longer data", async () => { await updateConfiguration({ config: [["markdown.extension.tableFormatter.delimiterRowNoPadding", true]] }); await testCommand('editor.action.formatDocument', [ '| a | b-long | c | d-longest |', '| --- | :--- | ---: | :---: |', '| w | x | y-longer | z |' ], new Selection(0, 0, 0, 0), [ '| a | b-long | c | d-longest |', '|---|:-------|---------:|:---------:|', '| w | x | y-longer | z |' ], new Selection(0,0,0,0)); await resetConfiguration(); }); }); ================================================ FILE: src/test/suite/integration/toc.test.ts ================================================ import { Selection } from "vscode"; import { resetConfiguration, updateConfiguration } from "../util/configuration"; import { testCommand, Test_Md_File_Path } from "../util/generic"; suite("TOC.", () => { suiteSetup(async () => { await resetConfiguration(); }); suiteTeardown(async () => { await resetConfiguration(); }); test("Create", () => { return testCommand('markdown.extension.toc.create', [ '# Section 1', '', '## Section 1.1', '', '# Section 2', '', '' ], new Selection(6, 0, 6, 0), [ '# Section 1', '', '## Section 1.1', '', '# Section 2', '', '- [Section 1](#section-1)', ' - [Section 1.1](#section-11)', '- [Section 2](#section-2)', '', ], new Selection(9, 0, 9, 0)); }); test("Update", () => { return testCommand('markdown.extension.toc.update', [ '# Section 1', '', '## Section 1.1', '', '# Section 2', '', '## Section 2.1', '', '- [Section 1](#section-1)', ' - [Section 1.1](#section-11)', '- [Section 2](#section-2)' ], new Selection(0, 0, 0, 0), [ '# Section 1', '', '## Section 1.1', '', '# Section 2', '', '## Section 2.1', '', '- [Section 1](#section-1)', ' - [Section 1.1](#section-11)', '- [Section 2](#section-2)', ' - [Section 2.1](#section-21)', '', ], new Selection(0, 0, 0, 0)); }); test("Update (ordered list)", async () => { await updateConfiguration({ config: [["markdown.extension.toc.orderedList", true]] }); await testCommand('markdown.extension.toc.update', [ '# Section 1', '', '## Section 1.1', '', '# Section 2', '', '## Section 2.1', '', '1. [Section 1](#section-1)', ' 1. [Section 1.1](#section-11)', '2. [Section 2](#section-2)' ], new Selection(0, 0, 0, 0), [ '# Section 1', '', '## Section 1.1', '', '# Section 2', '', '## Section 2.1', '', '1. [Section 1](#section-1)', ' 1. [Section 1.1](#section-11)', '2. [Section 2](#section-2)', ' 1. [Section 2.1](#section-21)', '', ], new Selection(0, 0, 0, 0) ); await resetConfiguration(); }); test("Create (levels 2..3)", async () => { await updateConfiguration({ config: [["markdown.extension.toc.levels", "2..3"]] }); await testCommand("markdown.extension.toc.create", [ '# Section 1', '', '## Section 1.1', '', '### Section 1.1.1', '', '#### Section 1.1.1.1', '', '# Section 2', '', '## Section 2.1', '', '### Section 2.1.1', '', '#### Section 2.1.1.1', '', '' ], new Selection(16, 0, 16, 0), [ '# Section 1', '', '## Section 1.1', '', '### Section 1.1.1', '', '#### Section 1.1.1.1', '', '# Section 2', '', '## Section 2.1', '', '### Section 2.1.1', '', '#### Section 2.1.1.1', '', '- [Section 1.1](#section-11)', ' - [Section 1.1.1](#section-111)', '- [Section 2.1](#section-21)', ' - [Section 2.1.1](#section-211)', '', ], new Selection(20, 0, 20, 0) ); await resetConfiguration(); }); test("Update (levels 2..3)", async () => { await updateConfiguration({ config: [["markdown.extension.toc.levels", "2..3"]] }); await testCommand("markdown.extension.toc.update", [ '# Section 1', '', '## Section 1.1', '', '### Section 1.1.1', '', '#### Section 1.1.1.1', '', '# Section 2', '', '## Section 2.1', '', '- [Section 1.1](#section-11)', ' - [Section 1.1.1](#section-111)', '- [Section 2.1](#section-21)', ' - [Section 2.1.1](#section-211)', ], new Selection(0, 0, 0, 0), [ '# Section 1', '', '## Section 1.1', '', '### Section 1.1.1', '', '#### Section 1.1.1.1', '', '# Section 2', '', '## Section 2.1', '', '- [Section 1.1](#section-11)', ' - [Section 1.1.1](#section-111)', '- [Section 2.1](#section-21)', '', ], new Selection(0, 0, 0, 0) ); await resetConfiguration(); }); test("Ordered list (list markers larger than 9)", async () => { await updateConfiguration({ config: [["markdown.extension.toc.orderedList", true]] }); await testCommand('markdown.extension.toc.create', [ '# H1', '# H2', '# H3', '# H4', '# H5', '# H6', '# H7', '# H8', '# H9', '# H10', '## H11', '### H12', '', '' ], new Selection(13, 0, 13, 0), [ '# H1', '# H2', '# H3', '# H4', '# H5', '# H6', '# H7', '# H8', '# H9', '# H10', '## H11', '### H12', '', '1. [H1](#h1)', '2. [H2](#h2)', '3. [H3](#h3)', '4. [H4](#h4)', '5. [H5](#h5)', '6. [H6](#h6)', '7. [H7](#h7)', '8. [H8](#h8)', '9. [H9](#h9)', '10. [H10](#h10)', ' 1. [H11](#h11)', ' 1. [H12](#h12)', '' ], new Selection(25, 0, 25, 0) ); await resetConfiguration(); }); test("Setext headings", () => { return testCommand('markdown.extension.toc.create', [ 'Section 1', '===', '', 'Section 1.1', '---', '', '' ], new Selection(6, 0, 6, 0), [ 'Section 1', '===', '', 'Section 1.1', '---', '', '- [Section 1](#section-1)', ' - [Section 1.1](#section-11)', '', ], new Selection(8, 0, 8, 0)); }); test("ATX Heading closing sequence", () => { return testCommand('markdown.extension.toc.create', [ '# H1 #', '## H1.1 ###', '## H1.2 ## ', '# H2 ## foo', '# H3#', '# H4 \\###', '# H5 #\\##', '# H6 \\#', '', '' ], new Selection(9, 0, 9, 0), [ '# H1 #', '## H1.1 ###', '## H1.2 ## ', '# H2 ## foo', '# H3#', '# H4 \\###', '# H5 #\\##', '# H6 \\#', '', '- [H1](#h1)', ' - [H1.1](#h11)', ' - [H1.2](#h12)', '- [H2 ## foo](#h2--foo)', '- [H3#](#h3)', '- [H4 ###](#h4-)', '- [H5 ###](#h5-)', '- [H6 #](#h6-)', '', ], new Selection(17, 0, 17, 0)); }); test("Update multiple TOCs", async () => { await updateConfiguration({ config: [["markdown.extension.toc.slugifyMode", "github"]] }); await testCommand('markdown.extension.toc.update', [ '# Head 1', '# Head 2', '', '- [Head 1](#head-1)', '- [Head 2](#head-2)', '- [Head 3](#head-3)', '', '- [Head 1](#head-1)', '- [Head 2](#head-2)', '- [Head 3](#head-3)', '', '# Head 3', '# Head 4' ], new Selection(0, 0, 0, 0), [ '# Head 1', '# Head 2', '', '- [Head 1](#head-1)', '- [Head 2](#head-2)', '- [Head 3](#head-3)', '- [Head 4](#head-4)', '', '- [Head 1](#head-1)', '- [Head 2](#head-2)', '- [Head 3](#head-3)', '- [Head 4](#head-4)', '', '# Head 3', '# Head 4' ], new Selection(0, 0, 0, 0) ); await resetConfiguration(); }); test("Exclude omitted headings (`toc.omittedFromToc`)", async () => { await updateConfiguration({ config: [["markdown.extension.toc.omittedFromToc", { [Test_Md_File_Path.fsPath]: [ // With more than one space between sharps and text. '# Introduction', // With spaces before sharps ans special chars. ' ## Ignored - with "special" ~ chars', '## Underlined heading' ], 'not-ignored.md': ['# Head 1'] }]] }); await testCommand( 'markdown.extension.toc.create', [ '', '', '# Introduction', '## Sub heading (should be ignored, too)', '# Head 1', '', // Underlined heading should be ignored, too. 'Underlined heading', '------------------', '', '- [Head 1](#head-1)', '- [Head 2](#head-2)', '- [Head 3](#head-3)', '', '- [Head 1](#head-1)', '- [Head 2](#head-2)', '- [Head 3](#head-3)', '', '# Head 3', '## Ignored - with "special" ~ chars', // Second "Introduction" heading is visible (should have a number suffix in ToC). '## Introduction', '# Head 4' ], new Selection(0, 0, 0, 0), [ '- [Head 1](#head-1)', '- [Head 3](#head-3)', ' - [Introduction](#introduction-1)', '- [Head 4](#head-4)', '', '', '# Introduction', '## Sub heading (should be ignored, too)', '# Head 1', '', 'Underlined heading', '------------------', '', '- [Head 1](#head-1)', '- [Head 2](#head-2)', '- [Head 3](#head-3)', '', '- [Head 1](#head-1)', '- [Head 2](#head-2)', '- [Head 3](#head-3)', '', '# Head 3', '## Ignored - with "special" ~ chars', '## Introduction', '# Head 4' ], new Selection(4, 0, 4, 0) ); await resetConfiguration(); }); test("Inline ", () => { return testCommand('markdown.extension.toc.create', [ '# Section 1', '', '## Section 1.1 ', '', '# Section 2', '', '' ], new Selection(6, 0, 6, 0), [ '# Section 1', '', '## Section 1.1 ', '', '# Section 2', '', '- [Section 1](#section-1)', '- [Section 2](#section-2)', '', ], new Selection(8, 0, 8, 0)); }); test(" in previous line", () => { return testCommand('markdown.extension.toc.create', [ '# Section 1', '', '', '## Section 1.1', '', '', '## Section 1.2', '', '# Section 2', '', '' ], new Selection(10, 0, 10, 0), [ '# Section 1', '', '', '## Section 1.1', '', '', '## Section 1.2', '', '# Section 2', '', '- [Section 1](#section-1)', '- [Section 2](#section-2)', '', ], new Selection(12, 0, 12, 0)); }); test("Ignore fenced code blocks", () => { return testCommand('markdown.extension.toc.create', [ '# Section 1', '', '```', '## Section 1.1', '```', '', '# Section 2', '', '' ], new Selection(8, 0, 8, 0), [ '# Section 1', '', '```', '## Section 1.1', '```', '', '# Section 2', '', '- [Section 1](#section-1)', '- [Section 2](#section-2)', '', ], new Selection(10, 0, 10, 0)); }); test("Ignore indented code blocks created by TAB (U+0009)", () => { return testCommand('markdown.extension.toc.create', [ '# Section 1', '', '\t```', '\t## Section 1.1', '\t```', '', '# Section 2', '', '' ], new Selection(8, 0, 8, 0), [ '# Section 1', '', '\t```', '\t## Section 1.1', '\t```', '', '# Section 2', '', '- [Section 1](#section-1)', '- [Section 2](#section-2)', '', ], new Selection(10, 0, 10, 0)); }); test("Ignore code blocks. TOC update", () => { return testCommand('markdown.extension.toc.update', [ '# H1', '# H2', '# H3', '', '```', '- [H1](#h1)', '- [H2](#h2)', '', '```' ], new Selection(0, 0, 0, 0), [ '# H1', '# H2', '# H3', '', '```', '- [H1](#h1)', '- [H2](#h2)', '', '```' ], new Selection(0, 0, 0, 0)); }); test("Markdown syntax in headings", () => { return testCommand('markdown.extension.toc.create', [ '# [text](link)', '# [text2][label]', '# [collapsed][ref]', '# **bold**', '# *it1* _it2_', '# `code`', '# 1. Heading', '# 1) Heading', '[ref]: uri', '', '' ], new Selection(10, 0, 10, 0), [ '# [text](link)', '# [text2][label]', '# [collapsed][ref]', '# **bold**', '# *it1* _it2_', '# `code`', '# 1. Heading', '# 1) Heading', '[ref]: uri', '', '- [text](#text)', '- [\\[text2\\]\\[label\\]](#text2label)', '- [collapsed](#collapsed)', '- [**bold**](#bold)', '- [*it1* _it2_](#it1-it2)', '- [`code`](#code)', '- [1. Heading](#1-heading)', '- [1) Heading](#1-heading-1)', '', ], new Selection(18, 0, 18, 0)); }); test("Add section numbers", () => { return testCommand('markdown.extension.toc.addSecNumbers', [ '---', 'title: test', '---', '# Heading 1', '## Heading 1.1', ' Heading 2', '===', '```markdown', '# _Heading 3', '```', '## Heading 2.1', '## _Heading 2.2 ', '', '## Heading 2.2', ], new Selection(0, 0, 0, 0), [ '---', 'title: test', '---', '# 1. Heading 1', '## 1.1. Heading 1.1', ' 2. Heading 2', '===', '```markdown', '# _Heading 3', '```', '## 2.1. Heading 2.1', '## _Heading 2.2 ', '', '## 2.2. Heading 2.2', ], new Selection(0, 0, 0, 0)); }); test("Update section numbers", () => { return testCommand('markdown.extension.toc.addSecNumbers', [ '---', 'title: test', '---', '# Heading 1', '## 1.2. Heading 1.1', '2. Not Heading', '===', '```markdown', '# _Heading 3', '```', '## 2.1.1. Heading 1.2', '## _Heading 2.2 ', '', '## 2.2. Heading 1.3', ], new Selection(0, 0, 0, 0), [ '---', 'title: test', '---', '# 1. Heading 1', '## 1.1. Heading 1.1', '2. Not Heading', '===', '```markdown', '# _Heading 3', '```', '## 1.2. Heading 1.2', '## _Heading 2.2 ', '', '## 1.3. Heading 1.3', ], new Selection(0, 0, 0, 0)); }); test("Remove section numbers", () => { return testCommand('markdown.extension.toc.removeSecNumbers', [ '---', 'title: test', '---', '# 1. Heading 1', '## 1.1. Heading 1.1', '2. Not Heading', '===', '```markdown', '# _Heading 3', '```', '## 2.1. Heading 2.1', '## _Heading 2.2 ', '', '## 2.2. Heading 2.2', ], new Selection(0, 0, 0, 0), [ '---', 'title: test', '---', '# Heading 1', '## Heading 1.1', '2. Not Heading', '===', '```markdown', '# _Heading 3', '```', '## Heading 2.1', '## _Heading 2.2 ', '', '## Heading 2.2', ], new Selection(0, 0, 0, 0)); }); test("Section numbering starting level", () => { return testCommand('markdown.extension.toc.addSecNumbers', [ '# Heading ', '## Heading 1', '## Heading 2', '## Heading 3', ], new Selection(0, 0, 0, 0), [ '# Heading ', '## 1. Heading 1', '## 2. Heading 2', '## 3. Heading 3', ], new Selection(0, 0, 0, 0)); }); test("Section numbering and `toc.levels`", async () => { await updateConfiguration({ config: [["markdown.extension.toc.levels", "2..6"]] }); await testCommand('markdown.extension.toc.addSecNumbers', [ '# Heading', '## Heading 1', '## Heading 2', '## Heading 3', ], new Selection(0, 0, 0, 0), [ '# Heading', '## 1. Heading 1', '## 2. Heading 2', '## 3. Heading 3', ], new Selection(0, 0, 0, 0) ); await resetConfiguration(); }); }); ================================================ FILE: src/test/suite/unit/linksRecognition.test.ts ================================================ import * as assert from 'assert'; import * as formatting from '../../../formatting'; suite("LinkRecognition.", () => { // commented test cases fails. const links = [ 'https://github.com/gliderlabs/docker-alpine/blob/master/docs/usage.md#disabling-cache', 'https://github.com/yzhang-gh/vscode-markdown/commit/bc50362b9bf86e298bd455c3cb7fad42dc550a45', 'https://ru.wikipedia.org/wiki/%D0%9F%D0%B5%D1%80%D1%84%D0%B5%D0%BA%D1%86%D0%B8%D0%BE%D0%BD%D0%B8%D0%B7%D0%BC_(%D0%BF%D1%81%D0%B8%D1%85%D0%BE%D0%BB%D0%BE%D0%B3%D0%B8%D1%8F)', 'http://тест.рф', // This is from django test cases https://github.com/django/django/blob/stable/2.2.x/tests/validators/valid_urls.txt 'http://www.djangoproject.com/', 'HTTP://WWW.DJANGOPROJECT.COM/', 'http://localhost/', 'http://example.com/', 'http://example.com./', 'http://www.example.com/', 'http://www.example.com:8000/test', 'http://valid-with-hyphens.com/', 'http://subdomain.example.com/', 'http://a.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'http://200.8.9.10/', 'http://200.8.9.10:8000/test', 'http://su--b.valid-----hyphens.com/', 'http://example.com?something=value', 'http://example.com/index.php?something=value&another=value2', 'https://example.com/', 'ftp://example.com/', 'ftps://example.com/', 'http://foo.com/blah_blah', 'http://foo.com/blah_blah/', 'http://foo.com/blah_blah_(wikipedia)', 'http://foo.com/blah_blah_(wikipedia)_(again)', 'http://www.example.com/wpstyle/?p=364', 'https://www.example.com/foo/?bar=baz&inga=42&quux', 'http://✪df.ws/123', 'http://userid:password@example.com:8080', 'http://userid:password@example.com:8080/', 'http://userid@example.com', 'http://userid@example.com/', 'http://userid@example.com:8080', 'http://userid@example.com:8080/', 'http://userid:password@example.com', 'http://userid:password@example.com/', 'http://142.42.1.1/', 'http://142.42.1.1:8080/', 'http://➡.ws/䨹', 'http://⌘.ws', 'http://⌘.ws/', 'http://foo.com/blah_(wikipedia)#cite-1', 'http://foo.com/blah_(wikipedia)_blah#cite-1', 'http://foo.com/unicode_(✪)_in_parens', 'http://foo.com/(something)?after=parens', 'http://☺.damowmow.com/', 'http://djangoproject.com/events/#&product=browser', 'http://j.mp', 'ftp://foo.bar/baz', 'http://foo.bar/?q=Test%20URL-encoded%20stuff', 'http://مثال.إختبار', 'http://例子.测试', 'http://उदाहरण.परीक्षा', 'http://-.~_!$&\'()*+,;=%40:80%2f@example.com', 'http://xn--7sbb4ac0ad0be6cf.xn--p1ai', 'http://1337.net', 'http://a.b-c.de', 'http://223.255.255.254', 'ftps://foo.bar/', 'http://10.1.1.254', 'http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html', 'http://[::192.9.5.5]/ipng', 'http://[::ffff:192.9.5.5]/ipng', 'http://[::1]:8080/', 'http://0.0.0.0/', 'http://255.255.255.255', 'http://224.0.0.0', 'http://224.1.1.1', 'http://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.example.com', 'http://example.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com', 'http://example.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'http://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'http://dashintld.c-m', 'http://multipledashintld.a-b-c', 'http://evenmoredashintld.a---c', 'http://dashinpunytld.xn---c', ]; const notLinks = [ ' Twitter < /a>', 'http://http://www.yahoo.com', // This is from django test cases https://github.com/django/django/blob/stable/2.2.x/tests/validators/invalid_urls.txt 'foo', 'http://', 'http://example', 'http://example.', 'http://.com', 'http://invalid-.com', 'http://-invalid.com', 'http://invalid.com-', 'http://invalid.-com', 'http://inv-.alid-.com', 'http://inv-.-alid.com', // failing because I don't restrict schemes // 'file://localhost/path', // 'git://example.com/', 'http://.', 'http://..', 'http://../', 'http://?', 'http://??', 'http://??/', 'http://#', 'http://##', 'http://##/', 'http://foo.bar?q=Spaces should be encoded', '//', '//a', '///a', '///', 'http:///a', 'foo.com', 'rdar://1234', 'h://test', 'http:// shouldfail.com', ':// should fail', 'http://foo.bar/foo(bar)baz quux', 'http://-error-.invalid/', 'http://dashinpunytld.trailingdot.xn--.', 'http://dashinpunytld.xn---', 'http://-a.b.co', 'http://a.b-.co', 'http://a.-b.co', 'http://a.b-.c.co', 'http: /', 'http://', 'http://', 'http://1.1.1.1.1', 'http://123.123.123', 'http://3628126748', 'http://123', 'http://.www.foo.bar/', 'http://.www.foo.bar./', // 'http://[::1:2::3]:8080/', // this does not pass because of simplified ipv6 regexp 'http://[]', 'http://[]:8080', 'http://example..com/', 'http://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.example.com', 'http://example.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com', 'http://example.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', // failing because I don't check the maximum length of a full host name // 'http://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaa', 'https://test.[com', 'http://foo@bar@example.com', 'http://foo/bar@example.com', 'http://foo:bar:baz@example.com', 'http://foo:bar@baz@example.com', 'http://foo:bar/baz@example.com', 'http://invalid-.com/?m=foo@example.com', ]; /** * Returns function to test if linkString link recognition equal to isLinkExpected. * * @param boolean isLinkExpected * * @return function(linkString: string) */ function genIsLinkFunc(isLinkExpected: boolean) { return (linkString: string) => { let testName = [ "String", "'" + linkString + "'", "should", (isLinkExpected) ? null : "not", "be recognized as a link" ].join(" "); test(testName, () => { let isLinkActual = formatting.isSingleLink(linkString); assert.strictEqual(isLinkActual, isLinkExpected); }); }; } const assertIsLink = genIsLinkFunc(true); links.forEach(assertIsLink); const assertIsNotLink = genIsLinkFunc(false); notLinks.forEach(assertIsNotLink); }); ================================================ FILE: src/test/suite/unit/slugify.test.ts ================================================ import * as assert from 'assert'; import SlugifyMode from "../../../contract/SlugifyMode"; import { importZolaSlug, slugify } from "../../../util/slugify"; type ICase = readonly [string, string]; /** * `mode -> [rawContent, slug]` */ const cases: Readonly> = { [SlugifyMode.AzureDevOps]: [ ["A !\"#$%&'()*+,-./:;<=>?@[\\\\]^_`{|}~", "a-!%22%23%24%25%26'()*%2B%2C-.%2F%3A%3B%3C%3D%3E%3F%40%5B%5C%5C%5D%5E_%60%7B%7C%7D~"], ["W\u{0020}\u{00A0}\u{2003}\u{202F}\u{205F}\u{3000}\u{1680}S", "w-------s"], ["1\u{0020}\u{007F}\u{0080}\u{07FF}\u{0800}\u{FFFF}\u{10000}\u{10FFFF}2", "%31%2D%7F%C2%80%DF%BF%E0%A0%80%EF%BF%BF%F0%90%80%80%F4%8F%BF%BF%32"], // Test UTF-8 encoding. ["1. Hi, there!!!", "%31%2E%2D%68%69%2C%2D%74%68%65%72%65%21%21%21"], ["Hi, there!!! without and index number", "hi%2C-there!!!-without-and-index-number"], ["Design & Process", "design-%26-process"], ["These symbols -\_.!~\*'(), should remain", "these-symbols--_.!~*'()%2C-should-remain"], ["1. These symbols -\_.!~\*'(), should be fully encoded because of the number in front", "%31%2E%2D%74%68%65%73%65%2D%73%79%6D%62%6F%6C%73%2D%2D%5F%2E%21%7E%2A%27%28%29%2C%2D%73%68%6F%75%6C%64%2D%62%65%2D%66%75%6C%6C%79%2D%65%6E%63%6F%64%65%64%2D%62%65%63%61%75%73%65%2D%6F%66%2D%74%68%65%2D%6E%75%6D%62%65%72%2D%69%6E%2D%66%72%6F%6E%74"], ], [SlugifyMode.BitbucketCloud]: [], [SlugifyMode.GitHub]: [ ["foo _italic_ bar", "foo-italic-bar"], ["foo_foo_bar", "foo_foo_bar"], ["`a.b` c", "ab-c"], ["Via [remark-cli][]", "via-remark-cli"], ["1. not a list", "1-not-a-list"], ["1) not a list", "1-not-a-list"], ["foo & < >  \"foo\"", "foo---foo"], ["$\\LaTeX equations$", "latex-equations"], ["Секция 1.1", "секция-11"], // Cyrillic. ["Section 中文", "section-中文"], // CJK. ], [SlugifyMode.GitLab]: [ ["foo _italic_ bar", "foo-italic-bar"], ["foo_foo_bar", "foo_foo_bar"], ["`a.b` c", "ab-c"], ["Via [remark-cli][]", "via-remark-cli"], ["1. not a list", "1-not-a-list"], ["1) not a list", "1-not-a-list"], ["A + B", "a-b"], // One dash. (#469) ["foo & < >  \"foo\"", "foo-foo"], ["1", "anchor-1"], // GitLab adds "anchor-" before digit-only IDs ["Секция 1.1", "секция-11"], // Cyrillic. (#469) ], [SlugifyMode.Gitea]: [ ["foo _italic_ bar", "foo-italic-bar"], ["foo_foo_bar", "foo-foo-bar"], ["`a.b` c", "a-b-c"], ["Via [remark-cli][]", "via-remark-cli"], ["1. not a list", "1-not-a-list"], ["1) not a list", "1-not-a-list"], ["foo & < >  \"foo\"", "foo-foo"], ["$\\LaTeX equations$", "latex-equations"], [":checkered_flag: with emoji shortname", "checkered-flag-with-emoji-shortname"], ["Секция 1.1", "секция-1-1"], // Cyrillic. ], [SlugifyMode.VisualStudioCode]: [ ["foo _italic_ bar", "foo-italic-bar"], ["`a.b` c", "ab-c"], ["Via [remark-cli][]", "via-remark-cli"], ["1. not a list", "1-not-a-list"], ], [SlugifyMode.Zola]: [ [ "this is some example [text](https://www.url.com) haha [fun](http://another.example)", "this-is-some-example-text-haha-fun", ], [ "Check out this [link](http://example.com) and this [another one](https://another.com)!", "check-out-this-link-and-this-another-one", ], ["No links here!", "no-links-here"], [ "[Edge cases](https://edge.com) lead to [interesting](http://test.com?query=example) results. 大時代", "edge-cases-lead-to-interesting-results-da-shi-dai", ], ["にでも長所と短所がある", "nidemochang-suo-toduan-suo-gaaru"], [ "命来犯天写最大巡祭視死乃読", "ming-lai-fan-tian-xie-zui-da-xun-ji-shi-si-nai-du", ], [ "국무위원은 국무총리의 제청으로 대통령이 임명한다", "gugmuwiweoneun-gugmucongriyi-jeceongeuro-daetongryeongi-immyeonghanda", ], ], }; const modeName: Readonly> = { [SlugifyMode.AzureDevOps]: "Azure DevOps", [SlugifyMode.BitbucketCloud]: "Bitbucket Cloud", [SlugifyMode.GitHub]: "GitHub", [SlugifyMode.GitLab]: "GitLab", [SlugifyMode.Gitea]: "Gitea", [SlugifyMode.VisualStudioCode]: "VS Code", [SlugifyMode.Zola]: "Zola", }; suite("Slugify function.", () => { importZolaSlug().then(() => { // import the wasm module before running the tests for (const [group, testCase] of Object.entries(cases) as ReadonlyArray<[SlugifyMode, readonly ICase[]]>) { for (const [rawContent, slug] of testCase) { globalThis.test(`(${modeName[group]}) ${rawContent} → ${slug}`, () => { assert.strictEqual(slugify(rawContent, { mode: group }), slug); }); } } }); }); ================================================ FILE: src/test/suite/util/configuration.ts ================================================ import * as vscode from "vscode"; /** * `[id, value]` */ export type IConfigurationRecord = readonly [string, T]; const Default_Config: readonly IConfigurationRecord[] = [ ["markdown.extension.toc.levels", "1..6"], ["markdown.extension.toc.unorderedList.marker", "-"], ["markdown.extension.toc.orderedList", false], ["markdown.extension.toc.plaintext", false], ["markdown.extension.toc.updateOnSave", true], ["markdown.extension.toc.slugifyMode", "github"], ["markdown.extension.toc.omittedFromToc", Object.create(null)], ["markdown.extension.preview.autoShowPreviewToSide", false], ["markdown.extension.orderedList.marker", "ordered"], ["markdown.extension.italic.indicator", "*"], ["markdown.extension.bold.indicator", "**"], ["markdown.extension.tableFormatter.normalizeIndentation", false], ["markdown.extension.tableFormatter.delimiterRowNoPadding", false], ["editor.insertSpaces", true], ["editor.tabSize", 4], ]; export function resetConfiguration(configurationTarget: vscode.ConfigurationTarget | boolean = true): Promise { return updateConfiguration({ config: Default_Config, configurationTarget }); } /** * A wrapper for `vscode.WorkspaceConfiguration.update()`. * * @param configurationTarget Defaults to `true` (Global). * @param overrideInLanguage Defaults to `undefined`. */ export async function updateConfiguration({ config, configurationTarget = true, overrideInLanguage, }: { config: Iterable; configurationTarget?: vscode.ConfigurationTarget | boolean; overrideInLanguage?: boolean; }): Promise { const configObj = vscode.workspace.getConfiguration(); for (const [id, value] of config) { await configObj.update(id, value, configurationTarget, overrideInLanguage); } } ================================================ FILE: src/test/suite/util/generic.ts ================================================ import { strict as assert } from "assert"; import * as path from "path"; import * as vscode from "vscode"; //#region Constant export const Test_Workspace_Path = vscode.Uri.file(path.resolve(__dirname, "..", "..", "..", "..", "test")); export const Test_Md_File_Path = vscode.Uri.joinPath(Test_Workspace_Path, "test.md"); //#endregion Constant //#region Utility /** * Opens a document with the corresponding editor. * @param file A Uri or file system path which identifies the resource. */ export const openDocument = async (file: vscode.Uri): Promise => { const document = await vscode.workspace.openTextDocument(file); const editor = await vscode.window.showTextDocument(document); return [document, editor]; }; /** * Pauses for a while. * @param ms - Time to pause in millisecond. * @example * await sleep(1000); */ export function sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Tests a command. */ export async function testCommand( command: string, initLines: readonly string[], initSelection: vscode.Selection, expectedLines: readonly string[], expectedSelection: vscode.Selection ): Promise { // Open the file. const [document, editor] = await openDocument(Test_Md_File_Path); // Place the initial content. await editor.edit(editBuilder => { const fullRange = new vscode.Range(new vscode.Position(0, 0), document.positionAt(document.getText().length)); editBuilder.delete(fullRange); editBuilder.insert(new vscode.Position(0, 0), initLines.join("\n")); }); editor.selection = initSelection; await sleep(50); // Run the command. await vscode.commands.executeCommand(command); // Assert. const actual = document.getText() .replace(/\r\n/g, "\n"); // Normalize line endings. assert.deepStrictEqual(actual, expectedLines.join("\n")); assert.deepStrictEqual(editor.selection, expectedSelection); } //#endregion Utility ================================================ FILE: src/theming/constant.ts ================================================ import * as vscode from "vscode"; import type { IConfigurationKnownKey } from "../configuration/model"; // Keys are sorted in alphabetical order. const enum Color { EditorCodeSpanBackground, EditorCodeSpanBorder, EditorFormattingMarkForeground, EditorTrailingSpaceBackground, } const colors: Readonly> = { [Color.EditorCodeSpanBackground]: new vscode.ThemeColor("markdown.extension.editor.codeSpan.background"), [Color.EditorCodeSpanBorder]: new vscode.ThemeColor("markdown.extension.editor.codeSpan.border"), [Color.EditorFormattingMarkForeground]: new vscode.ThemeColor("markdown.extension.editor.formattingMark.foreground"), [Color.EditorTrailingSpaceBackground]: new vscode.ThemeColor("markdown.extension.editor.trailingSpace.background"), }; const enum FontIcon { DownwardsArrow, DownwardsArrowWithCornerLeftwards, Link, Pilcrow, } const fontIcons: Readonly>> = { [FontIcon.DownwardsArrow]: { contentText: "↓", color: colors[Color.EditorFormattingMarkForeground], }, [FontIcon.DownwardsArrowWithCornerLeftwards]: { contentText: "↵", color: colors[Color.EditorFormattingMarkForeground], }, [FontIcon.Link]: { contentText: "\u{1F517}\u{FE0E}", color: colors[Color.EditorFormattingMarkForeground], }, [FontIcon.Pilcrow]: { contentText: "¶", color: colors[Color.EditorFormattingMarkForeground], }, }; export const enum DecorationClass { CodeSpan, HardLineBreak, Link, Paragraph, Strikethrough, TrailingSpace, } /** * Rendering styles for each decoration class. */ export const decorationStyles: Readonly>> = { [DecorationClass.CodeSpan]: { backgroundColor: colors[Color.EditorCodeSpanBackground], border: "1px solid", borderColor: colors[Color.EditorCodeSpanBorder], borderRadius: "3px", rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed, }, [DecorationClass.HardLineBreak]: { after: fontIcons[FontIcon.DownwardsArrowWithCornerLeftwards], rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed, }, [DecorationClass.Link]: { before: fontIcons[FontIcon.Link], rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed, }, [DecorationClass.Paragraph]: { after: fontIcons[FontIcon.Pilcrow], rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed, }, [DecorationClass.Strikethrough]: { rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed, textDecoration: "line-through", }, [DecorationClass.TrailingSpace]: { backgroundColor: colors[Color.EditorTrailingSpaceBackground], }, }; /** * DecorationClass -> Configuration key */ export const decorationClassConfigMap: Readonly> = { [DecorationClass.CodeSpan]: "theming.decoration.renderCodeSpan", [DecorationClass.HardLineBreak]: "theming.decoration.renderHardLineBreak", [DecorationClass.Link]: "theming.decoration.renderLink", [DecorationClass.Paragraph]: "theming.decoration.renderParagraph", [DecorationClass.Strikethrough]: "theming.decoration.renderStrikethrough", [DecorationClass.TrailingSpace]: "theming.decoration.renderTrailingSpace", }; ================================================ FILE: src/theming/decorationManager.ts ================================================ "use strict"; import * as vscode from "vscode"; import { configManager } from "../configuration/manager"; import type IDisposable from "../IDisposable"; import { isMdDocument } from "../util/generic"; import { DecorationClass, decorationClassConfigMap, decorationStyles } from "./constant"; import decorationWorkerRegistry from "./decorationWorkerRegistry"; /** * Represents a set of decoration ranges. */ export interface IDecorationRecord { readonly target: DecorationClass; /** * The ranges that the decoration applies to. */ readonly ranges: readonly vscode.Range[]; } /** * Represents a decoration analysis worker. * * A worker is a function, which analyzes a document, and returns a thenable that resolves to a `IDecorationRecord`. * A worker **may** accept a `CancellationToken`, and then must **reject** the promise when the task is cancelled. */ export interface IFuncAnalysisWorker { (document: vscode.TextDocument, token: vscode.CancellationToken): Thenable; } export type IWorkerRegistry = { readonly [target in DecorationClass]: IFuncAnalysisWorker; }; /** * Represents the state of an asynchronous or long running operation. */ const enum TaskState { Pending, Fulfilled, Cancelled, } /** * Represents a decoration analysis task. * * Such a task should be asynchronous. */ interface IDecorationAnalysisTask { /** * The document that the task works on. */ readonly document: vscode.TextDocument; /** * The thenable that represents the task. * * This is just a handle for caller to await. * It must be guaranteed to be still **fulfilled** on cancellation. */ readonly executor: Thenable; /** * The result. * * Only available when the task is `Fulfilled`. Otherwise, `undefined`. */ readonly result: undefined | readonly IDecorationRecord[]; /** * The state of the task. */ readonly state: TaskState; /** * Cancels the task. */ cancel(): void; } class DecorationAnalysisTask implements IDecorationAnalysisTask { private readonly _cts: vscode.CancellationTokenSource; private _result: readonly IDecorationRecord[] | undefined = undefined; public readonly document: vscode.TextDocument; public readonly executor: Thenable; public get result(): readonly IDecorationRecord[] | undefined { return this._result; } public get state(): TaskState { if (this._cts.token.isCancellationRequested) { return TaskState.Cancelled; } else if (this._result) { return TaskState.Fulfilled; } else { return TaskState.Pending; } } constructor(document: vscode.TextDocument, workers: IWorkerRegistry, targets: readonly DecorationClass[]) { this.document = document; const token = (this._cts = new vscode.CancellationTokenSource()).token; // The weird nesting is to defer the task creation to reduce runtime cost. // The outermost is a so-called "cancellable promise". // If you create a task and cancel it immediately, this design guarantees that most workers are not called. // Otherwise, you will observe thousands of discarded microtasks quickly. this.executor = new Promise((resolve, reject): void => { token.onCancellationRequested(reject); if (token.isCancellationRequested) { reject(); } resolve(Promise.all(targets.map>(target => workers[target](document, token)))); }) .then(result => this._result = result) // Copy the result and pass it down. .catch(reason => { // We'll adopt `vscode.CancellationError` when it matures. // For now, falsy indicates cancellation, and we won't throw an exception for that. if (reason) { throw reason; } }); } public cancel(): void { this._cts.cancel(); this._cts.dispose(); } } interface IDecorationManager extends IDisposable { } /** * Represents a text editor decoration manager. * * For reliability reasons, do not leak any mutable content out of the manager. * * VS Code does not define a corresponding `*Provider` interface, so we implement it ourselves. * The following scenarios are considered: * * * Activation. * * Opening a document with/without corresponding editors. * * Changing a document. * * Closing a document. * * Closing a Markdown editor, and immediately switching to an arbitrary editor. * * Switching between arbitrary editors, including Markdown to Markdown. * * Changing configuration after a decoration analysis task started. * * Deactivation. */ class DecorationManager implements IDecorationManager { /** * Decoration type instances **currently in use**. */ private readonly _decorationHandles = new Map(); /** * Decoration analysis workers. */ private readonly _decorationWorkers: IWorkerRegistry; /** * The keys of `_decorationWorkers`. * This is for improving performance. */ private readonly _supportedClasses: readonly DecorationClass[]; /** * Disposables which unregister contributions to VS Code. */ private readonly _disposables: vscode.Disposable[]; /** * Decoration analysis tasks **currently in use**. * This serves as both a task pool, and a result cache. */ private readonly _tasks = new Map(); /** * This is exclusive to `applyDecoration()`. */ private _displayDebounceHandle: vscode.CancellationTokenSource | undefined; constructor(workers: IWorkerRegistry) { this._decorationWorkers = Object.assign(Object.create(null), workers); // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Property_Accessors#property_names this._supportedClasses = Object.keys(workers).map(Number); // Here are many different kinds of calls. Bind `this` context carefully. // Load all. vscode.workspace.textDocuments.forEach(this.updateCache, this); const activeEditor = vscode.window.activeTextEditor; if (activeEditor) { this.applyDecoration(activeEditor); } // Register event listeners. this._disposables = [ vscode.workspace.onDidOpenTextDocument(this.updateCache, this), vscode.workspace.onDidChangeTextDocument(event => { this.updateCache(event.document); const activeEditor = vscode.window.activeTextEditor; if (activeEditor && activeEditor.document === event.document) { this.applyDecoration(activeEditor); } }), vscode.workspace.onDidCloseTextDocument(this.collectGarbage, this), vscode.window.onDidChangeActiveTextEditor(editor => { if (editor) { this.applyDecoration(editor); } }), ]; } public dispose(): void { // Unsubscribe event listeners. for (const disposable of this._disposables) { disposable.dispose(); } this._disposables.length = 0; // Stop rendering. if (this._displayDebounceHandle) { this._displayDebounceHandle.cancel(); this._displayDebounceHandle.dispose(); } this._displayDebounceHandle = undefined; // Terminate tasks. for (const task of this._tasks.values()) { task.cancel(); } this._tasks.clear(); // Remove decorations. for (const handle of this._decorationHandles.values()) { handle.dispose(); } this._decorationHandles.clear(); } /** * Applies a set of decorations to the text editor asynchronously. * * This method is expected to be started frequently on volatile state. * It begins with a short sync part, to make immediate response to event possible. * Then, it internally creates an async job, to keep data access correct. * It stops silently, if any condition is not met. * * For performance reasons, it only works on the **active** editor (not visible editors), * although VS Code renders decorations as long as the editor is visible. * Besides, we have a threshold to stop analyzing large documents. * When it is reached, related task will be unavailable, thus by design, this method will quit. */ private applyDecoration(editor: vscode.TextEditor): void { if (!isMdDocument(editor.document)) { return; } const document = editor.document; // The task can be in any state (typically pending, fulfilled, obsolete) during this call. // The editor can be suspended or even disposed at any time. // Thus, we have to check at each stage. const task = this._tasks.get(document); if (!task || task.state === TaskState.Cancelled) { return; } // Discard the previous operation, in case the user is switching between editors fast. // Although I don't think a debounce can make much value. if (this._displayDebounceHandle) { this._displayDebounceHandle.cancel(); this._displayDebounceHandle.dispose(); } const debounceToken = (this._displayDebounceHandle = new vscode.CancellationTokenSource()).token; // Queue the display refresh job. (async (): Promise => { if (task.state === TaskState.Pending) { await task.executor; } if (task.state !== TaskState.Fulfilled || debounceToken.isCancellationRequested) { return; } const results = task.result!; for (const { ranges, target } of results) { let handle = this._decorationHandles.get(target); // Recheck applicability, since the user may happen to change settings. if (configManager.get(decorationClassConfigMap[target]) as boolean) { // Create a new decoration type instance if needed. if (!handle) { handle = vscode.window.createTextEditorDecorationType(decorationStyles[target]); this._decorationHandles.set(target, handle); } } else { // Remove decorations if the type is disabled. if (handle) { handle.dispose(); this._decorationHandles.delete(target); } continue; } if ( debounceToken.isCancellationRequested || task.state !== TaskState.Fulfilled // Confirm the cache is still up-to-date. || vscode.window.activeTextEditor !== editor // Confirm the editor is still active. ) { return; } // Create a shallow copy for VS Code to use. This operation shouldn't cost much. editor.setDecorations(handle, Array.from(ranges)); } })(); } /** * Terminates tasks that are linked to the document, and frees corresponding resources. */ private collectGarbage(document: vscode.TextDocument): void { const task = this._tasks.get(document); if (task) { task.cancel(); this._tasks.delete(document); } } /** * Initiates and **queues** a decoration cache update task that is linked to the document. */ private updateCache(document: vscode.TextDocument): void { if (!isMdDocument(document)) { return; } // Discard previous tasks. Effectively mark existing cache as obsolete. this.collectGarbage(document); // Stop if the document exceeds max length. // The factor is for compatibility. There should be new logic someday. if (document.getText().length * 1.5 > configManager.get("syntax.decorationFileSizeLimit")) { return; } // Create the new task. this._tasks.set(document, new DecorationAnalysisTask( document, this._decorationWorkers, // No worry. `applyDecoration()` should recheck applicability. this._supportedClasses.filter(target => configManager.get(decorationClassConfigMap[target]) as boolean) )); } } export const decorationManager: IDecorationManager = new DecorationManager(decorationWorkerRegistry); ================================================ FILE: src/theming/decorationWorkerRegistry.ts ================================================ "use strict"; import * as vscode from "vscode"; import { commonMarkEngine, mdEngine } from "../markdownEngine"; import { DecorationClass } from "./constant"; import { IDecorationRecord, IWorkerRegistry } from "./decorationManager"; // ## Organization // // Sort in alphabetical order. // Place a blank line between two entries. // Place a trailing comma at the end of each entry. // // ## Template // // It's recommended to use this so-called "cancellable promise". // If you have to write async function for some reason, // remember to add cancellation checks based on your experience. // // ````typescript // [DecorationClass.TrailingSpace]: (document, token) => { // return new Promise((resolve, reject): void => { // token.onCancellationRequested(reject); // if (token.isCancellationRequested) { // reject(); // } // // const ranges: vscode.Range[] = []; // // resolve({ target: DecorationClass.TrailingSpace, ranges }); // }); // }, // ```` /** * The registry of decoration analysis workers. */ const decorationWorkerRegistry: IWorkerRegistry = { [DecorationClass.CodeSpan]: async (document, token): Promise => { if (token.isCancellationRequested) { throw undefined; } const text = document.getText(); // Tokens in, for example, table doesn't have `map`. // Tables themselves are strange enough. So, skipping them is acceptable. const tokens = (await mdEngine.getDocumentToken(document)).tokens .filter(t => t.type === "inline" && t.map); if (token.isCancellationRequested) { throw undefined; } const ranges: vscode.Range[] = []; for (const { content, children, map } of tokens) { let initOffset = document.offsetAt(new vscode.Position(map![0], 0)) initOffset = Math.max(text.indexOf(content, initOffset), initOffset); let beginOffset = initOffset; let endOffset = initOffset; for (const t of children!) { // see #1135 if (t.type === "html_inline") { continue; } if (t.type !== "code_inline") { beginOffset += t.content.length; // Not accurate, but enough. continue; } // The `content` is "normalized", not raw. // Thus, in some cases, we need to perform a fuzzy search, and the result cannot be precise. let codeSpanText = t.markup + t.content + t.markup; let cursor = text.indexOf(codeSpanText, beginOffset); if (cursor === -1) { // There may be one space on both sides. codeSpanText = t.markup + " " + t.content + " " + t.markup; cursor = text.indexOf(codeSpanText, beginOffset); } if (cursor !== -1) { // Good. beginOffset = cursor; endOffset = beginOffset + codeSpanText.length; } else { beginOffset = text.indexOf(t.markup, beginOffset); // See if the first piece of `content` can help us. const searchPos = beginOffset + t.markup.length; const searchText = t.content.slice(0, t.content.indexOf(" ")); cursor = text.indexOf(searchText, searchPos); endOffset = cursor !== -1 ? text.indexOf(t.markup, cursor + searchText.length) + t.markup.length : text.indexOf(t.markup, searchPos) + t.markup.length; } ranges.push(new vscode.Range( document.positionAt(beginOffset), document.positionAt(endOffset) )); beginOffset = endOffset; } if (token.isCancellationRequested) { throw undefined; } } return { target: DecorationClass.CodeSpan, ranges }; }, [DecorationClass.HardLineBreak]: (document, token) => { return new Promise((resolve, reject): void => { token.onCancellationRequested(reject); if (token.isCancellationRequested) { reject(); } // Use commonMarkEngine for reliability, at the expense of accuracy. const tokens = commonMarkEngine.getDocumentToken(document).tokens.filter(t => t.type === "inline"); const ranges: vscode.Range[] = []; for (const { children, map } of tokens) { let lineIndex = map![0]; for (const t of children!) { switch (t.type) { case "softbreak": lineIndex++; break; case "hardbreak": const pos = document.lineAt(lineIndex).range.end; ranges.push(new vscode.Range(pos, pos)); lineIndex++; break; } } } resolve({ target: DecorationClass.HardLineBreak, ranges }); }); }, [DecorationClass.Link]: (document, token) => { return new Promise((resolve, reject): void => { token.onCancellationRequested(reject); if (token.isCancellationRequested) { reject(); } // A few kinds of inline links. const ranges: vscode.Range[] = Array.from( document.getText().matchAll(/(? { const pos = document.positionAt(m.index!); return new vscode.Range(pos, pos); } ); resolve({ target: DecorationClass.Link, ranges }); }); }, [DecorationClass.Paragraph]: async (document, token): Promise => { if (token.isCancellationRequested) { throw undefined; } const { tokens } = (await mdEngine.getDocumentToken(document)); if (token.isCancellationRequested) { throw undefined; } const ranges: vscode.Range[] = []; for (const t of tokens) { if (t.type === "paragraph_open") { const pos = document.lineAt(t.map![1] - 1).range.end; ranges.push(new vscode.Range(pos, pos)); } } return { target: DecorationClass.Paragraph, ranges }; }, [DecorationClass.Strikethrough]: async (document, token): Promise => { if (token.isCancellationRequested) { throw undefined; } const searchRanges = (await mdEngine.getDocumentToken(document)).tokens .filter(t => t.type === "inline" && t.map).map(t => t.map!); // Tokens in, for example, table doesn't have `map`. if (token.isCancellationRequested) { throw undefined; } const ranges: vscode.Range[] = []; for (const [begin, end] of searchRanges) { const beginOffset = document.offsetAt(new vscode.Position(begin, 0)); const text = document.getText(new vscode.Range(begin, 0, end, 0)); // GitHub's definition is pretty strict. I've tried my best to simulate it. ranges.push(...Array.from( text.matchAll(/(? { return new vscode.Range( document.positionAt(beginOffset + m.index!), document.positionAt(beginOffset + m.index! + m[0].length) ); } )); if (token.isCancellationRequested) { throw undefined; } } return { target: DecorationClass.Strikethrough, ranges }; }, [DecorationClass.TrailingSpace]: (document, token) => { return new Promise((resolve, reject): void => { token.onCancellationRequested(reject); if (token.isCancellationRequested) { reject(); } const text = document.getText(); const ranges: vscode.Range[] = Array.from( text.matchAll(/ +(?=[\r\n])/g), m => { return new vscode.Range( document.positionAt(m.index!), document.positionAt(m.index! + m[0].length) ); } ); // Process the end of file case specially. const eof = text.match(/ +$/); if (eof) { ranges.push(new vscode.Range( document.positionAt(eof.index!), document.positionAt(eof.index! + eof[0].length) )); } resolve({ target: DecorationClass.TrailingSpace, ranges }); }); }, }; export default decorationWorkerRegistry; ================================================ FILE: src/toc.ts ================================================ 'use strict'; import * as path from 'path'; import * as stringSimilarity from 'string-similarity'; import { CancellationToken, CodeLens, CodeLensProvider, commands, EndOfLine, ExtensionContext, languages, Position, Range, TextDocument, TextDocumentWillSaveEvent, TextEditor, Uri, window, workspace, WorkspaceEdit } from 'vscode'; import { commonMarkEngine, mdEngine, Token } from './markdownEngine'; import { isMdDocument, Document_Selector_Markdown, Regexp_Fenced_Code_Block } from "./util/generic"; import { slugify } from "./util/slugify"; import type * as MarkdownSpec from "./contract/MarkdownSpec"; import SlugifyMode from "./contract/SlugifyMode"; /** * Represents the essential properties of a heading. */ interface IHeadingBase { /** * The heading level. */ level: MarkdownSpec.MarkdownHeadingLevel; /** * The raw content of the heading according to the CommonMark Spec. * Can be **multiline**. */ rawContent: string; /** * The **zero-based** index of the beginning line of the heading in original document. */ lineIndex: number; /** * `true` to show in TOC. `false` to omit from TOC. */ canInToc: boolean; } /** * Represents a heading. */ export interface IHeading extends IHeadingBase { /** * The **rich text** (single line Markdown inline without raw HTML) representation of the rendering result (in strict CommonMark mode) of the heading. * This must be able to be safely put into a `[]` bracket pair without breaking Markdown syntax. */ visibleText: string; /** * The anchor ID of the heading. * This must be a valid IRI fragment, which does not contain `#`. * See RFC 3986 section 3, and RFC 3987 section 2.2. */ slug: string; } /** * Workspace config */ const docConfig = { tab: ' ', eol: '\r\n' }; const tocConfig = { startDepth: 1, endDepth: 6, listMarker: '-', orderedList: false, updateOnSave: false, plaintext: false, tabSize: 2 }; export function activate(context: ExtensionContext) { context.subscriptions.push( commands.registerCommand('markdown.extension.toc.create', createToc), commands.registerCommand('markdown.extension.toc.update', updateToc), commands.registerCommand('markdown.extension.toc.addSecNumbers', addSectionNumbers), commands.registerCommand('markdown.extension.toc.removeSecNumbers', removeSectionNumbers), workspace.onWillSaveTextDocument(onWillSave), languages.registerCodeLensProvider(Document_Selector_Markdown, new TocCodeLensProvider()) ); } //#region TOC operation entrance async function createToc() { const editor = window.activeTextEditor; if (!editor || !isMdDocument(editor.document)) { return; } loadTocConfig(editor); let toc = await generateTocText(editor.document); await editor.edit(function (editBuilder) { editBuilder.delete(editor.selection); editBuilder.insert(editor.selection.active, toc); }); } async function updateToc() { const editor = window.activeTextEditor; if (!editor || !isMdDocument(editor.document)) { return; } loadTocConfig(editor); const doc = editor.document; const tocRangesAndText = await detectTocRanges(doc); const tocRanges = tocRangesAndText[0]; const newToc = tocRangesAndText[1]; await editor.edit(editBuilder => { for (const tocRange of tocRanges) { if (tocRange !== null) { const oldToc = doc.getText(tocRange).replace(/\r?\n|\r/g, docConfig.eol); if (oldToc !== newToc) { const unchangedLength = commonPrefixLength(oldToc, newToc); const newStart = doc.positionAt(doc.offsetAt(tocRange.start) + unchangedLength); const replaceRange = tocRange.with(newStart); if (replaceRange.isEmpty) { editBuilder.insert(replaceRange.start, newToc.substring(unchangedLength)); } else { editBuilder.replace(replaceRange, newToc.substring(unchangedLength)); } } } } }); } function addSectionNumbers() { const editor = window.activeTextEditor; if (!editor || !isMdDocument(editor.document)) { return; } loadTocConfig(editor); const doc = editor.document; const toc: readonly Readonly[] = getAllRootHeading(doc, true, true) .filter(i => i.canInToc && i.level >= tocConfig.startDepth && i.level <= tocConfig.endDepth); if (toc.length === 0) { return; } const startDepth = Math.max(tocConfig.startDepth, Math.min(...toc.map(h => h.level))); let secNumbers = [0, 0, 0, 0, 0, 0]; let edit = new WorkspaceEdit(); toc.forEach(entry => { const level = entry.level; const lineNum = entry.lineIndex; secNumbers[level - 1] += 1; secNumbers.fill(0, level); const secNumStr = [...Array(level - startDepth + 1).keys()].map(num => `${secNumbers[num + startDepth - 1]}.`).join(''); const lineText = doc.lineAt(lineNum).text; const newText = lineText.includes('#') ? lineText.replace(/^(\s{0,3}#+ +)((?:\d{1,9}\.)* )?(.*)/, (_, g1, _g2, g3) => `${g1}${secNumStr} ${g3}`) : lineText.replace(/^(\s{0,3})((?:\d{1,9}\.)* )?(.*)/, (_, g1, _g2, g3) => `${g1}${secNumStr} ${g3}`); edit.replace(doc.uri, doc.lineAt(lineNum).range, newText); }); return workspace.applyEdit(edit); } function removeSectionNumbers() { const editor = window.activeTextEditor; if (!editor || !isMdDocument(editor.document)) { return; } const doc = editor.document; const toc: readonly Readonly[] = getAllRootHeading(doc, false, false); let edit = new WorkspaceEdit(); toc.forEach(entry => { const lineNum = entry.lineIndex; const lineText = doc.lineAt(lineNum).text; const newText = lineText.includes('#') ? lineText.replace(/^(\s{0,3}#+ +)((?:\d{1,9}\.)* )?(.*)/, (_, g1, _g2, g3) => `${g1}${g3}`) : lineText.replace(/^(\s{0,3})((?:\d{1,9}\.)* )?(.*)/, (_, g1, _g2, g3) => `${g1}${g3}`); edit.replace(doc.uri, doc.lineAt(lineNum).range, newText); }); return workspace.applyEdit(edit); } function onWillSave(e: TextDocumentWillSaveEvent): void { if (!tocConfig.updateOnSave) { return; } if (e.document.languageId === 'markdown') { e.waitUntil(updateToc()); } } //#endregion TOC operation entrance /** * Returns a list of user defined excluded headings for the given document. * They are defined in the `toc.omittedFromToc` setting. * @param doc The document. */ function getProjectExcludedHeadings(doc: TextDocument): readonly Readonly<{ level: number, text: string; }>[] { const configObj = workspace.getConfiguration('markdown.extension.toc').get<{ [path: string]: string[]; }>('omittedFromToc'); if (typeof configObj !== 'object' || configObj === null) { window.showErrorMessage(`\`omittedFromToc\` must be an object (e.g. \`{"README.md": ["# Introduction"]}\`)`); return []; } const docUriString = doc.uri.toString(); const docWorkspace = workspace.getWorkspaceFolder(doc.uri); const workspaceUri = docWorkspace ? docWorkspace.uri : undefined; // A few possible duplicate entries are bearable, thus, an array is enough. const omittedHeadings: string[] = []; for (const filePath of Object.keys(configObj)) { let entryUri: Uri; // Convert file system path to VS Code Uri. if (path.isAbsolute(filePath)) { entryUri = Uri.file(filePath); } else if (workspaceUri !== undefined) { entryUri = Uri.joinPath(workspaceUri, filePath); } else { continue; // Discard this entry. } // If the entry matches the document, read it. if (entryUri.toString() === docUriString) { if (Array.isArray(configObj[filePath])) { omittedHeadings.push(...configObj[filePath]); } else { window.showErrorMessage('Each property value of `omittedFromToc` setting must be a string array.'); } } } return omittedHeadings.map(heading => { const matches = heading.match(/^ {0,3}(#{1,6})[ \t]+(.*)$/); if (matches === null) { window.showErrorMessage(`Invalid entry "${heading}" in \`omittedFromToc\``); return { level: -1, text: '' }; } const [, sharps, name] = matches; return { level: sharps.length, text: name }; }); } /** * Generates the Markdown text representation of the TOC. */ // TODO: Redesign data structure to solve another bunch of bugs. async function generateTocText(doc: TextDocument): Promise { const orderedListMarkerIsOne: boolean = workspace.getConfiguration('markdown.extension.orderedList').get('marker') === 'one'; const toc: string[] = []; const tocEntries: readonly Readonly[] = getAllTocEntry(doc, { respectMagicCommentOmit: true, respectProjectLevelOmit: true }) .filter(i => i.canInToc && i.level >= tocConfig.startDepth && i.level <= tocConfig.endDepth); // Filter out excluded headings. if (tocEntries.length === 0) { return ''; } // The actual level range of a document can be smaller than settings. So we need to calculate the real start level. const startDepth = Math.max(tocConfig.startDepth, Math.min(...tocEntries.map(h => h.level))); // Order counter for each heading level (from startDepth to endDepth), used only for ordered list const orderCounter: number[] = new Array(tocConfig.endDepth - startDepth + 1).fill(0); tocEntries.forEach(entry => { const relativeLevel = entry.level - startDepth; const currHeadingOrder = ++orderCounter[relativeLevel]; let indentationFix = ''; if (tocConfig.orderedList) { const shift = orderCounter.slice(0, relativeLevel).map(c => String(c).length - 1).reduce((a, b) => a + b, 0); indentationFix = ' '.repeat(shift); } const row = [ docConfig.tab.repeat(relativeLevel) + indentationFix, (tocConfig.orderedList ? (orderedListMarkerIsOne ? '1' : currHeadingOrder) + '.' : tocConfig.listMarker) + ' ', tocConfig.plaintext ? entry.visibleText : `[${entry.visibleText}](#${entry.slug})` ]; toc.push(row.join('')); // Reset order counter for its sub-headings if (tocConfig.orderedList) { orderCounter.fill(0, relativeLevel + 1); } }); while (/^[ \t]/.test(toc[0])) { toc.shift(); } toc.push(''); // Ensure the TOC text always ends with an EOL. return toc.join(docConfig.eol); } /** * Returns an array of TOC ranges. * If no TOC is found, returns an empty array. * @param doc a TextDocument */ async function detectTocRanges(doc: TextDocument): Promise<[Array, string]> { const docTokens = (await mdEngine.getDocumentToken(doc)).tokens; /** * `[beginLineIndex, endLineIndex, openingTokenIndex]` */ const candidateLists: readonly [number, number, number][] = docTokens.reduce<[number, number, number][]>((result, token, index) => { if ( token.level === 0 && ( token.type === 'bullet_list_open' || (token.type === 'ordered_list_open' && token.attrGet('start') === null) ) ) { result.push([...token.map!, index]); } return result; }, []); const tocRanges: Range[] = []; const newTocText = await generateTocText(doc); for (const item of candidateLists) { const beginLineIndex = item[0]; let endLineIndex = item[1]; const opTokenIndex = item[2]; //// #525 comment if ( beginLineIndex > 0 && doc.lineAt(beginLineIndex - 1).text === '' ) { continue; } // Check the first list item to see if it could be a TOC. // // ## Token stream // // +3 alway exists, even if it's an empty list. // In a target, +3 is `inline`: // // opTokenIndex: *_list_open // +1: list_item_open // +2: paragraph_open // +3: inline // +4: paragraph_close // ... // ...: list_item_close // // ## `inline.children` // // Ordinary TOC: `link_open`, ..., `link_close`. // Plain text TOC: No `link_*` tokens. const firstItemContent = docTokens[opTokenIndex + 3]; if (firstItemContent.type !== 'inline') { continue; } const tokens = firstItemContent.children!; if (workspace.getConfiguration('markdown.extension.toc').get('plaintext')) { if (tokens.some(t => t.type.startsWith('link_'))) { continue; } } else { if (!( tokens[0].type === 'link_open' && tokens[0].attrGet('href')!.startsWith('#') // Destination begins with `#`. (#304) && tokens.findIndex(t => t.type === 'link_close') === (tokens.length - 1) // Only one link. (#549, #683) )) { continue; } } // The original range may have trailing white lines. while (doc.lineAt(endLineIndex - 1).isEmptyOrWhitespace) { endLineIndex--; } const finalRange = new Range(new Position(beginLineIndex, 0), new Position(endLineIndex, 0)); const listText = doc.getText(finalRange); if (radioOfCommonPrefix(newTocText, listText) + stringSimilarity.compareTwoStrings(newTocText, listText) > 0.5) { tocRanges.push(finalRange); } } return [tocRanges, newTocText]; } function commonPrefixLength(s1: string, s2: string): number { let minLength = Math.min(s1.length, s2.length); for (let i = 0; i < minLength; i++) { if (s1[i] !== s2[i]) { return i; } } return minLength; } function radioOfCommonPrefix(s1: string, s2: string): number { let minLength = Math.min(s1.length, s2.length); let maxLength = Math.max(s1.length, s2.length); let prefixLength = commonPrefixLength(s1, s2); if (prefixLength < minLength) { return prefixLength / minLength; } else { return minLength / maxLength; } } /** * Updates `tocConfig` and `docConfig`. * @param editor The editor, from which we detect `docConfig`. */ function loadTocConfig(editor: TextEditor): void { const tocSectionCfg = workspace.getConfiguration('markdown.extension.toc'); const tocLevels = tocSectionCfg.get('levels')!; let matches; if (matches = tocLevels.match(/^([1-6])\.\.([1-6])$/)) { tocConfig.startDepth = Number(matches[1]); tocConfig.endDepth = Number(matches[2]); } tocConfig.orderedList = tocSectionCfg.get('orderedList')!; tocConfig.listMarker = tocSectionCfg.get('unorderedList.marker')!; tocConfig.plaintext = tocSectionCfg.get('plaintext')!; tocConfig.updateOnSave = tocSectionCfg.get('updateOnSave')!; // Load workspace config docConfig.eol = editor.document.eol === EndOfLine.CRLF ? '\r\n' : '\n'; let tabSize = Number(editor.options.tabSize); // Seems not robust. if (workspace.getConfiguration('markdown.extension.list', editor.document.uri).get('indentationSize') === 'adaptive') { tabSize = tocConfig.orderedList ? 3 : 2; } const insertSpaces = editor.options.insertSpaces; if (insertSpaces) { docConfig.tab = ' '.repeat(tabSize); } else { docConfig.tab = '\t'; } } /** * Extracts those that can be rendered to visible text from a string of CommonMark **inline** structures, * to create a single line string which can be safely used as **link text**. * * The result cannot be directly used as the content of a paragraph, * since this function does not escape all sequences that look like block structures. * * We roughly take GitLab's `[[_TOC_]]` as reference. * * @param raw - The Markdown string. * @param env - The markdown-it environment sandbox (**mutable**). * @returns A single line string, which only contains plain textual content, * backslash escape, code span, and emphasis. */ function createLinkText(raw: string, env: object): string { const inlineTokens: Token[] = commonMarkEngine.engine.parseInline(raw, env)[0].children!; return inlineTokens.reduce((result, token) => { switch (token.type) { case "text": return result + token.content.replace(/[&*<>\[\\\]_`]/g, "\\$&"); // Escape. case "code_inline": return result + token.markup + token.content + token.markup; // Emit as is. case "strong_open": case "strong_close": case "em_open": case "em_close": return result + token.markup; // Preserve emphasis indicators. case "link_open": case "link_close": case "image": case "html_inline": return result; // Discard them. case "softbreak": case "hardbreak": return result + " "; // Replace line breaks with spaces. default: return result + token.content; } }, ""); } //#region Public utility /** * Gets all headings in the root of the text document. * * The optional parameters default to `false`. * @returns In ascending order of `lineIndex`. */ export function getAllRootHeading(doc: TextDocument, respectMagicCommentOmit: boolean = false, respectProjectLevelOmit: boolean = false): Readonly[] { /** * Replaces line content with empty. * @param foundStr The multiline string. */ const replacer = (foundStr: string) => foundStr.replace(/[^\r\n]/g, ''); /* * Text normalization * ================== * including: * * 1. (easy) YAML front matter, tab to spaces, HTML comment, Markdown fenced code blocks * 2. (complex) Setext headings to ATX headings * 3. Remove trailing space or tab characters. * * Note: * When recognizing or trimming whitespace characters, comply with the CommonMark Spec. * Do not use anything that defines whitespace as per ECMAScript, like `trim()`. */ // (easy) const lines: string[] = doc.getText() .replace(/^---.+?(?:\r?\n)---(?=[ \t]*\r?\n)/s, replacer) //// Remove YAML front matter .replace(/^\t+/gm, (match: string) => ' '.repeat(match.length)) // .replace(/^( {0,3}).*$/gm, (match: string, leading: string, content: string) => { // Remove HTML block comment, together with all the text in the lines it occupies. // Exclude our magic comment. if (leading.length === 0 && /omit (in|from) toc/.test(content)) { return match; } else { return replacer(match); } }) .replace(Regexp_Fenced_Code_Block, replacer) //// Remove fenced code blocks (and #603, #675) .split(/\r?\n/g); // Do transformations as many as possible in one loop, to save time. lines.forEach((lineText, i, arr) => { // (complex) Setext headings to ATX headings. // Still cannot perfectly handle some weird cases, for example: // * Multiline heading. // * A setext heading next to a list. if ( i < arr.length - 1 // The current line is not the last. && /^ {0,3}(?:=+|-+)[ \t]*$/.test(arr[i + 1]) // The next line is a setext heading underline. && /^ {0,3}[^ \t\f\v]/.test(lineText) // The indentation of the line is 0~3. && !/^ {0,3}#{1,6}(?: |\t|$)/.test(lineText) // The line is not an ATX heading. && !/^ {0,3}(?:[*+-]|\d{1,9}(?:\.|\)))(?: |\t|$)/.test(lineText) // The line is not a list item. && !/^ {0,3}>/.test(lineText) // The line is not a block quote. // #629: Consecutive thematic breaks false positive. && !/^ {0,3}(?:(?:-[ \t]*){3,}|(?:\*[ \t]*){3,}|(?:_[ \t]*){3,})[ \t]*$/.test(lineText) ) { arr[i] = (arr[i + 1].includes('=') ? '# ' : '## ') + lineText; arr[i + 1] = ''; } // Remove trailing space or tab characters. // Since they have no effect on subsequent operations, and removing them can simplify those operations. // arr[i] = arr[i].replace(/[ \t]+$/, ''); }); /* * Mark omitted headings * ===================== * * - headings with magic comment `` (on their own) * - headings from `getProjectExcludedHeadings()` (and their subheadings) * * Note: * * We have trimmed trailing space or tab characters for every line above. * * We have performed leading tab-space conversion above. */ const projectLevelOmittedHeadings = respectProjectLevelOmit ? getProjectExcludedHeadings(doc) : []; /** * Keep track of the omitted heading's depth to also omit its subheadings. * This is only for project level omitting. */ let ignoredDepthBound: MarkdownSpec.MarkdownHeadingLevel | undefined = undefined; const toc: IHeadingBase[] = []; for (let i: number = 0; i < lines.length; i++) { const crtLineText = lines[i]; // Skip non-ATX heading lines. if ( // !/^ {0,3}#{1,6}(?: |\t|$)/.test(crtLineText) ) { continue; } // Extract heading info. const matches = /^ {0,3}(#{1,6})(.*)$/.exec(crtLineText)!; const entry: IHeadingBase = { level: matches[1].length as MarkdownSpec.MarkdownHeadingLevel, rawContent: matches[2].replace(/^[ \t]+/, '').replace(/[ \t]+#+[ \t]*$/, ''), lineIndex: i, canInToc: true, }; // Omit because of magic comment if ( respectMagicCommentOmit && entry.canInToc && ( // The magic comment is above the heading. ( i > 0 && /^$/.test(lines[i - 1]) ) // The magic comment is at the end of the heading. || /$/.test(crtLineText) ) ) { entry.canInToc = false; } // Omit because of `projectLevelOmittedHeadings`. if (respectProjectLevelOmit && entry.canInToc) { // Whether omitted as a subheading if ( ignoredDepthBound !== undefined && entry.level > ignoredDepthBound ) { entry.canInToc = false; } // Whether omitted because it is in `projectLevelOmittedHeadings`. if (entry.canInToc) { if (projectLevelOmittedHeadings.some(({ level, text }) => level === entry.level && text === entry.rawContent)) { entry.canInToc = false; ignoredDepthBound = entry.level; } else { // Otherwise reset ignore bound. ignoredDepthBound = undefined; } } } toc.push(entry); } return toc; } /** * Gets all headings in the root of the text document, with additional TOC specific properties. * @returns In ascending order of `lineIndex`. */ export function getAllTocEntry(doc: TextDocument, { respectMagicCommentOmit = false, respectProjectLevelOmit = false, slugifyMode = workspace.getConfiguration('markdown.extension.toc').get('slugifyMode')!, }: { respectMagicCommentOmit?: boolean; respectProjectLevelOmit?: boolean; slugifyMode?: SlugifyMode; }): Readonly[] { const rootHeadings: readonly Readonly[] = getAllRootHeading(doc, respectMagicCommentOmit, respectProjectLevelOmit); const { env } = commonMarkEngine.getDocumentToken(doc); const anchorOccurrences = new Map(); function getSlug(rawContent: string): string { let slug = slugify(rawContent, { env, mode: slugifyMode }); let count = anchorOccurrences.get(slug); if (count === undefined) { anchorOccurrences.set(slug, 0); } else { count++; anchorOccurrences.set(slug, count); slug += '-' + count.toString(); } return slug; } const toc: IHeading[] = rootHeadings.map((heading): IHeading => ({ level: heading.level, rawContent: heading.rawContent, lineIndex: heading.lineIndex, canInToc: heading.canInToc, visibleText: createLinkText(heading.rawContent, env), slug: getSlug(heading.rawContent), })); return toc; } //#endregion Public utility class TocCodeLensProvider implements CodeLensProvider { public provideCodeLenses(document: TextDocument, _: CancellationToken): CodeLens[] | Thenable { // VS Code asks for code lens as soon as a text editor is visible (atop the group that holds it), no matter whether it has focus. // Duplicate editor views refer to the same TextEditor, and the same TextDocument. const editor = window.visibleTextEditors.find(e => e.document === document)!; loadTocConfig(editor); const lenses: CodeLens[] = []; return detectTocRanges(document).then(tocRangesAndText => { const tocRanges = tocRangesAndText[0]; const newToc = tocRangesAndText[1]; for (let tocRange of tocRanges) { let status = document.getText(tocRange).replace(/\r?\n|\r/g, docConfig.eol) === newToc ? 'up to date' : 'out of date'; lenses.push(new CodeLens(tocRange, { arguments: [], title: `Table of Contents (${status})`, command: '' })); } return lenses; }); } } ================================================ FILE: src/util/contextCheck.ts ================================================ import * as vscode from "vscode"; import { commonMarkEngine } from "../markdownEngine"; /** * Checks whether the line is in a fenced code block. * @param lineIndex The zero-based line index. */ export function isInFencedCodeBlock(doc: vscode.TextDocument, lineIndex: number): boolean { const { tokens } = commonMarkEngine.getDocumentToken(doc); for (const token of tokens) { if (token.type === "fence" && token.tag === "code" && token.map![0] <= lineIndex && lineIndex < token.map![1]) { return true; } } return false; } export function mathEnvCheck(doc: vscode.TextDocument, pos: vscode.Position): "display" | "inline" | "" { const docText = doc.getText(); const crtOffset = doc.offsetAt(pos); const crtLine = doc.lineAt(pos.line); const lineTextBefore = crtLine.text.substring(0, pos.character); const lineTextAfter = crtLine.text.substring(pos.character); if (/(?:^|[^\$])\$(?:[^ \$].*)??\\\w*$/.test(lineTextBefore) && lineTextAfter.includes("$")) { // Inline math return "inline"; } else { const textBefore = docText.substring(0, crtOffset); const textAfter = docText.substring(crtOffset); let matches = textBefore.match(/\$\$/g); if (matches !== null && matches.length % 2 !== 0 && textAfter.includes("$$")) { // $$ ... $$ return "display"; } else { return ""; } } } ================================================ FILE: src/util/generic.ts ================================================ import * as vscode from "vscode"; import LanguageIdentifier from "../contract/LanguageIdentifier"; /** Scheme `File` or `Untitled` */ export const Document_Selector_Markdown: vscode.DocumentSelector = [ { language: LanguageIdentifier.Markdown, scheme: "file" }, { language: LanguageIdentifier.Markdown, scheme: "untitled" }, ]; /** * **Do not call `exec()` method, to avoid accidentally changing its state!** * * Match most kinds of fenced code blocks: * * * Only misses . * * Due to the limitations of regular expression, the "end of the document" cases are not handled. */ export const Regexp_Fenced_Code_Block = /^ {0,3}(?(?[`~])\k{2,})[^`\r\n]*$[^]*?^ {0,3}\k\k* *$/gm; export function isMdDocument(doc: vscode.TextDocument | undefined): boolean { if (doc) { const extraLangIds = vscode.workspace.getConfiguration("markdown.extension").get>("extraLangIds"); const langId = doc.languageId; if (extraLangIds?.includes(langId)) { return true; } if (langId === LanguageIdentifier.Markdown) { return true; } } return false; } ================================================ FILE: src/util/katex-funcs.ts ================================================ // // Suffixes explained: // \cmd -> 0 // \cmd{$1} -> 1 // \cmd{$1}{$2} -> 2 // // Use linebreak to mimic the structure of the KaTeX [Support Table](https://katex.org/docs/supported.html) // source https://github.com/KaTeX/KaTeX/blob/main/docs/supported.md // export const accents1 = [ 'tilde', 'mathring', 'widetilde', 'overgroup', 'utilde', 'undergroup', 'acute', 'vec', 'Overrightarrow', 'bar', 'overleftarrow', 'overrightarrow', 'breve', 'underleftarrow', 'underrightarrow', 'check', 'overleftharpoon', 'overrightharpoon', 'dot', 'overleftrightarrow', 'overbrace', 'ddot', 'underleftrightarrow', 'underbrace', 'grave', 'overline', 'overlinesegment', 'hat', 'underline', 'underlinesegment', 'widehat', 'widecheck', 'underbar' ]; export const delimiters0 = [ 'lparen', 'rparen', 'lceil', 'rceil', 'uparrow', 'lbrack', 'rbrack', 'lfloor', 'rfloor', 'downarrow', 'lbrace', 'rbrace', 'lmoustache', 'rmoustache', 'updownarrow', 'langle', 'rangle', 'lgroup', 'rgroup', 'Uparrow', 'vert', 'ulcorner', 'urcorner', 'Downarrow', 'Vert', 'llcorner', 'lrcorner', 'Updownarrow', 'lvert', 'rvert', 'lVert', 'rVert', 'backslash', 'lang', 'rang', 'lt', 'gt', 'llbracket', 'rrbracket', 'lBrace', 'rBrace' ]; export const delimeterSizing0 = [ 'left', 'big', 'bigl', 'bigm', 'bigr', 'middle', 'Big', 'Bigl', 'Bigm', 'Bigr', 'right', 'bigg', 'biggl', 'biggm', 'biggr', 'Bigg', 'Biggl', 'Biggm', 'Biggr' ]; export const greekLetters0 = [ 'Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon', 'Zeta', 'Eta', 'Theta', 'Iota', 'Kappa', 'Lambda', 'Mu', 'Nu', 'Xi', 'Omicron', 'Pi', 'Rho', 'Sigma', 'Tau', 'Upsilon', 'Phi', 'Chi', 'Psi', 'Omega', 'varGamma', 'varDelta', 'varTheta', 'varLambda', 'varXi', 'varPi', 'varSigma', 'varUpsilon', 'varPhi', 'varPsi', 'varOmega', 'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'zeta', 'eta', 'theta', 'iota', 'kappa', 'lambda', 'mu', 'nu', 'xi', 'omicron', 'pi', 'rho', 'sigma', 'tau', 'upsilon', 'phi', 'chi', 'psi', 'omega', 'varepsilon', 'varkappa', 'vartheta', 'thetasym', 'varpi', 'varrho', 'varsigma', 'varphi', 'digamma' ]; export const otherLetters0 = [ 'imath', 'nabla', 'Im', 'Reals', 'jmath', 'partial', 'image', 'wp', 'aleph', 'Game', 'Bbbk', 'weierp', 'alef', 'Finv', 'N', 'Z', 'alefsym', 'cnums', 'natnums', 'beth', 'Complex', 'R', 'gimel', 'ell', 'Re', 'daleth', 'hbar', 'real', 'eth', 'hslash', 'reals' ]; export const annotation1 = [ 'cancel', 'overbrace', 'bcancel', 'underbrace', 'xcancel', 'not =', 'sout', 'boxed', 'phase', 'tag', 'tag*' ]; export const verticalLayout0 = ['atop']; export const verticalLayout1 = ['substack']; export const verticalLayout2 = ['stackrel', 'overset', 'underset', 'raisebox']; export const overlap1 = ['mathllap', 'mathrlap', 'mathclap', 'llap', 'rlap', 'clap', 'smash']; export const spacing0 = [ 'thinspace', 'medspace', 'thickspace', 'enspace', 'quad', 'qquad', 'negthinspace', 'negmedspace', 'nobreakspace', 'negthickspace', 'space', 'mathstrut' ]; export const spacing1 = [ 'kern', 'mkern', 'mskip', 'hskip', 'hspace', 'hspace*', 'phantom', 'hphantom', 'vphantom' ]; export const logicAndSetTheory0 = [ 'forall', 'complement', 'therefore', 'emptyset', 'exists', 'subset', 'because', 'empty', 'exist', 'supset', 'mapsto', 'varnothing', 'nexists', 'mid', 'to', 'implies', 'in', 'land', 'gets', 'impliedby', 'isin', 'lor', 'leftrightarrow', 'iff', 'notin', 'ni', 'notni', 'neg', 'lnot' ]; export const logicAndSetTheory1 = [ 'Set', 'set' ] export const macros0 = [ 'def', 'gdef', 'edef', 'xdef', 'let', 'futurelet', 'global', 'newcommand', 'renewcommand', 'providecommand', 'long', 'char', 'mathchoice', 'TextOrMath', '@ifstar', '@ifnextchar', '@firstoftwo', '@secondoftwo', 'relax', 'expandafter', 'noexpand' ]; export const bigOperators0 = [ 'sum', 'prod', 'bigotimes', 'bigvee', 'int', 'coprod', 'bigoplus', 'bigwedge', 'iint', 'intop', 'bigodot', 'bigcap', 'iiint', 'smallint', 'biguplus', 'bigcup', 'oint', 'oiint', 'oiiint', 'bigsqcup' ]; export const binaryOperators0 = [ 'cdot', 'gtrdot', 'pmod', 'cdotp', 'intercal', 'pod', 'centerdot', 'land', 'rhd', 'circ', 'leftthreetimes', 'rightthreetimes', 'amalg', 'circledast', 'ldotp', 'rtimes', 'And', 'circledcirc', 'lor', 'setminus', 'ast', 'circleddash', 'lessdot', 'smallsetminus', 'barwedge', 'Cup', 'lhd', 'sqcap', 'bigcirc', 'cup', 'ltimes', 'sqcup', 'bmod', 'curlyvee', 'times', 'boxdot', 'curlywedge', 'mp', 'unlhd', 'boxminus', 'div', 'odot', 'unrhd', 'boxplus', 'divideontimes', 'ominus', 'uplus', 'boxtimes', 'dotplus', 'oplus', 'vee', 'bullet', 'doublebarwedge', 'otimes', 'veebar', 'Cap', 'doublecap', 'oslash', 'wedge', 'cap', 'doublecup', 'pm', 'plusmn', 'wr' ]; export const fractions0 = ['over', 'above']; export const fractions2 = ['frac', 'dfrac', 'tfrac', 'cfrac', 'genfrac']; export const binomialCoefficients0 = ['choose']; export const binomialCoefficients2 = ['binom', 'dbinom', 'tbinom', 'brace', 'brack']; export const mathOperators0 = [ 'arcsin', 'cosec', 'deg', 'sec', 'arccos', 'cosh', 'dim', 'sin', 'arctan', 'cot', 'exp', 'sinh', 'arctg', 'cotg', 'hom', 'sh', 'arcctg', 'coth', 'ker', 'tan', 'arg', 'csc', 'lg', 'tanh', 'ch', 'ctg', 'ln', 'tg', 'cos', 'cth', 'log', 'th', 'argmax', 'injlim', 'min', 'varinjlim', 'argmin', 'lim', 'plim', 'varliminf', 'det', 'liminf', 'Pr', 'varlimsup', 'gcd', 'limsup', 'projlim', 'varprojlim', 'inf', 'max', 'sup' ]; export const mathOperators1 = ['operatorname', 'operatorname*', 'operatornamewithlimits']; export const sqrt1 = ['sqrt']; export const relations0 = [ 'doteqdot', 'lessapprox', 'smile', 'eqcirc', 'lesseqgtr', 'sqsubset', 'eqcolon', 'minuscolon', 'lesseqqgtr', 'sqsubseteq', 'Eqcolon', 'minuscoloncolon', 'lessgtr', 'sqsupset', 'approx', 'eqqcolon', 'equalscolon', 'lesssim', 'sqsupseteq', 'approxcolon', 'Eqqcolon', 'equalscoloncolon', 'll', 'Subset', 'approxcoloncolon', 'eqsim', 'lll', 'subset', 'sub', 'approxeq', 'eqslantgtr', 'llless', 'subseteq', 'sube', 'asymp', 'eqslantless', 'lt', 'subseteqq', 'backepsilon', 'equiv', 'mid', 'succ', 'backsim', 'fallingdotseq', 'models', 'succapprox', 'backsimeq', 'frown', 'multimap', 'succcurlyeq', 'between', 'ge', 'origof', 'succeq', 'bowtie', 'geq', 'owns', 'succsim', 'bumpeq', 'geqq', 'parallel', 'Supset', 'Bumpeq', 'geqslant', 'perp', 'supset', 'circeq', 'gg', 'pitchfork', 'supseteq', 'supe', 'colonapprox', 'ggg', 'prec', 'supseteqq', 'Colonapprox', 'coloncolonapprox', 'gggtr', 'precapprox', 'thickapprox', 'coloneq', 'colonminus', 'gt', 'preccurlyeq', 'thicksim', 'Coloneq', 'coloncolonminus', 'gtrapprox', 'preceq', 'trianglelefteq', 'coloneqq', 'colonequals', 'gtreqless', 'precsim', 'triangleq', 'Coloneqq', 'coloncolonequals', 'gtreqqless', 'propto', 'trianglerighteq', 'colonsim', 'gtrless', 'risingdotseq', 'varpropto', 'Colonsim', 'coloncolonsim', 'gtrsim', 'shortmid', 'vartriangle', 'cong', 'imageof', 'shortparallel', 'vartriangleleft', 'curlyeqprec', 'in', 'isin', 'sim', 'vartriangleright', 'curlyeqsucc', 'Join', 'simcolon', 'vcentcolon', 'ratio', 'dashv', 'le', 'simcoloncolon', 'vdash', 'dblcolon', 'coloncolon', 'leq', 'simeq', 'vDash', 'doteq', 'leqq', 'smallfrown', 'Vdash', 'Doteq', 'leqslant', 'smallsmile', 'Vvdash', ]; export const negatedRelations0 = [ 'gnapprox', 'ngeqslant', 'nsubseteq', 'precneqq', 'gneq', 'ngtr', 'nsubseteqq', 'precnsim', 'gneqq', 'nleq', 'nsucc', 'subsetneq', 'gnsim', 'nleqq', 'nsucceq', 'subsetneqq', 'gvertneqq', 'nleqslant', 'nsupseteq', 'succnapprox', 'lnapprox', 'nless', 'nsupseteqq', 'succneqq', 'lneq', 'nmid', 'ntriangleleft', 'succnsim', 'lneqq', 'notin', 'ntrianglelefteq', 'supsetneq', 'lnsim', 'notni', 'ntriangleright', 'supsetneqq', 'lvertneqq', 'nparallel', 'ntrianglerighteq', 'varsubsetneq', 'ncong', 'nprec', 'nvdash', 'varsubsetneqq', 'ne', 'npreceq', 'nvDash', 'varsupsetneq', 'neq', 'nshortmid', 'nVDash', 'varsupsetneqq', 'ngeq', 'nshortparallel', 'nVdash', 'ngeqq', 'nsim', 'precnapprox' ]; export const arrows0 = [ 'circlearrowleft', 'leftharpoonup', 'rArr', 'circlearrowright', 'leftleftarrows', 'rarr', 'curvearrowleft', 'leftrightarrow', 'restriction', 'curvearrowright', 'Leftrightarrow', 'rightarrow', 'Darr', 'leftrightarrows', 'Rightarrow', 'dArr', 'leftrightharpoons', 'rightarrowtail', 'darr', 'leftrightsquigarrow', 'rightharpoondown', 'dashleftarrow', 'Lleftarrow', 'rightharpoonup', 'dashrightarrow', 'longleftarrow', 'rightleftarrows', 'downarrow', 'Longleftarrow', 'rightleftharpoons', 'Downarrow', 'longleftrightarrow', 'rightrightarrows', 'downdownarrows', 'Longleftrightarrow', 'rightsquigarrow', 'downharpoonleft', 'longmapsto', 'Rrightarrow', 'downharpoonright', 'longrightarrow', 'Rsh', 'gets', 'Longrightarrow', 'searrow', 'Harr', 'looparrowleft', 'swarrow', 'hArr', 'looparrowright', 'to', 'harr', 'Lrarr', 'twoheadleftarrow', 'hookleftarrow', 'lrArr', 'twoheadrightarrow', 'hookrightarrow', 'lrarr', 'Uarr', 'iff', 'Lsh', 'uArr', 'impliedby', 'mapsto', 'uarr', 'implies', 'nearrow', 'uparrow', 'Larr', 'nleftarrow', 'Uparrow', 'lArr', 'nLeftarrow', 'updownarrow', 'larr', 'nleftrightarrow', 'Updownarrow', 'leadsto', 'nLeftrightarrow', 'upharpoonleft', 'leftarrow', 'nrightarrow', 'upharpoonright', 'Leftarrow', 'nRightarrow', 'upuparrows', 'leftarrowtail', 'nwarrow', 'leftharpoondown', 'Rarr' ]; export const extensibleArrows1 = [ 'xleftarrow', 'xrightarrow', 'xLeftarrow', 'xRightarrow', 'xleftrightarrow', 'xLeftrightarrow', 'xhookleftarrow', 'xhookrightarrow', 'xtwoheadleftarrow', 'xtwoheadrightarrow', 'xleftharpoonup', 'xrightharpoonup', 'xleftharpoondown', 'xrightharpoondown', 'xleftrightharpoons', 'xrightleftharpoons', 'xtofrom', 'xmapsto', 'xlongequal' ]; export const braketNotation1 = ['bra', 'Bra', 'ket', 'Ket', 'braket', 'Braket']; export const classAssignment1 = [ 'mathbin', 'mathclose', 'mathinner', 'mathop', 'mathopen', 'mathord', 'mathpunct', 'mathrel' ]; export const color2 = ['color', 'textcolor', 'colorbox']; export const font0 = ['rm', 'bf', 'it', 'sf', 'tt']; export const font1 = [ 'mathrm', 'mathbf', 'mathit', 'mathnormal', 'textbf', 'textit', 'textrm', 'bold', 'Bbb', 'textnormal', 'boldsymbol', 'mathbb', 'text', 'bm', 'frak', 'mathsf', 'mathtt', 'mathfrak', 'textsf', 'texttt', 'mathcal', 'mathscr', 'pmb' ]; export const size0 = [ 'Huge', 'huge', 'LARGE', 'Large', 'large', 'normalsize', 'small', 'footnotesize', 'scriptsize', 'tiny' ]; export const style0 = [ 'displaystyle', 'textstyle', 'scriptstyle', 'scriptscriptstyle', 'limits', 'nolimits', 'verb' ]; export const symbolsAndPunctuation0 = [ 'cdots', 'LaTeX', 'ddots', 'TeX', 'ldots', 'nabla', 'vdots', 'infty', 'dotsb', 'infin', 'dotsc', 'checkmark', 'dotsi', 'dag', 'dotsm', 'dagger', 'dotso', 'sdot', 'ddag', 'mathellipsis', 'ddagger', 'Box', 'Dagger', 'lq', 'square', 'angle', 'blacksquare', 'measuredangle', 'rq', 'triangle', 'sphericalangle', 'triangledown', 'top', 'triangleleft', 'bot', 'triangleright', 'colon', 'bigtriangledown', 'backprime', 'bigtriangleup', 'pounds', 'prime', 'blacktriangle', 'mathsterling', 'blacktriangledown', 'blacktriangleleft', 'yen', 'blacktriangleright', 'surd', 'diamond', 'degree', 'Diamond', 'lozenge', 'mho', 'blacklozenge', 'diagdown', 'star', 'diagup', 'bigstar', 'flat', 'clubsuit', 'natural', 'copyright', 'clubs', 'sharp', 'circledR', 'diamondsuit', 'heartsuit', 'diamonds', 'hearts', 'circledS', 'spadesuit', 'spades', 'maltese', 'minuso' ]; export const debugging0 = ['message', 'errmessage', 'show']; export const envs = [ 'matrix', 'array', 'pmatrix', 'bmatrix', 'vmatrix', 'Vmatrix', 'Bmatrix', 'cases', 'rcases', 'smallmatrix', 'subarray', 'equation', 'split', 'align', 'gather', 'alignat', 'CD', 'darray', 'dcases', 'drcases', 'matrix*', 'pmatrix*', 'bmatrix*', 'Bmatrix*', 'vmatrix*', 'Vmatrix*', 'equation*', 'gather*', 'align*', 'alignat*', 'gathered', 'aligned', 'alignedat' ]; ================================================ FILE: src/util/lazy.ts ================================================ export interface ILazy { readonly isValueCreated: boolean; readonly value: T; } /** * @see {@link https://docs.microsoft.com/en-us/dotnet/framework/performance/lazy-initialization} */ export class Lazy implements ILazy { private readonly _factory: () => T; private _isValueCreated = false; private _value: T | null = null; public get isValueCreated(): boolean { return this._isValueCreated; } public get value(): T { if (!this._isValueCreated) { this._value = this._factory(); this._isValueCreated = true; } return this._value!; } constructor(factory: () => T) { this._factory = factory; } } ================================================ FILE: src/util/slugify.ts ================================================ import SlugifyMode from "../contract/SlugifyMode"; import { configManager } from "../configuration/manager"; import { commonMarkEngine } from "../markdownEngine"; import { window } from "vscode"; /** * the wasm equivalent to just doing `import * as zolaSLug from "zola-slug"`, which we can't do because it's a wasm module */ let zolaSlug: typeof import("zola-slug"); /** * Ideally this function is called before any code that relies on slugify, * and any code that relies on slugify should be called in the `then` block. */ export async function importZolaSlug() { zolaSlug = await import("zola-slug"); } const utf8Encoder = new TextEncoder(); // Converted from Ruby regular expression `/[^\p{Word}\- ]/u` // `\p{Word}` => Letter (Ll/Lm/Lo/Lt/Lu), Mark (Mc/Me/Mn), Number (Nd/Nl), Connector_Punctuation (Pc) // It's weird that Ruby's `\p{Word}` actually does not include Category No. // https://ruby-doc.org/core/Regexp.html // https://rubular.com/r/ThqXAm370XRMz6 /** * The definition of punctuation from GitHub and GitLab. */ const Regexp_Github_Punctuation = /[^\p{L}\p{M}\p{Nd}\p{Nl}\p{Pc}\- ]/gu; const Regexp_Gitlab_Product_Suffix = /[ \t\r\n\f\v]*\**\((?:core|starter|premium|ultimate)(?:[ \t\r\n\f\v]+only)?\)\**/g; /** * Converts a string of CommonMark **inline** structures to plain text * by removing Markdown syntax in it. * This function is only for the `github`, `gitlab` and `zola` slugify functions. * @see * * @param text - The Markdown string. * @param env - The markdown-it environment sandbox (**mutable**). * If you don't provide one properly, we cannot process reference links, etc. */ function mdInlineToPlainText(text: string, env: object): string { // Use a clean CommonMark only engine to avoid interfering with plugins from other extensions. // Use `parseInline` to avoid parsing the string as blocks accidentally. // See #567, #585, #732, #792; #515; #179; #175, #575 const inlineTokens = commonMarkEngine.engine.parseInline(text, env)[0].children!; return inlineTokens.reduce((result, token) => { switch (token.type) { case "image": case "html_inline": return result; default: return result + token.content; } }, ""); } /** * Slugify methods. * * Each key is a slugify mode. * A values is the corresponding slugify function, whose signature must be `(rawContent: string, env: object) => string`. */ const Slugify_Methods: { readonly [mode in SlugifyMode]: (rawContent: string, env: object) => string; } = { // Sort in alphabetical order. [SlugifyMode.AzureDevOps]: (slug: string): string => { // https://markdown-all-in-one.github.io/docs/specs/slugify/azure-devops.html // Encode every character. Although opposed by RFC 3986, it's the only way to solve #802. slug = slug.trim() .toLowerCase() .replace(/\p{Zs}/gu, "-") if (/^\d/.test(slug)) { slug = Array.from( utf8Encoder.encode(slug), (b) => "%" + b.toString(16) ) .join("") .toUpperCase(); } else { slug = encodeURIComponent(slug) } return slug }, [SlugifyMode.BitbucketCloud]: (slug: string, env: object): string => { // https://support.atlassian.com/bitbucket-cloud/docs/readme-content/ // https://bitbucket.org/tutorials/markdowndemo/ slug = "markdown-header-" + Slugify_Methods.github(slug, env).replace(/-+/g, "-"); return slug; }, [SlugifyMode.Gitea]: (slug: string): string => { // Gitea uses the blackfriday parser // https://godoc.org/github.com/russross/blackfriday#hdr-Sanitized_Anchor_Names slug = slug .replace(/^[^\p{L}\p{N}]+/u, "") .replace(/[^\p{L}\p{N}]+$/u, "") .replace(/[^\p{L}\p{N}]+/gu, "-") .toLowerCase(); return slug; }, [SlugifyMode.GitHub]: (slug: string, env: object): string => { // According to an inspection in 2020-12, GitHub passes the raw content as is, // and does not trim leading or trailing C0, Zs characters in any step. // slug = mdInlineToPlainText(slug, env) .replace(Regexp_Github_Punctuation, "") .toLowerCase() // According to an inspection in 2020-09, GitHub performs full Unicode case conversion now. .replace(/ /g, "-"); return slug; }, [SlugifyMode.GitLab]: (slug: string, env: object): string => { // https://gitlab.com/help/user/markdown // https://docs.gitlab.com/ee/api/markdown.html // https://docs.gitlab.com/ee/development/wikis.html // // https://gitlab.com/gitlab-org/gitlab/blob/a8c5858ce940decf1d263b59b39df58f89910faf/lib/gitlab/utils/markdown.rb slug = mdInlineToPlainText(slug, env) .replace(/^[ \t\r\n\f\v]+/, "") .replace(/[ \t\r\n\f\v]+$/, "") // https://ruby-doc.org/core/String.html#method-i-strip .toLowerCase() .replace(Regexp_Gitlab_Product_Suffix, "") .replace(Regexp_Github_Punctuation, "") .replace(/ /g, "-") // Replace space with dash. .replace(/-+/g, "-") // Replace multiple/consecutive dashes with only one. // digits-only hrefs conflict with issue refs .replace(/^(\d+)$/, "anchor-$1"); return slug; }, [SlugifyMode.VisualStudioCode]: (rawContent: string, env: object): string => { // https://github.com/microsoft/vscode/blob/0798d13f10b193df0297e301affe761b90a8bfa9/extensions/markdown-language-features/src/slugify.ts#L22-L29 return encodeURI( // Simulate . // Not the same, but should cover most needs. commonMarkEngine.engine.parseInline(rawContent, env)[0].children! .reduce((result, token) => result + token.content, "") .trim() .toLowerCase() .replace(/\s+/g, "-") // Replace whitespace with - .replace(/[\]\[\!\'\#\$\%\&\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~\`。,、;:?!…—·ˉ¨‘’“”々~‖∶"'`|〃〔〕〈〉《》「」『』.〖〗【】()[]{}]/g, "") // Remove known punctuators .replace(/^\-+/, "") // Remove leading - .replace(/\-+$/, "") // Remove trailing - ); }, [SlugifyMode.Zola]: (rawContent: string, env: object): string => { if (zolaSlug !== undefined) { return zolaSlug.slugify(mdInlineToPlainText(rawContent, env)); } else { importZolaSlug(); window.showErrorMessage("Importing Zola Slug... Please try again."); return rawContent; //unsure if we should throw an error, let it fail or return the original content } } }; /** * Slugify a string. * @param heading - The raw content of the heading according to the CommonMark Spec. * @param env - The markdown-it environment sandbox (**mutable**). * @param mode - The slugify mode. */ export function slugify(heading: string, { env = Object.create(null), mode = configManager.get("toc.slugifyMode"), }: { env?: object; mode?: SlugifyMode; }) { // Do never twist the input here! // Pass the raw heading content as is to slugify function. // Sort by popularity. switch (mode) { case SlugifyMode.GitHub: return Slugify_Methods[SlugifyMode.GitHub](heading, env); case SlugifyMode.GitLab: return Slugify_Methods[SlugifyMode.GitLab](heading, env); case SlugifyMode.Gitea: return Slugify_Methods[SlugifyMode.Gitea](heading, env); case SlugifyMode.VisualStudioCode: return Slugify_Methods[SlugifyMode.VisualStudioCode](heading, env); case SlugifyMode.AzureDevOps: return Slugify_Methods[SlugifyMode.AzureDevOps](heading, env); case SlugifyMode.BitbucketCloud: return Slugify_Methods[SlugifyMode.BitbucketCloud](heading, env); case SlugifyMode.Zola: return Slugify_Methods[SlugifyMode.Zola](heading, env); default: return Slugify_Methods[SlugifyMode.GitHub](heading, env); } } ================================================ FILE: src/zola-slug/.gitignore ================================================ debug/ target/ Cargo.lock **/*.rs.bk *.pdb pkg/ ================================================ FILE: src/zola-slug/Cargo.toml ================================================ [package] name = "zola-slug" version = "0.1.0" edition = "2021" authors = ["hill "] description = "expose slug to wasm" repository = "https://github.com/yzhang-gh/vscode-markdown/" license = "MIT" [lib] crate-type = ["cdylib", "rlib"] [features] default = ["mini-alloc"] mini-alloc = ["dep:mini-alloc"] [dependencies] slug = "0.1.0" mini-alloc = { version = "0.4.2", optional = true } [profile.release] codegen-units = 1 debug = false debug-assertions = false incremental = true lto = true opt-level = "s" overflow-checks = false panic = "unwind" rpath = false split-debuginfo = "off" strip = "symbols" [package.metadata.wasm-pack.profile.release] wasm-opt = ['-Os'] [package.metadata.wasm-pack.profile.release.wasm-bindgen] debug-js-glue = false demangle-name-section = false dwarf-debug-info = false omit-default-module-path = false ================================================ FILE: src/zola-slug/LICENSE ================================================ MIT License Copyright (c) 2024 hill Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: src/zola-slug/src/lib.rs ================================================ #[allow(unused_imports)] use slug::slugify; #[cfg(feature = "mini-alloc")] #[global_allocator] static ALLOC: mini_alloc::MiniAlloc = mini_alloc::MiniAlloc::INIT; #[cfg(test)] mod tests { use slug::slugify; #[test] fn test_slugify() { let test_cases = vec![ ("にでも長所と短所がある", "nidemochang-suo-toduan-suo-gaaru"), ( "命来犯天写最大巡祭視死乃読", "ming-lai-fan-tian-xie-zui-da-xun-ji-shi-si-nai-du", ), ( "국무위원은 국무총리의 제청으로 대통령이 임명한다", "gugmuwiweoneun-gugmucongriyi-jeceongeuro-daetongryeongi-immyeonghanda", ), ]; test_cases.into_iter().for_each(|case| { assert_eq!(slugify(case.0), case.1); }); } } ================================================ FILE: syntaxes/katex.tmLanguage.json ================================================ { "fileTypes": [], "patterns": [ { "include": "#environment" }, { "include": "#function" }, { "include": "#align" }, { "include": "#character" }, { "include": "#comment" }, { "include": "#macros" } ], "repository": { "align": { "name": "keyword.control.table.cell.katex", "match": "((?.txt`) in the project root. * * Due to security concerns, code points under Unicode General Category C are not allowed. * Thus, EOLs are also not allowed in both the file and the parameter. * Thus, the definition of "line" here differs greatly from the POSIX standard. * @param {string} message The welcome message. Must be single line, and safe for display. * @param {string} locale The locale ID, which should be recognized by VS Code. * But you can still provide an arbitrary one, as long as it matches the format. * @returns `true` for success. */ function setWelcomeMessage(message, locale = "en") { if (!message || /^\s*$/.test(message)) { throw new Error("The message must contain non-whitespace characters."); } if (/\p{C}/u.test(message)) { throw new Error("Control characters and other code points under Unicode General Category C are not allowed."); } if (!/^[A-Za-z]+(?:-[A-Za-z]+)*$/.test(locale)) { throw new Error("The locale ID must only contain ASCII letters or hyphens, and must begin and end with letters."); } const messageFilePath = path.resolve(__dirname, "..", "welcome", locale + ".txt"); fs.mkdirSync(path.resolve(__dirname, "..", "welcome"), { recursive: true }); fs.writeFileSync(messageFilePath, message, "utf8"); console.log(`\nSucceeded.\nMessage: ${message}\nPath: ${messageFilePath}\n`); return true; } module.exports = setWelcomeMessage; /* Main. */ if (process.argv[1] === __filename) { if (process.argv.length <= 4) { setWelcomeMessage(process.argv[2], process.argv[3]); } else { throw new Error("\nThis requires one or two arguments.\nAre you calling it in a correct way?\n"); } } ================================================ FILE: tsconfig.base.json ================================================ { "$schema": "https://json.schemastore.org/tsconfig", "$comment": "Here are only compilerOptions shared across the project. Please sort them in the order of the TSConfig Reference.", "compilerOptions": { "strict": true, "allowUnreachableCode": false, "allowUnusedLabels": false, "exactOptionalPropertyTypes": true, "noFallthroughCasesInSwitch": true, "noImplicitOverride": true, "noImplicitReturns": true, "noUnusedLocals": true, "noUnusedParameters": true, "moduleResolution": "Node", "newLine": "lf", "allowSyntheticDefaultImports": false, "esModuleInterop": false, "forceConsistentCasingInFileNames": true } } ================================================ FILE: tsconfig.json ================================================ { "extends": "./tsconfig.base.json", "compilerOptions": { "module": "CommonJS", "target": "ES2020", "lib": ["ES2020"], "noUnusedLocals": false, "rootDir": "src", "outDir": "out", "sourceMap": true }, "include": ["src/**/*"] } ================================================ FILE: webpack.config.js ================================================ // @ts-check // # Notes // // ## Target and environment // // Specify the `target` option precisely to help webpack 5 to do subtle optimization. // For example, use `node16` instead of `node` when targeting Node.js 16 or higher. // When in doubt, check: https://github.com/webpack/webpack/blob/main/lib/config/target.js // // ## Source map and debugging // // Source map, which is critical for debugging, is a tricky part. // TypeScript compiler emits maps with relative source paths and no source content. // Webpack is controlled by `devtool` and `devtoolModuleFilenameTemplate` which defaults to its `webpack` protocol. // Webpack's internal paths are something between file system path and URL. Quite awkward. // The VS Code JavaScript Debugger can understand various URLs, // and allows rewriting via the `sourceMapPathOverrides` in launch configuration. // // Recommendation: // Webpack's default file name template often works fine. // File URL may improve the experience of Node.js debugging. // Exclude source content to save space. // // Helpful resources: // https://code.visualstudio.com/docs/nodejs/nodejs-debugging // https://survivejs.com/webpack/building/source-maps/ // https://github.com/webpack/webpack/issues/3603 // https://github.com/webpack/webpack/issues/8226 // https://github.com/microsoft/vscode-js-debug/blob/main/src/configuration.ts // https://github.com/microsoft/vscode-js-debug/tree/main/src/targets // https://www.typescriptlang.org/tsconfig#sourceMap // https://chromedevtools.github.io/devtools-protocol/v8/ // https://gist.github.com/jarshwah/389f93f2282a165563990ed60f2b6d6c // // ## Webpack mode // // The `mode` is not set here. // The build pipeline or CI sets it according to OS environment variables. // In all other scenarios, please use webpack-cli flags. "use strict"; const path = require("path"); const { pathToFileURL } = require("url"); /** @typedef {import("webpack").Configuration} WebpackConfig **/ /** @type {WebpackConfig} */ const Config_Base = { context: __dirname, entry: { main: "./src/extension.ts", }, resolve: { extensions: [".ts", ".js"], mainFields: ["module", "main"], }, module: { rules: [ { test: /\.ts$/, include: path.resolve(__dirname, "src"), use: [ { loader: "ts-loader", }, ], }, ], }, externals: { vscode: "commonjs vscode", // It is only present in the VS Code extension hosts. }, devtool: "nosources-source-map", }; /** @type {WebpackConfig} */ const Config_Node = { ...Config_Base, name: "node", target: "node14", output: { filename: "[name].js", path: path.resolve(__dirname, "dist", "node"), library: { type: "commonjs2" }, webassemblyModuleFilename: "zola_slug_bg.wasm", // Don't use `absoluteResourcePath`, as it's often not a file system path. devtoolModuleFilenameTemplate: (info) => pathToFileURL(path.resolve(__dirname, info.resourcePath)).href, }, experiments: { asyncWebAssembly: true, } }; module.exports = [Config_Node];