├── .github └── workflows │ ├── release.yml │ └── validate.yml ├── .gitignore ├── .vscode └── launch.json ├── .vscodeignore ├── LICENSE ├── README.md ├── build.mjs ├── client ├── client.ts ├── commands.ts ├── extension.ts ├── index.ts ├── package-lock.json ├── package.json ├── partials.ts ├── preview.ts ├── server.ts ├── status.ts └── utils.ts ├── language-configuration.json ├── logo.png ├── package-lock.json ├── package.json ├── server ├── index.ts ├── package-lock.json ├── package.json ├── plugins │ ├── codeaction.ts │ ├── completion.ts │ ├── definition.test.ts │ ├── definition.ts │ ├── dependencies.ts │ ├── folding.test.ts │ ├── folding.ts │ ├── formatting.test.ts │ ├── formatting.ts │ ├── index.ts │ ├── link.ts │ ├── linkedEdit.ts │ ├── range.test.ts │ ├── range.ts │ ├── symbols.ts │ ├── validation.ts │ └── watch.ts ├── server.ts ├── services │ ├── commands.test.ts │ ├── commands.ts │ ├── documents.test.ts │ ├── documents.ts │ ├── index.ts │ ├── scanner.test.ts │ ├── scanner.ts │ ├── schema.test.ts │ ├── schema.ts │ ├── watcher.test.ts │ └── watcher.ts ├── test │ ├── content │ │ ├── file-1.md │ │ ├── file-2.md │ │ └── partials │ │ │ └── part.md │ └── schemas │ │ ├── example-1.js │ │ ├── example-1.mjs │ │ ├── example-2.js │ │ └── example-2.mjs ├── types.ts ├── utils.ts └── wrapper.ts ├── syntaxes ├── markdoc.markdown.tmLanguage.json └── markdoc.tmLanguage.json └── tsconfig.json /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish package to NPM 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version: 20 15 | registry-url: "https://registry.npmjs.org" 16 | cache: "npm" 17 | - run: npm ci 18 | working-directory: client 19 | - run: npm ci 20 | working-directory: server 21 | - run: npm ci 22 | - run: npm run build 23 | - run: npx vsce publish -p ${{ secrets.VSCODE_MARKET_TOKEN }} 24 | - run: cp -r dist/server server/dist # Move server build into server dir 25 | - run: cp README.md server 26 | - run: npm publish --access public 27 | working-directory: ./server 28 | env: 29 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 30 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | types: [opened, synchronize] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: 20 18 | cache: "npm" 19 | - run: npm ci 20 | working-directory: client 21 | - run: npm ci 22 | working-directory: server 23 | - name: npm install, build, and test 24 | run: | 25 | npm ci 26 | npm run build 27 | npm run build:types 28 | npm run test 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | node_modules 4 | .DS_Store 5 | .node-version 6 | .vscode/settings.json -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "extensionHost", 9 | "request": "launch", 10 | "name": "Launch Client", 11 | "runtimeExecutable": "${execPath}", 12 | "args": ["--extensionDevelopmentPath=${workspaceRoot}"], 13 | }, 14 | { 15 | "type": "node", 16 | "request": "attach", 17 | "name": "Attach to Server", 18 | "port": 6009, 19 | "restart": true, 20 | }, 21 | ], 22 | "compounds": [ 23 | { 24 | "name": "Client + Server", 25 | "configurations": ["Launch Client", "Attach to Server"] 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | **/* 2 | !dist/client/index.js 3 | !dist/client/server.js 4 | !language-configuration.json 5 | !LICENSE 6 | !README.md 7 | !logo.png 8 | !package.json 9 | !syntaxes 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2021- Stripe, Inc. (https://stripe.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Markdoc language server 2 | 3 | This is the official Visual Studio Code extension and language server for the [Markdoc](https://markdoc.dev/) authoring framework. 4 | 5 | When the language server and extension are used together and configured to load a Markdoc schema, they support the following features: 6 | 7 | - Syntax highlighting for Markdoc tags inside of Markdown content 8 | - Autocompletion of Markdoc tags and attribute values declared in the schema 9 | - Autocompletion of links based on routes declared in document frontmatter 10 | - Clicking through partial tags and document links to open the corresponding file 11 | - "Peeking" at the underlying file for a partial tag inside of a document 12 | - Identifying all of the references to a partial to show where it is used 13 | - Code folding for content ranges within block-level Markdoc tags 14 | - Formatting ranges and whole documents using Markdoc's built-in formatter 15 | - Validating the document against the user's schema and displaying inline error indicators 16 | - Displaying error messages for malformed Markdoc tag syntax and structural errors like unmatched opening and closing tags 17 | - Creating new Markdoc files from user-defined templates via the new file menu 18 | - Linked editing for matched opening and closing tag names 19 | 20 | ## Configuration quickstart 21 | 22 | After installing the Markdoc extension in Visual Studio Code, create a Markdoc language server configuration file. The extension looks for a file called `markdoc.config.json` in your workspace root, but you can customize this in the extension's settings. 23 | 24 | The JSON configuration file consists of an array of server instance descriptions. The following example demonstrates how to create a basic Markdoc language server configuration: 25 | 26 | ```json 27 | [ 28 | { 29 | "id": "my-site", 30 | "path": "docs/content", 31 | "schema": { 32 | "path": "docs/dist/schema.js", 33 | "type": "node", 34 | "property": "default", 35 | "watch": true 36 | }, 37 | "routing": { 38 | "frontmatter": "route" 39 | } 40 | } 41 | ] 42 | ``` 43 | 44 | - The `id` property is a string that uniquely identifies the server configuration. In Visual Studio Code, the extension displays this in the status bar when a file from the configuration has focus 45 | - The `path` property is a string that contains the path to the base directory where your Markdoc content is stored, relative to your workspace root directory 46 | - The `schema` property is an object that describes the location of your [Markdoc schema](https://markdoc.dev/docs/config) and how to load it from the filesystem 47 | - The `path` property is string that contains the path to the schema module, relative to the workspace root directory 48 | - The `type` property is a string that must be either `"node"` or `"esm"`. This property indicates whether the schema module uses node's `module.exports` convention or the standards-based ECMAScript modules `export` syntax 49 | - The `property` property is the name of the property exported by the module that contains the schema. 50 | - When this property is omitted and the type is set to `"node"`, the extension automatically assumes that the schema itself is the top-level value of `module.exports`. 51 | - When the property is omitted and the type is set to `"esm"`, the extension automatically assumes the schema itself is the default export (e.g. `export default {}`) 52 | - The `watch` property is a boolean value that, when set to `true`, configures the extension to monitor the schema file for changes and reload it automatically 53 | - The `routing` property is an optional object that describes your project's routing configuration 54 | - The `frontmatter` property is a string that tells the extension which property in the Markdoc file's YAML frontmatter contains the URL route associated with the file 55 | 56 | ### Standalone Server Configuration 57 | 58 | When using the server standalone without the client VS Code extension you can pass the configuration object to your LSP client like so: 59 | 60 | ```json 61 | { 62 | "root": "/path/to/project/root", 63 | "path": "relative/path/to/markdoc/files", 64 | "config": { 65 | "root": "/path/to/project/root", 66 | "path": "relative/path/to/markdoc/files" 67 | } 68 | } 69 | ``` 70 | 71 | Invoke the server with `markdoc-ls --stdio` from within your LSP client. 72 | 73 | ### File extensions 74 | 75 | In order to distinguish Markdoc files from Markdown files, the Visual Studio Code extension expects Markdoc files to use one of the following file extensions: `.markdoc`, `.markdoc.md`, or `.mdoc`. 76 | 77 | It does **not** recognize `.md` files by default. If you would like to still use a `.md` extension, you can instead modify your Visual Studio Code workspace configuration with a custom file association. Add the following content to your `.vscode/settings.json` file: 78 | 79 | ``` 80 | { 81 | "files.associations": { 82 | "*.md": "markdoc" 83 | } 84 | } 85 | ``` 86 | 87 | ### Advanced configuration 88 | 89 | It is possible to have multiple Markdoc configurations for the same workspace by adding additional configuration objects to the top-level array. This is useful in cases where you have multiple websites with different schemas under different subdirectories of the same workspace. For example, you might want separate configurations for narrative documentation and an API reference. 90 | 91 | In [multi-root workspaces](https://code.visualstudio.com/docs/editor/multi-root-workspaces), a Markdoc configuration file is specific to an individual workspace root. You can have separate Markdoc configuration files for each root. If you need to override the location of the Markdoc language server configuration file in a multi-root workspace, you can use a [folder setting](https://code.visualstudio.com/docs/editor/multi-root-workspaces#_settings) to customize this behavior per root. 92 | 93 | ## Extending the language server with custom functionality 94 | 95 | The language server is published as a [package on npm](https://www.npmjs.com/package/@markdoc/language-server) in order to support extensibility and customization. You can tailor the functionality to your needs by adding plugins or creating subclasses that substitute built-in plugins. Support for this is somewhat experimental and the APIs exposed by the package are still subject to change. We will not guarantee API stability or backwards compatibility for language server plugins until the 1.0 release. 96 | 97 | ## Contributing 98 | 99 | Contributions and feedback are welcomed and encouraged. Feel free to open PRs here, or open issues and discussion threads in the [Markdoc core repo](https://github.com/markdoc/markdoc). 100 | 101 | ### Building from source 102 | 103 | ``` 104 | $ npm install 105 | $ (cd server && npm install) && (cd client && npm install) 106 | $ npm run build 107 | $ npm run build:types 108 | $ npm run build:extension 109 | ``` 110 | 111 | ### Running unit tests 112 | 113 | The test suite relies on the 'node:test' module that is only included in Node.js 18.x or higher. 114 | 115 | ``` 116 | $ npm run test 117 | ``` 118 | 119 | ## License 120 | 121 | This project uses the [MIT license](LICENSE). 122 | -------------------------------------------------------------------------------- /build.mjs: -------------------------------------------------------------------------------- 1 | import {context, build} from 'esbuild'; 2 | 3 | const config = { 4 | bundle: true, 5 | entryPoints: ['client/index.ts', 'server/index.ts', 'server/wrapper.ts', 'client/server.ts'], 6 | outdir: 'dist', 7 | sourcemap: 'linked', 8 | external: ['vscode'], 9 | platform: 'node', 10 | format: 'cjs', 11 | banner: { js: '#!/usr/bin/env node' }, 12 | }; 13 | 14 | if (process.argv.includes('--watch')) { 15 | const ctx = await context(config); 16 | await ctx.watch(); 17 | } else build(config); 18 | -------------------------------------------------------------------------------- /client/client.ts: -------------------------------------------------------------------------------- 1 | import * as VSC from "vscode"; 2 | import * as LSP from "vscode-languageclient/node"; 3 | import * as utils from "./utils"; 4 | import pathutil from "path"; 5 | 6 | import type { DependencyInfo, Config } from "../server/types"; 7 | 8 | type WorkProgress = 9 | | LSP.WorkDoneProgressBegin 10 | | LSP.WorkDoneProgressReport 11 | | LSP.WorkDoneProgressEnd; 12 | 13 | export type TemplatePickerItem = VSC.QuickPickItem & { uri?: VSC.Uri }; 14 | 15 | export enum ClientState { 16 | NotStarted, 17 | Initializing, 18 | Running, 19 | Stopped, 20 | Disabled, 21 | } 22 | 23 | export default class MarkdocClient implements VSC.Disposable { 24 | private readonly stateDidChange = new VSC.EventEmitter(); 25 | private readonly disposables: VSC.Disposable[] = []; 26 | private templates?: VSC.Uri[]; 27 | private client?: LSP.LanguageClient; 28 | #state = ClientState.NotStarted; 29 | 30 | readonly onStateDidChange = this.stateDidChange.event; 31 | readonly id: string; 32 | readonly uri: VSC.Uri; 33 | 34 | constructor( 35 | readonly root: VSC.WorkspaceFolder, 36 | private config: Omit, 37 | private context: VSC.ExtensionContext 38 | ) { 39 | this.id = config.id ?? "MarkdocLanguageServer"; 40 | this.uri = VSC.Uri.joinPath(root.uri, config.path); 41 | } 42 | 43 | get state() { 44 | return this.#state; 45 | } 46 | 47 | protected set state(value: ClientState) { 48 | this.#state = value; 49 | this.stateDidChange.fire(value); 50 | } 51 | 52 | disable() { 53 | this.state = ClientState.Disabled; 54 | return this.stop(); 55 | } 56 | 57 | enable() { 58 | this.state = ClientState.Stopped; 59 | return this.start(); 60 | } 61 | 62 | start(): Promise | undefined { 63 | if ([ClientState.NotStarted, ClientState.Stopped].includes(this.state)) 64 | return new Promise((resolve) => this.createClient(resolve)); 65 | } 66 | 67 | stop() { 68 | this.dispose(); 69 | return this.client?.stop(); 70 | } 71 | 72 | canPreview() { 73 | return this.state == ClientState.Running && this.config.preview; 74 | } 75 | 76 | dispose() { 77 | this.disposables.forEach((d) => d.dispose()); 78 | } 79 | 80 | showOutputChannel() { 81 | this.client?.outputChannel.show(); 82 | } 83 | 84 | private async options() { 85 | const { scheme, fsPath } = this.root.uri; 86 | const config = { root: fsPath, ...this.config }; 87 | let serverPath = this.context.asAbsolutePath("dist/client/server.js"); 88 | 89 | if (config.server?.path) { 90 | const path = pathutil.join(fsPath, config.server?.path); 91 | 92 | try { 93 | await VSC.workspace.fs.stat(VSC.Uri.file(path)); 94 | serverPath = path; 95 | } 96 | catch (err) { 97 | console.log('Could not load server:', err); 98 | } 99 | } 100 | 101 | const run: LSP.NodeModule = { module: serverPath, transport: LSP.TransportKind.ipc }; 102 | const server: LSP.ServerOptions = { 103 | run, 104 | debug: { ...run, options: { execArgv: ["--nolazy", "--inspect=6009"] } }, 105 | }; 106 | 107 | const pattern = pathutil.join(fsPath, config.path, "**/*.{md,mdoc,markdoc}"); 108 | const client: LSP.LanguageClientOptions = { 109 | initializationOptions: { config }, 110 | documentSelector: [{ language: "markdoc", scheme, pattern }], 111 | markdown: { isTrusted: true }, 112 | }; 113 | 114 | return { server, client }; 115 | } 116 | 117 | private async createClient(resolve: () => void) { 118 | const { server, client } = await this.options(); 119 | const name = `Markdoc: ${this.id}`; 120 | 121 | this.client = new LSP.LanguageClient(this.id, name, server, client); 122 | this.disposables.push(this.client.start()); 123 | this.state = ClientState.Initializing; 124 | await this.client.onReady(); 125 | 126 | const onWorkProgress = ({ kind }: WorkProgress) => { 127 | if (kind !== "end") return; 128 | this.state = ClientState.Running; 129 | resolve(); 130 | }; 131 | 132 | if (this.config?.templates?.pattern) 133 | VSC.workspace 134 | .findFiles(this.config.templates.pattern) 135 | .then((result) => (this.templates = result)); 136 | 137 | this.disposables.push( 138 | this.client.onRequest("markdoc/diff", (file: string) => { 139 | const uri = VSC.Uri.parse(file); 140 | return utils.diff(uri); 141 | }), 142 | 143 | this.client.onProgress( 144 | LSP.WorkDoneProgress.type, 145 | "initialize", 146 | onWorkProgress 147 | ) 148 | ); 149 | } 150 | 151 | getTemplates(): TemplatePickerItem[] | void { 152 | if (!this.templates) return; 153 | return this.templates.map((uri) => ({ 154 | uri, 155 | label: pathutil.basename(uri.fsPath), 156 | detail: uri.fsPath.slice(this.root.uri.fsPath.length + 1), 157 | })); 158 | } 159 | 160 | async renderPreview( 161 | contentUri: VSC.Uri, 162 | assetUri?: VSC.Uri 163 | ): Promise { 164 | if (!this.client || this.state != ClientState.Running) return; 165 | return this.client.sendRequest("markdoc.renderPreview", [ 166 | contentUri.toString(), 167 | assetUri?.toString(), 168 | ]); 169 | } 170 | 171 | async getDependencies(uri: VSC.Uri): Promise { 172 | if (!this.client || this.state != ClientState.Running) return; 173 | const file = uri.fsPath.slice(this.uri.fsPath.length + 1); 174 | return this.client.sendRequest("markdoc.getDependencies", file); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /client/commands.ts: -------------------------------------------------------------------------------- 1 | import * as LSP from "vscode-languageclient/node"; 2 | import * as VSC from "vscode"; 3 | import * as utils from "./utils"; 4 | import Client, { ClientState } from "./client"; 5 | 6 | async function showServiceControls(client: Client) { 7 | const response = await VSC.window.showInformationMessage( 8 | "Markdoc Language Server", 9 | "Show Output", 10 | "Edit Configuration", 11 | client.state == ClientState.Disabled ? "Enable" : "Disable" 12 | ); 13 | 14 | switch (response) { 15 | case "Edit Configuration": 16 | const config = utils.getConfigForWorkspace(client.root); 17 | VSC.commands.executeCommand("vscode.open", config); 18 | break; 19 | case "Show Output": 20 | client.showOutputChannel(); 21 | break; 22 | case "Disable": 23 | client.disable(); 24 | break; 25 | case "Enable": 26 | client.enable(); 27 | break; 28 | } 29 | } 30 | 31 | function peekLocations( 32 | uri: string, 33 | position: LSP.Position, 34 | locations: LSP.Location[] 35 | ) { 36 | return VSC.commands.executeCommand( 37 | "editor.action.peekLocations", 38 | VSC.Uri.parse(uri), 39 | utils.convertPosition(position), 40 | locations.map(utils.convertLocation) 41 | ); 42 | } 43 | 44 | export default function register() { 45 | return [ 46 | VSC.commands.registerCommand("markdoc.peekLocations", peekLocations), 47 | VSC.commands.registerCommand( 48 | "markdoc.showServiceControls", 49 | showServiceControls 50 | ), 51 | ]; 52 | } 53 | -------------------------------------------------------------------------------- /client/extension.ts: -------------------------------------------------------------------------------- 1 | import * as VSC from "vscode"; 2 | import Client, { ClientState, TemplatePickerItem } from "./client"; 3 | import PartialTreeProvider from "./partials"; 4 | import StatusItem from "./status"; 5 | import Preview from "./preview"; 6 | import commands from "./commands"; 7 | import * as utils from "./utils"; 8 | import type { Config } from "../server/types"; 9 | 10 | export default class Extension { 11 | private watchers: VSC.FileSystemWatcher[] = []; 12 | private clients: Client[] = []; 13 | private partials: PartialTreeProvider; 14 | private status: StatusItem; 15 | private preview: Preview; 16 | private active?: Client; 17 | 18 | constructor(private context: VSC.ExtensionContext) { 19 | this.partials = new PartialTreeProvider(); 20 | this.status = new StatusItem(); 21 | this.preview = new Preview(); 22 | 23 | context.subscriptions.push( 24 | ...commands(), 25 | this.status, 26 | this.preview, 27 | VSC.commands.registerCommand( 28 | "markdoc.extractPartial", 29 | this.onExtractPartial.bind(this) 30 | ), 31 | VSC.commands.registerCommand( 32 | "markdoc.newFileFromTemplate", 33 | this.onNewFileFromTemplate.bind(this) 34 | ), 35 | VSC.commands.registerCommand( 36 | "markdoc.preview", 37 | this.onPreview.bind(this) 38 | ), 39 | VSC.workspace.onDidChangeConfiguration(this.onConfigChange.bind(this)), 40 | VSC.workspace.onDidChangeWorkspaceFolders(this.onFolderChange.bind(this)), 41 | VSC.workspace.onDidSaveTextDocument(this.onSave.bind(this)), 42 | VSC.window.onDidChangeActiveTextEditor(this.onActive.bind(this)), 43 | VSC.window.registerTreeDataProvider("markdocPartials", this.partials) 44 | ); 45 | } 46 | 47 | async parse(uri: VSC.Uri) { 48 | try { 49 | const raw = await VSC.workspace.fs.readFile(uri); 50 | const text = new TextDecoder().decode(raw); 51 | return JSON.parse(text); 52 | } catch (err) { 53 | console.log("Failed to parse Markdoc config:", err); 54 | return; 55 | } 56 | } 57 | 58 | async start() { 59 | const clientsToStart = []; 60 | VSC.commands.executeCommand("setContext", "markdoc.enabled", true); 61 | for (const root of VSC.workspace.workspaceFolders ?? []) { 62 | const configUri = utils.getConfigForWorkspace(root); 63 | const config: Config[] = await this.parse(configUri); 64 | 65 | if (!config) continue; 66 | 67 | this.addRestartWatcher(configUri.fsPath); 68 | const openUris = VSC.window.visibleTextEditors.map((e) => e.document.uri); 69 | 70 | for (const entry of config) { 71 | const client = new Client(root, entry, this.context); 72 | this.clients.push(client); 73 | 74 | if (entry.server?.path && entry.server.watch) { 75 | const { path, watch } = entry.server; 76 | const match = typeof watch === "string" ? watch : path; 77 | const pattern = new VSC.RelativePattern(root, match); 78 | this.addRestartWatcher(pattern); 79 | } 80 | 81 | const contentPath = VSC.Uri.joinPath(root.uri, entry.path); 82 | if (openUris.find((uri) => uri.fsPath.startsWith(contentPath.fsPath))) 83 | clientsToStart.push(client); 84 | } 85 | } 86 | 87 | if (clientsToStart.length < 1) return; 88 | this.status.setClient(clientsToStart[0]); 89 | await Promise.allSettled(clientsToStart.map((client) => client.start())); 90 | 91 | const uri = VSC.window.activeTextEditor?.document.uri; 92 | if (this.active && uri) this.updatePartialsPane(this.active, uri); 93 | } 94 | 95 | async restart() { 96 | await this.stop(); 97 | return this.start(); 98 | } 99 | 100 | async stop() { 101 | VSC.commands.executeCommand("setContext", "markdoc.enabled", false); 102 | await Promise.allSettled(this.clients.map((client) => client.stop())); 103 | this.watchers.forEach((watcher) => watcher.dispose()); 104 | this.clients.forEach((client) => client.dispose()); 105 | this.clients = []; 106 | this.watchers = []; 107 | } 108 | 109 | deactivate() { 110 | return this.stop(); 111 | } 112 | 113 | addRestartWatcher(pattern: VSC.GlobPattern) { 114 | const watcher = VSC.workspace.createFileSystemWatcher(pattern); 115 | watcher.onDidChange(this.restart.bind(this)); 116 | this.watchers.push(watcher); 117 | } 118 | 119 | findClient(uri: VSC.Uri): Client | undefined { 120 | return this.clients.find((client) => 121 | uri.fsPath.startsWith(client.uri.fsPath) 122 | ); 123 | } 124 | 125 | setActive(client?: Client) { 126 | VSC.commands.executeCommand("setContext", "markdoc.active", !!client); 127 | VSC.commands.executeCommand( 128 | "setContext", 129 | "markdoc.canPreview", 130 | client?.canPreview() 131 | ); 132 | 133 | if (!client) return; 134 | this.status.setClient(client); 135 | this.active = client; 136 | } 137 | 138 | async onActive(editor?: VSC.TextEditor) { 139 | if (!editor) return; 140 | if (editor.document.languageId !== "markdoc") return this.setActive(); 141 | 142 | const { uri } = editor.document; 143 | const client = this.findClient(uri); 144 | this.setActive(client); 145 | 146 | if (!client) return; 147 | await client.start(); 148 | 149 | this.updatePartialsPane(client, uri); 150 | this.updatePreview(client, uri); 151 | } 152 | 153 | async updatePartialsPane(client: Client, uri: VSC.Uri) { 154 | const output = await client.getDependencies(uri); 155 | this.partials.update(output ?? undefined, client.uri); 156 | } 157 | 158 | async onNewFileFromTemplate() { 159 | const validStates = [ClientState.Running, ClientState.Initializing]; 160 | const options: TemplatePickerItem[] = []; 161 | 162 | for (const client of this.clients) { 163 | if (!validStates.includes(client.state)) continue; 164 | 165 | const templates = client.getTemplates(); 166 | const separator = { 167 | label: client.id, 168 | kind: VSC.QuickPickItemKind.Separator, 169 | }; 170 | 171 | if (templates) options.push(separator, ...templates); 172 | } 173 | 174 | const selected = await VSC.window.showQuickPick(options, { 175 | title: "Select a Markdoc template", 176 | }); 177 | 178 | if (!selected?.uri) return; 179 | 180 | const raw = await VSC.workspace.fs.readFile(selected.uri); 181 | const content = new TextDecoder().decode(raw); 182 | const doc = await VSC.workspace.openTextDocument({ 183 | content, 184 | language: "markdoc", 185 | }); 186 | 187 | VSC.window.showTextDocument(doc); 188 | } 189 | 190 | async onExtractPartial() { 191 | const editor = VSC.window.activeTextEditor; 192 | if (!editor) return; 193 | 194 | const uri = await VSC.window.showSaveDialog({ 195 | saveLabel: 'Create', 196 | title: 'Name the new partial', 197 | filters: {'Markdoc': ['md', 'mdoc', 'markdoc', 'markdoc.md']} 198 | }); 199 | 200 | if (!uri) return; 201 | 202 | const client = this.findClient(uri); 203 | if (!client) return; 204 | 205 | const path = uri.fsPath.slice(client.uri.fsPath.length + 1); 206 | const partialTag = `{% partial file="${path}" /%}`; 207 | 208 | const edit = new VSC.WorkspaceEdit(); 209 | const contents = new TextEncoder().encode(editor.document.getText(editor.selection)); 210 | edit.createFile(uri, {overwrite: true, contents}); 211 | edit.replace(editor.document.uri, editor.selection, partialTag); 212 | VSC.workspace.applyEdit(edit); 213 | } 214 | 215 | async onPreview(previewUri: VSC.Uri) { 216 | const uri = previewUri ?? VSC.window.activeTextEditor?.document.uri; 217 | if (!uri) return; 218 | 219 | const client = this.findClient(uri); 220 | if (!client?.canPreview()) return; 221 | 222 | this.preview.display(); 223 | this.updatePreview(client, uri); 224 | } 225 | 226 | async updatePreview(client: Client, uri: VSC.Uri) { 227 | if (this.preview.exists() && client.canPreview()) { 228 | const assetUri = this.preview.getAssetUri(client.root.uri); 229 | const content = await client.renderPreview(uri, assetUri); 230 | if (content) this.preview.update(content); 231 | } 232 | } 233 | 234 | onSave(doc: VSC.TextDocument) { 235 | if (doc.languageId !== 'markdoc') return; 236 | const client = this.findClient(doc.uri); 237 | if (client) this.updatePreview(client, doc.uri); 238 | } 239 | 240 | onConfigChange(ev: VSC.ConfigurationChangeEvent) { 241 | if (ev.affectsConfiguration("markdoc.config.path")) this.restart(); 242 | } 243 | 244 | onFolderChange(ev: VSC.WorkspaceFoldersChangeEvent) { 245 | this.restart(); 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /client/index.ts: -------------------------------------------------------------------------------- 1 | import * as VSC from "vscode"; 2 | import Extension from "./extension"; 3 | 4 | let extension: Extension; 5 | 6 | export function activate(context: VSC.ExtensionContext) { 7 | extension = new Extension(context); 8 | extension.start(); 9 | } 10 | 11 | export function deactivate() { 12 | extension.deactivate(); 13 | } 14 | -------------------------------------------------------------------------------- /client/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.0.11", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "version": "0.0.11", 9 | "license": "MIT", 10 | "dependencies": { 11 | "vscode-languageclient": "^7.0.0" 12 | }, 13 | "devDependencies": { 14 | "@types/node": "^18.11.9", 15 | "@types/vscode": "^1.63.0", 16 | "@vscode/test-electron": "^2.1.2" 17 | }, 18 | "engines": { 19 | "vscode": "^1.63.0" 20 | } 21 | }, 22 | "node_modules/@tootallnate/once": { 23 | "version": "1.1.2", 24 | "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", 25 | "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", 26 | "dev": true, 27 | "engines": { 28 | "node": ">= 6" 29 | } 30 | }, 31 | "node_modules/@types/node": { 32 | "version": "18.16.10", 33 | "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.10.tgz", 34 | "integrity": "sha512-sMo3EngB6QkMBlB9rBe1lFdKSLqljyWPPWv6/FzSxh/IDlyVWSzE9RiF4eAuerQHybrWdqBgAGb03PM89qOasA==", 35 | "dev": true 36 | }, 37 | "node_modules/@types/vscode": { 38 | "version": "1.78.0", 39 | "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.78.0.tgz", 40 | "integrity": "sha512-LJZIJpPvKJ0HVQDqfOy6W4sNKUBBwyDu1Bs8chHBZOe9MNuKTJtidgZ2bqjhmmWpUb0TIIqv47BFUcVmAsgaVA==", 41 | "dev": true 42 | }, 43 | "node_modules/@vscode/test-electron": { 44 | "version": "2.3.2", 45 | "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.3.2.tgz", 46 | "integrity": "sha512-CRfQIs5Wi5Ok5SUCC3PTvRRXa74LD43cSXHC8EuNlmHHEPaJa/AGrv76brcA1hVSxrdja9tiYwp95Lq8kwY0tw==", 47 | "dev": true, 48 | "dependencies": { 49 | "http-proxy-agent": "^4.0.1", 50 | "https-proxy-agent": "^5.0.0", 51 | "jszip": "^3.10.1", 52 | "semver": "^7.3.8" 53 | }, 54 | "engines": { 55 | "node": ">=16" 56 | } 57 | }, 58 | "node_modules/agent-base": { 59 | "version": "6.0.2", 60 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", 61 | "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", 62 | "dev": true, 63 | "dependencies": { 64 | "debug": "4" 65 | }, 66 | "engines": { 67 | "node": ">= 6.0.0" 68 | } 69 | }, 70 | "node_modules/balanced-match": { 71 | "version": "1.0.2", 72 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 73 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" 74 | }, 75 | "node_modules/brace-expansion": { 76 | "version": "1.1.11", 77 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 78 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 79 | "dependencies": { 80 | "balanced-match": "^1.0.0", 81 | "concat-map": "0.0.1" 82 | } 83 | }, 84 | "node_modules/concat-map": { 85 | "version": "0.0.1", 86 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 87 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" 88 | }, 89 | "node_modules/core-util-is": { 90 | "version": "1.0.3", 91 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", 92 | "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", 93 | "dev": true 94 | }, 95 | "node_modules/debug": { 96 | "version": "4.3.4", 97 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", 98 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 99 | "dev": true, 100 | "dependencies": { 101 | "ms": "2.1.2" 102 | }, 103 | "engines": { 104 | "node": ">=6.0" 105 | }, 106 | "peerDependenciesMeta": { 107 | "supports-color": { 108 | "optional": true 109 | } 110 | } 111 | }, 112 | "node_modules/http-proxy-agent": { 113 | "version": "4.0.1", 114 | "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", 115 | "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", 116 | "dev": true, 117 | "dependencies": { 118 | "@tootallnate/once": "1", 119 | "agent-base": "6", 120 | "debug": "4" 121 | }, 122 | "engines": { 123 | "node": ">= 6" 124 | } 125 | }, 126 | "node_modules/https-proxy-agent": { 127 | "version": "5.0.1", 128 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", 129 | "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", 130 | "dev": true, 131 | "dependencies": { 132 | "agent-base": "6", 133 | "debug": "4" 134 | }, 135 | "engines": { 136 | "node": ">= 6" 137 | } 138 | }, 139 | "node_modules/immediate": { 140 | "version": "3.0.6", 141 | "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", 142 | "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", 143 | "dev": true 144 | }, 145 | "node_modules/inherits": { 146 | "version": "2.0.4", 147 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 148 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 149 | "dev": true 150 | }, 151 | "node_modules/isarray": { 152 | "version": "1.0.0", 153 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 154 | "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", 155 | "dev": true 156 | }, 157 | "node_modules/jszip": { 158 | "version": "3.10.1", 159 | "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", 160 | "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", 161 | "dev": true, 162 | "dependencies": { 163 | "lie": "~3.3.0", 164 | "pako": "~1.0.2", 165 | "readable-stream": "~2.3.6", 166 | "setimmediate": "^1.0.5" 167 | } 168 | }, 169 | "node_modules/lie": { 170 | "version": "3.3.0", 171 | "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", 172 | "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", 173 | "dev": true, 174 | "dependencies": { 175 | "immediate": "~3.0.5" 176 | } 177 | }, 178 | "node_modules/lru-cache": { 179 | "version": "6.0.0", 180 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", 181 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", 182 | "dependencies": { 183 | "yallist": "^4.0.0" 184 | }, 185 | "engines": { 186 | "node": ">=10" 187 | } 188 | }, 189 | "node_modules/minimatch": { 190 | "version": "3.1.2", 191 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 192 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 193 | "dependencies": { 194 | "brace-expansion": "^1.1.7" 195 | }, 196 | "engines": { 197 | "node": "*" 198 | } 199 | }, 200 | "node_modules/ms": { 201 | "version": "2.1.2", 202 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 203 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 204 | "dev": true 205 | }, 206 | "node_modules/pako": { 207 | "version": "1.0.11", 208 | "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", 209 | "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", 210 | "dev": true 211 | }, 212 | "node_modules/process-nextick-args": { 213 | "version": "2.0.1", 214 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", 215 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", 216 | "dev": true 217 | }, 218 | "node_modules/readable-stream": { 219 | "version": "2.3.8", 220 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", 221 | "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", 222 | "dev": true, 223 | "dependencies": { 224 | "core-util-is": "~1.0.0", 225 | "inherits": "~2.0.3", 226 | "isarray": "~1.0.0", 227 | "process-nextick-args": "~2.0.0", 228 | "safe-buffer": "~5.1.1", 229 | "string_decoder": "~1.1.1", 230 | "util-deprecate": "~1.0.1" 231 | } 232 | }, 233 | "node_modules/safe-buffer": { 234 | "version": "5.1.2", 235 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 236 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", 237 | "dev": true 238 | }, 239 | "node_modules/semver": { 240 | "version": "7.5.4", 241 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", 242 | "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", 243 | "dependencies": { 244 | "lru-cache": "^6.0.0" 245 | }, 246 | "bin": { 247 | "semver": "bin/semver.js" 248 | }, 249 | "engines": { 250 | "node": ">=10" 251 | } 252 | }, 253 | "node_modules/setimmediate": { 254 | "version": "1.0.5", 255 | "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", 256 | "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", 257 | "dev": true 258 | }, 259 | "node_modules/string_decoder": { 260 | "version": "1.1.1", 261 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 262 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 263 | "dev": true, 264 | "dependencies": { 265 | "safe-buffer": "~5.1.0" 266 | } 267 | }, 268 | "node_modules/util-deprecate": { 269 | "version": "1.0.2", 270 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 271 | "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", 272 | "dev": true 273 | }, 274 | "node_modules/vscode-jsonrpc": { 275 | "version": "6.0.0", 276 | "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz", 277 | "integrity": "sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==", 278 | "engines": { 279 | "node": ">=8.0.0 || >=10.0.0" 280 | } 281 | }, 282 | "node_modules/vscode-languageclient": { 283 | "version": "7.0.0", 284 | "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-7.0.0.tgz", 285 | "integrity": "sha512-P9AXdAPlsCgslpP9pRxYPqkNYV7Xq8300/aZDpO35j1fJm/ncize8iGswzYlcvFw5DQUx4eVk+KvfXdL0rehNg==", 286 | "dependencies": { 287 | "minimatch": "^3.0.4", 288 | "semver": "^7.3.4", 289 | "vscode-languageserver-protocol": "3.16.0" 290 | }, 291 | "engines": { 292 | "vscode": "^1.52.0" 293 | } 294 | }, 295 | "node_modules/vscode-languageserver-protocol": { 296 | "version": "3.16.0", 297 | "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz", 298 | "integrity": "sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A==", 299 | "dependencies": { 300 | "vscode-jsonrpc": "6.0.0", 301 | "vscode-languageserver-types": "3.16.0" 302 | } 303 | }, 304 | "node_modules/vscode-languageserver-types": { 305 | "version": "3.16.0", 306 | "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz", 307 | "integrity": "sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==" 308 | }, 309 | "node_modules/yallist": { 310 | "version": "4.0.0", 311 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 312 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" 313 | } 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "description": "Markdoc Extension", 4 | "author": "Ryan Paul", 5 | "license": "MIT", 6 | "version": "0.0.13", 7 | "scripts": { 8 | "build": "esbuild index.ts --bundle --outdir=dist --sourcemap=linked --external:vscode --platform=node --format=cjs", 9 | "build:server": "esbuild server.ts --bundle --outdir=dist --sourcemap=linked --external:vscode --platform=node --format=cjs" 10 | }, 11 | "engines": { 12 | "vscode": "^1.63.0" 13 | }, 14 | "dependencies": { 15 | "vscode-languageclient": "^7.0.0" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^18.11.9", 19 | "@types/vscode": "^1.63.0", 20 | "@vscode/test-electron": "^2.1.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /client/partials.ts: -------------------------------------------------------------------------------- 1 | import * as VSC from "vscode"; 2 | import { DependencyInfo, PartialReference } from "../server/types"; 3 | 4 | type PartialTreeRoot = { title: string; children?: PartialReference[] }; 5 | type PartialTreeItem = PartialTreeRoot | PartialReference; 6 | 7 | export default class PartialTreeProvider 8 | implements VSC.TreeDataProvider 9 | { 10 | private info?: DependencyInfo; 11 | private uri?: VSC.Uri; 12 | 13 | private updateEmitter = new VSC.EventEmitter(); 14 | readonly onDidChangeTreeData = this.updateEmitter.event; 15 | 16 | update(info: DependencyInfo | undefined, uri: VSC.Uri) { 17 | this.info = info; 18 | this.uri = uri; 19 | this.updateEmitter.fire(); 20 | } 21 | 22 | getTreeItem(element: PartialTreeItem): VSC.TreeItem { 23 | if (!this.uri) return {}; 24 | 25 | if ("title" in element) { 26 | const item = new VSC.TreeItem(element.title); 27 | item.collapsibleState = 28 | element?.children && element.children.length > 0 29 | ? VSC.TreeItemCollapsibleState.Expanded 30 | : VSC.TreeItemCollapsibleState.None; 31 | return item; 32 | } 33 | 34 | const uri = VSC.Uri.joinPath(this.uri, element.file); 35 | const item = new VSC.TreeItem(element.file); 36 | item.command = { command: "vscode.open", title: "Open", arguments: [uri] }; 37 | item.resourceUri = uri; 38 | item.collapsibleState = 39 | element?.children && element.children.length > 0 40 | ? VSC.TreeItemCollapsibleState.Collapsed 41 | : VSC.TreeItemCollapsibleState.None; 42 | 43 | return item; 44 | } 45 | 46 | getChildren( 47 | element?: PartialTreeItem 48 | ): VSC.ProviderResult { 49 | if (element) return element.children; 50 | if (!this.info) return [{ title: "Loading..." }]; 51 | 52 | const rootElements = []; 53 | const { dependencies, dependents } = this.info; 54 | 55 | if (dependencies.length > 0) 56 | rootElements.push({ title: "Partials", children: dependencies }); 57 | 58 | if (dependents.length > 0) 59 | rootElements.push({ title: "References", children: dependents }); 60 | 61 | if (rootElements.length < 1) rootElements.push({ title: "None" }); 62 | 63 | return rootElements; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /client/preview.ts: -------------------------------------------------------------------------------- 1 | import * as VSC from "vscode"; 2 | 3 | export default class Preview implements VSC.Disposable { 4 | protected panel?: VSC.WebviewPanel; 5 | 6 | exists() { 7 | return !!this.panel; 8 | } 9 | 10 | display() { 11 | if (this.panel) return this.panel.reveal(); 12 | 13 | this.panel = VSC.window.createWebviewPanel( 14 | "markdoc.preview", 15 | "Markdoc Preview", 16 | VSC.ViewColumn.Beside, 17 | { enableScripts: true } 18 | ); 19 | 20 | this.panel.onDidDispose(() => (this.panel = undefined)); 21 | } 22 | 23 | getAssetUri(uri: VSC.Uri) { 24 | return this.panel?.webview.asWebviewUri(uri); 25 | } 26 | 27 | update(content: string) { 28 | if (!this.panel) return; 29 | this.panel.webview.html = content; 30 | } 31 | 32 | dispose() { 33 | this.panel?.dispose(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /client/server.ts: -------------------------------------------------------------------------------- 1 | import { server } from "../server"; 2 | 3 | server(); 4 | -------------------------------------------------------------------------------- /client/status.ts: -------------------------------------------------------------------------------- 1 | import * as VSC from "vscode"; 2 | import Client, { ClientState } from "./client"; 3 | 4 | export default class StatusItem implements VSC.Disposable { 5 | protected item: VSC.StatusBarItem; 6 | protected listener?: VSC.Disposable; 7 | protected client?: Client; 8 | 9 | constructor() { 10 | this.item = VSC.window.createStatusBarItem(VSC.StatusBarAlignment.Left); 11 | this.item.text = "Initializing"; 12 | } 13 | 14 | setClient(client: Client) { 15 | this.listener?.dispose(); 16 | this.client = client; 17 | this.listener = this.client.onStateDidChange(() => this.update()); 18 | this.update(); 19 | } 20 | 21 | private update() { 22 | if (!this.client) return; 23 | 24 | const { id, state } = this.client; 25 | const text = `Markdoc: ${id}`; 26 | 27 | this.item.text = 28 | state == ClientState.Initializing 29 | ? `$(loading~spin) ${text}` 30 | : state == ClientState.Disabled 31 | ? `${text} (Disabled)` 32 | : text; 33 | 34 | this.item.command = { 35 | command: "markdoc.showServiceControls", 36 | title: "Show Service Controls", 37 | arguments: [this.client], 38 | }; 39 | 40 | this.item.show(); 41 | } 42 | 43 | dispose() { 44 | this.item.dispose(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /client/utils.ts: -------------------------------------------------------------------------------- 1 | import * as LSP from "vscode-languageclient/node"; 2 | import * as VSC from "vscode"; 3 | 4 | export function convertPosition(position: LSP.Position): VSC.Position { 5 | return new VSC.Position(position.line, position.character); 6 | } 7 | 8 | export function convertLocation(location: LSP.Location): VSC.Location { 9 | const uri = VSC.Uri.parse(location.uri); 10 | const start = convertPosition(location.range.start); 11 | const end = convertPosition(location.range.end); 12 | return new VSC.Location(uri, new VSC.Range(start, end)); 13 | } 14 | 15 | export function getConfigForWorkspace(root: VSC.WorkspaceFolder): VSC.Uri { 16 | const settings = VSC.workspace.getConfiguration("markdoc", root); 17 | const path = settings.get("config.path", "markdoc.config.json"); 18 | return VSC.Uri.joinPath(root.uri, path); 19 | } 20 | 21 | export async function diff(uri: VSC.Uri) { 22 | const git = VSC.extensions.getExtension("vscode.git"); 23 | if (!git) return; 24 | 25 | const extension = await git.activate(); 26 | const repo = extension.model.getRepository(uri); 27 | return repo.diffWithHEAD(uri.fsPath); 28 | } 29 | -------------------------------------------------------------------------------- /language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "blockComment": [ "" ] 4 | }, 5 | "brackets": [ 6 | ["{", "}"], 7 | ["[", "]"], 8 | ["(", ")"] 9 | ], 10 | "colorizedBracketPairs": [], 11 | "autoClosingPairs": [ 12 | { 13 | "open": "[", 14 | "close": "]" 15 | }, 16 | { 17 | "open": "(", 18 | "close": ")" 19 | }, 20 | ], 21 | "surroundingPairs": [ 22 | ["(", ")"], 23 | ["[", "]"], 24 | ["`", "`"], 25 | ["_", "_"], 26 | ["*", "*"], 27 | ["{", "}"], 28 | ["'", "'"], 29 | ["\"", "\""] 30 | ], 31 | "folding": { 32 | "offSide": true, 33 | "markers": { 34 | "start": "^\\s*", 35 | "end": "^\\s*" 36 | } 37 | }, 38 | "wordPattern": { 39 | "pattern": "(\\p{Alphabetic}|\\p{Number}|\\p{Nonspacing_Mark})(((\\p{Alphabetic}|\\p{Number}|\\p{Nonspacing_Mark})|[_])?(\\p{Alphabetic}|\\p{Number}|\\p{Nonspacing_Mark}))*", 40 | "flags": "ug" 41 | }, 42 | } -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markdoc/language-server/a6b8babf602735dc31bae1d10fad903dc40c9761/logo.png -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "markdoc-language-support", 3 | "version": "0.0.9", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "markdoc-language-support", 9 | "version": "0.0.9", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "@types/node": "^18.11.18", 13 | "@vscode/vsce": "^2.19.0", 14 | "esbuild": "0.17.17", 15 | "esbuild-register": "^3.4.2", 16 | "typescript": "^4.9.3" 17 | }, 18 | "engines": { 19 | "vscode": "^1.63.0" 20 | } 21 | }, 22 | "node_modules/@esbuild/android-arm": { 23 | "version": "0.17.17", 24 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.17.tgz", 25 | "integrity": "sha512-E6VAZwN7diCa3labs0GYvhEPL2M94WLF8A+czO8hfjREXxba8Ng7nM5VxV+9ihNXIY1iQO1XxUU4P7hbqbICxg==", 26 | "cpu": [ 27 | "arm" 28 | ], 29 | "dev": true, 30 | "optional": true, 31 | "os": [ 32 | "android" 33 | ], 34 | "engines": { 35 | "node": ">=12" 36 | } 37 | }, 38 | "node_modules/@esbuild/android-arm64": { 39 | "version": "0.17.17", 40 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.17.tgz", 41 | "integrity": "sha512-jaJ5IlmaDLFPNttv0ofcwy/cfeY4bh/n705Tgh+eLObbGtQBK3EPAu+CzL95JVE4nFAliyrnEu0d32Q5foavqg==", 42 | "cpu": [ 43 | "arm64" 44 | ], 45 | "dev": true, 46 | "optional": true, 47 | "os": [ 48 | "android" 49 | ], 50 | "engines": { 51 | "node": ">=12" 52 | } 53 | }, 54 | "node_modules/@esbuild/android-x64": { 55 | "version": "0.17.17", 56 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.17.tgz", 57 | "integrity": "sha512-446zpfJ3nioMC7ASvJB1pszHVskkw4u/9Eu8s5yvvsSDTzYh4p4ZIRj0DznSl3FBF0Z/mZfrKXTtt0QCoFmoHA==", 58 | "cpu": [ 59 | "x64" 60 | ], 61 | "dev": true, 62 | "optional": true, 63 | "os": [ 64 | "android" 65 | ], 66 | "engines": { 67 | "node": ">=12" 68 | } 69 | }, 70 | "node_modules/@esbuild/darwin-arm64": { 71 | "version": "0.17.17", 72 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.17.tgz", 73 | "integrity": "sha512-m/gwyiBwH3jqfUabtq3GH31otL/0sE0l34XKpSIqR7NjQ/XHQ3lpmQHLHbG8AHTGCw8Ao059GvV08MS0bhFIJQ==", 74 | "cpu": [ 75 | "arm64" 76 | ], 77 | "dev": true, 78 | "optional": true, 79 | "os": [ 80 | "darwin" 81 | ], 82 | "engines": { 83 | "node": ">=12" 84 | } 85 | }, 86 | "node_modules/@esbuild/darwin-x64": { 87 | "version": "0.17.17", 88 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.17.tgz", 89 | "integrity": "sha512-4utIrsX9IykrqYaXR8ob9Ha2hAY2qLc6ohJ8c0CN1DR8yWeMrTgYFjgdeQ9LIoTOfLetXjuCu5TRPHT9yKYJVg==", 90 | "cpu": [ 91 | "x64" 92 | ], 93 | "dev": true, 94 | "optional": true, 95 | "os": [ 96 | "darwin" 97 | ], 98 | "engines": { 99 | "node": ">=12" 100 | } 101 | }, 102 | "node_modules/@esbuild/freebsd-arm64": { 103 | "version": "0.17.17", 104 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.17.tgz", 105 | "integrity": "sha512-4PxjQII/9ppOrpEwzQ1b0pXCsFLqy77i0GaHodrmzH9zq2/NEhHMAMJkJ635Ns4fyJPFOlHMz4AsklIyRqFZWA==", 106 | "cpu": [ 107 | "arm64" 108 | ], 109 | "dev": true, 110 | "optional": true, 111 | "os": [ 112 | "freebsd" 113 | ], 114 | "engines": { 115 | "node": ">=12" 116 | } 117 | }, 118 | "node_modules/@esbuild/freebsd-x64": { 119 | "version": "0.17.17", 120 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.17.tgz", 121 | "integrity": "sha512-lQRS+4sW5S3P1sv0z2Ym807qMDfkmdhUYX30GRBURtLTrJOPDpoU0kI6pVz1hz3U0+YQ0tXGS9YWveQjUewAJw==", 122 | "cpu": [ 123 | "x64" 124 | ], 125 | "dev": true, 126 | "optional": true, 127 | "os": [ 128 | "freebsd" 129 | ], 130 | "engines": { 131 | "node": ">=12" 132 | } 133 | }, 134 | "node_modules/@esbuild/linux-arm": { 135 | "version": "0.17.17", 136 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.17.tgz", 137 | "integrity": "sha512-biDs7bjGdOdcmIk6xU426VgdRUpGg39Yz6sT9Xp23aq+IEHDb/u5cbmu/pAANpDB4rZpY/2USPhCA+w9t3roQg==", 138 | "cpu": [ 139 | "arm" 140 | ], 141 | "dev": true, 142 | "optional": true, 143 | "os": [ 144 | "linux" 145 | ], 146 | "engines": { 147 | "node": ">=12" 148 | } 149 | }, 150 | "node_modules/@esbuild/linux-arm64": { 151 | "version": "0.17.17", 152 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.17.tgz", 153 | "integrity": "sha512-2+pwLx0whKY1/Vqt8lyzStyda1v0qjJ5INWIe+d8+1onqQxHLLi3yr5bAa4gvbzhZqBztifYEu8hh1La5+7sUw==", 154 | "cpu": [ 155 | "arm64" 156 | ], 157 | "dev": true, 158 | "optional": true, 159 | "os": [ 160 | "linux" 161 | ], 162 | "engines": { 163 | "node": ">=12" 164 | } 165 | }, 166 | "node_modules/@esbuild/linux-ia32": { 167 | "version": "0.17.17", 168 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.17.tgz", 169 | "integrity": "sha512-IBTTv8X60dYo6P2t23sSUYym8fGfMAiuv7PzJ+0LcdAndZRzvke+wTVxJeCq4WgjppkOpndL04gMZIFvwoU34Q==", 170 | "cpu": [ 171 | "ia32" 172 | ], 173 | "dev": true, 174 | "optional": true, 175 | "os": [ 176 | "linux" 177 | ], 178 | "engines": { 179 | "node": ">=12" 180 | } 181 | }, 182 | "node_modules/@esbuild/linux-loong64": { 183 | "version": "0.17.17", 184 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.17.tgz", 185 | "integrity": "sha512-WVMBtcDpATjaGfWfp6u9dANIqmU9r37SY8wgAivuKmgKHE+bWSuv0qXEFt/p3qXQYxJIGXQQv6hHcm7iWhWjiw==", 186 | "cpu": [ 187 | "loong64" 188 | ], 189 | "dev": true, 190 | "optional": true, 191 | "os": [ 192 | "linux" 193 | ], 194 | "engines": { 195 | "node": ">=12" 196 | } 197 | }, 198 | "node_modules/@esbuild/linux-mips64el": { 199 | "version": "0.17.17", 200 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.17.tgz", 201 | "integrity": "sha512-2kYCGh8589ZYnY031FgMLy0kmE4VoGdvfJkxLdxP4HJvWNXpyLhjOvxVsYjYZ6awqY4bgLR9tpdYyStgZZhi2A==", 202 | "cpu": [ 203 | "mips64el" 204 | ], 205 | "dev": true, 206 | "optional": true, 207 | "os": [ 208 | "linux" 209 | ], 210 | "engines": { 211 | "node": ">=12" 212 | } 213 | }, 214 | "node_modules/@esbuild/linux-ppc64": { 215 | "version": "0.17.17", 216 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.17.tgz", 217 | "integrity": "sha512-KIdG5jdAEeAKogfyMTcszRxy3OPbZhq0PPsW4iKKcdlbk3YE4miKznxV2YOSmiK/hfOZ+lqHri3v8eecT2ATwQ==", 218 | "cpu": [ 219 | "ppc64" 220 | ], 221 | "dev": true, 222 | "optional": true, 223 | "os": [ 224 | "linux" 225 | ], 226 | "engines": { 227 | "node": ">=12" 228 | } 229 | }, 230 | "node_modules/@esbuild/linux-riscv64": { 231 | "version": "0.17.17", 232 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.17.tgz", 233 | "integrity": "sha512-Cj6uWLBR5LWhcD/2Lkfg2NrkVsNb2sFM5aVEfumKB2vYetkA/9Uyc1jVoxLZ0a38sUhFk4JOVKH0aVdPbjZQeA==", 234 | "cpu": [ 235 | "riscv64" 236 | ], 237 | "dev": true, 238 | "optional": true, 239 | "os": [ 240 | "linux" 241 | ], 242 | "engines": { 243 | "node": ">=12" 244 | } 245 | }, 246 | "node_modules/@esbuild/linux-s390x": { 247 | "version": "0.17.17", 248 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.17.tgz", 249 | "integrity": "sha512-lK+SffWIr0XsFf7E0srBjhpkdFVJf3HEgXCwzkm69kNbRar8MhezFpkIwpk0qo2IOQL4JE4mJPJI8AbRPLbuOQ==", 250 | "cpu": [ 251 | "s390x" 252 | ], 253 | "dev": true, 254 | "optional": true, 255 | "os": [ 256 | "linux" 257 | ], 258 | "engines": { 259 | "node": ">=12" 260 | } 261 | }, 262 | "node_modules/@esbuild/linux-x64": { 263 | "version": "0.17.17", 264 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.17.tgz", 265 | "integrity": "sha512-XcSGTQcWFQS2jx3lZtQi7cQmDYLrpLRyz1Ns1DzZCtn898cWfm5Icx/DEWNcTU+T+tyPV89RQtDnI7qL2PObPg==", 266 | "cpu": [ 267 | "x64" 268 | ], 269 | "dev": true, 270 | "optional": true, 271 | "os": [ 272 | "linux" 273 | ], 274 | "engines": { 275 | "node": ">=12" 276 | } 277 | }, 278 | "node_modules/@esbuild/netbsd-x64": { 279 | "version": "0.17.17", 280 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.17.tgz", 281 | "integrity": "sha512-RNLCDmLP5kCWAJR+ItLM3cHxzXRTe4N00TQyQiimq+lyqVqZWGPAvcyfUBM0isE79eEZhIuGN09rAz8EL5KdLA==", 282 | "cpu": [ 283 | "x64" 284 | ], 285 | "dev": true, 286 | "optional": true, 287 | "os": [ 288 | "netbsd" 289 | ], 290 | "engines": { 291 | "node": ">=12" 292 | } 293 | }, 294 | "node_modules/@esbuild/openbsd-x64": { 295 | "version": "0.17.17", 296 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.17.tgz", 297 | "integrity": "sha512-PAXswI5+cQq3Pann7FNdcpSUrhrql3wKjj3gVkmuz6OHhqqYxKvi6GgRBoaHjaG22HV/ZZEgF9TlS+9ftHVigA==", 298 | "cpu": [ 299 | "x64" 300 | ], 301 | "dev": true, 302 | "optional": true, 303 | "os": [ 304 | "openbsd" 305 | ], 306 | "engines": { 307 | "node": ">=12" 308 | } 309 | }, 310 | "node_modules/@esbuild/sunos-x64": { 311 | "version": "0.17.17", 312 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.17.tgz", 313 | "integrity": "sha512-V63egsWKnx/4V0FMYkr9NXWrKTB5qFftKGKuZKFIrAkO/7EWLFnbBZNM1CvJ6Sis+XBdPws2YQSHF1Gqf1oj/Q==", 314 | "cpu": [ 315 | "x64" 316 | ], 317 | "dev": true, 318 | "optional": true, 319 | "os": [ 320 | "sunos" 321 | ], 322 | "engines": { 323 | "node": ">=12" 324 | } 325 | }, 326 | "node_modules/@esbuild/win32-arm64": { 327 | "version": "0.17.17", 328 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.17.tgz", 329 | "integrity": "sha512-YtUXLdVnd6YBSYlZODjWzH+KzbaubV0YVd6UxSfoFfa5PtNJNaW+1i+Hcmjpg2nEe0YXUCNF5bkKy1NnBv1y7Q==", 330 | "cpu": [ 331 | "arm64" 332 | ], 333 | "dev": true, 334 | "optional": true, 335 | "os": [ 336 | "win32" 337 | ], 338 | "engines": { 339 | "node": ">=12" 340 | } 341 | }, 342 | "node_modules/@esbuild/win32-ia32": { 343 | "version": "0.17.17", 344 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.17.tgz", 345 | "integrity": "sha512-yczSLRbDdReCO74Yfc5tKG0izzm+lPMYyO1fFTcn0QNwnKmc3K+HdxZWLGKg4pZVte7XVgcFku7TIZNbWEJdeQ==", 346 | "cpu": [ 347 | "ia32" 348 | ], 349 | "dev": true, 350 | "optional": true, 351 | "os": [ 352 | "win32" 353 | ], 354 | "engines": { 355 | "node": ">=12" 356 | } 357 | }, 358 | "node_modules/@esbuild/win32-x64": { 359 | "version": "0.17.17", 360 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.17.tgz", 361 | "integrity": "sha512-FNZw7H3aqhF9OyRQbDDnzUApDXfC1N6fgBhkqEO2jvYCJ+DxMTfZVqg3AX0R1khg1wHTBRD5SdcibSJ+XF6bFg==", 362 | "cpu": [ 363 | "x64" 364 | ], 365 | "dev": true, 366 | "optional": true, 367 | "os": [ 368 | "win32" 369 | ], 370 | "engines": { 371 | "node": ">=12" 372 | } 373 | }, 374 | "node_modules/@types/node": { 375 | "version": "18.16.10", 376 | "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.10.tgz", 377 | "integrity": "sha512-sMo3EngB6QkMBlB9rBe1lFdKSLqljyWPPWv6/FzSxh/IDlyVWSzE9RiF4eAuerQHybrWdqBgAGb03PM89qOasA==", 378 | "dev": true 379 | }, 380 | "node_modules/@vscode/vsce": { 381 | "version": "2.19.0", 382 | "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.19.0.tgz", 383 | "integrity": "sha512-dAlILxC5ggOutcvJY24jxz913wimGiUrHaPkk16Gm9/PGFbz1YezWtrXsTKUtJws4fIlpX2UIlVlVESWq8lkfQ==", 384 | "dev": true, 385 | "dependencies": { 386 | "azure-devops-node-api": "^11.0.1", 387 | "chalk": "^2.4.2", 388 | "cheerio": "^1.0.0-rc.9", 389 | "commander": "^6.1.0", 390 | "glob": "^7.0.6", 391 | "hosted-git-info": "^4.0.2", 392 | "jsonc-parser": "^3.2.0", 393 | "leven": "^3.1.0", 394 | "markdown-it": "^12.3.2", 395 | "mime": "^1.3.4", 396 | "minimatch": "^3.0.3", 397 | "parse-semver": "^1.1.1", 398 | "read": "^1.0.7", 399 | "semver": "^5.1.0", 400 | "tmp": "^0.2.1", 401 | "typed-rest-client": "^1.8.4", 402 | "url-join": "^4.0.1", 403 | "xml2js": "^0.5.0", 404 | "yauzl": "^2.3.1", 405 | "yazl": "^2.2.2" 406 | }, 407 | "bin": { 408 | "vsce": "vsce" 409 | }, 410 | "engines": { 411 | "node": ">= 14" 412 | }, 413 | "optionalDependencies": { 414 | "keytar": "^7.7.0" 415 | } 416 | }, 417 | "node_modules/@vscode/vsce/node_modules/xml2js": { 418 | "version": "0.5.0", 419 | "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", 420 | "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", 421 | "dev": true, 422 | "dependencies": { 423 | "sax": ">=0.6.0", 424 | "xmlbuilder": "~11.0.0" 425 | }, 426 | "engines": { 427 | "node": ">=4.0.0" 428 | } 429 | }, 430 | "node_modules/ansi-styles": { 431 | "version": "3.2.1", 432 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 433 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 434 | "dev": true, 435 | "dependencies": { 436 | "color-convert": "^1.9.0" 437 | }, 438 | "engines": { 439 | "node": ">=4" 440 | } 441 | }, 442 | "node_modules/argparse": { 443 | "version": "2.0.1", 444 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 445 | "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 446 | "dev": true 447 | }, 448 | "node_modules/azure-devops-node-api": { 449 | "version": "11.2.0", 450 | "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-11.2.0.tgz", 451 | "integrity": "sha512-XdiGPhrpaT5J8wdERRKs5g8E0Zy1pvOYTli7z9E8nmOn3YGp4FhtjhrOyFmX/8veWCwdI69mCHKJw6l+4J/bHA==", 452 | "dev": true, 453 | "dependencies": { 454 | "tunnel": "0.0.6", 455 | "typed-rest-client": "^1.8.4" 456 | } 457 | }, 458 | "node_modules/balanced-match": { 459 | "version": "1.0.2", 460 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 461 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 462 | "dev": true 463 | }, 464 | "node_modules/base64-js": { 465 | "version": "1.5.1", 466 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 467 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", 468 | "dev": true, 469 | "funding": [ 470 | { 471 | "type": "github", 472 | "url": "https://github.com/sponsors/feross" 473 | }, 474 | { 475 | "type": "patreon", 476 | "url": "https://www.patreon.com/feross" 477 | }, 478 | { 479 | "type": "consulting", 480 | "url": "https://feross.org/support" 481 | } 482 | ], 483 | "optional": true 484 | }, 485 | "node_modules/bl": { 486 | "version": "4.1.0", 487 | "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", 488 | "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", 489 | "dev": true, 490 | "optional": true, 491 | "dependencies": { 492 | "buffer": "^5.5.0", 493 | "inherits": "^2.0.4", 494 | "readable-stream": "^3.4.0" 495 | } 496 | }, 497 | "node_modules/boolbase": { 498 | "version": "1.0.0", 499 | "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", 500 | "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", 501 | "dev": true 502 | }, 503 | "node_modules/brace-expansion": { 504 | "version": "1.1.11", 505 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 506 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 507 | "dev": true, 508 | "dependencies": { 509 | "balanced-match": "^1.0.0", 510 | "concat-map": "0.0.1" 511 | } 512 | }, 513 | "node_modules/buffer": { 514 | "version": "5.7.1", 515 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", 516 | "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", 517 | "dev": true, 518 | "funding": [ 519 | { 520 | "type": "github", 521 | "url": "https://github.com/sponsors/feross" 522 | }, 523 | { 524 | "type": "patreon", 525 | "url": "https://www.patreon.com/feross" 526 | }, 527 | { 528 | "type": "consulting", 529 | "url": "https://feross.org/support" 530 | } 531 | ], 532 | "optional": true, 533 | "dependencies": { 534 | "base64-js": "^1.3.1", 535 | "ieee754": "^1.1.13" 536 | } 537 | }, 538 | "node_modules/buffer-crc32": { 539 | "version": "0.2.13", 540 | "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", 541 | "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", 542 | "dev": true, 543 | "engines": { 544 | "node": "*" 545 | } 546 | }, 547 | "node_modules/call-bind": { 548 | "version": "1.0.2", 549 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", 550 | "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", 551 | "dev": true, 552 | "dependencies": { 553 | "function-bind": "^1.1.1", 554 | "get-intrinsic": "^1.0.2" 555 | }, 556 | "funding": { 557 | "url": "https://github.com/sponsors/ljharb" 558 | } 559 | }, 560 | "node_modules/chalk": { 561 | "version": "2.4.2", 562 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 563 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 564 | "dev": true, 565 | "dependencies": { 566 | "ansi-styles": "^3.2.1", 567 | "escape-string-regexp": "^1.0.5", 568 | "supports-color": "^5.3.0" 569 | }, 570 | "engines": { 571 | "node": ">=4" 572 | } 573 | }, 574 | "node_modules/cheerio": { 575 | "version": "1.0.0-rc.12", 576 | "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", 577 | "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", 578 | "dev": true, 579 | "dependencies": { 580 | "cheerio-select": "^2.1.0", 581 | "dom-serializer": "^2.0.0", 582 | "domhandler": "^5.0.3", 583 | "domutils": "^3.0.1", 584 | "htmlparser2": "^8.0.1", 585 | "parse5": "^7.0.0", 586 | "parse5-htmlparser2-tree-adapter": "^7.0.0" 587 | }, 588 | "engines": { 589 | "node": ">= 6" 590 | }, 591 | "funding": { 592 | "url": "https://github.com/cheeriojs/cheerio?sponsor=1" 593 | } 594 | }, 595 | "node_modules/cheerio-select": { 596 | "version": "2.1.0", 597 | "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", 598 | "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", 599 | "dev": true, 600 | "dependencies": { 601 | "boolbase": "^1.0.0", 602 | "css-select": "^5.1.0", 603 | "css-what": "^6.1.0", 604 | "domelementtype": "^2.3.0", 605 | "domhandler": "^5.0.3", 606 | "domutils": "^3.0.1" 607 | }, 608 | "funding": { 609 | "url": "https://github.com/sponsors/fb55" 610 | } 611 | }, 612 | "node_modules/chownr": { 613 | "version": "1.1.4", 614 | "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", 615 | "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", 616 | "dev": true, 617 | "optional": true 618 | }, 619 | "node_modules/color-convert": { 620 | "version": "1.9.3", 621 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 622 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 623 | "dev": true, 624 | "dependencies": { 625 | "color-name": "1.1.3" 626 | } 627 | }, 628 | "node_modules/color-name": { 629 | "version": "1.1.3", 630 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 631 | "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", 632 | "dev": true 633 | }, 634 | "node_modules/commander": { 635 | "version": "6.2.1", 636 | "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", 637 | "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", 638 | "dev": true, 639 | "engines": { 640 | "node": ">= 6" 641 | } 642 | }, 643 | "node_modules/concat-map": { 644 | "version": "0.0.1", 645 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 646 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 647 | "dev": true 648 | }, 649 | "node_modules/css-select": { 650 | "version": "5.1.0", 651 | "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", 652 | "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", 653 | "dev": true, 654 | "dependencies": { 655 | "boolbase": "^1.0.0", 656 | "css-what": "^6.1.0", 657 | "domhandler": "^5.0.2", 658 | "domutils": "^3.0.1", 659 | "nth-check": "^2.0.1" 660 | }, 661 | "funding": { 662 | "url": "https://github.com/sponsors/fb55" 663 | } 664 | }, 665 | "node_modules/css-what": { 666 | "version": "6.1.0", 667 | "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", 668 | "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", 669 | "dev": true, 670 | "engines": { 671 | "node": ">= 6" 672 | }, 673 | "funding": { 674 | "url": "https://github.com/sponsors/fb55" 675 | } 676 | }, 677 | "node_modules/debug": { 678 | "version": "4.3.4", 679 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", 680 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 681 | "dev": true, 682 | "dependencies": { 683 | "ms": "2.1.2" 684 | }, 685 | "engines": { 686 | "node": ">=6.0" 687 | }, 688 | "peerDependenciesMeta": { 689 | "supports-color": { 690 | "optional": true 691 | } 692 | } 693 | }, 694 | "node_modules/decompress-response": { 695 | "version": "6.0.0", 696 | "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", 697 | "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", 698 | "dev": true, 699 | "optional": true, 700 | "dependencies": { 701 | "mimic-response": "^3.1.0" 702 | }, 703 | "engines": { 704 | "node": ">=10" 705 | }, 706 | "funding": { 707 | "url": "https://github.com/sponsors/sindresorhus" 708 | } 709 | }, 710 | "node_modules/deep-extend": { 711 | "version": "0.6.0", 712 | "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", 713 | "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", 714 | "dev": true, 715 | "optional": true, 716 | "engines": { 717 | "node": ">=4.0.0" 718 | } 719 | }, 720 | "node_modules/detect-libc": { 721 | "version": "2.0.1", 722 | "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", 723 | "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", 724 | "dev": true, 725 | "optional": true, 726 | "engines": { 727 | "node": ">=8" 728 | } 729 | }, 730 | "node_modules/dom-serializer": { 731 | "version": "2.0.0", 732 | "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", 733 | "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", 734 | "dev": true, 735 | "dependencies": { 736 | "domelementtype": "^2.3.0", 737 | "domhandler": "^5.0.2", 738 | "entities": "^4.2.0" 739 | }, 740 | "funding": { 741 | "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" 742 | } 743 | }, 744 | "node_modules/domelementtype": { 745 | "version": "2.3.0", 746 | "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", 747 | "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", 748 | "dev": true, 749 | "funding": [ 750 | { 751 | "type": "github", 752 | "url": "https://github.com/sponsors/fb55" 753 | } 754 | ] 755 | }, 756 | "node_modules/domhandler": { 757 | "version": "5.0.3", 758 | "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", 759 | "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", 760 | "dev": true, 761 | "dependencies": { 762 | "domelementtype": "^2.3.0" 763 | }, 764 | "engines": { 765 | "node": ">= 4" 766 | }, 767 | "funding": { 768 | "url": "https://github.com/fb55/domhandler?sponsor=1" 769 | } 770 | }, 771 | "node_modules/domutils": { 772 | "version": "3.1.0", 773 | "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", 774 | "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", 775 | "dev": true, 776 | "dependencies": { 777 | "dom-serializer": "^2.0.0", 778 | "domelementtype": "^2.3.0", 779 | "domhandler": "^5.0.3" 780 | }, 781 | "funding": { 782 | "url": "https://github.com/fb55/domutils?sponsor=1" 783 | } 784 | }, 785 | "node_modules/end-of-stream": { 786 | "version": "1.4.4", 787 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", 788 | "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", 789 | "dev": true, 790 | "optional": true, 791 | "dependencies": { 792 | "once": "^1.4.0" 793 | } 794 | }, 795 | "node_modules/entities": { 796 | "version": "4.5.0", 797 | "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", 798 | "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", 799 | "dev": true, 800 | "engines": { 801 | "node": ">=0.12" 802 | }, 803 | "funding": { 804 | "url": "https://github.com/fb55/entities?sponsor=1" 805 | } 806 | }, 807 | "node_modules/esbuild": { 808 | "version": "0.17.17", 809 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.17.tgz", 810 | "integrity": "sha512-/jUywtAymR8jR4qsa2RujlAF7Krpt5VWi72Q2yuLD4e/hvtNcFQ0I1j8m/bxq238pf3/0KO5yuXNpuLx8BE1KA==", 811 | "dev": true, 812 | "hasInstallScript": true, 813 | "bin": { 814 | "esbuild": "bin/esbuild" 815 | }, 816 | "engines": { 817 | "node": ">=12" 818 | }, 819 | "optionalDependencies": { 820 | "@esbuild/android-arm": "0.17.17", 821 | "@esbuild/android-arm64": "0.17.17", 822 | "@esbuild/android-x64": "0.17.17", 823 | "@esbuild/darwin-arm64": "0.17.17", 824 | "@esbuild/darwin-x64": "0.17.17", 825 | "@esbuild/freebsd-arm64": "0.17.17", 826 | "@esbuild/freebsd-x64": "0.17.17", 827 | "@esbuild/linux-arm": "0.17.17", 828 | "@esbuild/linux-arm64": "0.17.17", 829 | "@esbuild/linux-ia32": "0.17.17", 830 | "@esbuild/linux-loong64": "0.17.17", 831 | "@esbuild/linux-mips64el": "0.17.17", 832 | "@esbuild/linux-ppc64": "0.17.17", 833 | "@esbuild/linux-riscv64": "0.17.17", 834 | "@esbuild/linux-s390x": "0.17.17", 835 | "@esbuild/linux-x64": "0.17.17", 836 | "@esbuild/netbsd-x64": "0.17.17", 837 | "@esbuild/openbsd-x64": "0.17.17", 838 | "@esbuild/sunos-x64": "0.17.17", 839 | "@esbuild/win32-arm64": "0.17.17", 840 | "@esbuild/win32-ia32": "0.17.17", 841 | "@esbuild/win32-x64": "0.17.17" 842 | } 843 | }, 844 | "node_modules/esbuild-register": { 845 | "version": "3.4.2", 846 | "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.4.2.tgz", 847 | "integrity": "sha512-kG/XyTDyz6+YDuyfB9ZoSIOOmgyFCH+xPRtsCa8W85HLRV5Csp+o3jWVbOSHgSLfyLc5DmP+KFDNwty4mEjC+Q==", 848 | "dev": true, 849 | "dependencies": { 850 | "debug": "^4.3.4" 851 | }, 852 | "peerDependencies": { 853 | "esbuild": ">=0.12 <1" 854 | } 855 | }, 856 | "node_modules/escape-string-regexp": { 857 | "version": "1.0.5", 858 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 859 | "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", 860 | "dev": true, 861 | "engines": { 862 | "node": ">=0.8.0" 863 | } 864 | }, 865 | "node_modules/expand-template": { 866 | "version": "2.0.3", 867 | "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", 868 | "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", 869 | "dev": true, 870 | "optional": true, 871 | "engines": { 872 | "node": ">=6" 873 | } 874 | }, 875 | "node_modules/fd-slicer": { 876 | "version": "1.1.0", 877 | "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", 878 | "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", 879 | "dev": true, 880 | "dependencies": { 881 | "pend": "~1.2.0" 882 | } 883 | }, 884 | "node_modules/fs-constants": { 885 | "version": "1.0.0", 886 | "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", 887 | "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", 888 | "dev": true, 889 | "optional": true 890 | }, 891 | "node_modules/fs.realpath": { 892 | "version": "1.0.0", 893 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 894 | "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", 895 | "dev": true 896 | }, 897 | "node_modules/function-bind": { 898 | "version": "1.1.1", 899 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 900 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", 901 | "dev": true 902 | }, 903 | "node_modules/get-intrinsic": { 904 | "version": "1.2.1", 905 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", 906 | "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", 907 | "dev": true, 908 | "dependencies": { 909 | "function-bind": "^1.1.1", 910 | "has": "^1.0.3", 911 | "has-proto": "^1.0.1", 912 | "has-symbols": "^1.0.3" 913 | }, 914 | "funding": { 915 | "url": "https://github.com/sponsors/ljharb" 916 | } 917 | }, 918 | "node_modules/github-from-package": { 919 | "version": "0.0.0", 920 | "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", 921 | "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", 922 | "dev": true, 923 | "optional": true 924 | }, 925 | "node_modules/glob": { 926 | "version": "7.2.3", 927 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", 928 | "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", 929 | "dev": true, 930 | "dependencies": { 931 | "fs.realpath": "^1.0.0", 932 | "inflight": "^1.0.4", 933 | "inherits": "2", 934 | "minimatch": "^3.1.1", 935 | "once": "^1.3.0", 936 | "path-is-absolute": "^1.0.0" 937 | }, 938 | "engines": { 939 | "node": "*" 940 | }, 941 | "funding": { 942 | "url": "https://github.com/sponsors/isaacs" 943 | } 944 | }, 945 | "node_modules/has": { 946 | "version": "1.0.3", 947 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 948 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 949 | "dev": true, 950 | "dependencies": { 951 | "function-bind": "^1.1.1" 952 | }, 953 | "engines": { 954 | "node": ">= 0.4.0" 955 | } 956 | }, 957 | "node_modules/has-flag": { 958 | "version": "3.0.0", 959 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 960 | "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", 961 | "dev": true, 962 | "engines": { 963 | "node": ">=4" 964 | } 965 | }, 966 | "node_modules/has-proto": { 967 | "version": "1.0.1", 968 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", 969 | "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", 970 | "dev": true, 971 | "engines": { 972 | "node": ">= 0.4" 973 | }, 974 | "funding": { 975 | "url": "https://github.com/sponsors/ljharb" 976 | } 977 | }, 978 | "node_modules/has-symbols": { 979 | "version": "1.0.3", 980 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 981 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", 982 | "dev": true, 983 | "engines": { 984 | "node": ">= 0.4" 985 | }, 986 | "funding": { 987 | "url": "https://github.com/sponsors/ljharb" 988 | } 989 | }, 990 | "node_modules/hosted-git-info": { 991 | "version": "4.1.0", 992 | "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", 993 | "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", 994 | "dev": true, 995 | "dependencies": { 996 | "lru-cache": "^6.0.0" 997 | }, 998 | "engines": { 999 | "node": ">=10" 1000 | } 1001 | }, 1002 | "node_modules/htmlparser2": { 1003 | "version": "8.0.2", 1004 | "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", 1005 | "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", 1006 | "dev": true, 1007 | "funding": [ 1008 | "https://github.com/fb55/htmlparser2?sponsor=1", 1009 | { 1010 | "type": "github", 1011 | "url": "https://github.com/sponsors/fb55" 1012 | } 1013 | ], 1014 | "dependencies": { 1015 | "domelementtype": "^2.3.0", 1016 | "domhandler": "^5.0.3", 1017 | "domutils": "^3.0.1", 1018 | "entities": "^4.4.0" 1019 | } 1020 | }, 1021 | "node_modules/ieee754": { 1022 | "version": "1.2.1", 1023 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 1024 | "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", 1025 | "dev": true, 1026 | "funding": [ 1027 | { 1028 | "type": "github", 1029 | "url": "https://github.com/sponsors/feross" 1030 | }, 1031 | { 1032 | "type": "patreon", 1033 | "url": "https://www.patreon.com/feross" 1034 | }, 1035 | { 1036 | "type": "consulting", 1037 | "url": "https://feross.org/support" 1038 | } 1039 | ], 1040 | "optional": true 1041 | }, 1042 | "node_modules/inflight": { 1043 | "version": "1.0.6", 1044 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 1045 | "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", 1046 | "dev": true, 1047 | "dependencies": { 1048 | "once": "^1.3.0", 1049 | "wrappy": "1" 1050 | } 1051 | }, 1052 | "node_modules/inherits": { 1053 | "version": "2.0.4", 1054 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 1055 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 1056 | "dev": true 1057 | }, 1058 | "node_modules/ini": { 1059 | "version": "1.3.8", 1060 | "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", 1061 | "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", 1062 | "dev": true, 1063 | "optional": true 1064 | }, 1065 | "node_modules/jsonc-parser": { 1066 | "version": "3.2.0", 1067 | "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", 1068 | "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", 1069 | "dev": true 1070 | }, 1071 | "node_modules/keytar": { 1072 | "version": "7.9.0", 1073 | "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", 1074 | "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", 1075 | "dev": true, 1076 | "hasInstallScript": true, 1077 | "optional": true, 1078 | "dependencies": { 1079 | "node-addon-api": "^4.3.0", 1080 | "prebuild-install": "^7.0.1" 1081 | } 1082 | }, 1083 | "node_modules/leven": { 1084 | "version": "3.1.0", 1085 | "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", 1086 | "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", 1087 | "dev": true, 1088 | "engines": { 1089 | "node": ">=6" 1090 | } 1091 | }, 1092 | "node_modules/linkify-it": { 1093 | "version": "3.0.3", 1094 | "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", 1095 | "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", 1096 | "dev": true, 1097 | "dependencies": { 1098 | "uc.micro": "^1.0.1" 1099 | } 1100 | }, 1101 | "node_modules/lru-cache": { 1102 | "version": "6.0.0", 1103 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", 1104 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", 1105 | "dev": true, 1106 | "dependencies": { 1107 | "yallist": "^4.0.0" 1108 | }, 1109 | "engines": { 1110 | "node": ">=10" 1111 | } 1112 | }, 1113 | "node_modules/markdown-it": { 1114 | "version": "12.3.2", 1115 | "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", 1116 | "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", 1117 | "dev": true, 1118 | "dependencies": { 1119 | "argparse": "^2.0.1", 1120 | "entities": "~2.1.0", 1121 | "linkify-it": "^3.0.1", 1122 | "mdurl": "^1.0.1", 1123 | "uc.micro": "^1.0.5" 1124 | }, 1125 | "bin": { 1126 | "markdown-it": "bin/markdown-it.js" 1127 | } 1128 | }, 1129 | "node_modules/markdown-it/node_modules/entities": { 1130 | "version": "2.1.0", 1131 | "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", 1132 | "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", 1133 | "dev": true, 1134 | "funding": { 1135 | "url": "https://github.com/fb55/entities?sponsor=1" 1136 | } 1137 | }, 1138 | "node_modules/mdurl": { 1139 | "version": "1.0.1", 1140 | "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", 1141 | "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", 1142 | "dev": true 1143 | }, 1144 | "node_modules/mime": { 1145 | "version": "1.6.0", 1146 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 1147 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 1148 | "dev": true, 1149 | "bin": { 1150 | "mime": "cli.js" 1151 | }, 1152 | "engines": { 1153 | "node": ">=4" 1154 | } 1155 | }, 1156 | "node_modules/mimic-response": { 1157 | "version": "3.1.0", 1158 | "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", 1159 | "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", 1160 | "dev": true, 1161 | "optional": true, 1162 | "engines": { 1163 | "node": ">=10" 1164 | }, 1165 | "funding": { 1166 | "url": "https://github.com/sponsors/sindresorhus" 1167 | } 1168 | }, 1169 | "node_modules/minimatch": { 1170 | "version": "3.1.2", 1171 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 1172 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 1173 | "dev": true, 1174 | "dependencies": { 1175 | "brace-expansion": "^1.1.7" 1176 | }, 1177 | "engines": { 1178 | "node": "*" 1179 | } 1180 | }, 1181 | "node_modules/minimist": { 1182 | "version": "1.2.8", 1183 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", 1184 | "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", 1185 | "dev": true, 1186 | "optional": true, 1187 | "funding": { 1188 | "url": "https://github.com/sponsors/ljharb" 1189 | } 1190 | }, 1191 | "node_modules/mkdirp-classic": { 1192 | "version": "0.5.3", 1193 | "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", 1194 | "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", 1195 | "dev": true, 1196 | "optional": true 1197 | }, 1198 | "node_modules/ms": { 1199 | "version": "2.1.2", 1200 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 1201 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 1202 | "dev": true 1203 | }, 1204 | "node_modules/mute-stream": { 1205 | "version": "0.0.8", 1206 | "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", 1207 | "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", 1208 | "dev": true 1209 | }, 1210 | "node_modules/napi-build-utils": { 1211 | "version": "1.0.2", 1212 | "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", 1213 | "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", 1214 | "dev": true, 1215 | "optional": true 1216 | }, 1217 | "node_modules/node-abi": { 1218 | "version": "3.40.0", 1219 | "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.40.0.tgz", 1220 | "integrity": "sha512-zNy02qivjjRosswoYmPi8hIKJRr8MpQyeKT6qlcq/OnOgA3Rhoae+IYOqsM9V5+JnHWmxKnWOT2GxvtqdtOCXA==", 1221 | "dev": true, 1222 | "optional": true, 1223 | "dependencies": { 1224 | "semver": "^7.3.5" 1225 | }, 1226 | "engines": { 1227 | "node": ">=10" 1228 | } 1229 | }, 1230 | "node_modules/node-abi/node_modules/semver": { 1231 | "version": "7.5.1", 1232 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", 1233 | "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", 1234 | "dev": true, 1235 | "optional": true, 1236 | "dependencies": { 1237 | "lru-cache": "^6.0.0" 1238 | }, 1239 | "bin": { 1240 | "semver": "bin/semver.js" 1241 | }, 1242 | "engines": { 1243 | "node": ">=10" 1244 | } 1245 | }, 1246 | "node_modules/node-addon-api": { 1247 | "version": "4.3.0", 1248 | "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", 1249 | "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", 1250 | "dev": true, 1251 | "optional": true 1252 | }, 1253 | "node_modules/nth-check": { 1254 | "version": "2.1.1", 1255 | "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", 1256 | "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", 1257 | "dev": true, 1258 | "dependencies": { 1259 | "boolbase": "^1.0.0" 1260 | }, 1261 | "funding": { 1262 | "url": "https://github.com/fb55/nth-check?sponsor=1" 1263 | } 1264 | }, 1265 | "node_modules/object-inspect": { 1266 | "version": "1.12.3", 1267 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", 1268 | "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", 1269 | "dev": true, 1270 | "funding": { 1271 | "url": "https://github.com/sponsors/ljharb" 1272 | } 1273 | }, 1274 | "node_modules/once": { 1275 | "version": "1.4.0", 1276 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 1277 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 1278 | "dev": true, 1279 | "dependencies": { 1280 | "wrappy": "1" 1281 | } 1282 | }, 1283 | "node_modules/parse-semver": { 1284 | "version": "1.1.1", 1285 | "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", 1286 | "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", 1287 | "dev": true, 1288 | "dependencies": { 1289 | "semver": "^5.1.0" 1290 | } 1291 | }, 1292 | "node_modules/parse5": { 1293 | "version": "7.1.2", 1294 | "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", 1295 | "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", 1296 | "dev": true, 1297 | "dependencies": { 1298 | "entities": "^4.4.0" 1299 | }, 1300 | "funding": { 1301 | "url": "https://github.com/inikulin/parse5?sponsor=1" 1302 | } 1303 | }, 1304 | "node_modules/parse5-htmlparser2-tree-adapter": { 1305 | "version": "7.0.0", 1306 | "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", 1307 | "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", 1308 | "dev": true, 1309 | "dependencies": { 1310 | "domhandler": "^5.0.2", 1311 | "parse5": "^7.0.0" 1312 | }, 1313 | "funding": { 1314 | "url": "https://github.com/inikulin/parse5?sponsor=1" 1315 | } 1316 | }, 1317 | "node_modules/path-is-absolute": { 1318 | "version": "1.0.1", 1319 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 1320 | "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", 1321 | "dev": true, 1322 | "engines": { 1323 | "node": ">=0.10.0" 1324 | } 1325 | }, 1326 | "node_modules/pend": { 1327 | "version": "1.2.0", 1328 | "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", 1329 | "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", 1330 | "dev": true 1331 | }, 1332 | "node_modules/prebuild-install": { 1333 | "version": "7.1.1", 1334 | "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", 1335 | "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", 1336 | "dev": true, 1337 | "optional": true, 1338 | "dependencies": { 1339 | "detect-libc": "^2.0.0", 1340 | "expand-template": "^2.0.3", 1341 | "github-from-package": "0.0.0", 1342 | "minimist": "^1.2.3", 1343 | "mkdirp-classic": "^0.5.3", 1344 | "napi-build-utils": "^1.0.1", 1345 | "node-abi": "^3.3.0", 1346 | "pump": "^3.0.0", 1347 | "rc": "^1.2.7", 1348 | "simple-get": "^4.0.0", 1349 | "tar-fs": "^2.0.0", 1350 | "tunnel-agent": "^0.6.0" 1351 | }, 1352 | "bin": { 1353 | "prebuild-install": "bin.js" 1354 | }, 1355 | "engines": { 1356 | "node": ">=10" 1357 | } 1358 | }, 1359 | "node_modules/pump": { 1360 | "version": "3.0.0", 1361 | "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", 1362 | "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", 1363 | "dev": true, 1364 | "optional": true, 1365 | "dependencies": { 1366 | "end-of-stream": "^1.1.0", 1367 | "once": "^1.3.1" 1368 | } 1369 | }, 1370 | "node_modules/qs": { 1371 | "version": "6.11.2", 1372 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", 1373 | "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", 1374 | "dev": true, 1375 | "dependencies": { 1376 | "side-channel": "^1.0.4" 1377 | }, 1378 | "engines": { 1379 | "node": ">=0.6" 1380 | }, 1381 | "funding": { 1382 | "url": "https://github.com/sponsors/ljharb" 1383 | } 1384 | }, 1385 | "node_modules/rc": { 1386 | "version": "1.2.8", 1387 | "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", 1388 | "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", 1389 | "dev": true, 1390 | "optional": true, 1391 | "dependencies": { 1392 | "deep-extend": "^0.6.0", 1393 | "ini": "~1.3.0", 1394 | "minimist": "^1.2.0", 1395 | "strip-json-comments": "~2.0.1" 1396 | }, 1397 | "bin": { 1398 | "rc": "cli.js" 1399 | } 1400 | }, 1401 | "node_modules/read": { 1402 | "version": "1.0.7", 1403 | "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", 1404 | "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", 1405 | "dev": true, 1406 | "dependencies": { 1407 | "mute-stream": "~0.0.4" 1408 | }, 1409 | "engines": { 1410 | "node": ">=0.8" 1411 | } 1412 | }, 1413 | "node_modules/readable-stream": { 1414 | "version": "3.6.2", 1415 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", 1416 | "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", 1417 | "dev": true, 1418 | "optional": true, 1419 | "dependencies": { 1420 | "inherits": "^2.0.3", 1421 | "string_decoder": "^1.1.1", 1422 | "util-deprecate": "^1.0.1" 1423 | }, 1424 | "engines": { 1425 | "node": ">= 6" 1426 | } 1427 | }, 1428 | "node_modules/rimraf": { 1429 | "version": "3.0.2", 1430 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", 1431 | "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", 1432 | "dev": true, 1433 | "dependencies": { 1434 | "glob": "^7.1.3" 1435 | }, 1436 | "bin": { 1437 | "rimraf": "bin.js" 1438 | }, 1439 | "funding": { 1440 | "url": "https://github.com/sponsors/isaacs" 1441 | } 1442 | }, 1443 | "node_modules/safe-buffer": { 1444 | "version": "5.2.1", 1445 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 1446 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 1447 | "dev": true, 1448 | "funding": [ 1449 | { 1450 | "type": "github", 1451 | "url": "https://github.com/sponsors/feross" 1452 | }, 1453 | { 1454 | "type": "patreon", 1455 | "url": "https://www.patreon.com/feross" 1456 | }, 1457 | { 1458 | "type": "consulting", 1459 | "url": "https://feross.org/support" 1460 | } 1461 | ], 1462 | "optional": true 1463 | }, 1464 | "node_modules/sax": { 1465 | "version": "1.2.4", 1466 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", 1467 | "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", 1468 | "dev": true 1469 | }, 1470 | "node_modules/semver": { 1471 | "version": "5.7.1", 1472 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 1473 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", 1474 | "dev": true, 1475 | "bin": { 1476 | "semver": "bin/semver" 1477 | } 1478 | }, 1479 | "node_modules/side-channel": { 1480 | "version": "1.0.4", 1481 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", 1482 | "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", 1483 | "dev": true, 1484 | "dependencies": { 1485 | "call-bind": "^1.0.0", 1486 | "get-intrinsic": "^1.0.2", 1487 | "object-inspect": "^1.9.0" 1488 | }, 1489 | "funding": { 1490 | "url": "https://github.com/sponsors/ljharb" 1491 | } 1492 | }, 1493 | "node_modules/simple-concat": { 1494 | "version": "1.0.1", 1495 | "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", 1496 | "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", 1497 | "dev": true, 1498 | "funding": [ 1499 | { 1500 | "type": "github", 1501 | "url": "https://github.com/sponsors/feross" 1502 | }, 1503 | { 1504 | "type": "patreon", 1505 | "url": "https://www.patreon.com/feross" 1506 | }, 1507 | { 1508 | "type": "consulting", 1509 | "url": "https://feross.org/support" 1510 | } 1511 | ], 1512 | "optional": true 1513 | }, 1514 | "node_modules/simple-get": { 1515 | "version": "4.0.1", 1516 | "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", 1517 | "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", 1518 | "dev": true, 1519 | "funding": [ 1520 | { 1521 | "type": "github", 1522 | "url": "https://github.com/sponsors/feross" 1523 | }, 1524 | { 1525 | "type": "patreon", 1526 | "url": "https://www.patreon.com/feross" 1527 | }, 1528 | { 1529 | "type": "consulting", 1530 | "url": "https://feross.org/support" 1531 | } 1532 | ], 1533 | "optional": true, 1534 | "dependencies": { 1535 | "decompress-response": "^6.0.0", 1536 | "once": "^1.3.1", 1537 | "simple-concat": "^1.0.0" 1538 | } 1539 | }, 1540 | "node_modules/string_decoder": { 1541 | "version": "1.3.0", 1542 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 1543 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 1544 | "dev": true, 1545 | "optional": true, 1546 | "dependencies": { 1547 | "safe-buffer": "~5.2.0" 1548 | } 1549 | }, 1550 | "node_modules/strip-json-comments": { 1551 | "version": "2.0.1", 1552 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", 1553 | "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", 1554 | "dev": true, 1555 | "optional": true, 1556 | "engines": { 1557 | "node": ">=0.10.0" 1558 | } 1559 | }, 1560 | "node_modules/supports-color": { 1561 | "version": "5.5.0", 1562 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 1563 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 1564 | "dev": true, 1565 | "dependencies": { 1566 | "has-flag": "^3.0.0" 1567 | }, 1568 | "engines": { 1569 | "node": ">=4" 1570 | } 1571 | }, 1572 | "node_modules/tar-fs": { 1573 | "version": "2.1.1", 1574 | "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", 1575 | "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", 1576 | "dev": true, 1577 | "optional": true, 1578 | "dependencies": { 1579 | "chownr": "^1.1.1", 1580 | "mkdirp-classic": "^0.5.2", 1581 | "pump": "^3.0.0", 1582 | "tar-stream": "^2.1.4" 1583 | } 1584 | }, 1585 | "node_modules/tar-stream": { 1586 | "version": "2.2.0", 1587 | "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", 1588 | "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", 1589 | "dev": true, 1590 | "optional": true, 1591 | "dependencies": { 1592 | "bl": "^4.0.3", 1593 | "end-of-stream": "^1.4.1", 1594 | "fs-constants": "^1.0.0", 1595 | "inherits": "^2.0.3", 1596 | "readable-stream": "^3.1.1" 1597 | }, 1598 | "engines": { 1599 | "node": ">=6" 1600 | } 1601 | }, 1602 | "node_modules/tmp": { 1603 | "version": "0.2.1", 1604 | "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", 1605 | "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", 1606 | "dev": true, 1607 | "dependencies": { 1608 | "rimraf": "^3.0.0" 1609 | }, 1610 | "engines": { 1611 | "node": ">=8.17.0" 1612 | } 1613 | }, 1614 | "node_modules/tunnel": { 1615 | "version": "0.0.6", 1616 | "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", 1617 | "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", 1618 | "dev": true, 1619 | "engines": { 1620 | "node": ">=0.6.11 <=0.7.0 || >=0.7.3" 1621 | } 1622 | }, 1623 | "node_modules/tunnel-agent": { 1624 | "version": "0.6.0", 1625 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 1626 | "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", 1627 | "dev": true, 1628 | "optional": true, 1629 | "dependencies": { 1630 | "safe-buffer": "^5.0.1" 1631 | }, 1632 | "engines": { 1633 | "node": "*" 1634 | } 1635 | }, 1636 | "node_modules/typed-rest-client": { 1637 | "version": "1.8.9", 1638 | "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.9.tgz", 1639 | "integrity": "sha512-uSmjE38B80wjL85UFX3sTYEUlvZ1JgCRhsWj/fJ4rZ0FqDUFoIuodtiVeE+cUqiVTOKPdKrp/sdftD15MDek6g==", 1640 | "dev": true, 1641 | "dependencies": { 1642 | "qs": "^6.9.1", 1643 | "tunnel": "0.0.6", 1644 | "underscore": "^1.12.1" 1645 | } 1646 | }, 1647 | "node_modules/typescript": { 1648 | "version": "4.9.5", 1649 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", 1650 | "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", 1651 | "dev": true, 1652 | "bin": { 1653 | "tsc": "bin/tsc", 1654 | "tsserver": "bin/tsserver" 1655 | }, 1656 | "engines": { 1657 | "node": ">=4.2.0" 1658 | } 1659 | }, 1660 | "node_modules/uc.micro": { 1661 | "version": "1.0.6", 1662 | "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", 1663 | "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", 1664 | "dev": true 1665 | }, 1666 | "node_modules/underscore": { 1667 | "version": "1.13.6", 1668 | "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", 1669 | "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", 1670 | "dev": true 1671 | }, 1672 | "node_modules/url-join": { 1673 | "version": "4.0.1", 1674 | "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", 1675 | "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", 1676 | "dev": true 1677 | }, 1678 | "node_modules/util-deprecate": { 1679 | "version": "1.0.2", 1680 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 1681 | "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", 1682 | "dev": true, 1683 | "optional": true 1684 | }, 1685 | "node_modules/wrappy": { 1686 | "version": "1.0.2", 1687 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1688 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 1689 | "dev": true 1690 | }, 1691 | "node_modules/xmlbuilder": { 1692 | "version": "11.0.1", 1693 | "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", 1694 | "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", 1695 | "dev": true, 1696 | "engines": { 1697 | "node": ">=4.0" 1698 | } 1699 | }, 1700 | "node_modules/yallist": { 1701 | "version": "4.0.0", 1702 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 1703 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", 1704 | "dev": true 1705 | }, 1706 | "node_modules/yauzl": { 1707 | "version": "2.10.0", 1708 | "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", 1709 | "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", 1710 | "dev": true, 1711 | "dependencies": { 1712 | "buffer-crc32": "~0.2.3", 1713 | "fd-slicer": "~1.1.0" 1714 | } 1715 | }, 1716 | "node_modules/yazl": { 1717 | "version": "2.5.1", 1718 | "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", 1719 | "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", 1720 | "dev": true, 1721 | "dependencies": { 1722 | "buffer-crc32": "~0.2.3" 1723 | } 1724 | } 1725 | } 1726 | } 1727 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "markdoc-language-support", 3 | "displayName": "Markdoc language support", 4 | "private": true, 5 | "icon": "logo.png", 6 | "preview": true, 7 | "author": "Ryan Paul", 8 | "publisher": "stripe", 9 | "license": "MIT", 10 | "version": "0.0.13", 11 | "description": "A Markdoc language server and Visual Studio Code extension", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/markdoc/language-server.git" 15 | }, 16 | "main": "./dist/client/index.js", 17 | "bin": { 18 | "markdoc-ls": "dist/server/wrapper.js" 19 | }, 20 | "scripts": { 21 | "build": "node build.mjs", 22 | "build:watch": "node build.mjs --watch", 23 | "build:types": "tsc --emitDeclarationOnly --outDir dist", 24 | "build:extension": "vsce package --out dist/markdoc.vsix", 25 | "test": "node -r esbuild-register --test server/**/*.test.ts", 26 | "test:watch": "node -r esbuild-register --watch --test server/**/*.test.ts" 27 | }, 28 | "engines": { 29 | "vscode": "^1.63.0" 30 | }, 31 | "devDependencies": { 32 | "@types/node": "^18.11.18", 33 | "esbuild": "0.17.17", 34 | "esbuild-register": "^3.4.2", 35 | "typescript": "^4.9.3", 36 | "@vscode/vsce": "^2.19.0" 37 | }, 38 | "activationEvents": [], 39 | "contributes": { 40 | "configuration": { 41 | "title": "Markdoc Language Server", 42 | "properties": { 43 | "markdoc.config.path": { 44 | "type": "string", 45 | "scope": "resource", 46 | "default": "markdoc.config.json", 47 | "description": "Path to Markdoc configuration" 48 | } 49 | } 50 | }, 51 | "views": { 52 | "explorer": [ 53 | { 54 | "id": "markdocPartials", 55 | "name": "Partials", 56 | "when": "markdoc.active" 57 | } 58 | ] 59 | }, 60 | "commands": [ 61 | { 62 | "command": "markdoc.newFileFromTemplate", 63 | "title": "Markdoc file from template" 64 | }, 65 | { 66 | "command": "markdoc.controlService", 67 | "title": "Control the Markdoc language server" 68 | }, 69 | { 70 | "command": "markdoc.preview", 71 | "title": "Display a rendered preview of Markdoc content", 72 | "icon": "$(ports-open-browser-icon)" 73 | } 74 | ], 75 | "menus": { 76 | "file/newFile": [ 77 | { 78 | "command": "markdoc.newFileFromTemplate", 79 | "when": "markdoc.enabled" 80 | } 81 | ], 82 | "editor/title": [ 83 | { 84 | "command": "markdoc.preview", 85 | "when": "resourceLangId == markdoc && markdoc.enabled && markdoc.canPreview", 86 | "group": "navigation" 87 | } 88 | ] 89 | }, 90 | "languages": [ 91 | { 92 | "id": "markdoc", 93 | "aliases": [ 94 | "Markdoc", 95 | "markdoc" 96 | ], 97 | "extensions": [ 98 | ".markdoc", 99 | ".markdoc.md", 100 | ".mdoc" 101 | ], 102 | "configuration": "./language-configuration.json" 103 | } 104 | ], 105 | "grammars": [ 106 | { 107 | "language": "markdoc", 108 | "scopeName": "text.html.markdown.markdoc", 109 | "path": "./syntaxes/markdoc.tmLanguage.json" 110 | }, 111 | { 112 | "injectTo": [ 113 | "text.html.markdown", 114 | "source.markup.markdoc" 115 | ], 116 | "scopeName": "text.html.markdown.markdoc.injection", 117 | "path": "./syntaxes/markdoc.markdown.tmLanguage.json", 118 | "embeddedLanguages": { 119 | "meta.embedded.inline.json": "json" 120 | } 121 | } 122 | ] 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /server/index.ts: -------------------------------------------------------------------------------- 1 | export * as Plugins from "./plugins"; 2 | export * as Services from "./services"; 3 | 4 | export * from "./types"; 5 | export * from "./utils"; 6 | export * from "./server"; 7 | -------------------------------------------------------------------------------- /server/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@markdoc/language-server", 3 | "version": "0.0.9", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@markdoc/language-server", 9 | "version": "0.0.9", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "@markdoc/markdoc": "^0.3.3", 13 | "@types/picomatch": "^2.3.0", 14 | "picomatch": "^2.3.1", 15 | "typescript": "^5.0.4", 16 | "vscode-languageserver": "^8.0.2", 17 | "vscode-languageserver-textdocument": "^1.0.7", 18 | "vscode-uri": "^3.0.7", 19 | "yaml": "^2.2.1" 20 | } 21 | }, 22 | "node_modules/@markdoc/markdoc": { 23 | "version": "0.3.3", 24 | "resolved": "https://registry.npmjs.org/@markdoc/markdoc/-/markdoc-0.3.3.tgz", 25 | "integrity": "sha512-z9vd8KO914o7vd+ojxnOHxvDXDE2nPNexyLFCl5wxKmqbg9SE6JyrtnFs0rOdliOgXqWctev0eg60qQsi2vdGA==", 26 | "dev": true, 27 | "engines": { 28 | "node": ">=14.7.0" 29 | }, 30 | "optionalDependencies": { 31 | "@types/markdown-it": "12.2.3" 32 | }, 33 | "peerDependencies": { 34 | "@types/react": "*", 35 | "react": "*" 36 | }, 37 | "peerDependenciesMeta": { 38 | "@types/react": { 39 | "optional": true 40 | }, 41 | "react": { 42 | "optional": true 43 | } 44 | } 45 | }, 46 | "node_modules/@types/linkify-it": { 47 | "version": "3.0.2", 48 | "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", 49 | "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==", 50 | "dev": true, 51 | "optional": true 52 | }, 53 | "node_modules/@types/markdown-it": { 54 | "version": "12.2.3", 55 | "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", 56 | "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", 57 | "dev": true, 58 | "optional": true, 59 | "dependencies": { 60 | "@types/linkify-it": "*", 61 | "@types/mdurl": "*" 62 | } 63 | }, 64 | "node_modules/@types/mdurl": { 65 | "version": "1.0.2", 66 | "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", 67 | "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==", 68 | "dev": true, 69 | "optional": true 70 | }, 71 | "node_modules/@types/picomatch": { 72 | "version": "2.3.0", 73 | "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-2.3.0.tgz", 74 | "integrity": "sha512-O397rnSS9iQI4OirieAtsDqvCj4+3eY1J+EPdNTKuHuRWIfUoGyzX294o8C4KJYaLqgSrd2o60c5EqCU8Zv02g==", 75 | "dev": true 76 | }, 77 | "node_modules/picomatch": { 78 | "version": "2.3.1", 79 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 80 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 81 | "dev": true, 82 | "engines": { 83 | "node": ">=8.6" 84 | }, 85 | "funding": { 86 | "url": "https://github.com/sponsors/jonschlinkert" 87 | } 88 | }, 89 | "node_modules/typescript": { 90 | "version": "5.0.4", 91 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", 92 | "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", 93 | "dev": true, 94 | "bin": { 95 | "tsc": "bin/tsc", 96 | "tsserver": "bin/tsserver" 97 | }, 98 | "engines": { 99 | "node": ">=12.20" 100 | } 101 | }, 102 | "node_modules/vscode-jsonrpc": { 103 | "version": "8.1.0", 104 | "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz", 105 | "integrity": "sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw==", 106 | "dev": true, 107 | "engines": { 108 | "node": ">=14.0.0" 109 | } 110 | }, 111 | "node_modules/vscode-languageserver": { 112 | "version": "8.1.0", 113 | "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-8.1.0.tgz", 114 | "integrity": "sha512-eUt8f1z2N2IEUDBsKaNapkz7jl5QpskN2Y0G01T/ItMxBxw1fJwvtySGB9QMecatne8jFIWJGWI61dWjyTLQsw==", 115 | "dev": true, 116 | "dependencies": { 117 | "vscode-languageserver-protocol": "3.17.3" 118 | }, 119 | "bin": { 120 | "installServerIntoExtension": "bin/installServerIntoExtension" 121 | } 122 | }, 123 | "node_modules/vscode-languageserver-protocol": { 124 | "version": "3.17.3", 125 | "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.3.tgz", 126 | "integrity": "sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA==", 127 | "dev": true, 128 | "dependencies": { 129 | "vscode-jsonrpc": "8.1.0", 130 | "vscode-languageserver-types": "3.17.3" 131 | } 132 | }, 133 | "node_modules/vscode-languageserver-textdocument": { 134 | "version": "1.0.8", 135 | "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.8.tgz", 136 | "integrity": "sha512-1bonkGqQs5/fxGT5UchTgjGVnfysL0O8v1AYMBjqTbWQTFn721zaPGDYFkOKtfDgFiSgXM3KwaG3FMGfW4Ed9Q==", 137 | "dev": true 138 | }, 139 | "node_modules/vscode-languageserver-types": { 140 | "version": "3.17.3", 141 | "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz", 142 | "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==", 143 | "dev": true 144 | }, 145 | "node_modules/vscode-uri": { 146 | "version": "3.0.7", 147 | "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.7.tgz", 148 | "integrity": "sha512-eOpPHogvorZRobNqJGhapa0JdwaxpjVvyBp0QIUMRMSf8ZAlqOdEquKuRmw9Qwu0qXtJIWqFtMkmvJjUZmMjVA==", 149 | "dev": true 150 | }, 151 | "node_modules/yaml": { 152 | "version": "2.2.2", 153 | "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz", 154 | "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==", 155 | "dev": true, 156 | "engines": { 157 | "node": ">= 14" 158 | } 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@markdoc/language-server", 3 | "version": "0.0.13", 4 | "description": "A Markdoc language server", 5 | "main": "dist/index.js", 6 | "author": "Ryan Paul", 7 | "license": "MIT", 8 | "devDependencies": { 9 | "@markdoc/markdoc": "^0.3.3", 10 | "@types/picomatch": "^2.3.0", 11 | "picomatch": "^2.3.1", 12 | "typescript": "^5.0.4", 13 | "vscode-languageserver": "^8.0.2", 14 | "vscode-languageserver-textdocument": "^1.0.7", 15 | "vscode-uri": "^3.0.7", 16 | "yaml": "^2.2.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /server/plugins/codeaction.ts: -------------------------------------------------------------------------------- 1 | import * as LSP from "vscode-languageserver/node"; 2 | import * as Markdoc from "@markdoc/markdoc"; 3 | 4 | import type { Config, ServiceInstances } from "../types"; 5 | 6 | export default class CodeActionProvider { 7 | constructor( 8 | protected config: Config, 9 | protected connection: LSP.Connection, 10 | protected services: ServiceInstances 11 | ) { 12 | connection.onCodeAction(this.onCodeAction.bind(this)); 13 | connection.onCodeActionResolve(this.onCodeActionResolve.bind(this)); 14 | } 15 | 16 | register(registration: LSP.BulkRegistration) { 17 | registration.add(LSP.CodeActionRequest.type, {documentSelector: null, resolveProvider: true}); 18 | } 19 | 20 | convertTable(uri: string, line: number): LSP.WorkspaceEdit | void { 21 | const ast = this.services.Documents.ast(uri); 22 | if (!ast) return; 23 | 24 | for (const node of ast.walk()) { 25 | if (node.type === 'table' && node.lines.includes(line)) { 26 | const content = new Markdoc.Ast.Node('tag', {}, [node], 'table'); 27 | const newText = Markdoc.format(content); 28 | const [start, end] = node.lines; 29 | const range = LSP.Range.create(start, 0, end + 1, 0); 30 | return {changes: {[uri]: [{range, newText}]}}; 31 | } 32 | } 33 | } 34 | 35 | async inlinePartial(uri: string, line: number, file: string): Promise { 36 | const ast = this.services.Documents.ast(uri); 37 | if (!ast) return; 38 | 39 | for (const node of ast.walk()) { 40 | if (node.tag === 'partial' && node.lines[0] === line && !node.attributes.variables) { 41 | const fullPath = this.services.Scanner.fullPath(file); 42 | const newText = await this.services.Scanner.read(fullPath); 43 | const [start, end] = node.lines; 44 | const range = LSP.Range.create(start, 0, end, 0); 45 | return {changes: {[uri]: [{range, newText}]}}; 46 | } 47 | } 48 | } 49 | 50 | findActions(ast: Markdoc.Node, params: LSP.CodeActionParams): LSP.CodeAction[] { 51 | const output: LSP.CodeAction[] = []; 52 | const {line} = params.range.start; 53 | const {uri} = params.textDocument; 54 | 55 | if (params.range.end.line - params.range.start.line > 3) 56 | output.push({ 57 | title: 'Extract content to new partial', 58 | command: LSP.Command.create('Extract Partial', 'markdoc.extractPartial') 59 | }); 60 | 61 | for (const node of ast.walk()) { 62 | if (node.type === 'table' && node.lines.includes(line)) { 63 | output.push({ 64 | data: {type: 'convertTable', uri, line}, 65 | title: 'Convert to Markdoc Table', 66 | }); 67 | 68 | continue; 69 | } 70 | 71 | if (node.tag === 'partial' && node.lines[0] === line && !node.attributes.variables) { 72 | output.push({ 73 | data: {type: 'inlinePartial', uri, line, file: node.attributes.file}, 74 | title: 'Inline contents of this partial', 75 | }); 76 | 77 | continue; 78 | } 79 | } 80 | 81 | return output; 82 | } 83 | 84 | onCodeAction(params: LSP.CodeActionParams): (LSP.CodeAction | LSP.Command)[] { 85 | const ast = this.services.Documents.ast(params.textDocument.uri); 86 | return ast ? this.findActions(ast, params) : []; 87 | } 88 | 89 | async onCodeActionResolve(action: LSP.CodeAction): Promise { 90 | if (!action.data?.type) return action; 91 | 92 | if (action.data.type === 'convertTable') { 93 | const {uri, line} = action.data; 94 | const edit = this.convertTable(uri, line); 95 | if (edit) return {...action, edit}; 96 | } 97 | 98 | if (action.data.type === 'inlinePartial') { 99 | const {uri, line, file} = action.data; 100 | const edit = await this.inlinePartial(uri, line, file); 101 | if (edit) return {...action, edit}; 102 | } 103 | 104 | return action; 105 | } 106 | } -------------------------------------------------------------------------------- /server/plugins/completion.ts: -------------------------------------------------------------------------------- 1 | import * as LSP from "vscode-languageserver/node"; 2 | import * as Markdoc from "@markdoc/markdoc"; 3 | import type { Config, ServiceInstances } from "../types"; 4 | 5 | type ResolveFn = (item: LSP.CompletionItem) => LSP.CompletionItem; 6 | 7 | type Completion = { 8 | match: RegExp; 9 | complete: ( 10 | params: LSP.CompletionParams, 11 | matches: RegExpMatchArray, 12 | text: string 13 | ) => LSP.CompletionItem[]; 14 | }; 15 | 16 | export default class CompletionProvider { 17 | protected completions: Completion[] = [ 18 | { 19 | match: /\{%([ ]*)(\/)?[^ ]+$/, 20 | complete: (params, matches, text) => { 21 | const uri = params.textDocument.uri; 22 | const schema = this.services.Schema.get(uri); 23 | if (!schema?.tags) return []; 24 | 25 | return Object.keys(schema.tags).map((label) => ({ 26 | data: { 27 | resolve: "tag", uri, 28 | block: text.trim() === matches[0], 29 | spacing: matches[1], 30 | closing: matches[2], 31 | pos: params.position 32 | }, 33 | label, 34 | })); 35 | }, 36 | }, 37 | 38 | { 39 | match: /.*\{%[ ]*([a-zA-Z-_]+)[^\}]* ([a-zA-Z-_]+)="?[^ ]+$/, 40 | complete: (params, matches) => { 41 | const [tagName, attrName] = matches.slice(1); 42 | const uri = params.textDocument.uri; 43 | const schema = this.services.Schema.get(uri); 44 | const attr = schema?.tags?.[tagName]?.attributes?.[attrName]; 45 | 46 | if (!attr?.matches) return []; 47 | 48 | let accepts: Markdoc.SchemaMatches = 49 | typeof attr.matches === "function" 50 | ? attr.matches(schema ?? {}) 51 | : attr.matches; 52 | 53 | if (!Array.isArray(accepts)) return []; 54 | 55 | const completions: LSP.CompletionItem[] = []; 56 | for (const option of accepts) { 57 | if (typeof option === "object") continue; 58 | completions.push({ label: `${option}` }); 59 | } 60 | 61 | return completions; 62 | }, 63 | }, 64 | 65 | { 66 | match: /(? { 68 | const routes = this.services.Scanner.routes.keys(); 69 | return Array.from(routes).map((label) => ({ 70 | insertText: label.slice(1), 71 | label, 72 | })); 73 | }, 74 | }, 75 | ]; 76 | 77 | protected resolvers: Record = { 78 | tag: (item) => { 79 | const schema = this.services.Schema.get(item.data.uri); 80 | const config = schema?.tags?.[item.label]; 81 | 82 | if (!config) return item; 83 | 84 | if (item.data.block) { 85 | const ast = this.services.Documents.ast(item.data.uri); 86 | if (ast) { 87 | for (const node of ast.walk()) 88 | if (node.tag && node.lines.includes(item.data.pos.line)) 89 | return {label: item.label}; 90 | } 91 | } else { 92 | const doc = this.services.Documents.get(item.data.uri); 93 | if (doc) { 94 | const pos: LSP.Position = item.data.pos; 95 | const range = LSP.Range.create(pos.line, pos.character, pos.line + 1, 0); 96 | const text = doc.getText(range); 97 | if (text.match(/^(?:(?!{%).)+%}/)) 98 | return {label: item.label}; 99 | } 100 | } 101 | 102 | if (item.data.closing) 103 | return {...item, insertText: `${item.label} %}`}; 104 | 105 | const attrs = Object.entries(config.attributes ?? {}); 106 | const required = attrs.filter(([_, { required }]) => required); 107 | 108 | let index = 1; 109 | let attrText = required 110 | .map(([name]) => ` ${name}=\${${index++}}`) 111 | .join(""); 112 | 113 | if (required.length < attrs.length) attrText += `\${${index++}}`; 114 | 115 | const spacing = item.data.spacing?.length > 0 ? '' : ' '; 116 | const text = config.selfClosing 117 | ? `${item.label}${attrText} /%}` 118 | : `${spacing}${item.label}${attrText} %}\n\$0\n{% /${item.label} %}`; 119 | 120 | return { 121 | ...item, 122 | insertText: item.data.block ? text : text.replaceAll("\n", ""), 123 | insertTextFormat: LSP.InsertTextFormat.Snippet, 124 | kind: LSP.CompletionItemKind.Function, 125 | documentation: config.description ?? "", 126 | }; 127 | }, 128 | }; 129 | 130 | constructor( 131 | protected config: Config, 132 | protected connection: LSP.Connection, 133 | protected services: ServiceInstances 134 | ) { 135 | connection.onCompletion(this.onCompletion.bind(this)); 136 | connection.onCompletionResolve(this.onCompletionResolve.bind(this)); 137 | } 138 | 139 | register(registration: LSP.BulkRegistration) { 140 | registration.add(LSP.CompletionRequest.type, { 141 | documentSelector: null, 142 | resolveProvider: true, 143 | triggerCharacters: [], 144 | }); 145 | } 146 | 147 | protected onCompletion(params: LSP.CompletionParams): LSP.CompletionItem[] { 148 | const doc = this.services.Documents.get(params.textDocument.uri); 149 | if (!doc) return []; 150 | 151 | const { 152 | position: { line, character }, 153 | } = params; 154 | 155 | const range = LSP.Range.create(line, 0, line, character); 156 | const text = doc.getText(range); 157 | 158 | for (const completion of this.completions) { 159 | const matches = text.match(completion.match); 160 | if (matches) return completion.complete?.(params, matches, text) ?? []; 161 | } 162 | 163 | return []; 164 | } 165 | 166 | protected onCompletionResolve(item: LSP.CompletionItem): LSP.CompletionItem { 167 | if (!item.data?.resolve) return item; 168 | return this.resolvers[item.data.resolve]?.(item) ?? item; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /server/plugins/definition.test.ts: -------------------------------------------------------------------------------- 1 | import * as LSP from "vscode-languageserver/node"; 2 | import * as Markdoc from "@markdoc/markdoc"; 3 | import * as assert from "node:assert"; 4 | import { test } from "node:test"; 5 | import DefinitionProvider from "./definition"; 6 | 7 | type DefinitionCallback = ( 8 | params: LSP.DefinitionParams 9 | ) => LSP.Definition | null; 10 | 11 | type ReferenceCallback = (params: LSP.ReferenceParams) => LSP.Location[] | null; 12 | 13 | class ConnectionMock { 14 | private referenceCallback?: ReferenceCallback; 15 | private definitionCallback?: DefinitionCallback; 16 | 17 | onDefinition(callback: DefinitionCallback) { 18 | this.definitionCallback = callback; 19 | } 20 | 21 | simulateDefinition(params: LSP.DefinitionParams) { 22 | return this.definitionCallback?.(params); 23 | } 24 | 25 | onReferences(callback: ReferenceCallback) { 26 | this.referenceCallback = callback; 27 | } 28 | 29 | simulateReference(params: LSP.ReferenceParams) { 30 | return this.referenceCallback?.(params); 31 | } 32 | } 33 | 34 | const example1 = ` 35 | # Example 1 36 | 37 | {% partial file="foo.md" /%} 38 | 39 | This is a test 40 | 41 | {% partial file="bar.md" /%} 42 | `; 43 | 44 | const example2 = ` 45 | # Example 2 46 | 47 | {% partial /%} 48 | `; 49 | 50 | test("definition provider", async (t) => { 51 | await t.test("find partial at line", async (t) => { 52 | const connection = new ConnectionMock(); 53 | // @ts-expect-error 54 | const provider = new DefinitionProvider({}, connection, {}); 55 | const ast = Markdoc.parse(example1); 56 | 57 | await t.test("simple example", () => { 58 | assert.strictEqual(provider.getPartialAtLine(ast, 3), "foo.md"); 59 | assert.strictEqual(provider.getPartialAtLine(ast, 7), "bar.md"); 60 | }); 61 | 62 | await t.test("without a partial", () => { 63 | assert.strictEqual(provider.getPartialAtLine(ast, 5), undefined); 64 | }); 65 | 66 | await t.test("with missing file attribute", () => { 67 | const ast = Markdoc.parse(example2); 68 | assert.strictEqual(provider.getPartialAtLine(ast, 3), undefined); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /server/plugins/definition.ts: -------------------------------------------------------------------------------- 1 | import * as LSP from "vscode-languageserver/node"; 2 | import type * as Markdoc from "@markdoc/markdoc"; 3 | import type { Config, ServiceInstances } from "../types"; 4 | 5 | export default class DefinitionProvider { 6 | constructor( 7 | protected config: Config, 8 | protected connection: LSP.Connection, 9 | protected services: ServiceInstances 10 | ) { 11 | connection.onDefinition(this.onDefinition.bind(this)); 12 | connection.onReferences(this.onReferences.bind(this)); 13 | } 14 | 15 | register(registration: LSP.BulkRegistration) { 16 | registration.add(LSP.DefinitionRequest.type, { documentSelector: null }); 17 | registration.add(LSP.ReferencesRequest.type, { documentSelector: null }); 18 | } 19 | 20 | getPartialAtLine(ast: Markdoc.Node, line: number): string | void { 21 | for (const node of ast.walk()) { 22 | if (!node.lines.includes(line)) continue; 23 | if (node.type !== "tag" || node.tag !== "partial") continue; 24 | 25 | const { file } = node.attributes; 26 | if (typeof file === "string") return file; 27 | } 28 | } 29 | 30 | onDefinition(params: LSP.DefinitionParams): LSP.Definition | null { 31 | const ast = this.services.Documents.ast(params.textDocument.uri); 32 | if (!ast) return null; 33 | 34 | const file = this.getPartialAtLine(ast, params.position.line); 35 | if (!file) return null; 36 | 37 | const uri = this.services.Scanner.fullPath(file); 38 | return { uri, range: LSP.Range.create(0, 0, 0, 0) }; 39 | } 40 | 41 | onReferences(params: LSP.ReferenceParams): LSP.Location[] | null { 42 | const ast = this.services.Documents.ast(params.textDocument.uri); 43 | if (!ast) return null; 44 | 45 | const file = this.getPartialAtLine(ast, params.position.line); 46 | if (!file) return null; 47 | 48 | const output: LSP.Location[] = []; 49 | for (const [key, { partials }] of this.services.Scanner.entries()) { 50 | const partial = partials.find((p) => p.attributes.file === file); 51 | if (!partial) continue; 52 | 53 | const [line] = partial?.lines ?? []; 54 | const uri = this.services.Scanner.fullPath(key); 55 | output.push({ uri, range: LSP.Range.create(line, 0, line, 0) }); 56 | } 57 | 58 | return output; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /server/plugins/dependencies.ts: -------------------------------------------------------------------------------- 1 | import * as LSP from "vscode-languageserver/node"; 2 | import type { 3 | DependencyInfo, 4 | PartialReference, 5 | Config, 6 | ServiceInstances, 7 | } from "../types"; 8 | 9 | export default class DependenciesProvider { 10 | constructor( 11 | protected config: Config, 12 | protected connection: LSP.Connection, 13 | protected services: ServiceInstances 14 | ) { 15 | connection.onRequest( 16 | "markdoc.getDependencies", 17 | this.onGetDependencies.bind(this) 18 | ); 19 | } 20 | 21 | onGetDependencies(file: string): DependencyInfo { 22 | return { 23 | dependencies: this.dependencies(file), 24 | dependents: this.dependents(file), 25 | }; 26 | } 27 | 28 | dependents(file: string): PartialReference[] { 29 | const output: PartialReference[] = []; 30 | for (const [key, { partials }] of this.services.Scanner.entries()) { 31 | const partial = partials.find((p) => p.attributes.file === file); 32 | if (partial) output.push({ file: key, line: partial.lines[0] }); 33 | } 34 | 35 | return output; 36 | } 37 | 38 | dependencies(file: string): PartialReference[] { 39 | const { partials = [] } = this.services.Scanner.get(file) ?? {}; 40 | return partials.map(({ attributes: { file }, lines: [line] }) => ({ 41 | file, 42 | line, 43 | children: this.dependencies(file), 44 | })); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /server/plugins/folding.test.ts: -------------------------------------------------------------------------------- 1 | import * as Markdoc from "@markdoc/markdoc"; 2 | import * as assert from "node:assert"; 3 | import { test } from "node:test"; 4 | import FoldingProvider from "./folding"; 5 | 6 | const example1 = ` 7 | # Example 1 8 | 9 | {% foo %} 10 | test 11 | {% /foo %} 12 | `; 13 | 14 | const example2 = ` 15 | # Example 2 16 | 17 | {% foo %} 18 | {% bar %} 19 | test 20 | 21 | {% baz %} 22 | test 23 | {% /baz %} 24 | {% /bar %} 25 | 26 | test 27 | {% /foo %} 28 | `; 29 | 30 | const connectionMock = { onFoldingRanges(...args) {} }; 31 | 32 | test("folding provider", async (t) => { 33 | // @ts-expect-error 34 | const provider = new FoldingProvider({}, connectionMock, {}); 35 | 36 | await t.test("simple example", () => { 37 | const ast = Markdoc.parse(example1); 38 | // @ts-expect-error 39 | const ranges = provider.ranges(ast); 40 | assert.equal(ranges.length, 1); 41 | assert.deepEqual(ranges, [{ startLine: 3, endLine: 5 }]); 42 | }); 43 | 44 | await t.test("nested example", () => { 45 | const ast = Markdoc.parse(example2); 46 | // @ts-expect-error 47 | const ranges = provider.ranges(ast); 48 | assert.equal(ranges.length, 3); 49 | assert.deepEqual(ranges, [ 50 | { startLine: 3, endLine: 13 }, 51 | { startLine: 4, endLine: 10 }, 52 | { startLine: 7, endLine: 9 }, 53 | ]); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /server/plugins/folding.ts: -------------------------------------------------------------------------------- 1 | import * as LSP from "vscode-languageserver/node"; 2 | import type * as Markdoc from "@markdoc/markdoc"; 3 | import type { Config, ServiceInstances } from "../types"; 4 | import * as utils from "../utils"; 5 | 6 | export default class FoldingProvider { 7 | constructor( 8 | protected config: Config, 9 | protected connection: LSP.Connection, 10 | protected services: ServiceInstances 11 | ) { 12 | connection.onFoldingRanges(this.onFoldingRange.bind(this)); 13 | } 14 | 15 | register(registration: LSP.BulkRegistration) { 16 | registration.add(LSP.FoldingRangeRequest.type, { documentSelector: null }); 17 | } 18 | 19 | protected ranges(ast: Markdoc.Node) { 20 | return Array.from(utils.getBlockRanges(ast)).map(({ start, end }) => 21 | LSP.FoldingRange.create(start, end) 22 | ); 23 | } 24 | 25 | protected onFoldingRange(params: LSP.FoldingRangeParams): LSP.FoldingRange[] { 26 | const ast = this.services.Documents.ast(params.textDocument.uri); 27 | return ast ? this.ranges(ast) : []; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /server/plugins/formatting.test.ts: -------------------------------------------------------------------------------- 1 | import * as LSP from "vscode-languageserver/node"; 2 | import { TextDocument } from "vscode-languageserver-textdocument"; 3 | import { test } from "node:test"; 4 | import assert from "node:assert"; 5 | import FormattingProvider from "./formatting"; 6 | 7 | const example1 = ` 8 | # This is a test {% id="foo" %} 9 | 10 | * This is an example [foo](/bar) 11 | `; 12 | 13 | const example1Formatted = ` 14 | # This is a test {% #foo %} 15 | 16 | * This is an example [foo](/bar) 17 | `; 18 | 19 | const example1LastLine = ` 20 | * This is an example [foo](/bar) 21 | `; 22 | 23 | const mockConnection = { 24 | onDocumentFormatting(...params) {}, 25 | onDocumentRangeFormatting(...params) {}, 26 | }; 27 | 28 | function mockDoc(content: string) { 29 | return TextDocument.create("file:///content.md", "markdoc", 0, content); 30 | } 31 | 32 | test("formatting provider", async (t) => { 33 | // @ts-expect-error 34 | const provider = new FormattingProvider({}, mockConnection, {Commands: {add(...args: any) {}}}); 35 | 36 | await t.test("simple full-text formatting", () => { 37 | const doc = mockDoc(example1); 38 | const [output] = provider.formatRange(doc); 39 | assert.strictEqual(output.newText.trim(), example1Formatted.trim()); 40 | }); 41 | 42 | await t.test("formatting a specific line", () => { 43 | const doc = mockDoc(example1); 44 | const [output] = provider.formatRange(doc, LSP.Range.create(3, 0, 4, 0)); 45 | assert.strictEqual(output.newText.trim(), example1LastLine.trim()); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /server/plugins/formatting.ts: -------------------------------------------------------------------------------- 1 | import * as LSP from "vscode-languageserver/node"; 2 | import * as Markdoc from "@markdoc/markdoc"; 3 | 4 | import type { Config, ServiceInstances } from "../types"; 5 | import { TextDocument } from "vscode-languageserver-textdocument"; 6 | 7 | export default class FormattingProvider { 8 | protected tokenizer: Markdoc.Tokenizer; 9 | 10 | constructor( 11 | protected config: Config, 12 | protected connection: LSP.Connection, 13 | protected services: ServiceInstances 14 | ) { 15 | this.tokenizer = new Markdoc.Tokenizer(config.markdoc ?? {}); 16 | connection.onDocumentFormatting(this.onDocumentFormatting.bind(this)); 17 | connection.onDocumentRangeFormatting(this.onRangeFormatting.bind(this)); 18 | } 19 | 20 | register(registration: LSP.BulkRegistration) { 21 | registration.add(LSP.DocumentFormattingRequest.type, { 22 | documentSelector: null, 23 | }); 24 | 25 | registration.add(LSP.DocumentRangeFormattingRequest.type, { 26 | documentSelector: null, 27 | }); 28 | } 29 | 30 | convertTable(uri: string, line: number) { 31 | const ast = this.services.Documents.ast(uri); 32 | if (!ast) return; 33 | 34 | for (const node of ast.walk()) 35 | if (node.type === 'table' && node.lines.includes(line)) { 36 | const content = new Markdoc.Ast.Node('tag', {}, [node], 'table'); 37 | const newText = Markdoc.format(content); 38 | const [start, end] = node.lines; 39 | const range = LSP.Range.create(start, 0, end + 1, 0); 40 | 41 | const wschange = new LSP.WorkspaceChange(); 42 | const edit = wschange.getTextEditChange(uri); 43 | edit.replace(range, newText); 44 | this.connection.workspace.applyEdit(wschange.edit); 45 | } 46 | } 47 | 48 | formatRange(doc: TextDocument, range?: LSP.Range) { 49 | const actualRange = range 50 | ? LSP.Range.create(range.start.line, 0, range.end.line + 1, 0) 51 | : LSP.Range.create(0, 0, doc.lineCount, 0); 52 | 53 | const text = doc.getText(actualRange); 54 | const tokens = this.tokenizer.tokenize(text); 55 | const ast = Markdoc.parse(tokens, { slots: this.config.markdoc?.slots }); 56 | const output = Markdoc.format(ast); 57 | 58 | return [LSP.TextEdit.replace(actualRange, output)]; 59 | } 60 | 61 | onDocumentFormatting(params: LSP.DocumentFormattingParams) { 62 | const doc = this.services.Documents.get(params.textDocument.uri); 63 | return doc && this.formatRange(doc); 64 | } 65 | 66 | onRangeFormatting(params: LSP.DocumentRangeFormattingParams) { 67 | const doc = this.services.Documents.get(params.textDocument.uri); 68 | return doc && this.formatRange(doc, params.range); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /server/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import CodeActionProvider from "./codeaction"; 2 | import CompletionProvider from "./completion"; 3 | import DefinitionProvider from "./definition"; 4 | import DependenciesProvider from "./dependencies"; 5 | import FoldingProvider from "./folding"; 6 | import FormattingProvider from "./formatting"; 7 | import LinkedEditProvider from "./linkedEdit"; 8 | import LinkProvider from "./link"; 9 | import SelectionRangeProvider from "./range"; 10 | import SymbolProvider from "./symbols"; 11 | import ValidationProvider from "./validation"; 12 | import Watch from "./watch"; 13 | 14 | export { 15 | CodeActionProvider, 16 | CompletionProvider, 17 | DefinitionProvider, 18 | DependenciesProvider, 19 | FoldingProvider, 20 | FormattingProvider, 21 | LinkedEditProvider, 22 | LinkProvider, 23 | SelectionRangeProvider, 24 | SymbolProvider, 25 | ValidationProvider, 26 | Watch, 27 | }; 28 | -------------------------------------------------------------------------------- /server/plugins/link.ts: -------------------------------------------------------------------------------- 1 | import * as LSP from "vscode-languageserver/node"; 2 | import * as utils from "../utils"; 3 | import type { Config, ServiceInstances } from "../types"; 4 | 5 | export default class LinkProvider { 6 | constructor( 7 | protected config: Config, 8 | protected connection: LSP.Connection, 9 | protected services: ServiceInstances 10 | ) { 11 | connection.onDocumentLinks(this.onDocumentLinks.bind(this)); 12 | } 13 | 14 | register(registration: LSP.BulkRegistration) { 15 | registration.add(LSP.DocumentLinkRequest.type, { documentSelector: null }); 16 | } 17 | 18 | protected onDocumentLinks({ textDocument: { uri } }: LSP.DocumentLinkParams) { 19 | const { Documents, Scanner } = this.services; 20 | const doc = Documents.get(uri); 21 | const ast = Documents.ast(uri); 22 | 23 | if (!ast || !doc) return []; 24 | 25 | const links: LSP.DocumentLink[] = []; 26 | for (const node of ast.walk()) { 27 | if (node.type === "link" && node.attributes.href?.startsWith("/")) { 28 | const { href } = node.attributes; 29 | if (typeof href !== "string") continue; 30 | 31 | const [url] = href.split("#"); 32 | const relative = Scanner.routes.get(url); 33 | if (!relative) continue; 34 | 35 | const range = utils.getContentRangeInLine(node.lines[0], doc, href); 36 | if (range) links.push({ target: Scanner.fullPath(relative), range }); 37 | continue; 38 | } 39 | 40 | if (node.type === "tag" && node.tag === "partial") { 41 | const { file } = node.attributes; 42 | if (typeof file !== "string") continue; 43 | 44 | const range = utils.getContentRangeInLine(node.lines[0], doc, file); 45 | if (range) links.push({ target: Scanner.fullPath(file), range }); 46 | } 47 | } 48 | 49 | return links; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /server/plugins/linkedEdit.ts: -------------------------------------------------------------------------------- 1 | import * as LSP from "vscode-languageserver/node"; 2 | import * as utils from "../utils"; 3 | import type { Config, ServiceInstances } from "../types"; 4 | 5 | export default class LinkedEditProvider { 6 | constructor( 7 | protected config: Config, 8 | protected connection: LSP.Connection, 9 | protected services: ServiceInstances 10 | ) { 11 | connection.languages.onLinkedEditingRange( 12 | this.onLinkedEditingRange.bind(this) 13 | ); 14 | } 15 | 16 | register(registration: LSP.BulkRegistration) { 17 | registration.add(LSP.LinkedEditingRangeRequest.type, { 18 | documentSelector: null, 19 | }); 20 | } 21 | 22 | onLinkedEditingRange( 23 | params: LSP.LinkedEditingRangeParams 24 | ): LSP.LinkedEditingRanges | undefined { 25 | const doc = this.services.Documents.get(params.textDocument.uri); 26 | const ast = this.services.Documents.ast(params.textDocument.uri); 27 | if (!ast || !doc) return; 28 | 29 | for (const { start, end, tag } of utils.getBlockRanges(ast)) { 30 | if (tag && [start, end].includes(params.position.line)) { 31 | const open = utils.getContentRangeInLine(start, doc, tag); 32 | const close = utils.getContentRangeInLine(end, doc, tag); 33 | if (open && close) return { ranges: [open, close] }; 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /server/plugins/range.test.ts: -------------------------------------------------------------------------------- 1 | import * as LSP from "vscode-languageserver/node"; 2 | import * as Markdoc from "@markdoc/markdoc"; 3 | import * as assert from "node:assert"; 4 | import { test } from "node:test"; 5 | import SelectionRangeProvider from "./range"; 6 | 7 | const example1 = ` 8 | # Example 1 9 | 10 | {% foo %} 11 | test 12 | {% /foo %} 13 | `; 14 | 15 | const example2 = ` 16 | # Example 2 17 | 18 | {% foo %} 19 | {% bar %} 20 | test 21 | 22 | {% baz %} 23 | test 24 | {% /baz %} 25 | {% /bar %} 26 | 27 | test 28 | {% /foo %} 29 | `; 30 | 31 | const connectionMock = { onSelectionRanges(...args) {} }; 32 | 33 | test("folding provider", async (t) => { 34 | // @ts-expect-error 35 | const provider = new SelectionRangeProvider({}, connectionMock, {}); 36 | 37 | await t.test("simple example", () => { 38 | const ast = Markdoc.parse(example1); 39 | const range = provider.findSelectionRange(ast, LSP.Position.create(4, 3)); 40 | assert.deepEqual(range, { 41 | parent: { 42 | parent: undefined, 43 | range: { 44 | start: { line: 3, character: 0 }, 45 | end: { line: 6, character: 0 }, 46 | }, 47 | }, 48 | range: { 49 | start: { line: 4, character: 0 }, 50 | end: { line: 5, character: 0 }, 51 | }, 52 | }); 53 | }); 54 | 55 | await t.test("nested example", () => { 56 | const ast = Markdoc.parse(example2); 57 | const range = provider.findSelectionRange(ast, LSP.Position.create(8, 3)); 58 | const expected = { 59 | parent: { 60 | parent: { 61 | parent: { 62 | parent: { 63 | parent: { 64 | parent: undefined, 65 | range: { 66 | start: { line: 3, character: 0 }, 67 | end: { line: 14, character: 0 }, 68 | }, 69 | }, 70 | range: { 71 | start: { line: 4, character: 0 }, 72 | end: { line: 13, character: 0 }, 73 | }, 74 | }, 75 | range: { 76 | start: { line: 4, character: 0 }, 77 | end: { line: 11, character: 0 }, 78 | }, 79 | }, 80 | range: { 81 | start: { line: 5, character: 0 }, 82 | end: { line: 10, character: 0 }, 83 | }, 84 | }, 85 | range: { 86 | start: { line: 7, character: 0 }, 87 | end: { line: 10, character: 0 }, 88 | }, 89 | }, 90 | range: { 91 | start: { line: 8, character: 0 }, 92 | end: { line: 9, character: 0 }, 93 | }, 94 | }; 95 | 96 | assert.deepEqual(range, expected); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /server/plugins/range.ts: -------------------------------------------------------------------------------- 1 | import * as LSP from "vscode-languageserver/node"; 2 | import * as utils from "../utils"; 3 | import type { Config, ServiceInstances } from "../types"; 4 | import type * as Markdoc from "@markdoc/markdoc"; 5 | 6 | export default class SelectionRangeProvider { 7 | constructor( 8 | protected config: Config, 9 | protected connection: LSP.Connection, 10 | protected services: ServiceInstances 11 | ) { 12 | connection.onSelectionRanges(this.onSelectionRange.bind(this)); 13 | } 14 | 15 | register(registration: LSP.BulkRegistration) { 16 | registration.add(LSP.SelectionRangeRequest.type, { 17 | documentSelector: null, 18 | }); 19 | } 20 | 21 | findSelectionRange(ast: Markdoc.Node, position: LSP.Position) { 22 | let currentRange: LSP.SelectionRange | undefined; 23 | for (const range of utils.getBlockRanges(ast)) { 24 | if (range.start > position.line) break; 25 | if (range.end > position.line) { 26 | currentRange = { 27 | range: LSP.Range.create(range.start + 1, 0, range.end, 0), 28 | parent: { 29 | range: LSP.Range.create(range.start, 0, range.end + 1, 0), 30 | parent: currentRange, 31 | }, 32 | }; 33 | } 34 | } 35 | 36 | return currentRange ?? { range: LSP.Range.create(position, position) }; 37 | } 38 | 39 | onSelectionRange({ textDocument, positions }: LSP.SelectionRangeParams) { 40 | const ast = this.services.Documents.ast(textDocument.uri); 41 | if (ast) 42 | return positions.map((position) => 43 | this.findSelectionRange(ast, position) 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /server/plugins/symbols.ts: -------------------------------------------------------------------------------- 1 | import * as LSP from "vscode-languageserver/node"; 2 | import { toPlainText } from "../utils"; 3 | import type { Config, ServiceInstances } from "../types"; 4 | import type * as Markdoc from "@markdoc/markdoc"; 5 | 6 | export default class SymbolProvider { 7 | constructor( 8 | protected config: Config, 9 | protected connection: LSP.Connection, 10 | protected services: ServiceInstances 11 | ) { 12 | connection.onDocumentSymbol(this.onDocumentSymbol.bind(this)); 13 | } 14 | 15 | register(registration: LSP.BulkRegistration) { 16 | registration.add(LSP.DocumentSymbolRequest.type, { 17 | documentSelector: null, 18 | }); 19 | } 20 | 21 | headings(node: Markdoc.Node) { 22 | let stack: Array & { level: number }> = 23 | [{ level: 0, children: [] }]; 24 | 25 | for (const child of node.walk()) { 26 | if (child.type !== "heading" || typeof child.attributes.level !== 'number') 27 | continue; 28 | 29 | const [start, finish] = child.lines; 30 | if (!start || !finish) continue; 31 | 32 | const range = LSP.Range.create(start, 0, finish + 1, 0); 33 | const entry = { 34 | name: `${"#".repeat(child.attributes.level)} ${toPlainText(child)}`, 35 | level: child.attributes.level, 36 | kind: LSP.SymbolKind.Key, 37 | range, 38 | selectionRange: range, 39 | children: [], 40 | }; 41 | 42 | while (entry.level <= stack[stack.length - 1].level) 43 | stack.pop(); 44 | 45 | stack[stack.length - 1].children?.push(entry); 46 | 47 | if (entry.level > stack[stack.length - 1].level) 48 | stack.push(entry); 49 | } 50 | 51 | return stack[0].children; 52 | } 53 | 54 | onDocumentSymbol({ textDocument }: LSP.DocumentSymbolParams) { 55 | const ast = this.services.Documents.ast(textDocument.uri); 56 | return ast && this.headings(ast); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /server/plugins/validation.ts: -------------------------------------------------------------------------------- 1 | import * as LSP from "vscode-languageserver/node"; 2 | import * as Markdoc from "@markdoc/markdoc"; 3 | import { URI } from "vscode-uri"; 4 | 5 | import type { Config, ServiceInstances, TextChangeEvent } from "../types"; 6 | 7 | export default class ValidationProvider { 8 | constructor( 9 | protected config: Config, 10 | protected connection: LSP.Connection, 11 | protected services: ServiceInstances 12 | ) { 13 | services.Documents.onDidSave(this.onDidSave, this); 14 | services.Documents.onDidClose(this.onDidClose, this); 15 | services.Documents.onDidChangeContent(this.onDidChangeContent, this); 16 | } 17 | 18 | severity(level: string): LSP.DiagnosticSeverity { 19 | const { Information, Warning, Error } = LSP.DiagnosticSeverity; 20 | switch (level) { 21 | case "debug": 22 | case "info": 23 | return Information; 24 | case "warning": 25 | return Warning; 26 | default: 27 | return Error; 28 | } 29 | } 30 | 31 | diagnostic(err: Markdoc.ValidateError): LSP.Diagnostic { 32 | const { 33 | lines: [line], 34 | location, 35 | error, 36 | } = err; 37 | 38 | return { 39 | code: error.id, 40 | range: this.createRange(line, error.location ?? location), 41 | severity: this.severity(error.level), 42 | message: error.message, 43 | source: `markdoc:${this.config.id ?? ""}`, 44 | }; 45 | } 46 | 47 | createRange(line: number = 0, location?: Markdoc.Location): LSP.Range { 48 | const { start, end } = location ?? {}; 49 | if (start?.character !== undefined && end?.character !== undefined) 50 | return LSP.Range.create( 51 | start.line, 52 | start.character, 53 | end.line - 1, 54 | end.character 55 | ); 56 | 57 | return LSP.Range.create(line, 0, line + 1, 0); 58 | } 59 | 60 | validate(uri: string) { 61 | const doc = this.services.Documents.ast(uri); 62 | const schema = this.services.Schema.get(uri); 63 | 64 | if (!schema || !doc) return; 65 | 66 | const config = this.configuration(uri); 67 | const errors = Markdoc.validate(doc, config); 68 | return errors.map((err) => this.diagnostic(err)); 69 | } 70 | 71 | configuration(uri: string): Markdoc.Config { 72 | const { Scanner, Schema } = this.services; 73 | const file = URI.parse(uri).fsPath; 74 | const metadata = Scanner.get(file); 75 | 76 | const partials: { [key: string]: boolean } = {}; 77 | for (const part of metadata?.partials ?? []) 78 | if (Scanner.has(part.attributes.file)) 79 | partials[part.attributes.file] = true; 80 | 81 | const schema = Schema?.get(uri); 82 | return { 83 | ...schema, 84 | partials, 85 | validation: { 86 | environment: 'language-server', 87 | ...schema?.validation 88 | } 89 | }; 90 | } 91 | 92 | onDidSave({ document: { uri } }: TextChangeEvent) { 93 | const diagnostics = this.validate(uri); 94 | if (diagnostics) this.connection.sendDiagnostics({ uri, diagnostics }); 95 | } 96 | 97 | onDidChangeContent({ document: { uri } }: TextChangeEvent) { 98 | const diagnostics = this.validate(uri); 99 | if (diagnostics) this.connection.sendDiagnostics({ uri, diagnostics }); 100 | } 101 | 102 | onDidClose({ document: { uri } }: TextChangeEvent) { 103 | this.connection.sendDiagnostics({ uri, diagnostics: [] }); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /server/plugins/watch.ts: -------------------------------------------------------------------------------- 1 | import * as LSP from "vscode-languageserver/node"; 2 | import pathutil from "path"; 3 | 4 | import type { FileWatchEvent, Config, ServiceInstances } from "../types"; 5 | 6 | export default class Watch { 7 | constructor( 8 | protected config: Config, 9 | protected connection: LSP.Connection, 10 | protected services: ServiceInstances 11 | ) { 12 | const content = pathutil.join(config.path, "**/*.{md,mdoc,markdoc}"); 13 | services.Watcher.add(content, this.onContentChange.bind(this)); 14 | 15 | if (config.schema?.watch && config.schema.path) 16 | services.Watcher.add(config.schema.path, this.onSchemaChange.bind(this)); 17 | } 18 | 19 | initialize() { 20 | this.services.Watcher.watch(); 21 | } 22 | 23 | onSchemaChange(changes: FileWatchEvent[]) { 24 | this.services.Schema.reload(); 25 | } 26 | 27 | onContentChange(changes: FileWatchEvent[]) { 28 | for (const change of changes) { 29 | const path = pathutil.join(this.config.root, change.path); 30 | if (change.type === LSP.FileChangeType.Deleted) 31 | this.services.Scanner.delete(path); 32 | else this.services.Scanner.update(path); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /server/server.ts: -------------------------------------------------------------------------------- 1 | import * as LSP from "vscode-languageserver/node"; 2 | import * as Services from "./services"; 3 | import * as Plugins from "./plugins"; 4 | 5 | import type { 6 | Config, 7 | DocumentMetadata, 8 | ServiceConstructors, 9 | PluginConstructors, 10 | } from "./types"; 11 | 12 | export const defaultCapabilities: LSP.ServerCapabilities = { 13 | textDocumentSync: { 14 | save: true, 15 | openClose: true, 16 | change: LSP.TextDocumentSyncKind.Full, 17 | }, 18 | }; 19 | 20 | export function connect( 21 | connection: LSP.Connection, 22 | capabilities: LSP.ServerCapabilities 23 | ): Promise { 24 | let options: any; 25 | 26 | connection.onInitialize((params) => { 27 | options = params.initializationOptions; 28 | return { capabilities }; 29 | }); 30 | 31 | return new Promise((resolve) => { 32 | connection.onInitialized(() => resolve(options)); 33 | connection.listen(); 34 | return options 35 | }); 36 | } 37 | 38 | export async function server< 39 | TConfig extends Config = Config, 40 | TMeta extends DocumentMetadata = DocumentMetadata 41 | >( 42 | serviceConstructors: ServiceConstructors = Services, 43 | pluginConstructors: PluginConstructors = Plugins, 44 | capabilities: LSP.ServerCapabilities = defaultCapabilities 45 | ) { 46 | const connection = LSP.createConnection(LSP.ProposedFeatures.all); 47 | const options = await connect(connection, capabilities); 48 | const config = options.config as TConfig; 49 | 50 | connection.sendProgress(LSP.WorkDoneProgress.type, "initialize", { 51 | kind: "begin", 52 | title: "Initializing", 53 | }); 54 | 55 | const services = Object.fromEntries( 56 | Object.entries(serviceConstructors).map(([name, service]) => [ 57 | name, 58 | new service(config, connection), 59 | ]) 60 | ); 61 | 62 | const plugins = Object.values(pluginConstructors).map( 63 | (plugin) => new plugin(config, connection, services) 64 | ); 65 | 66 | await Promise.allSettled( 67 | Object.values(services).map((service: any) => service.initialize?.()) 68 | ); 69 | 70 | const registration = LSP.BulkRegistration.create(); 71 | for (const item of [...Object.values(services), ...plugins]) 72 | item.register?.(registration); 73 | connection.client.register(registration); 74 | 75 | await Promise.allSettled(plugins.map((plugin) => plugin.initialize?.())); 76 | 77 | connection.sendProgress(LSP.WorkDoneProgress.type, "initialize", { 78 | kind: "end", 79 | }); 80 | } 81 | -------------------------------------------------------------------------------- /server/services/commands.test.ts: -------------------------------------------------------------------------------- 1 | import * as LSP from "vscode-languageserver/node"; 2 | import { test } from "node:test"; 3 | import assert from "node:assert"; 4 | import Commands from "./commands"; 5 | 6 | type ExecuteCommandCallback = (params: LSP.ExecuteCommandParams) => void; 7 | 8 | class ConnectionMock { 9 | private callback?: ExecuteCommandCallback; 10 | 11 | onExecuteCommand(callback: ExecuteCommandCallback) { 12 | this.callback = callback; 13 | } 14 | 15 | simulateCommand(params: LSP.ExecuteCommandParams) { 16 | this.callback?.(params); 17 | } 18 | } 19 | 20 | test("commands service", async (t) => { 21 | await t.test("sends command list during registration", (t) => { 22 | const connection = new ConnectionMock(); 23 | // @ts-expect-error 24 | const commands = new Commands({}, connection); 25 | commands.add("foo", () => true); 26 | commands.add("bar", () => true); 27 | commands.add("baz", () => true); 28 | 29 | const registration = LSP.BulkRegistration.create(); 30 | 31 | let items: string[] = []; 32 | const onRegister = ( 33 | _type: any, 34 | params: LSP.ExecuteCommandRegistrationOptions 35 | ) => (items = params.commands); 36 | 37 | t.mock.method(registration, "add", onRegister); 38 | commands.register(registration); 39 | assert.deepEqual(items, ["foo", "bar", "baz"]); 40 | }); 41 | 42 | await t.test("executes a command correctly", (t) => { 43 | const connection = new ConnectionMock(); 44 | // @ts-expect-error 45 | const commands = new Commands({}, connection); 46 | 47 | let params: string[] = []; 48 | commands.add("foo.bar", (param1, param2) => (params = [param1, param2])); 49 | connection.simulateCommand({ 50 | command: "foo.bar", 51 | arguments: ["baz", "qux"], 52 | }); 53 | 54 | assert.deepEqual(params, ["baz", "qux"]); 55 | }); 56 | 57 | await t.test("handles non-existent command", (t) => { 58 | const connection = new ConnectionMock(); 59 | // @ts-expect-error 60 | const commands = new Commands({}, connection); 61 | 62 | let params: string[] = []; 63 | commands.add("foo.bar", (param1, param2) => (params = [param1, param2])); 64 | connection.simulateCommand({ 65 | command: "zzzz", 66 | arguments: ["baz", "qux"], 67 | }); 68 | 69 | assert.equal(params.length, 0); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /server/services/commands.ts: -------------------------------------------------------------------------------- 1 | import * as LSP from "vscode-languageserver/node"; 2 | import type { Config } from "../types"; 3 | 4 | type Callback = (...args: any) => any; 5 | 6 | export default class Commands { 7 | protected commands = new Map(); 8 | 9 | constructor(protected config: TConfig, protected connection: LSP.Connection) { 10 | connection.onExecuteCommand(this.onCommand.bind(this)); 11 | } 12 | 13 | add(name: string, callback: Callback) { 14 | this.commands.set(name, callback); 15 | } 16 | 17 | register(registration: LSP.BulkRegistration) { 18 | registration.add(LSP.ExecuteCommandRequest.type, { 19 | commands: Array.from(this.commands.keys()), 20 | }); 21 | } 22 | 23 | protected onCommand({ command, arguments: args }: LSP.ExecuteCommandParams) { 24 | this.commands.get(command)?.(...(args ?? [])); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server/services/documents.test.ts: -------------------------------------------------------------------------------- 1 | import * as LSP from "vscode-languageserver/node"; 2 | import * as Markdoc from "@markdoc/markdoc"; 3 | import { test } from "node:test"; 4 | import assert from "node:assert"; 5 | import Documents from "./documents"; 6 | import { TextDocument } from "vscode-languageserver-textdocument"; 7 | import type { Config } from "../types"; 8 | 9 | class DocumentsMock extends Documents { 10 | constructor() { 11 | const connectionProxy = { 12 | get(target: object, prop: string, receiver: object) { 13 | return () => LSP.Disposable.create(() => true); 14 | }, 15 | }; 16 | 17 | const connection = new Proxy({}, connectionProxy); 18 | super({} as Config, connection as LSP.Connection); 19 | } 20 | } 21 | 22 | const createDoc = (filename: string, content: string) => 23 | TextDocument.create(filename, "markdown", 0, content); 24 | 25 | test("documents service", async (t) => { 26 | await t.test("change handler", async (t) => { 27 | const docs = new DocumentsMock(); 28 | const sample1 = createDoc("foo.md", "# Title\nThis is a test"); 29 | const sample2 = createDoc("bar.md", "# Title\nThis is another test"); 30 | 31 | // @ts-expect-error 32 | docs.handleChange({ document: sample1 }); 33 | // @ts-expect-error 34 | docs.handleChange({ document: sample2 }); 35 | 36 | await t.test("adds entries to the asts map", () => { 37 | // @ts-expect-error 38 | const keys = new Set(docs.asts.keys()); 39 | assert.deepEqual(keys, new Set(["foo.md", "bar.md"])); 40 | }); 41 | 42 | await t.test("parses documents", () => { 43 | const sample = docs.ast("foo.md"); 44 | assert(sample); 45 | assert.equal(sample.type, "document"); 46 | assert.equal(sample.children.length, 2); 47 | }); 48 | }); 49 | 50 | await t.test("close handler", async (t) => { 51 | const docs = new DocumentsMock(); 52 | const sample1 = createDoc("foo.md", "# Title\nThis is a test"); 53 | const sample2 = createDoc("bar.md", "# Title\nThis is another test"); 54 | 55 | // @ts-expect-error 56 | docs.handleChange({ document: sample1 }); 57 | // @ts-expect-error 58 | docs.handleChange({ document: sample2 }); 59 | 60 | // @ts-expect-error 61 | docs.handleClose({ document: sample1 }); 62 | 63 | await t.test("removes ast entries", () => { 64 | // @ts-expect-error 65 | assert.equal(docs.asts.size, 1); 66 | assert.equal(docs.ast("foo.md"), undefined); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /server/services/documents.ts: -------------------------------------------------------------------------------- 1 | import * as LSP from "vscode-languageserver/node"; 2 | import * as Markdoc from "@markdoc/markdoc"; 3 | import { TextDocument, DocumentUri } from "vscode-languageserver-textdocument"; 4 | 5 | import type { Config, TextChangeEvent } from "../types"; 6 | 7 | export default class Documents< 8 | TConfig extends Config = Config 9 | > extends LSP.TextDocuments { 10 | protected asts = new Map(); 11 | protected tokenizer: Markdoc.Tokenizer; 12 | 13 | constructor(protected config: TConfig, protected connection: LSP.Connection) { 14 | super(TextDocument); 15 | 16 | this.tokenizer = new Markdoc.Tokenizer(config.markdoc ?? {}); 17 | this.onDidOpen(this.handleChange, this); 18 | this.onDidSave(this.handleChange, this); 19 | this.onDidClose(this.handleClose, this); 20 | this.onDidChangeContent(this.handleChange, this); 21 | this.listen(connection); 22 | } 23 | 24 | ast(uri: DocumentUri) { 25 | return this.asts.get(uri); 26 | } 27 | 28 | parse(content: string, file: string) { 29 | const tokens = this.tokenizer.tokenize(content); 30 | return Markdoc.parse(tokens, { file, slots: this.config.markdoc?.slots }); 31 | } 32 | 33 | protected handleClose({ document }: TextChangeEvent) { 34 | this.asts.delete(document.uri); 35 | } 36 | 37 | protected handleChange({ document }: TextChangeEvent) { 38 | const content = this.parse(document.getText(), document.uri); 39 | this.asts.set(document.uri, content); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /server/services/index.ts: -------------------------------------------------------------------------------- 1 | import Commands from "./commands"; 2 | import Documents from "./documents"; 3 | import Scanner from "./scanner"; 4 | import Schema from "./schema"; 5 | import Watcher from "./watcher"; 6 | 7 | export { Commands, Documents, Scanner, Schema, Watcher }; 8 | -------------------------------------------------------------------------------- /server/services/scanner.test.ts: -------------------------------------------------------------------------------- 1 | import * as LSP from "vscode-languageserver/node"; 2 | import * as assert from "node:assert"; 3 | import pathutil from "node:path"; 4 | import { test } from "node:test"; 5 | import Scanner from "./scanner"; 6 | import type { Config } from "../types"; 7 | 8 | test("scanner service", async (t) => { 9 | await t.test("utility functions", async (t) => { 10 | const config = { 11 | root: "/root/directory", 12 | path: "docs/content", 13 | }; 14 | 15 | const scanner = new Scanner(config as Config); 16 | 17 | await t.test("fullPath", async (t) => { 18 | const fullPath = "/root/directory/docs/content/foo/bar/baz.md"; 19 | 20 | await t.test("with relative path", () => { 21 | assert.strictEqual(scanner.fullPath("foo/bar/baz.md"), fullPath); 22 | }); 23 | 24 | await t.test("with already full path", () => { 25 | assert.strictEqual(scanner.fullPath(fullPath), fullPath); 26 | }); 27 | 28 | await t.test("with unrelated path", () => { 29 | assert.strictEqual( 30 | scanner.fullPath("/foo/bar/baz.md"), 31 | "/foo/bar/baz.md" 32 | ); 33 | }); 34 | }); 35 | 36 | await t.test("relativePath", async (t) => { 37 | const fullPath = "/root/directory/docs/content/foo/bar/baz.md"; 38 | const relative = "foo/bar/baz.md"; 39 | 40 | await t.test("with relative path", () => { 41 | assert.strictEqual(scanner.relativePath(relative), relative); 42 | }); 43 | 44 | await t.test("with already full path", () => { 45 | assert.strictEqual(scanner.relativePath(fullPath), relative); 46 | }); 47 | 48 | await t.test("with unrelated path", () => { 49 | assert.strictEqual( 50 | scanner.relativePath("/foo/bar/baz.md"), 51 | "/foo/bar/baz.md" 52 | ); 53 | }); 54 | }); 55 | 56 | await t.test("matches", async (t) => { 57 | await t.test("does match", () => { 58 | assert.strictEqual( 59 | scanner.matches("/root/directory/docs/content/a/b/c.md"), 60 | true 61 | ); 62 | }); 63 | 64 | await t.test("does not match", async (t) => { 65 | assert.strictEqual(scanner.matches("foo/bar/baz.md"), false); 66 | assert.strictEqual( 67 | scanner.matches("/root/directory/foo/bar.md"), 68 | false 69 | ); 70 | 71 | await t.test("wrong extension", () => { 72 | assert.strictEqual( 73 | scanner.matches("/root/directory/docs/content/foo.zzz"), 74 | false 75 | ); 76 | }); 77 | }); 78 | }); 79 | }); 80 | 81 | await t.test("scanning", async (t) => { 82 | const root = pathutil.join(__dirname, "../test"); 83 | const config = { root, path: "content", routing: { frontmatter: "route" } }; 84 | const scanner = new Scanner(config as Config); 85 | await scanner.initialize(); 86 | 87 | await t.test("finds all of the files", () => { 88 | // @ts-expect-error 89 | assert.strictEqual(scanner.files.size, 3); 90 | assert.deepEqual( 91 | // @ts-expect-error 92 | Array.from(scanner.files.keys()).toSorted(), 93 | ["partials/part.md", "file-1.md", "file-2.md"].toSorted() 94 | ); 95 | }); 96 | 97 | await t.test("has function", async (t) => { 98 | await t.test("finds present files", async (t) => { 99 | assert.strictEqual(scanner.has("partials/part.md"), true); 100 | assert.strictEqual(scanner.has("file-2.md"), true); 101 | assert.strictEqual(scanner.has("partials/part.md"), true); 102 | 103 | await t.test("with full path", () => { 104 | assert.strictEqual( 105 | scanner.has(pathutil.join(root, "content/partials/part.md")), 106 | true 107 | ); 108 | }); 109 | }); 110 | 111 | await t.test("doesn't find non-existent files", () => { 112 | assert.strictEqual(scanner.has("foo/bar/baz.md"), false); 113 | }); 114 | }); 115 | 116 | await t.test("get function", async (t) => { 117 | const file = scanner.get("file-1.md"); 118 | assert.strictEqual(file?.route, "/docs/file-1"); 119 | 120 | await t.test("handles missing files", () => { 121 | assert.strictEqual(scanner.get("foo.md"), undefined); 122 | }); 123 | }); 124 | 125 | await t.test("file removal", async (t) => { 126 | const scanner = new Scanner(config as Config); 127 | await scanner.initialize(); 128 | 129 | // @ts-expect-error 130 | assert.strictEqual(scanner.files.size, 3); 131 | assert.strictEqual(scanner.routes.size, 2); 132 | scanner.delete("file-2.md"); 133 | 134 | // @ts-expect-error 135 | assert.strictEqual(scanner.files.size, 2); 136 | assert.strictEqual(scanner.routes.size, 1); 137 | assert.deepEqual(Array.from(scanner.routes.keys()), ["/docs/file-1"]); 138 | }); 139 | 140 | await t.test("extraction", async (t) => { 141 | await t.test("routing", async (t) => { 142 | const basicConfig = { root, path: "content" }; 143 | await t.test("identifies routes", async (t) => { 144 | assert.strictEqual(scanner.get("file-1.md")?.route, "/docs/file-1"); 145 | assert.strictEqual(scanner.get("file-2.md")?.route, "/docs/file-2"); 146 | assert.strictEqual(scanner.get("partials/part.md")?.route, undefined); 147 | }); 148 | 149 | await t.test("without routing config", async (t) => { 150 | const scanner = new Scanner(basicConfig as Config); 151 | await scanner.initialize(); 152 | assert.strictEqual(scanner.get("file-1.md")?.route, undefined); 153 | }); 154 | 155 | await t.test("with alternate routing config", async (t) => { 156 | const config = { ...basicConfig, routing: { frontmatter: "foo" } }; 157 | const scanner = new Scanner(config as Config); 158 | await scanner.initialize(); 159 | assert.strictEqual(scanner.get("file-1.md")?.route, "/docs/foo"); 160 | }); 161 | }); 162 | 163 | await t.test("identifies links", async (t) => { 164 | const { links } = scanner.get("file-1.md"); 165 | assert.strictEqual(links.length, 1); 166 | assert.strictEqual(links[0].attributes.href, "/docs/file-2"); 167 | }); 168 | 169 | await t.test("identifies partials", async (t) => { 170 | const { partials } = scanner.get("file-2.md"); 171 | assert.strictEqual(partials.length, 1); 172 | assert.strictEqual(partials[0].attributes.file, "partials/part.md"); 173 | }); 174 | 175 | await t.test("doesn't put route on partial", () => { 176 | const part = scanner.get("partials/part.md"); 177 | assert.strictEqual(part?.route, undefined); 178 | }); 179 | 180 | await t.test("adds partials to set", async (t) => { 181 | assert.strictEqual(scanner.partials.size, 1); 182 | assert.deepEqual(Array.from(scanner.partials), ["partials/part.md"]); 183 | }); 184 | }); 185 | }); 186 | }); 187 | -------------------------------------------------------------------------------- /server/services/scanner.ts: -------------------------------------------------------------------------------- 1 | import * as LSP from "vscode-languageserver/node"; 2 | import { promises as fs } from "fs"; 3 | import * as Markdoc from "@markdoc/markdoc"; 4 | import yaml from "yaml"; 5 | import pathutil from "path"; 6 | import { findFiles } from "../utils"; 7 | 8 | import type { DocumentMetadata, Config } from "../types"; 9 | 10 | export default class Scanner< 11 | TConfig extends Config = Config, 12 | TMeta extends DocumentMetadata = DocumentMetadata 13 | > { 14 | protected tokenizer: Markdoc.Tokenizer; 15 | protected files = new Map(); 16 | protected extensions = [".md", ".mdoc", ".markdoc"]; 17 | protected path: string; 18 | 19 | readonly routes = new Map(); 20 | readonly partials = new Set(); 21 | 22 | constructor(protected config: TConfig) { 23 | this.tokenizer = new Markdoc.Tokenizer(config.markdoc ?? {}); 24 | this.path = pathutil.join(config.root, config.path); 25 | } 26 | 27 | initialize() { 28 | return this.scan(); 29 | } 30 | 31 | has(file: string) { 32 | const path = this.relativePath(file); 33 | return this.files.has(path); 34 | } 35 | 36 | get(file: string) { 37 | const path = this.relativePath(file); 38 | return this.files.get(path); 39 | } 40 | 41 | delete(file: string) { 42 | const path = this.relativePath(file); 43 | 44 | if (this.config.routing) { 45 | const doc = this.files.get(path); 46 | if (doc?.route) this.routes.delete(doc.route); 47 | else this.partials.delete(path); 48 | } 49 | 50 | this.files.delete(path); 51 | } 52 | 53 | entries() { 54 | return this.files.entries(); 55 | } 56 | 57 | fullPath(file: string): string { 58 | return file.startsWith(this.path) 59 | ? file 60 | : file.startsWith("/") 61 | ? file 62 | : pathutil.join(this.path, file); 63 | } 64 | 65 | relativePath(file: string): string { 66 | return file.startsWith(this.path) ? file.slice(this.path.length + 1) : file; 67 | } 68 | 69 | matches(file: string): boolean { 70 | return ( 71 | file.startsWith(this.path) && 72 | this.extensions.includes(pathutil.extname(file)) 73 | ); 74 | } 75 | 76 | parse(content: string, file: string) { 77 | const tokens = this.tokenizer.tokenize(content); 78 | return Markdoc.parse(tokens, { file, slots: this.config.markdoc?.slots }); 79 | } 80 | 81 | async scan() { 82 | const promises = []; 83 | const files = findFiles(this.path, this.extensions); 84 | for await (let file of files) promises.push(this.update(file)); 85 | await Promise.allSettled(promises); 86 | } 87 | 88 | async update(file: string) { 89 | const content = await this.read(file); 90 | const ast = this.parse(content, file); 91 | const meta = this.extract(ast); 92 | const path = this.relativePath(file); 93 | 94 | if (this.config.routing) { 95 | if (meta.route) this.routes.set(meta.route, path); 96 | else this.partials.add(path); 97 | } 98 | 99 | this.files.set(path, meta); 100 | } 101 | 102 | async read(file: string): Promise { 103 | const buffer = await fs.readFile(file); 104 | return buffer.toString(); 105 | } 106 | 107 | frontmatter(content?: string) { 108 | if (!content) return {}; 109 | 110 | try { 111 | return yaml.parse(content); 112 | } catch (err) { 113 | return {}; 114 | } 115 | } 116 | 117 | extract(doc: Markdoc.Node): TMeta; 118 | extract(doc: Markdoc.Node): DocumentMetadata { 119 | const frontmatter = this.frontmatter(doc.attributes.frontmatter); 120 | 121 | const { routing } = this.config; 122 | const route = routing?.frontmatter && frontmatter[routing?.frontmatter]; 123 | const partials: Markdoc.Node[] = []; 124 | const links: Markdoc.Node[] = []; 125 | 126 | for (const node of doc.walk()) 127 | if (node.type === "tag" && node.tag === "partial") partials.push(node); 128 | else if (node.type === "link" && node.attributes?.href.startsWith("/")) 129 | links.push(node); 130 | 131 | return { frontmatter, partials, links, route }; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /server/services/schema.test.ts: -------------------------------------------------------------------------------- 1 | import * as pathutil from "node:path"; 2 | import * as assert from "node:assert"; 3 | import { test } from "node:test"; 4 | import Schema from "./schema"; 5 | import type { Config } from "../types"; 6 | 7 | const root = pathutil.join(__dirname, "../test"); 8 | 9 | test("schema service", async (t) => { 10 | await test("loads ESM schema", async (t) => { 11 | await test("with default export", async (t) => { 12 | const config = { 13 | root, 14 | schema: { path: "schemas/example-1.mjs", type: "esm" }, 15 | }; 16 | 17 | const schema = new Schema(config as Config); 18 | await schema.reload(); 19 | 20 | assert.strictEqual(schema.get()?.tags.foo.render, "foo"); 21 | }); 22 | 23 | await test("with keyed export", async (t) => { 24 | const config = { 25 | root, 26 | schema: { path: "schemas/example-2.mjs", type: "esm", property: "foo" }, 27 | }; 28 | 29 | const schema = new Schema(config as Config); 30 | await schema.reload(); 31 | assert.strictEqual(schema.get()?.tags.foo.render, "foo"); 32 | }); 33 | }); 34 | 35 | await test("loads CJS schema", async (t) => { 36 | await test("with default export", async (t) => { 37 | const config = { 38 | root, 39 | schema: { path: "schemas/example-1.js", type: "node" }, 40 | }; 41 | 42 | const schema = new Schema(config as Config); 43 | await schema.reload(); 44 | 45 | assert.strictEqual(schema.get()?.tags.foo.render, "foo"); 46 | }); 47 | 48 | await test("with keyed export", async (t) => { 49 | const config = { 50 | root, 51 | schema: { path: "schemas/example-2.mjs", type: "esm", property: "foo" }, 52 | }; 53 | 54 | const schema = new Schema(config as Config); 55 | await schema.reload(); 56 | assert.strictEqual(schema.get()?.tags.foo.render, "foo"); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /server/services/schema.ts: -------------------------------------------------------------------------------- 1 | import * as LSP from "vscode-languageserver/node"; 2 | import * as pathutil from "path"; 3 | import * as Markdoc from "@markdoc/markdoc"; 4 | import type { Config } from "../types"; 5 | 6 | export default class Schema { 7 | protected schema?: Markdoc.Config; 8 | 9 | constructor(protected config: TConfig) {} 10 | 11 | get(uri?: LSP.URI): Markdoc.Config | undefined { 12 | return this.schema; 13 | } 14 | 15 | initialize() { 16 | return this.reload(); 17 | } 18 | 19 | async reload() { 20 | const schema = await this.load(); 21 | this.schema = schema && this.merge(schema); 22 | } 23 | 24 | merge(config: Markdoc.Config) { 25 | return { 26 | ...config, 27 | tags: { 28 | ...Markdoc.tags, 29 | ...config.tags, 30 | }, 31 | nodes: { 32 | ...Markdoc.nodes, 33 | ...config.nodes, 34 | }, 35 | functions: { 36 | ...Markdoc.functions, 37 | ...config.functions, 38 | }, 39 | }; 40 | } 41 | 42 | async load(): Promise { 43 | const { schema, root } = this.config; 44 | if (!schema) return; 45 | 46 | const absolute = pathutil.join(root, schema.path); 47 | if (schema.type === "node") { 48 | delete require.cache[absolute]; 49 | const value = require(absolute); 50 | return schema.property ? value[schema.property] : value; 51 | } 52 | 53 | const value = await import(`${absolute}?${Date.now()}`); 54 | return value[schema.property ?? "default"]; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /server/services/watcher.test.ts: -------------------------------------------------------------------------------- 1 | import * as LSP from "vscode-languageserver/node"; 2 | import * as assert from "node:assert"; 3 | import { test } from "node:test"; 4 | import Watcher from "./watcher"; 5 | 6 | type WatchedFilesCallback = (params: LSP.DidChangeWatchedFilesParams) => void; 7 | 8 | class ConnectionMock { 9 | private callback?: WatchedFilesCallback; 10 | 11 | onDidChangeWatchedFiles(callback: WatchedFilesCallback) { 12 | this.callback = callback; 13 | } 14 | 15 | simulateEvent(params: LSP.DidChangeWatchedFilesParams) { 16 | this.callback?.(params); 17 | } 18 | } 19 | 20 | const config = { 21 | root: "/root", 22 | path: "docs/content", 23 | }; 24 | 25 | test("watcher service", async (t) => { 26 | const connection = new ConnectionMock(); 27 | 28 | await test("adding watches", () => { 29 | // @ts-expect-error 30 | const watcher = new Watcher(config, connection); 31 | watcher.add("foo/bar"); 32 | watcher.add("baz/qux"); 33 | 34 | // @ts-expect-error 35 | assert.equal(watcher.matchers.length, 2); 36 | // @ts-expect-error 37 | assert.equal(watcher.matchers[0].pattern, "foo/bar"); 38 | }); 39 | 40 | await test("glob matches", (t) => { 41 | let triggered = false; 42 | 43 | // @ts-expect-error 44 | const watcher = new Watcher(config, connection); 45 | watcher.add("foo/bar/*.md", () => (triggered = true)); 46 | 47 | connection.simulateEvent({ 48 | changes: [ 49 | { 50 | type: LSP.WatchKind.Change, 51 | uri: "file:///root/foo/bar/baz.md", 52 | }, 53 | ], 54 | }); 55 | 56 | assert.equal(triggered, true); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /server/services/watcher.ts: -------------------------------------------------------------------------------- 1 | import * as LSP from "vscode-languageserver/node"; 2 | import { URI } from "vscode-uri"; 3 | import pathutil from "path"; 4 | import picomatch from "picomatch"; 5 | import type { FileWatchEvent, Config } from "../types"; 6 | 7 | type WatcherCallback = (events: FileWatchEvent[]) => void; 8 | 9 | type Matcher = { 10 | pattern: LSP.Pattern; 11 | matcher?: picomatch.Matcher; 12 | callback?: WatcherCallback; 13 | }; 14 | 15 | export default class Watcher { 16 | private disposable?: LSP.Disposable; 17 | private matchers: Matcher[] = []; 18 | 19 | constructor(protected config: TConfig, protected connection: LSP.Connection) { 20 | connection.onDidChangeWatchedFiles(this.onDidChangeWatchedFiles.bind(this)); 21 | } 22 | 23 | protected onDidChangeWatchedFiles(params: LSP.DidChangeWatchedFilesParams) { 24 | const changes = params.changes.map(({ uri, type }) => ({ 25 | path: URI.parse(uri).fsPath.slice(this.config.root.length + 1), 26 | type, 27 | })); 28 | 29 | for (const { matcher, callback } of this.matchers) { 30 | if (!matcher || !callback) continue; 31 | const filtered = changes.filter((change) => matcher(change.path)); 32 | if (filtered.length) callback(filtered); 33 | } 34 | } 35 | 36 | add(pattern: LSP.Pattern, callback?: WatcherCallback) { 37 | this.matchers.push({ 38 | matcher: picomatch(pattern), 39 | pattern, 40 | callback, 41 | }); 42 | } 43 | 44 | async watch() { 45 | if (this.disposable) this.disposable.dispose(); 46 | 47 | const watchers: LSP.FileSystemWatcher[] = this.matchers.map( 48 | ({ pattern }) => ({ 49 | globPattern: pathutil.join(this.config.root, pattern), 50 | }) 51 | ); 52 | 53 | this.disposable = await this.connection.client.register( 54 | LSP.DidChangeWatchedFilesNotification.type, 55 | { watchers } 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /server/test/content/file-1.md: -------------------------------------------------------------------------------- 1 | --- 2 | route: /docs/file-1 3 | foo: /docs/foo 4 | --- 5 | 6 | # Example 1 7 | 8 | This is sample content that [links](/docs/file-2) to file 2. 9 | -------------------------------------------------------------------------------- /server/test/content/file-2.md: -------------------------------------------------------------------------------- 1 | --- 2 | route: /docs/file-2 3 | --- 4 | 5 | # Example 2 6 | 7 | This is sample content 8 | 9 | {% partial file="partials/part.md" /%} 10 | 11 | This is more content 12 | -------------------------------------------------------------------------------- /server/test/content/partials/part.md: -------------------------------------------------------------------------------- 1 | This is a partial 2 | -------------------------------------------------------------------------------- /server/test/schemas/example-1.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tags: { 3 | foo: { 4 | render: "foo", 5 | attributes: { 6 | bar: { type: String, required: true }, 7 | }, 8 | }, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /server/test/schemas/example-1.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | tags: { 3 | foo: { 4 | render: "foo", 5 | attributes: { 6 | bar: { type: String, required: true }, 7 | }, 8 | }, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /server/test/schemas/example-2.js: -------------------------------------------------------------------------------- 1 | module.exports.foo = { 2 | tags: { 3 | foo: { 4 | render: "foo", 5 | attributes: { 6 | bar: { type: String, required: true }, 7 | }, 8 | }, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /server/test/schemas/example-2.mjs: -------------------------------------------------------------------------------- 1 | export const foo = { 2 | tags: { 3 | foo: { 4 | render: "foo", 5 | attributes: { 6 | bar: { type: String, required: true }, 7 | }, 8 | }, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /server/types.ts: -------------------------------------------------------------------------------- 1 | import type { Node } from "@markdoc/markdoc"; 2 | import type * as LSP from "vscode-languageserver/node"; 3 | import type { TextDocument } from "vscode-languageserver-textdocument"; 4 | import type * as DefaultServices from "./services"; 5 | 6 | export type TextChangeEvent = LSP.TextDocumentChangeEvent; 7 | 8 | export type ServiceConstructor = new ( 9 | config: TConfig, 10 | connection: LSP.Connection 11 | ) => any; 12 | 13 | export type ServiceConstructors< 14 | TConfig extends Config = Config, 15 | TMeta extends DocumentMetadata = DocumentMetadata 16 | > = { 17 | Commands: typeof DefaultServices.Commands; 18 | Documents: typeof DefaultServices.Documents; 19 | Schema: typeof DefaultServices.Schema; 20 | Watcher: typeof DefaultServices.Watcher; 21 | Scanner: typeof DefaultServices.Scanner; 22 | [name: string]: ServiceConstructor; 23 | }; 24 | 25 | export type ServiceInstances< 26 | TServices extends ServiceConstructors< 27 | TConfig, 28 | TMeta 29 | > = typeof DefaultServices, 30 | TConfig extends Config = Config, 31 | TMeta extends DocumentMetadata = DocumentMetadata 32 | > = { 33 | [Name in keyof TServices]: InstanceType; 34 | }; 35 | 36 | export type PluginConstructor = new ( 37 | config: any, 38 | connection: LSP.Connection, 39 | services: any 40 | ) => any; 41 | 42 | export type PluginConstructors = Record; 43 | 44 | export type SchemaConfig = { 45 | path: string; 46 | type?: "node" | "esm"; 47 | property?: string; 48 | watch?: boolean; 49 | }; 50 | 51 | export type MarkdocConfig = { 52 | slots?: boolean; 53 | typographer?: boolean; 54 | allowIndentation?: boolean; 55 | allowComments?: boolean; 56 | validateFunctions?: boolean; 57 | }; 58 | 59 | export type RoutingConfig = { 60 | frontmatter: string; 61 | }; 62 | 63 | export type ServerConfig = { 64 | path: string; 65 | watch: boolean | string; 66 | }; 67 | 68 | export type TemplateConfig = { 69 | pattern?: string; 70 | }; 71 | 72 | export type Config = { 73 | id?: string; 74 | root: string; 75 | path: string; 76 | markdoc?: MarkdocConfig; 77 | schema?: SchemaConfig; 78 | routing?: RoutingConfig; 79 | server?: ServerConfig; 80 | templates?: TemplateConfig; 81 | preview?: boolean; 82 | watch?: string[]; 83 | }; 84 | 85 | export type PartialReference = { 86 | file: string; 87 | line: number; 88 | children?: PartialReference[]; 89 | }; 90 | 91 | export type DependencyInfo = { 92 | dependencies: PartialReference[]; 93 | dependents: PartialReference[]; 94 | }; 95 | 96 | export type DocumentMetadata = { 97 | frontmatter: Record; 98 | partials: Node[]; 99 | links?: Node[]; 100 | route?: string; 101 | }; 102 | 103 | export type FileWatchEvent = { 104 | path: string; 105 | type: LSP.FileChangeType; 106 | }; 107 | -------------------------------------------------------------------------------- /server/utils.ts: -------------------------------------------------------------------------------- 1 | import pathutil from "path"; 2 | import { promises as fs } from "fs"; 3 | import { TextDocument } from "vscode-languageserver-textdocument"; 4 | import * as LSP from "vscode-languageserver/node"; 5 | import type * as Markdoc from "@markdoc/markdoc"; 6 | 7 | export async function* findFiles( 8 | target: string, 9 | exts?: string[] 10 | ): AsyncIterable { 11 | const dir = await fs.readdir(target); 12 | 13 | for (const entry of dir) { 14 | const filename = pathutil.join(target, entry); 15 | const info = await fs.stat(filename); 16 | 17 | if (info.isDirectory()) yield* findFiles(filename, exts); 18 | else if (!exts || exts.includes(pathutil.extname(filename))) yield filename; 19 | } 20 | } 21 | 22 | export function toPlainText(node: Markdoc.Node): string { 23 | let output = ""; 24 | for (const child of node.walk()) 25 | if (child.type === "text") 26 | output += child.attributes.content; 27 | 28 | return output; 29 | } 30 | 31 | export function getContentRangeInLine( 32 | line: number, 33 | doc: TextDocument, 34 | text: string 35 | ) { 36 | if (typeof line !== "number" || line < 0) return null; 37 | const lineContent = doc.getText(LSP.Range.create(line, 0, line + 1, 0)); 38 | const startOffset = lineContent.indexOf(text); 39 | const endOffset = startOffset + text.length; 40 | return startOffset < 0 41 | ? null 42 | : LSP.Range.create(line, startOffset, line, endOffset); 43 | } 44 | 45 | export function* getBlockRanges(ast: Markdoc.Node) { 46 | for (const { type, lines, tag } of ast.walk()) 47 | if (type === "tag" && lines.length === 4) 48 | yield { start: lines[0], end: lines[2], tag }; 49 | } 50 | -------------------------------------------------------------------------------- /server/wrapper.ts: -------------------------------------------------------------------------------- 1 | import { server } from './server'; 2 | 3 | server(); 4 | -------------------------------------------------------------------------------- /syntaxes/markdoc.markdown.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", 3 | "scopeName": "text.html.markdown.markdoc.injection", 4 | "injectionSelector": ["L:text.html.markdown", "L:text.html.markdown.markdoc"], 5 | "patterns": [{ "include": "#tag" }], 6 | "repository": { 7 | "shortcut": { 8 | "match": "(\\$|\\.|#)([-_:a-zA-Z0-9]+)", 9 | "name": "string.other.markdoc-shortcut" 10 | }, 11 | "attribute": { 12 | "match": "([-_a-zA-Z0-9]+)(=)", 13 | "captures": { 14 | "1": { "name": "entity.other.attribute-name" }, 15 | "2": { "name": "punctuation.definition.tag.equal.markdoc" } 16 | } 17 | }, 18 | "tag": { 19 | "name": "punctuation.definition.tag", 20 | "begin": "({%)\\s*/?([-_a-zA-Z0-9]+)?", 21 | "end": "\\s*/?\\s*%}", 22 | "beginCaptures": { 23 | "1": { "name": "punctuation.definition.tag.begin.markdoc" }, 24 | "2": { "name": "entity.name.tag" } 25 | }, 26 | "endCaptures": { 27 | "0": { "name": "punctuation.definition.tag.end.markdoc" } 28 | }, 29 | "patterns": [ 30 | { "include": "#attribute" }, 31 | { "include": "#shortcut" }, 32 | { "include": "source.json" } 33 | ] 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /syntaxes/markdoc.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", 3 | "name": "markdoc", 4 | "scopeName": "text.html.markdown.markdoc", 5 | "patterns": [{ "include": "text.html.markdown" }] 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "ES2022", 5 | "lib": ["ES2022"], 6 | "allowJs": true, 7 | "allowSyntheticDefaultImports": true, 8 | "declaration": true, 9 | "moduleResolution": "node", 10 | "module": "NodeNext", 11 | "skipLibCheck": true 12 | }, 13 | "include": ["**/*.ts", "**/*.d.ts"], 14 | "exclude": ["dist", "**/*.test.ts"] 15 | } --------------------------------------------------------------------------------