123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415 |
- /*---------------------------------------------------------------------------------------------
- * Copyright (c) Microsoft Corporation. All rights reserved.
- * Licensed under the MIT License. See License.txt in the project root for license information.
- *--------------------------------------------------------------------------------------------*/
- import { TextDocument, Position, Range } from 'vscode-languageclient';
- import { LanguageService, TokenType } from 'vscode-html-languageservice';
- export interface LanguageRange extends Range {
- languageId: string | undefined;
- attributeValue?: boolean;
- }
- export interface HTMLDocumentRegions {
- getEmbeddedDocument(languageId: string, ignoreAttributeValues?: boolean): TextDocument;
- getLanguageRanges(range: Range): LanguageRange[];
- getLanguageAtPosition(position: Position): string | undefined;
- getLanguagesInDocument(): string[];
- getImportedScripts(): string[];
- }
- export const CSS_STYLE_RULE = '__';
- interface EmbeddedRegion {
- languageId: string | undefined;
- start: number;
- end: number;
- attributeValue?: boolean;
- }
- export function isInsideStyleRegion(
- languageService: LanguageService,
- documentText: string,
- offset: number
- ) {
- let scanner = languageService.createScanner(documentText);
- let token = scanner.scan();
- while (token !== TokenType.EOS) {
- switch (token) {
- case TokenType.Styles:
- if (offset >= scanner.getTokenOffset() && offset <= scanner.getTokenEnd()) {
- return true;
- }
- }
- token = scanner.scan();
- }
-
- return false;
- }
- export function getCSSVirtualContent(
- languageService: LanguageService,
- documentText: string
- ): string {
- let regions: EmbeddedRegion[] = [];
- let scanner = languageService.createScanner(documentText);
- let lastTagName: string = '';
- let lastAttributeName: string | null = null;
- let languageIdFromType: string | undefined = undefined;
- let importedScripts: string[] = [];
- let token = scanner.scan();
- while (token !== TokenType.EOS) {
- switch (token) {
- case TokenType.StartTag:
- lastTagName = scanner.getTokenText();
- lastAttributeName = null;
- languageIdFromType = 'javascript';
- break;
- case TokenType.Styles:
- regions.push({
- languageId: 'css',
- start: scanner.getTokenOffset(),
- end: scanner.getTokenEnd()
- });
- break;
- case TokenType.Script:
- regions.push({
- languageId: languageIdFromType,
- start: scanner.getTokenOffset(),
- end: scanner.getTokenEnd()
- });
- break;
- case TokenType.AttributeName:
- lastAttributeName = scanner.getTokenText();
- break;
- case TokenType.AttributeValue:
- if (lastAttributeName === 'src' && lastTagName.toLowerCase() === 'script') {
- let value = scanner.getTokenText();
- if (value[0] === "'" || value[0] === '"') {
- value = value.substr(1, value.length - 1);
- }
- importedScripts.push(value);
- } else if (lastAttributeName === 'type' && lastTagName.toLowerCase() === 'script') {
- if (
- /["'](module|(text|application)\/(java|ecma)script|text\/babel)["']/.test(
- scanner.getTokenText()
- )
- ) {
- languageIdFromType = 'javascript';
- } else if (/["']text\/typescript["']/.test(scanner.getTokenText())) {
- languageIdFromType = 'typescript';
- } else {
- languageIdFromType = undefined;
- }
- } else {
- let attributeLanguageId = getAttributeLanguage(lastAttributeName!);
- if (attributeLanguageId) {
- let start = scanner.getTokenOffset();
- let end = scanner.getTokenEnd();
- let firstChar = documentText[start];
- if (firstChar === "'" || firstChar === '"') {
- start++;
- end--;
- }
- regions.push({
- languageId: attributeLanguageId,
- start,
- end,
- attributeValue: true
- });
- }
- }
- lastAttributeName = null;
- break;
- }
- token = scanner.scan();
- }
- let content = documentText
- .split('\n')
- .map(line => {
- return ' '.repeat(line.length);
- }).join('\n');
- regions.forEach(r => {
- if (r.languageId === 'css') {
- content = content.slice(0, r.start) + documentText.slice(r.start, r.end) + content.slice(r.end);
- }
- });
- return content;
- }
- export function getDocumentRegions(
- languageService: LanguageService,
- document: TextDocument
- ): HTMLDocumentRegions {
- let regions: EmbeddedRegion[] = [];
- let scanner = languageService.createScanner(document.getText());
- let lastTagName: string = '';
- let lastAttributeName: string | null = null;
- let languageIdFromType: string | undefined = undefined;
- let importedScripts: string[] = [];
- let token = scanner.scan();
- while (token !== TokenType.EOS) {
- switch (token) {
- case TokenType.StartTag:
- lastTagName = scanner.getTokenText();
- lastAttributeName = null;
- languageIdFromType = 'javascript';
- break;
- case TokenType.Styles:
- regions.push({
- languageId: 'css',
- start: scanner.getTokenOffset(),
- end: scanner.getTokenEnd()
- });
- break;
- case TokenType.Script:
- regions.push({
- languageId: languageIdFromType,
- start: scanner.getTokenOffset(),
- end: scanner.getTokenEnd()
- });
- break;
- case TokenType.AttributeName:
- lastAttributeName = scanner.getTokenText();
- break;
- case TokenType.AttributeValue:
- if (lastAttributeName === 'src' && lastTagName.toLowerCase() === 'script') {
- let value = scanner.getTokenText();
- if (value[0] === "'" || value[0] === '"') {
- value = value.substr(1, value.length - 1);
- }
- importedScripts.push(value);
- } else if (lastAttributeName === 'type' && lastTagName.toLowerCase() === 'script') {
- if (
- /["'](module|(text|application)\/(java|ecma)script|text\/babel)["']/.test(
- scanner.getTokenText()
- )
- ) {
- languageIdFromType = 'javascript';
- } else if (/["']text\/typescript["']/.test(scanner.getTokenText())) {
- languageIdFromType = 'typescript';
- } else {
- languageIdFromType = undefined;
- }
- } else {
- let attributeLanguageId = getAttributeLanguage(lastAttributeName!);
- if (attributeLanguageId) {
- let start = scanner.getTokenOffset();
- let end = scanner.getTokenEnd();
- let firstChar = document.getText()[start];
- if (firstChar === "'" || firstChar === '"') {
- start++;
- end--;
- }
- regions.push({
- languageId: attributeLanguageId,
- start,
- end,
- attributeValue: true
- });
- }
- }
- lastAttributeName = null;
- break;
- }
- token = scanner.scan();
- }
- return {
- getLanguageRanges: (range: Range) => getLanguageRanges(document, regions, range),
- getEmbeddedDocument: (languageId: string, ignoreAttributeValues: boolean) =>
- getEmbeddedDocument(document, regions, languageId, ignoreAttributeValues),
- getLanguageAtPosition: (position: Position) =>
- getLanguageAtPosition(document, regions, position),
- getLanguagesInDocument: () => getLanguagesInDocument(document, regions),
- getImportedScripts: () => importedScripts
- };
- }
- function getLanguageRanges(
- document: TextDocument,
- regions: EmbeddedRegion[],
- range: Range
- ): LanguageRange[] {
- let result: LanguageRange[] = [];
- let currentPos = range ? range.start : Position.create(0, 0);
- let currentOffset = range ? document.offsetAt(range.start) : 0;
- let endOffset = range ? document.offsetAt(range.end) : document.getText().length;
- for (let region of regions) {
- if (region.end > currentOffset && region.start < endOffset) {
- let start = Math.max(region.start, currentOffset);
- let startPos = document.positionAt(start);
- if (currentOffset < region.start) {
- result.push({
- start: currentPos,
- end: startPos,
- languageId: 'html'
- });
- }
- let end = Math.min(region.end, endOffset);
- let endPos = document.positionAt(end);
- if (end > region.start) {
- result.push({
- start: startPos,
- end: endPos,
- languageId: region.languageId,
- attributeValue: region.attributeValue
- });
- }
- currentOffset = end;
- currentPos = endPos;
- }
- }
- if (currentOffset < endOffset) {
- let endPos = range ? range.end : document.positionAt(endOffset);
- result.push({
- start: currentPos,
- end: endPos,
- languageId: 'html'
- });
- }
- return result;
- }
- function getLanguagesInDocument(
- _document: TextDocument,
- regions: EmbeddedRegion[]
- ): string[] {
- let result = [];
- for (let region of regions) {
- if (region.languageId && result.indexOf(region.languageId) === -1) {
- result.push(region.languageId);
- if (result.length === 3) {
- return result;
- }
- }
- }
- result.push('html');
- return result;
- }
- function getLanguageAtPosition(
- document: TextDocument,
- regions: EmbeddedRegion[],
- position: Position
- ): string | undefined {
- let offset = document.offsetAt(position);
- for (let region of regions) {
- if (region.start <= offset) {
- if (offset <= region.end) {
- return region.languageId;
- }
- } else {
- break;
- }
- }
- return 'html';
- }
- function getEmbeddedDocument(
- document: TextDocument,
- contents: EmbeddedRegion[],
- languageId: string,
- ignoreAttributeValues: boolean
- ): TextDocument {
- let currentPos = 0;
- let oldContent = document.getText();
- let result = '';
- let lastSuffix = '';
- for (let c of contents) {
- if (c.languageId === languageId && (!ignoreAttributeValues || !c.attributeValue)) {
- result = substituteWithWhitespace(
- result,
- currentPos,
- c.start,
- oldContent,
- lastSuffix,
- getPrefix(c)
- );
- result += oldContent.substring(c.start, c.end);
- currentPos = c.end;
- lastSuffix = getSuffix(c);
- }
- }
- result = substituteWithWhitespace(
- result,
- currentPos,
- oldContent.length,
- oldContent,
- lastSuffix,
- ''
- );
- return TextDocument.create(document.uri, languageId, document.version, result);
- }
- function getPrefix(c: EmbeddedRegion) {
- if (c.attributeValue) {
- switch (c.languageId) {
- case 'css':
- return CSS_STYLE_RULE + '{';
- }
- }
- return '';
- }
- function getSuffix(c: EmbeddedRegion) {
- if (c.attributeValue) {
- switch (c.languageId) {
- case 'css':
- return '}';
- case 'javascript':
- return ';';
- }
- }
- return '';
- }
- function substituteWithWhitespace(
- result: string,
- start: number,
- end: number,
- oldContent: string,
- before: string,
- after: string
- ) {
- let accumulatedWS = 0;
- result += before;
- for (let i = start + before.length; i < end; i++) {
- let ch = oldContent[i];
- if (ch === '\n' || ch === '\r') {
- // only write new lines, skip the whitespace
- accumulatedWS = 0;
- result += ch;
- } else {
- accumulatedWS++;
- }
- }
- result = append(result, ' ', accumulatedWS - after.length);
- result += after;
- return result;
- }
- function append(result: string, str: string, n: number): string {
- while (n > 0) {
- if (n & 1) {
- result += str;
- }
- n >>= 1;
- str += str;
- }
- return result;
- }
- function getAttributeLanguage(attributeName: string): string | null {
- let match = attributeName.match(/^(style)$|^(on\w+)$/i);
- if (!match) {
- return null;
- }
- return match[1] ? 'css' : 'javascript';
- }
|