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

306 lines
10 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, Position, window, TextEdit } from 'vscode';
import * as path from 'path';
import { getImageSize } from './imageSizeHelper';
import { getFlatNode, iterateCSSToken, getCssPropertyFromRule, isStyleSheet, validate, offsetRangeToVsRange } from './util';
import { HtmlNode, CssToken, HtmlToken, Attribute, Property } from 'EmmetFlatNode';
import { locateFile } from './locateFile';
import parseStylesheet from '@emmetio/css-parser';
import { getRootNode } from './parseDocument';
/**
* Updates size of context image in given editor
*/
export function updateImageSize(): Promise<boolean> | undefined {
if (!validate() || !window.activeTextEditor) {
return;
}
const editor = window.activeTextEditor;
const allUpdatesPromise = editor.selections.reverse().map(selection => {
const 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 document = editor.document;
const rootNode = getRootNode(document, true);
const offset = document.offsetAt(position);
const currentNode = <HtmlNode>getFlatNode(rootNode, offset, true);
if (currentNode && currentNode.name === 'style'
&& currentNode.open && currentNode.close
&& currentNode.open.end < offset
&& currentNode.close.start > offset) {
const buffer = ' '.repeat(currentNode.open.end) +
document.getText().substring(currentNode.open.end, currentNode.close.start);
const innerRootNode = parseStylesheet(buffer);
const innerNode = getFlatNode(innerRootNode, offset, true);
return (innerNode && innerNode.type === 'property') ? <Property>innerNode : 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(editor, 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(editor, 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 document = editor.document;
const rootNode = getRootNode(document, true);
const offset = document.offsetAt(position);
const node = <HtmlNode>getFlatNode(rootNode, offset, 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 document = editor.document;
const rootNode = getRootNode(document, true);
const offset = document.offsetAt(position);
const node = getFlatNode(rootNode, offset, 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(editor: TextEditor, node: Property | undefined, position: Position): string | undefined {
if (!node) {
return;
}
const urlToken = findUrlToken(editor, 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 document = editor.document;
const srcAttr = getAttribute(node, 'src');
if (!srcAttr) {
return [];
}
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(offsetRangeToVsRange(document, widthAttr.value.start, widthAttr.value.end), String(width)));
}
if (!heightAttr) {
textToAdd += ` height=${quote}${height}${quote}`;
} else {
edits.push(new TextEdit(offsetRangeToVsRange(document, heightAttr.value.start, heightAttr.value.end), String(height)));
}
if (textToAdd) {
edits.push(new TextEdit(offsetRangeToVsRange(document, endOfAttributes, endOfAttributes), textToAdd));
}
return edits;
}
/**
* Updates size of given CSS rule
*/
function updateCSSNode(editor: TextEditor, srcProp: Property, width: number, height: number): TextEdit[] {
const document = editor.document;
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(offsetRangeToVsRange(document, srcProp.end, srcProp.end), ';'));
}
let textToAdd = '';
if (!widthProp) {
textToAdd += `${before}width${separator}${width}px;`;
} else {
edits.push(new TextEdit(offsetRangeToVsRange(document, widthProp.valueToken.start, widthProp.valueToken.end), `${width}px`));
}
if (!heightProp) {
textToAdd += `${before}height${separator}${height}px;`;
} else {
edits.push(new TextEdit(offsetRangeToVsRange(document, heightProp.valueToken.start, heightProp.valueToken.end), `${height}px`));
}
if (textToAdd) {
edits.push(new TextEdit(offsetRangeToVsRange(document, srcProp.end, srcProp.end), textToAdd));
}
return edits;
}
/**
* Returns attribute object with `attrName` name from given HTML node
*/
function getAttribute(node: HtmlNode, attrName: string): Attribute | undefined {
attrName = attrName.toLowerCase();
return node && node.attributes.find(attr => attr.name.toString().toLowerCase() === attrName);
}
/**
* Returns quote character, used for value of given attribute. May return empty
* string if attribute wasnt quoted
*/
function getAttributeQuote(editor: TextEditor, attr: Attribute): string {
const begin = attr.value ? attr.value.end : attr.end;
const end = attr.end;
return begin === end ? '' : editor.document.getText().substring(begin, end);
}
/**
* Finds 'url' token for given `pos` point in given CSS property `node`
*/
function findUrlToken(editor: TextEditor, node: Property, pos: Position): CssToken | undefined {
const offset = editor.document.offsetAt(pos);
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 <= offset && token.end >= offset) {
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().substring(anchor.end, node.start);
} else if (anchor = (node.nextSibling || node.parent.contentEndToken)) {
return editor.document.getText().substring(node.end, anchor.start);
}
return '';
}