├── .eslintrc.json
├── .github
├── assignment.yml
└── workflows
│ └── node.js.yml
├── .gitignore
├── .mocharc.json
├── .npmignore
├── .prettierrc.json
├── .vscode
├── launch.json
├── settings.json
└── tasks.json
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── SECURITY.md
├── build
├── copy-jsbeautify.js
├── generateData.js
├── pipeline.yml
├── remove-sourcemap-refs.js
└── update-jsbeautify.js
├── docs
├── customData.md
└── customData.schema.json
├── eslint.config.mjs
├── package-lock.json
├── package.json
├── src
├── beautify
│ ├── beautify-css.d.ts
│ ├── beautify-css.js
│ ├── beautify-html.d.ts
│ ├── beautify-html.js
│ ├── beautify-license
│ ├── beautify.ts
│ └── esm
│ │ ├── beautify-css.js
│ │ └── beautify-html.js
├── htmlLanguageService.ts
├── htmlLanguageTypes.ts
├── languageFacts
│ ├── data
│ │ └── webCustomData.ts
│ ├── dataManager.ts
│ └── dataProvider.ts
├── parser
│ ├── htmlEntities.ts
│ ├── htmlParser.ts
│ └── htmlScanner.ts
├── services
│ ├── htmlCompletion.ts
│ ├── htmlFolding.ts
│ ├── htmlFormatter.ts
│ ├── htmlHighlighting.ts
│ ├── htmlHover.ts
│ ├── htmlLinkedEditing.ts
│ ├── htmlLinks.ts
│ ├── htmlMatchingTagPosition.ts
│ ├── htmlRename.ts
│ ├── htmlSelectionRange.ts
│ ├── htmlSymbolsProvider.ts
│ └── pathCompletion.ts
├── test
│ ├── completion.test.ts
│ ├── completionParticipant.test.ts
│ ├── completionUtil.ts
│ ├── customProviders.test.ts
│ ├── folding.test.ts
│ ├── formatter.test.ts
│ ├── highlighting.test.ts
│ ├── hover.test.ts
│ ├── hoverUtil.ts
│ ├── linkedEditing.test.ts
│ ├── links.test.ts
│ ├── matchingTagPosition.test.ts
│ ├── parser.test.ts
│ ├── pathCompletionFixtures
│ │ ├── .foo.js
│ │ ├── about
│ │ │ ├── about.css
│ │ │ ├── about.html
│ │ │ └── media
│ │ │ │ └── icon.pic
│ │ ├── index.html
│ │ └── src
│ │ │ ├── feature.js
│ │ │ └── test.js
│ ├── pathCompletions.test.ts
│ ├── rename.test.ts
│ ├── scanner.test.ts
│ ├── selectionRange.test.ts
│ ├── symbols.test.ts
│ └── testUtil
│ │ ├── documentContext.ts
│ │ └── fsProvider.ts
├── tsconfig.esm.json
├── tsconfig.json
└── utils
│ ├── arrays.ts
│ ├── markup.ts
│ ├── object.ts
│ ├── paths.ts
│ ├── resources.ts
│ └── strings.ts
└── thirdpartynotices.txt
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "parserOptions": {
5 | "ecmaVersion": 6,
6 | "sourceType": "module"
7 | },
8 | "plugins": [
9 | "@typescript-eslint"
10 | ],
11 | "rules": {
12 | "@typescript-eslint/naming-convention": [
13 | "warn",
14 | {
15 | "selector": "typeLike",
16 | "format": [
17 | "PascalCase"
18 | ]
19 | }
20 | ],
21 | "@typescript-eslint/semi": "warn",
22 | "curly": "warn",
23 | "eqeqeq": "warn",
24 | "no-throw-literal": "warn",
25 | "semi": "off"
26 | }
27 | }
--------------------------------------------------------------------------------
/.github/assignment.yml:
--------------------------------------------------------------------------------
1 | {
2 | perform: true,
3 | assignees: [ aeschli ]
4 | }
5 |
--------------------------------------------------------------------------------
/.github/workflows/node.js.yml:
--------------------------------------------------------------------------------
1 | name: Node.js CI
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | strategy:
15 | matrix:
16 | node-version: [20.x]
17 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
18 |
19 | steps:
20 | - uses: actions/checkout@v3
21 | - name: Use Node.js ${{ matrix.node-version }}
22 | uses: actions/setup-node@v3
23 | with:
24 | node-version: ${{ matrix.node-version }}
25 | - run: npm ci
26 | - run: npm test
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | lib/
2 | node_modules/
3 | yarn.lock
4 |
--------------------------------------------------------------------------------
/.mocharc.json:
--------------------------------------------------------------------------------
1 | {
2 | "ui": "tdd",
3 | "color": true
4 | }
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .vscode/
2 | lib/**/*.js.map
3 | lib/*/*/*.d.ts
4 | !lib/*/*/htmlScanner.d.ts
5 | lib/*/test/
6 | src/
7 | build/
8 | .eslintrc
9 | .gitignore
10 | .github
11 | yarn.lock
12 | tslint.json
13 | .travis.yml
14 | gulpfile.js
15 | .mocharc.json
16 | .editorconfig
17 | .eslintrc.json
18 | .prettierrc.json
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 120,
3 | "semi": true,
4 | "singleQuote": true
5 | }
6 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": "Attach",
6 | "type": "node",
7 | "request": "attach",
8 | "port": 6004,
9 | "sourceMaps": true,
10 | "outFiles": ["${workspaceRoot}/lib/umd/**"]
11 | },
12 | {
13 | "name": "Unit Tests",
14 | "type": "node",
15 | "request": "launch",
16 | "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha",
17 | "stopOnEntry": false,
18 | "args": [
19 | "--timeout",
20 | "999999",
21 | "./lib/umd/test/*.js"
22 | ],
23 | "cwd": "${workspaceRoot}",
24 | "runtimeExecutable": null,
25 | "runtimeArgs": [],
26 | "env": {},
27 | "sourceMaps": true,
28 | "outFiles": ["${workspaceRoot}/lib/umd/**"],
29 | "preLaunchTask": "npm: compile"
30 | }
31 | ]
32 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.insertSpaces": false,
3 | "prettier.useTabs": true,
4 | "prettier.semi": true,
5 | "typescript.tsdk": "node_modules/typescript/lib",
6 | "git.branchProtection": [
7 | "main"
8 | ],
9 | "git.branchProtectionPrompt": "alwaysCommitToNewBranch",
10 | "git.branchRandomName.enable": true,
11 | "githubPullRequests.assignCreated": "${user}",
12 | "githubPullRequests.defaultMergeMethod": "squash"
13 | }
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "type": "npm",
6 | "script": "compile",
7 | "problemMatcher": "$tsc-watch",
8 | "isBackground": true,
9 | "presentation": {
10 | "reveal": "never"
11 | },
12 | "group": {
13 | "kind": "build",
14 | "isDefault": true
15 | }
16 | },
17 | {
18 | "type": "npm",
19 | "script": "watch",
20 | "problemMatcher": "$tsc-watch",
21 | "isBackground": true,
22 | "presentation": {
23 | "reveal": "never"
24 | },
25 | "group": {
26 | "kind": "build",
27 | "isDefault": true
28 | }
29 | }
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | 5.2.0 / 2024-03-22
4 | ================
5 | * Added `HTMLFormatConfiguration.templating` now also take a list of template languages.
6 |
7 | 5.1.0 / 2022-09-11
8 | ================
9 | * Added `LanguageService.findDocumentSymbols2`. Returns the symbols found in a document as `DocumentSymbol[]`.
10 |
11 | 5.0.0 / 2022-05-18
12 | ================
13 | * Update to `vscode-languageserver-types@3.17`
14 |
15 | 4.2.0 / 2021-11-29
16 | ==================
17 | * Added new API `htmlLanguageService.doQuoteComplete`. Called after an `attribute=`, it will compute either `""` or `''` depending on `CompletionConfiguration.attributeDefaultValue` or null, if no quote completion should be performed.
18 |
19 | 4.1.0 / 2021-09-27
20 | ==================
21 | * New settings `CompletionConfiguration.attributeDefaultValue`. Defines how attribute values are completed: With single or double quotes, or no quotes.
22 |
23 |
24 | 4.0.0 / 2020-12-14
25 | ==================
26 | * Update to `vscode-languageserver-types@3.16`
27 |
28 | 3.2.0 / 2020-11-30
29 | ==================
30 | * New parameter `HoverSettings` for `LanguageService.doHover`: Defines whether the hover contains element documentation and/or a reference to MDN.
31 | * Deprecated `LanguageService.findOnTypeRenameRanges`, replaced by New API `LanguageService.findLinkedEditingRanges`.
32 |
33 | 3.1.0 / 2020-07-29
34 | ==================
35 | * Use `TextDocument` from `vscode-languageserver-textdocument`
36 | * Fix formatting for `
` tags with optional closing
37 | * New API `LanguageService.findOnTypeRenameRanges`. For a given position, find the matching close tag so they can be renamed synchronously.
38 | * New API `LanguageServiceOptions.customDataProviders` to add the knowledge of custom tags, attributes and attribute-values and `LanguageService.setDataProviders` to update the data providers.
39 | * New API `getDefaultHTMLDataProvider` to get the default HTML data provider and `newHTMLDataProvider` to create a new provider from data.
40 | * New API `LanguageServiceOptions.fileSystemProvider` with `FileSystemProvider` to query the file system (currently used for path completion)
41 | * New API `LanguageService.doComplete2` which is synchronous and also returns path completion proposals when `LanguageServiceOptions.fileSystemProvider` is provided.
42 |
43 | 3.0.3 / 2019-07-25
44 | ==================
45 | * `DocumentContext.resolveReference` can also return undefined (if the ref is invalid)
46 |
47 | 3.0.0 / 2019-06-12
48 | ==================
49 | * Added API `htmlLanguageService.getSelectionRanges` returning selection ranges for a set of positions
50 | * New API `newHTMLDataProvider`
51 |
52 | 2.1.3 / 2018-04-16
53 | ==================
54 | * Added API `htmlLanguageService.getFoldingRanges` returning folding ranges for the given document
55 |
56 | 2.1.0 / 2018-03-08
57 | ==================
58 | * Added API `htmlLanguageService.setCompletionParticipants` that allows participation in code completion
59 | * provide ES modules in lib/esm
60 |
61 | 2.0.6 / 2017-08-25
62 | ==================
63 | * Added new API `htmlLanguageService.doTagComplete`. Called behind a `>` or `\`, `doTagComplete` will compute a closing tag. The result is a snippet string that can be inserted behind the position, or null, if no tag completion should be performed.
64 | * New settings `CompletionConfiguration.hideAutoCompleteProposals`. If set, `doComplete` will not propose a closing tag proposals on `>`.
65 | * These APIs are experimental and might be improved.
66 |
67 | 2.0.3 / 2017-03-21
68 | ==================
69 | * Fix indentation issues when formatting a range
70 |
71 | 2.0.1 / 2017-02-21
72 | ==================
73 | * Support for [base URLs](https://developer.mozilla.org/de/docs/Web/HTML/Element/base). `DocumentContext.resolveReference` now gets the base URI to take into account when resolving a reference. Refer to [links.test.ts](https://github.com/Microsoft/vscode-html-languageservice/blob/master/src/test/links.test.ts) for guidance on how to implement a `DocumentContext`.
74 | * Added `htmlLanguageService.findDocumentSymbols`: Returns a symbol for each tag in the document. Symbol name is in the form `tag(#id)?(.class)+`.
75 |
76 | 2.0.0 / 2017-02-17
77 | ==================
78 | * Updating to [language server type 3.0](https://github.com/Microsoft/vscode-languageserver-node/tree/master/types) API
79 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) Microsoft
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # vscode-html-languageservice
2 | HTML language service extracted from VSCode to be reused, e.g in the Monaco editor.
3 |
4 | [](https://www.npmjs.org/package/vscode-html-languageservice)
5 | [](https://npmjs.org/package/vscode-html-languageservice)
6 | [](https://github.com/microsoft/vscode-html-languageservice/actions)
7 | [](https://opensource.org/licenses/MIT)
8 |
9 | Why?
10 | ----
11 |
12 | The _vscode-html-languageservice_ contains the language smarts behind the HTML editing experience of Visual Studio Code
13 | and the Monaco editor.
14 |
15 | - *doComplete* / *doComplete2* (async) provide completion proposals for a given location.
16 | - *setCompletionParticipants* allows participant to provide suggestions for specific tokens.
17 | - *doHover* provides hover information at a given location.
18 |
19 | - *format* formats the code at the given range.
20 | - *findDocumentLinks* finds all links in the document.
21 | - *findDocumentSymbols* finds all the symbols in the document.
22 | - *getFoldingRanges* return folding ranges for the given document.
23 | - *getSelectionRanges* return the selection ranges for the given document.
24 | ...
25 |
26 | For the complete API see [htmlLanguageService.ts](./src/htmlLanguageService.ts) and [htmlLanguageTypes.ts](./src/htmlLanguageTypes.ts)
27 |
28 | Installation
29 | ------------
30 |
31 | npm install --save vscode-html-languageservice
32 |
33 | Development
34 | -----------
35 |
36 | - clone this repo, run `npm i`
37 | - `npm test` to compile and run tests
38 |
39 |
40 | How can I run and debug the service?
41 |
42 | - open the folder in VSCode.
43 | - set breakpoints, e.g. in `htmlCompletion.ts`
44 | - run the Unit tests from the run viewlet and wait until a breakpoint is hit:
45 | 
46 |
47 |
48 | How can I run and debug the service inside an instance of VSCode?
49 |
50 | - run VSCode out of sources setup as described here: https://github.com/Microsoft/vscode/wiki/How-to-Contribute
51 | - link the folder of the `vscode-html-languageservice` repo to `vscode/extensions/html-language-features/server` to run VSCode with the latest changes from that folder:
52 | - cd `vscode-html-languageservice`, `npm link`
53 | - cd `vscode/extensions/html-language-features/server`, `npm link vscode-html-languageservice`
54 | - run VSCode out of source (`vscode/scripts/code.sh|bat`) and open a `.html` file
55 | - in VSCode window that is open on the `vscode-html-languageservice` sources, run command `Debug: Attach to Node process` and pick the `code-oss` process with the `html-language-features` path
56 | 
57 | - set breakpoints, e.g. in `htmlCompletion.ts`
58 | - in the instance run from sources, invoke code completion in the `.html` file
59 |
60 |
61 | License
62 | -------
63 |
64 | (MIT License)
65 |
66 | Copyright 2016-2023, Microsoft
67 |
68 | `src/languageFacts/data/webCustomData.ts` (shipped as `lib/esm/languageFacts/data/webCustomData.ts` and `lib/umd/languageFacts/data/webCustomData.ts`)
69 | are built upon content from [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web) and distributed under CC BY-SA 2.5.
70 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## Security
4 |
5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
6 |
7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below.
8 |
9 | ## Reporting Security Issues
10 |
11 | **Please do not report security vulnerabilities through public GitHub issues.**
12 |
13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report).
14 |
15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey).
16 |
17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc).
18 |
19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
20 |
21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
22 | * Full paths of source file(s) related to the manifestation of the issue
23 | * The location of the affected source code (tag/branch/commit or direct URL)
24 | * Any special configuration required to reproduce the issue
25 | * Step-by-step instructions to reproduce the issue
26 | * Proof-of-concept or exploit code (if possible)
27 | * Impact of the issue, including how an attacker might exploit the issue
28 |
29 | This information will help us triage your report more quickly.
30 |
31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs.
32 |
33 | ## Preferred Languages
34 |
35 | We prefer all communications to be in English.
36 |
37 | ## Policy
38 |
39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd).
40 |
41 |
42 |
--------------------------------------------------------------------------------
/build/copy-jsbeautify.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 |
4 | function copy(from, to) {
5 | if (!fs.existsSync(to)) {
6 | fs.mkdirSync(to, { recursive: true });
7 | }
8 | const files = fs.readdirSync(from);
9 | for (let file of files) {
10 | if (path.extname(file) === '.js') {
11 | const fromPath = path.join(from, file);
12 | const toPath = path.join(to, file);
13 | console.log(`copy ${fromPath} to ${toPath}`);
14 | fs.copyFileSync(fromPath, toPath);
15 | }
16 | }
17 | }
18 |
19 | const umdDir = path.join(__dirname, '..', 'lib', 'umd', 'beautify');
20 | copy(path.join(__dirname, '..', 'src', 'beautify'), umdDir);
21 |
22 | const esmDir = path.join(__dirname, '..', 'lib', 'esm', 'beautify');
23 | copy(path.join(__dirname, '..', 'src', 'beautify', 'esm'), esmDir);
24 |
--------------------------------------------------------------------------------
/build/generateData.js:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation. All rights reserved.
3 | * Licensed under the MIT License. See License.txt in the project root for license information.
4 | *--------------------------------------------------------------------------------------------*/
5 |
6 | const fs = require('fs')
7 | const path = require('path')
8 | const os = require('os')
9 |
10 | const customData = require('@vscode/web-custom-data/data/browsers.html-data.json');
11 |
12 | function toJavaScript(obj) {
13 | return JSON.stringify(obj, null, '\t');
14 | }
15 |
16 | const DATA_TYPE = 'HTMLDataV1';
17 | const output = [
18 | '/*---------------------------------------------------------------------------------------------',
19 | ' * Copyright (c) Microsoft Corporation. All rights reserved.',
20 | ' * Licensed under the MIT License. See License.txt in the project root for license information.',
21 | ' *--------------------------------------------------------------------------------------------*/',
22 | '// file generated from @vscode/web-custom-data NPM package',
23 | '',
24 | `import { ${DATA_TYPE} } from '../../htmlLanguageTypes';`,
25 | '',
26 | `export const htmlData : ${DATA_TYPE} = ` + toJavaScript(customData) + ';'
27 | ];
28 |
29 | var outputPath = path.resolve(__dirname, '../src/languageFacts/data/webCustomData.ts');
30 | console.log('Writing to: ' + outputPath);
31 | var content = output.join(os.EOL);
32 | fs.writeFileSync(outputPath, content);
33 | console.log('Done');
34 |
--------------------------------------------------------------------------------
/build/pipeline.yml:
--------------------------------------------------------------------------------
1 | name: $(Date:yyyyMMdd)$(Rev:.r)
2 |
3 | trigger:
4 | batch: true
5 | branches:
6 | include:
7 | - main
8 | pr: none
9 |
10 | resources:
11 | repositories:
12 | - repository: templates
13 | type: github
14 | name: microsoft/vscode-engineering
15 | ref: main
16 | endpoint: Monaco
17 |
18 | parameters:
19 | - name: quality
20 | displayName: Quality
21 | type: string
22 | default: latest
23 | values:
24 | - latest
25 | - next
26 | - name: publishPackage
27 | displayName: 🚀 Publish vscode-html-languageservice
28 | type: boolean
29 | default: false
30 |
31 | extends:
32 | template: azure-pipelines/npm-package/pipeline.yml@templates
33 | parameters:
34 | npmPackages:
35 | - name: vscode-html-languageservice
36 |
37 | buildSteps:
38 | - script: npm ci
39 | displayName: Install dependencies
40 |
41 | # the rest of the build steps are part of the 'prepack' script, automatically run when the pipeline invokes 'npm run pack'
42 |
43 | tag: ${{ parameters.quality }}
44 | preReleaseTag: next
45 | publishPackage: ${{ parameters.publishPackage }}
46 | publishRequiresApproval: false
47 |
48 | testPlatforms:
49 | - name: Linux
50 | nodeVersions:
51 | - 20.x
52 | - name: MacOS
53 | nodeVersions:
54 | - 20.x
55 | - name: Windows
56 | nodeVersions:
57 | - 20.x
58 |
59 | testSteps:
60 | - script: npm ci
61 | displayName: Install dependencies
62 | - script: npm test
63 | displayName: Test npm package
64 |
--------------------------------------------------------------------------------
/build/remove-sourcemap-refs.js:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation. All rights reserved.
3 | * Licensed under the MIT License. See License.txt in the project root for license information.
4 | *--------------------------------------------------------------------------------------------*/
5 |
6 | const fs = require('fs');
7 | const path = require('path');
8 |
9 | function deleteRefs(dir) {
10 | const files = fs.readdirSync(dir);
11 | for (let file of files) {
12 | const filePath = path.join(dir, file);
13 | const stat = fs.statSync(filePath);
14 | if (stat.isDirectory()) {
15 | deleteRefs(filePath);
16 | } else if (path.extname(file) === '.js') {
17 | const content = fs.readFileSync(filePath, 'utf8');
18 | const newContent = content.replace(/\/\/\# sourceMappingURL=[^]+.js.map/, '')
19 | if (content.length !== newContent.length) {
20 | console.log('remove sourceMappingURL in ' + filePath);
21 | fs.writeFileSync(filePath, newContent);
22 | }
23 | } else if (path.extname(file) === '.map') {
24 | fs.unlinkSync(filePath)
25 | console.log('remove ' + filePath);
26 | }
27 | }
28 | }
29 |
30 | let location = path.join(__dirname, '..', 'lib');
31 | console.log('process ' + location);
32 | deleteRefs(location);
--------------------------------------------------------------------------------
/build/update-jsbeautify.js:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation. All rights reserved.
3 | * Licensed under the MIT License. See License.txt in the project root for license information.
4 | *--------------------------------------------------------------------------------------------*/
5 |
6 | 'use strict';
7 |
8 | var path = require('path');
9 | var fs = require('fs');
10 |
11 | function getVersion(moduleName) {
12 | var packageJSONPath = path.join(__dirname, '..', 'node_modules', moduleName, 'package.json');
13 | return readFile(packageJSONPath).then(function (content) {
14 | try {
15 | return JSON.parse(content).version;
16 | } catch (e) {
17 | return Promise.resolve(null);
18 | }
19 | });
20 | }
21 |
22 | function readFile(path) {
23 | return new Promise((s, e) => {
24 | fs.readFile(path, (err, res) => {
25 | if (err) {
26 | e(err);
27 | } else {
28 | s(res.toString());
29 | }
30 | })
31 | });
32 |
33 | }
34 |
35 | function update(moduleName, repoPath, dest, addHeader, patch) {
36 | var contentPath = path.join(__dirname, '..', 'node_modules', moduleName, repoPath);
37 | console.log('Reading from ' + contentPath);
38 | return readFile(contentPath).then(function (content) {
39 | return getVersion(moduleName).then(function (version) {
40 | let header = '';
41 | if (addHeader) {
42 | header = '// copied from js-beautify/' + repoPath + '\n';
43 | if (version) {
44 | header += '// version: ' + version + '\n';
45 | }
46 | }
47 | try {
48 | if (patch) {
49 | content = patch(content);
50 | }
51 | fs.writeFileSync(dest, header + content);
52 | if (version) {
53 | console.log('Updated ' + path.basename(dest) + ' (' + version + ')');
54 | } else {
55 | console.log('Updated ' + path.basename(dest));
56 | }
57 | } catch (e) {
58 | console.error(e);
59 | }
60 | });
61 |
62 | }, console.error);
63 | }
64 |
65 | update('js-beautify', 'js/lib/beautify-html.js', './src/beautify/beautify-html.js', true);
66 | update('js-beautify', 'js/lib/beautify-css.js', './src/beautify/beautify-css.js', true);
67 | update('js-beautify', 'LICENSE', './src/beautify/beautify-license');
68 |
69 | // ESM version
70 | update('js-beautify', 'js/lib/beautify-html.js', './src/beautify/esm/beautify-html.js', true, function (contents) {
71 | let topLevelFunction = '(function() {';
72 | let outputVar = 'var legacy_beautify_html';
73 | let footer = 'var style_html = legacy_beautify_html;';
74 | let index1 = contents.indexOf(topLevelFunction);
75 | let index2 = contents.indexOf(outputVar, index1);
76 | let index3 = contents.indexOf(footer, index2);
77 | if (index1 === -1) {
78 | throw new Error(`Problem patching beautify.html for ESM: '${topLevelFunction}' not found.`);
79 | }
80 | if (index2 === -1) {
81 | throw new Error(`Problem patching beautify.html for ESM: '${outputVar}' not found after '${topLevelFunction}'.`);
82 | }
83 | if (index3 === -1) {
84 | throw new Error(`Problem patching beautify.html for ESM: '${footer}' not found after '${outputVar}'.`);
85 | }
86 | return contents.substring(0, index1) +
87 | `import { js_beautify } from "./beautify";\nimport { css_beautify } from "./beautify-css";\n\n`
88 | + contents.substring(index2, index3) +
89 | `
90 | export function html_beautify(html_source, options) {
91 | return legacy_beautify_html(html_source, options, js_beautify, css_beautify);
92 | }`;
93 | });
94 | update('js-beautify', 'js/lib/beautify-css.js', './src/beautify/esm/beautify-css.js', true, function (contents) {
95 | let topLevelFunction = '(function() {';
96 | let outputVar = 'var legacy_beautify_css';
97 | let footer = 'var css_beautify = legacy_beautify_css;';
98 | let index1 = contents.indexOf(topLevelFunction);
99 | let index2 = contents.indexOf(outputVar, index1);
100 | let index3 = contents.indexOf(footer, index2);
101 | if (index1 === -1) {
102 | throw new Error(`Problem patching beautify.html for ESM: '${topLevelFunction}' not found.`);
103 | }
104 | if (index2 === -1) {
105 | throw new Error(`Problem patching beautify.html for ESM: '${outputVar}' not found after '${topLevelFunction}'.`);
106 | }
107 | if (index3 === -1) {
108 | throw new Error(`Problem patching beautify.html for ESM: '${footer}' not found after '${outputVar}'.`);
109 | }
110 | return contents.substring(0, index1) +
111 | contents.substring(index2, index3) +
112 | `\nexport var css_beautify = legacy_beautify_css;`;
113 | });
114 |
--------------------------------------------------------------------------------
/docs/customData.md:
--------------------------------------------------------------------------------
1 | # Custom Data for HTML Language Service
2 |
3 | In VS Code, there are two ways of loading custom HTML datasets:
4 |
5 | 1. With setting `html.customData`
6 | 2. With an extension that contributes `contributes.html.customData`
7 |
8 | Both setting point to a list of JSON files. This document describes the shape of the JSON files.
9 |
10 | You can read more about custom data at: https://github.com/microsoft/vscode-custom-data.
11 |
12 | ## Custom Data Format
13 |
14 | The JSON have one required property, `version` and 3 other top level properties:
15 |
16 | ```jsonc
17 | {
18 | "version": 1.1,
19 | "tags": [],
20 | "globalAttributes": [],
21 | "valueSets": []
22 | }
23 | ```
24 |
25 | Version denotes the schema version you are using. The latest schema version is `V1.1`.
26 |
27 | You can find other properties' shapes at [htmlLanguageTypes.ts](../src/htmlLanguageTypes.ts) or the [JSON Schema](./customData.schema.json).
28 |
29 | When working with VSCode, you should suffix your custom data file with `.html-data.json`, so VS Code will load the most recent schema for the JSON file.
30 |
31 | [html5.ts](../src/languageFacts/data/webCustomData.ts) contains that built-in dataset that conforms to the spec.
32 |
33 | ## Language Features
34 |
35 | Custom data receives the following language features:
36 |
37 | - Completion on tag, attribute and attribute value
38 | - Hover on tag (here's the [issue](https://github.com/Microsoft/vscode-html-languageservice/issues/47) for hover on attribute / attribute-name)
39 |
40 | For example, for the following custom data:
41 |
42 | ```json
43 | {
44 | "tags": [
45 | {
46 | "name": "foo",
47 | "description": "The foo element",
48 | "attributes": [
49 | { "name": "bar" },
50 | {
51 | "name": "baz",
52 | "values": [
53 | {
54 | "name": "baz-val-1"
55 | }
56 | ]
57 | }
58 | ]
59 | }
60 | ],
61 | "globalAttributes": [
62 | { "name": "fooAttr", "description": "Foo Attribute" },
63 | { "name": "xattr", "description": "X attributes", "valueSet": "x" }
64 | ],
65 | "valueSets": [
66 | {
67 | "name": "x",
68 | "values": [
69 | {
70 | "name": "xval",
71 | "description": "x value"
72 | }
73 | ]
74 | }
75 | ]
76 | }
77 | ```
78 |
79 | - Completion on `<|` will provide `foo`
80 | - Completion on `` will provide all values in valueSet `x`, which is `xval`
84 | - Hover on `foo` will show `The foo element`
85 |
86 | ### Additional properties
87 |
88 | For either `tag`, `attribute` or `attributeValue`, you can provide a `references` property of the following form
89 |
90 | ```json
91 | {
92 | "tags": [
93 | {
94 | "name": "foo",
95 | "description": "The foo element",
96 | "references": [
97 | {
98 | "name": "My foo element reference",
99 | "url": "https://www.foo.com/element/foo"
100 | }
101 | ]
102 | }
103 | ]
104 | }
105 | ```
106 |
107 | It will be displayed in Markdown form in completion and hover as `[My foo element reference](https://www.foo.com/element/foo)`.
--------------------------------------------------------------------------------
/docs/customData.schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/draft-07/schema",
3 | "$id": "vscode-html-customdata",
4 | "version": 1.1,
5 | "title": "VS Code HTML Custom Data format",
6 | "description": "Format for loading Custom Data in VS Code's HTML support",
7 | "type": "object",
8 | "required": [
9 | "version"
10 | ],
11 | "definitions": {
12 | "references": {
13 | "type": "object",
14 | "required": [
15 | "name",
16 | "url"
17 | ],
18 | "properties": {
19 | "name": {
20 | "type": "string",
21 | "description": "The name of the reference."
22 | },
23 | "url": {
24 | "type": "string",
25 | "description": "The URL of the reference.",
26 | "pattern": "https?:\/\/",
27 | "patternErrorMessage": "URL should start with http:// or https://"
28 | }
29 | }
30 | },
31 | "markupDescription": {
32 | "type": "object",
33 | "required": [
34 | "kind",
35 | "value"
36 | ],
37 | "properties": {
38 | "kind": {
39 | "type": "string",
40 | "description": "Whether `description.value` should be rendered as plaintext or markdown",
41 | "enum": [
42 | "plaintext",
43 | "markdown"
44 | ]
45 | },
46 | "value": {
47 | "type": "string",
48 | "description": "Description shown in completion and hover"
49 | }
50 | }
51 | }
52 | },
53 | "properties": {
54 | "version": {
55 | "const": 1.1,
56 | "description": "The custom data version",
57 | "type": "number"
58 | },
59 | "tags": {
60 | "description": "Custom HTML tags",
61 | "type": "array",
62 | "items": {
63 | "type": "object",
64 | "required": [
65 | "name"
66 | ],
67 | "defaultSnippets": [
68 | {
69 | "body": {
70 | "name": "$1",
71 | "description": "",
72 | "attributes": []
73 | }
74 | }
75 | ],
76 | "properties": {
77 | "name": {
78 | "type": "string",
79 | "description": "Name of tag"
80 | },
81 | "description": {
82 | "description": "Description of tag shown in completion and hover",
83 | "anyOf": [
84 | {
85 | "type": "string"
86 | },
87 | {
88 | "$ref": "#/definitions/markupDescription"
89 | }
90 | ]
91 | },
92 | "attributes": {
93 | "type": "array",
94 | "description": "A list of possible attributes for the tag",
95 | "items": {
96 | "type": "object",
97 | "required": [
98 | "name"
99 | ],
100 | "defaultSnippets": [
101 | {
102 | "body": {
103 | "name": "$1",
104 | "description": "",
105 | "values": []
106 | }
107 | }
108 | ],
109 | "properties": {
110 | "name": {
111 | "type": "string",
112 | "description": "Name of attribute"
113 | },
114 | "description": {
115 | "description": "Description of attribute shown in completion and hover",
116 | "anyOf": [
117 | {
118 | "type": "string"
119 | },
120 | {
121 | "$ref": "#/definitions/markupDescription"
122 | }
123 | ]
124 | },
125 | "valueSet": {
126 | "type": "string",
127 | "description": "Name of the matching attribute value set"
128 | },
129 | "values": {
130 | "type": "array",
131 | "description": "A list of possible values for the attribute",
132 | "items": {
133 | "type": "object",
134 | "required": [
135 | "name"
136 | ],
137 | "defaultSnippets": [
138 | {
139 | "body": {
140 | "name": "$1",
141 | "description": ""
142 | }
143 | }
144 | ],
145 | "properties": {
146 | "name": {
147 | "type": "string",
148 | "description": "Name of attribute value"
149 | },
150 | "description": {
151 | "description": "Description of attribute value shown in completion and hover",
152 | "anyOf": [
153 | {
154 | "type": "string"
155 | },
156 | {
157 | "$ref": "#/definitions/markupDescription"
158 | }
159 | ]
160 | },
161 | "references": {
162 | "type": "array",
163 | "description": "A list of references for the attribute value shown in completion and hover",
164 | "items": {
165 | "$ref": "#/definitions/references"
166 | }
167 | }
168 | }
169 | }
170 | },
171 | "references": {
172 | "type": "array",
173 | "description": "A list of references for the attribute shown in completion and hover",
174 | "items": {
175 | "$ref": "#/definitions/references"
176 | }
177 | }
178 | }
179 | }
180 | },
181 | "references": {
182 | "type": "array",
183 | "description": "A list of references for the tag shown in completion and hover",
184 | "items": {
185 | "$ref": "#/definitions/references"
186 | }
187 | },
188 | "browsers": {
189 | "type": "array",
190 | "description": "Supported browsers",
191 | "items": {
192 | "type": "string",
193 | "pattern": "(E|FFA|FF|SM|S|CA|C|IE|O)([\\d|\\.]+)?",
194 | "patternErrorMessage": "Browser item must follow the format of `${browser}${version}`. `browser` is one of:\n- E: Edge\n- FF: Firefox\n- FM: Firefox Android\n- S: Safari\n- SM: Safari on iOS\n- C: Chrome\n- CM: Chrome on Android\n- IE: Internet Explorer\n- O: Opera"
195 | }
196 | },
197 | "baseline": {
198 | "type": "object",
199 | "description": "Baseline information for the feature",
200 | "properties": {
201 | "status": {
202 | "type": "string",
203 | "description": "Baseline status",
204 | "enum": [
205 | "high",
206 | "low",
207 | "false"
208 | ]
209 | },
210 | "baseline_low_date": {
211 | "type": "string",
212 | "description": "Date when the feature became newly supported in all major browsers",
213 | "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
214 | "patternErrorMessage": "Date must be in the format of `YYYY-MM-DD`"
215 | },
216 | "baseline_high_date": {
217 | "type": "string",
218 | "description": "Date when the feature became widely supported in all major browsers",
219 | "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
220 | "patternErrorMessage": "Date must be in the format of `YYYY-MM-DD`"
221 | }
222 | }
223 | }
224 | }
225 | },
226 | "globalAttributes": {
227 | "description": "Custom HTML global attributes",
228 | "type": "array",
229 | "items": {
230 | "$ref": "#/properties/tags/items/properties/attributes/items"
231 | }
232 | },
233 | "valueSets": {
234 | "description": "A set of attribute value. When an attribute refers to an attribute set, its value completion will use value from that set",
235 | "type": "array",
236 | "items": {
237 | "type": "object",
238 | "required": [
239 | "name"
240 | ],
241 | "defaultSnippets": [
242 | {
243 | "body": {
244 | "name": "$1",
245 | "description": "",
246 | "values": []
247 | }
248 | }
249 | ],
250 | "properties": {
251 | "name": {
252 | "type": "string",
253 | "description": "Name of attribute value in value set"
254 | },
255 | "values": {
256 | "$ref": "#/properties/tags/items/properties/attributes/items/properties/values"
257 | }
258 | }
259 | }
260 | }
261 | }
262 | }
263 | }
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import typescriptEslint from "@typescript-eslint/eslint-plugin";
2 | import tsParser from "@typescript-eslint/parser";
3 |
4 | export default [{
5 | files: ["**/*.ts"],
6 |
7 | plugins: {
8 | "@typescript-eslint": typescriptEslint,
9 | },
10 |
11 | languageOptions: {
12 | parser: tsParser,
13 | ecmaVersion: 6,
14 | sourceType: "module",
15 | },
16 |
17 | rules: {
18 | "@typescript-eslint/naming-convention": ["warn", {
19 | selector: "typeLike",
20 | format: ["PascalCase"],
21 | }],
22 |
23 | curly: "warn",
24 | eqeqeq: "warn",
25 | "no-throw-literal": "warn",
26 | semi: "off",
27 | },
28 | }];
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vscode-html-languageservice",
3 | "version": "5.5.0",
4 | "description": "Language service for HTML",
5 | "main": "./lib/umd/htmlLanguageService.js",
6 | "typings": "./lib/umd/htmlLanguageService",
7 | "module": "./lib/esm/htmlLanguageService.js",
8 | "author": "Microsoft Corporation",
9 | "repository": {
10 | "type": "git",
11 | "url": "https://github.com/Microsoft/vscode-html-languageservice"
12 | },
13 | "license": "MIT",
14 | "bugs": {
15 | "url": "https://github.com/Microsoft/vscode-html-languageservice"
16 | },
17 | "devDependencies": {
18 | "@types/mocha": "^10.0.10",
19 | "@types/node": "20.x",
20 | "@typescript-eslint/eslint-plugin": "^8.33.0",
21 | "@typescript-eslint/parser": "^8.33.0",
22 | "@vscode/web-custom-data": "^0.6.0",
23 | "eslint": "^9.28.0",
24 | "js-beautify": "^1.15.4",
25 | "mocha": "^11.5.0",
26 | "rimraf": "^6.0.1",
27 | "typescript": "5.8.3"
28 | },
29 | "dependencies": {
30 | "@vscode/l10n": "^0.0.18",
31 | "vscode-languageserver-textdocument": "^1.0.12",
32 | "vscode-languageserver-types": "^3.17.5",
33 | "vscode-uri": "^3.1.0"
34 | },
35 | "scripts": {
36 | "prepack": "npm run clean && npm run compile-esm && npm run test && npm run remove-sourcemap-refs",
37 | "compile": "tsc -p ./src && npm run copy-jsbeautify",
38 | "compile-esm": "tsc -p ./src/tsconfig.esm.json",
39 | "watch": "tsc -w -p ./src && npm run copy-jsbeautify",
40 | "clean": "rimraf lib",
41 | "remove-sourcemap-refs": "node ./build/remove-sourcemap-refs.js",
42 | "test": "npm run compile && mocha --timeout 5000 ./lib/umd/test/*.js && npm run lint",
43 | "lint": "eslint src/**/*.ts",
44 | "install-types-next": "npm i vscode-languageserver-types@next -f -S && npm i vscode-languageserver-textdocument@next -f -S",
45 | "copy-jsbeautify": "node ./build/copy-jsbeautify.js",
46 | "update-jsbeautify": "npm i js-beautify && node ./build/update-jsbeautify.js",
47 | "update-jsbeautify-next": "npm i js-beautify@next && node ./build/update-jsbeautify.js",
48 | "update-data": "npm i @vscode/web-custom-data -D && node ./build/generateData.js"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/beautify/beautify-css.d.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation. All rights reserved.
3 | * Licensed under the MIT License. See License.txt in the project root for license information.
4 | *--------------------------------------------------------------------------------------------*/
5 |
6 | export interface IBeautifyCSSOptions {
7 | indent_size?: number; // (4) — indentation size,
8 | indent_char?: string; // (space) — character to indent with,
9 | selector_separator_newline?: boolean; // (true) - separate selectors with newline or not (e.g. "a,\nbr" or "a, br")
10 | end_with_newline?: boolean; // (false) - end with a newline
11 | newline_between_rules?: boolean; // (true) - add a new line after every css rule
12 | space_around_selector_separator?: boolean // (false) - ensure space around selector separators: '>', '+', '~' (e.g. "a>b" -> "a > b")
13 | brace_style?: 'collapse' | 'expand'; // (collapse) - place braces on the same line (collapse) or on a new line (expand)
14 | preserve_newlines?: boolean; // (true) - whether to preserve new line
15 | max_preserve_newlines?: number; // (32786) - the maximum numbers of new lines to preserve
16 | wrap_line_length?: number; // (undefined) - warp lines after a line offset
17 | indent_empty_lines?: number; // (false) - indent empty lines
18 | }
19 |
20 | export interface IBeautifyCSS {
21 | (value:string, options:IBeautifyCSSOptions): string;
22 | }
23 |
24 | export declare var css_beautify:IBeautifyCSS;
--------------------------------------------------------------------------------
/src/beautify/beautify-html.d.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation. All rights reserved.
3 | * Licensed under the MIT License. See License.txt in the project root for license information.
4 | *--------------------------------------------------------------------------------------------*/
5 |
6 | export interface IBeautifyHTMLOptions {
7 | /**
8 | * indent and sections
9 | * default false
10 | */
11 | indent_inner_html?: boolean;
12 |
13 | /**
14 | * indentation size
15 | * default 4
16 | */
17 | indent_size?: number; // indentation size,
18 |
19 | /**
20 | * character to indent with
21 | * default space
22 | */
23 | indent_char?: string; // character to indent with,
24 |
25 | /**
26 | * add indenting whitespace to empty lines
27 | * default false
28 | */
29 | indent_empty_lines?: boolean; // add indenting whitespace to empty lines
30 |
31 | /**
32 | * maximum amount of characters per line (0 = disable)
33 | * default 250
34 | */
35 | wrap_line_length?: number;
36 |
37 | /**
38 | * put braces on the same line as control statements (default), or put braces on own line (Allman / ANSI style), or just put end braces on own line, or attempt to keep them where they are.
39 | * "collapse" | "expand" | "end-expand" | "none"
40 | * default "collapse"
41 | */
42 | brace_style?: string;
43 |
44 | /**
45 | * list of tags, that shouldn't be reformatted
46 | * defaults to inline tags
47 | */
48 | unformatted?: string[];
49 |
50 | /**
51 | * list of tags, that its content shouldn't be reformatted
52 | * defaults to pre tag
53 | */
54 | content_unformatted?: string[];
55 |
56 | /**
57 | * "keep"|"separate"|"normal"
58 | * default normal
59 | */
60 | indent_scripts?: 'keep' | 'separate' | 'normal';
61 |
62 | /**
63 | * whether existing line breaks before elements should be preserved. Only works before elements, not inside tags or for text.
64 | * default true
65 | */
66 | preserve_newlines?: boolean;
67 |
68 | /**
69 | * maximum number of line breaks to be preserved in one chunk
70 | * default unlimited
71 | */
72 | max_preserve_newlines?: number;
73 |
74 | /**
75 | * format and indent {{#foo}} and {{/foo}}
76 | * default false
77 | */
78 | indent_handlebars?: boolean;
79 |
80 | /**
81 | * end with a newline
82 | * default false
83 | */
84 | end_with_newline?: boolean;
85 |
86 | /**
87 | * List of tags that should have an extra newline before them.
88 | * default [head,body,/html]
89 | */
90 | extra_liners?: string[];
91 |
92 | /**
93 | * wrap each attribute except first ('force')
94 | * wrap each attribute except first and align ('force-aligned')
95 | * wrap each attribute ('force-expand-multiline')
96 | * multiple attributes are allowed per line, attributes that wrap will align vertically ('aligned-multiple')
97 | * preserve wrapping of attributes ('preserve')
98 | * preserve wrapping of attributes but align ('preserve-aligned')
99 | * wrap only when line length is reached ('auto')
100 | *
101 | * default auto
102 | */
103 | wrap_attributes?: 'auto' | 'force' | 'force-expand-multiline' | 'force-aligned' | 'aligned-multiple' | 'preserve' | 'preserve-aligned';
104 |
105 | /**
106 | * Alignment size when using 'force-aligned' | 'aligned-multiple'
107 | */
108 | wrap_attributes_indent_size?: number;
109 |
110 | /*
111 | * end of line character to use
112 | */
113 | eol?: string;
114 |
115 | /**
116 | * List of templating languages (auto,none,angular,django,erb,handlebars,php,smarty)
117 | * default ["auto"] = all in html
118 | */
119 | templating?: ('auto' | 'none' | 'angular' | 'django' | 'erb' | 'handlebars' | 'php' | 'smarty')[];
120 |
121 | /**
122 | * Keep text content together between this string
123 | * default ""
124 | */
125 | unformatted_content_delimiter?: string;
126 | }
127 |
128 | export interface IBeautifyHTML {
129 | (value: string, options: IBeautifyHTMLOptions): string;
130 | }
131 |
132 | export declare var html_beautify: IBeautifyHTML;
--------------------------------------------------------------------------------
/src/beautify/beautify-license:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/src/beautify/beautify.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation. All rights reserved.
3 | * Licensed under the MIT License. See License.txt in the project root for license information.
4 | *--------------------------------------------------------------------------------------------*/
5 | /*
6 | * Mock for the JS formatter. Ignore formatting of JS content in HTML.
7 | */
8 | export function js_beautify(js_source_text: string, options: any) {
9 | // no formatting
10 | return js_source_text;
11 | }
--------------------------------------------------------------------------------
/src/htmlLanguageService.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation. All rights reserved.
3 | * Licensed under the MIT License. See License.txt in the project root for license information.
4 | *--------------------------------------------------------------------------------------------*/
5 |
6 | import { createScanner } from './parser/htmlScanner';
7 | import { HTMLParser } from './parser/htmlParser';
8 | import { HTMLCompletion } from './services/htmlCompletion';
9 | import { HTMLHover } from './services/htmlHover';
10 | import { format } from './services/htmlFormatter';
11 | import { HTMLDocumentLinks } from './services/htmlLinks';
12 | import { findDocumentHighlights } from './services/htmlHighlighting';
13 | import { findDocumentSymbols, findDocumentSymbols2 } from './services/htmlSymbolsProvider';
14 | import { doRename } from './services/htmlRename';
15 | import { findMatchingTagPosition } from './services/htmlMatchingTagPosition';
16 | import { findLinkedEditingRanges } from './services/htmlLinkedEditing';
17 | import {
18 | Scanner, HTMLDocument, CompletionConfiguration, ICompletionParticipant, HTMLFormatConfiguration, DocumentContext, DocumentSymbol,
19 | IHTMLDataProvider, HTMLDataV1, LanguageServiceOptions, TextDocument, SelectionRange, WorkspaceEdit,
20 | Position, CompletionList, Hover, Range, SymbolInformation, TextEdit, DocumentHighlight, DocumentLink, FoldingRange, HoverSettings,
21 | } from './htmlLanguageTypes';
22 | import { HTMLFolding } from './services/htmlFolding';
23 | import { HTMLSelectionRange } from './services/htmlSelectionRange';
24 | import { HTMLDataProvider } from './languageFacts/dataProvider';
25 | import { HTMLDataManager } from './languageFacts/dataManager';
26 | import { htmlData } from './languageFacts/data/webCustomData';
27 |
28 | export * from './htmlLanguageTypes';
29 |
30 | export interface LanguageService {
31 | setDataProviders(useDefaultDataProvider: boolean, customDataProviders: IHTMLDataProvider[]): void;
32 | createScanner(input: string, initialOffset?: number): Scanner;
33 | parseHTMLDocument(document: TextDocument): HTMLDocument;
34 | findDocumentHighlights(document: TextDocument, position: Position, htmlDocument: HTMLDocument): DocumentHighlight[];
35 | doComplete(document: TextDocument, position: Position, htmlDocument: HTMLDocument, options?: CompletionConfiguration): CompletionList;
36 | doComplete2(document: TextDocument, position: Position, htmlDocument: HTMLDocument, documentContext: DocumentContext, options?: CompletionConfiguration): Promise;
37 | setCompletionParticipants(registeredCompletionParticipants: ICompletionParticipant[]): void;
38 | doHover(document: TextDocument, position: Position, htmlDocument: HTMLDocument, options?: HoverSettings): Hover | null;
39 | format(document: TextDocument, range: Range | undefined, options: HTMLFormatConfiguration): TextEdit[];
40 | findDocumentLinks(document: TextDocument, documentContext: DocumentContext): DocumentLink[];
41 | findDocumentSymbols(document: TextDocument, htmlDocument: HTMLDocument): SymbolInformation[];
42 | findDocumentSymbols2(document: TextDocument, htmlDocument: HTMLDocument): DocumentSymbol[];
43 | doQuoteComplete(document: TextDocument, position: Position, htmlDocument: HTMLDocument, options?: CompletionConfiguration): string | null;
44 | doTagComplete(document: TextDocument, position: Position, htmlDocument: HTMLDocument): string | null;
45 | getFoldingRanges(document: TextDocument, context?: { rangeLimit?: number }): FoldingRange[];
46 | getSelectionRanges(document: TextDocument, positions: Position[]): SelectionRange[];
47 | doRename(document: TextDocument, position: Position, newName: string, htmlDocument: HTMLDocument): WorkspaceEdit | null;
48 | findMatchingTagPosition(document: TextDocument, position: Position, htmlDocument: HTMLDocument): Position | null;
49 | /** Deprecated, Use findLinkedEditingRanges instead */
50 | findOnTypeRenameRanges(document: TextDocument, position: Position, htmlDocument: HTMLDocument): Range[] | null;
51 | findLinkedEditingRanges(document: TextDocument, position: Position, htmlDocument: HTMLDocument): Range[] | null;
52 | }
53 |
54 | const defaultLanguageServiceOptions = {};
55 |
56 | export function getLanguageService(options: LanguageServiceOptions = defaultLanguageServiceOptions): LanguageService {
57 | const dataManager = new HTMLDataManager(options);
58 |
59 | const htmlHover = new HTMLHover(options, dataManager);
60 | const htmlCompletion = new HTMLCompletion(options, dataManager);
61 | const htmlParser = new HTMLParser(dataManager);
62 | const htmlSelectionRange = new HTMLSelectionRange(htmlParser);
63 | const htmlFolding = new HTMLFolding(dataManager);
64 | const htmlDocumentLinks = new HTMLDocumentLinks(dataManager);
65 |
66 | return {
67 | setDataProviders: dataManager.setDataProviders.bind(dataManager),
68 | createScanner,
69 | parseHTMLDocument: htmlParser.parseDocument.bind(htmlParser),
70 | doComplete: htmlCompletion.doComplete.bind(htmlCompletion),
71 | doComplete2: htmlCompletion.doComplete2.bind(htmlCompletion),
72 | setCompletionParticipants: htmlCompletion.setCompletionParticipants.bind(htmlCompletion),
73 | doHover: htmlHover.doHover.bind(htmlHover),
74 | format,
75 | findDocumentHighlights,
76 | findDocumentLinks: htmlDocumentLinks.findDocumentLinks.bind(htmlDocumentLinks),
77 | findDocumentSymbols,
78 | findDocumentSymbols2,
79 | getFoldingRanges: htmlFolding.getFoldingRanges.bind(htmlFolding),
80 | getSelectionRanges: htmlSelectionRange.getSelectionRanges.bind(htmlSelectionRange),
81 | doQuoteComplete: htmlCompletion.doQuoteComplete.bind(htmlCompletion),
82 | doTagComplete: htmlCompletion.doTagComplete.bind(htmlCompletion),
83 | doRename,
84 | findMatchingTagPosition,
85 | findOnTypeRenameRanges: findLinkedEditingRanges,
86 | findLinkedEditingRanges
87 | };
88 | }
89 |
90 | export function newHTMLDataProvider(id: string, customData: HTMLDataV1): IHTMLDataProvider {
91 | return new HTMLDataProvider(id, customData);
92 | }
93 |
94 | export function getDefaultHTMLDataProvider(): IHTMLDataProvider {
95 | return newHTMLDataProvider('default', htmlData);
96 | }
97 |
--------------------------------------------------------------------------------
/src/htmlLanguageTypes.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation. All rights reserved.
3 | * Licensed under the MIT License. See License.txt in the project root for license information.
4 | *--------------------------------------------------------------------------------------------*/
5 |
6 | import {
7 | Position, Range, Location,
8 | MarkupContent, MarkupKind, MarkedString, DocumentUri,
9 | SelectionRange, WorkspaceEdit,
10 | CompletionList, CompletionItemKind, CompletionItem, CompletionItemTag, InsertTextMode, Command,
11 | SymbolInformation, DocumentSymbol, SymbolKind,
12 | Hover, TextEdit, InsertReplaceEdit, InsertTextFormat, DocumentHighlight, DocumentHighlightKind,
13 | DocumentLink, FoldingRange, FoldingRangeKind,
14 | SignatureHelp, Definition, Diagnostic, FormattingOptions, Color, ColorInformation, ColorPresentation
15 | } from 'vscode-languageserver-types';
16 | import { TextDocument } from 'vscode-languageserver-textdocument';
17 |
18 |
19 | export {
20 | TextDocument,
21 | Position, Range, Location,
22 | MarkupContent, MarkupKind, MarkedString, DocumentUri,
23 | SelectionRange, WorkspaceEdit,
24 | CompletionList, CompletionItemKind, CompletionItem, CompletionItemTag, InsertTextMode, Command,
25 | SymbolInformation, DocumentSymbol, SymbolKind,
26 | Hover, TextEdit, InsertReplaceEdit, InsertTextFormat, DocumentHighlight, DocumentHighlightKind,
27 | DocumentLink, FoldingRange, FoldingRangeKind,
28 | SignatureHelp, Definition, Diagnostic, FormattingOptions, Color, ColorInformation, ColorPresentation
29 | };
30 |
31 | export interface HTMLFormatConfiguration {
32 | tabSize?: number;
33 | insertSpaces?: boolean;
34 | indentEmptyLines?: boolean;
35 | wrapLineLength?: number;
36 | unformatted?: string;
37 | contentUnformatted?: string;
38 | indentInnerHtml?: boolean;
39 | wrapAttributes?: 'auto' | 'force' | 'force-aligned' | 'force-expand-multiline' | 'aligned-multiple' | 'preserve' | 'preserve-aligned';
40 | wrapAttributesIndentSize?: number;
41 | preserveNewLines?: boolean;
42 | maxPreserveNewLines?: number;
43 | indentHandlebars?: boolean;
44 | endWithNewline?: boolean;
45 | extraLiners?: string;
46 | indentScripts?: 'keep' | 'separate' | 'normal';
47 | templating?: ('auto' | 'none' | 'angular' | 'django' | 'erb' | 'handlebars' | 'php' | 'smarty')[] | boolean;
48 | unformattedContentDelimiter?: string;
49 |
50 | }
51 |
52 | export interface HoverSettings {
53 | documentation?: boolean;
54 | references?: boolean
55 | }
56 |
57 | export interface CompletionConfiguration {
58 | [provider: string]: boolean | undefined | string;
59 | hideAutoCompleteProposals?: boolean;
60 | attributeDefaultValue?: 'empty' | 'singlequotes' | 'doublequotes';
61 | }
62 |
63 | export interface Node {
64 | tag: string | undefined;
65 | start: number;
66 | startTagEnd: number | undefined;
67 | end: number;
68 | endTagStart: number | undefined;
69 | children: Node[];
70 | parent?: Node;
71 | attributes?: { [name: string]: string | null } | undefined;
72 | }
73 |
74 | export enum TokenType {
75 | StartCommentTag,
76 | Comment,
77 | EndCommentTag,
78 | StartTagOpen,
79 | StartTagClose,
80 | StartTagSelfClose,
81 | StartTag,
82 | EndTagOpen,
83 | EndTagClose,
84 | EndTag,
85 | DelimiterAssign,
86 | AttributeName,
87 | AttributeValue,
88 | StartDoctypeTag,
89 | Doctype,
90 | EndDoctypeTag,
91 | Content,
92 | Whitespace,
93 | Unknown,
94 | Script,
95 | Styles,
96 | EOS
97 | }
98 |
99 | export enum ScannerState {
100 | WithinContent,
101 | AfterOpeningStartTag,
102 | AfterOpeningEndTag,
103 | WithinDoctype,
104 | WithinTag,
105 | WithinEndTag,
106 | WithinComment,
107 | WithinScriptContent,
108 | WithinStyleContent,
109 | AfterAttributeName,
110 | BeforeAttributeValue
111 | }
112 |
113 | export interface Scanner {
114 | scan(): TokenType;
115 | getTokenType(): TokenType;
116 | getTokenOffset(): number;
117 | getTokenLength(): number;
118 | getTokenEnd(): number;
119 | getTokenText(): string;
120 | getTokenError(): string | undefined;
121 | getScannerState(): ScannerState;
122 | }
123 |
124 | export declare type HTMLDocument = {
125 | roots: Node[];
126 | findNodeBefore(offset: number): Node;
127 | findNodeAt(offset: number): Node;
128 | };
129 |
130 | export interface DocumentContext {
131 | resolveReference(ref: string, base: string): string | undefined;
132 | }
133 |
134 | export interface HtmlAttributeValueContext {
135 | document: TextDocument;
136 | position: Position;
137 | tag: string;
138 | attribute: string;
139 | value: string;
140 | range: Range;
141 | }
142 |
143 | export interface HtmlContentContext {
144 | document: TextDocument;
145 | position: Position;
146 | }
147 |
148 | export interface ICompletionParticipant {
149 | onHtmlAttributeValue?: (context: HtmlAttributeValueContext) => void;
150 | onHtmlContent?: (context: HtmlContentContext) => void;
151 | }
152 |
153 | export interface IReference {
154 | name: string;
155 | url: string;
156 | }
157 |
158 | export interface ITagData {
159 | name: string;
160 | description?: string | MarkupContent;
161 | attributes: IAttributeData[];
162 | references?: IReference[];
163 | void?: boolean;
164 | browsers?: string[];
165 | status?: BaselineStatus;
166 | }
167 |
168 | export interface IAttributeData {
169 | name: string;
170 | description?: string | MarkupContent;
171 | valueSet?: string;
172 | values?: IValueData[];
173 | references?: IReference[];
174 | browsers?: string[];
175 | status?: BaselineStatus;
176 | }
177 |
178 | export interface IValueData {
179 | name: string;
180 | description?: string | MarkupContent;
181 | references?: IReference[];
182 | browsers?: string[];
183 | status?: BaselineStatus;
184 | }
185 |
186 | export interface IValueSet {
187 | name: string;
188 | values: IValueData[];
189 | }
190 |
191 | export interface HTMLDataV1 {
192 | version: 1 | 1.1;
193 | tags?: ITagData[];
194 | globalAttributes?: IAttributeData[];
195 | valueSets?: IValueSet[];
196 | }
197 |
198 | export interface BaselineStatus {
199 | baseline: Baseline;
200 | baseline_low_date?: string;
201 | baseline_high_date?: string;
202 | }
203 |
204 | export type Baseline = false | 'low' | 'high';
205 |
206 | export interface IHTMLDataProvider {
207 | getId(): string;
208 | isApplicable(languageId: string): boolean;
209 |
210 | provideTags(): ITagData[];
211 | provideAttributes(tag: string): IAttributeData[];
212 | provideValues(tag: string, attribute: string): IValueData[];
213 | }
214 |
215 | /**
216 | * Describes what LSP capabilities the client supports
217 | */
218 | export interface ClientCapabilities {
219 | /**
220 | * The text document client capabilities
221 | */
222 | textDocument?: {
223 | /**
224 | * Capabilities specific to completions.
225 | */
226 | completion?: {
227 | /**
228 | * The client supports the following `CompletionItem` specific
229 | * capabilities.
230 | */
231 | completionItem?: {
232 | /**
233 | * Client supports the follow content formats for the documentation
234 | * property. The order describes the preferred format of the client.
235 | */
236 | documentationFormat?: MarkupKind[];
237 | };
238 |
239 | };
240 | /**
241 | * Capabilities specific to hovers.
242 | */
243 | hover?: {
244 | /**
245 | * Client supports the follow content formats for the content
246 | * property. The order describes the preferred format of the client.
247 | */
248 | contentFormat?: MarkupKind[];
249 | };
250 | };
251 | }
252 |
253 | export namespace ClientCapabilities {
254 | export const LATEST: ClientCapabilities = {
255 | textDocument: {
256 | completion: {
257 | completionItem: {
258 | documentationFormat: [MarkupKind.Markdown, MarkupKind.PlainText]
259 | }
260 | },
261 | hover: {
262 | contentFormat: [MarkupKind.Markdown, MarkupKind.PlainText]
263 | }
264 | }
265 | };
266 | }
267 |
268 | export interface LanguageServiceOptions {
269 | /**
270 | * Unless set to false, the default HTML data provider will be used
271 | * along with the providers from customDataProviders.
272 | * Defaults to true.
273 | */
274 | useDefaultDataProvider?: boolean;
275 |
276 | /**
277 | * Provide data that could enhance the service's understanding of
278 | * HTML tag / attribute / attribute-value
279 | */
280 | customDataProviders?: IHTMLDataProvider[];
281 |
282 | /**
283 | * Abstract file system access away from the service.
284 | * Used for path completion, etc.
285 | */
286 | fileSystemProvider?: FileSystemProvider;
287 |
288 | /**
289 | * Describes the LSP capabilities the client supports.
290 | */
291 | clientCapabilities?: ClientCapabilities;
292 | }
293 |
294 | export enum FileType {
295 | /**
296 | * The file type is unknown.
297 | */
298 | Unknown = 0,
299 | /**
300 | * A regular file.
301 | */
302 | File = 1,
303 | /**
304 | * A directory.
305 | */
306 | Directory = 2,
307 | /**
308 | * A symbolic link to a file.
309 | */
310 | SymbolicLink = 64
311 | }
312 |
313 | export interface FileStat {
314 | /**
315 | * The type of the file, e.g. is a regular file, a directory, or symbolic link
316 | * to a file.
317 | */
318 | type: FileType;
319 | /**
320 | * The creation timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC.
321 | */
322 | ctime: number;
323 | /**
324 | * The modification timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC.
325 | */
326 | mtime: number;
327 | /**
328 | * The size in bytes.
329 | */
330 | size: number;
331 | }
332 |
333 | export interface FileSystemProvider {
334 | stat(uri: DocumentUri): Promise;
335 | readDirectory?(uri: DocumentUri): Promise<[string, FileType][]>;
336 | }
337 |
--------------------------------------------------------------------------------
/src/languageFacts/dataManager.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation. All rights reserved.
3 | * Licensed under the MIT License. See License.txt in the project root for license information.
4 | *--------------------------------------------------------------------------------------------*/
5 |
6 | import { IHTMLDataProvider } from '../htmlLanguageTypes';
7 | import { HTMLDataProvider } from './dataProvider';
8 | import { htmlData } from './data/webCustomData';
9 | import * as arrays from '../utils/arrays';
10 |
11 | export class HTMLDataManager {
12 | private dataProviders: IHTMLDataProvider[] = [];
13 |
14 | constructor(options: { useDefaultDataProvider?: boolean, customDataProviders?: IHTMLDataProvider[] }) {
15 | this.setDataProviders(options.useDefaultDataProvider !== false, options.customDataProviders || []);
16 | }
17 | setDataProviders(builtIn: boolean, providers: IHTMLDataProvider[]) {
18 | this.dataProviders = [];
19 | if (builtIn) {
20 | this.dataProviders.push(new HTMLDataProvider('html5', htmlData));
21 | }
22 | this.dataProviders.push(...providers);
23 | }
24 |
25 | getDataProviders() {
26 | return this.dataProviders;
27 | }
28 |
29 | isVoidElement(e: string, voidElements: string[]) {
30 | return !!e && arrays.binarySearch(voidElements, e.toLowerCase(), (s1: string, s2: string) => s1.localeCompare(s2)) >= 0;
31 | }
32 |
33 | getVoidElements(languageId: string): string[];
34 | getVoidElements(dataProviders: IHTMLDataProvider[]): string[];
35 | getVoidElements(languageOrProviders: string | IHTMLDataProvider[]): string[] {
36 | const dataProviders = Array.isArray(languageOrProviders) ? languageOrProviders : this.getDataProviders().filter(p => p.isApplicable(languageOrProviders!));
37 | const voidTags: string[] = [];
38 | dataProviders.forEach((provider) => {
39 | provider.provideTags().filter(tag => tag.void).forEach(tag => voidTags.push(tag.name));
40 | });
41 | return voidTags.sort();
42 | }
43 |
44 | isPathAttribute(tag: string, attr: string) {
45 | // should eventually come from custom data
46 |
47 | if (attr === 'src' || attr === 'href') {
48 | return true;
49 | }
50 | const a = PATH_TAG_AND_ATTR[tag];
51 | if (a) {
52 | if (typeof a === 'string') {
53 | return a === attr;
54 | } else {
55 | return a.indexOf(attr) !== -1;
56 | }
57 | }
58 | return false;
59 | }
60 | }
61 |
62 | // Selected from https://stackoverflow.com/a/2725168/1780148
63 | const PATH_TAG_AND_ATTR: { [tag: string]: string | string[] } = {
64 | // HTML 4
65 | a: 'href',
66 | area: 'href',
67 | body: 'background',
68 | blockquote: 'cite',
69 | del: 'cite',
70 | form: 'action',
71 | frame: ['src', 'longdesc'],
72 | img: ['src', 'longdesc'],
73 | ins: 'cite',
74 | link: 'href',
75 | object: 'data',
76 | q: 'cite',
77 | script: 'src',
78 | // HTML 5
79 | audio: 'src',
80 | button: 'formaction',
81 | command: 'icon',
82 | embed: 'src',
83 | html: 'manifest',
84 | input: ['src', 'formaction'],
85 | source: 'src',
86 | track: 'src',
87 | video: ['src', 'poster']
88 | };
89 |
--------------------------------------------------------------------------------
/src/parser/htmlParser.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation. All rights reserved.
3 | * Licensed under the MIT License. See License.txt in the project root for license information.
4 | *--------------------------------------------------------------------------------------------*/
5 |
6 | import { createScanner } from './htmlScanner';
7 | import { findFirst } from '../utils/arrays';
8 | import { TokenType, TextDocument } from '../htmlLanguageTypes';
9 | import { HTMLDataManager } from '../languageFacts/dataManager';
10 |
11 | export class Node {
12 | public tag: string | undefined;
13 | public closed: boolean = false;
14 | public startTagEnd: number | undefined;
15 | public endTagStart: number | undefined;
16 | public attributes: { [name: string]: string | null } | undefined;
17 | public get attributeNames(): string[] { return this.attributes ? Object.keys(this.attributes) : []; }
18 | constructor(public start: number, public end: number, public children: Node[], public parent?: Node) {
19 | }
20 | public isSameTag(tagInLowerCase: string | undefined) {
21 | if (this.tag === undefined) {
22 | return tagInLowerCase === undefined;
23 | } else {
24 | return tagInLowerCase !== undefined && this.tag.length === tagInLowerCase.length && this.tag.toLowerCase() === tagInLowerCase;
25 | }
26 | }
27 | public get firstChild(): Node | undefined { return this.children[0]; }
28 | public get lastChild(): Node | undefined { return this.children.length ? this.children[this.children.length - 1] : void 0; }
29 |
30 | public findNodeBefore(offset: number): Node {
31 | const idx = findFirst(this.children, c => offset <= c.start) - 1;
32 | if (idx >= 0) {
33 | const child = this.children[idx];
34 | if (offset > child.start) {
35 | if (offset < child.end) {
36 | return child.findNodeBefore(offset);
37 | }
38 | const lastChild = child.lastChild;
39 | if (lastChild && lastChild.end === child.end) {
40 | return child.findNodeBefore(offset);
41 | }
42 | return child;
43 | }
44 | }
45 | return this;
46 | }
47 |
48 | public findNodeAt(offset: number): Node {
49 | const idx = findFirst(this.children, c => offset <= c.start) - 1;
50 | if (idx >= 0) {
51 | const child = this.children[idx];
52 | if (offset > child.start && offset <= child.end) {
53 | return child.findNodeAt(offset);
54 | }
55 | }
56 | return this;
57 | }
58 | }
59 |
60 | export interface HTMLDocument {
61 | roots: Node[];
62 | findNodeBefore(offset: number): Node;
63 | findNodeAt(offset: number): Node;
64 | }
65 |
66 | export class HTMLParser {
67 | constructor(private dataManager: HTMLDataManager) {
68 |
69 | }
70 |
71 | public parseDocument(document: TextDocument): HTMLDocument {
72 | return this.parse(document.getText(), this.dataManager.getVoidElements(document.languageId));
73 | }
74 |
75 | public parse(text: string, voidElements: string[]): HTMLDocument {
76 | const scanner = createScanner(text, undefined, undefined, true);
77 |
78 | const htmlDocument = new Node(0, text.length, [], void 0);
79 | let curr = htmlDocument;
80 | let endTagStart: number = -1;
81 | let endTagName: string | undefined = undefined;
82 | let pendingAttribute: string | null = null;
83 | let token = scanner.scan();
84 | while (token !== TokenType.EOS) {
85 | switch (token) {
86 | case TokenType.StartTagOpen:
87 | const child = new Node(scanner.getTokenOffset(), text.length, [], curr);
88 | curr.children.push(child);
89 | curr = child;
90 | break;
91 | case TokenType.StartTag:
92 | curr.tag = scanner.getTokenText();
93 | break;
94 | case TokenType.StartTagClose:
95 | if (curr.parent) {
96 | curr.end = scanner.getTokenEnd(); // might be later set to end tag position
97 | if (scanner.getTokenLength()) {
98 | curr.startTagEnd = scanner.getTokenEnd();
99 | if (curr.tag && this.dataManager.isVoidElement(curr.tag, voidElements)) {
100 | curr.closed = true;
101 | curr = curr.parent;
102 | }
103 | } else {
104 | // pseudo close token from an incomplete start tag
105 | curr = curr.parent;
106 | }
107 | }
108 | break;
109 | case TokenType.StartTagSelfClose:
110 | if (curr.parent) {
111 | curr.closed = true;
112 | curr.end = scanner.getTokenEnd();
113 | curr.startTagEnd = scanner.getTokenEnd();
114 | curr = curr.parent;
115 | }
116 | break;
117 | case TokenType.EndTagOpen:
118 | endTagStart = scanner.getTokenOffset();
119 | endTagName = undefined;
120 | break;
121 | case TokenType.EndTag:
122 | endTagName = scanner.getTokenText().toLowerCase();
123 | break;
124 | case TokenType.EndTagClose:
125 | let node = curr;
126 | // see if we can find a matching tag
127 | while (!node.isSameTag(endTagName) && node.parent) {
128 | node = node.parent;
129 | }
130 | if (node.parent) {
131 | while (curr !== node) {
132 | curr.end = endTagStart;
133 | curr.closed = false;
134 | curr = curr.parent!;
135 | }
136 | curr.closed = true;
137 | curr.endTagStart = endTagStart;
138 | curr.end = scanner.getTokenEnd();
139 | curr = curr.parent!;
140 | }
141 | break;
142 | case TokenType.AttributeName: {
143 | pendingAttribute = scanner.getTokenText();
144 | let attributes = curr.attributes;
145 | if (!attributes) {
146 | curr.attributes = attributes = {};
147 | }
148 | attributes[pendingAttribute] = null; // Support valueless attributes such as 'checked'
149 | break;
150 | }
151 | case TokenType.AttributeValue: {
152 | const value = scanner.getTokenText();
153 | const attributes = curr.attributes;
154 | if (attributes && pendingAttribute) {
155 | attributes[pendingAttribute] = value;
156 | pendingAttribute = null;
157 | }
158 | break;
159 | }
160 | }
161 | token = scanner.scan();
162 | }
163 | while (curr.parent) {
164 | curr.end = text.length;
165 | curr.closed = false;
166 | curr = curr.parent;
167 | }
168 | return {
169 | roots: htmlDocument.children,
170 | findNodeBefore: htmlDocument.findNodeBefore.bind(htmlDocument),
171 | findNodeAt: htmlDocument.findNodeAt.bind(htmlDocument)
172 | };
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/src/services/htmlFolding.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation. All rights reserved.
3 | * Licensed under the MIT License. See License.txt in the project root for license information.
4 | *--------------------------------------------------------------------------------------------*/
5 |
6 | import { TokenType, FoldingRange, FoldingRangeKind, TextDocument } from '../htmlLanguageTypes';
7 | import { createScanner } from '../parser/htmlScanner';
8 | import { HTMLDataManager } from '../languageFacts/dataManager';
9 |
10 | export class HTMLFolding {
11 | constructor(private dataManager: HTMLDataManager) {
12 | }
13 | private limitRanges(ranges: FoldingRange[], rangeLimit: number) {
14 | ranges = ranges.sort((r1, r2) => {
15 | let diff = r1.startLine - r2.startLine;
16 | if (diff === 0) {
17 | diff = r1.endLine - r2.endLine;
18 | }
19 | return diff;
20 | });
21 |
22 | // compute each range's nesting level in 'nestingLevels'.
23 | // count the number of ranges for each level in 'nestingLevelCounts'
24 | let top: FoldingRange | undefined = void 0;
25 | const previous: FoldingRange[] = [];
26 | const nestingLevels: number[] = [];
27 | const nestingLevelCounts: number[] = [];
28 |
29 | const setNestingLevel = (index: number, level: number) => {
30 | nestingLevels[index] = level;
31 | if (level < 30) {
32 | nestingLevelCounts[level] = (nestingLevelCounts[level] || 0) + 1;
33 | }
34 | };
35 |
36 | // compute nesting levels and sanitize
37 | for (let i = 0; i < ranges.length; i++) {
38 | const entry = ranges[i];
39 | if (!top) {
40 | top = entry;
41 | setNestingLevel(i, 0);
42 | } else {
43 | if (entry.startLine > top.startLine) {
44 | if (entry.endLine <= top.endLine) {
45 | previous.push(top);
46 | top = entry;
47 | setNestingLevel(i, previous.length);
48 | } else if (entry.startLine > top.endLine) {
49 | do {
50 | top = previous.pop();
51 | } while (top && entry.startLine > top.endLine);
52 | if (top) {
53 | previous.push(top);
54 | }
55 | top = entry;
56 | setNestingLevel(i, previous.length);
57 | }
58 | }
59 | }
60 | }
61 | let entries = 0;
62 | let maxLevel = 0;
63 | for (let i = 0; i < nestingLevelCounts.length; i++) {
64 | const n = nestingLevelCounts[i];
65 | if (n) {
66 | if (n + entries > rangeLimit) {
67 | maxLevel = i;
68 | break;
69 | }
70 | entries += n;
71 | }
72 | }
73 |
74 | const result = [];
75 | for (let i = 0; i < ranges.length; i++) {
76 | const level = nestingLevels[i];
77 | if (typeof level === 'number') {
78 | if (level < maxLevel || (level === maxLevel && entries++ < rangeLimit)) {
79 | result.push(ranges[i]);
80 | }
81 | }
82 | }
83 | return result;
84 | }
85 |
86 | public getFoldingRanges(document: TextDocument, context: { rangeLimit?: number } | undefined): FoldingRange[] {
87 | const scanner = createScanner(document.getText());
88 | let token = scanner.scan();
89 | const ranges: FoldingRange[] = [];
90 | const stack: { startLine: number, tagName: string }[] = [];
91 | let lastTagName = null;
92 | let prevStart = -1;
93 | let voidElements: string[] | undefined;
94 |
95 | function addRange(range: FoldingRange) {
96 | ranges.push(range);
97 | prevStart = range.startLine;
98 | }
99 |
100 | while (token !== TokenType.EOS) {
101 | switch (token) {
102 | case TokenType.StartTag: {
103 | const tagName = scanner.getTokenText();
104 | const startLine = document.positionAt(scanner.getTokenOffset()).line;
105 | stack.push({ startLine, tagName });
106 | lastTagName = tagName;
107 | break;
108 | }
109 | case TokenType.EndTag: {
110 | lastTagName = scanner.getTokenText();
111 | break;
112 | }
113 | case TokenType.StartTagClose:
114 | if (!lastTagName) {
115 | break;
116 | }
117 | voidElements ??= this.dataManager.getVoidElements(document.languageId);
118 | if (!this.dataManager.isVoidElement(lastTagName, voidElements)) {
119 | break;
120 | }
121 | // fallthrough
122 | case TokenType.EndTagClose:
123 | case TokenType.StartTagSelfClose: {
124 | let i = stack.length - 1;
125 | while (i >= 0 && stack[i].tagName !== lastTagName) {
126 | i--;
127 | }
128 | if (i >= 0) {
129 | const stackElement = stack[i];
130 | stack.length = i;
131 | const line = document.positionAt(scanner.getTokenOffset()).line;
132 | const startLine = stackElement.startLine;
133 | const endLine = line - 1;
134 | if (endLine > startLine && prevStart !== startLine) {
135 | addRange({ startLine, endLine });
136 | }
137 | }
138 | break;
139 | }
140 | case TokenType.Comment: {
141 | let startLine = document.positionAt(scanner.getTokenOffset()).line;
142 | const text = scanner.getTokenText();
143 | const m = text.match(/^\s*#(region\b)|(endregion\b)/);
144 | if (m) {
145 | if (m[1]) { // start pattern match
146 | stack.push({ startLine, tagName: '' }); // empty tagName marks region
147 | } else {
148 | let i = stack.length - 1;
149 | while (i >= 0 && stack[i].tagName.length) {
150 | i--;
151 | }
152 | if (i >= 0) {
153 | const stackElement = stack[i];
154 | stack.length = i;
155 | const endLine = startLine;
156 | startLine = stackElement.startLine;
157 | if (endLine > startLine && prevStart !== startLine) {
158 | addRange({ startLine, endLine, kind: FoldingRangeKind.Region });
159 | }
160 | }
161 | }
162 | } else {
163 | const endLine = document.positionAt(scanner.getTokenOffset() + scanner.getTokenLength()).line;
164 | if (startLine < endLine) {
165 | addRange({ startLine, endLine, kind: FoldingRangeKind.Comment });
166 | }
167 | }
168 | break;
169 | }
170 | }
171 | token = scanner.scan();
172 | }
173 |
174 | const rangeLimit = context && context.rangeLimit || Number.MAX_VALUE;
175 | if (ranges.length > rangeLimit) {
176 | return this.limitRanges(ranges, rangeLimit);
177 | }
178 | return ranges;
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/src/services/htmlFormatter.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation. All rights reserved.
3 | * Licensed under the MIT License. See License.txt in the project root for license information.
4 | *--------------------------------------------------------------------------------------------*/
5 |
6 | import { HTMLFormatConfiguration, Range, TextEdit, Position, TextDocument } from '../htmlLanguageTypes';
7 | import { IBeautifyHTMLOptions, html_beautify } from '../beautify/beautify-html';
8 | import { repeat } from '../utils/strings';
9 |
10 | export function format(document: TextDocument, range: Range | undefined, options: HTMLFormatConfiguration): TextEdit[] {
11 | let value = document.getText();
12 | let includesEnd = true;
13 | let initialIndentLevel = 0;
14 | const tabSize = options.tabSize || 4;
15 | if (range) {
16 | let startOffset = document.offsetAt(range.start);
17 |
18 | // include all leading whitespace iff at the beginning of the line
19 | let extendedStart = startOffset;
20 | while (extendedStart > 0 && isWhitespace(value, extendedStart - 1)) {
21 | extendedStart--;
22 | }
23 | if (extendedStart === 0 || isEOL(value, extendedStart - 1)) {
24 | startOffset = extendedStart;
25 | } else {
26 | // else keep at least one whitespace
27 | if (extendedStart < startOffset) {
28 | startOffset = extendedStart + 1;
29 | }
30 | }
31 |
32 | // include all following whitespace until the end of the line
33 | let endOffset = document.offsetAt(range.end);
34 | let extendedEnd = endOffset;
35 | while (extendedEnd < value.length && isWhitespace(value, extendedEnd)) {
36 | extendedEnd++;
37 | }
38 | if (extendedEnd === value.length || isEOL(value, extendedEnd)) {
39 | endOffset = extendedEnd;
40 | }
41 | range = Range.create(document.positionAt(startOffset), document.positionAt(endOffset));
42 |
43 | // Do not modify if substring starts in inside an element
44 | // Ending inside an element is fine as it doesn't cause formatting errors
45 | const firstHalf = value.substring(0, startOffset);
46 | if (new RegExp(/.*[<][^>]*$/).test(firstHalf)) {
47 | //return without modification
48 | value = value.substring(startOffset, endOffset);
49 | return [{
50 | range: range,
51 | newText: value
52 | }];
53 | }
54 |
55 | includesEnd = endOffset === value.length;
56 | value = value.substring(startOffset, endOffset);
57 |
58 | if (startOffset !== 0) {
59 | const startOfLineOffset = document.offsetAt(Position.create(range.start.line, 0));
60 | initialIndentLevel = computeIndentLevel(document.getText(), startOfLineOffset, options);
61 | }
62 | } else {
63 | range = Range.create(Position.create(0, 0), document.positionAt(value.length));
64 | }
65 | const htmlOptions: IBeautifyHTMLOptions = {
66 | indent_size: tabSize,
67 | indent_char: options.insertSpaces ? ' ' : '\t',
68 | indent_empty_lines: getFormatOption(options, 'indentEmptyLines', false),
69 | wrap_line_length: getFormatOption(options, 'wrapLineLength', 120),
70 | unformatted: getTagsFormatOption(options, 'unformatted', void 0),
71 | content_unformatted: getTagsFormatOption(options, 'contentUnformatted', void 0),
72 | indent_inner_html: getFormatOption(options, 'indentInnerHtml', false),
73 | preserve_newlines: getFormatOption(options, 'preserveNewLines', true),
74 | max_preserve_newlines: getFormatOption(options, 'maxPreserveNewLines', 32786),
75 | indent_handlebars: getFormatOption(options, 'indentHandlebars', false),
76 | end_with_newline: includesEnd && getFormatOption(options, 'endWithNewline', false),
77 | extra_liners: getTagsFormatOption(options, 'extraLiners', void 0),
78 | wrap_attributes: getFormatOption(options, 'wrapAttributes', 'auto'),
79 | wrap_attributes_indent_size: getFormatOption(options, 'wrapAttributesIndentSize', void 0),
80 | eol: '\n',
81 | indent_scripts: getFormatOption(options, 'indentScripts', 'normal'),
82 | templating: getTemplatingFormatOption(options, 'all'),
83 | unformatted_content_delimiter: getFormatOption(options, 'unformattedContentDelimiter', ''),
84 | };
85 |
86 | let result = html_beautify(trimLeft(value), htmlOptions);
87 | if (initialIndentLevel > 0) {
88 | const indent = options.insertSpaces ? repeat(' ', tabSize * initialIndentLevel) : repeat('\t', initialIndentLevel);
89 | result = result.split('\n').join('\n' + indent);
90 | if (range.start.character === 0) {
91 | result = indent + result; // keep the indent
92 | }
93 | }
94 | return [{
95 | range: range,
96 | newText: result
97 | }];
98 | }
99 |
100 | function trimLeft(str: string) {
101 | return str.replace(/^\s+/, '');
102 | }
103 |
104 | function getFormatOption(options: HTMLFormatConfiguration, key: keyof HTMLFormatConfiguration, dflt: any): any {
105 | if (options && options.hasOwnProperty(key)) {
106 | const value = options[key];
107 | if (value !== null) {
108 | return value;
109 | }
110 | }
111 | return dflt;
112 | }
113 |
114 | function getTagsFormatOption(options: HTMLFormatConfiguration, key: keyof HTMLFormatConfiguration, dflt: string[] | undefined): string[] | undefined {
115 | const list = getFormatOption(options, key, null);
116 | if (typeof list === 'string') {
117 | if (list.length > 0) {
118 | return list.split(',').map(t => t.trim().toLowerCase());
119 | }
120 | return [];
121 | }
122 | return dflt;
123 | }
124 |
125 | function getTemplatingFormatOption(options: HTMLFormatConfiguration, dflt: string): ('auto' | 'none' | 'angular' | 'django' | 'erb' | 'handlebars' | 'php' | 'smarty')[] | undefined {
126 | const value = getFormatOption(options, 'templating', dflt);
127 | if (value === true) {
128 | return ['auto'];
129 | }
130 | if (value === false || value === dflt || Array.isArray(value) === false) {
131 | return ['none'];
132 | }
133 | return value;
134 | }
135 |
136 | function computeIndentLevel(content: string, offset: number, options: HTMLFormatConfiguration): number {
137 | let i = offset;
138 | let nChars = 0;
139 | const tabSize = options.tabSize || 4;
140 | while (i < content.length) {
141 | const ch = content.charAt(i);
142 | if (ch === ' ') {
143 | nChars++;
144 | } else if (ch === '\t') {
145 | nChars += tabSize;
146 | } else {
147 | break;
148 | }
149 | i++;
150 | }
151 | return Math.floor(nChars / tabSize);
152 | }
153 |
154 | function isEOL(text: string, offset: number) {
155 | return '\r\n'.indexOf(text.charAt(offset)) !== -1;
156 | }
157 |
158 | function isWhitespace(text: string, offset: number) {
159 | return ' \t'.indexOf(text.charAt(offset)) !== -1;
160 | }
161 |
--------------------------------------------------------------------------------
/src/services/htmlHighlighting.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation. All rights reserved.
3 | * Licensed under the MIT License. See License.txt in the project root for license information.
4 | *--------------------------------------------------------------------------------------------*/
5 |
6 | import { HTMLDocument } from '../parser/htmlParser';
7 | import { createScanner } from '../parser/htmlScanner';
8 | import { TokenType, TextDocument, Range, Position, DocumentHighlightKind, DocumentHighlight } from '../htmlLanguageTypes';
9 |
10 | export function findDocumentHighlights(document: TextDocument, position: Position, htmlDocument: HTMLDocument): DocumentHighlight[] {
11 | const offset = document.offsetAt(position);
12 | const node = htmlDocument.findNodeAt(offset);
13 | if (!node.tag) {
14 | return [];
15 | }
16 | const result = [];
17 | const startTagRange = getTagNameRange(TokenType.StartTag, document, node.start);
18 | const endTagRange = typeof node.endTagStart === 'number' && getTagNameRange(TokenType.EndTag, document, node.endTagStart);
19 | if (startTagRange && covers(startTagRange, position) || endTagRange && covers(endTagRange, position)) {
20 | if (startTagRange) {
21 | result.push({ kind: DocumentHighlightKind.Read, range: startTagRange });
22 | }
23 | if (endTagRange) {
24 | result.push({ kind: DocumentHighlightKind.Read, range: endTagRange });
25 | }
26 | }
27 | return result;
28 | }
29 |
30 | function isBeforeOrEqual(pos1: Position, pos2: Position) {
31 | return pos1.line < pos2.line || (pos1.line === pos2.line && pos1.character <= pos2.character);
32 | }
33 |
34 | function covers(range: Range, position: Position) {
35 | return isBeforeOrEqual(range.start, position) && isBeforeOrEqual(position, range.end);
36 | }
37 |
38 | function getTagNameRange(tokenType: TokenType, document: TextDocument, startOffset: number): Range | null {
39 | const scanner = createScanner(document.getText(), startOffset);
40 | let token = scanner.scan();
41 | while (token !== TokenType.EOS && token !== tokenType) {
42 | token = scanner.scan();
43 | }
44 | if (token !== TokenType.EOS) {
45 | return { start: document.positionAt(scanner.getTokenOffset()), end: document.positionAt(scanner.getTokenEnd()) };
46 | }
47 | return null;
48 | }
49 |
--------------------------------------------------------------------------------
/src/services/htmlHover.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation. All rights reserved.
3 | * Licensed under the MIT License. See License.txt in the project root for license information.
4 | *--------------------------------------------------------------------------------------------*/
5 |
6 | import { HTMLDocument } from '../parser/htmlParser';
7 | import { createScanner } from '../parser/htmlScanner';
8 | import { TokenType, LanguageServiceOptions, HoverSettings, TextDocument, Range, Position, Hover, MarkedString, MarkupContent, MarkupKind } from '../htmlLanguageTypes';
9 | import { HTMLDataManager } from '../languageFacts/dataManager';
10 | import { isDefined } from '../utils/object';
11 | import { generateDocumentation } from '../languageFacts/dataProvider';
12 | import { entities } from '../parser/htmlEntities';
13 | import { isLetterOrDigit } from '../utils/strings';
14 | import * as l10n from '@vscode/l10n';
15 |
16 | export class HTMLHover {
17 | private supportsMarkdown: boolean | undefined;
18 |
19 | constructor(private lsOptions: LanguageServiceOptions, private dataManager: HTMLDataManager) { }
20 |
21 | doHover(document: TextDocument, position: Position, htmlDocument: HTMLDocument, options?: HoverSettings): Hover | null {
22 | const convertContents = this.convertContents.bind(this);
23 | const doesSupportMarkdown = this.doesSupportMarkdown();
24 |
25 | const offset = document.offsetAt(position);
26 | const node = htmlDocument.findNodeAt(offset);
27 | const text = document.getText();
28 | if (!node || !node.tag) {
29 | return null;
30 | }
31 | const dataProviders = this.dataManager.getDataProviders().filter(p => p.isApplicable(document.languageId));
32 |
33 | function getTagHover(currTag: string, range: Range, open: boolean): Hover | null {
34 | for (const provider of dataProviders) {
35 | let hover: Hover | null = null;
36 |
37 | provider.provideTags().forEach(tag => {
38 | if (tag.name.toLowerCase() === currTag.toLowerCase()) {
39 | let markupContent = generateDocumentation(tag, options, doesSupportMarkdown);
40 | if (!markupContent) {
41 | markupContent = {
42 | kind: doesSupportMarkdown ? 'markdown' : 'plaintext',
43 | value: ''
44 | };
45 | }
46 | hover = { contents: markupContent, range };
47 | }
48 | });
49 |
50 | if (hover) {
51 | (hover as Hover).contents = convertContents((hover as Hover).contents);
52 | return hover;
53 | }
54 | }
55 | return null;
56 | }
57 |
58 | function getAttrHover(currTag: string, currAttr: string, range: Range): Hover | null {
59 | for (const provider of dataProviders) {
60 | let hover: Hover | null = null;
61 |
62 | provider.provideAttributes(currTag).forEach(attr => {
63 | if (currAttr === attr.name && attr.description) {
64 | const contentsDoc = generateDocumentation(attr, options, doesSupportMarkdown);
65 | if (contentsDoc) {
66 | hover = { contents: contentsDoc, range };
67 | } else {
68 | hover = null;
69 | }
70 | }
71 | });
72 |
73 | if (hover) {
74 | (hover as Hover).contents = convertContents((hover as Hover).contents);
75 | return hover;
76 | }
77 | }
78 | return null;
79 | }
80 |
81 | function getAttrValueHover(currTag: string, currAttr: string, currAttrValue: string, range: Range): Hover | null {
82 | for (const provider of dataProviders) {
83 | let hover: Hover | null = null;
84 |
85 | provider.provideValues(currTag, currAttr).forEach(attrValue => {
86 | if (currAttrValue === attrValue.name && attrValue.description) {
87 | const contentsDoc = generateDocumentation(attrValue, options, doesSupportMarkdown);
88 | if (contentsDoc) {
89 | hover = { contents: contentsDoc, range };
90 | } else {
91 | hover = null;
92 | }
93 | }
94 | });
95 |
96 | if (hover) {
97 | (hover as Hover).contents = convertContents((hover as Hover).contents);
98 | return hover;
99 | }
100 | }
101 | return null;
102 | }
103 |
104 | function getEntityHover(text: string, range: Range): Hover | null {
105 | let currEntity = filterEntity(text);
106 |
107 | for (const entity in entities) {
108 | let hover: Hover | null = null;
109 |
110 | const label = '&' + entity;
111 |
112 | if (currEntity === label) {
113 | let code = entities[entity].charCodeAt(0).toString(16).toUpperCase();
114 | let hex = 'U+';
115 |
116 | if (code.length < 4) {
117 | const zeroes = 4 - code.length;
118 | let k = 0;
119 |
120 | while (k < zeroes) {
121 | hex += '0';
122 | k += 1;
123 | }
124 | }
125 |
126 | hex += code;
127 | const contentsDoc = l10n.t('Character entity representing \'{0}\', unicode equivalent \'{1}\'', entities[entity], hex);
128 | if (contentsDoc) {
129 | hover = { contents: contentsDoc, range };
130 | } else {
131 | hover = null;
132 | }
133 | }
134 |
135 | if (hover) {
136 | (hover as Hover).contents = convertContents((hover as Hover).contents);
137 | return hover;
138 | }
139 | }
140 | return null;
141 | }
142 |
143 | function getTagNameRange(tokenType: TokenType, startOffset: number): Range | null {
144 | const scanner = createScanner(document.getText(), startOffset);
145 | let token = scanner.scan();
146 | while (token !== TokenType.EOS && (scanner.getTokenEnd() < offset || scanner.getTokenEnd() === offset && token !== tokenType)) {
147 | token = scanner.scan();
148 | }
149 | if (token === tokenType && offset <= scanner.getTokenEnd()) {
150 | return { start: document.positionAt(scanner.getTokenOffset()), end: document.positionAt(scanner.getTokenEnd()) };
151 | }
152 | return null;
153 | }
154 |
155 | function getEntityRange(): Range | null {
156 | let k = offset - 1;
157 | let characterStart = position.character;
158 |
159 | while (k >= 0 && isLetterOrDigit(text, k)) {
160 | k--;
161 | characterStart--;
162 | }
163 |
164 | let n = k + 1;
165 | let characterEnd = characterStart;
166 |
167 | while (isLetterOrDigit(text, n)) {
168 | n++;
169 | characterEnd++;
170 | }
171 |
172 | if (k >= 0 && text[k] === '&') {
173 | let range: Range | null = null;
174 |
175 | if (text[n] === ';') {
176 | range = Range.create(Position.create(position.line, characterStart), Position.create(position.line, characterEnd + 1));
177 | } else {
178 | range = Range.create(Position.create(position.line, characterStart), Position.create(position.line, characterEnd));
179 | }
180 |
181 | return range;
182 | }
183 |
184 | return null;
185 | }
186 |
187 | function filterEntity(text: string): string {
188 | let k = offset - 1;
189 | let newText = '&';
190 |
191 | while (k >= 0 && isLetterOrDigit(text, k)) {
192 | k--;
193 | }
194 |
195 | k = k + 1;
196 |
197 | while (isLetterOrDigit(text, k)) {
198 | newText += text[k];
199 | k += 1;
200 | }
201 |
202 | newText += ';';
203 |
204 | return newText;
205 | }
206 |
207 | if (node.endTagStart && offset >= node.endTagStart) {
208 | const tagRange = getTagNameRange(TokenType.EndTag, node.endTagStart);
209 | if (tagRange) {
210 | return getTagHover(node.tag, tagRange, false);
211 | }
212 | return null;
213 | }
214 |
215 | const tagRange = getTagNameRange(TokenType.StartTag, node.start);
216 | if (tagRange) {
217 | return getTagHover(node.tag, tagRange, true);
218 | }
219 |
220 | const attrRange = getTagNameRange(TokenType.AttributeName, node.start);
221 | if (attrRange) {
222 | const tag = node.tag;
223 | const attr = document.getText(attrRange);
224 | return getAttrHover(tag, attr, attrRange);
225 | }
226 |
227 | const entityRange = getEntityRange();
228 | if (entityRange) {
229 | return getEntityHover(text, entityRange);
230 | }
231 |
232 | function scanAttrAndAttrValue(nodeStart: number, attrValueStart: number) {
233 | const scanner = createScanner(document.getText(), nodeStart);
234 | let token = scanner.scan();
235 | let prevAttr = undefined;
236 | while (token !== TokenType.EOS && (scanner.getTokenEnd() <= attrValueStart)) {
237 | token = scanner.scan();
238 | if (token === TokenType.AttributeName) {
239 | prevAttr = scanner.getTokenText();
240 | }
241 | }
242 |
243 | return prevAttr;
244 | }
245 |
246 | const attrValueRange = getTagNameRange(TokenType.AttributeValue, node.start);
247 | if (attrValueRange) {
248 | const tag = node.tag;
249 | const attrValue = trimQuotes(document.getText(attrValueRange));
250 | const matchAttr = scanAttrAndAttrValue(node.start, document.offsetAt(attrValueRange.start));
251 |
252 | if (matchAttr) {
253 | return getAttrValueHover(tag, matchAttr, attrValue, attrValueRange);
254 | }
255 | }
256 |
257 | return null;
258 | }
259 |
260 | private convertContents(contents: MarkupContent | MarkedString | MarkedString[]): MarkupContent | MarkedString | MarkedString[] {
261 | if (!this.doesSupportMarkdown()) {
262 | if (typeof contents === 'string') {
263 | return contents;
264 | }
265 | // MarkupContent
266 | else if ('kind' in contents) {
267 | return {
268 | kind: 'plaintext',
269 | value: contents.value
270 | };
271 | }
272 | // MarkedString[]
273 | else if (Array.isArray(contents)) {
274 | contents.map(c => {
275 | return typeof c === 'string' ? c : c.value;
276 | });
277 | }
278 | // MarkedString
279 | else {
280 | return contents.value;
281 | }
282 | }
283 |
284 | return contents;
285 | }
286 |
287 | private doesSupportMarkdown(): boolean {
288 | if (!isDefined(this.supportsMarkdown)) {
289 | if (!isDefined(this.lsOptions.clientCapabilities)) {
290 | this.supportsMarkdown = true;
291 | return this.supportsMarkdown;
292 | }
293 |
294 | const contentFormat = this.lsOptions.clientCapabilities?.textDocument?.hover?.contentFormat;
295 | this.supportsMarkdown = Array.isArray(contentFormat) && contentFormat.indexOf(MarkupKind.Markdown) !== -1;
296 | }
297 | return this.supportsMarkdown;
298 | }
299 | }
300 |
301 | function trimQuotes(s: string) {
302 | if (s.length <= 1) {
303 | return s.replace(/['"]/, ''); // CodeQL [SM02383] False positive: The string length is at most one, so we don't need the global flag.
304 | }
305 |
306 | if (s[0] === `'` || s[0] === `"`) {
307 | s = s.slice(1);
308 | }
309 |
310 | if (s[s.length - 1] === `'` || s[s.length - 1] === `"`) {
311 | s = s.slice(0, -1);
312 | }
313 |
314 | return s;
315 | }
316 |
317 |
--------------------------------------------------------------------------------
/src/services/htmlLinkedEditing.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation. All rights reserved.
3 | * Licensed under the MIT License. See License.txt in the project root for license information.
4 | *--------------------------------------------------------------------------------------------*/
5 |
6 | import { TextDocument, Position, Range } from '../htmlLanguageTypes';
7 | import { HTMLDocument } from '../parser/htmlParser';
8 |
9 | export function findLinkedEditingRanges(
10 | document: TextDocument,
11 | position: Position,
12 | htmlDocument: HTMLDocument
13 | ): Range[] | null {
14 | const offset = document.offsetAt(position);
15 | const node = htmlDocument.findNodeAt(offset);
16 |
17 | const tagLength = node.tag ? node.tag.length : 0;
18 |
19 | if (!node.endTagStart) {
20 | return null;
21 | }
22 |
23 | if (
24 | // Within open tag, compute close tag
25 | (node.start + '<'.length <= offset && offset <= node.start + '<'.length + tagLength) ||
26 | // Within closing tag, compute open tag
27 | node.endTagStart + ''.length <= offset && offset <= node.endTagStart + ''.length + tagLength
28 | ) {
29 | return [
30 | Range.create(
31 | document.positionAt(node.start + '<'.length),
32 | document.positionAt(node.start + '<'.length + tagLength)
33 | ),
34 | Range.create(
35 | document.positionAt(node.endTagStart + ''.length),
36 | document.positionAt(node.endTagStart + ''.length + tagLength)
37 | )
38 | ];
39 | }
40 |
41 | return null;
42 | }
43 |
--------------------------------------------------------------------------------
/src/services/htmlLinks.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation. All rights reserved.
3 | * Licensed under the MIT License. See License.txt in the project root for license information.
4 | *--------------------------------------------------------------------------------------------*/
5 |
6 | import { createScanner } from '../parser/htmlScanner';
7 | import * as strings from '../utils/strings';
8 | import { URI as Uri } from 'vscode-uri';
9 |
10 | import { TokenType, DocumentContext, TextDocument, Range, DocumentLink } from '../htmlLanguageTypes';
11 | import { HTMLDataManager } from '../languageFacts/dataManager';
12 |
13 | function normalizeRef(url: string): string {
14 | const first = url[0];
15 | const last = url[url.length - 1];
16 | if (first === last && (first === '\'' || first === '\"')) {
17 | url = url.substring(1, url.length - 1);
18 | }
19 | return url;
20 | }
21 |
22 | function validateRef(url: string, languageId: string): boolean {
23 | if (!url.length) {
24 | return false;
25 | }
26 | if (languageId === 'handlebars' && /{{|}}/.test(url)) {
27 | return false;
28 | }
29 | return /\b(w[\w\d+.-]*:\/\/)?[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|\/?))/.test(url);
30 | }
31 |
32 | function getWorkspaceUrl(documentUri: string, tokenContent: string, documentContext: DocumentContext, base: string | undefined): string | undefined {
33 | if (/^\s*javascript\:/i.test(tokenContent) || /[\n\r]/.test(tokenContent)) {
34 | return undefined;
35 | }
36 | tokenContent = tokenContent.replace(/^\s*/g, '');
37 |
38 | const match = tokenContent.match(/^(\w[\w\d+.-]*):/);
39 | if (match) {
40 | // Absolute link that needs no treatment
41 | const schema = match[1].toLowerCase();
42 | if (schema === 'http' || schema === 'https' || schema === 'file') {
43 | return tokenContent;
44 | }
45 | return undefined;
46 | }
47 | if (/^\#/i.test(tokenContent)) {
48 | return documentUri + tokenContent;
49 | }
50 | if (/^\/\//i.test(tokenContent)) {
51 | // Absolute link (that does not name the protocol)
52 | const pickedScheme = strings.startsWith(documentUri, 'https://') ? 'https' : 'http';
53 | return pickedScheme + ':' + tokenContent.replace(/^\s*/g, '');
54 | }
55 | if (documentContext) {
56 | return documentContext.resolveReference(tokenContent, base || documentUri);
57 | }
58 | return tokenContent;
59 | }
60 |
61 | function createLink(document: TextDocument, documentContext: DocumentContext, attributeValue: string, startOffset: number, endOffset: number, base: string | undefined): DocumentLink | undefined {
62 | const tokenContent = normalizeRef(attributeValue);
63 | if (!validateRef(tokenContent, document.languageId)) {
64 | return undefined;
65 | }
66 | if (tokenContent.length < attributeValue.length) {
67 | startOffset++;
68 | endOffset--;
69 | }
70 | const workspaceUrl = getWorkspaceUrl(document.uri, tokenContent, documentContext, base);
71 | if (!workspaceUrl) {
72 | return undefined;
73 | }
74 | const target = validateAndCleanURI(workspaceUrl, document);
75 |
76 | return {
77 | range: Range.create(document.positionAt(startOffset), document.positionAt(endOffset)),
78 | target
79 | };
80 | }
81 |
82 | const _hash = '#'.charCodeAt(0);
83 |
84 | function validateAndCleanURI(uriStr: string, document: TextDocument): string | undefined {
85 | try {
86 | let uri = Uri.parse(uriStr);
87 | if (uri.scheme === 'file' && uri.query) {
88 | // see https://github.com/microsoft/vscode/issues/194577 & https://github.com/microsoft/vscode/issues/206238
89 | uri = uri.with({ query: null });
90 | uriStr = uri.toString(/* skipEncodig*/ true);
91 | }
92 | if (uri.scheme === 'file' && uri.fragment && !(uriStr.startsWith(document.uri) && uriStr.charCodeAt(document.uri.length) === _hash)) {
93 | return uri.with({ fragment: null }).toString(/* skipEncodig*/ true);
94 | }
95 | return uriStr;
96 | } catch (e) {
97 | return undefined;
98 | }
99 | }
100 |
101 | export class HTMLDocumentLinks {
102 |
103 | constructor(private dataManager: HTMLDataManager) {
104 | }
105 |
106 | findDocumentLinks(document: TextDocument, documentContext: DocumentContext): DocumentLink[] {
107 | const newLinks: DocumentLink[] = [];
108 |
109 | const scanner = createScanner(document.getText(), 0);
110 | let token = scanner.scan();
111 | let lastAttributeName: string | undefined = undefined;
112 | let lastTagName: string | undefined = undefined;
113 | let afterBase = false;
114 | let base: string | undefined = void 0;
115 | const idLocations: { [id: string]: number | undefined } = {};
116 |
117 | while (token !== TokenType.EOS) {
118 | switch (token) {
119 | case TokenType.StartTag:
120 | lastTagName = scanner.getTokenText().toLowerCase();
121 | if (!base) {
122 | afterBase = lastTagName === 'base';
123 | }
124 | break;
125 | case TokenType.AttributeName:
126 | lastAttributeName = scanner.getTokenText().toLowerCase();
127 | break;
128 | case TokenType.AttributeValue:
129 | if (lastTagName && lastAttributeName && this.dataManager.isPathAttribute(lastTagName, lastAttributeName)) {
130 | const attributeValue = scanner.getTokenText();
131 | if (!afterBase) { // don't highlight the base link itself
132 | const link = createLink(document, documentContext, attributeValue, scanner.getTokenOffset(), scanner.getTokenEnd(), base);
133 | if (link) {
134 | newLinks.push(link);
135 | }
136 | }
137 | if (afterBase && typeof base === 'undefined') {
138 | base = normalizeRef(attributeValue);
139 | if (base && documentContext) {
140 | base = documentContext.resolveReference(base, document.uri);
141 | }
142 | }
143 | afterBase = false;
144 | lastAttributeName = undefined;
145 | } else if (lastAttributeName === 'id') {
146 | const id = normalizeRef(scanner.getTokenText());
147 | idLocations[id] = scanner.getTokenOffset();
148 | }
149 | break;
150 | }
151 | token = scanner.scan();
152 | }
153 | // change local links with ids to actual positions
154 | for (const link of newLinks) {
155 | const localWithHash = document.uri + '#';
156 | if (link.target && strings.startsWith(link.target, localWithHash)) {
157 | const target = link.target.substring(localWithHash.length);
158 | const offset = idLocations[target];
159 | if (offset !== undefined) {
160 | const pos = document.positionAt(offset);
161 | link.target = `${localWithHash}${pos.line + 1},${pos.character + 1}`;
162 | } else {
163 | link.target = document.uri;
164 | }
165 | }
166 | }
167 | return newLinks;
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/src/services/htmlMatchingTagPosition.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation. All rights reserved.
3 | * Licensed under the MIT License. See License.txt in the project root for license information.
4 | *--------------------------------------------------------------------------------------------*/
5 |
6 | import { TextDocument, Position } from '../htmlLanguageTypes';
7 | import { HTMLDocument } from '../parser/htmlParser';
8 |
9 | export function findMatchingTagPosition(
10 | document: TextDocument,
11 | position: Position,
12 | htmlDocument: HTMLDocument
13 | ): Position | null {
14 | const offset = document.offsetAt(position);
15 | const node = htmlDocument.findNodeAt(offset);
16 |
17 | if (!node.tag) {
18 | return null;
19 | }
20 |
21 | if (!node.endTagStart) {
22 | return null;
23 | }
24 |
25 | // Within open tag, compute close tag
26 | if (node.start + '<'.length <= offset && offset <= node.start + '<'.length + node.tag.length) {
27 | const mirrorOffset = (offset - '<'.length - node.start) + node.endTagStart + ''.length;
28 | return document.positionAt(mirrorOffset);
29 | }
30 |
31 | // Within closing tag, compute open tag
32 | if (node.endTagStart + ''.length <= offset && offset <= node.endTagStart + ''.length + node.tag.length) {
33 | const mirrorOffset = (offset - ''.length - node.endTagStart) + node.start + '<'.length;
34 | return document.positionAt(mirrorOffset);
35 | }
36 |
37 | return null;
38 | }
39 |
--------------------------------------------------------------------------------
/src/services/htmlRename.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation. All rights reserved.
3 | * Licensed under the MIT License. See License.txt in the project root for license information.
4 | *--------------------------------------------------------------------------------------------*/
5 |
6 | import { TextDocument, Position, WorkspaceEdit, Range, TextEdit } from '../htmlLanguageTypes';
7 | import { HTMLDocument, Node } from '../parser/htmlParser';
8 |
9 | export function doRename(
10 | document: TextDocument,
11 | position: Position,
12 | newName: string,
13 | htmlDocument: HTMLDocument
14 | ): WorkspaceEdit | null {
15 | const offset = document.offsetAt(position);
16 | const node = htmlDocument.findNodeAt(offset);
17 |
18 | if (!node.tag) {
19 | return null;
20 | }
21 |
22 | if (!isWithinTagRange(node, offset, node.tag)) {
23 | return null;
24 | }
25 |
26 | const edits: TextEdit[] = [];
27 |
28 | const startTagRange: Range = {
29 | start: document.positionAt(node.start + '<'.length),
30 | end: document.positionAt(node.start + '<'.length + node.tag.length)
31 | };
32 | edits.push({
33 | range: startTagRange,
34 | newText: newName
35 | });
36 |
37 | if (node.endTagStart) {
38 | const endTagRange: Range = {
39 | start: document.positionAt(node.endTagStart + ''.length),
40 | end: document.positionAt(node.endTagStart + ''.length + node.tag.length)
41 | };
42 | edits.push({
43 | range: endTagRange,
44 | newText: newName
45 | });
46 | }
47 |
48 | const changes = {
49 | [document.uri.toString()]: edits
50 | };
51 |
52 | return {
53 | changes
54 | };
55 | }
56 |
57 | function toLocString(p: Position) {
58 | return `(${p.line}, ${p.character})`;
59 | }
60 |
61 | function isWithinTagRange(node: Node, offset: number, nodeTag: string) {
62 | // Self-closing tag
63 | if (node.endTagStart) {
64 | if (node.endTagStart + ''.length <= offset && offset <= node.endTagStart + ''.length + nodeTag.length) {
65 | return true;
66 | }
67 | }
68 |
69 | return node.start + '<'.length <= offset && offset <= node.start + '<'.length + nodeTag.length;
70 | }
71 |
--------------------------------------------------------------------------------
/src/services/htmlSelectionRange.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation. All rights reserved.
3 | * Licensed under the MIT License. See License.txt in the project root for license information.
4 | *--------------------------------------------------------------------------------------------*/
5 |
6 | import { createScanner } from '../parser/htmlScanner';
7 | import { Node, HTMLParser, HTMLDocument } from '../parser/htmlParser';
8 | import { TokenType, Range, Position, SelectionRange, TextDocument } from '../htmlLanguageTypes';
9 |
10 | export class HTMLSelectionRange {
11 |
12 | constructor(private htmlParser: HTMLParser) {
13 | }
14 |
15 | public getSelectionRanges(document: TextDocument, positions: Position[]): SelectionRange[] {
16 | const htmlDocument = this.htmlParser.parseDocument(document);
17 | return positions.map(p => this.getSelectionRange(p, document, htmlDocument));
18 | }
19 | private getSelectionRange(position: Position, document: TextDocument, htmlDocument: HTMLDocument): SelectionRange {
20 | const applicableRanges = this.getApplicableRanges(document, position, htmlDocument);
21 | let prev: [number, number] | undefined = undefined;
22 | let current: SelectionRange | undefined = undefined;
23 | for (let index = applicableRanges.length - 1; index >= 0; index--) {
24 | const range = applicableRanges[index];
25 | if (!prev || range[0] !== prev[0] || range[1] !== prev[1]) {
26 | current = SelectionRange.create(Range.create(
27 | document.positionAt(applicableRanges[index][0]),
28 | document.positionAt(applicableRanges[index][1])
29 | ), current);
30 | }
31 | prev = range;
32 | }
33 | if (!current) {
34 | current = SelectionRange.create(Range.create(position, position));
35 | }
36 | return current;
37 | }
38 | private getApplicableRanges(document: TextDocument, position: Position, htmlDoc: HTMLDocument): [number, number][] {
39 | const currOffset = document.offsetAt(position);
40 | const currNode = htmlDoc.findNodeAt(currOffset);
41 |
42 | let result = this.getAllParentTagRanges(currNode);
43 |
44 | // Self-closing or void elements
45 | if (currNode.startTagEnd && !currNode.endTagStart) {
46 |
47 | // THe rare case of unmatching tag pairs like
48 | if (currNode.startTagEnd !== currNode.end) {
49 | return [[currNode.start, currNode.end]];
50 | }
51 |
52 | const closeRange = Range.create(document.positionAt(currNode.startTagEnd - 2), document.positionAt(currNode.startTagEnd));
53 | const closeText = document.getText(closeRange);
54 |
55 | // Self-closing element
56 | if (closeText === '/>') {
57 | result.unshift([currNode.start + 1, currNode.startTagEnd - 2]);
58 | }
59 | // Void element
60 | else {
61 | result.unshift([currNode.start + 1, currNode.startTagEnd - 1]);
62 | }
63 |
64 | const attributeLevelRanges = this.getAttributeLevelRanges(document, currNode, currOffset);
65 | result = attributeLevelRanges.concat(result);
66 | return result;
67 | }
68 |
69 | if (!currNode.startTagEnd || !currNode.endTagStart) {
70 | return result;
71 | }
72 |
73 | /**
74 | * For html like
75 | * `
bar
`
76 | */
77 | result.unshift([currNode.start, currNode.end]);
78 |
79 | /**
80 | * Cursor inside `
`
81 | */
82 | if (currNode.start < currOffset && currOffset < currNode.startTagEnd) {
83 | result.unshift([currNode.start + 1, currNode.startTagEnd - 1]);
84 | const attributeLevelRanges = this.getAttributeLevelRanges(document, currNode, currOffset);
85 | result = attributeLevelRanges.concat(result);
86 | return result;
87 | }
88 | /**
89 | * Cursor inside `bar`
90 | */
91 | else if (currNode.startTagEnd <= currOffset && currOffset <= currNode.endTagStart) {
92 | result.unshift([currNode.startTagEnd, currNode.endTagStart]);
93 |
94 | return result;
95 | }
96 | /**
97 | * Cursor inside `
`
98 | */
99 | else {
100 | // `div` inside `
`
101 | if (currOffset >= currNode.endTagStart + 2) {
102 | result.unshift([currNode.endTagStart + 2, currNode.end - 1]);
103 | }
104 | return result;
105 | }
106 | }
107 |
108 | private getAllParentTagRanges(initialNode: Node): [number, number][] {
109 | let currNode = initialNode;
110 |
111 |
112 |
113 | const result: [number, number][] = [];
114 |
115 | while (currNode.parent) {
116 | currNode = currNode.parent;
117 | this.getNodeRanges(currNode).forEach(r => result.push(r));
118 | }
119 |
120 | return result;
121 | }
122 | private getNodeRanges(n: Node): [number, number][] {
123 | if (n.startTagEnd && n.endTagStart && n.startTagEnd < n.endTagStart) {
124 | return [
125 | [n.startTagEnd, n.endTagStart],
126 | [n.start, n.end]
127 | ];
128 | }
129 |
130 | return [
131 | [n.start, n.end]
132 | ];
133 | };
134 |
135 | private getAttributeLevelRanges(document: TextDocument, currNode: Node, currOffset: number): [number, number][] {
136 | const currNodeRange = Range.create(document.positionAt(currNode.start), document.positionAt(currNode.end));
137 | const currNodeText = document.getText(currNodeRange);
138 | const relativeOffset = currOffset - currNode.start;
139 |
140 | /**
141 | * Tag level semantic selection
142 | */
143 |
144 | const scanner = createScanner(currNodeText);
145 | let token = scanner.scan();
146 |
147 | /**
148 | * For text like
149 | * bar
150 | */
151 | const positionOffset = currNode.start;
152 |
153 | const result = [];
154 |
155 | let isInsideAttribute = false;
156 | let attrStart = -1;
157 | while (token !== TokenType.EOS) {
158 | switch (token) {
159 | case TokenType.AttributeName: {
160 | if (relativeOffset < scanner.getTokenOffset()) {
161 | isInsideAttribute = false;
162 | break;
163 | }
164 |
165 | if (relativeOffset <= scanner.getTokenEnd()) {
166 | // `class`
167 | result.unshift([scanner.getTokenOffset(), scanner.getTokenEnd()]);
168 | }
169 |
170 | isInsideAttribute = true;
171 | attrStart = scanner.getTokenOffset();
172 | break;
173 | }
174 | case TokenType.AttributeValue: {
175 | if (!isInsideAttribute) {
176 | break;
177 | }
178 |
179 | const valueText = scanner.getTokenText();
180 | if (relativeOffset < scanner.getTokenOffset()) {
181 | // `class="foo"`
182 | result.push([attrStart, scanner.getTokenEnd()]);
183 | break;
184 | }
185 |
186 | if (relativeOffset >= scanner.getTokenOffset() && relativeOffset <= scanner.getTokenEnd()) {
187 | // `"foo"`
188 | result.unshift([scanner.getTokenOffset(), scanner.getTokenEnd()]);
189 |
190 | // `foo`
191 | if ((valueText[0] === `"` && valueText[valueText.length - 1] === `"`) || (valueText[0] === `'` && valueText[valueText.length - 1] === `'`)) {
192 | if (relativeOffset >= scanner.getTokenOffset() + 1 && relativeOffset <= scanner.getTokenEnd() - 1) {
193 | result.unshift([scanner.getTokenOffset() + 1, scanner.getTokenEnd() - 1]);
194 | }
195 | }
196 |
197 | // `class="foo"`
198 | result.push([attrStart, scanner.getTokenEnd()]);
199 | }
200 |
201 | break;
202 | }
203 | }
204 | token = scanner.scan();
205 | }
206 |
207 | return result.map(pair => {
208 | return [pair[0] + positionOffset, pair[1] + positionOffset];
209 | });
210 | }
211 | }
212 |
--------------------------------------------------------------------------------
/src/services/htmlSymbolsProvider.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation. All rights reserved.
3 | * Licensed under the MIT License. See License.txt in the project root for license information.
4 | *--------------------------------------------------------------------------------------------*/
5 |
6 | import { DocumentSymbol, Range, SymbolInformation, SymbolKind, TextDocument } from '../htmlLanguageTypes';
7 | import { HTMLDocument, Node } from '../parser/htmlParser';
8 |
9 | export function findDocumentSymbols(document: TextDocument, htmlDocument: HTMLDocument): SymbolInformation[] {
10 | const symbols: SymbolInformation[] = [];
11 | const symbols2 = findDocumentSymbols2(document, htmlDocument);
12 |
13 | for (const symbol of symbols2) {
14 | walk(symbol, undefined);
15 | }
16 |
17 | return symbols;
18 |
19 | function walk(node: DocumentSymbol, parent: DocumentSymbol | undefined) {
20 | const symbol = SymbolInformation.create(node.name, node.kind, node.range, document.uri, parent?.name);
21 | symbol.containerName ??= '';
22 | symbols.push(symbol);
23 |
24 | if (node.children) {
25 | for (const child of node.children) {
26 | walk(child, node);
27 | }
28 | }
29 | }
30 | }
31 |
32 | export function findDocumentSymbols2(document: TextDocument, htmlDocument: HTMLDocument): DocumentSymbol[] {
33 | const symbols: DocumentSymbol[] = [];
34 |
35 | htmlDocument.roots.forEach(node => {
36 | provideFileSymbolsInternal(document, node, symbols);
37 | });
38 |
39 | return symbols;
40 | }
41 |
42 | function provideFileSymbolsInternal(document: TextDocument, node: Node, symbols: DocumentSymbol[]): void {
43 |
44 | const name = nodeToName(node);
45 | const range = Range.create(document.positionAt(node.start), document.positionAt(node.end));
46 | const symbol = DocumentSymbol.create(
47 | name,
48 | undefined,
49 | SymbolKind.Field,
50 | range,
51 | range,
52 | );
53 |
54 | symbols.push(symbol);
55 |
56 | node.children.forEach(child => {
57 | symbol.children ??= [];
58 | provideFileSymbolsInternal(document, child, symbol.children);
59 | });
60 | }
61 |
62 | function nodeToName(node: Node): string {
63 | let name = node.tag;
64 |
65 | if (node.attributes) {
66 | const id = node.attributes['id'];
67 | const classes = node.attributes['class'];
68 |
69 | if (id) {
70 | name += `#${id.replace(/[\"\']/g, '')}`;
71 | }
72 |
73 | if (classes) {
74 | name += classes.replace(/[\"\']/g, '').split(/\s+/).map(className => `.${className}`).join('');
75 | }
76 | }
77 |
78 | return name || '?';
79 | }
--------------------------------------------------------------------------------
/src/services/pathCompletion.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation. All rights reserved.
3 | * Licensed under the MIT License. See License.txt in the project root for license information.
4 | *--------------------------------------------------------------------------------------------*/
5 |
6 | import { ICompletionParticipant, TextDocument, CompletionItemKind, CompletionItem, TextEdit, Range, Position, DocumentUri, FileType, HtmlAttributeValueContext, DocumentContext, CompletionList } from '../htmlLanguageTypes';
7 | import { HTMLDataManager } from '../languageFacts/dataManager';
8 | import { startsWith } from '../utils/strings';
9 |
10 | export class PathCompletionParticipant implements ICompletionParticipant {
11 | private atributeCompletions: HtmlAttributeValueContext[] = [];
12 |
13 | constructor(private dataManager: HTMLDataManager, private readonly readDirectory: (uri: DocumentUri) => Promise<[string, FileType][]>) {
14 | }
15 |
16 | public onHtmlAttributeValue(context: HtmlAttributeValueContext) {
17 | if (this.dataManager.isPathAttribute(context.tag, context.attribute)) {
18 | this.atributeCompletions.push(context);
19 | }
20 | }
21 |
22 | public async computeCompletions(document: TextDocument, documentContext: DocumentContext): Promise {
23 | const result: CompletionList = { items: [], isIncomplete: false };
24 | for (const attributeCompletion of this.atributeCompletions) {
25 | const fullValue = stripQuotes(document.getText(attributeCompletion.range));
26 | if (isCompletablePath(fullValue)) {
27 | if (fullValue === '.' || fullValue === '..') {
28 | result.isIncomplete = true;
29 | } else {
30 | const replaceRange = pathToReplaceRange(attributeCompletion.value, fullValue, attributeCompletion.range);
31 | const suggestions = await this.providePathSuggestions(attributeCompletion.value, replaceRange, document, documentContext);
32 | for (const item of suggestions) {
33 | result.items.push(item);
34 | }
35 | }
36 | }
37 | }
38 | return result;
39 | }
40 |
41 | private async providePathSuggestions(valueBeforeCursor: string, replaceRange: Range, document: TextDocument, documentContext: DocumentContext) {
42 | const valueBeforeLastSlash = valueBeforeCursor.substring(0, valueBeforeCursor.lastIndexOf('/') + 1); // keep the last slash
43 |
44 | let parentDir = documentContext.resolveReference(valueBeforeLastSlash || '.', document.uri);
45 | if (parentDir) {
46 | try {
47 | const result: CompletionItem[] = [];
48 | const infos = await this.readDirectory(parentDir);
49 | for (const [name, type] of infos) {
50 | // Exclude paths that start with `.`
51 | if (name.charCodeAt(0) !== CharCode_dot) {
52 | result.push(createCompletionItem(name, type === FileType.Directory, replaceRange));
53 | }
54 | }
55 | return result;
56 | } catch (e) {
57 | // ignore
58 | }
59 | }
60 | return [];
61 | }
62 |
63 | }
64 |
65 | const CharCode_dot = '.'.charCodeAt(0);
66 |
67 | function stripQuotes(fullValue: string) {
68 | if (startsWith(fullValue, `'`) || startsWith(fullValue, `"`)) {
69 | return fullValue.slice(1, -1);
70 | } else {
71 | return fullValue;
72 | }
73 | }
74 |
75 | function isCompletablePath(value: string) {
76 | if (startsWith(value, 'http') || startsWith(value, 'https') || startsWith(value, '//')) {
77 | return false;
78 | }
79 | return true;
80 | }
81 |
82 | function pathToReplaceRange(valueBeforeCursor: string, fullValue: string, range: Range) {
83 | let replaceRange: Range;
84 | const lastIndexOfSlash = valueBeforeCursor.lastIndexOf('/');
85 | if (lastIndexOfSlash === -1) {
86 | replaceRange = shiftRange(range, 1, -1);
87 | } else {
88 | // For cases where cursor is in the middle of attribute value, like '
309 | ].join('\n');
310 |
311 | var expected = [
312 | ''
313 | ].join('\n');
314 |
315 | format(content, expected);
316 | });
317 |
318 | test('beautify-web/js-beautify#1491', () => {
319 | var content = [
320 | '',
321 | ' ',
322 | ' ',
323 | '',
324 | ].join('\n');
325 |
326 | var expected = [
327 | '',
328 | ' ',
329 | ' ',
330 | '',
331 | ].join('\n');
332 |
333 | format(content, expected);
334 | });
335 |
336 | test('bug 58693', () => {
337 | var content = [
338 | ''
339 | ].join('\n');
340 |
341 | var expected = [
342 | ''
343 | ].join('\n');
344 |
345 | format(content, expected);
346 | });
347 |
348 | });
349 |
--------------------------------------------------------------------------------
/src/test/highlighting.test.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation. All rights reserved.
3 | * Licensed under the MIT License. See License.txt in the project root for license information.
4 | *--------------------------------------------------------------------------------------------*/
5 |
6 | import * as assert from 'assert';
7 | import * as htmlLanguageService from '../htmlLanguageService';
8 | import { TextDocument } from '../htmlLanguageService';
9 |
10 |
11 |
12 | suite('HTML Highlighting', () => {
13 |
14 |
15 | function assertHighlights(value: string, expectedMatches: number[], elementName: string | null): void {
16 | const offset = value.indexOf('|');
17 | value = value.substr(0, offset) + value.substr(offset + 1);
18 |
19 | const document = TextDocument.create('test://test/test.html', 'html', 0, value);
20 |
21 | const position = document.positionAt(offset);
22 | const ls = htmlLanguageService.getLanguageService();
23 | const htmlDoc = ls.parseHTMLDocument(document);
24 |
25 | const highlights = ls.findDocumentHighlights(document, position, htmlDoc);
26 | assert.equal(highlights.length, expectedMatches.length);
27 | for (let i = 0; i < highlights.length; i++) {
28 | const actualStartOffset = document.offsetAt(highlights[i].range.start);
29 | assert.equal(actualStartOffset, expectedMatches[i]);
30 | const actualEndOffset = document.offsetAt(highlights[i].range.end);
31 | assert.equal(actualEndOffset, expectedMatches[i] + elementName!.length);
32 |
33 | assert.equal(document.getText().substring(actualStartOffset, actualEndOffset).toLowerCase(), elementName);
34 | }
35 | }
36 |
37 | test('Single', function (): any {
38 | assertHighlights('|', [], null);
39 | assertHighlights('<|html>', [1, 8], 'html');
40 | assertHighlights('', [1, 8], 'html');
41 | assertHighlights('