mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-24 10:38:59 +01:00
[html] embedded css/javascript in attribute values
This commit is contained in:
@@ -17,6 +17,9 @@ export function getCSSMode(htmlLanguageService: HTMLLanguageService, htmlDocumen
|
||||
let getEmbeddedCSSDocument = (document: TextDocument) => getEmbeddedDocument(htmlLanguageService, document, htmlDocuments.get(document), 'css');
|
||||
|
||||
return {
|
||||
getId() {
|
||||
return 'css';
|
||||
},
|
||||
configure(options: any) {
|
||||
cssLanguageService.configure(options && options.css);
|
||||
},
|
||||
|
||||
@@ -5,36 +5,54 @@
|
||||
'use strict';
|
||||
|
||||
|
||||
import { TextDocument, Position, HTMLDocument, Node, LanguageService, TokenType, Range } from 'vscode-html-languageservice';
|
||||
import { TextDocument, Position, HTMLDocument, Node, LanguageService, TokenType, Range, Scanner } from 'vscode-html-languageservice';
|
||||
|
||||
export interface LanguageRange extends Range {
|
||||
languageId: string;
|
||||
}
|
||||
|
||||
interface EmbeddedContent { languageId: string; start: number; end: number; attributeValue?: boolean; };
|
||||
|
||||
export function getLanguageAtPosition(languageService: LanguageService, document: TextDocument, htmlDocument: HTMLDocument, position: Position): string {
|
||||
let offset = document.offsetAt(position);
|
||||
let node = htmlDocument.findNodeAt(offset);
|
||||
if (node && node.children.length === 0) {
|
||||
if (node) {
|
||||
let embeddedContent = getEmbeddedContentForNode(languageService, document, node);
|
||||
if (embeddedContent && embeddedContent.start <= offset && offset <= embeddedContent.end) {
|
||||
return embeddedContent.languageId;
|
||||
if (embeddedContent) {
|
||||
for (let c of embeddedContent) {
|
||||
if (c.start <= offset && offset <= c.end) {
|
||||
return c.languageId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 'html';
|
||||
}
|
||||
|
||||
export function getLanguagesInContent(languageService: LanguageService, document: TextDocument, htmlDocument: HTMLDocument): string[] {
|
||||
let embeddedLanguageIds: { [languageId: string]: boolean } = { html: true };
|
||||
let embeddedLanguageIds = ['html'];
|
||||
const maxEmbbeddedLanguages = 3;
|
||||
function collectEmbeddedLanguages(node: Node): void {
|
||||
let c = getEmbeddedContentForNode(languageService, document, node);
|
||||
if (c && !isWhitespace(document.getText().substring(c.start, c.end))) {
|
||||
embeddedLanguageIds[c.languageId] = true;
|
||||
if (embeddedLanguageIds.length < maxEmbbeddedLanguages) {
|
||||
let embeddedContent = getEmbeddedContentForNode(languageService, document, node);
|
||||
if (embeddedContent) {
|
||||
for (let c of embeddedContent) {
|
||||
if (!isWhitespace(document.getText(), c.start, c.end)) {
|
||||
if (embeddedLanguageIds.lastIndexOf(c.languageId) === -1) {
|
||||
embeddedLanguageIds.push(c.languageId);
|
||||
if (embeddedLanguageIds.length === maxEmbbeddedLanguages) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
node.children.forEach(collectEmbeddedLanguages);
|
||||
}
|
||||
node.children.forEach(collectEmbeddedLanguages);
|
||||
}
|
||||
|
||||
htmlDocument.roots.forEach(collectEmbeddedLanguages);
|
||||
return Object.keys(embeddedLanguageIds);
|
||||
return embeddedLanguageIds;
|
||||
}
|
||||
|
||||
export function getLanguagesInRange(languageService: LanguageService, document: TextDocument, htmlDocument: HTMLDocument, range: Range): LanguageRange[] {
|
||||
@@ -44,27 +62,31 @@ export function getLanguagesInRange(languageService: LanguageService, document:
|
||||
let rangeEndOffset = document.offsetAt(range.end);
|
||||
function collectEmbeddedNodes(node: Node): void {
|
||||
if (node.start < rangeEndOffset && node.end > currentOffset) {
|
||||
let c = getEmbeddedContentForNode(languageService, document, node);
|
||||
if (c && c.start < rangeEndOffset) {
|
||||
let startPos = document.positionAt(c.start);
|
||||
if (currentOffset < c.start) {
|
||||
ranges.push({
|
||||
start: currentPos,
|
||||
end: startPos,
|
||||
languageId: 'html'
|
||||
});
|
||||
let embeddedContent = getEmbeddedContentForNode(languageService, document, node);
|
||||
if (embeddedContent) {
|
||||
for (let c of embeddedContent) {
|
||||
if (c.start < rangeEndOffset) {
|
||||
let startPos = document.positionAt(c.start);
|
||||
if (currentOffset < c.start) {
|
||||
ranges.push({
|
||||
start: currentPos,
|
||||
end: startPos,
|
||||
languageId: 'html'
|
||||
});
|
||||
}
|
||||
let end = Math.min(c.end, rangeEndOffset);
|
||||
let endPos = document.positionAt(end);
|
||||
if (end > c.start) {
|
||||
ranges.push({
|
||||
start: startPos,
|
||||
end: endPos,
|
||||
languageId: c.languageId
|
||||
});
|
||||
}
|
||||
currentOffset = end;
|
||||
currentPos = endPos;
|
||||
}
|
||||
}
|
||||
let end = Math.min(c.end, rangeEndOffset);
|
||||
let endPos = document.positionAt(end);
|
||||
if (end > c.start) {
|
||||
ranges.push({
|
||||
start: startPos,
|
||||
end: endPos,
|
||||
languageId: c.languageId
|
||||
});
|
||||
}
|
||||
currentOffset = end;
|
||||
currentPos = endPos;
|
||||
}
|
||||
}
|
||||
node.children.forEach(collectEmbeddedNodes);
|
||||
@@ -82,11 +104,15 @@ export function getLanguagesInRange(languageService: LanguageService, document:
|
||||
}
|
||||
|
||||
export function getEmbeddedDocument(languageService: LanguageService, document: TextDocument, htmlDocument: HTMLDocument, languageId: string): TextDocument {
|
||||
let contents = [];
|
||||
let contents: EmbeddedContent[] = [];
|
||||
function collectEmbeddedNodes(node: Node): void {
|
||||
let c = getEmbeddedContentForNode(languageService, document, node);
|
||||
if (c && c.languageId === languageId) {
|
||||
contents.push(c);
|
||||
let embeddedContent = getEmbeddedContentForNode(languageService, document, node);
|
||||
if (embeddedContent) {
|
||||
for (let c of embeddedContent) {
|
||||
if (c.languageId === languageId) {
|
||||
contents.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
node.children.forEach(collectEmbeddedNodes);
|
||||
}
|
||||
@@ -96,18 +122,40 @@ export function getEmbeddedDocument(languageService: LanguageService, document:
|
||||
let currentPos = 0;
|
||||
let oldContent = document.getText();
|
||||
let result = '';
|
||||
let lastSuffix = '';
|
||||
for (let c of contents) {
|
||||
result = substituteWithWhitespace(result, currentPos, c.start, oldContent);
|
||||
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);
|
||||
result = substituteWithWhitespace(result, currentPos, oldContent.length, oldContent, lastSuffix, '');
|
||||
return TextDocument.create(document.uri, languageId, document.version, result);
|
||||
}
|
||||
|
||||
function substituteWithWhitespace(result, start, end, oldContent) {
|
||||
function getPrefix(c: EmbeddedContent) {
|
||||
if (c.attributeValue) {
|
||||
switch (c.languageId) {
|
||||
case 'css': return 'x{';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
function getSuffix(c: EmbeddedContent) {
|
||||
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;
|
||||
for (let i = start; i < end; i++) {
|
||||
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
|
||||
@@ -117,12 +165,13 @@ function substituteWithWhitespace(result, start, end, oldContent) {
|
||||
accumulatedWS++;
|
||||
}
|
||||
}
|
||||
result = append(result, ' ', accumulatedWS);
|
||||
result = append(result, ' ', accumulatedWS - after.length);
|
||||
result += after;
|
||||
return result;
|
||||
}
|
||||
|
||||
function append(result: string, str: string, n: number): string {
|
||||
while (n) {
|
||||
while (n > 0) {
|
||||
if (n & 1) {
|
||||
result += str;
|
||||
}
|
||||
@@ -132,13 +181,13 @@ function append(result: string, str: string, n: number): string {
|
||||
return result;
|
||||
}
|
||||
|
||||
function getEmbeddedContentForNode(languageService: LanguageService, document: TextDocument, node: Node): { languageId: string, start: number, end: number } {
|
||||
function getEmbeddedContentForNode(languageService: LanguageService, document: TextDocument, node: Node): EmbeddedContent[] {
|
||||
if (node.tag === 'style') {
|
||||
let scanner = languageService.createScanner(document.getText().substring(node.start, node.end));
|
||||
let token = scanner.scan();
|
||||
while (token !== TokenType.EOS) {
|
||||
if (token === TokenType.Styles) {
|
||||
return { languageId: 'css', start: node.start + scanner.getTokenOffset(), end: node.start + scanner.getTokenEnd() };
|
||||
return [{ languageId: 'css', start: node.start + scanner.getTokenOffset(), end: node.start + scanner.getTokenEnd() }];
|
||||
}
|
||||
token = scanner.scan();
|
||||
}
|
||||
@@ -160,14 +209,59 @@ function getEmbeddedContentForNode(languageService: LanguageService, document: T
|
||||
}
|
||||
isTypeAttribute = false;
|
||||
} else if (token === TokenType.Script) {
|
||||
return { languageId, start: node.start + scanner.getTokenOffset(), end: node.start + scanner.getTokenEnd() };
|
||||
return [{ languageId, start: node.start + scanner.getTokenOffset(), end: node.start + scanner.getTokenEnd() }];
|
||||
}
|
||||
token = scanner.scan();
|
||||
}
|
||||
} else if (node.attributeNames) {
|
||||
let scanner: Scanner;
|
||||
let result;
|
||||
for (let name of node.attributeNames) {
|
||||
let languageId = getAttributeLanguage(name);
|
||||
if (languageId) {
|
||||
if (!scanner) {
|
||||
scanner = languageService.createScanner(document.getText().substring(node.start, node.end));
|
||||
}
|
||||
let token = scanner.scan();
|
||||
let lastAttribute;
|
||||
while (token !== TokenType.EOS) {
|
||||
if (token === TokenType.AttributeName) {
|
||||
lastAttribute = scanner.getTokenText();
|
||||
} else if (token === TokenType.AttributeValue && lastAttribute === name) {
|
||||
let start = scanner.getTokenOffset() + node.start;
|
||||
let end = scanner.getTokenEnd() + node.start;
|
||||
let firstChar = document.getText()[start];
|
||||
if (firstChar === '\'' || firstChar === '"') {
|
||||
start++;
|
||||
end--;
|
||||
}
|
||||
if (!result) {
|
||||
result = [];
|
||||
}
|
||||
result.push({ languageId, start, end, attributeValue: true });
|
||||
lastAttribute = null;
|
||||
break;
|
||||
}
|
||||
token = scanner.scan();
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return void 0;
|
||||
}
|
||||
|
||||
function isWhitespace(str: string) {
|
||||
return str.match(/^\s*$/);
|
||||
function getAttributeLanguage(attributeName: string): string {
|
||||
let match = attributeName.match(/^(style)|(on\w+)$/i);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
return match[1] ? 'css' : 'javascript';
|
||||
}
|
||||
|
||||
function isWhitespace(str: string, start: number, end: number): boolean {
|
||||
if (start === end) {
|
||||
return true;
|
||||
}
|
||||
return !!str.substring(start, end).match(/^\s*$/);
|
||||
}
|
||||
@@ -13,6 +13,9 @@ export function getHTMLMode(htmlLanguageService: HTMLLanguageService, htmlDocume
|
||||
let settings: any = {};
|
||||
|
||||
return {
|
||||
getId() {
|
||||
return 'html';
|
||||
},
|
||||
configure(options: any) {
|
||||
settings = options && options.html;
|
||||
},
|
||||
|
||||
@@ -53,6 +53,9 @@ export function getJavascriptMode(htmlLanguageService: HTMLLanguageService, html
|
||||
let settings: any = {};
|
||||
|
||||
return {
|
||||
getId() {
|
||||
return 'html';
|
||||
},
|
||||
configure(options: any) {
|
||||
settings = options && options.javascript;
|
||||
},
|
||||
|
||||
@@ -17,6 +17,7 @@ import { getJavascriptMode } from './javascriptMode';
|
||||
import { getHTMLMode } from './htmlMode';
|
||||
|
||||
export interface LanguageMode {
|
||||
getId();
|
||||
configure?: (options: any) => void;
|
||||
doValidation?: (document: TextDocument) => Diagnostic[];
|
||||
doComplete?: (document: TextDocument, position: Position) => CompletionList;
|
||||
|
||||
@@ -48,12 +48,30 @@ suite('HTML Embedded Support', () => {
|
||||
assertLanguageId('<html><style>foo { }</sty|le></html>', 'html');
|
||||
});
|
||||
|
||||
test('Style in attribute', function (): any {
|
||||
assertLanguageId('<div id="xy" |style="color: red"/>', 'html');
|
||||
assertLanguageId('<div id="xy" styl|e="color: red"/>', 'html');
|
||||
assertLanguageId('<div id="xy" style=|"color: red"/>', 'html');
|
||||
assertLanguageId('<div id="xy" style="|color: red"/>', 'css');
|
||||
assertLanguageId('<div id="xy" style="color|: red"/>', 'css');
|
||||
assertLanguageId('<div id="xy" style="color: red|"/>', 'css');
|
||||
assertLanguageId('<div id="xy" style="color: red"|/>', 'html');
|
||||
assertLanguageId('<div id="xy" style=\'color: r|ed\'/>', 'css');
|
||||
assertLanguageId('<div id="xy" style|=color:red/>', 'html');
|
||||
assertLanguageId('<div id="xy" style=|color:red/>', 'css');
|
||||
assertLanguageId('<div id="xy" style=color:r|ed/>', 'css');
|
||||
assertLanguageId('<div id="xy" style=color:red|/>', 'css');
|
||||
assertLanguageId('<div id="xy" style=color:red/|>', 'html');
|
||||
});
|
||||
|
||||
test('Style content', function (): any {
|
||||
assertEmbeddedLanguageContent('<html><style>foo { }</style></html>', 'css', ' foo { } ');
|
||||
assertEmbeddedLanguageContent('<html><script>var i = 0;</script></html>', 'css', ' ');
|
||||
assertEmbeddedLanguageContent('<html><style>foo { }</style>Hello<style>foo { }</style></html>', 'css', ' foo { } foo { } ');
|
||||
assertEmbeddedLanguageContent('<html>\n <style>\n foo { } \n </style>\n</html>\n', 'css', '\n \n foo { } \n \n\n');
|
||||
|
||||
assertEmbeddedLanguageContent('<div style="color: red"></div>', 'css', ' x{color: red} ');
|
||||
assertEmbeddedLanguageContent('<div style=color:red></div>', 'css', ' x{color:red} ');
|
||||
});
|
||||
|
||||
test('Scripts', function (): any {
|
||||
@@ -73,9 +91,31 @@ suite('HTML Embedded Support', () => {
|
||||
assertLanguageId('<script type=\'text/javascript\'>var| i = 0;</script>', 'javascript');
|
||||
});
|
||||
|
||||
test('Scripts in attribute', function (): any {
|
||||
assertLanguageId('<div |onKeyUp="foo()" onkeydown=\'bar()\'/>', 'html');
|
||||
assertLanguageId('<div onKeyUp=|"foo()" onkeydown=\'bar()\'/>', 'html');
|
||||
assertLanguageId('<div onKeyUp="|foo()" onkeydown=\'bar()\'/>', 'javascript');
|
||||
assertLanguageId('<div onKeyUp="foo(|)" onkeydown=\'bar()\'/>', 'javascript');
|
||||
assertLanguageId('<div onKeyUp="foo()|" onkeydown=\'bar()\'/>', 'javascript');
|
||||
assertLanguageId('<div onKeyUp="foo()"| onkeydown=\'bar()\'/>', 'html');
|
||||
assertLanguageId('<div onKeyUp="foo()" onkeydown=|\'bar()\'/>', 'html');
|
||||
assertLanguageId('<div onKeyUp="foo()" onkeydown=\'|bar()\'/>', 'javascript');
|
||||
assertLanguageId('<div onKeyUp="foo()" onkeydown=\'bar()|\'/>', 'javascript');
|
||||
assertLanguageId('<div onKeyUp="foo()" onkeydown=\'bar()\'|/>', 'html');
|
||||
|
||||
assertLanguageId('<DIV ONKEYUP|=foo()</DIV>', 'html');
|
||||
assertLanguageId('<DIV ONKEYUP=|foo()</DIV>', 'javascript');
|
||||
assertLanguageId('<DIV ONKEYUP=f|oo()</DIV>', 'javascript');
|
||||
assertLanguageId('<DIV ONKEYUP=foo(|)</DIV>', 'javascript');
|
||||
assertLanguageId('<DIV ONKEYUP=foo()|</DIV>', 'javascript');
|
||||
assertLanguageId('<DIV ONKEYUP=foo()<|/DIV>', 'html');
|
||||
});
|
||||
|
||||
test('Script content', function (): any {
|
||||
assertEmbeddedLanguageContent('<html><script>var i = 0;</script></html>', 'javascript', ' var i = 0; ');
|
||||
assertEmbeddedLanguageContent('<script type="text/javascript">var i = 0;</script>', 'javascript', ' var i = 0; ');
|
||||
|
||||
assertEmbeddedLanguageContent('<div onKeyUp="foo()" onkeydown="bar()"/>', 'javascript', ' foo(); bar(); ');
|
||||
});
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user