Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/emmet/src/util.ts
4772 views
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 vscode from 'vscode';
7
import parse from '@emmetio/html-matcher';
8
import parseStylesheet from '@emmetio/css-parser';
9
import { Node as FlatNode, HtmlNode as HtmlFlatNode, Property as FlatProperty, Rule as FlatRule, CssToken as FlatCssToken, Stylesheet as FlatStylesheet } from 'EmmetFlatNode';
10
import { DocumentStreamReader } from './bufferStream';
11
import * as EmmetHelper from '@vscode/emmet-helper';
12
import { TextDocument as LSTextDocument } from 'vscode-languageserver-textdocument';
13
import { getRootNode } from './parseDocument';
14
15
let _emmetHelper: typeof EmmetHelper;
16
let _currentExtensionsPath: string[] | undefined;
17
18
let _homeDir: vscode.Uri | undefined;
19
20
21
export function setHomeDir(homeDir: vscode.Uri) {
22
_homeDir = homeDir;
23
}
24
25
export function getEmmetHelper() {
26
// Lazy load vscode-emmet-helper instead of importing it
27
// directly to reduce the start-up time of the extension
28
if (!_emmetHelper) {
29
_emmetHelper = require('@vscode/emmet-helper');
30
}
31
return _emmetHelper;
32
}
33
34
/**
35
* Update Emmet Helper to use user snippets from the extensionsPath setting
36
*/
37
export function updateEmmetExtensionsPath(forceRefresh: boolean = false) {
38
const helper = getEmmetHelper();
39
let extensionsPath = vscode.workspace.getConfiguration('emmet').get<string[]>('extensionsPath');
40
if (!extensionsPath) {
41
extensionsPath = [];
42
}
43
if (forceRefresh || _currentExtensionsPath !== extensionsPath) {
44
_currentExtensionsPath = extensionsPath;
45
const rootPaths = vscode.workspace.workspaceFolders?.length ? vscode.workspace.workspaceFolders.map(f => f.uri) : undefined;
46
const fileSystem = vscode.workspace.fs;
47
helper.updateExtensionsPath(extensionsPath, fileSystem, rootPaths, _homeDir).catch(err => {
48
if (Array.isArray(extensionsPath) && extensionsPath.length) {
49
vscode.window.showErrorMessage(err.message);
50
}
51
});
52
}
53
}
54
55
/**
56
* Migrate old configuration(string) for extensionsPath to new type(string[])
57
* https://github.com/microsoft/vscode/issues/117517
58
*/
59
export function migrateEmmetExtensionsPath() {
60
// Get the detail info of emmet.extensionsPath setting
61
const config = vscode.workspace.getConfiguration().inspect('emmet.extensionsPath');
62
63
// Update Global setting if the value type is string or the value is null
64
if (typeof config?.globalValue === 'string') {
65
vscode.workspace.getConfiguration().update('emmet.extensionsPath', [config.globalValue], true);
66
} else if (config?.globalValue === null) {
67
vscode.workspace.getConfiguration().update('emmet.extensionsPath', [], true);
68
}
69
// Update Workspace setting if the value type is string or the value is null
70
if (typeof config?.workspaceValue === 'string') {
71
vscode.workspace.getConfiguration().update('emmet.extensionsPath', [config.workspaceValue], false);
72
} else if (config?.workspaceValue === null) {
73
vscode.workspace.getConfiguration().update('emmet.extensionsPath', [], false);
74
}
75
// Update WorkspaceFolder setting if the value type is string or the value is null
76
if (typeof config?.workspaceFolderValue === 'string') {
77
vscode.workspace.getConfiguration().update('emmet.extensionsPath', [config.workspaceFolderValue]);
78
} else if (config?.workspaceFolderValue === null) {
79
vscode.workspace.getConfiguration().update('emmet.extensionsPath', []);
80
}
81
}
82
83
/**
84
* Mapping between languages that support Emmet and completion trigger characters
85
*/
86
export const LANGUAGE_MODES: { [id: string]: string[] } = {
87
'html': ['!', '.', '}', ':', '*', '$', ']', '/', '>', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
88
'jade': ['!', '.', '}', ':', '*', '$', ']', '/', '>', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
89
'slim': ['!', '.', '}', ':', '*', '$', ']', '/', '>', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
90
'haml': ['!', '.', '}', ':', '*', '$', ']', '/', '>', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
91
'xml': ['.', '}', '*', '$', ']', '/', '>', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
92
'xsl': ['!', '.', '}', '*', '$', '/', ']', '>', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
93
'css': [':', '!', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
94
'scss': [':', '!', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
95
'sass': [':', '!', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
96
'less': [':', '!', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
97
'stylus': [':', '!', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
98
'javascriptreact': ['!', '.', '}', '*', '$', ']', '/', '>', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
99
'typescriptreact': ['!', '.', '}', '*', '$', ']', '/', '>', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
100
};
101
102
export function isStyleSheet(syntax: string): boolean {
103
const stylesheetSyntaxes = ['css', 'scss', 'sass', 'less', 'stylus'];
104
return stylesheetSyntaxes.includes(syntax);
105
}
106
107
export function validate(allowStylesheet: boolean = true): boolean {
108
const editor = vscode.window.activeTextEditor;
109
if (!editor) {
110
vscode.window.showInformationMessage('No editor is active');
111
return false;
112
}
113
if (!allowStylesheet && isStyleSheet(editor.document.languageId)) {
114
return false;
115
}
116
return true;
117
}
118
119
export function getMappingForIncludedLanguages(): Record<string, string> {
120
// Explicitly map languages that have built-in grammar in VS Code to their parent language
121
// to get emmet completion support
122
// For other languages, users will have to use `emmet.includeLanguages` or
123
// language specific extensions can provide emmet completion support
124
const MAPPED_MODES: Record<string, string> = {
125
'handlebars': 'html',
126
'php': 'html'
127
};
128
129
const finalMappedModes: Record<string, string> = {};
130
const includeLanguagesConfig = vscode.workspace.getConfiguration('emmet').get<Record<string, string>>('includeLanguages');
131
const includeLanguages = Object.assign({}, MAPPED_MODES, includeLanguagesConfig ?? {});
132
Object.keys(includeLanguages).forEach(syntax => {
133
if (typeof includeLanguages[syntax] === 'string' && LANGUAGE_MODES[includeLanguages[syntax]]) {
134
finalMappedModes[syntax] = includeLanguages[syntax];
135
}
136
});
137
return finalMappedModes;
138
}
139
140
/**
141
* Get the corresponding emmet mode for given vscode language mode
142
* E.g.: jsx for typescriptreact/javascriptreact or pug for jade
143
* If the language is not supported by emmet or has been excluded via `excludeLanguages` setting,
144
* then nothing is returned
145
*
146
* @param excludedLanguages Array of language ids that user has chosen to exclude for emmet
147
*/
148
export function getEmmetMode(language: string, mappedModes: Record<string, string>, excludedLanguages: string[]): string | undefined {
149
if (!language || excludedLanguages.includes(language)) {
150
return;
151
}
152
153
if (language === 'jsx-tags') {
154
language = 'javascriptreact';
155
}
156
157
if (mappedModes[language]) {
158
language = mappedModes[language];
159
}
160
161
if (/\b(typescriptreact|javascriptreact|jsx-tags)\b/.test(language)) { // treat tsx like jsx
162
language = 'jsx';
163
}
164
else if (language === 'sass-indented') { // map sass-indented to sass
165
language = 'sass';
166
}
167
else if (language === 'jade' || language === 'pug') {
168
language = 'pug';
169
}
170
171
const syntaxes = getSyntaxes();
172
if (syntaxes.markup.includes(language) || syntaxes.stylesheet.includes(language)) {
173
return language;
174
}
175
return;
176
}
177
178
const closeBrace = 125;
179
const openBrace = 123;
180
const slash = 47;
181
const star = 42;
182
183
/**
184
* Traverse the given document backward & forward from given position
185
* to find a complete ruleset, then parse just that to return a Stylesheet
186
* @param document vscode.TextDocument
187
* @param position vscode.Position
188
*/
189
export function parsePartialStylesheet(document: vscode.TextDocument, position: vscode.Position): FlatStylesheet | undefined {
190
const isCSS = document.languageId === 'css';
191
const positionOffset = document.offsetAt(position);
192
let startOffset = 0;
193
let endOffset = document.getText().length;
194
const limitCharacter = positionOffset - 5000;
195
const limitOffset = limitCharacter > 0 ? limitCharacter : startOffset;
196
const stream = new DocumentStreamReader(document, positionOffset);
197
198
function findOpeningCommentBeforePosition(pos: number): number | undefined {
199
const text = document.getText().substring(0, pos);
200
const offset = text.lastIndexOf('/*');
201
if (offset === -1) {
202
return;
203
}
204
return offset;
205
}
206
207
function findClosingCommentAfterPosition(pos: number): number | undefined {
208
const text = document.getText().substring(pos);
209
let offset = text.indexOf('*/');
210
if (offset === -1) {
211
return;
212
}
213
offset += 2 + pos;
214
return offset;
215
}
216
217
function consumeLineCommentBackwards() {
218
const posLineNumber = document.positionAt(stream.pos).line;
219
if (!isCSS && currentLine !== posLineNumber) {
220
currentLine = posLineNumber;
221
const startLineComment = document.lineAt(currentLine).text.indexOf('//');
222
if (startLineComment > -1) {
223
stream.pos = document.offsetAt(new vscode.Position(currentLine, startLineComment));
224
}
225
}
226
}
227
228
function consumeBlockCommentBackwards() {
229
if (!stream.sof() && stream.peek() === slash) {
230
if (stream.backUp(1) === star) {
231
stream.pos = findOpeningCommentBeforePosition(stream.pos) ?? startOffset;
232
} else {
233
stream.next();
234
}
235
}
236
}
237
238
function consumeCommentForwards() {
239
if (stream.eat(slash)) {
240
if (stream.eat(slash) && !isCSS) {
241
const posLineNumber = document.positionAt(stream.pos).line;
242
stream.pos = document.offsetAt(new vscode.Position(posLineNumber + 1, 0));
243
} else if (stream.eat(star)) {
244
stream.pos = findClosingCommentAfterPosition(stream.pos) ?? endOffset;
245
}
246
}
247
}
248
249
// Go forward until we find a closing brace.
250
while (!stream.eof() && !stream.eat(closeBrace)) {
251
if (stream.peek() === slash) {
252
consumeCommentForwards();
253
} else {
254
stream.next();
255
}
256
}
257
258
if (!stream.eof()) {
259
endOffset = stream.pos;
260
}
261
262
stream.pos = positionOffset;
263
let openBracesToFind = 1;
264
let currentLine = position.line;
265
let exit = false;
266
267
// Go back until we found an opening brace. If we find a closing one, consume its pair and continue.
268
while (!exit && openBracesToFind > 0 && !stream.sof()) {
269
consumeLineCommentBackwards();
270
271
switch (stream.backUp(1)) {
272
case openBrace:
273
openBracesToFind--;
274
break;
275
case closeBrace:
276
if (isCSS) {
277
stream.next();
278
startOffset = stream.pos;
279
exit = true;
280
} else {
281
openBracesToFind++;
282
}
283
break;
284
case slash:
285
consumeBlockCommentBackwards();
286
break;
287
default:
288
break;
289
}
290
291
if (position.line - document.positionAt(stream.pos).line > 100
292
|| stream.pos <= limitOffset) {
293
exit = true;
294
}
295
}
296
297
// We are at an opening brace. We need to include its selector.
298
currentLine = document.positionAt(stream.pos).line;
299
openBracesToFind = 0;
300
let foundSelector = false;
301
while (!exit && !stream.sof() && !foundSelector && openBracesToFind >= 0) {
302
consumeLineCommentBackwards();
303
304
const ch = stream.backUp(1);
305
if (/\s/.test(String.fromCharCode(ch))) {
306
continue;
307
}
308
309
switch (ch) {
310
case slash:
311
consumeBlockCommentBackwards();
312
break;
313
case closeBrace:
314
openBracesToFind++;
315
break;
316
case openBrace:
317
openBracesToFind--;
318
break;
319
default:
320
if (!openBracesToFind) {
321
foundSelector = true;
322
}
323
break;
324
}
325
326
if (!stream.sof() && foundSelector) {
327
startOffset = stream.pos;
328
}
329
}
330
331
try {
332
const buffer = ' '.repeat(startOffset) + document.getText().substring(startOffset, endOffset);
333
return parseStylesheet(buffer);
334
} catch (e) {
335
return;
336
}
337
}
338
339
/**
340
* Returns node corresponding to given position in the given root node
341
*/
342
export function getFlatNode(root: FlatNode | undefined, offset: number, includeNodeBoundary: boolean): FlatNode | undefined {
343
if (!root) {
344
return;
345
}
346
347
function getFlatNodeChild(child: FlatNode | undefined): FlatNode | undefined {
348
if (!child) {
349
return;
350
}
351
const nodeStart = child.start;
352
const nodeEnd = child.end;
353
if ((nodeStart < offset && nodeEnd > offset)
354
|| (includeNodeBoundary && nodeStart <= offset && nodeEnd >= offset)) {
355
return getFlatNodeChildren(child.children) ?? child;
356
}
357
else if ('close' in child) {
358
// We have an HTML node in this case.
359
// In case this node is an invalid unpaired HTML node,
360
// we still want to search its children
361
const htmlChild = <HtmlFlatNode>child;
362
if (htmlChild.open && !htmlChild.close) {
363
return getFlatNodeChildren(htmlChild.children);
364
}
365
}
366
return;
367
}
368
369
function getFlatNodeChildren(children: FlatNode[]): FlatNode | undefined {
370
for (let i = 0; i < children.length; i++) {
371
const foundChild = getFlatNodeChild(children[i]);
372
if (foundChild) {
373
return foundChild;
374
}
375
}
376
return;
377
}
378
379
return getFlatNodeChildren(root.children);
380
}
381
382
export const allowedMimeTypesInScriptTag = ['text/html', 'text/plain', 'text/x-template', 'text/template', 'text/ng-template'];
383
384
/**
385
* Finds the HTML node within an HTML document at a given position
386
* If position is inside a script tag of type template, then it will be parsed to find the inner HTML node as well
387
*/
388
export function getHtmlFlatNode(documentText: string, root: FlatNode | undefined, offset: number, includeNodeBoundary: boolean): HtmlFlatNode | undefined {
389
let currentNode: HtmlFlatNode | undefined = <HtmlFlatNode | undefined>getFlatNode(root, offset, includeNodeBoundary);
390
if (!currentNode) { return; }
391
392
// If the currentNode is a script one, first set up its subtree and then find HTML node.
393
if (currentNode.name === 'script' && currentNode.children.length === 0) {
394
const scriptNodeBody = setupScriptNodeSubtree(documentText, currentNode);
395
if (scriptNodeBody) {
396
currentNode = getHtmlFlatNode(scriptNodeBody, currentNode, offset, includeNodeBoundary) ?? currentNode;
397
}
398
}
399
else if (currentNode.type === 'cdata') {
400
const cdataBody = setupCdataNodeSubtree(documentText, currentNode);
401
currentNode = getHtmlFlatNode(cdataBody, currentNode, offset, includeNodeBoundary) ?? currentNode;
402
}
403
return currentNode;
404
}
405
406
export function setupScriptNodeSubtree(documentText: string, scriptNode: HtmlFlatNode): string {
407
const isTemplateScript = scriptNode.name === 'script' &&
408
(scriptNode.attributes &&
409
scriptNode.attributes.some(x => x.name.toString() === 'type'
410
&& allowedMimeTypesInScriptTag.includes(x.value.toString())));
411
if (isTemplateScript
412
&& scriptNode.open) {
413
// blank out the rest of the document and generate the subtree.
414
const beforePadding = ' '.repeat(scriptNode.open.end);
415
const endToUse = scriptNode.close ? scriptNode.close.start : scriptNode.end;
416
const scriptBodyText = beforePadding + documentText.substring(scriptNode.open.end, endToUse);
417
const innerRoot: HtmlFlatNode = parse(scriptBodyText);
418
innerRoot.children.forEach(child => {
419
scriptNode.children.push(child);
420
child.parent = scriptNode;
421
});
422
return scriptBodyText;
423
}
424
return '';
425
}
426
427
export function setupCdataNodeSubtree(documentText: string, cdataNode: HtmlFlatNode): string {
428
// blank out the rest of the document and generate the subtree.
429
const cdataStart = '<![CDATA[';
430
const cdataEnd = ']]>';
431
const startToUse = cdataNode.start + cdataStart.length;
432
const endToUse = cdataNode.end - cdataEnd.length;
433
const beforePadding = ' '.repeat(startToUse);
434
const cdataBody = beforePadding + documentText.substring(startToUse, endToUse);
435
const innerRoot: HtmlFlatNode = parse(cdataBody);
436
innerRoot.children.forEach(child => {
437
cdataNode.children.push(child);
438
child.parent = cdataNode;
439
});
440
return cdataBody;
441
}
442
443
export function isOffsetInsideOpenOrCloseTag(node: FlatNode, offset: number): boolean {
444
const htmlNode = node as HtmlFlatNode;
445
if ((htmlNode.open && offset > htmlNode.open.start && offset < htmlNode.open.end)
446
|| (htmlNode.close && offset > htmlNode.close.start && offset < htmlNode.close.end)) {
447
return true;
448
}
449
450
return false;
451
}
452
453
export function offsetRangeToSelection(document: vscode.TextDocument, start: number, end: number): vscode.Selection {
454
const startPos = document.positionAt(start);
455
const endPos = document.positionAt(end);
456
return new vscode.Selection(startPos, endPos);
457
}
458
459
export function offsetRangeToVsRange(document: vscode.TextDocument, start: number, end: number): vscode.Range {
460
const startPos = document.positionAt(start);
461
const endPos = document.positionAt(end);
462
return new vscode.Range(startPos, endPos);
463
}
464
465
/**
466
* Returns the deepest non comment node under given node
467
*/
468
export function getDeepestFlatNode(node: FlatNode | undefined): FlatNode | undefined {
469
if (!node || !node.children || node.children.length === 0 || !node.children.find(x => x.type !== 'comment')) {
470
return node;
471
}
472
for (let i = node.children.length - 1; i >= 0; i--) {
473
if (node.children[i].type !== 'comment') {
474
return getDeepestFlatNode(node.children[i]);
475
}
476
}
477
return undefined;
478
}
479
480
export function findNextWord(propertyValue: string, pos: number): [number | undefined, number | undefined] {
481
482
let foundSpace = pos === -1;
483
let foundStart = false;
484
let foundEnd = false;
485
486
let newSelectionStart;
487
let newSelectionEnd;
488
while (pos < propertyValue.length - 1) {
489
pos++;
490
if (!foundSpace) {
491
if (propertyValue[pos] === ' ') {
492
foundSpace = true;
493
}
494
continue;
495
}
496
if (foundSpace && !foundStart && propertyValue[pos] === ' ') {
497
continue;
498
}
499
if (!foundStart) {
500
newSelectionStart = pos;
501
foundStart = true;
502
continue;
503
}
504
if (propertyValue[pos] === ' ') {
505
newSelectionEnd = pos;
506
foundEnd = true;
507
break;
508
}
509
}
510
511
if (foundStart && !foundEnd) {
512
newSelectionEnd = propertyValue.length;
513
}
514
515
return [newSelectionStart, newSelectionEnd];
516
}
517
518
export function findPrevWord(propertyValue: string, pos: number): [number | undefined, number | undefined] {
519
520
let foundSpace = pos === propertyValue.length;
521
let foundStart = false;
522
let foundEnd = false;
523
524
let newSelectionStart;
525
let newSelectionEnd;
526
while (pos > -1) {
527
pos--;
528
if (!foundSpace) {
529
if (propertyValue[pos] === ' ') {
530
foundSpace = true;
531
}
532
continue;
533
}
534
if (foundSpace && !foundEnd && propertyValue[pos] === ' ') {
535
continue;
536
}
537
if (!foundEnd) {
538
newSelectionEnd = pos + 1;
539
foundEnd = true;
540
continue;
541
}
542
if (propertyValue[pos] === ' ') {
543
newSelectionStart = pos + 1;
544
foundStart = true;
545
break;
546
}
547
}
548
549
if (foundEnd && !foundStart) {
550
newSelectionStart = 0;
551
}
552
553
return [newSelectionStart, newSelectionEnd];
554
}
555
556
export function getNodesInBetween(node1: FlatNode, node2: FlatNode): FlatNode[] {
557
// Same node
558
if (sameNodes(node1, node2)) {
559
return [node1];
560
}
561
562
// Not siblings
563
if (!sameNodes(node1.parent, node2.parent)) {
564
// node2 is ancestor of node1
565
if (node2.start < node1.start) {
566
return [node2];
567
}
568
569
// node1 is ancestor of node2
570
if (node2.start < node1.end) {
571
return [node1];
572
}
573
574
// Get the highest ancestor of node1 that should be commented
575
while (node1.parent && node1.parent.end < node2.start) {
576
node1 = node1.parent;
577
}
578
579
// Get the highest ancestor of node2 that should be commented
580
while (node2.parent && node2.parent.start > node1.start) {
581
node2 = node2.parent;
582
}
583
}
584
585
const siblings: FlatNode[] = [];
586
let currentNode: FlatNode | undefined = node1;
587
const position = node2.end;
588
while (currentNode && position > currentNode.start) {
589
siblings.push(currentNode);
590
currentNode = currentNode.nextSibling;
591
}
592
return siblings;
593
}
594
595
export function sameNodes(node1: FlatNode | undefined, node2: FlatNode | undefined): boolean {
596
// return true if they're both undefined
597
if (!node1 && !node2) {
598
return true;
599
}
600
// return false if only one of them is undefined
601
if (!node1 || !node2) {
602
return false;
603
}
604
return node1.start === node2.start && node1.end === node2.end;
605
}
606
607
export function getEmmetConfiguration(syntax: string) {
608
const emmetConfig = vscode.workspace.getConfiguration('emmet');
609
const syntaxProfiles = Object.assign({}, emmetConfig['syntaxProfiles'] || {});
610
const preferences = Object.assign({}, emmetConfig['preferences'] || {});
611
// jsx, xml and xsl syntaxes need to have self closing tags unless otherwise configured by user
612
if (syntax === 'jsx' || syntax === 'xml' || syntax === 'xsl') {
613
syntaxProfiles[syntax] = syntaxProfiles[syntax] || {};
614
if (typeof syntaxProfiles[syntax] === 'object'
615
&& !syntaxProfiles[syntax].hasOwnProperty('self_closing_tag') // Old Emmet format
616
&& !syntaxProfiles[syntax].hasOwnProperty('selfClosingStyle') // Emmet 2.0 format
617
) {
618
syntaxProfiles[syntax] = {
619
...syntaxProfiles[syntax],
620
selfClosingStyle: syntax === 'jsx' ? 'xhtml' : 'xml'
621
};
622
}
623
}
624
625
return {
626
preferences,
627
showExpandedAbbreviation: emmetConfig['showExpandedAbbreviation'],
628
showAbbreviationSuggestions: emmetConfig['showAbbreviationSuggestions'],
629
syntaxProfiles,
630
variables: emmetConfig['variables'],
631
excludeLanguages: emmetConfig['excludeLanguages'],
632
showSuggestionsAsSnippets: emmetConfig['showSuggestionsAsSnippets']
633
};
634
}
635
636
/**
637
* Itereates by each child, as well as nested child's children, in their order
638
* and invokes `fn` for each. If `fn` function returns `false`, iteration stops
639
*/
640
export function iterateCSSToken(token: FlatCssToken, fn: (x: any) => any): boolean {
641
for (let i = 0, il = token.size; i < il; i++) {
642
if (fn(token.item(i)) === false || iterateCSSToken(token.item(i), fn) === false) {
643
return false;
644
}
645
}
646
return true;
647
}
648
649
/**
650
* Returns `name` CSS property from given `rule`
651
*/
652
export function getCssPropertyFromRule(rule: FlatRule, name: string): FlatProperty | undefined {
653
return rule.children.find(node => node.type === 'property' && node.name === name) as FlatProperty;
654
}
655
656
/**
657
* Returns css property under caret in given editor or `null` if such node cannot
658
* be found
659
*/
660
export function getCssPropertyFromDocument(editor: vscode.TextEditor, position: vscode.Position): FlatProperty | null {
661
const document = editor.document;
662
const rootNode = getRootNode(document, true);
663
const offset = document.offsetAt(position);
664
const node = getFlatNode(rootNode, offset, true);
665
666
if (isStyleSheet(editor.document.languageId)) {
667
return node && node.type === 'property' ? <FlatProperty>node : null;
668
}
669
670
const htmlNode = <HtmlFlatNode>node;
671
if (htmlNode
672
&& htmlNode.name === 'style'
673
&& htmlNode.open && htmlNode.close
674
&& htmlNode.open.end < offset
675
&& htmlNode.close.start > offset) {
676
const buffer = ' '.repeat(htmlNode.start) +
677
document.getText().substring(htmlNode.start, htmlNode.end);
678
const innerRootNode = parseStylesheet(buffer);
679
const innerNode = getFlatNode(innerRootNode, offset, true);
680
return (innerNode && innerNode.type === 'property') ? <FlatProperty>innerNode : null;
681
}
682
683
return null;
684
}
685
686
687
export function getEmbeddedCssNodeIfAny(document: vscode.TextDocument, currentNode: FlatNode | undefined, position: vscode.Position): FlatNode | undefined {
688
if (!currentNode) {
689
return;
690
}
691
const currentHtmlNode = <HtmlFlatNode>currentNode;
692
if (currentHtmlNode && currentHtmlNode.open && currentHtmlNode.close) {
693
const offset = document.offsetAt(position);
694
if (currentHtmlNode.open.end < offset && offset <= currentHtmlNode.close.start) {
695
if (currentHtmlNode.name === 'style') {
696
const buffer = ' '.repeat(currentHtmlNode.open.end) + document.getText().substring(currentHtmlNode.open.end, currentHtmlNode.close.start);
697
return parseStylesheet(buffer);
698
}
699
}
700
}
701
return;
702
}
703
704
export function isStyleAttribute(currentNode: FlatNode | undefined, offset: number): boolean {
705
if (!currentNode) {
706
return false;
707
}
708
const currentHtmlNode = <HtmlFlatNode>currentNode;
709
const index = (currentHtmlNode.attributes || []).findIndex(x => x.name.toString() === 'style');
710
if (index === -1) {
711
return false;
712
}
713
const styleAttribute = currentHtmlNode.attributes[index];
714
return offset >= styleAttribute.value.start && offset <= styleAttribute.value.end;
715
}
716
717
export function isNumber(obj: any): obj is number {
718
return typeof obj === 'number';
719
}
720
721
export function toLSTextDocument(doc: vscode.TextDocument): LSTextDocument {
722
return LSTextDocument.create(doc.uri.toString(), doc.languageId, doc.version, doc.getText());
723
}
724
725
export function getPathBaseName(path: string): string {
726
const pathAfterSlashSplit = path.split('/').pop();
727
const pathAfterBackslashSplit = pathAfterSlashSplit ? pathAfterSlashSplit.split('\\').pop() : '';
728
return pathAfterBackslashSplit ?? '';
729
}
730
731
export function getSyntaxes() {
732
/**
733
* List of all known syntaxes, from emmetio/emmet
734
*/
735
return {
736
markup: ['html', 'xml', 'xsl', 'jsx', 'js', 'pug', 'slim', 'haml'],
737
stylesheet: ['css', 'sass', 'scss', 'less', 'sss', 'stylus']
738
};
739
}
740
741