diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index b2c3352..0000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -docs/**/* linguist-generated=true diff --git a/.github/workflows/mdbook.yml b/.github/workflows/mdbook.yml index ef8def8..b7953ff 100644 --- a/.github/workflows/mdbook.yml +++ b/.github/workflows/mdbook.yml @@ -2,7 +2,7 @@ # # To get started with mdBook see: https://rust-lang.github.io/mdBook/index.html # -name: Deploy mdBook site to Pages +name: Deploy on: # Runs on pushes targeting the default branch @@ -37,7 +37,7 @@ jobs: curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf -y | sh rustup update cargo install --version ${MDBOOK_VERSION} mdbook - cargo install mdbook-plantuml + cargo install mdbook-alerts - name: Setup Pages id: pages uses: actions/configure-pages@v4 diff --git a/.github/workflows/reviewdog.yml b/.github/workflows/reviewdog.yml deleted file mode 100644 index b28394e..0000000 --- a/.github/workflows/reviewdog.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: reviewdog - -on: - push: - branches: - - main - pull_request: - -jobs: - misspell: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: misspell - uses: reviewdog/action-misspell@v1 - with: - github_token: ${{ secrets.github_token }} - locale: "US" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ec33f1b..e9c1f91 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,21 +2,18 @@ name: Test env: DENO_VERSION: 1.x + MDBOOK_VERSION: 0.4.36 on: push: - branches: - - main pull_request: - branches: - - main jobs: test: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/cache@v2 + - uses: actions/checkout@v4 + - uses: actions/cache@v4 with: path: | ~/.cargo/bin/ @@ -26,17 +23,18 @@ jobs: target/ .tools/ key: ${{ runner.os }}-cargo - - uses: denoland/setup-deno@main + - uses: denoland/setup-deno@v1.1.4 with: deno-version: ${{ env.DENO_VERSION }} - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - - name: Format - run: make fmt-check - - name: Install tools - run: make tools - - name: Generate + - name: Install mdBook run: | - make gen - git diff --check + curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf -y | sh + rustup update + cargo install --version ${MDBOOK_VERSION} mdbook + cargo install mdbook-alerts + - name: Build with mdBook + run: mdbook build + - name: Format + run: deno fmt --check + - name: Misspell + uses: reviewdog/action-misspell@v1.15.0 diff --git a/.gitignore b/.gitignore index c6afb2f..5a0bf03 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ -/.tools /book diff --git a/Makefile b/Makefile deleted file mode 100644 index 23cc93a..0000000 --- a/Makefile +++ /dev/null @@ -1,25 +0,0 @@ -TOOLS := ${CURDIR}/.tools - -.DEFAULT_GOAL := help - -help: - @cat $(MAKEFILE_LIST) | \ - perl -ne 'print if /^\w+.*##/;' | \ - perl -pe 's/(.*):.*##\s*/sprintf("%-20s",$$1)/eg;' - -tools: FORCE ## Install development tools - @mkdir -p ${TOOLS} - @cargo install mdbook --root ${TOOLS} - @cargo install mdbook-plantuml --root ${TOOLS} - -fmt: FORCE ## Format code - @deno fmt src README.md - -fmt-check: FORCE ## Format check - @deno fmt --check src README.md - -gen: FORCE ## Generate codes - @${TOOLS}/bin/mdbook build - @make fmt - -FORCE: diff --git a/README.md b/README.md index edd4e87..8a638ad 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ -# Denops Documentation +# denops-documentation [![Test](https://github.com/vim-denops/denops-documentation/actions/workflows/test.yml/badge.svg)](https://github.com/vim-denops/denops-documentation/actions/workflows/test.yml) +[![Deploy](https://github.com/vim-denops/denops-documentation/actions/workflows/mdbook.yml/badge.svg)](https://github.com/vim-denops/denops-documentation/actions/workflows/mdbook.yml) [![Documentation](https://img.shields.io/badge/denops-Documentation-yellow.svg)](https://vim-denops.github.io/denops-documentation/) -This is an official documentation of [denops.vim][denops.vim], an ecosystem of -Vim/Neovim which allows developers to write plugins in [Deno][Deno]. +This is an official documentation of [denops.vim], an ecosystem of Vim/Neovim +which allows developers to write plugins in [Deno]. [denops.vim]: https://github.com/vim-denops/denops.vim [deno]: https://deno.land @@ -20,26 +21,31 @@ To contribute, install the latest versions of the followings in your environment - [Rust](https://www.rust-lang.org/tools/install) - [Deno](https://deno.land/) -Then, install [mdBook](https://github.com/rust-lang/mdBook) and its plugins in -`.tools` directory by +Then, install [mdBook](https://github.com/rust-lang/mdBook) and its plugins ``` -make tools +cargo install mdbook +cargo install mdbook-alerts ``` -Once required tools are installed, execute the following command to generate -static files in [`./docs`](./docs) from markdown files in [`./src`](./src). +Once required tools are installed, execute the following command to serve the +book on http://localhost:3000 ``` -make gen +mdbook serve ``` -Note that all markdown files are formatted with -[Deno's code formatter](https://deno.land/manual/tools/formatter) and checked by -CI thus make sure to format codes by the following command +Or build book into `book` directory ``` -make fmt +mdbook build +``` + +Don't forget to format Markdown files with `deno fmt` before sending a pull +request. + +``` +deno fmt ``` ## See also diff --git a/book.toml b/book.toml index 0cddb7c..93641a5 100644 --- a/book.toml +++ b/book.toml @@ -5,5 +5,4 @@ multilingual = false src = "src" title = "Denops Documentation" -[preprocessor.plantuml] -plantuml-cmd="http://www.plantuml.com/plantuml" +[preprocessor.alerts] diff --git a/deno.jsonc b/deno.jsonc new file mode 100644 index 0000000..b6d6a7b --- /dev/null +++ b/deno.jsonc @@ -0,0 +1,3 @@ +{ + "exclude": ["theme/**/*", "book/**/*"] +} diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 56be2d9..154fcdd 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -3,13 +3,18 @@ [Introduction](./introduction.md) - [Install](./install.md) -- [Tutorial](./tutorial.md) - - [Preparing Deno and Denops](./tutorial/preparing-deno-and-denops.md) - - [Developing Your First Plugin](./tutorial/developing-your-first-plugin.md) - - [Vim/Neovim Configuration](./tutorial/vimneovim-configuration.md) - - [Making a Plugin Directory Tree](./tutorial/making-a-directory-tree.md) - - [Adding a Skelton of Denops Plugin](./tutorial/adding-a-skelton-of-denops-plugin.md) - - [Adding an API](./tutorial/adding-an-api.md) - - [Calling Vim/Neovim Features](./tutorial/calling-vimneovim-features.md) - - [Developing More Applicative Plugin](./tutorial/developing-more-applicative-plugin.md) - - [Developing Your Next Plugins](./tutorial/developing-your-next-plugins.md) +- [Getting Started](./getting-started/README.md) + - [Explanation of the Getting started](./getting-started/explanation.md) +- [Tutorial (Hello world)](./tutorial/helloworld/README.md) + - [Creating a minimal Vim plugin](./tutorial/helloworld/creating-a-minimal-vim-plugin.md) + - [Creating a minimal Denops plugin](./tutorial/helloworld/creating-a-minimal-denops-plugin.md) + - [Adding Denops APIs](./tutorial/helloworld/adding-an-api.md) + - [Calling Vim features](./tutorial/helloworld/calling-vim-features.md) +- [Tutorial (Maze)](./tutorial/maze/README.md) + - [Utilizing third-party library](./tutorial/maze/utilizing-third-party-library.md) + - [Outputting content to buffer](./tutorial/maze/outputting-content-to-buffer.md) + - [Adjusting maze size to fit the window](./tutorial/maze/adjusting-maze-size-to-fit-the-window.md) + - [Properly create a virtual buffer](./tutorial/maze/properly-create-a-virtual-buffer.md) + - [Properly configured the buffer](./tutorial/maze/properly-configured-the-buffer.md) + - [Reduce the number of RPC calls](./tutorial/maze/reduce-the-number-of-rpc-calls.md) +- [FAQ](./faq.md) diff --git a/src/faq.md b/src/faq.md new file mode 100644 index 0000000..b61009a --- /dev/null +++ b/src/faq.md @@ -0,0 +1,24 @@ +# FAQ + +## How to Check Denops Startup Time + +To check the startup time of Denops or Denops plugins, utilize +[denops-startup-recorder]. This plugin visualizes the timing of events related +to Denops and Denops plugin startup. + +[denops-startup-recorder]: https://github.com/vim-denops/denops-startup-recorder.vim + +It shows the result in echo area like: + +![](./img/faq-1.png) + +## How to Check Denops Performance + +To assess Denops performance, employ [denops-benchmark]. This plugin measures +the number of operations or characters that can be processed in milliseconds. + +[denops-benchmark]: https://github.com/vim-denops/denops-benchmark.vim + +It shows the result in a buffer like: + +![](./img/faq-2.png) diff --git a/src/getting-started/README.md b/src/getting-started/README.md new file mode 100644 index 0000000..a092f6b --- /dev/null +++ b/src/getting-started/README.md @@ -0,0 +1,68 @@ +# Getting Started + +[Denops] ([/ˈdiːnoʊps/](http://ipa-reader.xyz/?text=%CB%88di%CB%90no%CA%8Aps), +pronounced `dee-nops`) is an ecosystem for [Vim] / [Neovim] that empowers +developers to write plugins in [TypeScript] / [JavaScript] powered by [Deno]. + +Let's start by creating a simple plugin to learn how to develop Denops plugins. + +## Create a Plugin + +Create a directory named `denops-getting-started` in your home directory and a +file named `main.ts` within it, under `denops/denops-getting-started/`: + +``` +$HOME +└── denops-getting-started + └── denops + └── denops-getting-started + └── main.ts +``` + +Next, write the following TypeScript code in `main.ts`: + +```typescript +import type { Denops } from "https://deno.land/x/denops_std@v6.0.0/mod.ts"; + +export function main(denops: Denops): void { + denops.dispatcher = { + async hello() { + await denops.cmd(`echo "Hello, Denops!"`); + }, + }; +} +``` + +## Activate the Plugin + +Add the following line to your Vim or Neovim configuration file (e.g., +`~/.vimrc` or `~/.config/nvim/init.vim`): + +```vim +set runtimepath+=~/denops-getting-started +``` + +Or Neovim Lua configuration file (e.g., `~/.config/nvim/init.lua`): + +```lua +vim.opt.runtimepath:append("~/denops-getting-started") +``` + +## Try the Plugin + +Restart Vim/Neovim and execute the following command: + +```vim +:call denops#request('denops-getting-started', 'hello', []) +``` + +You should see "Hello, Denops!" displayed on the screen like: + +![](./img/README-01.png) + +[Denops]: https://github.com/vim-denops/denops.vim +[Vim]: https://www.vim.org/ +[Neovim]: https://neovim.io/ +[TypeScript]: https://www.typescriptlang.org/ +[JavaScript]: https://developer.mozilla.org/en-US/docs/Web/JavaScript +[Deno]: https://deno.land/ diff --git a/src/getting-started/explanation.md b/src/getting-started/explanation.md new file mode 100644 index 0000000..2655ffd --- /dev/null +++ b/src/getting-started/explanation.md @@ -0,0 +1,241 @@ +# Explanation of the Getting Started + +In this section, we'll provide detailed information about the Getting Started. +If you find it too detailed, feel free to skip this section and move on to the +next chapter, especially if your goal is to start developing a Denops plugin +promptly. + +## What is Denops? + +Denops claims to be an ecosystem for developing Vim / Neovim (hereafter, when we +refer to Vim without restriction, we also include Neovim) plugins using Deno, +but, in reality, it is a Vim plugin with the following features: + +- Detection and registration of Denops plugins +- Launching and connecting to Deno processes +- Calling Deno process-side functions from Vim via RPC (Remote Procedure Call) +- Calling Vim features from Deno process-side via RPC + +By utilizing this plugin, you can control Vim from code written in TypeScript +(Denops plugins). + +> [!NOTE] +> +> [RPC (Remote Procedure Call)](https://en.wikipedia.org/wiki/Remote_procedure_call) +> is used, and while Vim uses a +> [JSON-based custom specification](https://vim-jp.org/vimdoc-en/channel.html#channel-use), +> Neovim uses [MessagePack-RPC](https://github.com/msgpack-rpc/msgpack-rpc) (a +> slightly modified specification). However, Denops abstracts away these +> differences, so Denops plugin developers don't need to be aware of the RPC +> specification differences between Vim and Neovim. + +## What is a Vim Plugin? + +When Vim starts, it searches for files named `plugin/*.vim` in directories +specified in `runtimepath`. Additionally, if a function like `foo#bar#hoge()` is +called, it searches for files named `autoload/foo/bar.vim` in the `runtimepath` +and reads the file, calling the `foo#bar#hoge()` function defined within. + +A Vim plugin is a set of predefined features provided to users, utilizing the +functionality mentioned above. Typically, an entry point is defined in +`plugin/{plugin_name}.vim`, and detailed features are implemented in +`autoload/{plugin_name}.vim` or `autoload/{plugin_name}/*.vim`. For example, +here is the directory structure for a Vim plugin named `hello`: + +``` +vim-hello +├── autoload +│ └── hello.vim # Defines the function `hello#hello()` +└── plugin + └── hello.vim # Defines the `Hello` command +``` + +> [!NOTE] +> +> For more detailed information on creating Vim plugins, refer to +> `:help write-plugin`. + +## What is a Denops Plugin? + +When Denops is installed, in addition to Vim plugins, files named +`denops/*/main.ts` are also searched when Vim starts. If a corresponding file is +found, Denops registers the parent directory name (`foo` in the case of +`denops/foo/main.ts`) as the plugin name. Then, it imports the corresponding +file as a TypeScript module and calls the function named `main`. + +A Denops plugin, similar to a Vim plugin, provides a set of features written in +TypeScript to users. Since Denops plugins typically include both TypeScript and +Vim script code, the directory structure looks like an extension of the Vim +plugin structure with an added `denops` directory. For example, here is the +directory structure for a Denops plugin named `hello`: + +``` +denops-hello +├── autoload +│ └── hello.vim # Tasks better written in Vim script (may not exist) +├── denops +│ └── hello +│ └── main.ts # Entry point for the Denops plugin (mandatory) +└── plugin + └── hello.vim # Entry point written in Vim script (optional) +``` + +In the Getting Started, we created a file named +`denops/denops-getting-started/main.ts` and added its parent directory +(`denops-getting-started`) to `runtimepath`. There were no `autoload` or +`plugin` directories because we didn't provide an entry point that Vim could +easily call. + +## Understanding the Code in Getting Started + +In the Getting Started, we wrote the following code in the `main.ts` file: + +```typescript +import type { Denops } from "https://deno.land/x/denops_std@v6.0.0/mod.ts"; + +export function main(denops: Denops): void { + denops.dispatcher = { + async hello() { + await denops.cmd(`echo "Hello, Denops!"`); + }, + }; +} +``` + +Let's break down this code step by step. + +### About Imports + +```typescript +import type { Denops } from "https://deno.land/x/denops_std@v6.0.0/mod.ts"; +``` + +The first line imports the `Denops` type from the [denops_std] standard library. +You can find detailed information about the library by checking the URL: +`https://deno.land/x/denops_std@v6.0.0` (remove `/mod.ts`). We fixed the version +in the import URL, so it's recommended to check for details and update to the +latest version URL. + +Note that we use `import type` syntax, which is part of TypeScript's +[Type-Only Imports and Export](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html). +This syntax can be written as `import { type Denops }` with the same meaning. +Using `import { Denops }` for a type-only import is also valid. + +> [!NOTE] +> +> Denops plugins are dynamically imported, so there might be differences in +> Denops versions between development and usage. Therefore, to minimize +> differences between Denops versions, only the `Denops` type information is +> exposed. The implementation can be found in +> [`denops/@denops-private/denops.ts`](https://github.com/vim-denops/denops.vim/blob/main/denops/%40denops-private/denops.ts), +> but it is not publicly exposed for the reasons mentioned above. +> +> This type information is provided by [denops_core], and [denops_std] simply +> re-exports the type information from [denops_core]. However, [denops_core] is +> intended to be referenced only by [denops.vim] and [denops_std], so Denops +> plugin developers don't need to use it directly. + +### About Entry Point + +```typescript +export function main(denops: Denops): void { + // Omitted... +} +``` + +The above code exports the `main` function. The `main` function is called by +Denops, and it takes the +[Denops instance](https://deno.land/x/denops_std/mod.ts?s=Denops) (`denops`) as +an argument. Denops plugins use this `denops` to add user-defined APIs or call +Vim's features. + +### About User-Defined APIs + +```typescript +denops.dispatcher = { + async hello() { + // Omitted... + }, +}; +``` + +The code above adds a user-defined API named `hello` to `denops.dispatcher`. +`denops.dispatcher` is defined as follows, and each method takes `unknown` types +for both arguments and return values: + +```typescript +interface Dispatcher { + [key: string]: (...args: unknown[]) => unknown; +} +``` + +By defining methods in `denops.dispatcher`, you can freely define APIs. Since +the methods registered in `denops.dispatcher` are always called with `await`, +you can make them asynchronous by returning a `Promise`. + +The methods defined in `denops.dispatcher` can be called from Vim using the +following functions: + +| Function | Description | +| ---------------------- | -------------------------------------------------------------------------------- | +| `denops#request` | Synchronously calls a user-defined API and returns the result. | +| `denops#request_async` | Asynchronously calls a user-defined API and passes the result to callbacks. | +| `denops#notify` | Calls a user-defined API without waiting for completion and discards the result. | + +At the end of the Getting Started, we used +`denops#request('denops-getting-started', 'hello', [])` to call the user-defined +API named `hello` in `denops-getting-started` plugin. + +### About Calling Vim's features + +```typescript +await denops.cmd(`echo "Hello, Denops!"`); +``` + +With the received `denops`, you can call Vim functions, execute Vim commands, or +evaluate Vim expressions. In the example above, the `hello` API internally uses +`denops.cmd` to execute the `echo` command in Vim. The `denops` object provides +several methods: + +| Method | Description | +| ---------- | ---------------------------------------------------------------------------------------------------------- | +| `call` | Calls a Vim function and returns the result. | +| `batch` | Calls multiple Vim functions in bulk and returns the results in bulk. | +| `cmd` | Executes a Vim command. If `ctx` is provided, it is expanded as local variables. | +| `eval` | Evaluate a Vim expression and returns the result. If `ctx` is provided, it is expanded as local variables. | +| `dispatch` | Calls a user-defined API of another Denops plugin and returns the result. | + +Although `denops` provides low-level interfaces, [denops_std] combines these +low-level interfaces to offer higher-level interfaces. Therefore, it's +recommended to use [denops_std] to call Vim's features in actual plugin +development. + +For example, use +[`function` module](https://deno.land/x/denops_std@v6.0.0/function/mod.ts) to +call Vim's function instead of `denops.call` like: + +```typescript +import * as fn from "https://deno.land/x/denops_std@v6.0.0/function/mod.ts"; + +// Bad (result1 is `unknown`) +const result1 = await denops.call("expand", "%"); + +// Good (result2 is `string`) +const result2 = await fn.expand(denops, "%"); +``` + +If developers use `function` module instead, they can benefit from features like +auto-completion and type checking provided by LSP (Language Server Protocol). + +## Next Steps + +In the next step, follow the tutorial to learn how to develop a minimum Denops +plugin. + +- [Tutorial (Hello World)](../tutorial/helloworld/README.md) +- [Tutorial (Maze)](../tutorial/maze/README.md) +- [API reference](https://deno.land/x/denops_std/mod.ts) + +[denops.vim]: https://github.com/vim-denops/denops.vim +[denops_std]: https://deno.land/x/denops_std +[denops_core]: https://deno.land/x/denops_core diff --git a/src/getting-started/img/README-01.png b/src/getting-started/img/README-01.png new file mode 100644 index 0000000..2028207 Binary files /dev/null and b/src/getting-started/img/README-01.png differ diff --git a/src/img/adding-a-skelton-of-denops-plugin-1.png b/src/img/adding-a-skelton-of-denops-plugin-1.png deleted file mode 100644 index bd8b3a7..0000000 Binary files a/src/img/adding-a-skelton-of-denops-plugin-1.png and /dev/null differ diff --git a/src/img/adding-an-api-1.png b/src/img/adding-an-api-1.png deleted file mode 100644 index 5cfd704..0000000 Binary files a/src/img/adding-an-api-1.png and /dev/null differ diff --git a/src/img/adding-an-api-2.png b/src/img/adding-an-api-2.png deleted file mode 100644 index 56c0039..0000000 Binary files a/src/img/adding-an-api-2.png and /dev/null differ diff --git a/src/img/calling-vimneovim-features-1.png b/src/img/calling-vimneovim-features-1.png deleted file mode 100644 index 6175246..0000000 Binary files a/src/img/calling-vimneovim-features-1.png and /dev/null differ diff --git a/src/img/developing-more-applicative-plugin-2.png b/src/img/developing-more-applicative-plugin-2.png deleted file mode 100644 index dfe3ff5..0000000 Binary files a/src/img/developing-more-applicative-plugin-2.png and /dev/null differ diff --git a/src/img/developing-more-applicative-plugin-3.png b/src/img/developing-more-applicative-plugin-3.png deleted file mode 100644 index db5dd52..0000000 Binary files a/src/img/developing-more-applicative-plugin-3.png and /dev/null differ diff --git a/src/img/faq-1.png b/src/img/faq-1.png new file mode 100644 index 0000000..75b01a9 Binary files /dev/null and b/src/img/faq-1.png differ diff --git a/src/img/faq-2.png b/src/img/faq-2.png new file mode 100644 index 0000000..be65513 Binary files /dev/null and b/src/img/faq-2.png differ diff --git a/src/install.md b/src/install.md index 91df932..3c29039 100644 --- a/src/install.md +++ b/src/install.md @@ -1,88 +1,89 @@ -## Requirements +# Installation -Denops require the followings +## Prerequisites -- [Vim][Vim] >= 8.1.2424 or [Neovim][Neovim] >= 0.4.4 -- [Deno][Deno] >= 1.11.0 +Denops requires [Deno] to be installed on your system. Please refer to +[Deno's official manual](https://docs.deno.com/runtime/manual#install-deno) for +installation instructions. -Make sure `deno` command is executable from your Vim/Neovim by: - -``` -:echo exepath('deno') -``` - -It would show an executable path of `deno` command. If nothing is shown, make -sure the `$PATH` is correct in your Vim/Neovim. - -Use `g:denops#deno` if you'd like to specify deno executable manually like: +After installing Deno, ensure that the `deno` command is accessible from your +Vim. ```vim -let g:denops#deno = '/opt/deno/bin/deno' +:echo execpath("deno") ``` -[vim]: https://www.vim.org/ -[neovim]: https://neovim.io/ -[deno]: https://deno.land/ +It should display the path to the `deno` command. If it prints an empty string, +add the path to the `deno` command to your `PATH` environment variable. -## Install +> [!TIP] +> +> If you prefer not to modify the `PATH` environment variable, you can set the +> executable path to the `g:denops#deno` variable in your `vimrc` file like +> this: +> +> ```vim +> let g:denops#deno = "/path/to/deno" +> ``` -Denops must be installed in a `runtimepath`, like a general Vim plugin. Install -it with your favorite Vim plugin managers like: +[Deno]: https://deno.land/ -### By [vim-plug][vim-plug] +## Installation -Add `vim-denops/denops.vim` like: +Denops itself is a Vim plugin and can be installed using a Vim plugin manager, +similar to other Vim plugins. -``` +##### [vim-plug](https://github.com/junegunn/vim-plug) + +```vim Plug 'vim-denops/denops.vim' ``` -Then execute `:PlugInstall` to install. - -[vim-plug]: https://github.com/junegunn/vim-plug +##### [Jetpack.vim](https://github.com/tani/vim-jetpack) -### By [minpac][minpac] - -Add `vim-denops/denops.vim` like: - -``` -call minpac#add('vim-denops/denops.vim') +```vim +Jetpack 'vim-denops/denops.vim' ``` -Then execute `:call minpac#update()` to install. - -[minpac]: https://github.com/k-takata/minpac +##### [dein.vim](https://github.com/Shougo/dein.vim) -### By [dein.vim][dein.vim] +```vim +call dein#add('vim-denops/denops.vim') +``` -Add `vim-denops/denops.vim` like: +##### [minpac](https://github.com/k-takata/minpac) -``` -call dein#add('vim-denops/denops.vim') +```vim +call minpac#add('vim-denops/denops.vim') ``` -Then execute `:call dein#install()` to install. +##### [lazy.nvim](https://github.com/folke/lazy.nvim) -[dein.vim]: https://github.com/Shougo/dein.vim +```lua +require("lazy").setup({ + "vim-denops/denops.vim", + -- ... +}) +``` -## Check health +## Health Check -Denops support `:checkheath` (Neovim) or `:CheckHealth` (Vim with -[vim-healthcheck][vim-healthcheck]) to check denops health like: +Denops provides a health checker to confirm that Denops is installed correctly. +You can check the health of Denops by running the `:checkhealth` command +(Neovim) or `:CheckHealth` (Vim with [vim-healthcheck]). ``` -health#denops#check -======================================================================== - - INFO: Supported Deno version: `1.11.0` - - INFO: Detected Deno version: `1.11.5` - - OK: Deno version check: passed - - INFO: Supported Neovim version: `0.4.4` - - INFO: Detected Neovim version: `0.5.0` - - OK: Neovim version check: passed - - INFO: Denops status: `running` - - OK: Denops status check: passed +============================================================================== +denops: health#denops#check + +- Supported Deno version: `1.38.5` +- Detected Deno version: `1.39.4` +- OK Deno version check: passed +- Supported Neovim version: `0.9.4` +- Detected Neovim version: `0.9.5` +- OK Neovim version check: passed +- Denops status: `running` +- OK Denops status check: passed ``` -Execute those commands to investigate why denops does not work. - [vim-healthcheck]: https://github.com/rhysd/vim-healthcheck diff --git a/src/introduction.md b/src/introduction.md index 3804157..0afa18c 100644 --- a/src/introduction.md +++ b/src/introduction.md @@ -1,19 +1,48 @@ -# Denops +# Introduction -

