diff --git a/.gitignore b/.gitignore index 5e7ab46..8fa8d01 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ node_modules/ +npm-debug.log *.actual.css +*.sublime-* diff --git a/README.md b/README.md index e612b87..c08f539 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,29 @@ PostCSS Assets ============== -An asset manager for CSS. +PostCSS Assets is an asset manager for CSS. It isolates stylesheets from environmental changes, gets image sizes and inlines files. + +Table of contents +----------------- + +* [Installation](#installation) +* [Usage](#usage) +* [URL resolution](#url-resolution) + * [Load paths](#load-paths) + * [Base path](#base-path) + * [Base URL](#base-url) + * [Relative paths](#relative-paths) +* [Image dimensions](#image-dimensions) +* [Inlining files](#inlining-files) +* [Full list of options](#full-list-of-options) +* [Full list of modifiers](#full-list-of-modifiers) + +Installation +------------ + +```bash +npm install postcss-assets --save-dev +``` Usage ----- @@ -14,7 +36,9 @@ gulp.task('assets', function () { var assets = require('postcss-assets'); return gulp.src('source/*.css') - .pipe(postcss([ assets() ])) + .pipe(postcss([assets({ + loadPaths: ['images/'] + })])) .pipe(gulp.dest('build/')); }); ``` @@ -28,10 +52,123 @@ grunt.initConfig({ postcss: { options: { processors: [ - assets().postcss + assets({ + loadPaths: ['images/'] + }) ] }, dist: { src: 'build/*.css' } }, }); ``` + +URL resolution +-------------- + +These options isolate stylesheets from environmental changes. + +### Load paths + +To make PostCSS Assets search for files in specific directories, define load paths: + +```js +var options = { + loadPaths: ['fonts/', 'media/patterns/', 'images/'] +}; +``` + +Example: + +```css +body { + background: url('foobar.jpg'); + background: url('icons/baz.png'); +} +``` + +PostCSS Assets would look for the files in load paths, then in the base path. If it succeed, it would resolve a true URL: + +```css +body { + background: url('/media/patterns/foobar.jpg'); + background: url('/images/icons/baz.png'); +} +``` + +### Base path + +If the root directory of your site is not where you execute PostCSS Assets, correct it: + +```js +var options = { + basePath: 'source/' +}; +``` + +PostCSS Assets would treat `source` directory as `/` for all URLs and load paths would be relative to it. + +### Base URL + +If the URL of your base path is not `/`, correct it: + +```js +var options = { + baseUrl: 'http://example.com/wp-content/themes/' +}; +``` + +### Relative paths + +To make resolved paths relative, define a directory to relate to: + +```js +var options = { + relativeTo: 'assets/css' +}; +``` + +Image dimensions +---------------- + +PostCSS Assets calculates dimensions of PNG, JPEG, GIF, SVG and WebP images: + +```css +body { + width: width('images/foobar.png'); /* 320px */ + height: height('images/foobar.png'); /* 240px */ + background-size: size('images/foobar.png'); /* 320px 240px */ +} +``` + +To correct the dimensions for images with a high density, pass it as a second parameter: + +```css +body { + width: width('images/foobar.png', 2); /* 160px */ + height: height('images/foobar.png', 2); /* 120px */ + background-size: size('images/foobar.png', 2); /* 160px 120px */ +} +``` + +Inlining files +-------------- + +PostCSS inlines files to a stylesheet in Base64 encoding: + +```css +body { + background: inline('images/foobar.png'); +} +``` + +SVG files would be inlined unencoded, because [then they benefit in size](http://css-tricks.com/probably-dont-base64-svg/). + +Full list of options +-------------------- + +| Option | Description | Default | +|:-----------------|:--------------------------------------------------------------------------------|:--------| +| `basePath` | Root directory of the project. | `.` | +| `baseUrl` | URL of the project when running the web server. | `/` | +| `loadPaths` | Specific directories to look for the files. | `[]` | +| `relativeTo` | Directory to relate to when resolving URLs. If `false`, disables relative URLs. | `false` | diff --git a/index.js b/index.js index 0630e2b..9871c3d 100644 --- a/index.js +++ b/index.js @@ -13,17 +13,6 @@ var cssesc = require('cssesc'); var mime = require('mime'); var sizeOf = require('image-size'); -const AUTO_SIZE = ['background-size', 'border-image-width', 'border-width', - 'margin', 'padding']; -const AUTO_WIDTH = ['border-left', 'border-left-width', 'border-right', - 'border-right-width', 'left', 'margin-left', - 'margin-right', 'max-width', 'min-width', 'padding-left', - 'padding-right', 'width']; -const AUTO_HEIGHT = ['border-bottom', 'border-bottom-width', 'border-top', - 'border-top-width', 'bottom', 'height', 'margin-bottom', - 'margin-top', 'max-height', 'min-height', - 'padding-bottom', 'padding-top']; - module.exports = function (options) { options = options || {}; @@ -50,6 +39,25 @@ module.exports = function (options) { options.relativeTo = false; } + function getImageSize(assetStr, density) { + var assetPath = resolvePath(assetStr.value); + var size; + try { + size = sizeOf(assetPath); + if (typeof density !== 'undefined') { + density = parseFloat(density.value, 10); + console.log(density); + size.width = +(size.width / density).toFixed(4); + size.height = +(size.height / density).toFixed(4); + } + return size; + } catch (exception) { + var err = new Error("Image corrupted: " + assetPath); + err.name = 'ECORRUPT'; + throw err; + } + } + function matchPath(assetPath) { var exception, matchingPath; var isFound = options.loadPaths.some(function (loadPath) { @@ -95,50 +103,45 @@ module.exports = function (options) { return cssesc(url.format(assetUrl)); } - function shouldBeInline(assetPath) { - if (options.inline && options.inline.maxSize) { - var size = fs.statSync(assetPath).size; - return (size <= parseBytes(options.inline.maxSize)); - } - return false; - } - return function (cssTree) { cssTree.eachDecl(function (decl) { - - decl.value = mapFunctions(decl.value, function (before, quote, assetStr, modifier, after) { - - try { - - var assetPath = resolvePath(assetStr); - var prop = vendor.unprefixed(decl.prop); - - if (modifier === 'width' || AUTO_WIDTH.indexOf(prop) !== -1) { - return sizeOf(assetPath).width + 'px'; - } - - if (modifier === 'height' || AUTO_HEIGHT.indexOf(prop) !== -1) { - return sizeOf(assetPath).height + 'px'; - } - - if (modifier === 'size' || AUTO_SIZE.indexOf(prop) !== -1) { - var size = sizeOf(assetPath); + try { + decl.value = mapFunctions(decl.value, { + 'url': function (assetStr) { + assetStr.value = resolveUrl(assetStr.value); + return 'url(' + assetStr + ')'; + }, + + 'inline': function (assetStr) { + assetStr.value = resolveDataUrl(assetStr.value); + return 'url(' + assetStr + ')'; + }, + + 'width': function (assetStr, density) { + return getImageSize(assetStr, density).width + 'px'; + }, + + 'height': function (assetStr, density) { + return getImageSize(assetStr, density).height + 'px'; + }, + + 'size': function (assetStr, density) { + var size = getImageSize(assetStr, density); return size.width + 'px ' + size.height + 'px'; } - - if (shouldBeInline(assetPath)) { - return 'url(' + before + quote + resolveDataUrl(assetStr) + quote + after + ')'; - } - - return 'url(' + before + quote + resolveUrl(assetStr) + quote + after + ')'; - - } catch (exception) { - if (exception.name !== 'ENOENT') { - throw exception; - } + }); + } catch (exception) { + switch (exception.name) { + case 'ECORRUPT': + console.warn(exception.message); + break; + case 'ENOENT': console.warn('%s\nLoad paths:\n %s', exception.message, options.loadPaths.join('\n ')); + break; + default: + throw exception; } - }); + } }); }; }; diff --git a/lib/mapFunctions.js b/lib/mapFunctions.js index 6450912..e87f07e 100644 --- a/lib/mapFunctions.js +++ b/lib/mapFunctions.js @@ -1,12 +1,56 @@ -const R_FUNC = /url\((\s*)((['"]?).*?\3.*?)(\s*)\)/gi; -const R_PARAMS = /^(['"]?)(.+?)\1(?:\s+(.+))?$/; - -module.exports = function mapFunctions(cssValue, callback) { - return cssValue.replace(R_FUNC, function (matches, before, params, quote, after) { - params = params.match(R_PARAMS); - var assetStr = params[2]; - var modifier = params[3]; - var result = callback(before, quote, assetStr, modifier, after); - return result || matches; - }); +var gonzales = require('gonzales'); +var list = require('postcss/lib/list'); + +function CSSString (str) { + this.quotes = str[0]; + + if (this.quotes === "'" || this.quotes === '"') { + this.value = str.slice(1, -1); + } else { + this.value = str; + this.quotes = ''; + } +} + +CSSString.prototype.toString = function () { + return this.quotes + this.value + this.quotes; +}; + +module.exports = function mapFunctions(cssValue, map) { + var ast = gonzales.srcToCSSP(cssValue, 'value'); + + var traverse = function (node) { + var type = node[0]; + var children = node.slice(1); + + if (type === 'uri') { + node = gonzales.srcToCSSP(gonzales.csspToSrc(node), 'funktion'); + return traverse(node); + } + + if (type === 'funktion') { + var name = children[0][1]; + var body = children[1].slice(1).map(function (x) { + return gonzales.csspToSrc(traverse(x));; + }).join(''); + + var process = map[name]; + + if (typeof process === 'function') { + return ['raw', process.apply(this, list.comma(body).map(function (param) { + return new CSSString(param); + }))]; + } + } + + return [type].concat(children.map(function (child) { + if (Array.isArray(child)) { + return traverse(child); + } + + return child; + })); + }; + + return gonzales.csspToSrc(traverse(ast)); }; diff --git a/package.json b/package.json index 0bc6c07..d0d5297 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,23 @@ { "name": "postcss-assets", - "version": "0.9.1", + "version": "1.0.0", "description": "PostCSS plugin to manage assets", "author": "Vadim Borodean ", "license": "MIT", "repository": { "type": "git", - "url": "https://github.com/borodean/postcss-assets" + "url": "https://github.com/borodean/postcss-assets" }, "dependencies": { "cssesc": "^0.1.0", - "image-size": "^0.3.3", + "gonzales": "^1.0.7", + "image-size": "^0.3.5", "js-base64": "^2.1.5", "mime": "^1.2.11", - "postcss": "^2.2.5" + "postcss": "^3.0.7" }, "devDependencies": { - "tape": "^3.0.0" + "tape": "^3.0.3" }, "scripts": { "test": "tape test" diff --git a/test/fixtures/alpha/invalid.jpg b/test/fixtures/alpha/invalid.jpg new file mode 100644 index 0000000..e40413f --- /dev/null +++ b/test/fixtures/alpha/invalid.jpg @@ -0,0 +1 @@ +Wishmaster diff --git a/test/fixtures/dimensions.css b/test/fixtures/dimensions.css index 947bf6a..09c7aab 100644 --- a/test/fixtures/dimensions.css +++ b/test/fixtures/dimensions.css @@ -1,43 +1,12 @@ body { - width: url('beta/maria.jpg' width); - height: url('beta/maria.jpg' height); - height: url('beta/maria.jpg?foo=bar#baz' height); - width: url('beta/maria.jpg'); - height: url('beta/maria.jpg'); - background-size: url('beta/maria.jpg' size); - -webkit-background-size: url('beta/maria.jpg'); - -moz-background-size: url('beta/maria.jpg'); - background-size: url('beta/maria.jpg'); + background-size: size('beta/maria.jpg'); + width: width('beta/maria.jpg'); + height: height('beta/maria.jpg'); - background-size: url('beta/maria.jpg'); - border-image-width: url('beta/maria.jpg'); - border-width: url('beta/maria.jpg'); - margin: url('beta/maria.jpg'); - padding: url('beta/maria.jpg'); + width: width('beta/maria.jpg', 2); + height: height('beta/maria.jpg', 2); - border-left: url('beta/maria.jpg') solid red; - border-left-width: url('beta/maria.jpg'); - border-right: dashed url('beta/maria.jpg') blue; - border-right-width: url('beta/maria.jpg'); - left: url('beta/maria.jpg'); - margin-left: url('beta/maria.jpg'); - margin-right: url('beta/maria.jpg'); - max-width: url('beta/maria.jpg'); - min-width: url('beta/maria.jpg'); - padding-left: url('beta/maria.jpg'); - padding-right: url('beta/maria.jpg'); - width: url('beta/maria.jpg'); + width: width('beta/maria.jpg', 1.3); - border-bottom: double green url('beta/maria.jpg'); - border-bottom-width: url('beta/maria.jpg'); - border-top: black url('beta/maria.jpg') solid; - border-top-width: url('beta/maria.jpg'); - bottom: url('beta/maria.jpg'); - height: url('beta/maria.jpg'); - margin-bottom: url('beta/maria.jpg'); - margin-top: url('beta/maria.jpg'); - max-height: url('beta/maria.jpg'); - min-height: url('beta/maria.jpg'); - padding-bottom: url('beta/maria.jpg'); - padding-top: url('beta/maria.jpg'); + width: width('alpha/invalid.jpg'); } diff --git a/test/fixtures/dimensions.expected.css b/test/fixtures/dimensions.expected.css index 85cab24..7622a19 100644 --- a/test/fixtures/dimensions.expected.css +++ b/test/fixtures/dimensions.expected.css @@ -1,43 +1,12 @@ body { + background-size: 45px 60px; width: 45px; height: 60px; - height: 60px; - width: 45px; - height: 60px; - background-size: 45px 60px; - -webkit-background-size: 45px 60px; - -moz-background-size: 45px 60px; - background-size: 45px 60px; - background-size: 45px 60px; - border-image-width: 45px 60px; - border-width: 45px 60px; - margin: 45px 60px; - padding: 45px 60px; + width: 22.5px; + height: 30px; - border-left: 45px solid red; - border-left-width: 45px; - border-right: dashed 45px blue; - border-right-width: 45px; - left: 45px; - margin-left: 45px; - margin-right: 45px; - max-width: 45px; - min-width: 45px; - padding-left: 45px; - padding-right: 45px; - width: 45px; + width: 34.6154px; - border-bottom: double green 60px; - border-bottom-width: 60px; - border-top: black 60px solid; - border-top-width: 60px; - bottom: 60px; - height: 60px; - margin-bottom: 60px; - margin-top: 60px; - max-height: 60px; - min-height: 60px; - padding-bottom: 60px; - padding-top: 60px; + width: width('alpha/invalid.jpg'); } diff --git a/test/fixtures/inline.css b/test/fixtures/inline.css index f188033..f80d95f 100644 --- a/test/fixtures/inline.css +++ b/test/fixtures/inline.css @@ -1,7 +1,6 @@ body { - background: url('alpha/blank.gif'); - background: url( 'alpha/blank.gif' ); - background: url('alpha/kateryna.jpg'); - background: url('alpha/trapezium.svg'); - background: url('alpha/trapezium-single-quotes.svg'); + background: inline('alpha/blank.gif'); + background: inline( 'alpha/blank.gif' ); + background: inline('alpha/trapezium.svg'); + background: inline('alpha/trapezium-single-quotes.svg'); } diff --git a/test/fixtures/inline.expected.css b/test/fixtures/inline.expected.css index 0bdff64..a3e32aa 100644 --- a/test/fixtures/inline.expected.css +++ b/test/fixtures/inline.expected.css @@ -1,7 +1,6 @@ body { background: url('data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw=='); - background: url( 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==' ); - background: url('/alpha/kateryna.jpg'); + background: url('data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw=='); background: url('data:image/svg+xml;utf8,\D\A \D\A\D\A'); background: url('data:image/svg+xml;utf8,\D\A \D\A\D\A'); } diff --git a/test/fixtures/resolve-spelling.expected.css b/test/fixtures/resolve-spelling.expected.css index 3214a4e..3c5ee51 100644 --- a/test/fixtures/resolve-spelling.expected.css +++ b/test/fixtures/resolve-spelling.expected.css @@ -1,7 +1,7 @@ body { color: red; background: url(/alpha/kateryna.jpg); - background: url( /alpha/kateryna.jpg ); + background: url(/alpha/kateryna.jpg); background: no-repeat url(/alpha/kateryna.jpg) #000; background: url('/alpha/kateryna.jpg'), diff --git a/test/index.js b/test/index.js index 49d52a7..40311dd 100644 --- a/test/index.js +++ b/test/index.js @@ -72,14 +72,7 @@ test('path resolving', function (t) { }); test('path inlining', function (t) { - - compareFixtures(t, 'inline', 'base64-encodes matching assets', { - basePath: 'test/fixtures/', - inline: { - maxSize: '2K' - } - }); - + compareFixtures(t, 'inline', 'base64-encodes assets', { basePath: 'test/fixtures/' }); t.end(); }); diff --git a/test/lib/mapFunctions.js b/test/lib/mapFunctions.js index 2735bc1..003b183 100644 --- a/test/lib/mapFunctions.js +++ b/test/lib/mapFunctions.js @@ -2,36 +2,70 @@ var test = require('tape'); var mapFunctions = require('../../lib/mapFunctions'); -function compact(arr) { - return Array.prototype.filter.call(arr, function (i) { return i; }); -} +const MAP = { + 'decrease': function (params) { + return parseFloat(params, 10) - 1 + 'px'; + }, + 'increase': function (params) { + return parseFloat(params, 10) + 1 + 'px'; + }, + 'double': function (params) { + return parseFloat(params, 10) * 2 + 'px'; + }, + 'url': function (params) { + return 'url(https://github.com/' + params + ')'; + }, + 'combine': function (a, b) { + return parseFloat(a, 10) + parseFloat(b, 10) + 'px'; + } +}; -function checkArgs(t, input, expected, msg) { - var actual = []; - mapFunctions(input, function () { - actual = actual.concat(compact(arguments)); - }); - t.deepEqual(actual, expected, msg); +function checkMapping (t, source, expectedResult, msg) { + t.equal(mapFunctions(source, MAP), expectedResult, msg); } test('mapFunctions', function (t) { + checkMapping(t, + 'increase(100px)', + '101px', + 'maps functions' + ); + + checkMapping(t, + 'increase(100px), decrease(100px)', + '101px, 99px', + 'maps sibling functions' + ); - checkArgs(t, 'url(kateryna.jpg)', ['kateryna.jpg'], 'parses unquoted param'); - checkArgs(t, 'url(\'kateryna.jpg\')', ['\'', 'kateryna.jpg'], 'parses single-quoted param'); - checkArgs(t, 'url("kateryna.jpg")', ['"', 'kateryna.jpg'], 'parses double-quoted param'); - checkArgs(t, 'url("kateryna_(shevchenko).jpg")', ['"', 'kateryna_(shevchenko).jpg'], 'parses param with parentheses'); + checkMapping(t, + 'double(increase(100px))', + '202px', + 'maps nested functions' + ); - checkArgs(t, 'url("Gupsy Fortuneteller.jpg")', ['"', 'Gupsy Fortuneteller.jpg'], 'parses param with spaces'); - checkArgs(t, 'url(\'Baba O\\\'Riley.jpg\')', ['\'', 'Baba O\\\'Riley.jpg'], 'parses param escaped quotes'); + checkMapping(t, + 'unknown(100px)', + 'unknown(100px)', + 'skips unknown functions' + ); - checkArgs(t, 'url(kateryna.jpg width)', ['kateryna.jpg', 'width'], 'parses unquoted param with modifier'); - checkArgs(t, 'url(kateryna.jpg width)', ['kateryna.jpg', 'width'], 'parses unquoted param with modifier with extra space'); - checkArgs(t, 'url("kateryna.jpg" width)', ['"', 'kateryna.jpg', 'width'], 'parses quoted param with modifier'); + checkMapping(t, + 'unknown(increase(100px))', + 'unknown(101px)', + 'maps inside unknown functions' + ); - checkArgs(t, 'url( kateryna.jpg )', [' ', 'kateryna.jpg', ' '], 'parses extra space'); + checkMapping(t, + 'url(borodean)', + 'url(https://github.com/borodean)', + 'maps urls as functions' + ); - checkArgs(t, '#000 url(kateryna.jpg) no-repeat', ['kateryna.jpg'], 'parses complex values'); - checkArgs(t, 'url(kateryna.jpg), url(odalisque.jpg)', ['kateryna.jpg', 'odalisque.jpg'], 'parses multiple values'); + checkMapping(t, + 'combine(20px, 15px)', + '35px', + 'accepts multiple parameters' + ); t.end(); });