embeddedSupport.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  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. import { TextDocument, Position, Range } from 'vscode-languageclient';
  6. import { LanguageService, TokenType } from 'vscode-html-languageservice';
  7. export interface LanguageRange extends Range {
  8. languageId: string | undefined;
  9. attributeValue?: boolean;
  10. }
  11. export interface HTMLDocumentRegions {
  12. getEmbeddedDocument(languageId: string, ignoreAttributeValues?: boolean): TextDocument;
  13. getLanguageRanges(range: Range): LanguageRange[];
  14. getLanguageAtPosition(position: Position): string | undefined;
  15. getLanguagesInDocument(): string[];
  16. getImportedScripts(): string[];
  17. }
  18. export const CSS_STYLE_RULE = '__';
  19. interface EmbeddedRegion {
  20. languageId: string | undefined;
  21. start: number;
  22. end: number;
  23. attributeValue?: boolean;
  24. }
  25. export function isInsideStyleRegion(
  26. languageService: LanguageService,
  27. documentText: string,
  28. offset: number
  29. ) {
  30. let scanner = languageService.createScanner(documentText);
  31. let token = scanner.scan();
  32. while (token !== TokenType.EOS) {
  33. switch (token) {
  34. case TokenType.Styles:
  35. if (offset >= scanner.getTokenOffset() && offset <= scanner.getTokenEnd()) {
  36. return true;
  37. }
  38. }
  39. token = scanner.scan();
  40. }
  41. return false;
  42. }
  43. export function getCSSVirtualContent(
  44. languageService: LanguageService,
  45. documentText: string
  46. ): string {
  47. let regions: EmbeddedRegion[] = [];
  48. let scanner = languageService.createScanner(documentText);
  49. let lastTagName: string = '';
  50. let lastAttributeName: string | null = null;
  51. let languageIdFromType: string | undefined = undefined;
  52. let importedScripts: string[] = [];
  53. let token = scanner.scan();
  54. while (token !== TokenType.EOS) {
  55. switch (token) {
  56. case TokenType.StartTag:
  57. lastTagName = scanner.getTokenText();
  58. lastAttributeName = null;
  59. languageIdFromType = 'javascript';
  60. break;
  61. case TokenType.Styles:
  62. regions.push({
  63. languageId: 'css',
  64. start: scanner.getTokenOffset(),
  65. end: scanner.getTokenEnd()
  66. });
  67. break;
  68. case TokenType.Script:
  69. regions.push({
  70. languageId: languageIdFromType,
  71. start: scanner.getTokenOffset(),
  72. end: scanner.getTokenEnd()
  73. });
  74. break;
  75. case TokenType.AttributeName:
  76. lastAttributeName = scanner.getTokenText();
  77. break;
  78. case TokenType.AttributeValue:
  79. if (lastAttributeName === 'src' && lastTagName.toLowerCase() === 'script') {
  80. let value = scanner.getTokenText();
  81. if (value[0] === "'" || value[0] === '"') {
  82. value = value.substr(1, value.length - 1);
  83. }
  84. importedScripts.push(value);
  85. } else if (lastAttributeName === 'type' && lastTagName.toLowerCase() === 'script') {
  86. if (
  87. /["'](module|(text|application)\/(java|ecma)script|text\/babel)["']/.test(
  88. scanner.getTokenText()
  89. )
  90. ) {
  91. languageIdFromType = 'javascript';
  92. } else if (/["']text\/typescript["']/.test(scanner.getTokenText())) {
  93. languageIdFromType = 'typescript';
  94. } else {
  95. languageIdFromType = undefined;
  96. }
  97. } else {
  98. let attributeLanguageId = getAttributeLanguage(lastAttributeName!);
  99. if (attributeLanguageId) {
  100. let start = scanner.getTokenOffset();
  101. let end = scanner.getTokenEnd();
  102. let firstChar = documentText[start];
  103. if (firstChar === "'" || firstChar === '"') {
  104. start++;
  105. end--;
  106. }
  107. regions.push({
  108. languageId: attributeLanguageId,
  109. start,
  110. end,
  111. attributeValue: true
  112. });
  113. }
  114. }
  115. lastAttributeName = null;
  116. break;
  117. }
  118. token = scanner.scan();
  119. }
  120. let content = documentText
  121. .split('\n')
  122. .map(line => {
  123. return ' '.repeat(line.length);
  124. }).join('\n');
  125. regions.forEach(r => {
  126. if (r.languageId === 'css') {
  127. content = content.slice(0, r.start) + documentText.slice(r.start, r.end) + content.slice(r.end);
  128. }
  129. });
  130. return content;
  131. }
  132. export function getDocumentRegions(
  133. languageService: LanguageService,
  134. document: TextDocument
  135. ): HTMLDocumentRegions {
  136. let regions: EmbeddedRegion[] = [];
  137. let scanner = languageService.createScanner(document.getText());
  138. let lastTagName: string = '';
  139. let lastAttributeName: string | null = null;
  140. let languageIdFromType: string | undefined = undefined;
  141. let importedScripts: string[] = [];
  142. let token = scanner.scan();
  143. while (token !== TokenType.EOS) {
  144. switch (token) {
  145. case TokenType.StartTag:
  146. lastTagName = scanner.getTokenText();
  147. lastAttributeName = null;
  148. languageIdFromType = 'javascript';
  149. break;
  150. case TokenType.Styles:
  151. regions.push({
  152. languageId: 'css',
  153. start: scanner.getTokenOffset(),
  154. end: scanner.getTokenEnd()
  155. });
  156. break;
  157. case TokenType.Script:
  158. regions.push({
  159. languageId: languageIdFromType,
  160. start: scanner.getTokenOffset(),
  161. end: scanner.getTokenEnd()
  162. });
  163. break;
  164. case TokenType.AttributeName:
  165. lastAttributeName = scanner.getTokenText();
  166. break;
  167. case TokenType.AttributeValue:
  168. if (lastAttributeName === 'src' && lastTagName.toLowerCase() === 'script') {
  169. let value = scanner.getTokenText();
  170. if (value[0] === "'" || value[0] === '"') {
  171. value = value.substr(1, value.length - 1);
  172. }
  173. importedScripts.push(value);
  174. } else if (lastAttributeName === 'type' && lastTagName.toLowerCase() === 'script') {
  175. if (
  176. /["'](module|(text|application)\/(java|ecma)script|text\/babel)["']/.test(
  177. scanner.getTokenText()
  178. )
  179. ) {
  180. languageIdFromType = 'javascript';
  181. } else if (/["']text\/typescript["']/.test(scanner.getTokenText())) {
  182. languageIdFromType = 'typescript';
  183. } else {
  184. languageIdFromType = undefined;
  185. }
  186. } else {
  187. let attributeLanguageId = getAttributeLanguage(lastAttributeName!);
  188. if (attributeLanguageId) {
  189. let start = scanner.getTokenOffset();
  190. let end = scanner.getTokenEnd();
  191. let firstChar = document.getText()[start];
  192. if (firstChar === "'" || firstChar === '"') {
  193. start++;
  194. end--;
  195. }
  196. regions.push({
  197. languageId: attributeLanguageId,
  198. start,
  199. end,
  200. attributeValue: true
  201. });
  202. }
  203. }
  204. lastAttributeName = null;
  205. break;
  206. }
  207. token = scanner.scan();
  208. }
  209. return {
  210. getLanguageRanges: (range: Range) => getLanguageRanges(document, regions, range),
  211. getEmbeddedDocument: (languageId: string, ignoreAttributeValues: boolean) =>
  212. getEmbeddedDocument(document, regions, languageId, ignoreAttributeValues),
  213. getLanguageAtPosition: (position: Position) =>
  214. getLanguageAtPosition(document, regions, position),
  215. getLanguagesInDocument: () => getLanguagesInDocument(document, regions),
  216. getImportedScripts: () => importedScripts
  217. };
  218. }
  219. function getLanguageRanges(
  220. document: TextDocument,
  221. regions: EmbeddedRegion[],
  222. range: Range
  223. ): LanguageRange[] {
  224. let result: LanguageRange[] = [];
  225. let currentPos = range ? range.start : Position.create(0, 0);
  226. let currentOffset = range ? document.offsetAt(range.start) : 0;
  227. let endOffset = range ? document.offsetAt(range.end) : document.getText().length;
  228. for (let region of regions) {
  229. if (region.end > currentOffset && region.start < endOffset) {
  230. let start = Math.max(region.start, currentOffset);
  231. let startPos = document.positionAt(start);
  232. if (currentOffset < region.start) {
  233. result.push({
  234. start: currentPos,
  235. end: startPos,
  236. languageId: 'html'
  237. });
  238. }
  239. let end = Math.min(region.end, endOffset);
  240. let endPos = document.positionAt(end);
  241. if (end > region.start) {
  242. result.push({
  243. start: startPos,
  244. end: endPos,
  245. languageId: region.languageId,
  246. attributeValue: region.attributeValue
  247. });
  248. }
  249. currentOffset = end;
  250. currentPos = endPos;
  251. }
  252. }
  253. if (currentOffset < endOffset) {
  254. let endPos = range ? range.end : document.positionAt(endOffset);
  255. result.push({
  256. start: currentPos,
  257. end: endPos,
  258. languageId: 'html'
  259. });
  260. }
  261. return result;
  262. }
  263. function getLanguagesInDocument(
  264. _document: TextDocument,
  265. regions: EmbeddedRegion[]
  266. ): string[] {
  267. let result = [];
  268. for (let region of regions) {
  269. if (region.languageId && result.indexOf(region.languageId) === -1) {
  270. result.push(region.languageId);
  271. if (result.length === 3) {
  272. return result;
  273. }
  274. }
  275. }
  276. result.push('html');
  277. return result;
  278. }
  279. function getLanguageAtPosition(
  280. document: TextDocument,
  281. regions: EmbeddedRegion[],
  282. position: Position
  283. ): string | undefined {
  284. let offset = document.offsetAt(position);
  285. for (let region of regions) {
  286. if (region.start <= offset) {
  287. if (offset <= region.end) {
  288. return region.languageId;
  289. }
  290. } else {
  291. break;
  292. }
  293. }
  294. return 'html';
  295. }
  296. function getEmbeddedDocument(
  297. document: TextDocument,
  298. contents: EmbeddedRegion[],
  299. languageId: string,
  300. ignoreAttributeValues: boolean
  301. ): TextDocument {
  302. let currentPos = 0;
  303. let oldContent = document.getText();
  304. let result = '';
  305. let lastSuffix = '';
  306. for (let c of contents) {
  307. if (c.languageId === languageId && (!ignoreAttributeValues || !c.attributeValue)) {
  308. result = substituteWithWhitespace(
  309. result,
  310. currentPos,
  311. c.start,
  312. oldContent,
  313. lastSuffix,
  314. getPrefix(c)
  315. );
  316. result += oldContent.substring(c.start, c.end);
  317. currentPos = c.end;
  318. lastSuffix = getSuffix(c);
  319. }
  320. }
  321. result = substituteWithWhitespace(
  322. result,
  323. currentPos,
  324. oldContent.length,
  325. oldContent,
  326. lastSuffix,
  327. ''
  328. );
  329. return TextDocument.create(document.uri, languageId, document.version, result);
  330. }
  331. function getPrefix(c: EmbeddedRegion) {
  332. if (c.attributeValue) {
  333. switch (c.languageId) {
  334. case 'css':
  335. return CSS_STYLE_RULE + '{';
  336. }
  337. }
  338. return '';
  339. }
  340. function getSuffix(c: EmbeddedRegion) {
  341. if (c.attributeValue) {
  342. switch (c.languageId) {
  343. case 'css':
  344. return '}';
  345. case 'javascript':
  346. return ';';
  347. }
  348. }
  349. return '';
  350. }
  351. function substituteWithWhitespace(
  352. result: string,
  353. start: number,
  354. end: number,
  355. oldContent: string,
  356. before: string,
  357. after: string
  358. ) {
  359. let accumulatedWS = 0;
  360. result += before;
  361. for (let i = start + before.length; i < end; i++) {
  362. let ch = oldContent[i];
  363. if (ch === '\n' || ch === '\r') {
  364. // only write new lines, skip the whitespace
  365. accumulatedWS = 0;
  366. result += ch;
  367. } else {
  368. accumulatedWS++;
  369. }
  370. }
  371. result = append(result, ' ', accumulatedWS - after.length);
  372. result += after;
  373. return result;
  374. }
  375. function append(result: string, str: string, n: number): string {
  376. while (n > 0) {
  377. if (n & 1) {
  378. result += str;
  379. }
  380. n >>= 1;
  381. str += str;
  382. }
  383. return result;
  384. }
  385. function getAttributeLanguage(attributeName: string): string | null {
  386. let match = attributeName.match(/^(style)$|^(on\w+)$/i);
  387. if (!match) {
  388. return null;
  389. }
  390. return match[1] ? 'css' : 'javascript';
  391. }