- -

+
+Denops Mascot +
-[Denops][Denops] is an ecosystem of Vim/Neovim which allows developers to write -plugins in [Deno][Deno]. It has the following features: +**[Denops]** +([/ˈdiːnoʊps/](http://ipa-reader.xyz/?text=%CB%88di%CB%90no%CA%8Aps), pronounced +as `dee-nops`) is an ecosystem designed for developing plugins for [Vim] and +[Neovim] using [Deno] (a [TypeScript] / [JavaScript] runtime). -- Same code can be used in both Vim and Neovim -- Can be installed as a Vim plugin -- Deno uses V8 engine which is much faster than Vim script -- User don't need to manage library dependencies -- Denops runs as a separate process, so Vim won't freeze -- Each plugin work on its own thread, so that there is less chance of - interference +Denops and Denops plugins (Vim plugins powered by Denops) offer the following +features: -[deno]: https://deno.land -[denops]: https://github.com/vim-denops/denops.vim +- **Installable as a Vim plugin**:
Denops follows the standard Vim plugin + architecture. Users can install Denops itself and Denops plugins using a Vim + plugin manager, just like any other Vim plugins. +- **Unified codebase for Vim and Neovim**:
Denops provides a unified API for + both Vim and Neovim. You can write a plugin that functions on both Vim and + Neovim with a single codebase. +- **No worries about dependency management**:
Deno includes a built-in + dependency management system, allowing developers to write plugins with + third-party libraries without concerns about dependency management. +- **Simple and efficient code**:
Deno utilizes the V8 engine, significantly + faster than Vim script. You can write a plugin with straightforward code, + without the need for complex optimizations solely for performance. +- **Risk-free execution**:
Denops plugins run in a separate process from Vim + / Neovim. Even if a plugin freezes, Vim / Neovim remains unaffected. + +Check out [vim-denops](https://github.com/topics/vim-denops) GitHub Topics to +discover Vim plugins using Denops. + +Denops is primarily developed and maintained by the [vim-denops] organization +(separated from the [vim-jp] organization). For questions, you can use +[GitHub Discussions](https://github.com/orgs/vim-denops/discussions) (English), +or visit the [#tech-denops](https://vim-jp.slack.com/archives/C01N4L5362D) +channel on +[Slack workspace for vim-jp](https://join.slack.com/t/vim-jp/shared_invite/zt-zcifn2id-e6EsDjIKEzx~UlF~hE2Njg) +(Japanese). + +[Denops]: https://github.com/vim-denops/denops.vim +[Vim]: https://www.vim.org/ +[Neovim]: https://neovim.io/ +[TypeScript]: https://www.typescriptlang.org/ +[JavaScript]: https://developer.mozilla.org/en-US/docs/Web/JavaScript +[Deno]: https://deno.land/ +[vim-jp]: https://github.com/vim-jp +[vim-denops]: https://github.com/vim-denops diff --git a/src/tutorial.md b/src/tutorial.md index f6a35cc..f4fc703 100644 --- a/src/tutorial.md +++ b/src/tutorial.md @@ -6,10 +6,10 @@ This article is a tutorial on developing Denops plugins. In this tutorial, we use the following software and version as of writing. -- [denops.vim v1.0.0](https://github.com/vim-denops/denops.vim/releases/tag/v1.0.0) - (2021-07-19) -- [denops_std v1.0.0](https://github.com/vim-denops/deno-denops-std/releases/tag/v1.0.0) - (2021-07-19) +- [denops.vim v6.0.0](https://github.com/vim-denops/denops.vim/releases/tag/v6.0.0) + (2024-02-03) +- [denops_std v6.0.0](https://github.com/vim-denops/deno-denops-std/releases/tag/v6.0.0) + (2024-02-03) [vim-jp]: https://vim-jp.org/ [denops.vim]: https://github.com/vim-denops/denops.vim @@ -17,11 +17,11 @@ In this tutorial, we use the following software and version as of writing. ## Glossary -| Term | Meaning | -| ------------------------ | -------------------------------------------------------------------------- | -| vim | Vim or Neovim. | -| vim plugin | Vim plugin or Neovim plugin. | -| [Deno][deno] | A JavaScript and TypeScript runtime. | -| [Denops][denops.vim] | An ecosystem for vim plugins based on Deno runtime. | -| Denops plugin | A vim plugin that works on both Vim and Neovim and is written with Denops. | -| [denops.vim][denops.vim] | The name of the vim plugin to introduce Denops into vim. | +| Term | Meaning | +| ------------- | -------------------------------------------------------------------------- | +| Vim | Vim or Neovim. | +| Vim plugin | Vim plugin or Neovim plugin. | +| [Deno] | A JavaScript and TypeScript runtime. | +| Denops | An ecosystem for vim plugins based on Deno runtime. | +| Denops plugin | A vim plugin that works on both Vim and Neovim and is written with Denops. | +| [denops.vim] | The name of the vim plugin to introduce Denops into vim. | diff --git a/src/tutorial/adding-a-skelton-of-denops-plugin.md b/src/tutorial/adding-a-skelton-of-denops-plugin.md deleted file mode 100644 index 8e9ec5f..0000000 --- a/src/tutorial/adding-a-skelton-of-denops-plugin.md +++ /dev/null @@ -1,26 +0,0 @@ -# Adding a Skelton of Denops Plugin - -Once a Denops plugin is loaded, Denops calls the `main` function exported from -`main.ts` of the plugin code. So initially you can write `main.ts` like: - -```ts:main.ts -import { Denops } from "https://deno.land/x/denops_std@v1.0.0/mod.ts"; - -export async function main(denops: Denops): Promise { - // Plugin program starts from here - console.log("Hello Denops!"); -}; -``` - -An argument `denops` is passed to the `main` function, where `denops` is an -instance of `Denops` class exported from [denops-std][denops-std]. - -Then you restart vim, and you can see a message `[denops] Hello Denops!` on the -vim window. - -![](../img/adding-a-skelton-of-denops-plugin-1.png) - -[denops-std]: https://deno.land/x/denops_std - -If you are too lazy to restart vim, you can simply run -`:call denops#server#restart()` on vim to reload Denops only. diff --git a/src/tutorial/adding-an-api.md b/src/tutorial/adding-an-api.md deleted file mode 100644 index fc7a38f..0000000 --- a/src/tutorial/adding-an-api.md +++ /dev/null @@ -1,43 +0,0 @@ -# Adding an API - -Each Denops plugin registers one or more functions as APIs to Denops. First, try -to write an `echo()` function that returns a given string and register it as an -API. You can rewrite `main.ts` as follows: - -```ts:main.ts -import { Denops } from "https://deno.land/x/denops_std@v1.0.0/mod.ts"; -import { ensureString } from "https://deno.land/x/unknownutil@v0.1.1/mod.ts"; - -export async function main(denops: Denops): Promise { - denops.dispatcher = { - async echo(text: unknown): Promise { - // assure `text` is string type. - ensureString(text); - return await Promise.resolve(text); - }, - }; -}; -``` - -Note that you can register a function that satisfies the following as an API: - -- All of its arguments must be of type `unknown`. -- The type of its return value must be either `Promise` or - `Promise`. - -Thus an `echo` API is registered to the `helloworld` plugin. To call an API, you -can use a vim command of the form `denops#request({plugin}, {func}, {args})`. So -you can use the `echo` API to execute the command below after restarting vim: - -```vim -:echo denops#request('helloworld', 'echo', ["Hello Denops!"]) -``` - -If it goes well, you will see `Hello Denops!`. - -![](../img/adding-an-api-1.png) - -If a non-string argument is passed to the `echo` API, such as -`denops#request('helloworld', 'echo', [123])`, Denops will raise an error: - -![](../img/adding-an-api-2.png) diff --git a/src/tutorial/calling-vimneovim-features.md b/src/tutorial/calling-vimneovim-features.md deleted file mode 100644 index c20e37e..0000000 --- a/src/tutorial/calling-vimneovim-features.md +++ /dev/null @@ -1,42 +0,0 @@ -# Calling Vim/Neovim Features - -If you want to use a vim feature from your Denops plugin, you can call it via -the `denops` instance passed to the plugin's `main` function. You can rewrite -`main.ts` like below to register the `echo` API as a vim command: - -```ts:main.ts -import { Denops } from "https://deno.land/x/denops_std@v1.0.0/mod.ts"; -import { execute } from "https://deno.land/x/denops_std@v1.0.0/helper/mod.ts"; -import { ensureString } from "https://deno.land/x/unknownutil@v0.1.1/mod.ts"; - -export async function main(denops: Denops): Promise { - denops.dispatcher = { - async echo(text: unknown): Promise { - ensureString(text); - return await Promise.resolve(text); - }, - }; - - await execute( - denops, - `command! -nargs=1 HelloWorldEcho echomsg denops#request('${denops.name}', 'echo', [])`, - ); -}; -``` - -The helper function `execute()` receives a multiline string and executes it as a -Vim script, where `denops.name` represents the name of the running plugin -itself. Once vim is restarted, the `HelloWorldEcho` command will be registered. -Then you can run: - -```vim -:HelloWorldEcho Hello Vim! -``` - -If the plugin has been registered successfully, you will see `Hello Vim!` as a -result. - -![](../img/calling-vimneovim-features-1.png) - -If you want to learn more details on `denops` API, you can refer to -[denops-std API document](https://doc.deno.land/https/deno.land/x/denops_std/mod.ts#Denops). diff --git a/src/tutorial/developing-more-applicative-plugin.md b/src/tutorial/developing-more-applicative-plugin.md deleted file mode 100644 index b23be8b..0000000 --- a/src/tutorial/developing-more-applicative-plugin.md +++ /dev/null @@ -1,111 +0,0 @@ -# Developing More Applicative Plugin - -Now you have learned the basics of developing Denpos plugins in the previous -sections. Then it would be best if you tried to create a more functional plugin. - -So let me ask you, out of the blue, have you ever itched to solve mazes while -programming? I never have. In any case, there may be people who love solving -mazes and can't get enough of it. So let's try to develop a Denops plugin that -can generate and display a maze in vim at any time. - -Of course, it would be nice to start by coding a maze generation algorithm. -However, you are now with Deno so that you can use a third-party library -[maze_generator](https://deno.land/x/maze_generator@v0.4.0) for your -convenience. First, you should define a `Maze` command similarly to -`HelloWorldEcho`; `Maze` generates a maze and outputs it with `console.log()`. - -```ts:main.ts -import { Denops } from "https://deno.land/x/denops_std@v1.0.0/mod.ts"; -import { Maze } from "https://deno.land/x/maze_generator@v0.4.0/mod.js"; - -export async function main(denops: Denops): Promise { - denops.dispatcher = { - async maze(): Promise { - const maze = new Maze({}).generate(); - const content = maze.getString(); - console.log(content); - }, - }; - - await denops.cmd(`command! Maze call denops#request('${denops.name}', 'maze', [])`); -}; -``` - -Restarting vim, and you will see a maze by commands: - -```vim -:Maze -:mes -``` - -![](../img/developing-more-applicative-plugin-1.png) - -Well done! But it is a little boring... So let's try to modify the code to make -a generated maze output to a buffer. - -```ts:main.ts -import { Denops } from "https://deno.land/x/denops_std@v1.0.0/mod.ts"; -import { Maze } from "https://deno.land/x/maze_generator@v0.4.0/mod.js"; - -export async function main(denops: Denops): Promise { - denops.dispatcher = { - async maze(): Promise { - const maze = new Maze({}).generate(); - const content = maze.getString(); - await denops.cmd("enew"); - await denops.call("setline", 1, content.split(/\n/)); - }, - }; - - await denops.cmd(`command! Maze call denops#request('${denops.name}', 'maze', [])`); -}; -``` - -In this code, `denops.cmd()` executes the vim command `enew` to open a new -buffer in the current window and then `denops.call()` calls the vim function -`setline()` to write the maze to the buffer. Restart vim, rerun the commands, -and then you can see: - -![](../img/developing-more-applicative-plugin-2.png) - -Awesome! Even if it looks like enough, you can improve your code a bit more. -Here is an example of a modification so that the `Maze` command can receive a -vim command other than `enew`, make a produced maze fit the current display -area, etc.: - -```ts:main.ts -import { Denops } from "https://deno.land/x/denops_std@v1.0.0/mod.ts"; -import { execute } from "https://deno.land/x/denops_std@v1.0.0/helper/mod.ts"; -import { Maze } from "https://deno.land/x/maze_generator@v0.4.0/mod.js"; -import { ensureString } from "https://deno.land/x/unknownutil@v0.1.1/mod.ts"; - -export async function main(denops: Denops): Promise { - denops.dispatcher = { - async maze(opener: unknown): Promise { - ensureString(opener); - const [xSize, ySize] = (await denops.eval("[&columns, &lines]")) as [ - number, - number - ]; - const maze = new Maze({ - xSize: xSize / 3, - ySize: ySize / 3, - }).generate(); - const content = maze.getString(); - await denops.cmd(opener || "new"); - await denops.call("setline", 1, content.split(/\r?\n/g)); - await execute(denops, ` - setlocal bufhidden=wipe buftype=nofile - setlocal nobackup noswapfile - setlocal nomodified nomodifiable - `); - }, - }; - - await denops.cmd(`command! -nargs=? -bar Maze call denops#request('${denops.name}', 'maze', [])`); -}; -``` - -Now you can see a smaller maze shown on the window. - -![](../img/developing-more-applicative-plugin-3.png) diff --git a/src/tutorial/developing-your-first-plugin.md b/src/tutorial/developing-your-first-plugin.md deleted file mode 100644 index cd7057e..0000000 --- a/src/tutorial/developing-your-first-plugin.md +++ /dev/null @@ -1,12 +0,0 @@ -# Developing Your First Plugin - -Now you are ready to write a Denops plugin. It would be better to start by -developing a small plugin. So we will name the plugin `helloworld` and place it -under `~/dps-helloworld`. - -- [Vim/Neovim Configuration](./vimneovim-configuration.md) -- [Making a Plugin Directory Tree](./making-a-directory-tree.md) -- [Adding a Skelton of Denops Plugin](./adding-a-skelton-of-denops-plugin.md) -- [Adding an API](./adding-an-api.md) -- [Calling Vim/Neovim Features](./calling-vimneovim-features.md) -- [Developing More Applicative Plugin](./developing-more-applicative-plugin.md) diff --git a/src/tutorial/developing-your-next-plugins.md b/src/tutorial/developing-your-next-plugins.md deleted file mode 100644 index 1b20b1c..0000000 --- a/src/tutorial/developing-your-next-plugins.md +++ /dev/null @@ -1,14 +0,0 @@ -# Developing Your Next Plugins - -How do you feel about Denops plugin development? I think you could understand -that you can create Vim/Neovim plugins with Denops so easily. Denops is a -fantastic portable ecosystem for Vim/Neovim plugins, though it is going under -development. If you are interested in creating Denops plugins, this tutorial and -the following documents will help you. - -- [denops-std API Document](https://doc.deno.land/https/deno.land/x/denops_std/mod.ts) -- [denops Sample Project](https://github.com/vim-denops/denops-helloworld.vim) -- [denops Developer's Channel (vim-jp - Slack)](https://vim-jp.slack.com/archives/C01N4L5362D) - -We are looking forward to your feedback and contributions to our development. 🙇 diff --git a/src/tutorial/helloworld/README.md b/src/tutorial/helloworld/README.md new file mode 100644 index 0000000..3ad9983 --- /dev/null +++ b/src/tutorial/helloworld/README.md @@ -0,0 +1,11 @@ +# Tutorial: Hello World + +In this chapter, we will create a minimal Denops plugin that greets the user. +Most of the code resembles the previous +[Getting Started](../../getting-started/index.html), so feel free to skip this +chapter if you are already familiar with it. + +> [!NOTE] +> +> The plugin we will create in this section can be found at +> [https://github.com/vim-denops/denops-helloworld.vim](https://github.com/vim-denops/denops-helloworld.vim) diff --git a/src/tutorial/helloworld/adding-an-api.md b/src/tutorial/helloworld/adding-an-api.md new file mode 100644 index 0000000..d0b5695 --- /dev/null +++ b/src/tutorial/helloworld/adding-an-api.md @@ -0,0 +1,44 @@ +# Adding Denops API to the Plugin + +In the previous section, we created a minimal Denops plugin. In this section, we +will enhance the plugin by adding an API. + +Open `denops/denops-helloworld/main.ts` and rewrite the content with the +following code: + +```typescript:denops/denops-helloworld/main.ts +import type { Denops } from "https://deno.land/x/denops_std@v6.0.0/mod.ts"; +import { assert, is } from "https://deno.land/x/unknownutil@v3.14.1/mod.ts"; + +export function main(denops: Denops): void { + denops.dispatcher = { + hello(name) { + assert(name, is.String); + return `Hello, ${name || "Denops"}!`; + }, + }; +} +``` + +The above code adds a new API `hello` to the plugin. The `hello` API takes a +string `name` and returns a greeting message. See +[About User-Defined APIs in Explanation of the Getting started](../../getting-started/explanation.md#about-user-defined-apis) +for details about User-Defined APIs. + +> [!NOTE] +> +> While Vim script does not facilitate types, Denops uses `unknown` types on the +> interface between Vim and Denops. That's why we use +> [unknownutil](https://deno.land/x/unknownutil) to ensure that the `name` is of +> type `string` in the above code. + +Once you've updated the file, restart Vim, and execute the following command, +you will see the message "Hello, Your name!". + +``` +:echo denops#request("denops-helloworld", "hello", ["Your name"]) +``` + +As shown, users can call the Denops API via the `denops#request()` function. + +![](./img/adding-an-api-01.png) diff --git a/src/tutorial/helloworld/calling-vim-features.md b/src/tutorial/helloworld/calling-vim-features.md new file mode 100644 index 0000000..33f50a3 --- /dev/null +++ b/src/tutorial/helloworld/calling-vim-features.md @@ -0,0 +1,64 @@ +# Calling Vim Features from the Plugin + +If you want to use a Vim feature from your Denops plugin, you can call it via +the `denops` instance passed to the plugin's `main` function. You can rewrite +`main.ts` as follows to register the `DenopsHello` as a Vim command: + +```ts:denops/denops-helloworld/main.ts +import { Denops } from "https://deno.land/x/denops_std@v6.0.0/mod.ts"; +import { assert, is } from "https://deno.land/x/unknownutil@v3.14.1/mod.ts"; + +export function main(denops: Denops): void { + denops.dispatcher = { + async init() { + // This is just an example. + // Developers usually should define commands directly in Vim script. + await denops.cmd( + `command! -nargs=? DenopsHello echomsg denops#request('denops-helloworld', 'hello', [])`, + ); + }, + + hello(name) { + assert(name, is.String); + return `Hello, ${name || "Denops"}!`; + }, + }; +} +``` + +Then, rewrite `plugin/denops-helloworld.vim` to automatically call the `init` +API on plugin load via the `DenopsPluginPost:{plugin_name}` autocmd: + +```vim:plugin/denops-helloworld.vim +if exists('g:loaded_denops_helloworld') + finish +endif +let g:loaded_denops_helloworld = 1 + +augroup denops_helloworld + autocmd! + autocmd User DenopsPluginPost:denops-helloworld + \ call denops#notify('denops-helloworld', 'init', []) +augroup END +``` + +Once Vim is restarted, the `DenopsHello` command will be registered. + +Then you can run: + +```vim +:DenopsHello Your name +``` + +If the plugin has been registered successfully, you will see `Hello, Your name!` +as a result. + +![](./img/calling-vim-features-01.png) + +## Next Steps + +In the next step, follow the tutorial to learn how to develop a real Denops +plugin. + +- [Tutorial (Maze)](../tutorial/maze/README.md) +- [API reference](https://deno.land/x/denops_std/mod.ts) diff --git a/src/tutorial/helloworld/creating-a-minimal-denops-plugin.md b/src/tutorial/helloworld/creating-a-minimal-denops-plugin.md new file mode 100644 index 0000000..5b9a758 --- /dev/null +++ b/src/tutorial/helloworld/creating-a-minimal-denops-plugin.md @@ -0,0 +1,53 @@ +# Creating a Minimal Denops Plugin + +When [denops.vim] is installed, it searches for files named `denops/*/main.ts` +in addition to Vim plugins when Vim starts. + +If a corresponding file is found, Denops registers the parent directory name +(`foo` in the case of `denops/foo/main.ts`) as the plugin name. It then imports +the corresponding file as a TypeScript module and calls the function named +`main`. + +> [!NOTE] +> +> Denops plugins typically include both TypeScript and Vim script code, so the +> directory structure looks like an extension of the Vim plugin structure with +> an added `denops` directory. + +[denops.vim]: https://github.com/vim-denops/denops.vim + +Let's add `denops/denops-helloworld/main.ts` to the `denops-helloworld` +directory that we created in the previous section. The directory tree will be as +follows: + +``` +denops-helloworld +├── denops +│ └── denops-helloworld +│ └── main.ts +└── plugin + └── denops-helloworld.vim +``` + +Here is the content of the `denops/denops-helloworld/main.ts` file: + +```typescript:denops/denops-helloworld/main.ts +import type { Denops } from "https://deno.land/x/denops_std@v6.0.0/mod.ts"; + +export function main(denops: Denops): void { + console.log("Hello, Denops from TypeScript!"); +} +``` + +> [!WARNING] +> +> As shown above, developers can use `console.log` (or `console.warn`, +> `console.error`, etc.) for debug output. The content will be echoed to Vim. +> However, it is not recommended to use `console.log` in production code. +> Instead, use `denops.cmd("echo '...'")` or the `echo` function in the `helper` +> module of the `denops_std` library. + +Once you've created the file, restart Vim, and "Hello, Denops from TypeScript!" +will be displayed on Vim startup. + +![](./img/creating-a-minimal-denops-plugin-01.png) diff --git a/src/tutorial/helloworld/creating-a-minimal-vim-plugin.md b/src/tutorial/helloworld/creating-a-minimal-vim-plugin.md new file mode 100644 index 0000000..1cf9d19 --- /dev/null +++ b/src/tutorial/helloworld/creating-a-minimal-vim-plugin.md @@ -0,0 +1,60 @@ +# Creating a Minimal Vim Plugin + +Let's start by crafting a minimal Vim plugin called `denops-helloworld`. + +Create a directory named `denops-helloworld` in your home directory and a Vim +script file as follows: + +``` +denops-helloworld +└── plugin + └── denops-helloworld.vim +``` + +The content of the `plugin/denops-helloworld.vim` file is as follows: + +```vim:plugin/denops-helloworld.vim +if exists('g:loaded_denops_helloworld') + finish +endif +let g:loaded_denops_helloworld = 1 + +command! DenopsHello echo 'Hello, Denops!' +``` + +The initial four lines (as shown below) serve as a guard, preventing the plugin +from being loaded more than once. Using this guard is a common practice to +ensure the plugin is loaded only once. + +```vim +if exists('g:loaded_denops_helloworld') + finish +endif +let g:loaded_denops_helloworld = 1 +``` + +The final line (as displayed below) defines a command named `DenopsHello` that +invokes the `echo 'Hello, Denops!'` command. Thus, when you execute +`:DenopsHello` in Vim, it will display "Hello, Denops!". + +```vim +command! DenopsHello echo 'Hello, Denops!' +``` + +Upon startup, Vim searches and loads files named `plugin/*.vim` in directories +specified in `runtimepath`. To activate the plugin, add the following line to +your Vim configuration file (e.g., `~/.vimrc` or `~/.config/nvim/init.vim`): + +```vim +set runtimepath+=~/denops-helloworld +``` + +For Neovim's Lua configuration file (e.g., `~/.config/nvim/init.lua`), use: + +```lua +vim.opt.runtimepath:append("~/denops-helloworld") +``` + +Restart Vim and execute `:DenopsHello` to witness the message "Hello, Denops!". + +![](./img/creating-a-minimal-vim-plugin-01.png) diff --git a/src/tutorial/helloworld/img/adding-an-api-01.png b/src/tutorial/helloworld/img/adding-an-api-01.png new file mode 100644 index 0000000..392d156 Binary files /dev/null and b/src/tutorial/helloworld/img/adding-an-api-01.png differ diff --git a/src/tutorial/helloworld/img/calling-vim-features-01.png b/src/tutorial/helloworld/img/calling-vim-features-01.png new file mode 100644 index 0000000..21329e6 Binary files /dev/null and b/src/tutorial/helloworld/img/calling-vim-features-01.png differ diff --git a/src/tutorial/helloworld/img/creating-a-minimal-denops-plugin-01.png b/src/tutorial/helloworld/img/creating-a-minimal-denops-plugin-01.png new file mode 100644 index 0000000..4ff214c Binary files /dev/null and b/src/tutorial/helloworld/img/creating-a-minimal-denops-plugin-01.png differ diff --git a/src/tutorial/helloworld/img/creating-a-minimal-vim-plugin-01.png b/src/tutorial/helloworld/img/creating-a-minimal-vim-plugin-01.png new file mode 100644 index 0000000..646ad43 Binary files /dev/null and b/src/tutorial/helloworld/img/creating-a-minimal-vim-plugin-01.png differ diff --git a/src/tutorial/making-a-directory-tree.md b/src/tutorial/making-a-directory-tree.md deleted file mode 100644 index d82f0ba..0000000 --- a/src/tutorial/making-a-directory-tree.md +++ /dev/null @@ -1,29 +0,0 @@ -# Making a Plugin Directory Tree - -Next, you have to make a directory `~/dps-helloworld` to store plugin codes and -change the current working directory to it. If you use Windows, you should find -and use equivalent commands. - -```sh -mkdir ~/dps-helloworld -cd ~/dps-helloworld -``` - -Then make a minimum directory tree and a code file required by Denops at least: - -```sh -mkdir -p denops/helloworld -touch denops/helloworld/main.ts -``` - -Finally, you will get a directory tree like: - -``` -dps-helloworld -└── denops - └── helloworld - └── main.ts -``` - -This directory tree is a basis for developing a Denops plugin; Denops loads -`denops/*/main.ts` on `runtimepath` automatically after your vim starts up. diff --git a/src/tutorial/maze/README.md b/src/tutorial/maze/README.md new file mode 100644 index 0000000..bec0a89 --- /dev/null +++ b/src/tutorial/maze/README.md @@ -0,0 +1,15 @@ +# Tutorial (Maze) + +Now that you have grasped the basics of developing Denops plugins in the +previous chapters, it's time to delve into creating a more functional plugin. + +So, out of the blue, have you ever felt the urge to solve mazes while +programming? Personally, I haven't, but there might be enthusiasts who enjoy it +immensely. In any case, let's embark on developing a Denops plugin that can +generate and display a maze in Vim at any time. Well, don't ask me why we would +want to do that. + +> [!NOTE] +> +> The plugin we will create in this chapter can be found at +> [https://github.com/vim-denops/denops-maze.vim](https://github.com/vim-denops/denops-maze.vim) diff --git a/src/tutorial/maze/adjusting-maze-size-to-fit-the-window.md b/src/tutorial/maze/adjusting-maze-size-to-fit-the-window.md new file mode 100644 index 0000000..0f34a95 --- /dev/null +++ b/src/tutorial/maze/adjusting-maze-size-to-fit-the-window.md @@ -0,0 +1,58 @@ +# Adjusting Maze Size to Fit the Window + +In the previous section, we outputted the maze to a buffer. However, the maze +size can sometimes be too large or too small for the window. It would be better +to have a maze that fits the current window size. + +Let's modify the plugin to ensure the generated maze fits the current window +size. + +```typescript:denops/denops-helloworld/main.ts +import type { Denops } from "https://deno.land/x/denops_std@v6.0.0/mod.ts"; +import * as fn from "https://deno.land/x/denops_std@v6.0.0/function/mod.ts"; +import { Maze } from "https://deno.land/x/maze_generator@v0.4.0/mod.js"; + +export function main(denops: Denops): void { + denops.dispatcher = { + async maze() { + await denops.cmd("enew"); + + const winWidth = await fn.winwidth(denops, 0); + const winHeight = await fn.winheight(denops, 0); + const maze = new Maze({ + xSize: winWidth / 3, + ySize: winHeight / 3, + }).generate(); + const content = maze.getString(); + + await fn.setline(denops, 1, content.split(/\r?\n/g)); + }, + }; +} +``` + +In this code, we utilize the `function` module (aliased to `fn`) of `denops_std` +(Denops Standard Library) to call `winwidth()`, `winheight()`, and `setline()` +functions. Then, we create a maze that fits the current window size and write it +to the buffer. + +So why do we use the `function` module instead of `denops.call`? With +`denops.call`, developers must know the function name, arguments, return type, +and manually cast the return value to the expected type (like `as string`). +However, with the `function` module, developers can use auto-completion, type +checking, etc. It is more convenient and safe to use the `function` module. + +> [!TIP] +> +> The `function` module of the `denops_std` library provides a set of functions +> that are available on both Vim and Neovim. If you'd like to use Vim or Neovim +> only functions, use the `vim` or `nvim` module under the `function` module +> instead. +> +> See the +> [function module of denops_std API document](https://doc.deno.land/https/deno.land/x/denops_std/function/mod.ts) +> for more details. + +Restart Vim, rerun the `:Maze` command, and then you can see: + +![](./img/fitting-maze-to-the-window-01.png) diff --git a/src/tutorial/maze/creating-applicative-plugin.md b/src/tutorial/maze/creating-applicative-plugin.md new file mode 100644 index 0000000..850eddd --- /dev/null +++ b/src/tutorial/maze/creating-applicative-plugin.md @@ -0,0 +1,85 @@ +# How to create an applicative plugin + +In the previous section, we created a plugin that outputs a maze to a buffer. +However, who wants to see a maze in a buffer that is too small or too large for +it? It would be better to see a maze that fits the current window size. + +In this section, we will modify the plugin to make the generated maze fit the +current window size. So, let's create a production ready enterprise edition of +the maze plugin that will satisfy your crazy addictive maze solver friend. + +First, modify `plugin/denops-maze.vim` to accept the `Maze` command with an +optional argument. + +```vim:plugin/denops-maze.vim +if exists('g:loaded_denops_maze') + finish +endif +let g:loaded_denops_maze = 1 + +" Function called once the plugin is loaded +function! s:init() abort + command! -nargs=? Maze call denops#request('denops-maze', 'maze', []) +endfunction + +augroup denops_maze + autocmd! + autocmd User DenopsPluginPost:denops-maze call s:init() +augroup END +``` + +Then, modify the `main.ts` file to accept the optional argument for a custom +opener, generate a maze that fits the current window size, configure the buffer +options to make it non-file readonly buffer, etc. + +```ts:denops/denops-maze/main.ts +import type { Denops } from "https://deno.land/x/denops_std@v6.0.0/mod.ts"; +import { batch, collect } from "https://deno.land/x/denops_std@v6.0.0/batch/mod.ts"; +import * as fn from "https://deno.land/x/denops_std@v6.0.0/function/mod.ts"; +import * as op from "https://deno.land/x/denops_std@v6.0.0/option/mod.ts"; +import { Maze } from "https://deno.land/x/maze_generator@v0.4.0/mod.js"; +import { assert, is } from "https://deno.land/x/unknownutil@v3.14.1/mod.ts"; + +export function main(denops: Denops): void { + denops.dispatcher = { + async maze(opener) { + assert(opener, is.String); + const [xSize, ySize] = await collect(denops, (denops) => [ + op.columns.get(denops), + op.lines.get(denops), + ]); + const maze = new Maze({ + xSize: xSize / 3, + ySize: ySize / 3, + }).generate(); + const content = maze.getString(); + await batch(denops, (denops) => { + await denops.cmd(opener || "new"); + await op.modifiable.setLocal(denops, true); + await fn.setline(denops, 1, content.split(/\r?\n/g)); + await op.bufhidden.setLocal(denops, "wipe"); + await op.buftype.setLocal(denops, "nofile"); + await op.swapfile.setLocal(denops, false); + await op.modifiable.setLocal(denops, false); + await op.modified.setLocal(denops, false); + }); + }, + }; +}; +``` + +In above code, we utilize the following denops_std modules: + +- `batch` and `collect` functions in a `batch` module to execute multiple + commands in a single RPC +- `function` module to call Vim's functions +- `option` module to get and set Vim's options + +See the +[denops_std API document](https://doc.deno.land/https/deno.land/x/denops_std/mod.ts) +for more details about each modules. + +That's it. Now you can see a smaller maze shown on the window with `:Maze` +command. + +![](../img/developing-more-applicative-plugin-3.png) diff --git a/src/tutorial/maze/img/fitting-maze-to-the-window-01.png b/src/tutorial/maze/img/fitting-maze-to-the-window-01.png new file mode 100644 index 0000000..e0fc5cc Binary files /dev/null and b/src/tutorial/maze/img/fitting-maze-to-the-window-01.png differ diff --git a/src/tutorial/maze/img/outputting-content-to-buffer-01.png b/src/tutorial/maze/img/outputting-content-to-buffer-01.png new file mode 100644 index 0000000..6cb0279 Binary files /dev/null and b/src/tutorial/maze/img/outputting-content-to-buffer-01.png differ diff --git a/src/tutorial/maze/img/properly-create-a-virtual-buffer-01.png b/src/tutorial/maze/img/properly-create-a-virtual-buffer-01.png new file mode 100644 index 0000000..2751211 Binary files /dev/null and b/src/tutorial/maze/img/properly-create-a-virtual-buffer-01.png differ diff --git a/src/img/developing-more-applicative-plugin-1.png b/src/tutorial/maze/img/utilizing-third-party-library-01.png similarity index 100% rename from src/img/developing-more-applicative-plugin-1.png rename to src/tutorial/maze/img/utilizing-third-party-library-01.png diff --git a/src/tutorial/maze/outputting-content-to-buffer.md b/src/tutorial/maze/outputting-content-to-buffer.md new file mode 100644 index 0000000..a10ac14 --- /dev/null +++ b/src/tutorial/maze/outputting-content-to-buffer.md @@ -0,0 +1,31 @@ +# Outputting Content to a Buffer + +In the previous section, we echoed the maze to the echo area. However, echoing +the maze to the echo area is not very practical. In this section, we will output +the maze to a buffer so that users can yank the maze with daily Vim operations! + +Let's modify the code to make the generated maze output to a buffer. + +```ts:denops/denops-maze/main.ts +import type { Denops } from "https://deno.land/x/denops_std@v6.0.0/mod.ts"; +import { Maze } from "https://deno.land/x/maze_generator@v0.4.0/mod.js"; + +export function main(denops: Denops): void { + denops.dispatcher = { + async maze() { + const maze = new Maze({}).generate(); + const content = maze.getString(); + await denops.cmd("enew"); + await denops.call("setline", 1, content.split(/\r?\n/g)); + }, + }; +} +``` + +In this code, `denops.cmd` executes the Vim command `enew` to open a new buffer +in the current window. Then, `denops.call` calls the Vim function `setline()` to +write the maze to the buffer. + +Restart Vim, rerun the `Maze` command, and then you can see: + +![Outputting Content to a Buffer](./img/outputting-content-to-buffer-01.png) diff --git a/src/tutorial/maze/properly-configured-the-buffer.md b/src/tutorial/maze/properly-configured-the-buffer.md new file mode 100644 index 0000000..9b2758e --- /dev/null +++ b/src/tutorial/maze/properly-configured-the-buffer.md @@ -0,0 +1,47 @@ +# Properly Configure the Buffer + +In the previous section, we didn't configure the buffer options, so the buffer +remains modifiable and persists after being closed. In this section, we will +configure the buffer options to make the buffer non-modifiable and remove the +buffer after closure. Open the `main.ts` file and modify the `maze` method as +follows: + +```typescript:denops/denops-maze/main.ts +import type { Denops } from "https://deno.land/x/denops_std@v6.0.0/mod.ts"; +import * as buffer from "https://deno.land/x/denops_std@v6.0.0/buffer/mod.ts"; +import * as fn from "https://deno.land/x/denops_std@v6.0.0/function/mod.ts"; +import * as op from "https://deno.land/x/denops_std@v6.0.0/option/mod.ts"; +import { Maze } from "https://deno.land/x/maze_generator@v0.4.0/mod.js"; + +export function main(denops: Denops): void { + denops.dispatcher = { + async maze() { + const { bufnr, winnr } = await buffer.open(denops, "maze://"); + + const winWidth = await fn.winwidth(denops, winnr); + const winHeight = await fn.winheight(denops, winnr); + const maze = new Maze({ + xSize: winWidth / 3, + ySize: winHeight / 3, + }).generate(); + const content = maze.getString(); + + await buffer.replace(denops, bufnr, content.split(/\r?\n/g)); + await buffer.concrete(denops, bufnr); + + await op.bufhidden.setLocal(denops, "wipe"); + await op.modifiable.setLocal(denops, false); + }, + }; +} +``` + +In this code, we use `op.bufhidden.setLocal` to set the `bufhidden` option to +`wipe` so that the buffer is wiped out when it is closed. Additionally, we use +`op.modifiable.setLocal` to set the `modifiable` option to `false` to make the +buffer non-modifiable. Note that since we use `buffer.replace` to replace the +content of the buffer, there is no need to explicitly set the `modifiable` +option to `true` before replacing the content. + +Restart Vim, rerun the `:Maze` command, and confirm that the buffer is not +modifiable. diff --git a/src/tutorial/maze/properly-create-a-virtual-buffer.md b/src/tutorial/maze/properly-create-a-virtual-buffer.md new file mode 100644 index 0000000..eeb916f --- /dev/null +++ b/src/tutorial/maze/properly-create-a-virtual-buffer.md @@ -0,0 +1,72 @@ +# Properly Create a Virtual Buffer + +Now that the maze is displayed in a buffer, but it is not properly configured. +For example, if a user executes the `:edit` command on the buffer, the maze will +disappear. This is because Vim does not know how to reload the buffer content, +and we must inform Vim about the content of the buffer when it is reloaded. + +In this section, we will use the `buffer` module of `denops_std` to create a +proper virtual buffer that concretizes the buffer content. Let's modify the +`main.ts` file as follows: + +```typescript:denops/denops-maze/main.ts +import type { Denops } from "https://deno.land/x/denops_std@v6.0.0/mod.ts"; +import * as buffer from "https://deno.land/x/denops_std@v6.0.0/buffer/mod.ts"; +import * as fn from "https://deno.land/x/denops_std@v6.0.0/function/mod.ts"; +import { Maze } from "https://deno.land/x/maze_generator@v0.4.0/mod.js"; + +export function main(denops: Denops): void { + denops.dispatcher = { + async maze() { + const { bufnr, winnr } = await buffer.open(denops, "maze://"); + + const winWidth = await fn.winwidth(denops, winnr); + const winHeight = await fn.winheight(denops, winnr); + const maze = new Maze({ + xSize: winWidth / 3, + ySize: winHeight / 3, + }).generate(); + const content = maze.getString(); + + await buffer.replace(denops, bufnr, content.split(/\r?\n/g)); + await buffer.concrete(denops, bufnr); + }, + }; +} +``` + +In this code, we use `buffer.open` to open a `maze://` buffer and get the buffer +number (`bufnr`) and the window number (`winnr`). Because Denops works +asynchronously, **the current buffer or window may be changed from what we +expected**. That's why developers should use `buffer.open` to open a buffer and +save the buffer number and the window number for further operations. + +Then, we call `fn.winwidth` and `fn.winheight` with the obtained window number +to get the window size. Again, the current window might be changed, so we should +use `winnr` to specify the window. + +> [!NOTE] +> +> Vim may execute some events between RPC calls, so the current buffer or window +> really may be changed from what we expected. Denops plugin developers should +> be careful about this. The best practice for avoiding this problem is to avoid +> using _current_ and always specify the buffer number or window number. + +After that, we use `buffer.replace` to replace the content of the buffer. +Actually, replacing the buffer content is a bit tricky. Developers should care +about `modifiable` options to avoid unmodifiable errors, `foldmethod` options to +keep foldings, and should remove the buffer content that is not replaced by +`setline` or `setbufline`, etc. The `buffer.replace` function will care about +all of those, so developers should avoid using `setline` or `setbufline` +directly. + +At the end, we call `buffer.concrete` to concretize the buffer content. This +function defines `BufReadCmd` autocmd to restore the content when the buffer is +reloaded. Without this, the buffer content will be discarded when the user +executes the `:edit` command. + +Restart Vim, rerun the `:Maze` command, and then you can see: + +![](./img/properly-create-a-virtual-buffer-01.png) + +Try the `:edit` command on the buffer, and you can see the maze is still there. diff --git a/src/tutorial/maze/reduce-the-number-of-rpc-calls.md b/src/tutorial/maze/reduce-the-number-of-rpc-calls.md new file mode 100644 index 0000000..737bcbd --- /dev/null +++ b/src/tutorial/maze/reduce-the-number-of-rpc-calls.md @@ -0,0 +1,103 @@ +# Reducing the Number of RPC Calls + +As Denops employs RPC to interact with Vim, the volume of RPC calls +significantly influences the plugin's performance. In this section, we aim to +enhance performance by reducing the number of RPC calls using the `batch` module +from `denops_std`. Let's revise the `main.ts` file as follows: + +```typescript:denops/denops-maze/main.ts +import type { Denops } from "https://deno.land/x/denops_std@v6.0.0/mod.ts"; +import { batch, collect } from "https://deno.land/x/denops_std@v6.0.0/batch/mod.ts"; +import * as buffer from "https://deno.land/x/denops_std@v6.0.0/buffer/mod.ts"; +import * as fn from "https://deno.land/x/denops_std@v6.0.0/function/mod.ts"; +import * as op from "https://deno.land/x/denops_std@v6.0.0/option/mod.ts"; +import { Maze } from "https://deno.land/x/maze_generator@v0.4.0/mod.js"; + +export function main(denops: Denops): void { + denops.dispatcher = { + async maze() { + const { bufnr, winnr } = await buffer.open(denops, "maze://"); + + const [winWidth, winHeight] = await collect(denops, (denops) => [ + fn.winwidth(denops, winnr), + fn.winheight(denops, winnr), + ]); + const maze = new Maze({ + xSize: winWidth / 3, + ySize: winHeight / 3, + }).generate(); + const content = maze.getString(); + + await batch(denops, async (denops) => { + await buffer.replace(denops, bufnr, content.split(/\r?\n/g)); + await buffer.concrete(denops, bufnr); + await op.bufhidden.setLocal(denops, "wipe"); + await op.modifiable.setLocal(denops, false); + }); + }, + }; +} +``` + +In this code, we use the `collect` function to gather window size values and the +`batch` function to execute multiple commands in a single RPC. This optimization +significantly reduces the number of RPC calls, thereby improving the plugin's +performance. + +The `collect` function is designed for collecting multiple values in a single +RPC, offering the following features: + +- Execution of `denops.call` or `denops.eval` within the `collect` is delayed + and executed in a single RPC with the results. +- The result of `denops.call` or `denops.eval` in the `collect` is always falsy, + indicating that branching (if, switch, etc.) is not allowed. +- Execution of `denops.redraw` or `denops.cmd` in the `collect` is not allowed. +- Execution of `batch` or `collect` in the `collect` is not allowed, indicating + that nesting is not allowed. + +In short, only the following operations are allowed in the `collect`: + +- `denops.call` or `denops.eval` that returns a value. +- Functions in the `function` module that return a value. +- Functions in the `option` module that return a value. +- Functions in the `variable` module that return a value. + +The `batch` function is designed for executing multiple commands in a single +RPC, offering the following features: + +- Execution of `denops.call`, `denops.cmd`, or `denops.eval` in the `batch` is + delayed and executed in a single RPC without the results. +- The result of `denops.call` or `denops.eval` in the `batch` is always falsy, + indicating that branching (if, switch, etc.) is not allowed. +- Execution of `denops.redraw` is accumulated and only executed once at the end + of the `batch`. +- Execution of `batch` in the `batch` is allowed, indicating that nesting is + allowed. +- Execution of `collect` in the `batch` is not allowed, indicating that nesting + is not allowed. + +In short, only the following operations are allowed in the `batch`: + +- `denops.call`, `denops.cmd`, or `denops.eval` (without the results). +- Functions in the `function` module (without the results). +- Functions in the `option` module (without the results). +- Functions in the `variable` module (without the results). +- Functions in other modules that do not call `collect` internally. + +In the previous code, the number of RPC calls was more than 7, but after using +`batch` and `collect`, the number of RPC calls is reduced to 3. Although this is +a small plugin, the performance improvement may not be noticeable. However, in a +larger plugin, the performance improvement will be significant. + +Restart Vim, rerun the `:Maze` command, and confirm that the plugin works +properly with `batch` and `collect`. + +## Next Steps + +In the next step, read API references or real-world plugins + +- [API reference](https://deno.land/x/denops_std/mod.ts) +- [lambdalisue/gin.vim](https://github.com/lambdalisue/gin.vim) +- [vim-skk/skkeleton](https://github.com/vim-skk/skkeleton) +- [Shougo/ddu.vim](https://github.com/Shougo/ddu.vim) +- [Find one from the `vim-denops` topic](https://github.com/topics/vim-denops) diff --git a/src/tutorial/maze/utilizing-denops-std-library.md b/src/tutorial/maze/utilizing-denops-std-library.md new file mode 100644 index 0000000..c7f2be7 --- /dev/null +++ b/src/tutorial/maze/utilizing-denops-std-library.md @@ -0,0 +1,90 @@ +# Utilizing Denops Standard library + +In the previous section, we created a plugin that outputs a maze to a buffer. +However, the plugin has the following problems: + +- The maze does not fit the window size. The maze may be too large for it +- The buffer is modifiable so that the content may accidentally be modified +- When user execute `:edit` command, the maze is disappeared + +Additionally, we used `denops.cmd` or `denops.call` to execute Vim commands and +functions directly. However, it was a bit painful to use those because we must +know the definitions of the commands and functions. No auto-completion, no type +checking, etc. + +In this section, we introduce Denops standard library (`denops_std`) to solves +all above problems. The plugin will output the maze to non-file like buffer that +is non-modifiable and the content is remained even if user execute `:edit` +command. + +First, modify the `denops/denops-helloworld/main.ts` file as follows: + +```typescript:denops/denops-helloworld/main.ts +import type { Denops } from "https://deno.land/x/denops_std@v6.0.0/mod.ts"; +import * as buffer from "https://deno.land/x/denops_std@v6.0.0/buffer/mod.ts"; +import * as fn from "https://deno.land/x/denops_std@v6.0.0/function/mod.ts"; +import * as op from "https://deno.land/x/denops_std@v6.0.0/option/mod.ts"; +import { Maze } from "https://deno.land/x/maze_generator@v0.4.0/mod.js"; + +export function main(denops: Denops): void { + denops.dispatcher = { + async maze() { + // Get the current window size + const winWidth = await fn.winwidth(denops, 0); + const winHeight = await fn.winheight(denops, 0); + + // Create a maze that fits the current window size + const maze = new Maze({ + xSize: winWidth / 3, + ySize: winHeight / 3, + }).generate(); + const content = maze.getString(); + + // Open a 'maze://' buffer with specified opener + const { bufnr } = await buffer.open(denops, "maze://"); + + // Replace the buffer content with the maze + await buffer.replace(denops, bufnr, content.split(/\r?\n/g)); + + // Concrete (fix) the buffer content + await buffer.concrete(denops, bufnr); + + // Set the buffer options + await op.bufhidden.setLocal(denops, "wipe"); + await op.modifiable.setLocal(denops, false); + }, + }; +} +``` + +Let's break down this code step by step. + +### Get the current window size + +```typescript +// Get the current window size +const winWidth = await fn.winwidth(denops, 0); +const winHeight = await fn.winheight(denops, 0); +``` + +This code call Vim's `winwidth` and `winheight` functions to get the current +window size. While `function` module (alised to `fn`) of `denops_std` provides a +set of functions that are available on both Vim and Neovim, LSP can provide +auto-completion and type checking for the functions. + +> [!NOTE] +> +> The `function` module of the `denops_std` library provides a set of functions +> that are available on both Vim and Neovim. If you'd like to use Vim or Neovim +> only functions, use the `vim` or `nvim` module under the `function` module +> instead. +> +> See the +> [function module of denops_std API document](https://doc.deno.land/https/deno.land/x/denops_std/function/mod.ts) +> for more details. + +### Open a 'maze://' buffer with specified opener + +```typescript +const { bufnr } = await buffer.open(denops, "maze://"); +``` diff --git a/src/tutorial/maze/utilizing-third-party-library.md b/src/tutorial/maze/utilizing-third-party-library.md new file mode 100644 index 0000000..e662614 --- /dev/null +++ b/src/tutorial/maze/utilizing-third-party-library.md @@ -0,0 +1,101 @@ +# Utilizing Third-Party Library + +Certainly, starting with coding a maze generation algorithm would be nice. +However, since you're now using Deno, you can conveniently employ a third-party +library called [maze_generator](https://deno.land/x/maze_generator@v0.4.0). +Let's define a `Maze` command similar to `DenopsHello`; `Maze` generates a maze +and outputs it. + +> [!NOTE] +> +> The `maze_generator` library is a third-party library that generates a maze. +> It is not a part of Deno or Denops. You can use any third-party library that +> is compatible with Deno in your Denops plugin. Thanks to Deno, developers and +> users don't need to worry about the installation of third-party libraries. +> Deno automatically downloads and caches the library when it is imported. + +Create the `denops-maze` plugin and place it under `~/denops-maze`. The +directory tree will look like this: + +``` +~/denops-maze +├── denops +│ └── denops-maze +│ └── main.ts +└── plugin + └── denops-maze.vim +``` + +The content of the `denops/denops-maze/main.ts` file will be: + +```typescript:denops/denops-maze/main.ts +import type { Denops } from "https://deno.land/x/denops_std@v6.0.0/mod.ts"; +import { Maze } from "https://deno.land/x/maze_generator@v0.4.0/mod.js"; + +export function main(denops: Denops): void { + denops.dispatcher = { + maze() { + const maze = new Maze({}).generate(); + const content = maze.getString(); + console.log(content); + }, + }; +} +``` + +The content of the `plugin/denops-maze.vim` file will be: + +```vim:plugin/denops-maze.vim +if exists('g:loaded_denops_maze') + finish +endif +let g:loaded_denops_maze = 1 + +" Function called once the plugin is loaded +function! s:init() abort + command! Maze call denops#request('denops-maze', 'maze', []) +endfunction + +augroup denops_maze + autocmd! + autocmd User DenopsPluginPost:denops-maze call s:init() +augroup END +``` + +> [!TIP] +> +> The `Maze` command is defined once the plugin is loaded with the above code. +> If you wish to define the command immediately after Vim startup, you can +> define the command and use `denops#plugin#wait()` or +> `denops#plugin#wait_async()` in the function to wait for plugin load, like +> this: +> +> ```vim +> if exists('g:loaded_denops_maze') +> finish +> endif +> let g:loaded_denops_maze = 1 +> +> function! s:maze() abort +> if denops#plugin#wait('denops-maze') +> " Something went wrong +> return +> endif +> call denops#request('denops-maze', 'maze', []) +> endfunction +> +> command! Maze call s:maze() +> ``` + +Don't forget to activate the plugin by adding the following line to your +`vimrc`: + +```vim +set runtimepath+=~/denops-maze +``` + +Then, restart Vim and execute `:Maze` to see the generated maze. Note that it +may take a few seconds for the first startup because Deno will download the +dependencies, but it happens only once. + +![Utilizing Third-Party Library](./img/utilizing-third-party-library-01.png) diff --git a/src/tutorial/preparing-deno-and-denops.md b/src/tutorial/preparing-deno-and-denops.md deleted file mode 100644 index 55076ad..0000000 --- a/src/tutorial/preparing-deno-and-denops.md +++ /dev/null @@ -1,43 +0,0 @@ -# Preparing Deno and Denops - -First of all, whichever you want to either use or develop Denops plugins, you -have to install tools; [Deno][Deno] and [Denops][denops.vim] in addition to your -vim. - -[denops.vim]: https://github.com/vim-denops/denops.vim -[deno]: https://deno.land/ - -## Installing Deno - -Deno can be installed to follow the instructions in the -[Deno document](https://deno.land/#installation). In addition, you can check if -Deno has been installed successfully by -[the command](https://deno.land/#getting-started): - -```sh -deno run https://deno.land/std/examples/welcome.ts -``` - -If you have already installed Deno, upgrade it to the latest version. - -```sh -deno upgrade -``` - -## Installing Denops - -It is necessary for using Denops to install as a vim plugin -[denops.vim][denops.vim]. For example, when you use [vim-plug][vim-plug] as a -vim plugin manager, add the following command to your `.vimrc` and execute -`:PlugInstall` on vim to install Denops. - -```vim -Plug 'vim-denops/denops.vim' -``` - -[vim-plug]: https://github.com/junegunn/vim-plug - -If you prefer another vim plugin manager, you can find instructions for it on -the [Install](../install.md) page. - -Thus Deno and Denops are available in your environment. diff --git a/src/tutorial/vimneovim-configuration.md b/src/tutorial/vimneovim-configuration.md deleted file mode 100644 index 45b2061..0000000 --- a/src/tutorial/vimneovim-configuration.md +++ /dev/null @@ -1,19 +0,0 @@ -# Vim/Neovim Configuration - -Vim plugins have to be located under a path in `runtimepath` on your vim -configuration. Denops plugins also have to be placed in `runtimepath` because -they are also vim plugins. To add the plugin path to your `.vimrc`, you write: - -```vim -set runtimepath^=~/dps-helloworld -``` - -The other setting to add to your `.vimrc` is to make Denops launch in debug mode -to enable type checkings at startup of Deno: - -```vim -let g:denops#debug = 1 -``` - -Note that running Denops in debug mode has a performance problem. Once your -development goes well, it would be better for you to disable the debug mode. diff --git a/theme/highlight.js b/theme/highlight.js new file mode 100644 index 0000000..8c711e9 --- /dev/null +++ b/theme/highlight.js @@ -0,0 +1,3902 @@ +/*! + Highlight.js v11.9.0 (git: b7ec4bfafc) + (c) 2006-2023 undefined and other contributors + License: BSD-3-Clause + */ +var hljs = (function () { + "use strict"; + + /* eslint-disable no-multi-assign */ + + function deepFreeze(obj) { + if (obj instanceof Map) { + obj.clear = + obj.delete = + obj.set = + function () { + throw new Error("map is read-only"); + }; + } else if (obj instanceof Set) { + obj.add = + obj.clear = + obj.delete = + function () { + throw new Error("set is read-only"); + }; + } + + // Freeze self + Object.freeze(obj); + + Object.getOwnPropertyNames(obj).forEach((name) => { + const prop = obj[name]; + const type = typeof prop; + + // Freeze prop if it is an object or function and also not already frozen + if ( + (type === "object" || type === "function") && !Object.isFrozen(prop) + ) { + deepFreeze(prop); + } + }); + + return obj; + } + + /** @typedef {import('highlight.js').CallbackResponse} CallbackResponse */ + /** @typedef {import('highlight.js').CompiledMode} CompiledMode */ + /** @implements CallbackResponse */ + + class Response { + /** + * @param {CompiledMode} mode + */ + constructor(mode) { + // eslint-disable-next-line no-undefined + if (mode.data === undefined) mode.data = {}; + + this.data = mode.data; + this.isMatchIgnored = false; + } + + ignoreMatch() { + this.isMatchIgnored = true; + } + } + + /** + * @param {string} value + * @returns {string} + */ + function escapeHTML(value) { + return value + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + } + + /** + * performs a shallow merge of multiple objects into one + * + * @template T + * @param {T} original + * @param {Record[]} objects + * @returns {T} a single new object + */ + function inherit$1(original, ...objects) { + /** @type Record */ + const result = Object.create(null); + + for (const key in original) { + result[key] = original[key]; + } + objects.forEach(function (obj) { + for (const key in obj) { + result[key] = obj[key]; + } + }); + return /** @type {T} */ (result); + } + + /** + * @typedef {object} Renderer + * @property {(text: string) => void} addText + * @property {(node: Node) => void} openNode + * @property {(node: Node) => void} closeNode + * @property {() => string} value + */ + + /** @typedef {{scope?: string, language?: string, sublanguage?: boolean}} Node */ + /** @typedef {{walk: (r: Renderer) => void}} Tree */ + /** */ + + const SPAN_CLOSE = ""; + + /** + * Determines if a node needs to be wrapped in + * + * @param {Node} node */ + const emitsWrappingTags = (node) => { + // rarely we can have a sublanguage where language is undefined + // TODO: track down why + return !!node.scope; + }; + + /** + * @param {string} name + * @param {{prefix:string}} options + */ + const scopeToCSSClass = (name, { prefix }) => { + // sub-language + if (name.startsWith("language:")) { + return name.replace("language:", "language-"); + } + // tiered scope: comment.line + if (name.includes(".")) { + const pieces = name.split("."); + return [ + `${prefix}${pieces.shift()}`, + ...(pieces.map((x, i) => `${x}${"_".repeat(i + 1)}`)), + ].join(" "); + } + // simple scope + return `${prefix}${name}`; + }; + + /** @type {Renderer} */ + class HTMLRenderer { + /** + * Creates a new HTMLRenderer + * + * @param {Tree} parseTree - the parse tree (must support `walk` API) + * @param {{classPrefix: string}} options + */ + constructor(parseTree, options) { + this.buffer = ""; + this.classPrefix = options.classPrefix; + parseTree.walk(this); + } + + /** + * Adds texts to the output stream + * + * @param {string} text */ + addText(text) { + this.buffer += escapeHTML(text); + } + + /** + * Adds a node open to the output stream (if needed) + * + * @param {Node} node */ + openNode(node) { + if (!emitsWrappingTags(node)) return; + + const className = scopeToCSSClass(node.scope, { + prefix: this.classPrefix, + }); + this.span(className); + } + + /** + * Adds a node close to the output stream (if needed) + * + * @param {Node} node */ + closeNode(node) { + if (!emitsWrappingTags(node)) return; + + this.buffer += SPAN_CLOSE; + } + + /** + * returns the accumulated buffer + */ + value() { + return this.buffer; + } + + // helpers + + /** + * Builds a span element + * + * @param {string} className */ + span(className) { + this.buffer += ``; + } + } + + /** @typedef {{scope?: string, language?: string, children: Node[]} | string} Node */ + /** @typedef {{scope?: string, language?: string, children: Node[]} } DataNode */ + /** @typedef {import('highlight.js').Emitter} Emitter */ + /** */ + + /** @returns {DataNode} */ + const newNode = (opts = {}) => { + /** @type DataNode */ + const result = { children: [] }; + Object.assign(result, opts); + return result; + }; + + class TokenTree { + constructor() { + /** @type DataNode */ + this.rootNode = newNode(); + this.stack = [this.rootNode]; + } + + get top() { + return this.stack[this.stack.length - 1]; + } + + get root() { + return this.rootNode; + } + + /** @param {Node} node */ + add(node) { + this.top.children.push(node); + } + + /** @param {string} scope */ + openNode(scope) { + /** @type Node */ + const node = newNode({ scope }); + this.add(node); + this.stack.push(node); + } + + closeNode() { + if (this.stack.length > 1) { + return this.stack.pop(); + } + // eslint-disable-next-line no-undefined + return undefined; + } + + closeAllNodes() { + while (this.closeNode()); + } + + toJSON() { + return JSON.stringify(this.rootNode, null, 4); + } + + /** + * @typedef { import("./html_renderer").Renderer } Renderer + * @param {Renderer} builder + */ + walk(builder) { + // this does not + return this.constructor._walk(builder, this.rootNode); + // this works + // return TokenTree._walk(builder, this.rootNode); + } + + /** + * @param {Renderer} builder + * @param {Node} node + */ + static _walk(builder, node) { + if (typeof node === "string") { + builder.addText(node); + } else if (node.children) { + builder.openNode(node); + node.children.forEach((child) => this._walk(builder, child)); + builder.closeNode(node); + } + return builder; + } + + /** + * @param {Node} node + */ + static _collapse(node) { + if (typeof node === "string") return; + if (!node.children) return; + + if (node.children.every((el) => typeof el === "string")) { + // node.text = node.children.join(""); + // delete node.children; + node.children = [node.children.join("")]; + } else { + node.children.forEach((child) => { + TokenTree._collapse(child); + }); + } + } + } + + /** + Currently this is all private API, but this is the minimal API necessary + that an Emitter must implement to fully support the parser. + + Minimal interface: + + - addText(text) + - __addSublanguage(emitter, subLanguageName) + - startScope(scope) + - endScope() + - finalize() + - toHTML() + + */ + + /** + * @implements {Emitter} + */ + class TokenTreeEmitter extends TokenTree { + /** + * @param {*} options + */ + constructor(options) { + super(); + this.options = options; + } + + /** + * @param {string} text + */ + addText(text) { + if (text === "") return; + + this.add(text); + } + + /** @param {string} scope */ + startScope(scope) { + this.openNode(scope); + } + + endScope() { + this.closeNode(); + } + + /** + * @param {Emitter & {root: DataNode}} emitter + * @param {string} name + */ + __addSublanguage(emitter, name) { + /** @type DataNode */ + const node = emitter.root; + if (name) node.scope = `language:${name}`; + + this.add(node); + } + + toHTML() { + const renderer = new HTMLRenderer(this, this.options); + return renderer.value(); + } + + finalize() { + this.closeAllNodes(); + return true; + } + } + + /** + * @param {string} value + * @returns {RegExp} + */ + + /** + * @param {RegExp | string } re + * @returns {string} + */ + function source(re) { + if (!re) return null; + if (typeof re === "string") return re; + + return re.source; + } + + /** + * @param {RegExp | string } re + * @returns {string} + */ + function lookahead(re) { + return concat("(?=", re, ")"); + } + + /** + * @param {RegExp | string } re + * @returns {string} + */ + function anyNumberOfTimes(re) { + return concat("(?:", re, ")*"); + } + + /** + * @param {RegExp | string } re + * @returns {string} + */ + function optional(re) { + return concat("(?:", re, ")?"); + } + + /** + * @param {...(RegExp | string) } args + * @returns {string} + */ + function concat(...args) { + const joined = args.map((x) => source(x)).join(""); + return joined; + } + + /** + * @param { Array } args + * @returns {object} + */ + function stripOptionsFromArgs(args) { + const opts = args[args.length - 1]; + + if (typeof opts === "object" && opts.constructor === Object) { + args.splice(args.length - 1, 1); + return opts; + } else { + return {}; + } + } + + /** @typedef { {capture?: boolean} } RegexEitherOptions */ + + /** + * Any of the passed expresssions may match + * + * Creates a huge this | this | that | that match + * @param {(RegExp | string)[] | [...(RegExp | string)[], RegexEitherOptions]} args + * @returns {string} + */ + function either(...args) { + /** @type { object & {capture?: boolean} } */ + const opts = stripOptionsFromArgs(args); + const joined = "(" + + (opts.capture ? "" : "?:") + + args.map((x) => source(x)).join("|") + ")"; + return joined; + } + + /** + * @param {RegExp | string} re + * @returns {number} + */ + function countMatchGroups(re) { + return (new RegExp(re.toString() + "|")).exec("").length - 1; + } + + /** + * Does lexeme start with a regular expression match at the beginning + * @param {RegExp} re + * @param {string} lexeme + */ + function startsWith(re, lexeme) { + const match = re && re.exec(lexeme); + return match && match.index === 0; + } + + // BACKREF_RE matches an open parenthesis or backreference. To avoid + // an incorrect parse, it additionally matches the following: + // - [...] elements, where the meaning of parentheses and escapes change + // - other escape sequences, so we do not misparse escape sequences as + // interesting elements + // - non-matching or lookahead parentheses, which do not capture. These + // follow the '(' with a '?'. + const BACKREF_RE = /\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./; + + // **INTERNAL** Not intended for outside usage + // join logically computes regexps.join(separator), but fixes the + // backreferences so they continue to match. + // it also places each individual regular expression into it's own + // match group, keeping track of the sequencing of those match groups + // is currently an exercise for the caller. :-) + /** + * @param {(string | RegExp)[]} regexps + * @param {{joinWith: string}} opts + * @returns {string} + */ + function _rewriteBackreferences(regexps, { joinWith }) { + let numCaptures = 0; + + return regexps.map((regex) => { + numCaptures += 1; + const offset = numCaptures; + let re = source(regex); + let out = ""; + + while (re.length > 0) { + const match = BACKREF_RE.exec(re); + if (!match) { + out += re; + break; + } + out += re.substring(0, match.index); + re = re.substring(match.index + match[0].length); + if (match[0][0] === "\\" && match[1]) { + // Adjust the backreference. + out += "\\" + String(Number(match[1]) + offset); + } else { + out += match[0]; + if (match[0] === "(") { + numCaptures++; + } + } + } + return out; + }).map((re) => `(${re})`).join(joinWith); + } + + /** @typedef {import('highlight.js').Mode} Mode */ + /** @typedef {import('highlight.js').ModeCallback} ModeCallback */ + + // Common regexps + const MATCH_NOTHING_RE = /\b\B/; + const IDENT_RE = "[a-zA-Z]\\w*"; + const UNDERSCORE_IDENT_RE = "[a-zA-Z_]\\w*"; + const NUMBER_RE = "\\b\\d+(\\.\\d+)?"; + const C_NUMBER_RE = + "(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)"; // 0x..., 0..., decimal, float + const BINARY_NUMBER_RE = "\\b(0b[01]+)"; // 0b... + const RE_STARTERS_RE = + "!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~"; + + /** + * @param { Partial & {binary?: string | RegExp} } opts + */ + const SHEBANG = (opts = {}) => { + const beginShebang = /^#![ ]*\//; + if (opts.binary) { + opts.begin = concat( + beginShebang, + /.*\b/, + opts.binary, + /\b.*/, + ); + } + return inherit$1({ + scope: "meta", + begin: beginShebang, + end: /$/, + relevance: 0, + /** @type {ModeCallback} */ + "on:begin": (m, resp) => { + if (m.index !== 0) resp.ignoreMatch(); + }, + }, opts); + }; + + // Common modes + const BACKSLASH_ESCAPE = { + begin: "\\\\[\\s\\S]", + relevance: 0, + }; + const APOS_STRING_MODE = { + scope: "string", + begin: "'", + end: "'", + illegal: "\\n", + contains: [BACKSLASH_ESCAPE], + }; + const QUOTE_STRING_MODE = { + scope: "string", + begin: '"', + end: '"', + illegal: "\\n", + contains: [BACKSLASH_ESCAPE], + }; + const PHRASAL_WORDS_MODE = { + begin: + /\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/, + }; + /** + * Creates a comment mode + * + * @param {string | RegExp} begin + * @param {string | RegExp} end + * @param {Mode | {}} [modeOptions] + * @returns {Partial} + */ + const COMMENT = function (begin, end, modeOptions = {}) { + const mode = inherit$1( + { + scope: "comment", + begin, + end, + contains: [], + }, + modeOptions, + ); + mode.contains.push({ + scope: "doctag", + // hack to avoid the space from being included. the space is necessary to + // match here to prevent the plain text rule below from gobbling up doctags + begin: "[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)", + end: /(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/, + excludeBegin: true, + relevance: 0, + }); + const ENGLISH_WORD = either( + // list of common 1 and 2 letter words in English + "I", + "a", + "is", + "so", + "us", + "to", + "at", + "if", + "in", + "it", + "on", + // note: this is not an exhaustive list of contractions, just popular ones + /[A-Za-z]+['](d|ve|re|ll|t|s|n)/, // contractions - can't we'd they're let's, etc + /[A-Za-z]+[-][a-z]+/, // `no-way`, etc. + /[A-Za-z][a-z]{2,}/, // allow capitalized words at beginning of sentences + ); + // looking like plain text, more likely to be a comment + mode.contains.push( + { + // TODO: how to include ", (, ) without breaking grammars that use these for + // comment delimiters? + // begin: /[ ]+([()"]?([A-Za-z'-]{3,}|is|a|I|so|us|[tT][oO]|at|if|in|it|on)[.]?[()":]?([.][ ]|[ ]|\))){3}/ + // --- + + // this tries to find sequences of 3 english words in a row (without any + // "programming" type syntax) this gives us a strong signal that we've + // TRULY found a comment - vs perhaps scanning with the wrong language. + // It's possible to find something that LOOKS like the start of the + // comment - but then if there is no readable text - good chance it is a + // false match and not a comment. + // + // for a visual example please see: + // https://github.com/highlightjs/highlight.js/issues/2827 + + begin: concat( + /[ ]+/, // necessary to prevent us gobbling up doctags like /* @author Bob Mcgill */ + "(", + ENGLISH_WORD, + /[.]?[:]?([.][ ]|[ ])/, + "){3}", + ), // look for 3 words in a row + }, + ); + return mode; + }; + const C_LINE_COMMENT_MODE = COMMENT("//", "$"); + const C_BLOCK_COMMENT_MODE = COMMENT("/\\*", "\\*/"); + const HASH_COMMENT_MODE = COMMENT("#", "$"); + const NUMBER_MODE = { + scope: "number", + begin: NUMBER_RE, + relevance: 0, + }; + const C_NUMBER_MODE = { + scope: "number", + begin: C_NUMBER_RE, + relevance: 0, + }; + const BINARY_NUMBER_MODE = { + scope: "number", + begin: BINARY_NUMBER_RE, + relevance: 0, + }; + const REGEXP_MODE = { + scope: "regexp", + begin: /\/(?=[^/\n]*\/)/, + end: /\/[gimuy]*/, + contains: [ + BACKSLASH_ESCAPE, + { + begin: /\[/, + end: /\]/, + relevance: 0, + contains: [BACKSLASH_ESCAPE], + }, + ], + }; + const TITLE_MODE = { + scope: "title", + begin: IDENT_RE, + relevance: 0, + }; + const UNDERSCORE_TITLE_MODE = { + scope: "title", + begin: UNDERSCORE_IDENT_RE, + relevance: 0, + }; + const METHOD_GUARD = { + // excludes method names from keyword processing + begin: "\\.\\s*" + UNDERSCORE_IDENT_RE, + relevance: 0, + }; + + /** + * Adds end same as begin mechanics to a mode + * + * Your mode must include at least a single () match group as that first match + * group is what is used for comparison + * @param {Partial} mode + */ + const END_SAME_AS_BEGIN = function (mode) { + return Object.assign(mode, { + /** @type {ModeCallback} */ + "on:begin": (m, resp) => { + resp.data._beginMatch = m[1]; + }, + /** @type {ModeCallback} */ + "on:end": (m, resp) => { + if (resp.data._beginMatch !== m[1]) resp.ignoreMatch(); + }, + }); + }; + + var MODES = /*#__PURE__*/ Object.freeze({ + __proto__: null, + APOS_STRING_MODE: APOS_STRING_MODE, + BACKSLASH_ESCAPE: BACKSLASH_ESCAPE, + BINARY_NUMBER_MODE: BINARY_NUMBER_MODE, + BINARY_NUMBER_RE: BINARY_NUMBER_RE, + COMMENT: COMMENT, + C_BLOCK_COMMENT_MODE: C_BLOCK_COMMENT_MODE, + C_LINE_COMMENT_MODE: C_LINE_COMMENT_MODE, + C_NUMBER_MODE: C_NUMBER_MODE, + C_NUMBER_RE: C_NUMBER_RE, + END_SAME_AS_BEGIN: END_SAME_AS_BEGIN, + HASH_COMMENT_MODE: HASH_COMMENT_MODE, + IDENT_RE: IDENT_RE, + MATCH_NOTHING_RE: MATCH_NOTHING_RE, + METHOD_GUARD: METHOD_GUARD, + NUMBER_MODE: NUMBER_MODE, + NUMBER_RE: NUMBER_RE, + PHRASAL_WORDS_MODE: PHRASAL_WORDS_MODE, + QUOTE_STRING_MODE: QUOTE_STRING_MODE, + REGEXP_MODE: REGEXP_MODE, + RE_STARTERS_RE: RE_STARTERS_RE, + SHEBANG: SHEBANG, + TITLE_MODE: TITLE_MODE, + UNDERSCORE_IDENT_RE: UNDERSCORE_IDENT_RE, + UNDERSCORE_TITLE_MODE: UNDERSCORE_TITLE_MODE, + }); + + /** + @typedef {import('highlight.js').CallbackResponse} CallbackResponse + @typedef {import('highlight.js').CompilerExt} CompilerExt + */ + + // Grammar extensions / plugins + // See: https://github.com/highlightjs/highlight.js/issues/2833 + + // Grammar extensions allow "syntactic sugar" to be added to the grammar modes + // without requiring any underlying changes to the compiler internals. + + // `compileMatch` being the perfect small example of now allowing a grammar + // author to write `match` when they desire to match a single expression rather + // than being forced to use `begin`. The extension then just moves `match` into + // `begin` when it runs. Ie, no features have been added, but we've just made + // the experience of writing (and reading grammars) a little bit nicer. + + // ------ + + // TODO: We need negative look-behind support to do this properly + /** + * Skip a match if it has a preceding dot + * + * This is used for `beginKeywords` to prevent matching expressions such as + * `bob.keyword.do()`. The mode compiler automatically wires this up as a + * special _internal_ 'on:begin' callback for modes with `beginKeywords` + * @param {RegExpMatchArray} match + * @param {CallbackResponse} response + */ + function skipIfHasPrecedingDot(match, response) { + const before = match.input[match.index - 1]; + if (before === ".") { + response.ignoreMatch(); + } + } + + /** + * @type {CompilerExt} + */ + function scopeClassName(mode, _parent) { + // eslint-disable-next-line no-undefined + if (mode.className !== undefined) { + mode.scope = mode.className; + delete mode.className; + } + } + + /** + * `beginKeywords` syntactic sugar + * @type {CompilerExt} + */ + function beginKeywords(mode, parent) { + if (!parent) return; + if (!mode.beginKeywords) return; + + // for languages with keywords that include non-word characters checking for + // a word boundary is not sufficient, so instead we check for a word boundary + // or whitespace - this does no harm in any case since our keyword engine + // doesn't allow spaces in keywords anyways and we still check for the boundary + // first + mode.begin = "\\b(" + mode.beginKeywords.split(" ").join("|") + + ")(?!\\.)(?=\\b|\\s)"; + mode.__beforeBegin = skipIfHasPrecedingDot; + mode.keywords = mode.keywords || mode.beginKeywords; + delete mode.beginKeywords; + + // prevents double relevance, the keywords themselves provide + // relevance, the mode doesn't need to double it + // eslint-disable-next-line no-undefined + if (mode.relevance === undefined) mode.relevance = 0; + } + + /** + * Allow `illegal` to contain an array of illegal values + * @type {CompilerExt} + */ + function compileIllegal(mode, _parent) { + if (!Array.isArray(mode.illegal)) return; + + mode.illegal = either(...mode.illegal); + } + + /** + * `match` to match a single expression for readability + * @type {CompilerExt} + */ + function compileMatch(mode, _parent) { + if (!mode.match) return; + if (mode.begin || mode.end) { + throw new Error("begin & end are not supported with match"); + } + + mode.begin = mode.match; + delete mode.match; + } + + /** + * provides the default 1 relevance to all modes + * @type {CompilerExt} + */ + function compileRelevance(mode, _parent) { + // eslint-disable-next-line no-undefined + if (mode.relevance === undefined) mode.relevance = 1; + } + + // allow beforeMatch to act as a "qualifier" for the match + // the full match begin must be [beforeMatch][begin] + const beforeMatchExt = (mode, parent) => { + if (!mode.beforeMatch) return; + // starts conflicts with endsParent which we need to make sure the child + // rule is not matched multiple times + if (mode.starts) throw new Error("beforeMatch cannot be used with starts"); + + const originalMode = Object.assign({}, mode); + Object.keys(mode).forEach((key) => { + delete mode[key]; + }); + + mode.keywords = originalMode.keywords; + mode.begin = concat( + originalMode.beforeMatch, + lookahead(originalMode.begin), + ); + mode.starts = { + relevance: 0, + contains: [ + Object.assign(originalMode, { endsParent: true }), + ], + }; + mode.relevance = 0; + + delete originalMode.beforeMatch; + }; + + // keywords that should have no default relevance value + const COMMON_KEYWORDS = [ + "of", + "and", + "for", + "in", + "not", + "or", + "if", + "then", + "parent", // common variable name + "list", // common variable name + "value", // common variable name + ]; + + const DEFAULT_KEYWORD_SCOPE = "keyword"; + + /** + * Given raw keywords from a language definition, compile them. + * + * @param {string | Record | Array} rawKeywords + * @param {boolean} caseInsensitive + */ + function compileKeywords( + rawKeywords, + caseInsensitive, + scopeName = DEFAULT_KEYWORD_SCOPE, + ) { + /** @type {import("highlight.js/private").KeywordDict} */ + const compiledKeywords = Object.create(null); + + // input can be a string of keywords, an array of keywords, or a object with + // named keys representing scopeName (which can then point to a string or array) + if (typeof rawKeywords === "string") { + compileList(scopeName, rawKeywords.split(" ")); + } else if (Array.isArray(rawKeywords)) { + compileList(scopeName, rawKeywords); + } else { + Object.keys(rawKeywords).forEach(function (scopeName) { + // collapse all our objects back into the parent object + Object.assign( + compiledKeywords, + compileKeywords(rawKeywords[scopeName], caseInsensitive, scopeName), + ); + }); + } + return compiledKeywords; + + // --- + + /** + * Compiles an individual list of keywords + * + * Ex: "for if when while|5" + * + * @param {string} scopeName + * @param {Array} keywordList + */ + function compileList(scopeName, keywordList) { + if (caseInsensitive) { + keywordList = keywordList.map((x) => x.toLowerCase()); + } + keywordList.forEach(function (keyword) { + const pair = keyword.split("|"); + compiledKeywords[pair[0]] = [ + scopeName, + scoreForKeyword(pair[0], pair[1]), + ]; + }); + } + } + + /** + * Returns the proper score for a given keyword + * + * Also takes into account comment keywords, which will be scored 0 UNLESS + * another score has been manually assigned. + * @param {string} keyword + * @param {string} [providedScore] + */ + function scoreForKeyword(keyword, providedScore) { + // manual scores always win over common keywords + // so you can force a score of 1 if you really insist + if (providedScore) { + return Number(providedScore); + } + + return commonKeyword(keyword) ? 0 : 1; + } + + /** + * Determines if a given keyword is common or not + * + * @param {string} keyword */ + function commonKeyword(keyword) { + return COMMON_KEYWORDS.includes(keyword.toLowerCase()); + } + + /* + + For the reasoning behind this please see: + https://github.com/highlightjs/highlight.js/issues/2880#issuecomment-747275419 + + */ + + /** + * @type {Record} + */ + const seenDeprecations = {}; + + /** + * @param {string} message + */ + const error = (message) => { + console.error(message); + }; + + /** + * @param {string} message + * @param {any} args + */ + const warn = (message, ...args) => { + console.log(`WARN: ${message}`, ...args); + }; + + /** + * @param {string} version + * @param {string} message + */ + const deprecated = (version, message) => { + if (seenDeprecations[`${version}/${message}`]) return; + + console.log(`Deprecated as of ${version}. ${message}`); + seenDeprecations[`${version}/${message}`] = true; + }; + + /* eslint-disable no-throw-literal */ + + /** + @typedef {import('highlight.js').CompiledMode} CompiledMode + */ + + const MultiClassError = new Error(); + + /** + * Renumbers labeled scope names to account for additional inner match + * groups that otherwise would break everything. + * + * Lets say we 3 match scopes: + * + * { 1 => ..., 2 => ..., 3 => ... } + * + * So what we need is a clean match like this: + * + * (a)(b)(c) => [ "a", "b", "c" ] + * + * But this falls apart with inner match groups: + * + * (a)(((b)))(c) => ["a", "b", "b", "b", "c" ] + * + * Our scopes are now "out of alignment" and we're repeating `b` 3 times. + * What needs to happen is the numbers are remapped: + * + * { 1 => ..., 2 => ..., 5 => ... } + * + * We also need to know that the ONLY groups that should be output + * are 1, 2, and 5. This function handles this behavior. + * + * @param {CompiledMode} mode + * @param {Array} regexes + * @param {{key: "beginScope"|"endScope"}} opts + */ + function remapScopeNames(mode, regexes, { key }) { + let offset = 0; + const scopeNames = mode[key]; + /** @type Record */ + const emit = {}; + /** @type Record */ + const positions = {}; + + for (let i = 1; i <= regexes.length; i++) { + positions[i + offset] = scopeNames[i]; + emit[i + offset] = true; + offset += countMatchGroups(regexes[i - 1]); + } + // we use _emit to keep track of which match groups are "top-level" to avoid double + // output from inside match groups + mode[key] = positions; + mode[key]._emit = emit; + mode[key]._multi = true; + } + + /** + * @param {CompiledMode} mode + */ + function beginMultiClass(mode) { + if (!Array.isArray(mode.begin)) return; + + if (mode.skip || mode.excludeBegin || mode.returnBegin) { + error( + "skip, excludeBegin, returnBegin not compatible with beginScope: {}", + ); + throw MultiClassError; + } + + if (typeof mode.beginScope !== "object" || mode.beginScope === null) { + error("beginScope must be object"); + throw MultiClassError; + } + + remapScopeNames(mode, mode.begin, { key: "beginScope" }); + mode.begin = _rewriteBackreferences(mode.begin, { joinWith: "" }); + } + + /** + * @param {CompiledMode} mode + */ + function endMultiClass(mode) { + if (!Array.isArray(mode.end)) return; + + if (mode.skip || mode.excludeEnd || mode.returnEnd) { + error("skip, excludeEnd, returnEnd not compatible with endScope: {}"); + throw MultiClassError; + } + + if (typeof mode.endScope !== "object" || mode.endScope === null) { + error("endScope must be object"); + throw MultiClassError; + } + + remapScopeNames(mode, mode.end, { key: "endScope" }); + mode.end = _rewriteBackreferences(mode.end, { joinWith: "" }); + } + + /** + * this exists only to allow `scope: {}` to be used beside `match:` + * Otherwise `beginScope` would necessary and that would look weird + + { + match: [ /def/, /\w+/ ] + scope: { 1: "keyword" , 2: "title" } + } + + * @param {CompiledMode} mode + */ + function scopeSugar(mode) { + if (mode.scope && typeof mode.scope === "object" && mode.scope !== null) { + mode.beginScope = mode.scope; + delete mode.scope; + } + } + + /** + * @param {CompiledMode} mode + */ + function MultiClass(mode) { + scopeSugar(mode); + + if (typeof mode.beginScope === "string") { + mode.beginScope = { _wrap: mode.beginScope }; + } + if (typeof mode.endScope === "string") { + mode.endScope = { _wrap: mode.endScope }; + } + + beginMultiClass(mode); + endMultiClass(mode); + } + + /** + @typedef {import('highlight.js').Mode} Mode + @typedef {import('highlight.js').CompiledMode} CompiledMode + @typedef {import('highlight.js').Language} Language + @typedef {import('highlight.js').HLJSPlugin} HLJSPlugin + @typedef {import('highlight.js').CompiledLanguage} CompiledLanguage + */ + + // compilation + + /** + * Compiles a language definition result + * + * Given the raw result of a language definition (Language), compiles this so + * that it is ready for highlighting code. + * @param {Language} language + * @returns {CompiledLanguage} + */ + function compileLanguage(language) { + /** + * Builds a regex with the case sensitivity of the current language + * + * @param {RegExp | string} value + * @param {boolean} [global] + */ + function langRe(value, global) { + return new RegExp( + source(value), + "m" + + (language.case_insensitive ? "i" : "") + + (language.unicodeRegex ? "u" : "") + + (global ? "g" : ""), + ); + } + + /** + Stores multiple regular expressions and allows you to quickly search for + them all in a string simultaneously - returning the first match. It does + this by creating a huge (a|b|c) regex - each individual item wrapped with () + and joined by `|` - using match groups to track position. When a match is + found checking which position in the array has content allows us to figure + out which of the original regexes / match groups triggered the match. + + The match object itself (the result of `Regex.exec`) is returned but also + enhanced by merging in any meta-data that was registered with the regex. + This is how we keep track of which mode matched, and what type of rule + (`illegal`, `begin`, end, etc). + */ + class MultiRegex { + constructor() { + this.matchIndexes = {}; + // @ts-ignore + this.regexes = []; + this.matchAt = 1; + this.position = 0; + } + + // @ts-ignore + addRule(re, opts) { + opts.position = this.position++; + // @ts-ignore + this.matchIndexes[this.matchAt] = opts; + this.regexes.push([opts, re]); + this.matchAt += countMatchGroups(re) + 1; + } + + compile() { + if (this.regexes.length === 0) { + // avoids the need to check length every time exec is called + // @ts-ignore + this.exec = () => null; + } + const terminators = this.regexes.map((el) => el[1]); + this.matcherRe = langRe( + _rewriteBackreferences(terminators, { joinWith: "|" }), + true, + ); + this.lastIndex = 0; + } + + /** @param {string} s */ + exec(s) { + this.matcherRe.lastIndex = this.lastIndex; + const match = this.matcherRe.exec(s); + if (!match) return null; + + // eslint-disable-next-line no-undefined + const i = match.findIndex((el, i) => i > 0 && el !== undefined); + // @ts-ignore + const matchData = this.matchIndexes[i]; + // trim off any earlier non-relevant match groups (ie, the other regex + // match groups that make up the multi-matcher) + match.splice(0, i); + + return Object.assign(match, matchData); + } + } + + /* + Created to solve the key deficiently with MultiRegex - there is no way to + test for multiple matches at a single location. Why would we need to do + that? In the future a more dynamic engine will allow certain matches to be + ignored. An example: if we matched say the 3rd regex in a large group but + decided to ignore it - we'd need to started testing again at the 4th + regex... but MultiRegex itself gives us no real way to do that. + + So what this class creates MultiRegexs on the fly for whatever search + position they are needed. + + NOTE: These additional MultiRegex objects are created dynamically. For most + grammars most of the time we will never actually need anything more than the + first MultiRegex - so this shouldn't have too much overhead. + + Say this is our search group, and we match regex3, but wish to ignore it. + + regex1 | regex2 | regex3 | regex4 | regex5 ' ie, startAt = 0 + + What we need is a new MultiRegex that only includes the remaining + possibilities: + + regex4 | regex5 ' ie, startAt = 3 + + This class wraps all that complexity up in a simple API... `startAt` decides + where in the array of expressions to start doing the matching. It + auto-increments, so if a match is found at position 2, then startAt will be + set to 3. If the end is reached startAt will return to 0. + + MOST of the time the parser will be setting startAt manually to 0. + */ + class ResumableMultiRegex { + constructor() { + // @ts-ignore + this.rules = []; + // @ts-ignore + this.multiRegexes = []; + this.count = 0; + + this.lastIndex = 0; + this.regexIndex = 0; + } + + // @ts-ignore + getMatcher(index) { + if (this.multiRegexes[index]) return this.multiRegexes[index]; + + const matcher = new MultiRegex(); + this.rules.slice(index).forEach(([re, opts]) => + matcher.addRule(re, opts) + ); + matcher.compile(); + this.multiRegexes[index] = matcher; + return matcher; + } + + resumingScanAtSamePosition() { + return this.regexIndex !== 0; + } + + considerAll() { + this.regexIndex = 0; + } + + // @ts-ignore + addRule(re, opts) { + this.rules.push([re, opts]); + if (opts.type === "begin") this.count++; + } + + /** @param {string} s */ + exec(s) { + const m = this.getMatcher(this.regexIndex); + m.lastIndex = this.lastIndex; + let result = m.exec(s); + + // The following is because we have no easy way to say "resume scanning at the + // existing position but also skip the current rule ONLY". What happens is + // all prior rules are also skipped which can result in matching the wrong + // thing. Example of matching "booger": + + // our matcher is [string, "booger", number] + // + // ....booger.... + + // if "booger" is ignored then we'd really need a regex to scan from the + // SAME position for only: [string, number] but ignoring "booger" (if it + // was the first match), a simple resume would scan ahead who knows how + // far looking only for "number", ignoring potential string matches (or + // future "booger" matches that might be valid.) + + // So what we do: We execute two matchers, one resuming at the same + // position, but the second full matcher starting at the position after: + + // /--- resume first regex match here (for [number]) + // |/---- full match here for [string, "booger", number] + // vv + // ....booger.... + + // Which ever results in a match first is then used. So this 3-4 step + // process essentially allows us to say "match at this position, excluding + // a prior rule that was ignored". + // + // 1. Match "booger" first, ignore. Also proves that [string] does non match. + // 2. Resume matching for [number] + // 3. Match at index + 1 for [string, "booger", number] + // 4. If #2 and #3 result in matches, which came first? + if (this.resumingScanAtSamePosition()) { + if (result && result.index === this.lastIndex); + else { // use the second matcher result + const m2 = this.getMatcher(0); + m2.lastIndex = this.lastIndex + 1; + result = m2.exec(s); + } + } + + if (result) { + this.regexIndex += result.position + 1; + if (this.regexIndex === this.count) { + // wrap-around to considering all matches again + this.considerAll(); + } + } + + return result; + } + } + + /** + * Given a mode, builds a huge ResumableMultiRegex that can be used to walk + * the content and find matches. + * + * @param {CompiledMode} mode + * @returns {ResumableMultiRegex} + */ + function buildModeRegex(mode) { + const mm = new ResumableMultiRegex(); + + mode.contains.forEach((term) => + mm.addRule(term.begin, { rule: term, type: "begin" }) + ); + + if (mode.terminatorEnd) { + mm.addRule(mode.terminatorEnd, { type: "end" }); + } + if (mode.illegal) { + mm.addRule(mode.illegal, { type: "illegal" }); + } + + return mm; + } + + /** skip vs abort vs ignore + * + * @skip - The mode is still entered and exited normally (and contains rules apply), + * but all content is held and added to the parent buffer rather than being + * output when the mode ends. Mostly used with `sublanguage` to build up + * a single large buffer than can be parsed by sublanguage. + * + * - The mode begin ands ends normally. + * - Content matched is added to the parent mode buffer. + * - The parser cursor is moved forward normally. + * + * @abort - A hack placeholder until we have ignore. Aborts the mode (as if it + * never matched) but DOES NOT continue to match subsequent `contains` + * modes. Abort is bad/suboptimal because it can result in modes + * farther down not getting applied because an earlier rule eats the + * content but then aborts. + * + * - The mode does not begin. + * - Content matched by `begin` is added to the mode buffer. + * - The parser cursor is moved forward accordingly. + * + * @ignore - Ignores the mode (as if it never matched) and continues to match any + * subsequent `contains` modes. Ignore isn't technically possible with + * the current parser implementation. + * + * - The mode does not begin. + * - Content matched by `begin` is ignored. + * - The parser cursor is not moved forward. + */ + + /** + * Compiles an individual mode + * + * This can raise an error if the mode contains certain detectable known logic + * issues. + * @param {Mode} mode + * @param {CompiledMode | null} [parent] + * @returns {CompiledMode | never} + */ + function compileMode(mode, parent) { + const cmode = /** @type CompiledMode */ (mode); + if (mode.isCompiled) return cmode; + + [ + scopeClassName, + // do this early so compiler extensions generally don't have to worry about + // the distinction between match/begin + compileMatch, + MultiClass, + beforeMatchExt, + ].forEach((ext) => ext(mode, parent)); + + language.compilerExtensions.forEach((ext) => ext(mode, parent)); + + // __beforeBegin is considered private API, internal use only + mode.__beforeBegin = null; + + [ + beginKeywords, + // do this later so compiler extensions that come earlier have access to the + // raw array if they wanted to perhaps manipulate it, etc. + compileIllegal, + // default to 1 relevance if not specified + compileRelevance, + ].forEach((ext) => ext(mode, parent)); + + mode.isCompiled = true; + + let keywordPattern = null; + if (typeof mode.keywords === "object" && mode.keywords.$pattern) { + // we need a copy because keywords might be compiled multiple times + // so we can't go deleting $pattern from the original on the first + // pass + mode.keywords = Object.assign({}, mode.keywords); + keywordPattern = mode.keywords.$pattern; + delete mode.keywords.$pattern; + } + keywordPattern = keywordPattern || /\w+/; + + if (mode.keywords) { + mode.keywords = compileKeywords( + mode.keywords, + language.case_insensitive, + ); + } + + cmode.keywordPatternRe = langRe(keywordPattern, true); + + if (parent) { + if (!mode.begin) mode.begin = /\B|\b/; + cmode.beginRe = langRe(cmode.begin); + if (!mode.end && !mode.endsWithParent) mode.end = /\B|\b/; + if (mode.end) cmode.endRe = langRe(cmode.end); + cmode.terminatorEnd = source(cmode.end) || ""; + if (mode.endsWithParent && parent.terminatorEnd) { + cmode.terminatorEnd += (mode.end ? "|" : "") + parent.terminatorEnd; + } + } + if (mode.illegal) { + cmode.illegalRe = langRe(/** @type {RegExp | string} */ (mode.illegal)); + } + if (!mode.contains) mode.contains = []; + + mode.contains = [].concat(...mode.contains.map(function (c) { + return expandOrCloneMode(c === "self" ? mode : c); + })); + mode.contains.forEach(function (c) { + compileMode(/** @type Mode */ (c), cmode); + }); + + if (mode.starts) { + compileMode(mode.starts, parent); + } + + cmode.matcher = buildModeRegex(cmode); + return cmode; + } + + if (!language.compilerExtensions) language.compilerExtensions = []; + + // self is not valid at the top-level + if (language.contains && language.contains.includes("self")) { + throw new Error( + "ERR: contains `self` is not supported at the top-level of a language. See documentation.", + ); + } + + // we need a null object, which inherit will guarantee + language.classNameAliases = inherit$1(language.classNameAliases || {}); + + return compileMode(/** @type Mode */ (language)); + } + + /** + * Determines if a mode has a dependency on it's parent or not + * + * If a mode does have a parent dependency then often we need to clone it if + * it's used in multiple places so that each copy points to the correct parent, + * where-as modes without a parent can often safely be re-used at the bottom of + * a mode chain. + * + * @param {Mode | null} mode + * @returns {boolean} - is there a dependency on the parent? + */ + function dependencyOnParent(mode) { + if (!mode) return false; + + return mode.endsWithParent || dependencyOnParent(mode.starts); + } + + /** + * Expands a mode or clones it if necessary + * + * This is necessary for modes with parental dependenceis (see notes on + * `dependencyOnParent`) and for nodes that have `variants` - which must then be + * exploded into their own individual modes at compile time. + * + * @param {Mode} mode + * @returns {Mode | Mode[]} + */ + function expandOrCloneMode(mode) { + if (mode.variants && !mode.cachedVariants) { + mode.cachedVariants = mode.variants.map(function (variant) { + return inherit$1(mode, { variants: null }, variant); + }); + } + + // EXPAND + // if we have variants then essentially "replace" the mode with the variants + // this happens in compileMode, where this function is called from + if (mode.cachedVariants) { + return mode.cachedVariants; + } + + // CLONE + // if we have dependencies on parents then we need a unique + // instance of ourselves, so we can be reused with many + // different parents without issue + if (dependencyOnParent(mode)) { + return inherit$1(mode, { + starts: mode.starts ? inherit$1(mode.starts) : null, + }); + } + + if (Object.isFrozen(mode)) { + return inherit$1(mode); + } + + // no special dependency issues, just return ourselves + return mode; + } + + var version = "11.9.0"; + + class HTMLInjectionError extends Error { + constructor(reason, html) { + super(reason); + this.name = "HTMLInjectionError"; + this.html = html; + } + } + + /* + Syntax highlighting with language autodetection. + https://highlightjs.org/ + */ + + /** + @typedef {import('highlight.js').Mode} Mode + @typedef {import('highlight.js').CompiledMode} CompiledMode + @typedef {import('highlight.js').CompiledScope} CompiledScope + @typedef {import('highlight.js').Language} Language + @typedef {import('highlight.js').HLJSApi} HLJSApi + @typedef {import('highlight.js').HLJSPlugin} HLJSPlugin + @typedef {import('highlight.js').PluginEvent} PluginEvent + @typedef {import('highlight.js').HLJSOptions} HLJSOptions + @typedef {import('highlight.js').LanguageFn} LanguageFn + @typedef {import('highlight.js').HighlightedHTMLElement} HighlightedHTMLElement + @typedef {import('highlight.js').BeforeHighlightContext} BeforeHighlightContext + @typedef {import('highlight.js/private').MatchType} MatchType + @typedef {import('highlight.js/private').KeywordData} KeywordData + @typedef {import('highlight.js/private').EnhancedMatch} EnhancedMatch + @typedef {import('highlight.js/private').AnnotatedError} AnnotatedError + @typedef {import('highlight.js').AutoHighlightResult} AutoHighlightResult + @typedef {import('highlight.js').HighlightOptions} HighlightOptions + @typedef {import('highlight.js').HighlightResult} HighlightResult + */ + + const escape = escapeHTML; + const inherit = inherit$1; + const NO_MATCH = Symbol("nomatch"); + const MAX_KEYWORD_HITS = 7; + + /** + * @param {any} hljs - object that is extended (legacy) + * @returns {HLJSApi} + */ + const HLJS = function (hljs) { + // Global internal variables used within the highlight.js library. + /** @type {Record} */ + const languages = Object.create(null); + /** @type {Record} */ + const aliases = Object.create(null); + /** @type {HLJSPlugin[]} */ + const plugins = []; + + // safe/production mode - swallows more errors, tries to keep running + // even if a single syntax or parse hits a fatal error + let SAFE_MODE = true; + const LANGUAGE_NOT_FOUND = + "Could not find the language '{}', did you forget to load/include a language module?"; + /** @type {Language} */ + const PLAINTEXT_LANGUAGE = { + disableAutodetect: true, + name: "Plain text", + contains: [], + }; + + // Global options used when within external APIs. This is modified when + // calling the `hljs.configure` function. + /** @type HLJSOptions */ + let options = { + ignoreUnescapedHTML: false, + throwUnescapedHTML: false, + noHighlightRe: /^(no-?highlight)$/i, + languageDetectRe: /\blang(?:uage)?-([\w-]+)\b/i, + classPrefix: "hljs-", + cssSelector: "pre code", + languages: null, + // beta configuration options, subject to change, welcome to discuss + // https://github.com/highlightjs/highlight.js/issues/1086 + __emitter: TokenTreeEmitter, + }; + + /* Utility functions */ + + /** + * Tests a language name to see if highlighting should be skipped + * @param {string} languageName + */ + function shouldNotHighlight(languageName) { + return options.noHighlightRe.test(languageName); + } + + /** + * @param {HighlightedHTMLElement} block - the HTML element to determine language for + */ + function blockLanguage(block) { + let classes = block.className + " "; + + classes += block.parentNode ? block.parentNode.className : ""; + + // language-* takes precedence over non-prefixed class names. + const match = options.languageDetectRe.exec(classes); + if (match) { + const language = getLanguage(match[1]); + if (!language) { + warn(LANGUAGE_NOT_FOUND.replace("{}", match[1])); + warn("Falling back to no-highlight mode for this block.", block); + } + return language ? match[1] : "no-highlight"; + } + + return classes + .split(/\s+/) + .find((_class) => shouldNotHighlight(_class) || getLanguage(_class)); + } + + /** + * Core highlighting function. + * + * OLD API + * highlight(lang, code, ignoreIllegals, continuation) + * + * NEW API + * highlight(code, {lang, ignoreIllegals}) + * + * @param {string} codeOrLanguageName - the language to use for highlighting + * @param {string | HighlightOptions} optionsOrCode - the code to highlight + * @param {boolean} [ignoreIllegals] - whether to ignore illegal matches, default is to bail + * + * @returns {HighlightResult} Result - an object that represents the result + * @property {string} language - the language name + * @property {number} relevance - the relevance score + * @property {string} value - the highlighted HTML code + * @property {string} code - the original raw code + * @property {CompiledMode} top - top of the current mode stack + * @property {boolean} illegal - indicates whether any illegal matches were found + */ + function highlight(codeOrLanguageName, optionsOrCode, ignoreIllegals) { + let code = ""; + let languageName = ""; + if (typeof optionsOrCode === "object") { + code = codeOrLanguageName; + ignoreIllegals = optionsOrCode.ignoreIllegals; + languageName = optionsOrCode.language; + } else { + // old API + deprecated( + "10.7.0", + "highlight(lang, code, ...args) has been deprecated.", + ); + deprecated( + "10.7.0", + "Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277", + ); + languageName = codeOrLanguageName; + code = optionsOrCode; + } + + // https://github.com/highlightjs/highlight.js/issues/3149 + // eslint-disable-next-line no-undefined + if (ignoreIllegals === undefined) ignoreIllegals = true; + + /** @type {BeforeHighlightContext} */ + const context = { + code, + language: languageName, + }; + // the plugin can change the desired language or the code to be highlighted + // just be changing the object it was passed + fire("before:highlight", context); + + // a before plugin can usurp the result completely by providing it's own + // in which case we don't even need to call highlight + const result = context.result + ? context.result + : _highlight(context.language, context.code, ignoreIllegals); + + result.code = context.code; + // the plugin can change anything in result to suite it + fire("after:highlight", result); + + return result; + } + + /** + * private highlight that's used internally and does not fire callbacks + * + * @param {string} languageName - the language to use for highlighting + * @param {string} codeToHighlight - the code to highlight + * @param {boolean?} [ignoreIllegals] - whether to ignore illegal matches, default is to bail + * @param {CompiledMode?} [continuation] - current continuation mode, if any + * @returns {HighlightResult} - result of the highlight operation + */ + function _highlight( + languageName, + codeToHighlight, + ignoreIllegals, + continuation, + ) { + const keywordHits = Object.create(null); + + /** + * Return keyword data if a match is a keyword + * @param {CompiledMode} mode - current mode + * @param {string} matchText - the textual match + * @returns {KeywordData | false} + */ + function keywordData(mode, matchText) { + return mode.keywords[matchText]; + } + + function processKeywords() { + if (!top.keywords) { + emitter.addText(modeBuffer); + return; + } + + let lastIndex = 0; + top.keywordPatternRe.lastIndex = 0; + let match = top.keywordPatternRe.exec(modeBuffer); + let buf = ""; + + while (match) { + buf += modeBuffer.substring(lastIndex, match.index); + const word = language.case_insensitive + ? match[0].toLowerCase() + : match[0]; + const data = keywordData(top, word); + if (data) { + const [kind, keywordRelevance] = data; + emitter.addText(buf); + buf = ""; + + keywordHits[word] = (keywordHits[word] || 0) + 1; + if (keywordHits[word] <= MAX_KEYWORD_HITS) { + relevance += keywordRelevance; + } + if (kind.startsWith("_")) { + // _ implied for relevance only, do not highlight + // by applying a class name + buf += match[0]; + } else { + const cssClass = language.classNameAliases[kind] || kind; + emitKeyword(match[0], cssClass); + } + } else { + buf += match[0]; + } + lastIndex = top.keywordPatternRe.lastIndex; + match = top.keywordPatternRe.exec(modeBuffer); + } + buf += modeBuffer.substring(lastIndex); + emitter.addText(buf); + } + + function processSubLanguage() { + if (modeBuffer === "") return; + /** @type HighlightResult */ + let result = null; + + if (typeof top.subLanguage === "string") { + if (!languages[top.subLanguage]) { + emitter.addText(modeBuffer); + return; + } + result = _highlight( + top.subLanguage, + modeBuffer, + true, + continuations[top.subLanguage], + ); + continuations[top.subLanguage] = + /** @type {CompiledMode} */ (result._top); + } else { + result = highlightAuto( + modeBuffer, + top.subLanguage.length ? top.subLanguage : null, + ); + } + + // Counting embedded language score towards the host language may be disabled + // with zeroing the containing mode relevance. Use case in point is Markdown that + // allows XML everywhere and makes every XML snippet to have a much larger Markdown + // score. + if (top.relevance > 0) { + relevance += result.relevance; + } + emitter.__addSublanguage(result._emitter, result.language); + } + + function processBuffer() { + if (top.subLanguage != null) { + processSubLanguage(); + } else { + processKeywords(); + } + modeBuffer = ""; + } + + /** + * @param {string} text + * @param {string} scope + */ + function emitKeyword(keyword, scope) { + if (keyword === "") return; + + emitter.startScope(scope); + emitter.addText(keyword); + emitter.endScope(); + } + + /** + * @param {CompiledScope} scope + * @param {RegExpMatchArray} match + */ + function emitMultiClass(scope, match) { + let i = 1; + const max = match.length - 1; + while (i <= max) { + if (!scope._emit[i]) { + i++; + continue; + } + const klass = language.classNameAliases[scope[i]] || scope[i]; + const text = match[i]; + if (klass) { + emitKeyword(text, klass); + } else { + modeBuffer = text; + processKeywords(); + modeBuffer = ""; + } + i++; + } + } + + /** + * @param {CompiledMode} mode - new mode to start + * @param {RegExpMatchArray} match + */ + function startNewMode(mode, match) { + if (mode.scope && typeof mode.scope === "string") { + emitter.openNode(language.classNameAliases[mode.scope] || mode.scope); + } + if (mode.beginScope) { + // beginScope just wraps the begin match itself in a scope + if (mode.beginScope._wrap) { + emitKeyword( + modeBuffer, + language.classNameAliases[mode.beginScope._wrap] || + mode.beginScope._wrap, + ); + modeBuffer = ""; + } else if (mode.beginScope._multi) { + // at this point modeBuffer should just be the match + emitMultiClass(mode.beginScope, match); + modeBuffer = ""; + } + } + + top = Object.create(mode, { parent: { value: top } }); + return top; + } + + /** + * @param {CompiledMode } mode - the mode to potentially end + * @param {RegExpMatchArray} match - the latest match + * @param {string} matchPlusRemainder - match plus remainder of content + * @returns {CompiledMode | void} - the next mode, or if void continue on in current mode + */ + function endOfMode(mode, match, matchPlusRemainder) { + let matched = startsWith(mode.endRe, matchPlusRemainder); + + if (matched) { + if (mode["on:end"]) { + const resp = new Response(mode); + mode["on:end"](match, resp); + if (resp.isMatchIgnored) matched = false; + } + + if (matched) { + while (mode.endsParent && mode.parent) { + mode = mode.parent; + } + return mode; + } + } + // even if on:end fires an `ignore` it's still possible + // that we might trigger the end node because of a parent mode + if (mode.endsWithParent) { + return endOfMode(mode.parent, match, matchPlusRemainder); + } + } + + /** + * Handle matching but then ignoring a sequence of text + * + * @param {string} lexeme - string containing full match text + */ + function doIgnore(lexeme) { + if (top.matcher.regexIndex === 0) { + // no more regexes to potentially match here, so we move the cursor forward one + // space + modeBuffer += lexeme[0]; + return 1; + } else { + // no need to move the cursor, we still have additional regexes to try and + // match at this very spot + resumeScanAtSamePosition = true; + return 0; + } + } + + /** + * Handle the start of a new potential mode match + * + * @param {EnhancedMatch} match - the current match + * @returns {number} how far to advance the parse cursor + */ + function doBeginMatch(match) { + const lexeme = match[0]; + const newMode = match.rule; + + const resp = new Response(newMode); + // first internal before callbacks, then the public ones + const beforeCallbacks = [newMode.__beforeBegin, newMode["on:begin"]]; + for (const cb of beforeCallbacks) { + if (!cb) continue; + cb(match, resp); + if (resp.isMatchIgnored) return doIgnore(lexeme); + } + + if (newMode.skip) { + modeBuffer += lexeme; + } else { + if (newMode.excludeBegin) { + modeBuffer += lexeme; + } + processBuffer(); + if (!newMode.returnBegin && !newMode.excludeBegin) { + modeBuffer = lexeme; + } + } + startNewMode(newMode, match); + return newMode.returnBegin ? 0 : lexeme.length; + } + + /** + * Handle the potential end of mode + * + * @param {RegExpMatchArray} match - the current match + */ + function doEndMatch(match) { + const lexeme = match[0]; + const matchPlusRemainder = codeToHighlight.substring(match.index); + + const endMode = endOfMode(top, match, matchPlusRemainder); + if (!endMode) return NO_MATCH; + + const origin = top; + if (top.endScope && top.endScope._wrap) { + processBuffer(); + emitKeyword(lexeme, top.endScope._wrap); + } else if (top.endScope && top.endScope._multi) { + processBuffer(); + emitMultiClass(top.endScope, match); + } else if (origin.skip) { + modeBuffer += lexeme; + } else { + if (!(origin.returnEnd || origin.excludeEnd)) { + modeBuffer += lexeme; + } + processBuffer(); + if (origin.excludeEnd) { + modeBuffer = lexeme; + } + } + do { + if (top.scope) { + emitter.closeNode(); + } + if (!top.skip && !top.subLanguage) { + relevance += top.relevance; + } + top = top.parent; + } while (top !== endMode.parent); + if (endMode.starts) { + startNewMode(endMode.starts, match); + } + return origin.returnEnd ? 0 : lexeme.length; + } + + function processContinuations() { + const list = []; + for ( + let current = top; + current !== language; + current = current.parent + ) { + if (current.scope) { + list.unshift(current.scope); + } + } + list.forEach((item) => emitter.openNode(item)); + } + + /** @type {{type?: MatchType, index?: number, rule?: Mode}}} */ + let lastMatch = {}; + + /** + * Process an individual match + * + * @param {string} textBeforeMatch - text preceding the match (since the last match) + * @param {EnhancedMatch} [match] - the match itself + */ + function processLexeme(textBeforeMatch, match) { + const lexeme = match && match[0]; + + // add non-matched text to the current mode buffer + modeBuffer += textBeforeMatch; + + if (lexeme == null) { + processBuffer(); + return 0; + } + + // we've found a 0 width match and we're stuck, so we need to advance + // this happens when we have badly behaved rules that have optional matchers to the degree that + // sometimes they can end up matching nothing at all + // Ref: https://github.com/highlightjs/highlight.js/issues/2140 + if ( + lastMatch.type === "begin" && match.type === "end" && + lastMatch.index === match.index && lexeme === "" + ) { + // spit the "skipped" character that our regex choked on back into the output sequence + modeBuffer += codeToHighlight.slice(match.index, match.index + 1); + if (!SAFE_MODE) { + /** @type {AnnotatedError} */ + const err = new Error(`0 width match regex (${languageName})`); + err.languageName = languageName; + err.badRule = lastMatch.rule; + throw err; + } + return 1; + } + lastMatch = match; + + if (match.type === "begin") { + return doBeginMatch(match); + } else if (match.type === "illegal" && !ignoreIllegals) { + // illegal match, we do not continue processing + /** @type {AnnotatedError} */ + const err = new Error( + 'Illegal lexeme "' + lexeme + '" for mode "' + + (top.scope || "") + '"', + ); + err.mode = top; + throw err; + } else if (match.type === "end") { + const processed = doEndMatch(match); + if (processed !== NO_MATCH) { + return processed; + } + } + + // edge case for when illegal matches $ (end of line) which is technically + // a 0 width match but not a begin/end match so it's not caught by the + // first handler (when ignoreIllegals is true) + if (match.type === "illegal" && lexeme === "") { + // advance so we aren't stuck in an infinite loop + return 1; + } + + // infinite loops are BAD, this is a last ditch catch all. if we have a + // decent number of iterations yet our index (cursor position in our + // parsing) still 3x behind our index then something is very wrong + // so we bail + if (iterations > 100000 && iterations > match.index * 3) { + const err = new Error( + "potential infinite loop, way more iterations than matches", + ); + throw err; + } + + /* + Why might be find ourselves here? An potential end match that was + triggered but could not be completed. IE, `doEndMatch` returned NO_MATCH. + (this could be because a callback requests the match be ignored, etc) + + This causes no real harm other than stopping a few times too many. + */ + + modeBuffer += lexeme; + return lexeme.length; + } + + const language = getLanguage(languageName); + if (!language) { + error(LANGUAGE_NOT_FOUND.replace("{}", languageName)); + throw new Error('Unknown language: "' + languageName + '"'); + } + + const md = compileLanguage(language); + let result = ""; + /** @type {CompiledMode} */ + let top = continuation || md; + /** @type Record */ + const continuations = {}; // keep continuations for sub-languages + const emitter = new options.__emitter(options); + processContinuations(); + let modeBuffer = ""; + let relevance = 0; + let index = 0; + let iterations = 0; + let resumeScanAtSamePosition = false; + + try { + if (!language.__emitTokens) { + top.matcher.considerAll(); + + for (;;) { + iterations++; + if (resumeScanAtSamePosition) { + // only regexes not matched previously will now be + // considered for a potential match + resumeScanAtSamePosition = false; + } else { + top.matcher.considerAll(); + } + top.matcher.lastIndex = index; + + const match = top.matcher.exec(codeToHighlight); + // console.log("match", match[0], match.rule && match.rule.begin) + + if (!match) break; + + const beforeMatch = codeToHighlight.substring(index, match.index); + const processedCount = processLexeme(beforeMatch, match); + index = match.index + processedCount; + } + processLexeme(codeToHighlight.substring(index)); + } else { + language.__emitTokens(codeToHighlight, emitter); + } + + emitter.finalize(); + result = emitter.toHTML(); + + return { + language: languageName, + value: result, + relevance, + illegal: false, + _emitter: emitter, + _top: top, + }; + } catch (err) { + if (err.message && err.message.includes("Illegal")) { + return { + language: languageName, + value: escape(codeToHighlight), + illegal: true, + relevance: 0, + _illegalBy: { + message: err.message, + index, + context: codeToHighlight.slice(index - 100, index + 100), + mode: err.mode, + resultSoFar: result, + }, + _emitter: emitter, + }; + } else if (SAFE_MODE) { + return { + language: languageName, + value: escape(codeToHighlight), + illegal: false, + relevance: 0, + errorRaised: err, + _emitter: emitter, + _top: top, + }; + } else { + throw err; + } + } + } + + /** + * returns a valid highlight result, without actually doing any actual work, + * auto highlight starts with this and it's possible for small snippets that + * auto-detection may not find a better match + * @param {string} code + * @returns {HighlightResult} + */ + function justTextHighlightResult(code) { + const result = { + value: escape(code), + illegal: false, + relevance: 0, + _top: PLAINTEXT_LANGUAGE, + _emitter: new options.__emitter(options), + }; + result._emitter.addText(code); + return result; + } + + /** + Highlighting with language detection. Accepts a string with the code to + highlight. Returns an object with the following properties: + + - language (detected language) + - relevance (int) + - value (an HTML string with highlighting markup) + - secondBest (object with the same structure for second-best heuristically + detected language, may be absent) + + @param {string} code + @param {Array} [languageSubset] + @returns {AutoHighlightResult} + */ + function highlightAuto(code, languageSubset) { + languageSubset = languageSubset || options.languages || + Object.keys(languages); + const plaintext = justTextHighlightResult(code); + + const results = languageSubset.filter(getLanguage).filter(autoDetection) + .map((name) => _highlight(name, code, false)); + results.unshift(plaintext); // plaintext is always an option + + const sorted = results.sort((a, b) => { + // sort base on relevance + if (a.relevance !== b.relevance) return b.relevance - a.relevance; + + // always award the tie to the base language + // ie if C++ and Arduino are tied, it's more likely to be C++ + if (a.language && b.language) { + if (getLanguage(a.language).supersetOf === b.language) { + return 1; + } else if (getLanguage(b.language).supersetOf === a.language) { + return -1; + } + } + + // otherwise say they are equal, which has the effect of sorting on + // relevance while preserving the original ordering - which is how ties + // have historically been settled, ie the language that comes first always + // wins in the case of a tie + return 0; + }); + + const [best, secondBest] = sorted; + + /** @type {AutoHighlightResult} */ + const result = best; + result.secondBest = secondBest; + + return result; + } + + /** + * Builds new class name for block given the language name + * + * @param {HTMLElement} element + * @param {string} [currentLang] + * @param {string} [resultLang] + */ + function updateClassName(element, currentLang, resultLang) { + const language = (currentLang && aliases[currentLang]) || resultLang; + + element.classList.add("hljs"); + element.classList.add(`language-${language}`); + } + + /** + * Applies highlighting to a DOM node containing code. + * + * @param {HighlightedHTMLElement} element - the HTML element to highlight + */ + function highlightElement(element) { + /** @type HTMLElement */ + let node = null; + const language = blockLanguage(element); + + if (shouldNotHighlight(language)) return; + + fire("before:highlightElement", { el: element, language }); + + if (element.dataset.highlighted) { + console.log( + "Element previously highlighted. To highlight again, first unset `dataset.highlighted`.", + element, + ); + return; + } + + // we should be all text, no child nodes (unescaped HTML) - this is possibly + // an HTML injection attack - it's likely too late if this is already in + // production (the code has likely already done its damage by the time + // we're seeing it)... but we yell loudly about this so that hopefully it's + // more likely to be caught in development before making it to production + if (element.children.length > 0) { + if (!options.ignoreUnescapedHTML) { + console.warn( + "One of your code blocks includes unescaped HTML. This is a potentially serious security risk.", + ); + console.warn( + "https://github.com/highlightjs/highlight.js/wiki/security", + ); + console.warn("The element with unescaped HTML:"); + console.warn(element); + } + if (options.throwUnescapedHTML) { + const err = new HTMLInjectionError( + "One of your code blocks includes unescaped HTML.", + element.innerHTML, + ); + throw err; + } + } + + node = element; + const text = node.textContent; + const result = language + ? highlight(text, { language, ignoreIllegals: true }) + : highlightAuto(text); + + element.innerHTML = result.value; + element.dataset.highlighted = "yes"; + updateClassName(element, language, result.language); + element.result = { + language: result.language, + // TODO: remove with version 11.0 + re: result.relevance, + relevance: result.relevance, + }; + if (result.secondBest) { + element.secondBest = { + language: result.secondBest.language, + relevance: result.secondBest.relevance, + }; + } + + fire("after:highlightElement", { el: element, result, text }); + } + + /** + * Updates highlight.js global options with the passed options + * + * @param {Partial} userOptions + */ + function configure(userOptions) { + options = inherit(options, userOptions); + } + + // TODO: remove v12, deprecated + const initHighlighting = () => { + highlightAll(); + deprecated( + "10.6.0", + "initHighlighting() deprecated. Use highlightAll() now.", + ); + }; + + // TODO: remove v12, deprecated + function initHighlightingOnLoad() { + highlightAll(); + deprecated( + "10.6.0", + "initHighlightingOnLoad() deprecated. Use highlightAll() now.", + ); + } + + let wantsHighlight = false; + + /** + * auto-highlights all pre>code elements on the page + */ + function highlightAll() { + // if we are called too early in the loading process + if (document.readyState === "loading") { + wantsHighlight = true; + return; + } + + const blocks = document.querySelectorAll(options.cssSelector); + blocks.forEach(highlightElement); + } + + function boot() { + // if a highlight was requested before DOM was loaded, do now + if (wantsHighlight) highlightAll(); + } + + // make sure we are in the browser environment + if (typeof window !== "undefined" && window.addEventListener) { + window.addEventListener("DOMContentLoaded", boot, false); + } + + /** + * Register a language grammar module + * + * @param {string} languageName + * @param {LanguageFn} languageDefinition + */ + function registerLanguage(languageName, languageDefinition) { + let lang = null; + try { + lang = languageDefinition(hljs); + } catch (error$1) { + error( + "Language definition for '{}' could not be registered.".replace( + "{}", + languageName, + ), + ); + // hard or soft error + if (!SAFE_MODE) throw error$1; + else error(error$1); + // languages that have serious errors are replaced with essentially a + // "plaintext" stand-in so that the code blocks will still get normal + // css classes applied to them - and one bad language won't break the + // entire highlighter + lang = PLAINTEXT_LANGUAGE; + } + // give it a temporary name if it doesn't have one in the meta-data + if (!lang.name) lang.name = languageName; + languages[languageName] = lang; + lang.rawDefinition = languageDefinition.bind(null, hljs); + + if (lang.aliases) { + registerAliases(lang.aliases, { languageName }); + } + } + + /** + * Remove a language grammar module + * + * @param {string} languageName + */ + function unregisterLanguage(languageName) { + delete languages[languageName]; + for (const alias of Object.keys(aliases)) { + if (aliases[alias] === languageName) { + delete aliases[alias]; + } + } + } + + /** + * @returns {string[]} List of language internal names + */ + function listLanguages() { + return Object.keys(languages); + } + + /** + * @param {string} name - name of the language to retrieve + * @returns {Language | undefined} + */ + function getLanguage(name) { + name = (name || "").toLowerCase(); + return languages[name] || languages[aliases[name]]; + } + + /** + * @param {string|string[]} aliasList - single alias or list of aliases + * @param {{languageName: string}} opts + */ + function registerAliases(aliasList, { languageName }) { + if (typeof aliasList === "string") { + aliasList = [aliasList]; + } + aliasList.forEach((alias) => { + aliases[alias.toLowerCase()] = languageName; + }); + } + + /** + * Determines if a given language has auto-detection enabled + * @param {string} name - name of the language + */ + function autoDetection(name) { + const lang = getLanguage(name); + return lang && !lang.disableAutodetect; + } + + /** + * Upgrades the old highlightBlock plugins to the new + * highlightElement API + * @param {HLJSPlugin} plugin + */ + function upgradePluginAPI(plugin) { + // TODO: remove with v12 + if ( + plugin["before:highlightBlock"] && !plugin["before:highlightElement"] + ) { + plugin["before:highlightElement"] = (data) => { + plugin["before:highlightBlock"]( + Object.assign({ block: data.el }, data), + ); + }; + } + if (plugin["after:highlightBlock"] && !plugin["after:highlightElement"]) { + plugin["after:highlightElement"] = (data) => { + plugin["after:highlightBlock"]( + Object.assign({ block: data.el }, data), + ); + }; + } + } + + /** + * @param {HLJSPlugin} plugin + */ + function addPlugin(plugin) { + upgradePluginAPI(plugin); + plugins.push(plugin); + } + + /** + * @param {HLJSPlugin} plugin + */ + function removePlugin(plugin) { + const index = plugins.indexOf(plugin); + if (index !== -1) { + plugins.splice(index, 1); + } + } + + /** + * @param {PluginEvent} event + * @param {any} args + */ + function fire(event, args) { + const cb = event; + plugins.forEach(function (plugin) { + if (plugin[cb]) { + plugin[cb](args); + } + }); + } + + /** + * DEPRECATED + * @param {HighlightedHTMLElement} el + */ + function deprecateHighlightBlock(el) { + deprecated("10.7.0", "highlightBlock will be removed entirely in v12.0"); + deprecated("10.7.0", "Please use highlightElement now."); + + return highlightElement(el); + } + + /* Interface definition */ + Object.assign(hljs, { + highlight, + highlightAuto, + highlightAll, + highlightElement, + // TODO: Remove with v12 API + highlightBlock: deprecateHighlightBlock, + configure, + initHighlighting, + initHighlightingOnLoad, + registerLanguage, + unregisterLanguage, + listLanguages, + getLanguage, + registerAliases, + autoDetection, + inherit, + addPlugin, + removePlugin, + }); + + hljs.debugMode = function () { + SAFE_MODE = false; + }; + hljs.safeMode = function () { + SAFE_MODE = true; + }; + hljs.versionString = version; + + hljs.regex = { + concat: concat, + lookahead: lookahead, + either: either, + optional: optional, + anyNumberOfTimes: anyNumberOfTimes, + }; + + for (const key in MODES) { + // @ts-ignore + if (typeof MODES[key] === "object") { + // @ts-ignore + deepFreeze(MODES[key]); + } + } + + // merge all the modes/regexes into our main object + Object.assign(hljs, MODES); + + return hljs; + }; + + // Other names for the variable may break build script + const highlight = HLJS({}); + + // returns a new instance of the highlighter to be used for extensions + // check https://github.com/wooorm/lowlight/issues/47 + highlight.newInstance = () => HLJS({}); + + return highlight; +})(); +if (typeof exports === "object" && typeof module !== "undefined") { + module.exports = hljs; +} +/*! `lua` grammar compiled for Highlight.js 11.9.0 */ +(function () { + var hljsGrammar = (function () { + "use strict"; + + /* + Language: Lua + Description: Lua is a powerful, efficient, lightweight, embeddable scripting language. + Author: Andrew Fedorov + Category: common, gaming, scripting + Website: https://www.lua.org + */ + + function lua(hljs) { + const OPENING_LONG_BRACKET = "\\[=*\\["; + const CLOSING_LONG_BRACKET = "\\]=*\\]"; + const LONG_BRACKETS = { + begin: OPENING_LONG_BRACKET, + end: CLOSING_LONG_BRACKET, + contains: ["self"], + }; + const COMMENTS = [ + hljs.COMMENT("--(?!" + OPENING_LONG_BRACKET + ")", "$"), + hljs.COMMENT( + "--" + OPENING_LONG_BRACKET, + CLOSING_LONG_BRACKET, + { + contains: [LONG_BRACKETS], + relevance: 10, + }, + ), + ]; + return { + name: "Lua", + keywords: { + $pattern: hljs.UNDERSCORE_IDENT_RE, + literal: "true false nil", + keyword: + "and break do else elseif end for goto if in local not or repeat return then until while", + built_in: + // Metatags and globals: + "_G _ENV _VERSION __index __newindex __mode __call __metatable __tostring __len " + + "__gc __add __sub __mul __div __mod __pow __concat __unm __eq __lt __le assert " + + // Standard methods and properties: + "collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstring " + + "module next pairs pcall print rawequal rawget rawset require select setfenv " + + "setmetatable tonumber tostring type unpack xpcall arg self " + + // Library methods and properties (one line per library): + "coroutine resume yield status wrap create running debug getupvalue " + + "debug sethook getmetatable gethook setmetatable setlocal traceback setfenv getinfo setupvalue getlocal getregistry getfenv " + + "io lines write close flush open output type read stderr stdin input stdout popen tmpfile " + + "math log max acos huge ldexp pi cos tanh pow deg tan cosh sinh random randomseed frexp ceil floor rad abs sqrt modf asin min mod fmod log10 atan2 exp sin atan " + + "os exit setlocale date getenv difftime remove time clock tmpname rename execute package preload loadlib loaded loaders cpath config path seeall " + + "string sub upper len gfind rep find match char dump gmatch reverse byte format gsub lower " + + "table setn insert getn foreachi maxn foreach concat sort remove", + }, + contains: COMMENTS.concat([ + { + className: "function", + beginKeywords: "function", + end: "\\)", + contains: [ + hljs.inherit(hljs.TITLE_MODE, { + begin: "([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*", + }), + { + className: "params", + begin: "\\(", + endsWithParent: true, + contains: COMMENTS, + }, + ].concat(COMMENTS), + }, + hljs.C_NUMBER_MODE, + hljs.APOS_STRING_MODE, + hljs.QUOTE_STRING_MODE, + { + className: "string", + begin: OPENING_LONG_BRACKET, + end: CLOSING_LONG_BRACKET, + contains: [LONG_BRACKETS], + relevance: 5, + }, + ]), + }; + } + + return lua; + })(); + + hljs.registerLanguage("lua", hljsGrammar); +})(); /*! `shell` grammar compiled for Highlight.js 11.9.0 */ +(function () { + var hljsGrammar = (function () { + "use strict"; + + /* + Language: Shell Session + Requires: bash.js + Author: TSUYUSATO Kitsune + Category: common + Audit: 2020 + */ + + /** @type LanguageFn */ + function shell(hljs) { + return { + name: "Shell Session", + aliases: [ + "console", + "shellsession", + ], + contains: [ + { + className: "meta.prompt", + // We cannot add \s (spaces) in the regular expression otherwise it will be too broad and produce unexpected result. + // For instance, in the following example, it would match "echo /path/to/home >" as a prompt: + // echo /path/to/home > t.exe + begin: /^\s{0,3}[/~\w\d[\]()@-]*[>%$#][ ]?/, + starts: { + end: /[^\\](?=\s*$)/, + subLanguage: "bash", + }, + }, + ], + }; + } + + return shell; + })(); + + hljs.registerLanguage("shell", hljsGrammar); +})(); /*! `typescript` grammar compiled for Highlight.js 11.9.0 */ +(function () { + var hljsGrammar = (function () { + "use strict"; + + const IDENT_RE = "[A-Za-z$_][0-9A-Za-z$_]*"; + const KEYWORDS = [ + "as", // for exports + "in", + "of", + "if", + "for", + "while", + "finally", + "var", + "new", + "function", + "do", + "return", + "void", + "else", + "break", + "catch", + "instanceof", + "with", + "throw", + "case", + "default", + "try", + "switch", + "continue", + "typeof", + "delete", + "let", + "yield", + "const", + "class", + // JS handles these with a special rule + // "get", + // "set", + "debugger", + "async", + "await", + "static", + "import", + "from", + "export", + "extends", + ]; + const LITERALS = [ + "true", + "false", + "null", + "undefined", + "NaN", + "Infinity", + ]; + + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects + const TYPES = [ + // Fundamental objects + "Object", + "Function", + "Boolean", + "Symbol", + // numbers and dates + "Math", + "Date", + "Number", + "BigInt", + // text + "String", + "RegExp", + // Indexed collections + "Array", + "Float32Array", + "Float64Array", + "Int8Array", + "Uint8Array", + "Uint8ClampedArray", + "Int16Array", + "Int32Array", + "Uint16Array", + "Uint32Array", + "BigInt64Array", + "BigUint64Array", + // Keyed collections + "Set", + "Map", + "WeakSet", + "WeakMap", + // Structured data + "ArrayBuffer", + "SharedArrayBuffer", + "Atomics", + "DataView", + "JSON", + // Control abstraction objects + "Promise", + "Generator", + "GeneratorFunction", + "AsyncFunction", + // Reflection + "Reflect", + "Proxy", + // Internationalization + "Intl", + // WebAssembly + "WebAssembly", + ]; + + const ERROR_TYPES = [ + "Error", + "EvalError", + "InternalError", + "RangeError", + "ReferenceError", + "SyntaxError", + "TypeError", + "URIError", + ]; + + const BUILT_IN_GLOBALS = [ + "setInterval", + "setTimeout", + "clearInterval", + "clearTimeout", + + "require", + "exports", + + "eval", + "isFinite", + "isNaN", + "parseFloat", + "parseInt", + "decodeURI", + "decodeURIComponent", + "encodeURI", + "encodeURIComponent", + "escape", + "unescape", + ]; + + const BUILT_IN_VARIABLES = [ + "arguments", + "this", + "super", + "console", + "window", + "document", + "localStorage", + "sessionStorage", + "module", + "global", // Node.js + ]; + + const BUILT_INS = [].concat( + BUILT_IN_GLOBALS, + TYPES, + ERROR_TYPES, + ); + + /* + Language: JavaScript + Description: JavaScript (JS) is a lightweight, interpreted, or just-in-time compiled programming language with first-class functions. + Category: common, scripting, web + Website: https://developer.mozilla.org/en-US/docs/Web/JavaScript + */ + + /** @type LanguageFn */ + function javascript(hljs) { + const regex = hljs.regex; + /** + * Takes a string like " { + const tag = "", + end: "", + }; + // to avoid some special cases inside isTrulyOpeningTag + const XML_SELF_CLOSING = /<[A-Za-z0-9\\._:-]+\s*\/>/; + const XML_TAG = { + begin: /<[A-Za-z0-9\\._:-]+/, + end: /\/[A-Za-z0-9\\._:-]+>|\/>/, + /** + * @param {RegExpMatchArray} match + * @param {CallbackResponse} response + */ + isTrulyOpeningTag: (match, response) => { + const afterMatchIndex = match[0].length + match.index; + const nextChar = match.input[afterMatchIndex]; + if ( + // HTML should not include another raw `<` inside a tag + // nested type? + // `>`, etc. + nextChar === "<" || + // the , gives away that this is not HTML + // `` + nextChar === "," + ) { + response.ignoreMatch(); + return; + } + + // `` + // Quite possibly a tag, lets look for a matching closing tag... + if (nextChar === ">") { + // if we cannot find a matching closing tag, then we + // will ignore it + if (!hasClosingTag(match, { after: afterMatchIndex })) { + response.ignoreMatch(); + } + } + + // `` (self-closing) + // handled by simpleSelfClosing rule + + let m; + const afterMatch = match.input.substring(afterMatchIndex); + + // some more template typing stuff + // (key?: string) => Modify< + if ((m = afterMatch.match(/^\s*=/))) { + response.ignoreMatch(); + return; + } + + // `` + // technically this could be HTML, but it smells like a type + // NOTE: This is ugh, but added specifically for https://github.com/highlightjs/highlight.js/issues/3276 + if ((m = afterMatch.match(/^\s+extends\s+/))) { + if (m.index === 0) { + response.ignoreMatch(); + // eslint-disable-next-line no-useless-return + return; + } + } + }, + }; + const KEYWORDS$1 = { + $pattern: IDENT_RE, + keyword: KEYWORDS, + literal: LITERALS, + built_in: BUILT_INS, + "variable.language": BUILT_IN_VARIABLES, + }; + + // https://tc39.es/ecma262/#sec-literals-numeric-literals + const decimalDigits = "[0-9](_?[0-9])*"; + const frac = `\\.(${decimalDigits})`; + // DecimalIntegerLiteral, including Annex B NonOctalDecimalIntegerLiteral + // https://tc39.es/ecma262/#sec-additional-syntax-numeric-literals + const decimalInteger = `0|[1-9](_?[0-9])*|0[0-7]*[89][0-9]*`; + const NUMBER = { + className: "number", + variants: [ + // DecimalLiteral + { + begin: `(\\b(${decimalInteger})((${frac})|\\.)?|(${frac}))` + + `[eE][+-]?(${decimalDigits})\\b`, + }, + { + begin: `\\b(${decimalInteger})\\b((${frac})\\b|\\.)?|(${frac})\\b`, + }, + + // DecimalBigIntegerLiteral + { begin: `\\b(0|[1-9](_?[0-9])*)n\\b` }, + + // NonDecimalIntegerLiteral + { begin: "\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*n?\\b" }, + { begin: "\\b0[bB][0-1](_?[0-1])*n?\\b" }, + { begin: "\\b0[oO][0-7](_?[0-7])*n?\\b" }, + + // LegacyOctalIntegerLiteral (does not include underscore separators) + // https://tc39.es/ecma262/#sec-additional-syntax-numeric-literals + { begin: "\\b0[0-7]+n?\\b" }, + ], + relevance: 0, + }; + + const SUBST = { + className: "subst", + begin: "\\$\\{", + end: "\\}", + keywords: KEYWORDS$1, + contains: [], // defined later + }; + const HTML_TEMPLATE = { + begin: "html`", + end: "", + starts: { + end: "`", + returnEnd: false, + contains: [ + hljs.BACKSLASH_ESCAPE, + SUBST, + ], + subLanguage: "xml", + }, + }; + const CSS_TEMPLATE = { + begin: "css`", + end: "", + starts: { + end: "`", + returnEnd: false, + contains: [ + hljs.BACKSLASH_ESCAPE, + SUBST, + ], + subLanguage: "css", + }, + }; + const GRAPHQL_TEMPLATE = { + begin: "gql`", + end: "", + starts: { + end: "`", + returnEnd: false, + contains: [ + hljs.BACKSLASH_ESCAPE, + SUBST, + ], + subLanguage: "graphql", + }, + }; + const TEMPLATE_STRING = { + className: "string", + begin: "`", + end: "`", + contains: [ + hljs.BACKSLASH_ESCAPE, + SUBST, + ], + }; + const JSDOC_COMMENT = hljs.COMMENT( + /\/\*\*(?!\/)/, + "\\*/", + { + relevance: 0, + contains: [ + { + begin: "(?=@[A-Za-z]+)", + relevance: 0, + contains: [ + { + className: "doctag", + begin: "@[A-Za-z]+", + }, + { + className: "type", + begin: "\\{", + end: "\\}", + excludeEnd: true, + excludeBegin: true, + relevance: 0, + }, + { + className: "variable", + begin: IDENT_RE$1 + "(?=\\s*(-)|$)", + endsParent: true, + relevance: 0, + }, + // eat spaces (not newlines) so we can find + // types or variables + { + begin: /(?=[^\n])\s/, + relevance: 0, + }, + ], + }, + ], + }, + ); + const COMMENT = { + className: "comment", + variants: [ + JSDOC_COMMENT, + hljs.C_BLOCK_COMMENT_MODE, + hljs.C_LINE_COMMENT_MODE, + ], + }; + const SUBST_INTERNALS = [ + hljs.APOS_STRING_MODE, + hljs.QUOTE_STRING_MODE, + HTML_TEMPLATE, + CSS_TEMPLATE, + GRAPHQL_TEMPLATE, + TEMPLATE_STRING, + // Skip numbers when they are part of a variable name + { match: /\$\d+/ }, + NUMBER, + // This is intentional: + // See https://github.com/highlightjs/highlight.js/issues/3288 + // hljs.REGEXP_MODE + ]; + SUBST.contains = SUBST_INTERNALS + .concat({ + // we need to pair up {} inside our subst to prevent + // it from ending too early by matching another } + begin: /\{/, + end: /\}/, + keywords: KEYWORDS$1, + contains: [ + "self", + ].concat(SUBST_INTERNALS), + }); + const SUBST_AND_COMMENTS = [].concat(COMMENT, SUBST.contains); + const PARAMS_CONTAINS = SUBST_AND_COMMENTS.concat([ + // eat recursive parens in sub expressions + { + begin: /\(/, + end: /\)/, + keywords: KEYWORDS$1, + contains: ["self"].concat(SUBST_AND_COMMENTS), + }, + ]); + const PARAMS = { + className: "params", + begin: /\(/, + end: /\)/, + excludeBegin: true, + excludeEnd: true, + keywords: KEYWORDS$1, + contains: PARAMS_CONTAINS, + }; + + // ES6 classes + const CLASS_OR_EXTENDS = { + variants: [ + // class Car extends vehicle + { + match: [ + /class/, + /\s+/, + IDENT_RE$1, + /\s+/, + /extends/, + /\s+/, + regex.concat( + IDENT_RE$1, + "(", + regex.concat(/\./, IDENT_RE$1), + ")*", + ), + ], + scope: { + 1: "keyword", + 3: "title.class", + 5: "keyword", + 7: "title.class.inherited", + }, + }, + // class Car + { + match: [ + /class/, + /\s+/, + IDENT_RE$1, + ], + scope: { + 1: "keyword", + 3: "title.class", + }, + }, + ], + }; + + const CLASS_REFERENCE = { + relevance: 0, + match: regex.either( + // Hard coded exceptions + /\bJSON/, + // Float32Array, OutT + /\b[A-Z][a-z]+([A-Z][a-z]*|\d)*/, + // CSSFactory, CSSFactoryT + /\b[A-Z]{2,}([A-Z][a-z]+|\d)+([A-Z][a-z]*)*/, + // FPs, FPsT + /\b[A-Z]{2,}[a-z]+([A-Z][a-z]+|\d)*([A-Z][a-z]*)*/, + // P + // single letters are not highlighted + // BLAH + // this will be flagged as a UPPER_CASE_CONSTANT instead + ), + className: "title.class", + keywords: { + _: [ + // se we still get relevance credit for JS library classes + ...TYPES, + ...ERROR_TYPES, + ], + }, + }; + + const USE_STRICT = { + label: "use_strict", + className: "meta", + relevance: 10, + begin: /^\s*['"]use (strict|asm)['"]/, + }; + + const FUNCTION_DEFINITION = { + variants: [ + { + match: [ + /function/, + /\s+/, + IDENT_RE$1, + /(?=\s*\()/, + ], + }, + // anonymous function + { + match: [ + /function/, + /\s*(?=\()/, + ], + }, + ], + className: { + 1: "keyword", + 3: "title.function", + }, + label: "func.def", + contains: [PARAMS], + illegal: /%/, + }; + + const UPPER_CASE_CONSTANT = { + relevance: 0, + match: /\b[A-Z][A-Z_0-9]+\b/, + className: "variable.constant", + }; + + function noneOf(list) { + return regex.concat("(?!", list.join("|"), ")"); + } + + const FUNCTION_CALL = { + match: regex.concat( + /\b/, + noneOf([ + ...BUILT_IN_GLOBALS, + "super", + "import", + ]), + IDENT_RE$1, + regex.lookahead(/\(/), + ), + className: "title.function", + relevance: 0, + }; + + const PROPERTY_ACCESS = { + begin: regex.concat( + /\./, + regex.lookahead( + regex.concat(IDENT_RE$1, /(?![0-9A-Za-z$_(])/), + ), + ), + end: IDENT_RE$1, + excludeBegin: true, + keywords: "prototype", + className: "property", + relevance: 0, + }; + + const GETTER_OR_SETTER = { + match: [ + /get|set/, + /\s+/, + IDENT_RE$1, + /(?=\()/, + ], + className: { + 1: "keyword", + 3: "title.function", + }, + contains: [ + { // eat to avoid empty params + begin: /\(\)/, + }, + PARAMS, + ], + }; + + const FUNC_LEAD_IN_RE = "(\\(" + + "[^()]*(\\(" + + "[^()]*(\\(" + + "[^()]*" + + "\\)[^()]*)*" + + "\\)[^()]*)*" + + "\\)|" + hljs.UNDERSCORE_IDENT_RE + ")\\s*=>"; + + const FUNCTION_VARIABLE = { + match: [ + /const|var|let/, + /\s+/, + IDENT_RE$1, + /\s*/, + /=\s*/, + /(async\s*)?/, // async is optional + regex.lookahead(FUNC_LEAD_IN_RE), + ], + keywords: "async", + className: { + 1: "keyword", + 3: "title.function", + }, + contains: [ + PARAMS, + ], + }; + + return { + name: "JavaScript", + aliases: ["js", "jsx", "mjs", "cjs"], + keywords: KEYWORDS$1, + // this will be extended by TypeScript + exports: { PARAMS_CONTAINS, CLASS_REFERENCE }, + illegal: /#(?![$_A-z])/, + contains: [ + hljs.SHEBANG({ + label: "shebang", + binary: "node", + relevance: 5, + }), + USE_STRICT, + hljs.APOS_STRING_MODE, + hljs.QUOTE_STRING_MODE, + HTML_TEMPLATE, + CSS_TEMPLATE, + GRAPHQL_TEMPLATE, + TEMPLATE_STRING, + COMMENT, + // Skip numbers when they are part of a variable name + { match: /\$\d+/ }, + NUMBER, + CLASS_REFERENCE, + { + className: "attr", + begin: IDENT_RE$1 + regex.lookahead(":"), + relevance: 0, + }, + FUNCTION_VARIABLE, + { // "value" container + begin: "(" + hljs.RE_STARTERS_RE + + "|\\b(case|return|throw)\\b)\\s*", + keywords: "return throw case", + relevance: 0, + contains: [ + COMMENT, + hljs.REGEXP_MODE, + { + className: "function", + // we have to count the parens to make sure we actually have the + // correct bounding ( ) before the =>. There could be any number of + // sub-expressions inside also surrounded by parens. + begin: FUNC_LEAD_IN_RE, + returnBegin: true, + end: "\\s*=>", + contains: [ + { + className: "params", + variants: [ + { + begin: hljs.UNDERSCORE_IDENT_RE, + relevance: 0, + }, + { + className: null, + begin: /\(\s*\)/, + skip: true, + }, + { + begin: /\(/, + end: /\)/, + excludeBegin: true, + excludeEnd: true, + keywords: KEYWORDS$1, + contains: PARAMS_CONTAINS, + }, + ], + }, + ], + }, + { // could be a comma delimited list of params to a function call + begin: /,/, + relevance: 0, + }, + { + match: /\s+/, + relevance: 0, + }, + { // JSX + variants: [ + { begin: FRAGMENT.begin, end: FRAGMENT.end }, + { match: XML_SELF_CLOSING }, + { + begin: XML_TAG.begin, + // we carefully check the opening tag to see if it truly + // is a tag and not a false positive + "on:begin": XML_TAG.isTrulyOpeningTag, + end: XML_TAG.end, + }, + ], + subLanguage: "xml", + contains: [ + { + begin: XML_TAG.begin, + end: XML_TAG.end, + skip: true, + contains: ["self"], + }, + ], + }, + ], + }, + FUNCTION_DEFINITION, + { + // prevent this from getting swallowed up by function + // since they appear "function like" + beginKeywords: "while if switch catch for", + }, + { + // we have to count the parens to make sure we actually have the correct + // bounding ( ). There could be any number of sub-expressions inside + // also surrounded by parens. + begin: "\\b(?!function)" + hljs.UNDERSCORE_IDENT_RE + + "\\(" + // first parens + "[^()]*(\\(" + + "[^()]*(\\(" + + "[^()]*" + + "\\)[^()]*)*" + + "\\)[^()]*)*" + + "\\)\\s*\\{", // end parens + returnBegin: true, + label: "func.def", + contains: [ + PARAMS, + hljs.inherit(hljs.TITLE_MODE, { + begin: IDENT_RE$1, + className: "title.function", + }), + ], + }, + // catch ... so it won't trigger the property rule below + { + match: /\.\.\./, + relevance: 0, + }, + PROPERTY_ACCESS, + // hack: prevents detection of keywords in some circumstances + // .keyword() + // $keyword = x + { + match: "\\$" + IDENT_RE$1, + relevance: 0, + }, + { + match: [/\bconstructor(?=\s*\()/], + className: { 1: "title.function" }, + contains: [PARAMS], + }, + FUNCTION_CALL, + UPPER_CASE_CONSTANT, + CLASS_OR_EXTENDS, + GETTER_OR_SETTER, + { + match: /\$[(.]/, // relevance booster for a pattern common to JS libs: `$(something)` and `$.something` + }, + ], + }; + } + + /* + Language: TypeScript + Author: Panu Horsmalahti + Contributors: Ike Ku + Description: TypeScript is a strict superset of JavaScript + Website: https://www.typescriptlang.org + Category: common, scripting + */ + + /** @type LanguageFn */ + function typescript(hljs) { + const tsLanguage = javascript(hljs); + + const IDENT_RE$1 = IDENT_RE; + const TYPES = [ + "any", + "void", + "number", + "boolean", + "string", + "object", + "never", + "symbol", + "bigint", + "unknown", + ]; + const NAMESPACE = { + beginKeywords: "namespace", + end: /\{/, + excludeEnd: true, + contains: [tsLanguage.exports.CLASS_REFERENCE], + }; + const INTERFACE = { + beginKeywords: "interface", + end: /\{/, + excludeEnd: true, + keywords: { + keyword: "interface extends", + built_in: TYPES, + }, + contains: [tsLanguage.exports.CLASS_REFERENCE], + }; + const USE_STRICT = { + className: "meta", + relevance: 10, + begin: /^\s*['"]use strict['"]/, + }; + const TS_SPECIFIC_KEYWORDS = [ + "type", + "namespace", + "interface", + "public", + "private", + "protected", + "implements", + "declare", + "abstract", + "readonly", + "enum", + "override", + ]; + const KEYWORDS$1 = { + $pattern: IDENT_RE, + keyword: KEYWORDS.concat(TS_SPECIFIC_KEYWORDS), + literal: LITERALS, + built_in: BUILT_INS.concat(TYPES), + "variable.language": BUILT_IN_VARIABLES, + }; + const DECORATOR = { + className: "meta", + begin: "@" + IDENT_RE$1, + }; + + const swapMode = (mode, label, replacement) => { + const indx = mode.contains.findIndex((m) => m.label === label); + if (indx === -1) throw new Error("can not find mode to replace"); + + mode.contains.splice(indx, 1, replacement); + }; + + // this should update anywhere keywords is used since + // it will be the same actual JS object + Object.assign(tsLanguage.keywords, KEYWORDS$1); + + tsLanguage.exports.PARAMS_CONTAINS.push(DECORATOR); + tsLanguage.contains = tsLanguage.contains.concat([ + DECORATOR, + NAMESPACE, + INTERFACE, + ]); + + // TS gets a simpler shebang rule than JS + swapMode(tsLanguage, "shebang", hljs.SHEBANG()); + // JS use strict rule purposely excludes `asm` which makes no sense + swapMode(tsLanguage, "use_strict", USE_STRICT); + + const functionDeclaration = tsLanguage.contains.find((m) => + m.label === "func.def" + ); + functionDeclaration.relevance = 0; // () => {} is more typical in TypeScript + + Object.assign(tsLanguage, { + name: "TypeScript", + aliases: [ + "ts", + "tsx", + "mts", + "cts", + ], + }); + + return tsLanguage; + } + + return typescript; + })(); + + hljs.registerLanguage("typescript", hljsGrammar); +})(); /*! `vim` grammar compiled for Highlight.js 11.9.0 */ +(function () { + var hljsGrammar = (function () { + "use strict"; + + /* + Language: Vim Script + Author: Jun Yang + Description: full keyword and built-in from http://vimdoc.sourceforge.net/htmldoc/ + Website: https://www.vim.org + Category: scripting + */ + + function vim(hljs) { + return { + name: "Vim Script", + keywords: { + $pattern: /[!#@\w]+/, + keyword: + // express version except: ! & * < = > !! # @ @@ + "N|0 P|0 X|0 a|0 ab abc abo al am an|0 ar arga argd arge argdo argg argl argu as au aug aun b|0 bN ba bad bd be bel bf bl bm bn bo bp br brea breaka breakd breakl bro bufdo buffers bun bw c|0 cN cNf ca cabc caddb cad caddf cal cat cb cc ccl cd ce cex cf cfir cgetb cgete cg changes chd che checkt cl cla clo cm cmapc cme cn cnew cnf cno cnorea cnoreme co col colo com comc comp con conf cope " + + "cp cpf cq cr cs cst cu cuna cunme cw delm deb debugg delc delf dif diffg diffo diffp diffpu diffs diffthis dig di dl dell dj dli do doautoa dp dr ds dsp e|0 ea ec echoe echoh echom echon el elsei em en endfo endf endt endw ene ex exe exi exu f|0 files filet fin fina fini fir fix fo foldc foldd folddoc foldo for fu go gr grepa gu gv ha helpf helpg helpt hi hid his ia iabc if ij il im imapc " + + "ime ino inorea inoreme int is isp iu iuna iunme j|0 ju k|0 keepa kee keepj lN lNf l|0 lad laddb laddf la lan lat lb lc lch lcl lcs le lefta let lex lf lfir lgetb lgete lg lgr lgrepa lh ll lla lli lmak lm lmapc lne lnew lnf ln loadk lo loc lockv lol lope lp lpf lr ls lt lu lua luad luaf lv lvimgrepa lw m|0 ma mak map mapc marks mat me menut mes mk mks mksp mkv mkvie mod mz mzf nbc nb nbs new nm nmapc nme nn nnoreme noa no noh norea noreme norm nu nun nunme ol o|0 om omapc ome on ono onoreme opt ou ounme ow p|0 " + + "profd prof pro promptr pc ped pe perld po popu pp pre prev ps pt ptN ptf ptj ptl ptn ptp ptr pts pu pw py3 python3 py3d py3f py pyd pyf quita qa rec red redi redr redraws reg res ret retu rew ri rightb rub rubyd rubyf rund ru rv sN san sa sal sav sb sbN sba sbf sbl sbm sbn sbp sbr scrip scripte scs se setf setg setl sf sfir sh sim sig sil sl sla sm smap smapc sme sn sni sno snor snoreme sor " + + "so spelld spe spelli spellr spellu spellw sp spr sre st sta startg startr star stopi stj sts sun sunm sunme sus sv sw sy synti sync tN tabN tabc tabdo tabe tabf tabfir tabl tabm tabnew " + + "tabn tabo tabp tabr tabs tab ta tags tc tcld tclf te tf th tj tl tm tn to tp tr try ts tu u|0 undoj undol una unh unl unlo unm unme uns up ve verb vert vim vimgrepa vi viu vie vm vmapc vme vne vn vnoreme vs vu vunme windo w|0 wN wa wh wi winc winp wn wp wq wqa ws wu wv x|0 xa xmapc xm xme xn xnoreme xu xunme y|0 z|0 ~ " + + // full version + "Next Print append abbreviate abclear aboveleft all amenu anoremenu args argadd argdelete argedit argglobal arglocal argument ascii autocmd augroup aunmenu buffer bNext ball badd bdelete behave belowright bfirst blast bmodified bnext botright bprevious brewind break breakadd breakdel breaklist browse bunload " + + "bwipeout change cNext cNfile cabbrev cabclear caddbuffer caddexpr caddfile call catch cbuffer cclose center cexpr cfile cfirst cgetbuffer cgetexpr cgetfile chdir checkpath checktime clist clast close cmap cmapclear cmenu cnext cnewer cnfile cnoremap cnoreabbrev cnoremenu copy colder colorscheme command comclear compiler continue confirm copen cprevious cpfile cquit crewind cscope cstag cunmap " + + "cunabbrev cunmenu cwindow delete delmarks debug debuggreedy delcommand delfunction diffupdate diffget diffoff diffpatch diffput diffsplit digraphs display deletel djump dlist doautocmd doautoall deletep drop dsearch dsplit edit earlier echo echoerr echohl echomsg else elseif emenu endif endfor " + + "endfunction endtry endwhile enew execute exit exusage file filetype find finally finish first fixdel fold foldclose folddoopen folddoclosed foldopen function global goto grep grepadd gui gvim hardcopy help helpfind helpgrep helptags highlight hide history insert iabbrev iabclear ijump ilist imap " + + "imapclear imenu inoremap inoreabbrev inoremenu intro isearch isplit iunmap iunabbrev iunmenu join jumps keepalt keepmarks keepjumps lNext lNfile list laddexpr laddbuffer laddfile last language later lbuffer lcd lchdir lclose lcscope left leftabove lexpr lfile lfirst lgetbuffer lgetexpr lgetfile lgrep lgrepadd lhelpgrep llast llist lmake lmap lmapclear lnext lnewer lnfile lnoremap loadkeymap loadview " + + "lockmarks lockvar lolder lopen lprevious lpfile lrewind ltag lunmap luado luafile lvimgrep lvimgrepadd lwindow move mark make mapclear match menu menutranslate messages mkexrc mksession mkspell mkvimrc mkview mode mzscheme mzfile nbclose nbkey nbsart next nmap nmapclear nmenu nnoremap " + + "nnoremenu noautocmd noremap nohlsearch noreabbrev noremenu normal number nunmap nunmenu oldfiles open omap omapclear omenu only onoremap onoremenu options ounmap ounmenu ownsyntax print profdel profile promptfind promptrepl pclose pedit perl perldo pop popup ppop preserve previous psearch ptag ptNext " + + "ptfirst ptjump ptlast ptnext ptprevious ptrewind ptselect put pwd py3do py3file python pydo pyfile quit quitall qall read recover redo redir redraw redrawstatus registers resize retab return rewind right rightbelow ruby rubydo rubyfile rundo runtime rviminfo substitute sNext sandbox sargument sall saveas sbuffer sbNext sball sbfirst sblast sbmodified sbnext sbprevious sbrewind scriptnames scriptencoding " + + "scscope set setfiletype setglobal setlocal sfind sfirst shell simalt sign silent sleep slast smagic smapclear smenu snext sniff snomagic snoremap snoremenu sort source spelldump spellgood spellinfo spellrepall spellundo spellwrong split sprevious srewind stop stag startgreplace startreplace " + + "startinsert stopinsert stjump stselect sunhide sunmap sunmenu suspend sview swapname syntax syntime syncbind tNext tabNext tabclose tabedit tabfind tabfirst tablast tabmove tabnext tabonly tabprevious tabrewind tag tcl tcldo tclfile tearoff tfirst throw tjump tlast tmenu tnext topleft tprevious " + + "trewind tselect tunmenu undo undojoin undolist unabbreviate unhide unlet unlockvar unmap unmenu unsilent update vglobal version verbose vertical vimgrep vimgrepadd visual viusage view vmap vmapclear vmenu vnew " + + "vnoremap vnoremenu vsplit vunmap vunmenu write wNext wall while winsize wincmd winpos wnext wprevious wqall wsverb wundo wviminfo xit xall xmapclear xmap xmenu xnoremap xnoremenu xunmap xunmenu yank", + built_in: // built in func + "synIDtrans atan2 range matcharg did_filetype asin feedkeys xor argv " + + "complete_check add getwinposx getqflist getwinposy screencol " + + "clearmatches empty extend getcmdpos mzeval garbagecollect setreg " + + "ceil sqrt diff_hlID inputsecret get getfperm getpid filewritable " + + "shiftwidth max sinh isdirectory synID system inputrestore winline " + + "atan visualmode inputlist tabpagewinnr round getregtype mapcheck " + + "hasmapto histdel argidx findfile sha256 exists toupper getcmdline " + + "taglist string getmatches bufnr strftime winwidth bufexists " + + "strtrans tabpagebuflist setcmdpos remote_read printf setloclist " + + "getpos getline bufwinnr float2nr len getcmdtype diff_filler luaeval " + + "resolve libcallnr foldclosedend reverse filter has_key bufname " + + "str2float strlen setline getcharmod setbufvar index searchpos " + + "shellescape undofile foldclosed setqflist buflisted strchars str2nr " + + "virtcol floor remove undotree remote_expr winheight gettabwinvar " + + "reltime cursor tabpagenr finddir localtime acos getloclist search " + + "tanh matchend rename gettabvar strdisplaywidth type abs py3eval " + + "setwinvar tolower wildmenumode log10 spellsuggest bufloaded " + + "synconcealed nextnonblank server2client complete settabwinvar " + + "executable input wincol setmatches getftype hlID inputsave " + + "searchpair or screenrow line settabvar histadd deepcopy strpart " + + "remote_peek and eval getftime submatch screenchar winsaveview " + + "matchadd mkdir screenattr getfontname libcall reltimestr getfsize " + + "winnr invert pow getbufline byte2line soundfold repeat fnameescape " + + "tagfiles sin strwidth spellbadword trunc maparg log lispindent " + + "hostname setpos globpath remote_foreground getchar synIDattr " + + "fnamemodify cscope_connection stridx winbufnr indent min " + + "complete_add nr2char searchpairpos inputdialog values matchlist " + + "items hlexists strridx browsedir expand fmod pathshorten line2byte " + + "argc count getwinvar glob foldtextresult getreg foreground cosh " + + "matchdelete has char2nr simplify histget searchdecl iconv " + + "winrestcmd pumvisible writefile foldlevel haslocaldir keys cos " + + "matchstr foldtext histnr tan tempname getcwd byteidx getbufvar " + + "islocked escape eventhandler remote_send serverlist winrestview " + + "synstack pyeval prevnonblank readfile cindent filereadable changenr " + + "exp", + }, + illegal: /;/, + contains: [ + hljs.NUMBER_MODE, + { + className: "string", + begin: "'", + end: "'", + illegal: "\\n", + }, + + /* + A double quote can start either a string or a line comment. Strings are + ended before the end of a line by another double quote and can contain + escaped double-quotes and post-escaped line breaks. + + Also, any double quote at the beginning of a line is a comment but we + don't handle that properly at the moment: any double quote inside will + turn them into a string. Handling it properly will require a smarter + parser. + */ + { + className: "string", + begin: /"(\\"|\n\\|[^"\n])*"/, + }, + hljs.COMMENT('"', "$"), + + { + className: "variable", + begin: /[bwtglsav]:[\w\d_]+/, + }, + { + begin: [ + /\b(?:function|function!)/, + /\s+/, + hljs.IDENT_RE, + ], + className: { + 1: "keyword", + 3: "title", + }, + end: "$", + relevance: 0, + contains: [ + { + className: "params", + begin: "\\(", + end: "\\)", + }, + ], + }, + { + className: "symbol", + begin: /<[\w-]+>/, + }, + ], + }; + } + + return vim; + })(); + + hljs.registerLanguage("vim", hljsGrammar); +})();