Path: blob/main/extensions/emmet/src/updateImageSize.ts
4772 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45// Based on @sergeche's work on the emmet plugin for atom67import { TextEditor, Position, window, TextEdit } from 'vscode';8import * as path from 'path';9import { getImageSize, ImageInfoWithScale } from './imageSizeHelper';10import { getFlatNode, iterateCSSToken, getCssPropertyFromRule, isStyleSheet, validate, offsetRangeToVsRange } from './util';11import { HtmlNode, CssToken, HtmlToken, Attribute, Property } from 'EmmetFlatNode';12import { locateFile } from './locateFile';13import parseStylesheet from '@emmetio/css-parser';14import { getRootNode } from './parseDocument';1516/**17* Updates size of context image in given editor18*/19export function updateImageSize(): Promise<boolean> | undefined {20if (!validate() || !window.activeTextEditor) {21return;22}23const editor = window.activeTextEditor;2425const allUpdatesPromise = Array.from(editor.selections).reverse().map(selection => {26const position = selection.isReversed ? selection.active : selection.anchor;27if (!isStyleSheet(editor.document.languageId)) {28return updateImageSizeHTML(editor, position);29} else {30return updateImageSizeCSSFile(editor, position);31}32});3334return Promise.all(allUpdatesPromise).then((updates) => {35return editor.edit(builder => {36updates.forEach(update => {37update.forEach((textEdit: TextEdit) => {38builder.replace(textEdit.range, textEdit.newText);39});40});41});42});43}4445/**46* Updates image size of context tag of HTML model47*/48function updateImageSizeHTML(editor: TextEditor, position: Position): Promise<TextEdit[]> {49const imageNode = getImageHTMLNode(editor, position);5051const src = imageNode && getImageSrcHTML(imageNode);5253if (!src) {54return updateImageSizeStyleTag(editor, position);55}5657return locateFile(path.dirname(editor.document.fileName), src)58.then(getImageSize)59.then((size: any) => {60// since this action is asynchronous, we have to ensure that editor wasn't61// changed and user didn't moved caret outside <img> node62const img = getImageHTMLNode(editor, position);63if (img && getImageSrcHTML(img) === src) {64return updateHTMLTag(editor, img, size.width, size.height);65}66return [];67})68.catch(err => { console.warn('Error while updating image size:', err); return []; });69}7071function updateImageSizeStyleTag(editor: TextEditor, position: Position): Promise<TextEdit[]> {72const getPropertyInsiderStyleTag = (editor: TextEditor): Property | null => {73const document = editor.document;74const rootNode = getRootNode(document, true);75const offset = document.offsetAt(position);76const currentNode = <HtmlNode>getFlatNode(rootNode, offset, true);77if (currentNode && currentNode.name === 'style'78&& currentNode.open && currentNode.close79&& currentNode.open.end < offset80&& currentNode.close.start > offset) {81const buffer = ' '.repeat(currentNode.open.end) +82document.getText().substring(currentNode.open.end, currentNode.close.start);83const innerRootNode = parseStylesheet(buffer);84const innerNode = getFlatNode(innerRootNode, offset, true);85return (innerNode && innerNode.type === 'property') ? <Property>innerNode : null;86}87return null;88};8990return updateImageSizeCSS(editor, position, getPropertyInsiderStyleTag);91}9293function updateImageSizeCSSFile(editor: TextEditor, position: Position): Promise<TextEdit[]> {94return updateImageSizeCSS(editor, position, getImageCSSNode);95}9697/**98* Updates image size of context rule of stylesheet model99*/100function updateImageSizeCSS(editor: TextEditor, position: Position, fetchNode: (editor: TextEditor, position: Position) => Property | null): Promise<TextEdit[]> {101const node = fetchNode(editor, position);102const src = node && getImageSrcCSS(editor, node, position);103104if (!src) {105return Promise.reject(new Error('No valid image source'));106}107108return locateFile(path.dirname(editor.document.fileName), src)109.then(getImageSize)110.then((size: ImageInfoWithScale | undefined): TextEdit[] => {111// since this action is asynchronous, we have to ensure that editor wasn't112// changed and user didn't moved caret outside <img> node113const prop = fetchNode(editor, position);114if (size && prop && getImageSrcCSS(editor, prop, position) === src) {115return updateCSSNode(editor, prop, size.width, size.height);116}117return [];118})119.catch(err => { console.warn('Error while updating image size:', err); return []; });120}121122/**123* Returns <img> node under caret in given editor or `null` if such node cannot124* be found125*/126function getImageHTMLNode(editor: TextEditor, position: Position): HtmlNode | null {127const document = editor.document;128const rootNode = getRootNode(document, true);129const offset = document.offsetAt(position);130const node = <HtmlNode>getFlatNode(rootNode, offset, true);131132return node && node.name.toLowerCase() === 'img' ? node : null;133}134135/**136* Returns css property under caret in given editor or `null` if such node cannot137* be found138*/139function getImageCSSNode(editor: TextEditor, position: Position): Property | null {140const document = editor.document;141const rootNode = getRootNode(document, true);142const offset = document.offsetAt(position);143const node = getFlatNode(rootNode, offset, true);144return node && node.type === 'property' ? <Property>node : null;145}146147/**148* Returns image source from given <img> node149*/150function getImageSrcHTML(node: HtmlNode): string | undefined {151const srcAttr = getAttribute(node, 'src');152if (!srcAttr) {153return;154}155156return (<HtmlToken>srcAttr.value).value;157}158159/**160* Returns image source from given `url()` token161*/162function getImageSrcCSS(editor: TextEditor, node: Property | undefined, position: Position): string | undefined {163if (!node) {164return;165}166const urlToken = findUrlToken(editor, node, position);167if (!urlToken) {168return;169}170171// A stylesheet token may contain either quoted ('string') or unquoted URL172let urlValue = urlToken.item(0);173if (urlValue && urlValue.type === 'string') {174urlValue = urlValue.item(0);175}176177return urlValue && urlValue.valueOf();178}179180/**181* Updates size of given HTML node182*/183function updateHTMLTag(editor: TextEditor, node: HtmlNode, width: number, height: number): TextEdit[] {184const document = editor.document;185const srcAttr = getAttribute(node, 'src');186if (!srcAttr) {187return [];188}189190const widthAttr = getAttribute(node, 'width');191const heightAttr = getAttribute(node, 'height');192const quote = getAttributeQuote(editor, srcAttr);193const endOfAttributes = node.attributes[node.attributes.length - 1].end;194195const edits: TextEdit[] = [];196let textToAdd = '';197198if (!widthAttr) {199textToAdd += ` width=${quote}${width}${quote}`;200} else {201edits.push(new TextEdit(offsetRangeToVsRange(document, widthAttr.value.start, widthAttr.value.end), String(width)));202}203if (!heightAttr) {204textToAdd += ` height=${quote}${height}${quote}`;205} else {206edits.push(new TextEdit(offsetRangeToVsRange(document, heightAttr.value.start, heightAttr.value.end), String(height)));207}208if (textToAdd) {209edits.push(new TextEdit(offsetRangeToVsRange(document, endOfAttributes, endOfAttributes), textToAdd));210}211212return edits;213}214215/**216* Updates size of given CSS rule217*/218function updateCSSNode(editor: TextEditor, srcProp: Property, width: number, height: number): TextEdit[] {219const document = editor.document;220const rule = srcProp.parent;221const widthProp = getCssPropertyFromRule(rule, 'width');222const heightProp = getCssPropertyFromRule(rule, 'height');223224// Detect formatting225const separator = srcProp.separator || ': ';226const before = getPropertyDelimitor(editor, srcProp);227228const edits: TextEdit[] = [];229if (!srcProp.terminatorToken) {230edits.push(new TextEdit(offsetRangeToVsRange(document, srcProp.end, srcProp.end), ';'));231}232233let textToAdd = '';234if (!widthProp) {235textToAdd += `${before}width${separator}${width}px;`;236} else {237edits.push(new TextEdit(offsetRangeToVsRange(document, widthProp.valueToken.start, widthProp.valueToken.end), `${width}px`));238}239if (!heightProp) {240textToAdd += `${before}height${separator}${height}px;`;241} else {242edits.push(new TextEdit(offsetRangeToVsRange(document, heightProp.valueToken.start, heightProp.valueToken.end), `${height}px`));243}244if (textToAdd) {245edits.push(new TextEdit(offsetRangeToVsRange(document, srcProp.end, srcProp.end), textToAdd));246}247248return edits;249}250251/**252* Returns attribute object with `attrName` name from given HTML node253*/254function getAttribute(node: HtmlNode, attrName: string): Attribute | undefined {255attrName = attrName.toLowerCase();256return node && node.attributes.find(attr => attr.name.toString().toLowerCase() === attrName);257}258259/**260* Returns quote character, used for value of given attribute. May return empty261* string if attribute wasn't quoted262263*/264function getAttributeQuote(editor: TextEditor, attr: Attribute): string {265const begin = attr.value ? attr.value.end : attr.end;266const end = attr.end;267return begin === end ? '' : editor.document.getText().substring(begin, end);268}269270/**271* Finds 'url' token for given `pos` point in given CSS property `node`272*/273function findUrlToken(editor: TextEditor, node: Property, pos: Position): CssToken | undefined {274const offset = editor.document.offsetAt(pos);275if (!('parsedValue' in node) || !Array.isArray(node.parsedValue)) {276return undefined;277}278279for (let i = 0, il = node.parsedValue.length, url; i < il; i++) {280iterateCSSToken(node.parsedValue[i], (token: CssToken) => {281if (token.type === 'url' && token.start <= offset && token.end >= offset) {282url = token;283return false;284}285return true;286});287288if (url) {289return url;290}291}292return undefined;293}294295/**296* Returns a string that is used to delimit properties in current node's rule297*/298function getPropertyDelimitor(editor: TextEditor, node: Property): string {299let anchor;300if (anchor = (node.previousSibling || node.parent.contentStartToken)) {301return editor.document.getText().substring(anchor.end, node.start);302} else if (anchor = (node.nextSibling || node.parent.contentEndToken)) {303return editor.document.getText().substring(node.end, anchor.start);304}305306return '';307}308309310311