Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/emmet/src/updateImageSize.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
// Based on @sergeche's work on the emmet plugin for atom
7
8
import { TextEditor, Position, window, TextEdit } from 'vscode';
9
import * as path from 'path';
10
import { getImageSize, ImageInfoWithScale } from './imageSizeHelper';
11
import { getFlatNode, iterateCSSToken, getCssPropertyFromRule, isStyleSheet, validate, offsetRangeToVsRange } from './util';
12
import { HtmlNode, CssToken, HtmlToken, Attribute, Property } from 'EmmetFlatNode';
13
import { locateFile } from './locateFile';
14
import parseStylesheet from '@emmetio/css-parser';
15
import { getRootNode } from './parseDocument';
16
17
/**
18
* Updates size of context image in given editor
19
*/
20
export function updateImageSize(): Promise<boolean> | undefined {
21
if (!validate() || !window.activeTextEditor) {
22
return;
23
}
24
const editor = window.activeTextEditor;
25
26
const allUpdatesPromise = Array.from(editor.selections).reverse().map(selection => {
27
const position = selection.isReversed ? selection.active : selection.anchor;
28
if (!isStyleSheet(editor.document.languageId)) {
29
return updateImageSizeHTML(editor, position);
30
} else {
31
return updateImageSizeCSSFile(editor, position);
32
}
33
});
34
35
return Promise.all(allUpdatesPromise).then((updates) => {
36
return editor.edit(builder => {
37
updates.forEach(update => {
38
update.forEach((textEdit: TextEdit) => {
39
builder.replace(textEdit.range, textEdit.newText);
40
});
41
});
42
});
43
});
44
}
45
46
/**
47
* Updates image size of context tag of HTML model
48
*/
49
function updateImageSizeHTML(editor: TextEditor, position: Position): Promise<TextEdit[]> {
50
const imageNode = getImageHTMLNode(editor, position);
51
52
const src = imageNode && getImageSrcHTML(imageNode);
53
54
if (!src) {
55
return updateImageSizeStyleTag(editor, position);
56
}
57
58
return locateFile(path.dirname(editor.document.fileName), src)
59
.then(getImageSize)
60
.then((size: any) => {
61
// since this action is asynchronous, we have to ensure that editor wasn't
62
// changed and user didn't moved caret outside <img> node
63
const img = getImageHTMLNode(editor, position);
64
if (img && getImageSrcHTML(img) === src) {
65
return updateHTMLTag(editor, img, size.width, size.height);
66
}
67
return [];
68
})
69
.catch(err => { console.warn('Error while updating image size:', err); return []; });
70
}
71
72
function updateImageSizeStyleTag(editor: TextEditor, position: Position): Promise<TextEdit[]> {
73
const getPropertyInsiderStyleTag = (editor: TextEditor): Property | null => {
74
const document = editor.document;
75
const rootNode = getRootNode(document, true);
76
const offset = document.offsetAt(position);
77
const currentNode = <HtmlNode>getFlatNode(rootNode, offset, true);
78
if (currentNode && currentNode.name === 'style'
79
&& currentNode.open && currentNode.close
80
&& currentNode.open.end < offset
81
&& currentNode.close.start > offset) {
82
const buffer = ' '.repeat(currentNode.open.end) +
83
document.getText().substring(currentNode.open.end, currentNode.close.start);
84
const innerRootNode = parseStylesheet(buffer);
85
const innerNode = getFlatNode(innerRootNode, offset, true);
86
return (innerNode && innerNode.type === 'property') ? <Property>innerNode : null;
87
}
88
return null;
89
};
90
91
return updateImageSizeCSS(editor, position, getPropertyInsiderStyleTag);
92
}
93
94
function updateImageSizeCSSFile(editor: TextEditor, position: Position): Promise<TextEdit[]> {
95
return updateImageSizeCSS(editor, position, getImageCSSNode);
96
}
97
98
/**
99
* Updates image size of context rule of stylesheet model
100
*/
101
function updateImageSizeCSS(editor: TextEditor, position: Position, fetchNode: (editor: TextEditor, position: Position) => Property | null): Promise<TextEdit[]> {
102
const node = fetchNode(editor, position);
103
const src = node && getImageSrcCSS(editor, node, position);
104
105
if (!src) {
106
return Promise.reject(new Error('No valid image source'));
107
}
108
109
return locateFile(path.dirname(editor.document.fileName), src)
110
.then(getImageSize)
111
.then((size: ImageInfoWithScale | undefined): TextEdit[] => {
112
// since this action is asynchronous, we have to ensure that editor wasn't
113
// changed and user didn't moved caret outside <img> node
114
const prop = fetchNode(editor, position);
115
if (size && prop && getImageSrcCSS(editor, prop, position) === src) {
116
return updateCSSNode(editor, prop, size.width, size.height);
117
}
118
return [];
119
})
120
.catch(err => { console.warn('Error while updating image size:', err); return []; });
121
}
122
123
/**
124
* Returns <img> node under caret in given editor or `null` if such node cannot
125
* be found
126
*/
127
function getImageHTMLNode(editor: TextEditor, position: Position): HtmlNode | null {
128
const document = editor.document;
129
const rootNode = getRootNode(document, true);
130
const offset = document.offsetAt(position);
131
const node = <HtmlNode>getFlatNode(rootNode, offset, true);
132
133
return node && node.name.toLowerCase() === 'img' ? node : null;
134
}
135
136
/**
137
* Returns css property under caret in given editor or `null` if such node cannot
138
* be found
139
*/
140
function getImageCSSNode(editor: TextEditor, position: Position): Property | null {
141
const document = editor.document;
142
const rootNode = getRootNode(document, true);
143
const offset = document.offsetAt(position);
144
const node = getFlatNode(rootNode, offset, true);
145
return node && node.type === 'property' ? <Property>node : null;
146
}
147
148
/**
149
* Returns image source from given <img> node
150
*/
151
function getImageSrcHTML(node: HtmlNode): string | undefined {
152
const srcAttr = getAttribute(node, 'src');
153
if (!srcAttr) {
154
return;
155
}
156
157
return (<HtmlToken>srcAttr.value).value;
158
}
159
160
/**
161
* Returns image source from given `url()` token
162
*/
163
function getImageSrcCSS(editor: TextEditor, node: Property | undefined, position: Position): string | undefined {
164
if (!node) {
165
return;
166
}
167
const urlToken = findUrlToken(editor, node, position);
168
if (!urlToken) {
169
return;
170
}
171
172
// A stylesheet token may contain either quoted ('string') or unquoted URL
173
let urlValue = urlToken.item(0);
174
if (urlValue && urlValue.type === 'string') {
175
urlValue = urlValue.item(0);
176
}
177
178
return urlValue && urlValue.valueOf();
179
}
180
181
/**
182
* Updates size of given HTML node
183
*/
184
function updateHTMLTag(editor: TextEditor, node: HtmlNode, width: number, height: number): TextEdit[] {
185
const document = editor.document;
186
const srcAttr = getAttribute(node, 'src');
187
if (!srcAttr) {
188
return [];
189
}
190
191
const widthAttr = getAttribute(node, 'width');
192
const heightAttr = getAttribute(node, 'height');
193
const quote = getAttributeQuote(editor, srcAttr);
194
const endOfAttributes = node.attributes[node.attributes.length - 1].end;
195
196
const edits: TextEdit[] = [];
197
let textToAdd = '';
198
199
if (!widthAttr) {
200
textToAdd += ` width=${quote}${width}${quote}`;
201
} else {
202
edits.push(new TextEdit(offsetRangeToVsRange(document, widthAttr.value.start, widthAttr.value.end), String(width)));
203
}
204
if (!heightAttr) {
205
textToAdd += ` height=${quote}${height}${quote}`;
206
} else {
207
edits.push(new TextEdit(offsetRangeToVsRange(document, heightAttr.value.start, heightAttr.value.end), String(height)));
208
}
209
if (textToAdd) {
210
edits.push(new TextEdit(offsetRangeToVsRange(document, endOfAttributes, endOfAttributes), textToAdd));
211
}
212
213
return edits;
214
}
215
216
/**
217
* Updates size of given CSS rule
218
*/
219
function updateCSSNode(editor: TextEditor, srcProp: Property, width: number, height: number): TextEdit[] {
220
const document = editor.document;
221
const rule = srcProp.parent;
222
const widthProp = getCssPropertyFromRule(rule, 'width');
223
const heightProp = getCssPropertyFromRule(rule, 'height');
224
225
// Detect formatting
226
const separator = srcProp.separator || ': ';
227
const before = getPropertyDelimitor(editor, srcProp);
228
229
const edits: TextEdit[] = [];
230
if (!srcProp.terminatorToken) {
231
edits.push(new TextEdit(offsetRangeToVsRange(document, srcProp.end, srcProp.end), ';'));
232
}
233
234
let textToAdd = '';
235
if (!widthProp) {
236
textToAdd += `${before}width${separator}${width}px;`;
237
} else {
238
edits.push(new TextEdit(offsetRangeToVsRange(document, widthProp.valueToken.start, widthProp.valueToken.end), `${width}px`));
239
}
240
if (!heightProp) {
241
textToAdd += `${before}height${separator}${height}px;`;
242
} else {
243
edits.push(new TextEdit(offsetRangeToVsRange(document, heightProp.valueToken.start, heightProp.valueToken.end), `${height}px`));
244
}
245
if (textToAdd) {
246
edits.push(new TextEdit(offsetRangeToVsRange(document, srcProp.end, srcProp.end), textToAdd));
247
}
248
249
return edits;
250
}
251
252
/**
253
* Returns attribute object with `attrName` name from given HTML node
254
*/
255
function getAttribute(node: HtmlNode, attrName: string): Attribute | undefined {
256
attrName = attrName.toLowerCase();
257
return node && node.attributes.find(attr => attr.name.toString().toLowerCase() === attrName);
258
}
259
260
/**
261
* Returns quote character, used for value of given attribute. May return empty
262
* string if attribute wasn't quoted
263
264
*/
265
function getAttributeQuote(editor: TextEditor, attr: Attribute): string {
266
const begin = attr.value ? attr.value.end : attr.end;
267
const end = attr.end;
268
return begin === end ? '' : editor.document.getText().substring(begin, end);
269
}
270
271
/**
272
* Finds 'url' token for given `pos` point in given CSS property `node`
273
*/
274
function findUrlToken(editor: TextEditor, node: Property, pos: Position): CssToken | undefined {
275
const offset = editor.document.offsetAt(pos);
276
if (!('parsedValue' in node) || !Array.isArray(node.parsedValue)) {
277
return undefined;
278
}
279
280
for (let i = 0, il = node.parsedValue.length, url; i < il; i++) {
281
iterateCSSToken(node.parsedValue[i], (token: CssToken) => {
282
if (token.type === 'url' && token.start <= offset && token.end >= offset) {
283
url = token;
284
return false;
285
}
286
return true;
287
});
288
289
if (url) {
290
return url;
291
}
292
}
293
return undefined;
294
}
295
296
/**
297
* Returns a string that is used to delimit properties in current node's rule
298
*/
299
function getPropertyDelimitor(editor: TextEditor, node: Property): string {
300
let anchor;
301
if (anchor = (node.previousSibling || node.parent.contentStartToken)) {
302
return editor.document.getText().substring(anchor.end, node.start);
303
} else if (anchor = (node.nextSibling || node.parent.contentEndToken)) {
304
return editor.document.getText().substring(node.end, anchor.start);
305
}
306
307
return '';
308
}
309
310
311