[html] embedded css/javascript in attribute values

This commit is contained in:
Martin Aeschlimann 2016-11-21 17:02:52 +01:00
parent b0780bc908
commit 287a5deb91
8 changed files with 192 additions and 48 deletions

View file

@ -8,9 +8,9 @@
"resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-2.0.0-next.3.tgz"
},
"vscode-html-languageservice": {
"version": "1.0.1-next.3",
"version": "1.0.1-next.4",
"from": "vscode-html-languageservice@next",
"resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-1.0.1-next.3.tgz"
"resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-1.0.1-next.4.tgz"
},
"vscode-jsonrpc": {
"version": "2.4.0",

View file

@ -9,7 +9,7 @@
},
"dependencies": {
"vscode-css-languageservice": "^2.0.0-next.3",
"vscode-html-languageservice": "^1.0.1-next.3",
"vscode-html-languageservice": "^1.0.1-next.4",
"vscode-languageserver": "^2.6.2-next.1",
"vscode-nls": "^1.0.4",
"vscode-uri": "^1.0.0"

View file

@ -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);
},

View file

@ -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*$/);
}

View file

@ -13,6 +13,9 @@ export function getHTMLMode(htmlLanguageService: HTMLLanguageService, htmlDocume
let settings: any = {};
return {
getId() {
return 'html';
},
configure(options: any) {
settings = options && options.html;
},

View file

@ -53,6 +53,9 @@ export function getJavascriptMode(htmlLanguageService: HTMLLanguageService, html
let settings: any = {};
return {
getId() {
return 'html';
},
configure(options: any) {
settings = options && options.javascript;
},

View file

@ -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;

View file

@ -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(); ');
});
});