code-server/lib/vscode/extensions/emmet/src/updateImageSize.ts

290 lines
9.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// Based on @sergeche's work on the emmet plugin for atom
import { TextEditor, Range, Position, window, TextEdit } from 'vscode';
import * as path from 'path';
import { getImageSize } from './imageSizeHelper';
import { parseDocument, getNode, iterateCSSToken, getCssPropertyFromRule, isStyleSheet, validate } from './util';
import { HtmlNode, CssToken, HtmlToken, Attribute, Property } from 'EmmetNode';
import { locateFile } from './locateFile';
import parseStylesheet from '@emmetio/css-parser';
import { DocumentStreamReader } from './bufferStream';
/**
* Updates size of context image in given editor
*/
export function updateImageSize() {
if (!validate() || !window.activeTextEditor) {
return;
}
const editor = window.activeTextEditor;
let allUpdatesPromise = editor.selections.reverse().map(selection => {
let position = selection.isReversed ? selection.active : selection.anchor;
if (!isStyleSheet(editor.document.languageId)) {
return updateImageSizeHTML(editor, position);
} else {
return updateImageSizeCSSFile(editor, position);
}
});
return Promise.all(allUpdatesPromise).then((updates) => {
return editor.edit(builder => {
updates.forEach(update => {
update.forEach((textEdit: TextEdit) => {
builder.replace(textEdit.range, textEdit.newText);
});
});
});
});
}
/**
* Updates image size of context tag of HTML model
*/
function updateImageSizeHTML(editor: TextEditor, position: Position): Promise<TextEdit[]> {
const imageNode = getImageHTMLNode(editor, position);
const src = imageNode && getImageSrcHTML(imageNode);
if (!src) {
return updateImageSizeStyleTag(editor, position);
}
return locateFile(path.dirname(editor.document.fileName), src)
.then(getImageSize)
.then((size: any) => {
// since this action is asynchronous, we have to ensure that editor wasnt
// changed and user didnt moved caret outside <img> node
const img = getImageHTMLNode(editor, position);
if (img && getImageSrcHTML(img) === src) {
return updateHTMLTag(editor, img, size.width, size.height);
}
return [];
})
.catch(err => { console.warn('Error while updating image size:', err); return []; });
}
function updateImageSizeStyleTag(editor: TextEditor, position: Position): Promise<TextEdit[]> {
const getPropertyInsiderStyleTag = (editor: TextEditor): Property | null => {
const rootNode = parseDocument(editor.document);
const currentNode = <HtmlNode>getNode(rootNode, position, true);
if (currentNode && currentNode.name === 'style'
&& currentNode.open.end.isBefore(position)
&& currentNode.close.start.isAfter(position)) {
let buffer = new DocumentStreamReader(editor.document, currentNode.open.end, new Range(currentNode.open.end, currentNode.close.start));
let rootNode = parseStylesheet(buffer);
const node = getNode(rootNode, position, true);
return (node && node.type === 'property') ? <Property>node : null;
}
return null;
};
return updateImageSizeCSS(editor, position, getPropertyInsiderStyleTag);
}
function updateImageSizeCSSFile(editor: TextEditor, position: Position): Promise<TextEdit[]> {
return updateImageSizeCSS(editor, position, getImageCSSNode);
}
/**
* Updates image size of context rule of stylesheet model
*/
function updateImageSizeCSS(editor: TextEditor, position: Position, fetchNode: (editor: TextEditor, position: Position) => Property | null): Promise<TextEdit[]> {
const node = fetchNode(editor, position);
const src = node && getImageSrcCSS(node, position);
if (!src) {
return Promise.reject(new Error('No valid image source'));
}
return locateFile(path.dirname(editor.document.fileName), src)
.then(getImageSize)
.then((size: any): TextEdit[] => {
// since this action is asynchronous, we have to ensure that editor wasnt
// changed and user didnt moved caret outside <img> node
const prop = fetchNode(editor, position);
if (prop && getImageSrcCSS(prop, position) === src) {
return updateCSSNode(editor, prop, size.width, size.height);
}
return [];
})
.catch(err => { console.warn('Error while updating image size:', err); return []; });
}
/**
* Returns <img> node under caret in given editor or `null` if such node cannot
* be found
*/
function getImageHTMLNode(editor: TextEditor, position: Position): HtmlNode | null {
const rootNode = parseDocument(editor.document);
const node = <HtmlNode>getNode(rootNode, position, true);
return node && node.name.toLowerCase() === 'img' ? node : null;
}
/**
* Returns css property under caret in given editor or `null` if such node cannot
* be found
*/
function getImageCSSNode(editor: TextEditor, position: Position): Property | null {
const rootNode = parseDocument(editor.document);
const node = getNode(rootNode, position, true);
return node && node.type === 'property' ? <Property>node : null;
}
/**
* Returns image source from given <img> node
*/
function getImageSrcHTML(node: HtmlNode): string | undefined {
const srcAttr = getAttribute(node, 'src');
if (!srcAttr) {
return;
}
return (<HtmlToken>srcAttr.value).value;
}
/**
* Returns image source from given `url()` token
*/
function getImageSrcCSS(node: Property | undefined, position: Position): string | undefined {
if (!node) {
return;
}
const urlToken = findUrlToken(node, position);
if (!urlToken) {
return;
}
// A stylesheet token may contain either quoted ('string') or unquoted URL
let urlValue = urlToken.item(0);
if (urlValue && urlValue.type === 'string') {
urlValue = urlValue.item(0);
}
return urlValue && urlValue.valueOf();
}
/**
* Updates size of given HTML node
*/
function updateHTMLTag(editor: TextEditor, node: HtmlNode, width: number, height: number): TextEdit[] {
const srcAttr = getAttribute(node, 'src');
const widthAttr = getAttribute(node, 'width');
const heightAttr = getAttribute(node, 'height');
const quote = getAttributeQuote(editor, srcAttr);
const endOfAttributes = node.attributes[node.attributes.length - 1].end;
let edits: TextEdit[] = [];
let textToAdd = '';
if (!widthAttr) {
textToAdd += ` width=${quote}${width}${quote}`;
} else {
edits.push(new TextEdit(new Range(widthAttr.value.start, widthAttr.value.end), String(width)));
}
if (!heightAttr) {
textToAdd += ` height=${quote}${height}${quote}`;
} else {
edits.push(new TextEdit(new Range(heightAttr.value.start, heightAttr.value.end), String(height)));
}
if (textToAdd) {
edits.push(new TextEdit(new Range(endOfAttributes, endOfAttributes), textToAdd));
}
return edits;
}
/**
* Updates size of given CSS rule
*/
function updateCSSNode(editor: TextEditor, srcProp: Property, width: number, height: number): TextEdit[] {
const rule = srcProp.parent;
const widthProp = getCssPropertyFromRule(rule, 'width');
const heightProp = getCssPropertyFromRule(rule, 'height');
// Detect formatting
const separator = srcProp.separator || ': ';
const before = getPropertyDelimitor(editor, srcProp);
let edits: TextEdit[] = [];
if (!srcProp.terminatorToken) {
edits.push(new TextEdit(new Range(srcProp.end, srcProp.end), ';'));
}
let textToAdd = '';
if (!widthProp) {
textToAdd += `${before}width${separator}${width}px;`;
} else {
edits.push(new TextEdit(new Range(widthProp.valueToken.start, widthProp.valueToken.end), `${width}px`));
}
if (!heightProp) {
textToAdd += `${before}height${separator}${height}px;`;
} else {
edits.push(new TextEdit(new Range(heightProp.valueToken.start, heightProp.valueToken.end), `${height}px`));
}
if (textToAdd) {
edits.push(new TextEdit(new Range(srcProp.end, srcProp.end), textToAdd));
}
return edits;
}
/**
* Returns attribute object with `attrName` name from given HTML node
*/
function getAttribute(node: HtmlNode, attrName: string): Attribute {
attrName = attrName.toLowerCase();
return node && (node.open as any).attributes.find((attr: any) => attr.name.value.toLowerCase() === attrName);
}
/**
* Returns quote character, used for value of given attribute. May return empty
* string if attribute wasnt quoted
*/
function getAttributeQuote(editor: TextEditor, attr: any): string {
const range = new Range(attr.value ? attr.value.end : attr.end, attr.end);
return range.isEmpty ? '' : editor.document.getText(range);
}
/**
* Finds 'url' token for given `pos` point in given CSS property `node`
*/
function findUrlToken(node: Property, pos: Position): CssToken | undefined {
for (let i = 0, il = (node as any).parsedValue.length, url; i < il; i++) {
iterateCSSToken((node as any).parsedValue[i], (token: CssToken) => {
if (token.type === 'url' && token.start.isBeforeOrEqual(pos) && token.end.isAfterOrEqual(pos)) {
url = token;
return false;
}
return true;
});
if (url) {
return url;
}
}
return;
}
/**
* Returns a string that is used to delimit properties in current nodes rule
*/
function getPropertyDelimitor(editor: TextEditor, node: Property): string {
let anchor;
if (anchor = (node.previousSibling || node.parent.contentStartToken)) {
return editor.document.getText(new Range(anchor.end, node.start));
} else if (anchor = (node.nextSibling || node.parent.contentEndToken)) {
return editor.document.getText(new Range(node.end, anchor.start));
}
return '';
}