提交 0bee43b5 编写于 作者: R Ramya Achutha Rao

Emmet Update Image Size command

上级 caa0ef4e
......@@ -75,6 +75,7 @@
"@emmetio/css-parser": "^0.3.0",
"@emmetio/math-expression": "^0.1.1",
"vscode-emmet-helper":"0.0.28",
"vscode-languageserver-types": "^3.0.3"
"vscode-languageserver-types": "^3.0.3",
"image-size": "^0.5.2"
}
}
\ No newline at end of file
......@@ -19,6 +19,7 @@ import { evaluateMathExpression } from './evaluateMathExpression';
import { incrementDecrement } from './incrementDecrement';
import { LANGUAGE_MODES, getMappingForIncludedLanguages } from './util';
import { updateExtensionsPath } from 'vscode-emmet-helper';
import { updateImageSize } from './updateImageSize';
import * as path from 'path';
export function activate(context: vscode.ExtensionContext) {
......@@ -113,6 +114,10 @@ export function activate(context: vscode.ExtensionContext) {
return incrementDecrement(-10);
}));
context.subscriptions.push(vscode.commands.registerCommand('emmet.updateImageSize', () => {
return updateImageSize();
}));
let currentExtensionsPath = undefined;
let resolveUpdateExtensionsPath = () => {
let extensionsPath = vscode.workspace.getConfiguration('emmet')['extensionsPath'];
......
/*---------------------------------------------------------------------------------------------
* 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
// TODO: Move to https://github.com/emmetio/image-size
'use strict';
import * as path from 'path';
import * as http from 'http';
import * as https from 'https';
import { parse as parseUrl } from 'url';
import * as sizeOf from 'image-size';
const reUrl = /^https?:/;
/**
* Get size of given image file. Supports files from local filesystem,
* as well as URLs
* @param {String} file Path to local file or URL
* @return {Promise}
*/
export function getImageSize(file) {
file = file.replace(/^file:\/\//, '');
return reUrl.test(file) ? getImageSizeFromURL(file) : getImageSizeFromFile(file);
}
/**
* Get image size from file on local file system
* @param {String} file
* @return {Promise}
*/
function getImageSizeFromFile(file) {
return new Promise((resolve, reject) => {
const isDataUrl = file.match(/^data:.+?;base64,/);
if (isDataUrl) {
// NB should use sync version of `sizeOf()` for buffers
try {
const data = Buffer.from(file.slice(isDataUrl[0].length), 'base64');
return resolve(sizeForFileName('', sizeOf(data)));
} catch (err) {
return reject(err);
}
}
sizeOf(file, (err, size) => {
if (err) {
reject(err);
} else {
resolve(sizeForFileName(path.basename(file), size));
}
});
});
}
/**
* Get image size from given remove URL
* @param {String} url
* @return {Promise}
*/
function getImageSizeFromURL(url) {
return new Promise((resolve, reject) => {
url = parseUrl(url);
const getTransport = url.protocol === 'https:' ? https.get : http.get;
getTransport(url, resp => {
const chunks = [];
let bufSize = 0;
const trySize = chunks => {
try {
const size = sizeOf(Buffer.concat(chunks, bufSize));
resp.removeListener('data', onData);
resp.destroy(); // no need to read further
resolve(sizeForFileName(path.basename(url.pathname), size));
} catch (err) {
// might not have enough data, skip error
}
};
const onData = chunk => {
bufSize += chunk.length;
chunks.push(chunk);
trySize(chunks);
};
resp
.on('data', onData)
.on('end', () => trySize(chunks))
.once('error', err => {
resp.removeListener('data', onData);
reject(err);
});
})
.once('error', reject);
});
}
/**
* Returns size object for given file name. If file name contains `@Nx` token,
* the final dimentions will be downscaled by N
* @param {String} fileName
* @param {Object} size
* @return {Object}
*/
function sizeForFileName(fileName, size) {
const m = fileName.match(/@(\d+)x\./);
const scale = m ? +m[1] : 1;
return {
realWidth: size.width,
realHeight: size.height,
width: Math.floor(size.width / scale),
height: Math.floor(size.height / scale)
};
}
/*---------------------------------------------------------------------------------------------
* 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
// TODO: Move to https://github.com/emmetio/file-utils
'use strict';
import * as path from 'path';
import * as fs from 'fs';
const reAbsolute = /^\/+/;
/**
* Locates given `filePath` on user’s file system and returns absolute path to it.
* This method expects either URL, or relative/absolute path to resource
* @param {String} basePath Base path to use if filePath is not absoulte
* @param {String} filePath File to locate.
* @return {Promise}
*/
export function locateFile(base: string, filePath: string): Promise<string> {
if (/^\w+:/.test(filePath)) {
// path with protocol, already absolute
return Promise.resolve(filePath);
}
filePath = path.normalize(filePath);
return reAbsolute.test(filePath)
? resolveAbsolute(base, filePath)
: resolveRelative(base, filePath);
}
/**
* Resolves relative file path
* @param {TextEditor|String} base
* @param {String} filePath
* @return {Promise}
*/
function resolveRelative(basePath, filePath): Promise<string> {
return tryFile(path.resolve(basePath, filePath));
}
/**
* Resolves absolute file path agaist given editor: tries to find file in every
* parent of editor’s file
* @param {TextEditor|String} base
* @param {String} filePath
* @return {Promise}
*/
function resolveAbsolute(basePath, filePath): Promise<string> {
return new Promise((resolve, reject) => {
filePath = filePath.replace(reAbsolute, '');
const next = ctx => {
tryFile(path.resolve(ctx, filePath))
.then(resolve, err => {
const dir = path.dirname(ctx);
if (!dir || dir === ctx) {
return reject(`Unable to locate absolute file ${filePath}`);
}
next(dir);
});
};
next(basePath);
});
}
/**
* Check if given file exists and it’s a file, not directory
* @param {String} file
* @return {Promise}
*/
function tryFile(file): Promise<string> {
return new Promise((resolve, reject) => {
fs.stat(file, (err, stat) => {
if (err) {
return reject(err);
}
if (!stat.isFile()) {
return reject(new Error(`${file} is not a file`));
}
resolve(file);
});
});
}
......@@ -24,6 +24,16 @@ declare module 'EmmetNode' {
toString(): string
}
export interface CssToken extends Token {
size: number
item(number): any
type: string
}
export interface HtmlToken extends Token {
value: string
}
export interface Attribute extends Token {
name: string
value: Token
......@@ -51,10 +61,15 @@ declare module 'EmmetNode' {
export interface Rule extends CssNode {
selectorToken: Token
contentStartToken: Token
contentEndToken: Token
}
export interface Property extends CssNode {
valueToken: Token
separator: Token
parent: Rule
terminatorToken: Token
}
export interface Stylesheet extends Node {
......
/*---------------------------------------------------------------------------------------------
* 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
'use strict';
import { TextEditor, Range, Position, window } from 'vscode';
import * as path from 'path';
import { getImageSize } from './imageSizeHelper';
import { isStyleSheet } from 'vscode-emmet-helper';
import { parse, getNode, iterateCSSToken } from './util';
import { HtmlNode, CssToken, HtmlToken, Attribute, Property } from 'EmmetNode';
import { locateFile } from './locateFile';
/**
* Updates size of context image in given editor
*/
export function updateImageSize() {
let editor = window.activeTextEditor;
if (!editor) {
window.showInformationMessage('No editor is active.');
return;
}
if (!isStyleSheet(editor.document.languageId)) {
return updateImageSizeHTML(editor);
} else {
return updateImageSizeCSS(editor);
}
}
/**
* Updates image size of context tag of HTML model
*/
function updateImageSizeHTML(editor: TextEditor) {
const src = getImageSrcHTML(getImageHTMLNode(editor));
if (!src) {
return Promise.reject(new Error('No valid image source'));
}
locateFile(path.dirname(editor.document.fileName), src)
.then(getImageSize)
.then((size: any) => {
// since this action is asynchronous, we have to ensure that editor wasn’t
// changed and user didn’t moved caret outside <img> node
const img = getImageHTMLNode(editor);
if (getImageSrcHTML(img) === src) {
updateHTMLTag(editor, img, size.width, size.height);
}
})
.catch(err => console.warn('Error while updating image size:', err));
}
/**
* Updates image size of context rule of stylesheet model
*/
function updateImageSizeCSS(editor: TextEditor) {
const src = getImageSrcCSS(getImageCSSNode(editor), editor.selection.active);
if (!src) {
return Promise.reject(new Error('No valid image source'));
}
locateFile(path.dirname(editor.document.fileName), src)
.then(getImageSize)
.then((size: any) => {
// since this action is asynchronous, we have to ensure that editor wasn’t
// changed and user didn’t moved caret outside <img> node
const prop = getImageCSSNode(editor);
if (getImageSrcCSS(prop, editor.selection.active) === src) {
updateCSSNode(editor, prop, size.width, size.height);
}
})
.catch(err => console.warn('Error while updating image size:', err));
}
/**
* Returns <img> node under caret in given editor or `null` if such node cannot
* be found
* @param {TextEditor} editor
* @return {HtmlNode}
*/
function getImageHTMLNode(editor: TextEditor): HtmlNode {
const rootNode = parse(editor.document);
const node = <HtmlNode>getNode(rootNode, editor.selection.active, 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
* @param {TextEditor} editor
* @return {Property}
*/
function getImageCSSNode(editor: TextEditor): Property {
const rootNode = parse(editor.document);
const node = getNode(rootNode, editor.selection.active, true);
return node && node.type === 'property' ? <Property>node : null;
}
/**
* Returns image source from given <img> node
* @param {HtmlNode} node
* @return {string}
*/
function getImageSrcHTML(node: HtmlNode): string {
const srcAttr = getAttribute(node, 'src');
if (!srcAttr) {
console.warn('No "src" attribute in', node && node.open);
return;
}
return (<HtmlToken>srcAttr.value).value;
}
/**
* Returns image source from given `url()` token
* @param {Property} node
* @param {Position}
* @return {string}
*/
function getImageSrcCSS(node: Property, position: Position): string {
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
* @param {TextEditor} editor
* @param {HtmlNode} node
* @param {number} width
* @param {number} height
*/
function updateHTMLTag(editor: TextEditor, node: HtmlNode, width: number, height: number) {
const srcAttr = getAttribute(node, 'src');
const widthAttr = getAttribute(node, 'width');
const heightAttr = getAttribute(node, 'height');
let edits: [Range, string][] = [];
// apply changes from right to left, first for height, then for width
let point: Position;
const quote = getAttributeQuote(editor, widthAttr || heightAttr || srcAttr);
if (!heightAttr) {
// no `height` attribute, add it right after `width` or `src`
point = widthAttr ? widthAttr.end : srcAttr.end;
edits.push([new Range(point, point), ` height=${quote}${height}${quote}`]);
} else {
edits.push([new Range(heightAttr.value.start, heightAttr.value.end), String(height)]);
}
if (!widthAttr) {
// no `width` attribute, add it right before `height` or after `src`
point = heightAttr ? heightAttr.start : srcAttr.end;
edits.push([new Range(point, point), ` width=${quote}${width}${quote}`]);
} else {
edits.push([new Range(widthAttr.value.start, widthAttr.value.end), String(width)]);
}
return editor.edit(builder => {
edits.forEach(([rangeToReplace, textToReplace]) => {
builder.replace(rangeToReplace, textToReplace);
});
});
}
/**
* Updates size of given CSS rule
* @param {TextEditor} editor
* @param {Property} srcProp
* @param {number} width
* @param {number} height
*/
function updateCSSNode(editor: TextEditor, srcProp: Property, width: number, height: number) {
const rule = srcProp.parent;
const widthProp = getProperty(rule, 'width');
const heightProp = getProperty(rule, 'height');
// Detect formatting
const separator = srcProp.separator || ': ';
const before = getBefore(editor, srcProp);
let edits: [Range, string][] = [];
if (!srcProp.terminatorToken) {
edits.push([new Range(srcProp.end, srcProp.end), ';']);
}
let point: Position;
if (!heightProp) {
// no `height` property, add it right after `width` or source property
point = widthProp ? widthProp.start : srcProp.end;
edits.push([new Range(point, point), `${before}height${separator}${height}px;`]);
} else {
edits.push([new Range(heightProp.valueToken.start, heightProp.valueTokenend), `${height}px`]);
}
if (!widthProp) {
// no `width` attribute, add it right after `height` or source property
if (heightProp) {
point = heightProp.previousSibling
? heightProp.previousSibling.end
: rule.contentStartToken.end;
} else {
point = srcProp.end;
}
edits.push([new Range(point, point), `${before}width${separator}${width}px;`]);
} else {
edits.push([new Range(widthProp.valueToken.start, widthProp.valueTokenend), `${width}px`]);
}
return editor.edit(builder => {
edits.forEach(([rangeToReplace, textToReplace]) => {
builder.replace(rangeToReplace, textToReplace);
});
});
}
/**
* Returns attribute object with `attrName` name from given HTML node
* @param {Node} node
* @param {String} attrName
* @return {Object}
*/
function getAttribute(node, attrName): Attribute {
attrName = attrName.toLowerCase();
return node && node.open.attributes.find(attr => attr.name.value.toLowerCase() === attrName);
}
/**
* Returns quote character, used for value of given attribute. May return empty
* string if attribute wasn’t quoted
* @param {TextEditor} editor
* @param {Object} attr
* @return {String}
*/
function getAttributeQuote(editor, attr) {
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`
* @param {Node} node
* @param {Position} pos
* @return {Token}
*/
function findUrlToken(node, pos: Position) {
for (let i = 0, il = node.parsedValue.length, url; i < il; i++) {
iterateCSSToken(node.parsedValue[i], (token: CssToken) => {
if (token.type === 'url' && token.start.isBeforeOrEqual(pos) && token.end.isAfterOrEqual(pos)) {
url = token;
return false;
}
});
if (url) {
return url;
}
}
}
/**
* Returns `name` CSS property from given `rule`
* @param {Node} rule
* @param {String} name
* @return {Node}
*/
function getProperty(rule, name) {
return rule.children.find(node => node.type === 'property' && node.name === name);
}
/**
* Returns a string that is used to delimit properties in current node’s rule
* @param {TextEditor} editor
* @param {Node} node
* @return {String}
*/
function getBefore(editor: TextEditor, node: Property) {
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 '';
}
......@@ -6,7 +6,7 @@
import * as vscode from 'vscode';
import parse from '@emmetio/html-matcher';
import parseStylesheet from '@emmetio/css-parser';
import { Node, HtmlNode } from 'EmmetNode';
import { Node, HtmlNode, CssToken } from 'EmmetNode';
import { DocumentStreamReader } from './bufferStream';
import { isStyleSheet } from 'vscode-emmet-helper';
......@@ -262,3 +262,17 @@ export function getEmmetConfiguration() {
variables: emmetConfig['variables']
};
}
/**
* Itereates by each child, as well as nested child’ children, in their order
* and invokes `fn` for each. If `fn` function returns `false`, iteration stops
* @param {Token} token
* @param {Function} fn
*/
export function iterateCSSToken(token: CssToken, fn) {
for (let i = 0, il = token.size; i < il; i++) {
if (fn(token.item(i)) === false || iterateCSSToken(token.item(i), fn) === false) {
return false;
}
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册