├── .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 | [![npm Package](https://img.shields.io/npm/v/vscode-html-languageservice.svg?style=flat-square)](https://www.npmjs.org/package/vscode-html-languageservice) 5 | [![NPM Downloads](https://img.shields.io/npm/dm/vscode-html-languageservice.svg)](https://npmjs.org/package/vscode-html-languageservice) 6 | [![Build Status](https://github.com/microsoft/vscode-html-languageservice/actions/workflows/node.js.yml/badge.svg)](https://github.com/microsoft/vscode-html-languageservice/actions) 7 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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 | ![image](https://user-images.githubusercontent.com/6461412/94239202-bdad4e80-ff11-11ea-99c3-cb9dbeb1c0b2.png) 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 | ![image](https://user-images.githubusercontent.com/6461412/94239296-dfa6d100-ff11-11ea-8e30-6444cf5defb8.png) 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 + ']+(?:\([\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 + ' 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('', [1, 8], 'html'); 42 | assertHighlights('', [1, 8], 'html'); 43 | assertHighlights('|', [], null); 44 | assertHighlights('<|/html>', [], null); 45 | assertHighlights('', [1, 8], 'html'); 46 | assertHighlights('', [1, 8], 'html'); 47 | assertHighlights('', [1, 8], 'html'); 48 | assertHighlights('', [1, 8], 'html'); 49 | assertHighlights('', [1, 8], 'html'); 50 | assertHighlights('|', [], null); 51 | }); 52 | 53 | test('Nested', function (): any { 54 | assertHighlights('|
', [], null); 55 | assertHighlights('<|div>', [7, 13], 'div'); 56 | assertHighlights('
|
', [], null); 57 | assertHighlights('
', [7, 13], 'div'); 58 | assertHighlights('
', [7, 24], 'div'); 59 | assertHighlights('
', [12, 18], 'div'); 60 | assertHighlights('
', [12, 18], 'div'); 61 | assertHighlights('
', [1, 30], 'html'); 62 | assertHighlights('
', [7, 13], 'div'); 63 | assertHighlights('
', [18, 24], 'div'); 64 | }); 65 | 66 | test('Selfclosed', function (): any { 67 | assertHighlights('<|div/>', [7], 'div'); 68 | assertHighlights('<|br>', [7], 'br'); 69 | assertHighlights('
', [12], 'div'); 70 | }); 71 | 72 | test('Case insensivity', function (): any { 73 | assertHighlights('
', [7, 24], 'div'); 74 | assertHighlights('
', [7, 24], 'div'); 75 | }); 76 | 77 | test('Incomplete', function (): any { 78 | assertHighlights('

', [1, 29], 'div'); 79 | }); 80 | }); -------------------------------------------------------------------------------- /src/test/hover.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 { assertHover, assertHover2 } from './hoverUtil'; 7 | import { MarkupContent } from '../htmlLanguageTypes'; 8 | import { BaselineImages } from '../languageFacts/dataProvider'; 9 | 10 | suite('HTML Hover', () => { 11 | test('Single', function (): any { 12 | const descriptionAndReference = 13 | 'The html element represents the root of an HTML document.' + 14 | '\n\n' + 15 | `![Baseline icon](${BaselineImages.BASELINE_HIGH}) _Widely available across major browsers (Baseline since 2015)_` + 16 | '\n\n' + 17 | '[MDN Reference](https://developer.mozilla.org/docs/Web/HTML/Reference/Elements/html)'; 18 | 19 | const htmlContent: MarkupContent = { 20 | kind: 'markdown', 21 | value: descriptionAndReference 22 | }; 23 | const closeHtmlContent: MarkupContent = { 24 | kind: 'markdown', 25 | value: descriptionAndReference 26 | }; 27 | 28 | const entityDescription = `Character entity representing '\u00A0', unicode equivalent 'U+00A0'`; 29 | 30 | 31 | assertHover('|', void 0, void 0); 32 | assertHover('<|html>', htmlContent, 1); 33 | assertHover('', htmlContent, 1); 34 | assertHover('', htmlContent, 1); 35 | assertHover('', htmlContent, 1); 36 | assertHover('|', void 0, void 0); 37 | assertHover('<|/html>', void 0, void 0); 38 | assertHover('', closeHtmlContent, 8); 39 | assertHover('', closeHtmlContent, 8); 40 | assertHover('', closeHtmlContent, 8); 41 | assertHover('', closeHtmlContent, 8); 42 | assertHover('', closeHtmlContent, 8); 43 | assertHover('|', void 0, void 0); 44 | 45 | assertHover2('| ', '', ''); 46 | assertHover2('&|nbsp;', entityDescription, 'nbsp;'); 47 | assertHover2('&n|bsp;', entityDescription, 'nbsp;'); 48 | assertHover2('&nb|sp;', entityDescription, 'nbsp;'); 49 | assertHover2('&nbs|p;', entityDescription, 'nbsp;'); 50 | assertHover2(' |;', entityDescription, 'nbsp;'); 51 | assertHover2(' |', '', ''); 52 | 53 | const noDescription: MarkupContent = { 54 | kind: 'markdown', 55 | value: '[MDN Reference](https://developer.mozilla.org/docs/Web/HTML/Reference/Elements/html)' 56 | }; 57 | assertHover2('', noDescription, 'html', undefined, { documentation: false }); 58 | 59 | const noReferences: MarkupContent = { 60 | kind: 'markdown', 61 | value: 'The html element represents the root of an HTML document.' + 62 | `\n\n![Baseline icon](${BaselineImages.BASELINE_HIGH}) _Widely available across major browsers (Baseline since 2015)_` 63 | }; 64 | assertHover2('', noReferences, 'html', undefined, { references: false }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /src/test/hoverUtil.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 { HoverSettings, TextDocument, MarkupContent } from '../htmlLanguageService'; 9 | 10 | export function assertHover(value: string, expectedHoverContent: MarkupContent | undefined, expectedHoverOffset: number | undefined): void { 11 | const offset = value.indexOf('|'); 12 | value = value.substr(0, offset) + value.substr(offset + 1); 13 | 14 | const document = TextDocument.create('test://test/test.html', 'html', 0, value); 15 | 16 | const position = document.positionAt(offset); 17 | const ls = htmlLanguageService.getLanguageService(); 18 | const htmlDoc = ls.parseHTMLDocument(document); 19 | 20 | const hover = ls.doHover(document, position, htmlDoc); 21 | assert.deepEqual(hover && hover.contents, expectedHoverContent); 22 | assert.equal(hover && document.offsetAt(hover.range!.start), expectedHoverOffset); 23 | } 24 | 25 | export function assertHover2(value: string, contents: string | MarkupContent, rangeText: string, lsOptions?: htmlLanguageService.LanguageServiceOptions, hoverSettings?: HoverSettings): void { 26 | const offset = value.indexOf('|'); 27 | value = value.substr(0, offset) + value.substr(offset + 1); 28 | 29 | const document = TextDocument.create('test://test/test.html', 'html', 0, value); 30 | 31 | const position = document.positionAt(offset); 32 | const ls = htmlLanguageService.getLanguageService(lsOptions); 33 | const htmlDoc = ls.parseHTMLDocument(document); 34 | 35 | const hover = ls.doHover(document, position, htmlDoc, hoverSettings); 36 | if (hover) { 37 | if (typeof contents === 'string') { 38 | assert.equal(hover.contents, contents); 39 | } else { 40 | assert.equal((hover.contents as MarkupContent).kind, contents.kind); 41 | assert.equal((hover.contents as MarkupContent).value, contents.value); 42 | } 43 | 44 | if (hover.range) { 45 | assert.equal(rangeText, document.getText(hover.range)); 46 | } 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /src/test/linkedEditing.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 | interface OffsetWithText { 11 | 0: number; 12 | 1: string; 13 | } 14 | 15 | export function testMatchingTagPosition(value: string, expected: OffsetWithText[]): void { 16 | const originalValue = value; 17 | 18 | let offset = value.indexOf('|'); 19 | value = value.substr(0, offset) + value.substr(offset + 1); 20 | 21 | const ls = htmlLanguageService.getLanguageService(); 22 | 23 | const document = TextDocument.create('test://test/test.html', 'html', 0, value); 24 | const position = document.positionAt(offset); 25 | const htmlDoc = ls.parseHTMLDocument(document); 26 | 27 | const syncedRegions = ls.findLinkedEditingRanges(document, position, htmlDoc); 28 | if (!syncedRegions) { 29 | if (expected.length > 0) { 30 | assert.fail(`No linked editing ranges for ${originalValue} but expecting\n${JSON.stringify(expected)}`); 31 | } else { 32 | return; 33 | } 34 | } 35 | 36 | const actual: OffsetWithText[] = syncedRegions.map(r => { 37 | return [document.offsetAt(r.start), document.getText(r)]; 38 | }); 39 | 40 | assert.deepStrictEqual(actual, expected, `Actual\n${JSON.stringify(actual)}\ndoes not match expected:\n${JSON.stringify(expected)}`); 41 | } 42 | 43 | suite('HTML Linked Editing', () => { 44 | test('Linked Editing', () => { 45 | testMatchingTagPosition('|
', []); 46 | testMatchingTagPosition('<|div>
', [[1, 'div'], [7, 'div']]); 47 | testMatchingTagPosition('
', [[1, 'div'], [7, 'div']]); 48 | testMatchingTagPosition('
', [[1, 'div'], [7, 'div']]); 49 | testMatchingTagPosition('', [[1, 'div'], [7, 'div']]); 50 | 51 | testMatchingTagPosition('
|
', []); 52 | testMatchingTagPosition('
<|/div>', []); 53 | 54 | testMatchingTagPosition('
', [[1, 'div'], [7, 'div']]); 55 | testMatchingTagPosition('
', [[1, 'div'], [7, 'div']]); 56 | testMatchingTagPosition('
', [[1, 'div'], [7, 'div']]); 57 | testMatchingTagPosition('
', [[1, 'div'], [7, 'div']]); 58 | 59 | testMatchingTagPosition('
|', []); 60 | testMatchingTagPosition('
', []); 61 | testMatchingTagPosition('
', []); 62 | 63 | testMatchingTagPosition('
', [[1, 'div'], [8, 'div']]); 64 | testMatchingTagPosition('
', [[1, 'div'], [16, 'div']]); 65 | 66 | testMatchingTagPosition('<|>', [[1, ''], [4, '']]); 67 | testMatchingTagPosition('<>
', [[1, ''], [15, '']]); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /src/test/links.test.ts: -------------------------------------------------------------------------------- 1 | 2 | /*--------------------------------------------------------------------------------------------- 3 | * Copyright (c) Microsoft Corporation. All rights reserved. 4 | * Licensed under the MIT License. See License.txt in the project root for license information. 5 | *--------------------------------------------------------------------------------------------*/ 6 | 7 | import * as assert from 'assert'; 8 | import { TextDocument } from '../htmlLanguageTypes'; 9 | import * as htmlLanguageService from '../htmlLanguageService'; 10 | import { getDocumentContext } from './testUtil/documentContext'; 11 | 12 | suite('HTML Link Detection', () => { 13 | 14 | const ext2lang: { [ext: string]: string } = { 15 | html: 'html', 16 | hbs: 'handlebars' 17 | }; 18 | 19 | function testLinkCreation(modelUrl: string, tokenContent: string, expected: string | null): void { 20 | const langId = ext2lang[modelUrl.substr(modelUrl.lastIndexOf('.') + 1)] || 'html'; 21 | const document = TextDocument.create(modelUrl, langId, 0, ``); 22 | const ls = htmlLanguageService.getLanguageService(); 23 | const links = ls.findDocumentLinks(document, getDocumentContext()); 24 | assert.equal(links[0] && links[0].target, expected); 25 | } 26 | 27 | function testLinkDetection(value: string, expectedLinks: { offset: number, length: number, target: string; }[]): void { 28 | const document = TextDocument.create('file:///test/data/abc/test.html', 'html', 0, value); 29 | const ls = htmlLanguageService.getLanguageService(); 30 | const links = ls.findDocumentLinks(document, getDocumentContext()); 31 | assert.deepEqual(links.map(l => ({ offset: l.range.start.character, length: l.range.end.character - l.range.start.character, target: l.target })), expectedLinks); 32 | } 33 | 34 | test('Link creation', () => { 35 | testLinkCreation('http://model/1.html', 'javascript:void;', null); 36 | testLinkCreation('http://model/1.html', ' \tjavascript:alert(7);', null); 37 | testLinkCreation('http://model/1.html', ' #relative', 'http://model/1.html'); 38 | testLinkCreation('http://model/1.html', 'file:///C:\\Alex\\src\\path\\to\\file.txt', 'file:///C:\\Alex\\src\\path\\to\\file.txt'); 39 | testLinkCreation('http://model/1.html', 'http://www.microsoft.com/', 'http://www.microsoft.com/'); 40 | testLinkCreation('http://model/1.html', 'https://www.microsoft.com/', 'https://www.microsoft.com/'); 41 | testLinkCreation('http://model/1.html', '//www.microsoft.com/', 'http://www.microsoft.com/'); 42 | testLinkCreation('http://model/x/1.html', 'a.js', 'http://model/x/a.js'); 43 | testLinkCreation('http://model/x/1.html', './a2.js', 'http://model/x/a2.js'); 44 | testLinkCreation('http://model/x/1.html', '/b.js', 'http://model/b.js'); 45 | testLinkCreation('http://model/x/y/1.html', '../../c.js', 'http://model/c.js'); 46 | 47 | testLinkCreation('file:///C:/Alex/src/path/to/file.html', 'javascript:void;', null); 48 | testLinkCreation('file:///C:/Alex/src/path/to/file.html', ' \tjavascript:alert(7);', null); 49 | testLinkCreation('file:///C:/Alex/src/path/to/file.html', ' #relative', 'file:///C:/Alex/src/path/to/file.html'); 50 | testLinkCreation('file:///C:/Alex/src/path/to/file.html', 'file:///C:\\Alex\\src\\path\\to\\file.txt', 'file:///C:\\Alex\\src\\path\\to\\file.txt'); 51 | testLinkCreation('file:///C:/Alex/src/path/to/file.html', 'http://www.microsoft.com/', 'http://www.microsoft.com/'); 52 | testLinkCreation('file:///C:/Alex/src/path/to/file.html', 'https://www.microsoft.com/', 'https://www.microsoft.com/'); 53 | testLinkCreation('file:///C:/Alex/src/path/to/file.html', ' //www.microsoft.com/', 'http://www.microsoft.com/'); 54 | testLinkCreation('file:///C:/Alex/src/path/to/file.html', 'a.js', 'file:///C:/Alex/src/path/to/a.js'); 55 | testLinkCreation('file:///C:/Alex/src/path/to/file.html', '/a.js', 'file:///a.js'); 56 | 57 | testLinkCreation('https://www.test.com/path/to/file.html', 'file:///C:\\Alex\\src\\path\\to\\file.txt', 'file:///C:\\Alex\\src\\path\\to\\file.txt'); 58 | testLinkCreation('https://www.test.com/path/to/file.html', '//www.microsoft.com/', 'https://www.microsoft.com/'); 59 | testLinkCreation('https://www.test.com/path/to/file.html', '//www.microsoft.com/', 'https://www.microsoft.com/'); 60 | 61 | // invalid uris are ignored 62 | testLinkCreation('https://www.test.com/path/to/file.html', '%', null); 63 | 64 | // Bug #18314: Ctrl + Click does not open existing file if folder's name starts with 'c' character 65 | testLinkCreation('file:///c:/Alex/working_dir/18314-link-detection/test.html', '/class/class.js', 'file:///class/class.js'); 66 | 67 | testLinkCreation('http://foo/bar.hbs', '/class/class.js', 'http://foo/class/class.js'); 68 | testLinkCreation('http://foo/bar.hbs', '{{asset foo}}/class/class.js', null); 69 | testLinkCreation('http://foo/bar.hbs', '{{href-to', null); // issue https://github.com/microsoft/vscode/issues/134334 70 | }); 71 | 72 | test('Link detection', () => { 73 | testLinkDetection('', [{ offset: 10, length: 7, target: 'file:///test/data/abc/foo.png' }]); 74 | testLinkDetection('', [{ offset: 9, length: 22, target: 'http://server/foo.html' }]); 75 | testLinkDetection('', []); 76 | testLinkDetection('', [{ offset: 12, length: 6, target: 'file:///test/data/abc/a.html' }]); 77 | testLinkDetection('', [{ offset: 35, length: 7, target: 'file:///test/data/abc/docs/foo.png' }]); 81 | testLinkDetection('', [{ offset: 62, length: 7, target: 'http://www.example.com/foo.png' }]); 82 | testLinkDetection('', [{ offset: 32, length: 7, target: 'file:///test/data/foo.png' }]); 83 | testLinkDetection('', [{ offset: 31, length: 7, target: 'file:///test/data/abc/foo.png' }]); 84 | testLinkDetection('', [{ offset: 36, length: 7, target: 'file:///docs/foo.png' }]); 85 | 86 | testLinkDetection(' <% - mail %>@<% - domain %> ', []); 87 | 88 | testLinkDetection('', []); 89 | testLinkDetection('
', [{ offset: 18, length: 7, target: 'file:///test/data/abc/foo.png' }]); 90 | testLinkDetection('