This commit is contained in:
2015-11-02 11:36:10 +01:00
commit 08cd6febee
1419 changed files with 221401 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
test
support
benchmarks
examples
lib-cov
coverage
.gitmodules
.travis.yml
History.md
Makefile
test/
support/
benchmarks/
examples/
docs/
+1
View File
@@ -0,0 +1 @@
"2ab04e8289982bfac3548a9c6016476472dcdb4f"
+22
View File
@@ -0,0 +1,22 @@
(The MIT License)
Copyright (c) 2009-2014 TJ Holowaychuk <tj@vision-media.ca>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+154
View File
@@ -0,0 +1,154 @@
# [![Jade - Node Template Engine](http://garthdb.com/img/jade_branding/jade-01.svg)](http://jade-lang.com/)
Full documentation is at [jade-lang.com](http://jade-lang.com/)
Jade is a high performance template engine heavily influenced by [Haml](http://haml-lang.com)
and implemented with JavaScript for [node](http://nodejs.org) and browsers. For bug reports,
feature requests and questions, [open an issue](https://github.com/jadejs/jade/issues/new).
For discussion join the [chat room](https://gitter.im/jadejs/jade).
You can test drive Jade online [here](http://naltatis.github.com/jade-syntax-docs).
[![Build Status](https://img.shields.io/travis/jadejs/jade/master.svg?style=flat)](https://travis-ci.org/jadejs/jade)
[![Coverage Status](https://img.shields.io/coveralls/jadejs/jade/master.svg?style=flat)](https://coveralls.io/r/jadejs/jade?branch=master)
[![Dependency Status](https://img.shields.io/david/jadejs/jade.svg?style=flat)](https://david-dm.org/jadejs/jade)
[![devDependencies Status](https://img.shields.io/david/dev/jadejs/jade.svg?style=flat)](https://david-dm.org/jadejs/jade#info=devDependencies)
[![NPM version](https://img.shields.io/npm/v/jade.svg?style=flat)](http://badge.fury.io/js/jade)
[![Join Gitter Chat](https://img.shields.io/badge/gitter-join%20chat%20%E2%86%92-brightgreen.svg?style=flat)](https://gitter.im/jadejs/jade?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
## Installation
via npm:
```bash
$ npm install jade
```
## Syntax
Jade is a clean, whitespace sensitive syntax for writing html. Here is a simple example:
```jade
doctype html
html(lang="en")
head
title= pageTitle
script(type='text/javascript').
if (foo) bar(1 + 5)
body
h1 Jade - node template engine
#container.col
if youAreUsingJade
p You are amazing
else
p Get on it!
p.
Jade is a terse and simple templating language with a
strong focus on performance and powerful features.
```
becomes
```html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Jade</title>
<script type="text/javascript">
if (foo) bar(1 + 5)
</script>
</head>
<body>
<h1>Jade - node template engine</h1>
<div id="container" class="col">
<p>You are amazing</p>
<p>Jade is a terse and simple templating language with a strong focus on performance and powerful features.</p>
</div>
</body>
</html>
```
The official [jade tutorial](http://jade-lang.com/tutorial/) is a great place to start. While that (and the syntax documentation) is being finished, you can view some of the old documentation [here](https://github.com/jadejs/jade/blob/master/jade.md) and [here](https://github.com/jadejs/jade/blob/master/jade-language.md)
## API
For full API, see [jade-lang.com/api](http://jade-lang.com/api/)
```js
var jade = require('jade');
// compile
var fn = jade.compile('string of jade', options);
var html = fn(locals);
// render
var html = jade.render('string of jade', merge(options, locals));
// renderFile
var html = jade.renderFile('filename.jade', merge(options, locals));
```
### Options
- `filename` Used in exceptions, and required when using includes
- `compileDebug` When `false` no debug instrumentation is compiled
- `pretty` Add pretty-indentation whitespace to output _(false by default)_
## Browser Support
The latest version of jade can be download for the browser in standalone form from [here](https://github.com/jadejs/jade/raw/master/jade.js). It only supports the very latest browsers though, and is a large file. It is recommended that you pre-compile your jade templates to JavaScript and then just use the [runtime.js](https://github.com/jadejs/jade/raw/master/runtime.js) library on the client.
To compile a template for use on the client using the command line, do:
```console
$ jade --client --no-debug filename.jade
```
which will produce `filename.js` containing the compiled template.
## Command Line
After installing the latest version of [node](http://nodejs.org/), install with:
```console
$ npm install jade -g
```
and run with
```console
$ jade --help
```
## Additional Resources
Tutorials:
- cssdeck interactive [Jade syntax tutorial](http://cssdeck.com/labs/learning-the-jade-templating-engine-syntax)
- cssdeck interactive [Jade logic tutorial](http://cssdeck.com/labs/jade-templating-tutorial-codecast-part-2)
- [Jade について。](https://gist.github.com/japboy/5402844) (A Japanese Tutorial)
- [Jade - 模板引擎](https://github.com/jadejs/jade/blob/master/Readme_zh-cn.md)
Implementations in other languages:
- [php](http://github.com/everzet/jade.php)
- [scala](http://scalate.fusesource.org/versions/snapshot/documentation/scaml-reference.html)
- [ruby](https://github.com/slim-template/slim)
- [python](https://github.com/SyrusAkbary/pyjade)
- [java](https://github.com/neuland/jade4j)
Other:
- [Emacs Mode](https://github.com/brianc/jade-mode)
- [Vim Syntax](https://github.com/digitaltoad/vim-jade)
- [TextMate Bundle](http://github.com/miksago/jade-tmbundle)
- [Coda/SubEtha syntax Mode](https://github.com/aaronmccall/jade.mode)
- [Screencasts](http://tjholowaychuk.com/post/1004255394/jade-screencast-template-engine-for-nodejs)
- [html2jade](https://github.com/donpark/html2jade) converter
- [jade2php](https://github.com/SE7ENSKY/jade2php) converter
- [Jade Server](https://github.com/ded/jade-server) Ideal for building local prototypes apart from any application
## License
MIT
+1285
View File
File diff suppressed because it is too large Load Diff
Generated Vendored Executable
+295
View File
@@ -0,0 +1,295 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
var fs = require('fs')
, program = require('commander')
, path = require('path')
, basename = path.basename
, dirname = path.dirname
, resolve = path.resolve
, normalize = path.normalize
, join = path.join
, mkdirp = require('mkdirp')
, jade = require('../');
// jade options
var options = {};
// options
program
.version(require('../package.json').version)
.usage('[options] [dir|file ...]')
.option('-O, --obj <str|path>', 'JavaScript options object or JSON file containing it')
.option('-o, --out <dir>', 'output the compiled html to <dir>')
.option('-p, --path <path>', 'filename used to resolve includes')
.option('-P, --pretty', 'compile pretty html output')
.option('-c, --client', 'compile function for client-side runtime.js')
.option('-n, --name <str>', 'The name of the compiled template (requires --client)')
.option('-D, --no-debug', 'compile without debugging (smaller functions)')
.option('-w, --watch', 'watch files for changes and automatically re-render')
.option('-E, --extension <ext>', 'specify the output file extension')
.option('-H, --hierarchy', 'keep directory hierarchy when a directory is specified')
.option('--name-after-file', 'Name the template after the last section of the file path (requires --client and overriden by --name)')
.option('--doctype <str>', 'Specify the doctype on the command line (useful if it is not specified by the template)')
program.on('--help', function(){
console.log(' Examples:');
console.log('');
console.log(' # translate jade the templates dir');
console.log(' $ jade templates');
console.log('');
console.log(' # create {foo,bar}.html');
console.log(' $ jade {foo,bar}.jade');
console.log('');
console.log(' # jade over stdio');
console.log(' $ jade < my.jade > my.html');
console.log('');
console.log(' # jade over stdio');
console.log(' $ echo \'h1 Jade!\' | jade');
console.log('');
console.log(' # foo, bar dirs rendering to /tmp');
console.log(' $ jade foo bar --out /tmp ');
console.log('');
});
program.parse(process.argv);
// options given, parse them
if (program.obj) {
options = parseObj(program.obj);
}
/**
* Parse object either in `input` or in the file called `input`. The latter is
* searched first.
*/
function parseObj (input) {
var str, out;
try {
str = fs.readFileSync(program.obj);
} catch (e) {
return eval('(' + program.obj + ')');
}
// We don't want to catch exceptions thrown in JSON.parse() so have to
// use this two-step approach.
return JSON.parse(str);
}
// --path
if (program.path) options.filename = program.path;
// --no-debug
options.compileDebug = program.debug;
// --client
options.client = program.client;
// --pretty
options.pretty = program.pretty;
// --watch
options.watch = program.watch;
// --name
if (typeof program.name === 'string') {
options.name = program.name;
}
// --doctype
options.doctype = program.doctype;
// left-over args are file paths
var files = program.args;
// array of paths that are being watched
var watchList = [];
// function for rendering
var render = program.watch ? tryRender : renderFile;
// compile files
if (files.length) {
console.log();
if (options.watch) {
process.on('SIGINT', function() {
process.exit(1);
});
}
files.forEach(function (file) {
render(file);
});
process.on('exit', function () {
console.log();
});
// stdio
} else {
stdin();
}
/**
* Watch for changes on path
*
* Renders `base` if specified, otherwise renders `path`.
*/
function watchFile(path, base, rootPath) {
path = normalize(path);
if (watchList.indexOf(path) !== -1) return;
console.log(" \033[90mwatching \033[36m%s\033[0m", path);
fs.watchFile(path, {persistent: true, interval: 200},
function (curr, prev) {
// File doesn't exist anymore. Keep watching.
if (curr.mtime.getTime() === 0) return;
// istanbul ignore if
if (curr.mtime.getTime() === prev.mtime.getTime()) return;
tryRender(base || path, rootPath);
});
watchList.push(path);
}
/**
* Convert error to string
*/
function errorToString(e) {
return e.stack || /* istanbul ignore next */ (e.message || e);
}
/**
* Try to render `path`; if an exception is thrown it is printed to stderr and
* otherwise ignored.
*
* This is used in watch mode.
*/
function tryRender(path, rootPath) {
try {
renderFile(path, rootPath);
} catch (e) {
// keep watching when error occured.
console.error(errorToString(e));
}
}
/**
* Compile from stdin.
*/
function stdin() {
var buf = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', function(chunk){ buf += chunk; });
process.stdin.on('end', function(){
var output;
if (options.client) {
output = jade.compileClient(buf, options);
} else {
var fn = jade.compile(buf, options);
var output = fn(options);
}
process.stdout.write(output);
}).resume();
process.on('SIGINT', function() {
process.stdout.write('\n');
process.stdin.emit('end');
process.stdout.write('\n');
process.exit();
})
}
var hierarchyWarned = false;
/**
* Process the given path, compiling the jade files found.
* Always walk the subdirectories.
*
* @param path path of the file, might be relative
* @param rootPath path relative to the directory specified in the command
*/
function renderFile(path, rootPath) {
var re = /\.jade$/;
var stat = fs.lstatSync(path);
// Found jade file/\.jade$/
if (stat.isFile() && re.test(path)) {
// Try to watch the file if needed. watchFile takes care of duplicates.
if (options.watch) watchFile(path, null, rootPath);
if (program.nameAfterFile) {
options.name = getNameFromFileName(path);
}
var fn = options.client
? jade.compileFileClient(path, options)
: jade.compileFile(path, options);
if (options.watch && fn.dependencies) {
// watch dependencies, and recompile the base
fn.dependencies.forEach(function (dep) {
watchFile(dep, path, rootPath);
});
}
// --extension
var extname;
if (program.extension) extname = '.' + program.extension;
else if (options.client) extname = '.js';
else extname = '.html';
// path: foo.jade -> foo.<ext>
path = path.replace(re, extname);
if (program.out) {
// prepend output directory
if (rootPath && program.hierarchy) {
// replace the rootPath of the resolved path with output directory
path = resolve(path).replace(new RegExp('^' + resolve(rootPath)), '');
path = join(program.out, path);
} else {
if (rootPath && !hierarchyWarned) {
console.warn('In Jade 2.0.0 --hierarchy will become the default.');
hierarchyWarned = true;
}
// old behavior or if no rootPath handling is needed
path = join(program.out, basename(path));
}
}
var dir = resolve(dirname(path));
mkdirp.sync(dir, 0755);
var output = options.client ? fn : fn(options);
fs.writeFileSync(path, output);
console.log(' \033[90mrendered \033[36m%s\033[0m', normalize(path));
// Found directory
} else if (stat.isDirectory()) {
var files = fs.readdirSync(path);
files.map(function(filename) {
return path + '/' + filename;
}).forEach(function (file) {
render(file, rootPath || path);
});
}
}
/**
* Get a sensible name for a template function from a file path
*
* @param {String} filename
* @returns {String}
*/
function getNameFromFileName(filename) {
var file = basename(filename, '.jade');
return file.toLowerCase().replace(/[^a-z0-9]+([a-z])/g, function (_, character) {
return character.toUpperCase();
}) + 'Template';
}
View File
+16
View File
@@ -0,0 +1,16 @@
{
"name": "jade",
"repo": "visionmedia/jade",
"description": "Jade template runtime",
"version": "1.11.0",
"keywords": [
"template"
],
"dependencies": {},
"development": {},
"license": "MIT",
"scripts": [
"lib/runtime.js"
],
"main": "lib/runtime.js"
}
+9196
View File
File diff suppressed because it is too large Load Diff
+723
View File
@@ -0,0 +1,723 @@
'use strict';
var nodes = require('./nodes');
var filters = require('./filters');
var doctypes = require('./doctypes');
var runtime = require('./runtime');
var utils = require('./utils');
var selfClosing = require('void-elements');
var parseJSExpression = require('character-parser').parseMax;
var constantinople = require('constantinople');
function isConstant(src) {
return constantinople(src, {jade: runtime, 'jade_interp': undefined});
}
function toConstant(src) {
return constantinople.toConstant(src, {jade: runtime, 'jade_interp': undefined});
}
function errorAtNode(node, error) {
error.line = node.line;
error.filename = node.filename;
return error;
}
/**
* Initialize `Compiler` with the given `node`.
*
* @param {Node} node
* @param {Object} options
* @api public
*/
var Compiler = module.exports = function Compiler(node, options) {
this.options = options = options || {};
this.node = node;
this.hasCompiledDoctype = false;
this.hasCompiledTag = false;
this.pp = options.pretty || false;
if (this.pp && typeof this.pp !== 'string') {
this.pp = ' ';
}
this.debug = false !== options.compileDebug;
this.indents = 0;
this.parentIndents = 0;
this.terse = false;
this.mixins = {};
this.dynamicMixins = false;
if (options.doctype) this.setDoctype(options.doctype);
};
/**
* Compiler prototype.
*/
Compiler.prototype = {
/**
* Compile parse tree to JavaScript.
*
* @api public
*/
compile: function(){
this.buf = [];
if (this.pp) this.buf.push("var jade_indent = [];");
this.lastBufferedIdx = -1;
this.visit(this.node);
if (!this.dynamicMixins) {
// if there are no dynamic mixins we can remove any un-used mixins
var mixinNames = Object.keys(this.mixins);
for (var i = 0; i < mixinNames.length; i++) {
var mixin = this.mixins[mixinNames[i]];
if (!mixin.used) {
for (var x = 0; x < mixin.instances.length; x++) {
for (var y = mixin.instances[x].start; y < mixin.instances[x].end; y++) {
this.buf[y] = '';
}
}
}
}
}
return this.buf.join('\n');
},
/**
* Sets the default doctype `name`. Sets terse mode to `true` when
* html 5 is used, causing self-closing tags to end with ">" vs "/>",
* and boolean attributes are not mirrored.
*
* @param {string} name
* @api public
*/
setDoctype: function(name){
this.doctype = doctypes[name.toLowerCase()] || '<!DOCTYPE ' + name + '>';
this.terse = this.doctype.toLowerCase() == '<!doctype html>';
this.xml = 0 == this.doctype.indexOf('<?xml');
},
/**
* Buffer the given `str` exactly as is or with interpolation
*
* @param {String} str
* @param {Boolean} interpolate
* @api public
*/
buffer: function (str, interpolate) {
var self = this;
if (interpolate) {
var match = /(\\)?([#!]){((?:.|\n)*)$/.exec(str);
if (match) {
this.buffer(str.substr(0, match.index), false);
if (match[1]) { // escape
this.buffer(match[2] + '{', false);
this.buffer(match[3], true);
return;
} else {
var rest = match[3];
var range = parseJSExpression(rest);
var code = ('!' == match[2] ? '' : 'jade.escape') + "((jade_interp = " + range.src + ") == null ? '' : jade_interp)";
this.bufferExpression(code);
this.buffer(rest.substr(range.end + 1), true);
return;
}
}
}
str = utils.stringify(str);
str = str.substr(1, str.length - 2);
if (this.lastBufferedIdx == this.buf.length) {
if (this.lastBufferedType === 'code') this.lastBuffered += ' + "';
this.lastBufferedType = 'text';
this.lastBuffered += str;
this.buf[this.lastBufferedIdx - 1] = 'buf.push(' + this.bufferStartChar + this.lastBuffered + '");'
} else {
this.buf.push('buf.push("' + str + '");');
this.lastBufferedType = 'text';
this.bufferStartChar = '"';
this.lastBuffered = str;
this.lastBufferedIdx = this.buf.length;
}
},
/**
* Buffer the given `src` so it is evaluated at run time
*
* @param {String} src
* @api public
*/
bufferExpression: function (src) {
if (isConstant(src)) {
return this.buffer(toConstant(src) + '', false)
}
if (this.lastBufferedIdx == this.buf.length) {
if (this.lastBufferedType === 'text') this.lastBuffered += '"';
this.lastBufferedType = 'code';
this.lastBuffered += ' + (' + src + ')';
this.buf[this.lastBufferedIdx - 1] = 'buf.push(' + this.bufferStartChar + this.lastBuffered + ');'
} else {
this.buf.push('buf.push(' + src + ');');
this.lastBufferedType = 'code';
this.bufferStartChar = '';
this.lastBuffered = '(' + src + ')';
this.lastBufferedIdx = this.buf.length;
}
},
/**
* Buffer an indent based on the current `indent`
* property and an additional `offset`.
*
* @param {Number} offset
* @param {Boolean} newline
* @api public
*/
prettyIndent: function(offset, newline){
offset = offset || 0;
newline = newline ? '\n' : '';
this.buffer(newline + Array(this.indents + offset).join(this.pp));
if (this.parentIndents)
this.buf.push("buf.push.apply(buf, jade_indent);");
},
/**
* Visit `node`.
*
* @param {Node} node
* @api public
*/
visit: function(node){
var debug = this.debug;
if (debug) {
this.buf.push('jade_debug.unshift(new jade.DebugItem( ' + node.line
+ ', ' + (node.filename
? utils.stringify(node.filename)
: 'jade_debug[0].filename')
+ ' ));');
}
// Massive hack to fix our context
// stack for - else[ if] etc
if (false === node.debug && this.debug) {
this.buf.pop();
this.buf.pop();
}
this.visitNode(node);
if (debug) this.buf.push('jade_debug.shift();');
},
/**
* Visit `node`.
*
* @param {Node} node
* @api public
*/
visitNode: function(node){
return this['visit' + node.type](node);
},
/**
* Visit case `node`.
*
* @param {Literal} node
* @api public
*/
visitCase: function(node){
var _ = this.withinCase;
this.withinCase = true;
this.buf.push('switch (' + node.expr + '){');
this.visit(node.block);
this.buf.push('}');
this.withinCase = _;
},
/**
* Visit when `node`.
*
* @param {Literal} node
* @api public
*/
visitWhen: function(node){
if ('default' == node.expr) {
this.buf.push('default:');
} else {
this.buf.push('case ' + node.expr + ':');
}
if (node.block) {
this.visit(node.block);
this.buf.push(' break;');
}
},
/**
* Visit literal `node`.
*
* @param {Literal} node
* @api public
*/
visitLiteral: function(node){
this.buffer(node.str);
},
/**
* Visit all nodes in `block`.
*
* @param {Block} block
* @api public
*/
visitBlock: function(block){
var len = block.nodes.length
, escape = this.escape
, pp = this.pp
// Pretty print multi-line text
if (pp && len > 1 && !escape && block.nodes[0].isText && block.nodes[1].isText)
this.prettyIndent(1, true);
for (var i = 0; i < len; ++i) {
// Pretty print text
if (pp && i > 0 && !escape && block.nodes[i].isText && block.nodes[i-1].isText)
this.prettyIndent(1, false);
this.visit(block.nodes[i]);
// Multiple text nodes are separated by newlines
if (block.nodes[i+1] && block.nodes[i].isText && block.nodes[i+1].isText)
this.buffer('\n');
}
},
/**
* Visit a mixin's `block` keyword.
*
* @param {MixinBlock} block
* @api public
*/
visitMixinBlock: function(block){
if (this.pp) this.buf.push("jade_indent.push('" + Array(this.indents + 1).join(this.pp) + "');");
this.buf.push('block && block();');
if (this.pp) this.buf.push("jade_indent.pop();");
},
/**
* Visit `doctype`. Sets terse mode to `true` when html 5
* is used, causing self-closing tags to end with ">" vs "/>",
* and boolean attributes are not mirrored.
*
* @param {Doctype} doctype
* @api public
*/
visitDoctype: function(doctype){
if (doctype && (doctype.val || !this.doctype)) {
this.setDoctype(doctype.val || 'default');
}
if (this.doctype) this.buffer(this.doctype);
this.hasCompiledDoctype = true;
},
/**
* Visit `mixin`, generating a function that
* may be called within the template.
*
* @param {Mixin} mixin
* @api public
*/
visitMixin: function(mixin){
var name = 'jade_mixins[';
var args = mixin.args || '';
var block = mixin.block;
var attrs = mixin.attrs;
var attrsBlocks = mixin.attributeBlocks.slice();
var pp = this.pp;
var dynamic = mixin.name[0]==='#';
var key = mixin.name;
if (dynamic) this.dynamicMixins = true;
name += (dynamic ? mixin.name.substr(2,mixin.name.length-3):'"'+mixin.name+'"')+']';
this.mixins[key] = this.mixins[key] || {used: false, instances: []};
if (mixin.call) {
this.mixins[key].used = true;
if (pp) this.buf.push("jade_indent.push('" + Array(this.indents + 1).join(pp) + "');")
if (block || attrs.length || attrsBlocks.length) {
this.buf.push(name + '.call({');
if (block) {
this.buf.push('block: function(){');
// Render block with no indents, dynamically added when rendered
this.parentIndents++;
var _indents = this.indents;
this.indents = 0;
this.visit(mixin.block);
this.indents = _indents;
this.parentIndents--;
if (attrs.length || attrsBlocks.length) {
this.buf.push('},');
} else {
this.buf.push('}');
}
}
if (attrsBlocks.length) {
if (attrs.length) {
var val = this.attrs(attrs);
attrsBlocks.unshift(val);
}
this.buf.push('attributes: jade.merge([' + attrsBlocks.join(',') + '])');
} else if (attrs.length) {
var val = this.attrs(attrs);
this.buf.push('attributes: ' + val);
}
if (args) {
this.buf.push('}, ' + args + ');');
} else {
this.buf.push('});');
}
} else {
this.buf.push(name + '(' + args + ');');
}
if (pp) this.buf.push("jade_indent.pop();")
} else {
var mixin_start = this.buf.length;
args = args ? args.split(',') : [];
var rest;
if (args.length && /^\.\.\./.test(args[args.length - 1].trim())) {
rest = args.pop().trim().replace(/^\.\.\./, '');
}
// we need use jade_interp here for v8: https://code.google.com/p/v8/issues/detail?id=4165
// once fixed, use this: this.buf.push(name + ' = function(' + args.join(',') + '){');
this.buf.push(name + ' = jade_interp = function(' + args.join(',') + '){');
this.buf.push('var block = (this && this.block), attributes = (this && this.attributes) || {};');
if (rest) {
this.buf.push('var ' + rest + ' = [];');
this.buf.push('for (jade_interp = ' + args.length + '; jade_interp < arguments.length; jade_interp++) {');
this.buf.push(' ' + rest + '.push(arguments[jade_interp]);');
this.buf.push('}');
}
this.parentIndents++;
this.visit(block);
this.parentIndents--;
this.buf.push('};');
var mixin_end = this.buf.length;
this.mixins[key].instances.push({start: mixin_start, end: mixin_end});
}
},
/**
* Visit `tag` buffering tag markup, generating
* attributes, visiting the `tag`'s code and block.
*
* @param {Tag} tag
* @api public
*/
visitTag: function(tag){
this.indents++;
var name = tag.name
, pp = this.pp
, self = this;
function bufferName() {
if (tag.buffer) self.bufferExpression(name);
else self.buffer(name);
}
if ('pre' == tag.name) this.escape = true;
if (!this.hasCompiledTag) {
if (!this.hasCompiledDoctype && 'html' == name) {
this.visitDoctype();
}
this.hasCompiledTag = true;
}
// pretty print
if (pp && !tag.isInline())
this.prettyIndent(0, true);
if (tag.selfClosing || (!this.xml && selfClosing[tag.name])) {
this.buffer('<');
bufferName();
this.visitAttributes(tag.attrs, tag.attributeBlocks.slice());
this.terse
? this.buffer('>')
: this.buffer('/>');
// if it is non-empty throw an error
if (tag.block &&
!(tag.block.type === 'Block' && tag.block.nodes.length === 0) &&
tag.block.nodes.some(function (tag) {
return tag.type !== 'Text' || !/^\s*$/.test(tag.val)
})) {
throw errorAtNode(tag, new Error(name + ' is self closing and should not have content.'));
}
} else {
// Optimize attributes buffering
this.buffer('<');
bufferName();
this.visitAttributes(tag.attrs, tag.attributeBlocks.slice());
this.buffer('>');
if (tag.code) this.visitCode(tag.code);
this.visit(tag.block);
// pretty print
if (pp && !tag.isInline() && 'pre' != tag.name && !tag.canInline())
this.prettyIndent(0, true);
this.buffer('</');
bufferName();
this.buffer('>');
}
if ('pre' == tag.name) this.escape = false;
this.indents--;
},
/**
* Visit `filter`, throwing when the filter does not exist.
*
* @param {Filter} filter
* @api public
*/
visitFilter: function(filter){
var text = filter.block.nodes.map(
function(node){ return node.val; }
).join('\n');
filter.attrs.filename = this.options.filename;
try {
this.buffer(filters(filter.name, text, filter.attrs), true);
} catch (err) {
throw errorAtNode(filter, err);
}
},
/**
* Visit `text` node.
*
* @param {Text} text
* @api public
*/
visitText: function(text){
this.buffer(text.val, true);
},
/**
* Visit a `comment`, only buffering when the buffer flag is set.
*
* @param {Comment} comment
* @api public
*/
visitComment: function(comment){
if (!comment.buffer) return;
if (this.pp) this.prettyIndent(1, true);
this.buffer('<!--' + comment.val + '-->');
},
/**
* Visit a `BlockComment`.
*
* @param {Comment} comment
* @api public
*/
visitBlockComment: function(comment){
if (!comment.buffer) return;
if (this.pp) this.prettyIndent(1, true);
this.buffer('<!--' + comment.val);
this.visit(comment.block);
if (this.pp) this.prettyIndent(1, true);
this.buffer('-->');
},
/**
* Visit `code`, respecting buffer / escape flags.
* If the code is followed by a block, wrap it in
* a self-calling function.
*
* @param {Code} code
* @api public
*/
visitCode: function(code){
// Wrap code blocks with {}.
// we only wrap unbuffered code blocks ATM
// since they are usually flow control
// Buffer code
if (code.buffer) {
var val = code.val.trim();
val = 'null == (jade_interp = '+val+') ? "" : jade_interp';
if (code.escape) val = 'jade.escape(' + val + ')';
this.bufferExpression(val);
} else {
this.buf.push(code.val);
}
// Block support
if (code.block) {
if (!code.buffer) this.buf.push('{');
this.visit(code.block);
if (!code.buffer) this.buf.push('}');
}
},
/**
* Visit `each` block.
*
* @param {Each} each
* @api public
*/
visitEach: function(each){
this.buf.push(''
+ '// iterate ' + each.obj + '\n'
+ ';(function(){\n'
+ ' var $$obj = ' + each.obj + ';\n'
+ ' if (\'number\' == typeof $$obj.length) {\n');
if (each.alternative) {
this.buf.push(' if ($$obj.length) {');
}
this.buf.push(''
+ ' for (var ' + each.key + ' = 0, $$l = $$obj.length; ' + each.key + ' < $$l; ' + each.key + '++) {\n'
+ ' var ' + each.val + ' = $$obj[' + each.key + '];\n');
this.visit(each.block);
this.buf.push(' }\n');
if (each.alternative) {
this.buf.push(' } else {');
this.visit(each.alternative);
this.buf.push(' }');
}
this.buf.push(''
+ ' } else {\n'
+ ' var $$l = 0;\n'
+ ' for (var ' + each.key + ' in $$obj) {\n'
+ ' $$l++;'
+ ' var ' + each.val + ' = $$obj[' + each.key + '];\n');
this.visit(each.block);
this.buf.push(' }\n');
if (each.alternative) {
this.buf.push(' if ($$l === 0) {');
this.visit(each.alternative);
this.buf.push(' }');
}
this.buf.push(' }\n}).call(this);\n');
},
/**
* Visit `attrs`.
*
* @param {Array} attrs
* @api public
*/
visitAttributes: function(attrs, attributeBlocks){
if (attributeBlocks.length) {
if (attrs.length) {
var val = this.attrs(attrs);
attributeBlocks.unshift(val);
}
this.bufferExpression('jade.attrs(jade.merge([' + attributeBlocks.join(',') + ']), ' + utils.stringify(this.terse) + ')');
} else if (attrs.length) {
this.attrs(attrs, true);
}
},
/**
* Compile attributes.
*/
attrs: function(attrs, buffer){
var buf = [];
var classes = [];
var classEscaping = [];
attrs.forEach(function(attr){
var key = attr.name;
var escaped = attr.escaped;
if (key === 'class') {
classes.push(attr.val);
classEscaping.push(attr.escaped);
} else if (isConstant(attr.val)) {
if (buffer) {
this.buffer(runtime.attr(key, toConstant(attr.val), escaped, this.terse));
} else {
var val = toConstant(attr.val);
if (key === 'style') val = runtime.style(val);
if (escaped && !(key.indexOf('data') === 0 && typeof val !== 'string')) {
val = runtime.escape(val);
}
buf.push(utils.stringify(key) + ': ' + utils.stringify(val));
}
} else {
if (buffer) {
this.bufferExpression('jade.attr("' + key + '", ' + attr.val + ', ' + utils.stringify(escaped) + ', ' + utils.stringify(this.terse) + ')');
} else {
var val = attr.val;
if (key === 'style') {
val = 'jade.style(' + val + ')';
}
if (escaped && !(key.indexOf('data') === 0)) {
val = 'jade.escape(' + val + ')';
} else if (escaped) {
val = '(typeof (jade_interp = ' + val + ') == "string" ? jade.escape(jade_interp) : jade_interp)';
}
buf.push(utils.stringify(key) + ': ' + val);
}
}
}.bind(this));
if (buffer) {
if (classes.every(isConstant)) {
this.buffer(runtime.cls(classes.map(toConstant), classEscaping));
} else {
this.bufferExpression('jade.cls([' + classes.join(',') + '], ' + utils.stringify(classEscaping) + ')');
}
} else if (classes.length) {
if (classes.every(isConstant)) {
classes = utils.stringify(runtime.joinClasses(classes.map(toConstant).map(runtime.joinClasses).map(function (cls, i) {
return classEscaping[i] ? runtime.escape(cls) : cls;
})));
} else {
classes = '(jade_interp = ' + utils.stringify(classEscaping) + ',' +
' jade.joinClasses([' + classes.join(',') + '].map(jade.joinClasses).map(function (cls, i) {' +
' return jade_interp[i] ? jade.escape(cls) : cls' +
' }))' +
')';
}
if (classes.length)
buf.push('"class": ' + classes);
}
return '{' + buf.join(',') + '}';
}
};
+12
View File
@@ -0,0 +1,12 @@
'use strict';
module.exports = {
'default': '<!DOCTYPE html>'
, 'xml': '<?xml version="1.0" encoding="utf-8" ?>'
, 'transitional': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'
, 'strict': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
, 'frameset': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">'
, '1.1': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">'
, 'basic': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">'
, 'mobile': '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">'
};
+10
View File
@@ -0,0 +1,10 @@
'use strict';
module.exports = filter;
function filter(name, str, options) {
if (typeof filter[name] === 'function') {
return filter[name](str, options);
} else {
throw new Error('unknown filter ":' + name + '"');
}
}
+96
View File
@@ -0,0 +1,96 @@
'use strict';
var transformers = require('transformers');
var jstransformer = require('jstransformer');
var uglify = require('uglify-js');
var CleanCSS = require('clean-css');
var warned = {};
var alternatives = {
uglifyJS: 'uglify-js',
uglify: 'uglify-js',
uglifyCSS: 'clean-css',
'uglify-css': 'clean-css' ,
uglifyJSON: 'json',
'uglify-json': 'json',
live: 'livescript',
LiveScript: 'livescript',
ls: 'livescript',
// TODO: remove if we add support for coffeekup
coffeekup: 'coffeecup',
// The `style` transformer is not the same as the `stylus` jstransformer
styl: 'stylus',
coffee: 'coffee-script',
coffeescript: 'coffee-script',
coffeeScript: 'coffee-script',
// these marker transformers haven't made sense in a long time
css: 'verbatim',
js: 'verbatim',
};
var deprecated = ['jqtpl', 'jazz'];
function getMarkdownImplementation() {
var implementations = ['marked', 'supermarked', 'markdown-js', 'markdown'];
while (implementations.length) {
try {
require(implementations[0]);
return implementations[0];
} catch (ex) {
implementations.shift();
}
}
return 'markdown-it';
}
module.exports = filter;
function filter(name, str, options) {
if (typeof filter[name] === 'function') {
return filter[name](str, options);
} else {
var tr;
try {
tr = jstransformer(require('jstransformer-' + name));
} catch (ex) {}
if (tr) {
// TODO: we may want to add a way for people to separately specify "locals"
var result = tr.render(str, options, options).body;
if (options && options.minify) {
try {
switch (tr.outputFormat) {
case 'js':
result = uglify.minify(result, {fromString: true}).code;
break;
case 'css':
result = new CleanCSS().minify(result).styles;
break;
}
} catch (ex) {
// better to fail to minify than output nothing
}
}
return result;
} else if (transformers[name]) {
if (!warned[name]) {
warned[name] = true;
if (name === 'md' || name === 'markdown') {
var implementation = getMarkdownImplementation();
console.log('Transformers.' + name + ' is deprecated, you must replace the :' +
name + ' jade filter, with :' +
implementation + ' and install jstransformer-' +
implementation + ' before you update to jade@2.0.0.');
} else if (alternatives[name]) {
console.log('Transformers.' + name + ' is deprecated, you must replace the :' +
name + ' jade filter, with :' +
alternatives[name] + ' and install jstransformer-' +
alternatives[name] + ' before you update to jade@2.0.0.');
} else {
console.log('Transformers.' + name + ' is deprecated, to continue using the :' +
name + ' jade filter after jade@2.0.0, you will need to install jstransformer-' +
name.toLowerCase() + '.');
}
}
return transformers[name].renderSync(str, options);
} else {
throw new Error('unknown filter ":' + name + '"');
}
}
}
+418
View File
@@ -0,0 +1,418 @@
'use strict';
/*!
* Jade
* Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca>
* MIT Licensed
*/
/**
* Module dependencies.
*/
var Parser = require('./parser')
, Lexer = require('./lexer')
, Compiler = require('./compiler')
, runtime = require('./runtime')
, addWith = require('with')
, fs = require('fs')
, utils = require('./utils');
/**
* Expose self closing tags.
*/
// FIXME: either stop exporting selfClosing in v2 or export the new object
// form
exports.selfClosing = Object.keys(require('void-elements'));
/**
* Default supported doctypes.
*/
exports.doctypes = require('./doctypes');
/**
* Text filters.
*/
exports.filters = require('./filters');
/**
* Utilities.
*/
exports.utils = utils;
/**
* Expose `Compiler`.
*/
exports.Compiler = Compiler;
/**
* Expose `Parser`.
*/
exports.Parser = Parser;
/**
* Expose `Lexer`.
*/
exports.Lexer = Lexer;
/**
* Nodes.
*/
exports.nodes = require('./nodes');
/**
* Jade runtime helpers.
*/
exports.runtime = runtime;
/**
* Template function cache.
*/
exports.cache = {};
/**
* Parse the given `str` of jade and return a function body.
*
* @param {String} str
* @param {Object} options
* @return {Object}
* @api private
*/
function parse(str, options){
if (options.lexer) {
console.warn('Using `lexer` as a local in render() is deprecated and '
+ 'will be interpreted as an option in Jade 2.0.0');
}
// Parse
var parser = new (options.parser || Parser)(str, options.filename, options);
var tokens;
try {
// Parse
tokens = parser.parse();
} catch (err) {
parser = parser.context();
runtime.rethrow(err, parser.filename, parser.lexer.lineno, parser.input);
}
// Compile
var compiler = new (options.compiler || Compiler)(tokens, options);
var js;
try {
js = compiler.compile();
} catch (err) {
if (err.line && (err.filename || !options.filename)) {
runtime.rethrow(err, err.filename, err.line, parser.input);
} else {
if (err instanceof Error) {
err.message += '\n\nPlease report this entire error and stack trace to https://github.com/jadejs/jade/issues';
}
throw err;
}
}
// Debug compiler
if (options.debug) {
console.error('\nCompiled Function:\n\n\u001b[90m%s\u001b[0m', js.replace(/^/gm, ' '));
}
var globals = [];
if (options.globals) {
globals = options.globals.slice();
}
globals.push('jade');
globals.push('jade_mixins');
globals.push('jade_interp');
globals.push('jade_debug');
globals.push('buf');
var body = ''
+ 'var buf = [];\n'
+ 'var jade_mixins = {};\n'
+ 'var jade_interp;\n'
+ (options.self
? 'var self = locals || {};\n' + js
: addWith('locals || {}', '\n' + js, globals)) + ';'
+ 'return buf.join("");';
return {body: body, dependencies: parser.dependencies};
}
/**
* Get the template from a string or a file, either compiled on-the-fly or
* read from cache (if enabled), and cache the template if needed.
*
* If `str` is not set, the file specified in `options.filename` will be read.
*
* If `options.cache` is true, this function reads the file from
* `options.filename` so it must be set prior to calling this function.
*
* @param {Object} options
* @param {String=} str
* @return {Function}
* @api private
*/
function handleTemplateCache (options, str) {
var key = options.filename;
if (options.cache && exports.cache[key]) {
return exports.cache[key];
} else {
if (str === undefined) str = fs.readFileSync(options.filename, 'utf8');
var templ = exports.compile(str, options);
if (options.cache) exports.cache[key] = templ;
return templ;
}
}
/**
* Compile a `Function` representation of the given jade `str`.
*
* Options:
*
* - `compileDebug` when `false` debugging code is stripped from the compiled
template, when it is explicitly `true`, the source code is included in
the compiled template for better accuracy.
* - `filename` used to improve errors when `compileDebug` is not `false` and to resolve imports/extends
*
* @param {String} str
* @param {Options} options
* @return {Function}
* @api public
*/
exports.compile = function(str, options){
var options = options || {}
, filename = options.filename
? utils.stringify(options.filename)
: 'undefined'
, fn;
str = String(str);
var parsed = parse(str, options);
if (options.compileDebug !== false) {
fn = [
'var jade_debug = [ new jade.DebugItem( 1, ' + filename + ' ) ];'
, 'try {'
, parsed.body
, '} catch (err) {'
, ' jade.rethrow(err, jade_debug[0].filename, jade_debug[0].lineno' + (options.compileDebug === true ? ',' + utils.stringify(str) : '') + ');'
, '}'
].join('\n');
} else {
fn = parsed.body;
}
fn = new Function('locals, jade', fn)
var res = function(locals){ return fn(locals, Object.create(runtime)) };
if (options.client) {
res.toString = function () {
var err = new Error('The `client` option is deprecated, use the `jade.compileClient` method instead');
err.name = 'Warning';
console.error(err.stack || /* istanbul ignore next */ err.message);
return exports.compileClient(str, options);
};
}
res.dependencies = parsed.dependencies;
return res;
};
/**
* Compile a JavaScript source representation of the given jade `str`.
*
* Options:
*
* - `compileDebug` When it is `true`, the source code is included in
* the compiled template for better error messages.
* - `filename` used to improve errors when `compileDebug` is not `true` and to resolve imports/extends
* - `name` the name of the resulting function (defaults to "template")
*
* @param {String} str
* @param {Options} options
* @return {Object}
* @api public
*/
exports.compileClientWithDependenciesTracked = function(str, options){
var options = options || {};
var name = options.name || 'template';
var filename = options.filename ? utils.stringify(options.filename) : 'undefined';
var fn;
str = String(str);
options.compileDebug = options.compileDebug ? true : false;
var parsed = parse(str, options);
if (options.compileDebug) {
fn = [
'var jade_debug = [ new jade.DebugItem( 1, ' + filename + ' ) ];'
, 'try {'
, parsed.body
, '} catch (err) {'
, ' jade.rethrow(err, jade_debug[0].filename, jade_debug[0].lineno, ' + utils.stringify(str) + ');'
, '}'
].join('\n');
} else {
fn = parsed.body;
}
return {body: 'function ' + name + '(locals) {\n' + fn + '\n}', dependencies: parsed.dependencies};
};
/**
* Compile a JavaScript source representation of the given jade `str`.
*
* Options:
*
* - `compileDebug` When it is `true`, the source code is included in
* the compiled template for better error messages.
* - `filename` used to improve errors when `compileDebug` is not `true` and to resolve imports/extends
* - `name` the name of the resulting function (defaults to "template")
*
* @param {String} str
* @param {Options} options
* @return {String}
* @api public
*/
exports.compileClient = function (str, options) {
return exports.compileClientWithDependenciesTracked(str, options).body;
};
/**
* Compile a `Function` representation of the given jade file.
*
* Options:
*
* - `compileDebug` when `false` debugging code is stripped from the compiled
template, when it is explicitly `true`, the source code is included in
the compiled template for better accuracy.
*
* @param {String} path
* @param {Options} options
* @return {Function}
* @api public
*/
exports.compileFile = function (path, options) {
options = options || {};
options.filename = path;
return handleTemplateCache(options);
};
/**
* Render the given `str` of jade.
*
* Options:
*
* - `cache` enable template caching
* - `filename` filename required for `include` / `extends` and caching
*
* @param {String} str
* @param {Object|Function} options or fn
* @param {Function|undefined} fn
* @returns {String}
* @api public
*/
exports.render = function(str, options, fn){
// support callback API
if ('function' == typeof options) {
fn = options, options = undefined;
}
if (typeof fn === 'function') {
var res
try {
res = exports.render(str, options);
} catch (ex) {
return fn(ex);
}
return fn(null, res);
}
options = options || {};
// cache requires .filename
if (options.cache && !options.filename) {
throw new Error('the "filename" option is required for caching');
}
return handleTemplateCache(options, str)(options);
};
/**
* Render a Jade file at the given `path`.
*
* @param {String} path
* @param {Object|Function} options or callback
* @param {Function|undefined} fn
* @returns {String}
* @api public
*/
exports.renderFile = function(path, options, fn){
// support callback API
if ('function' == typeof options) {
fn = options, options = undefined;
}
if (typeof fn === 'function') {
var res
try {
res = exports.renderFile(path, options);
} catch (ex) {
return fn(ex);
}
return fn(null, res);
}
options = options || {};
options.filename = path;
return handleTemplateCache(options)(options);
};
/**
* Compile a Jade file at the given `path` for use on the client.
*
* @param {String} path
* @param {Object} options
* @returns {String}
* @api public
*/
exports.compileFileClient = function(path, options){
var key = path + ':client';
options = options || {};
options.filename = path;
if (options.cache && exports.cache[key]) {
return exports.cache[key];
}
var str = fs.readFileSync(options.filename, 'utf8');
var out = exports.compileClient(str, options);
if (options.cache) exports.cache[key] = out;
return out;
};
/**
* Express support.
*/
exports.__express = function(path, options, fn) {
if(options.compileDebug == undefined && process.env.NODE_ENV === 'production') {
options.compileDebug = false;
}
exports.renderFile(path, options, fn);
}
+23
View File
@@ -0,0 +1,23 @@
'use strict';
module.exports = [
'a'
, 'abbr'
, 'acronym'
, 'b'
, 'br'
, 'code'
, 'em'
, 'font'
, 'i'
, 'img'
, 'ins'
, 'kbd'
, 'map'
, 'samp'
, 'small'
, 'span'
, 'strong'
, 'sub'
, 'sup'
];
+949
View File
@@ -0,0 +1,949 @@
'use strict';
var utils = require('./utils');
var characterParser = require('character-parser');
/**
* Initialize `Lexer` with the given `str`.
*
* @param {String} str
* @param {String} filename
* @api private
*/
var Lexer = module.exports = function Lexer(str, filename) {
this.input = str.replace(/\r\n|\r/g, '\n');
this.filename = filename;
this.deferredTokens = [];
this.lastIndents = 0;
this.lineno = 1;
this.stash = [];
this.indentStack = [];
this.indentRe = null;
this.pipeless = false;
};
function assertExpression(exp) {
//this verifies that a JavaScript expression is valid
Function('', 'return (' + exp + ')');
}
function assertNestingCorrect(exp) {
//this verifies that code is properly nested, but allows
//invalid JavaScript such as the contents of `attributes`
var res = characterParser(exp)
if (res.isNesting()) {
throw new Error('Nesting must match on expression `' + exp + '`')
}
}
/**
* Lexer prototype.
*/
Lexer.prototype = {
/**
* Construct a token with the given `type` and `val`.
*
* @param {String} type
* @param {String} val
* @return {Object}
* @api private
*/
tok: function(type, val){
return {
type: type
, line: this.lineno
, val: val
}
},
/**
* Consume the given `len` of input.
*
* @param {Number} len
* @api private
*/
consume: function(len){
this.input = this.input.substr(len);
},
/**
* Scan for `type` with the given `regexp`.
*
* @param {String} type
* @param {RegExp} regexp
* @return {Object}
* @api private
*/
scan: function(regexp, type){
var captures;
if (captures = regexp.exec(this.input)) {
this.consume(captures[0].length);
return this.tok(type, captures[1]);
}
},
/**
* Defer the given `tok`.
*
* @param {Object} tok
* @api private
*/
defer: function(tok){
this.deferredTokens.push(tok);
},
/**
* Lookahead `n` tokens.
*
* @param {Number} n
* @return {Object}
* @api private
*/
lookahead: function(n){
var fetch = n - this.stash.length;
while (fetch-- > 0) this.stash.push(this.next());
return this.stash[--n];
},
/**
* Return the indexOf `(` or `{` or `[` / `)` or `}` or `]` delimiters.
*
* @return {Number}
* @api private
*/
bracketExpression: function(skip){
skip = skip || 0;
var start = this.input[skip];
if (start != '(' && start != '{' && start != '[') throw new Error('unrecognized start character');
var end = ({'(': ')', '{': '}', '[': ']'})[start];
var range = characterParser.parseMax(this.input, {start: skip + 1});
if (this.input[range.end] !== end) throw new Error('start character ' + start + ' does not match end character ' + this.input[range.end]);
return range;
},
/**
* Stashed token.
*/
stashed: function() {
return this.stash.length
&& this.stash.shift();
},
/**
* Deferred token.
*/
deferred: function() {
return this.deferredTokens.length
&& this.deferredTokens.shift();
},
/**
* end-of-source.
*/
eos: function() {
if (this.input.length) return;
if (this.indentStack.length) {
this.indentStack.shift();
return this.tok('outdent');
} else {
return this.tok('eos');
}
},
/**
* Blank line.
*/
blank: function() {
var captures;
if (captures = /^\n *\n/.exec(this.input)) {
this.consume(captures[0].length - 1);
++this.lineno;
if (this.pipeless) return this.tok('text', '');
return this.next();
}
},
/**
* Comment.
*/
comment: function() {
var captures;
if (captures = /^\/\/(-)?([^\n]*)/.exec(this.input)) {
this.consume(captures[0].length);
var tok = this.tok('comment', captures[2]);
tok.buffer = '-' != captures[1];
this.pipeless = true;
return tok;
}
},
/**
* Interpolated tag.
*/
interpolation: function() {
if (/^#\{/.test(this.input)) {
var match = this.bracketExpression(1);
this.consume(match.end + 1);
return this.tok('interpolation', match.src);
}
},
/**
* Tag.
*/
tag: function() {
var captures;
if (captures = /^(\w[-:\w]*)(\/?)/.exec(this.input)) {
this.consume(captures[0].length);
var tok, name = captures[1];
if (':' == name[name.length - 1]) {
name = name.slice(0, -1);
tok = this.tok('tag', name);
this.defer(this.tok(':'));
if (this.input[0] !== ' ') {
console.warn('Warning: space required after `:` on line ' + this.lineno +
' of jade file "' + this.filename + '"');
}
while (' ' == this.input[0]) this.input = this.input.substr(1);
} else {
tok = this.tok('tag', name);
}
tok.selfClosing = !!captures[2];
return tok;
}
},
/**
* Filter.
*/
filter: function() {
var tok = this.scan(/^:([\w\-]+)/, 'filter');
if (tok) {
this.pipeless = true;
return tok;
}
},
/**
* Doctype.
*/
doctype: function() {
if (this.scan(/^!!! *([^\n]+)?/, 'doctype')) {
throw new Error('`!!!` is deprecated, you must now use `doctype`');
}
var node = this.scan(/^(?:doctype) *([^\n]+)?/, 'doctype');
if (node && node.val && node.val.trim() === '5') {
throw new Error('`doctype 5` is deprecated, you must now use `doctype html`');
}
return node;
},
/**
* Id.
*/
id: function() {
return this.scan(/^#([\w-]+)/, 'id');
},
/**
* Class.
*/
className: function() {
return this.scan(/^\.([\w-]+)/, 'class');
},
/**
* Text.
*/
text: function() {
return this.scan(/^(?:\| ?| )([^\n]+)/, 'text') ||
this.scan(/^\|?( )/, 'text') ||
this.scan(/^(<[^\n]*)/, 'text');
},
textFail: function () {
var tok;
if (tok = this.scan(/^([^\.\n][^\n]+)/, 'text')) {
console.warn('Warning: missing space before text for line ' + this.lineno +
' of jade file "' + this.filename + '"');
return tok;
}
},
/**
* Dot.
*/
dot: function() {
var match;
if (match = this.scan(/^\./, 'dot')) {
this.pipeless = true;
return match;
}
},
/**
* Extends.
*/
"extends": function() {
return this.scan(/^extends? +([^\n]+)/, 'extends');
},
/**
* Block prepend.
*/
prepend: function() {
var captures;
if (captures = /^prepend +([^\n]+)/.exec(this.input)) {
this.consume(captures[0].length);
var mode = 'prepend'
, name = captures[1]
, tok = this.tok('block', name);
tok.mode = mode;
return tok;
}
},
/**
* Block append.
*/
append: function() {
var captures;
if (captures = /^append +([^\n]+)/.exec(this.input)) {
this.consume(captures[0].length);
var mode = 'append'
, name = captures[1]
, tok = this.tok('block', name);
tok.mode = mode;
return tok;
}
},
/**
* Block.
*/
block: function() {
var captures;
if (captures = /^block\b *(?:(prepend|append) +)?([^\n]+)/.exec(this.input)) {
this.consume(captures[0].length);
var mode = captures[1] || 'replace'
, name = captures[2]
, tok = this.tok('block', name);
tok.mode = mode;
return tok;
}
},
/**
* Mixin Block.
*/
mixinBlock: function() {
var captures;
if (captures = /^block[ \t]*(\n|$)/.exec(this.input)) {
this.consume(captures[0].length - captures[1].length);
return this.tok('mixin-block');
}
},
/**
* Yield.
*/
'yield': function() {
return this.scan(/^yield */, 'yield');
},
/**
* Include.
*/
include: function() {
return this.scan(/^include +([^\n]+)/, 'include');
},
/**
* Include with filter
*/
includeFiltered: function() {
var captures;
if (captures = /^include:([\w\-]+)([\( ])/.exec(this.input)) {
this.consume(captures[0].length - 1);
var filter = captures[1];
var attrs = captures[2] === '(' ? this.attrs() : null;
if (!(captures[2] === ' ' || this.input[0] === ' ')) {
throw new Error('expected space after include:filter but got ' + utils.stringify(this.input[0]));
}
captures = /^ *([^\n]+)/.exec(this.input);
if (!captures || captures[1].trim() === '') {
throw new Error('missing path for include:filter');
}
this.consume(captures[0].length);
var path = captures[1];
var tok = this.tok('include', path);
tok.filter = filter;
tok.attrs = attrs;
return tok;
}
},
/**
* Case.
*/
"case": function() {
return this.scan(/^case +([^\n]+)/, 'case');
},
/**
* When.
*/
when: function() {
return this.scan(/^when +([^:\n]+)/, 'when');
},
/**
* Default.
*/
"default": function() {
return this.scan(/^default */, 'default');
},
/**
* Call mixin.
*/
call: function(){
var tok, captures;
if (captures = /^\+(\s*)(([-\w]+)|(#\{))/.exec(this.input)) {
// try to consume simple or interpolated call
if (captures[3]) {
// simple call
this.consume(captures[0].length);
tok = this.tok('call', captures[3]);
} else {
// interpolated call
var match = this.bracketExpression(2 + captures[1].length);
this.consume(match.end + 1);
assertExpression(match.src);
tok = this.tok('call', '#{'+match.src+'}');
}
// Check for args (not attributes)
if (captures = /^ *\(/.exec(this.input)) {
var range = this.bracketExpression(captures[0].length - 1);
if (!/^\s*[-\w]+ *=/.test(range.src)) { // not attributes
this.consume(range.end + 1);
tok.args = range.src;
}
if (tok.args) {
assertExpression('[' + tok.args + ']');
}
}
return tok;
}
},
/**
* Mixin.
*/
mixin: function(){
var captures;
if (captures = /^mixin +([-\w]+)(?: *\((.*)\))? */.exec(this.input)) {
this.consume(captures[0].length);
var tok = this.tok('mixin', captures[1]);
tok.args = captures[2];
return tok;
}
},
/**
* Conditional.
*/
conditional: function() {
var captures;
if (captures = /^(if|unless|else if|else)\b([^\n]*)/.exec(this.input)) {
this.consume(captures[0].length);
var type = captures[1]
var js = captures[2];
var isIf = false;
var isElse = false;
switch (type) {
case 'if':
assertExpression(js)
js = 'if (' + js + ')';
isIf = true;
break;
case 'unless':
assertExpression(js)
js = 'if (!(' + js + '))';
isIf = true;
break;
case 'else if':
assertExpression(js)
js = 'else if (' + js + ')';
isIf = true;
isElse = true;
break;
case 'else':
if (js && js.trim()) {
throw new Error('`else` cannot have a condition, perhaps you meant `else if`');
}
js = 'else';
isElse = true;
break;
}
var tok = this.tok('code', js);
tok.isElse = isElse;
tok.isIf = isIf;
tok.requiresBlock = true;
return tok;
}
},
/**
* While.
*/
"while": function() {
var captures;
if (captures = /^while +([^\n]+)/.exec(this.input)) {
this.consume(captures[0].length);
assertExpression(captures[1])
var tok = this.tok('code', 'while (' + captures[1] + ')');
tok.requiresBlock = true;
return tok;
}
},
/**
* Each.
*/
each: function() {
var captures;
if (captures = /^(?:- *)?(?:each|for) +([a-zA-Z_$][\w$]*)(?: *, *([a-zA-Z_$][\w$]*))? * in *([^\n]+)/.exec(this.input)) {
this.consume(captures[0].length);
var tok = this.tok('each', captures[1]);
tok.key = captures[2] || '$index';
assertExpression(captures[3])
tok.code = captures[3];
return tok;
}
},
/**
* Code.
*/
code: function() {
var captures;
if (captures = /^(!?=|-)[ \t]*([^\n]+)/.exec(this.input)) {
this.consume(captures[0].length);
var flags = captures[1];
captures[1] = captures[2];
var tok = this.tok('code', captures[1]);
tok.escape = flags.charAt(0) === '=';
tok.buffer = flags.charAt(0) === '=' || flags.charAt(1) === '=';
if (tok.buffer) assertExpression(captures[1])
return tok;
}
},
/**
* Block code.
*/
blockCode: function() {
var captures;
if (captures = /^-\n/.exec(this.input)) {
this.consume(captures[0].length - 1);
var tok = this.tok('blockCode');
this.pipeless = true;
return tok;
}
},
/**
* Attributes.
*/
attrs: function() {
if ('(' == this.input.charAt(0)) {
var index = this.bracketExpression().end
, str = this.input.substr(1, index-1)
, tok = this.tok('attrs');
assertNestingCorrect(str);
var quote = '';
var interpolate = function (attr) {
return attr.replace(/(\\)?#\{(.+)/g, function(_, escape, expr){
if (escape) return _;
try {
var range = characterParser.parseMax(expr);
if (expr[range.end] !== '}') return _.substr(0, 2) + interpolate(_.substr(2));
assertExpression(range.src)
return quote + " + (" + range.src + ") + " + quote + interpolate(expr.substr(range.end + 1));
} catch (ex) {
return _.substr(0, 2) + interpolate(_.substr(2));
}
});
}
this.consume(index + 1);
tok.attrs = [];
var escapedAttr = true
var key = '';
var val = '';
var interpolatable = '';
var state = characterParser.defaultState();
var loc = 'key';
var isEndOfAttribute = function (i) {
if (key.trim() === '') return false;
if (i === str.length) return true;
if (loc === 'key') {
if (str[i] === ' ' || str[i] === '\n') {
for (var x = i; x < str.length; x++) {
if (str[x] != ' ' && str[x] != '\n') {
if (str[x] === '=' || str[x] === '!' || str[x] === ',') return false;
else return true;
}
}
}
return str[i] === ','
} else if (loc === 'value' && !state.isNesting()) {
try {
assertExpression(val);
if (str[i] === ' ' || str[i] === '\n') {
for (var x = i; x < str.length; x++) {
if (str[x] != ' ' && str[x] != '\n') {
if (characterParser.isPunctuator(str[x]) && str[x] != '"' && str[x] != "'") return false;
else return true;
}
}
}
return str[i] === ',';
} catch (ex) {
return false;
}
}
}
this.lineno += str.split("\n").length - 1;
for (var i = 0; i <= str.length; i++) {
if (isEndOfAttribute(i)) {
val = val.trim();
if (val) assertExpression(val)
key = key.trim();
key = key.replace(/^['"]|['"]$/g, '');
tok.attrs.push({
name: key,
val: '' == val ? true : val,
escaped: escapedAttr
});
key = val = '';
loc = 'key';
escapedAttr = false;
} else {
switch (loc) {
case 'key-char':
if (str[i] === quote) {
loc = 'key';
if (i + 1 < str.length && [' ', ',', '!', '=', '\n'].indexOf(str[i + 1]) === -1)
throw new Error('Unexpected character ' + str[i + 1] + ' expected ` `, `\\n`, `,`, `!` or `=`');
} else {
key += str[i];
}
break;
case 'key':
if (key === '' && (str[i] === '"' || str[i] === "'")) {
loc = 'key-char';
quote = str[i];
} else if (str[i] === '!' || str[i] === '=') {
escapedAttr = str[i] !== '!';
if (str[i] === '!') i++;
if (str[i] !== '=') throw new Error('Unexpected character ' + str[i] + ' expected `=`');
loc = 'value';
state = characterParser.defaultState();
} else {
key += str[i]
}
break;
case 'value':
state = characterParser.parseChar(str[i], state);
if (state.isString()) {
loc = 'string';
quote = str[i];
interpolatable = str[i];
} else {
val += str[i];
}
break;
case 'string':
state = characterParser.parseChar(str[i], state);
interpolatable += str[i];
if (!state.isString()) {
loc = 'value';
val += interpolate(interpolatable);
}
break;
}
}
}
if ('/' == this.input.charAt(0)) {
this.consume(1);
tok.selfClosing = true;
}
return tok;
}
},
/**
* &attributes block
*/
attributesBlock: function () {
var captures;
if (/^&attributes\b/.test(this.input)) {
this.consume(11);
var args = this.bracketExpression();
this.consume(args.end + 1);
return this.tok('&attributes', args.src);
}
},
/**
* Indent | Outdent | Newline.
*/
indent: function() {
var captures, re;
// established regexp
if (this.indentRe) {
captures = this.indentRe.exec(this.input);
// determine regexp
} else {
// tabs
re = /^\n(\t*) */;
captures = re.exec(this.input);
// spaces
if (captures && !captures[1].length) {
re = /^\n( *)/;
captures = re.exec(this.input);
}
// established
if (captures && captures[1].length) this.indentRe = re;
}
if (captures) {
var tok
, indents = captures[1].length;
++this.lineno;
this.consume(indents + 1);
if (' ' == this.input[0] || '\t' == this.input[0]) {
throw new Error('Invalid indentation, you can use tabs or spaces but not both');
}
// blank line
if ('\n' == this.input[0]) {
this.pipeless = false;
return this.tok('newline');
}
// outdent
if (this.indentStack.length && indents < this.indentStack[0]) {
while (this.indentStack.length && this.indentStack[0] > indents) {
this.stash.push(this.tok('outdent'));
this.indentStack.shift();
}
tok = this.stash.pop();
// indent
} else if (indents && indents != this.indentStack[0]) {
this.indentStack.unshift(indents);
tok = this.tok('indent', indents);
// newline
} else {
tok = this.tok('newline');
}
this.pipeless = false;
return tok;
}
},
/**
* Pipe-less text consumed only when
* pipeless is true;
*/
pipelessText: function() {
if (!this.pipeless) return;
var captures, re;
// established regexp
if (this.indentRe) {
captures = this.indentRe.exec(this.input);
// determine regexp
} else {
// tabs
re = /^\n(\t*) */;
captures = re.exec(this.input);
// spaces
if (captures && !captures[1].length) {
re = /^\n( *)/;
captures = re.exec(this.input);
}
// established
if (captures && captures[1].length) this.indentRe = re;
}
var indents = captures && captures[1].length;
if (indents && (this.indentStack.length === 0 || indents > this.indentStack[0])) {
var indent = captures[1];
var line;
var tokens = [];
var isMatch;
do {
// text has `\n` as a prefix
var i = this.input.substr(1).indexOf('\n');
if (-1 == i) i = this.input.length - 1;
var str = this.input.substr(1, i);
isMatch = str.substr(0, indent.length) === indent || !str.trim();
if (isMatch) {
// consume test along with `\n` prefix if match
this.consume(str.length + 1);
++this.lineno;
tokens.push(str.substr(indent.length));
}
} while(this.input.length && isMatch);
while (this.input.length === 0 && tokens[tokens.length - 1] === '') tokens.pop();
return this.tok('pipeless-text', tokens);
}
},
/**
* ':'
*/
colon: function() {
var good = /^: +/.test(this.input);
var res = this.scan(/^: */, ':');
if (res && !good) {
console.warn('Warning: space required after `:` on line ' + this.lineno +
' of jade file "' + this.filename + '"');
}
return res;
},
fail: function () {
throw new Error('unexpected text ' + this.input.substr(0, 5));
},
/**
* Return the next token object, or those
* previously stashed by lookahead.
*
* @return {Object}
* @api private
*/
advance: function(){
return this.stashed()
|| this.next();
},
/**
* Return the next token object.
*
* @return {Object}
* @api private
*/
next: function() {
return this.deferred()
|| this.blank()
|| this.eos()
|| this.pipelessText()
|| this.yield()
|| this.doctype()
|| this.interpolation()
|| this["case"]()
|| this.when()
|| this["default"]()
|| this["extends"]()
|| this.append()
|| this.prepend()
|| this.block()
|| this.mixinBlock()
|| this.include()
|| this.includeFiltered()
|| this.mixin()
|| this.call()
|| this.conditional()
|| this.each()
|| this["while"]()
|| this.tag()
|| this.filter()
|| this.blockCode()
|| this.code()
|| this.id()
|| this.className()
|| this.attrs()
|| this.attributesBlock()
|| this.indent()
|| this.text()
|| this.comment()
|| this.colon()
|| this.dot()
|| this.textFail()
|| this.fail();
}
};
+83
View File
@@ -0,0 +1,83 @@
'use strict';
var Node = require('./node');
/**
* Initialize a `Attrs` node.
*
* @api public
*/
var Attrs = module.exports = function Attrs() {
this.attributeNames = [];
this.attrs = [];
this.attributeBlocks = [];
};
// Inherit from `Node`.
Attrs.prototype = Object.create(Node.prototype);
Attrs.prototype.constructor = Attrs;
Attrs.prototype.type = 'Attrs';
/**
* Set attribute `name` to `val`, keep in mind these become
* part of a raw js object literal, so to quote a value you must
* '"quote me"', otherwise or example 'user.name' is literal JavaScript.
*
* @param {String} name
* @param {String} val
* @param {Boolean} escaped
* @return {Tag} for chaining
* @api public
*/
Attrs.prototype.setAttribute = function(name, val, escaped){
if (name !== 'class' && this.attributeNames.indexOf(name) !== -1) {
throw new Error('Duplicate attribute "' + name + '" is not allowed.');
}
this.attributeNames.push(name);
this.attrs.push({ name: name, val: val, escaped: escaped });
return this;
};
/**
* Remove attribute `name` when present.
*
* @param {String} name
* @api public
*/
Attrs.prototype.removeAttribute = function(name){
var err = new Error('attrs.removeAttribute is deprecated and will be removed in v2.0.0');
console.warn(err.stack);
for (var i = 0, len = this.attrs.length; i < len; ++i) {
if (this.attrs[i] && this.attrs[i].name == name) {
delete this.attrs[i];
}
}
};
/**
* Get attribute value by `name`.
*
* @param {String} name
* @return {String}
* @api public
*/
Attrs.prototype.getAttribute = function(name){
var err = new Error('attrs.getAttribute is deprecated and will be removed in v2.0.0');
console.warn(err.stack);
for (var i = 0, len = this.attrs.length; i < len; ++i) {
if (this.attrs[i] && this.attrs[i].name == name) {
return this.attrs[i].val;
}
}
};
Attrs.prototype.addAttributes = function (src) {
this.attributeBlocks.push(src);
};
+24
View File
@@ -0,0 +1,24 @@
'use strict';
var Node = require('./node');
/**
* Initialize a `BlockComment` with the given `block`.
*
* @param {String} val
* @param {Block} block
* @param {Boolean} buffer
* @api public
*/
var BlockComment = module.exports = function BlockComment(val, block, buffer) {
this.block = block;
this.val = val;
this.buffer = buffer;
};
// Inherit from `Node`.
BlockComment.prototype = Object.create(Node.prototype);
BlockComment.prototype.constructor = BlockComment;
BlockComment.prototype.type = 'BlockComment';
+118
View File
@@ -0,0 +1,118 @@
'use strict';
var Node = require('./node');
/**
* Initialize a new `Block` with an optional `node`.
*
* @param {Node} node
* @api public
*/
var Block = module.exports = function Block(node){
this.nodes = [];
if (node) this.push(node);
};
// Inherit from `Node`.
Block.prototype = Object.create(Node.prototype);
Block.prototype.constructor = Block;
Block.prototype.type = 'Block';
/**
* Block flag.
*/
Block.prototype.isBlock = true;
/**
* Replace the nodes in `other` with the nodes
* in `this` block.
*
* @param {Block} other
* @api private
*/
Block.prototype.replace = function(other){
var err = new Error('block.replace is deprecated and will be removed in v2.0.0');
console.warn(err.stack);
other.nodes = this.nodes;
};
/**
* Push the given `node`.
*
* @param {Node} node
* @return {Number}
* @api public
*/
Block.prototype.push = function(node){
return this.nodes.push(node);
};
/**
* Check if this block is empty.
*
* @return {Boolean}
* @api public
*/
Block.prototype.isEmpty = function(){
return 0 == this.nodes.length;
};
/**
* Unshift the given `node`.
*
* @param {Node} node
* @return {Number}
* @api public
*/
Block.prototype.unshift = function(node){
return this.nodes.unshift(node);
};
/**
* Return the "last" block, or the first `yield` node.
*
* @return {Block}
* @api private
*/
Block.prototype.includeBlock = function(){
var ret = this
, node;
for (var i = 0, len = this.nodes.length; i < len; ++i) {
node = this.nodes[i];
if (node.yield) return node;
else if (node.textOnly) continue;
else if (node.includeBlock) ret = node.includeBlock();
else if (node.block && !node.block.isEmpty()) ret = node.block.includeBlock();
if (ret.yield) return ret;
}
return ret;
};
/**
* Return a clone of this block.
*
* @return {Block}
* @api private
*/
Block.prototype.clone = function(){
var err = new Error('block.clone is deprecated and will be removed in v2.0.0');
console.warn(err.stack);
var clone = new Block;
for (var i = 0, len = this.nodes.length; i < len; ++i) {
clone.push(this.nodes[i].clone());
}
return clone;
};
+33
View File
@@ -0,0 +1,33 @@
'use strict';
var Node = require('./node');
/**
* Initialize a new `Case` with `expr`.
*
* @param {String} expr
* @api public
*/
var Case = exports = module.exports = function Case(expr, block){
this.expr = expr;
this.block = block;
};
// Inherit from `Node`.
Case.prototype = Object.create(Node.prototype);
Case.prototype.constructor = Case;
Case.prototype.type = 'Case';
var When = exports.When = function When(expr, block){
this.expr = expr;
this.block = block;
this.debug = false;
};
// Inherit from `Node`.
When.prototype = Object.create(Node.prototype);
When.prototype.constructor = When;
When.prototype.type = 'When';
+26
View File
@@ -0,0 +1,26 @@
'use strict';
var Node = require('./node');
/**
* Initialize a `Code` node with the given code `val`.
* Code may also be optionally buffered and escaped.
*
* @param {String} val
* @param {Boolean} buffer
* @param {Boolean} escape
* @api public
*/
var Code = module.exports = function Code(val, buffer, escape) {
this.val = val;
this.buffer = buffer;
this.escape = escape;
if (val.match(/^ *else/)) this.debug = false;
};
// Inherit from `Node`.
Code.prototype = Object.create(Node.prototype);
Code.prototype.constructor = Code;
Code.prototype.type = 'Code'; // prevent the minifiers removing this
+23
View File
@@ -0,0 +1,23 @@
'use strict';
var Node = require('./node');
/**
* Initialize a `Comment` with the given `val`, optionally `buffer`,
* otherwise the comment may render in the output.
*
* @param {String} val
* @param {Boolean} buffer
* @api public
*/
var Comment = module.exports = function Comment(val, buffer) {
this.val = val;
this.buffer = buffer;
};
// Inherit from `Node`.
Comment.prototype = Object.create(Node.prototype);
Comment.prototype.constructor = Comment;
Comment.prototype.type = 'Comment';
+20
View File
@@ -0,0 +1,20 @@
'use strict';
var Node = require('./node');
/**
* Initialize a `Doctype` with the given `val`.
*
* @param {String} val
* @api public
*/
var Doctype = module.exports = function Doctype(val) {
this.val = val;
};
// Inherit from `Node`.
Doctype.prototype = Object.create(Node.prototype);
Doctype.prototype.constructor = Doctype;
Doctype.prototype.type = 'Doctype';
+26
View File
@@ -0,0 +1,26 @@
'use strict';
var Node = require('./node');
/**
* Initialize an `Each` node, representing iteration
*
* @param {String} obj
* @param {String} val
* @param {String} key
* @param {Block} block
* @api public
*/
var Each = module.exports = function Each(obj, val, key, block) {
this.obj = obj;
this.val = val;
this.key = key;
this.block = block;
};
// Inherit from `Node`.
Each.prototype = Object.create(Node.prototype);
Each.prototype.constructor = Each;
Each.prototype.type = 'Each';
+24
View File
@@ -0,0 +1,24 @@
'use strict';
var Node = require('./node');
/**
* Initialize a `Filter` node with the given
* filter `name` and `block`.
*
* @param {String} name
* @param {Block|Node} block
* @api public
*/
var Filter = module.exports = function Filter(name, block, attrs) {
this.name = name;
this.block = block;
this.attrs = attrs;
};
// Inherit from `Node`.
Filter.prototype = Object.create(Node.prototype);
Filter.prototype.constructor = Filter;
Filter.prototype.type = 'Filter';
+16
View File
@@ -0,0 +1,16 @@
'use strict';
exports.Node = require('./node');
exports.Tag = require('./tag');
exports.Code = require('./code');
exports.Each = require('./each');
exports.Case = require('./case');
exports.Text = require('./text');
exports.Block = require('./block');
exports.MixinBlock = require('./mixin-block');
exports.Mixin = require('./mixin');
exports.Filter = require('./filter');
exports.Comment = require('./comment');
exports.Literal = require('./literal');
exports.BlockComment = require('./block-comment');
exports.Doctype = require('./doctype');
+20
View File
@@ -0,0 +1,20 @@
'use strict';
var Node = require('./node');
/**
* Initialize a `Literal` node with the given `str.
*
* @param {String} str
* @api public
*/
var Literal = module.exports = function Literal(str) {
this.str = str;
};
// Inherit from `Node`.
Literal.prototype = Object.create(Node.prototype);
Literal.prototype.constructor = Literal;
Literal.prototype.type = 'Literal';
+18
View File
@@ -0,0 +1,18 @@
'use strict';
var Node = require('./node');
/**
* Initialize a new `Block` with an optional `node`.
*
* @param {Node} node
* @api public
*/
var MixinBlock = module.exports = function MixinBlock(){};
// Inherit from `Node`.
MixinBlock.prototype = Object.create(Node.prototype);
MixinBlock.prototype.constructor = MixinBlock;
MixinBlock.prototype.type = 'MixinBlock';
+26
View File
@@ -0,0 +1,26 @@
'use strict';
var Attrs = require('./attrs');
/**
* Initialize a new `Mixin` with `name` and `block`.
*
* @param {String} name
* @param {String} args
* @param {Block} block
* @api public
*/
var Mixin = module.exports = function Mixin(name, args, block, call){
Attrs.call(this);
this.name = name;
this.args = args;
this.block = block;
this.call = call;
};
// Inherit from `Attrs`.
Mixin.prototype = Object.create(Attrs.prototype);
Mixin.prototype.constructor = Mixin;
Mixin.prototype.type = 'Mixin';
+18
View File
@@ -0,0 +1,18 @@
'use strict';
var Node = module.exports = function Node(){};
/**
* Clone this node (return itself)
*
* @return {Node}
* @api private
*/
Node.prototype.clone = function(){
var err = new Error('node.clone is deprecated and will be removed in v2.0.0');
console.warn(err.stack);
return this;
};
Node.prototype.type = '';
+89
View File
@@ -0,0 +1,89 @@
'use strict';
var Attrs = require('./attrs');
var Block = require('./block');
var inlineTags = require('../inline-tags');
/**
* Initialize a `Tag` node with the given tag `name` and optional `block`.
*
* @param {String} name
* @param {Block} block
* @api public
*/
var Tag = module.exports = function Tag(name, block) {
Attrs.call(this);
this.name = name;
this.block = block || new Block;
};
// Inherit from `Attrs`.
Tag.prototype = Object.create(Attrs.prototype);
Tag.prototype.constructor = Tag;
Tag.prototype.type = 'Tag';
/**
* Clone this tag.
*
* @return {Tag}
* @api private
*/
Tag.prototype.clone = function(){
var err = new Error('tag.clone is deprecated and will be removed in v2.0.0');
console.warn(err.stack);
var clone = new Tag(this.name, this.block.clone());
clone.line = this.line;
clone.attrs = this.attrs;
clone.textOnly = this.textOnly;
return clone;
};
/**
* Check if this tag is an inline tag.
*
* @return {Boolean}
* @api private
*/
Tag.prototype.isInline = function(){
return ~inlineTags.indexOf(this.name);
};
/**
* Check if this tag's contents can be inlined. Used for pretty printing.
*
* @return {Boolean}
* @api private
*/
Tag.prototype.canInline = function(){
var nodes = this.block.nodes;
function isInline(node){
// Recurse if the node is a block
if (node.isBlock) return node.nodes.every(isInline);
return node.isText || (node.isInline && node.isInline());
}
// Empty tag
if (!nodes.length) return true;
// Text-only or inline-only tag
if (1 == nodes.length) return isInline(nodes[0]);
// Multi-line inline-only tag
if (this.block.nodes.every(isInline)) {
for (var i = 1, len = nodes.length; i < len; ++i) {
if (nodes[i-1].isText && nodes[i].isText)
return false;
}
return true;
}
// Mixed tag
return false;
};
+26
View File
@@ -0,0 +1,26 @@
'use strict';
var Node = require('./node');
/**
* Initialize a `Text` node with optional `line`.
*
* @param {String} line
* @api public
*/
var Text = module.exports = function Text(line) {
this.val = line;
};
// Inherit from `Node`.
Text.prototype = Object.create(Node.prototype);
Text.prototype.constructor = Text;
Text.prototype.type = 'Text';
/**
* Flag as text.
*/
Text.prototype.isText = true;
+846
View File
@@ -0,0 +1,846 @@
'use strict';
var Lexer = require('./lexer');
var nodes = require('./nodes');
var utils = require('./utils');
var filters = require('./filters');
var path = require('path');
var constantinople = require('constantinople');
var parseJSExpression = require('character-parser').parseMax;
var extname = path.extname;
/**
* Initialize `Parser` with the given input `str` and `filename`.
*
* @param {String} str
* @param {String} filename
* @param {Object} options
* @api public
*/
var Parser = exports = module.exports = function Parser(str, filename, options){
//Strip any UTF-8 BOM off of the start of `str`, if it exists.
this.input = str.replace(/^\uFEFF/, '');
this.lexer = new Lexer(this.input, filename);
this.filename = filename;
this.blocks = {};
this.mixins = {};
this.options = options;
this.contexts = [this];
this.inMixin = 0;
this.dependencies = [];
this.inBlock = 0;
};
/**
* Parser prototype.
*/
Parser.prototype = {
/**
* Save original constructor
*/
constructor: Parser,
/**
* Push `parser` onto the context stack,
* or pop and return a `Parser`.
*/
context: function(parser){
if (parser) {
this.contexts.push(parser);
} else {
return this.contexts.pop();
}
},
/**
* Return the next token object.
*
* @return {Object}
* @api private
*/
advance: function(){
return this.lexer.advance();
},
/**
* Single token lookahead.
*
* @return {Object}
* @api private
*/
peek: function() {
return this.lookahead(1);
},
/**
* Return lexer lineno.
*
* @return {Number}
* @api private
*/
line: function() {
return this.lexer.lineno;
},
/**
* `n` token lookahead.
*
* @param {Number} n
* @return {Object}
* @api private
*/
lookahead: function(n){
return this.lexer.lookahead(n);
},
/**
* Parse input returning a string of js for evaluation.
*
* @return {String}
* @api public
*/
parse: function(){
var block = new nodes.Block, parser;
block.line = 0;
block.filename = this.filename;
while ('eos' != this.peek().type) {
if ('newline' == this.peek().type) {
this.advance();
} else {
var next = this.peek();
var expr = this.parseExpr();
expr.filename = expr.filename || this.filename;
expr.line = next.line;
block.push(expr);
}
}
if (parser = this.extending) {
this.context(parser);
var ast = parser.parse();
this.context();
// hoist mixins
for (var name in this.mixins)
ast.unshift(this.mixins[name]);
return ast;
}
if (!this.extending && !this.included && Object.keys(this.blocks).length){
var blocks = [];
utils.walkAST(block, function (node) {
if (node.type === 'Block' && node.name) {
blocks.push(node.name);
}
});
Object.keys(this.blocks).forEach(function (name) {
if (blocks.indexOf(name) === -1 && !this.blocks[name].isSubBlock) {
console.warn('Warning: Unexpected block "'
+ name
+ '" '
+ ' on line '
+ this.blocks[name].line
+ ' of '
+ (this.blocks[name].filename)
+ '. This block is never used. This warning will be an error in v2.0.0');
}
}.bind(this));
}
return block;
},
/**
* Expect the given type, or throw an exception.
*
* @param {String} type
* @api private
*/
expect: function(type){
if (this.peek().type === type) {
return this.advance();
} else {
throw new Error('expected "' + type + '", but got "' + this.peek().type + '"');
}
},
/**
* Accept the given `type`.
*
* @param {String} type
* @api private
*/
accept: function(type){
if (this.peek().type === type) {
return this.advance();
}
},
/**
* tag
* | doctype
* | mixin
* | include
* | filter
* | comment
* | text
* | each
* | code
* | yield
* | id
* | class
* | interpolation
*/
parseExpr: function(){
switch (this.peek().type) {
case 'tag':
return this.parseTag();
case 'mixin':
return this.parseMixin();
case 'block':
return this.parseBlock();
case 'mixin-block':
return this.parseMixinBlock();
case 'case':
return this.parseCase();
case 'extends':
return this.parseExtends();
case 'include':
return this.parseInclude();
case 'doctype':
return this.parseDoctype();
case 'filter':
return this.parseFilter();
case 'comment':
return this.parseComment();
case 'text':
return this.parseText();
case 'each':
return this.parseEach();
case 'code':
return this.parseCode();
case 'blockCode':
return this.parseBlockCode();
case 'call':
return this.parseCall();
case 'interpolation':
return this.parseInterpolation();
case 'yield':
this.advance();
var block = new nodes.Block;
block.yield = true;
return block;
case 'id':
case 'class':
var tok = this.advance();
this.lexer.defer(this.lexer.tok('tag', 'div'));
this.lexer.defer(tok);
return this.parseExpr();
default:
throw new Error('unexpected token "' + this.peek().type + '"');
}
},
/**
* Text
*/
parseText: function(){
var tok = this.expect('text');
var tokens = this.parseInlineTagsInText(tok.val);
if (tokens.length === 1) return tokens[0];
var node = new nodes.Block;
for (var i = 0; i < tokens.length; i++) {
node.push(tokens[i]);
};
return node;
},
/**
* ':' expr
* | block
*/
parseBlockExpansion: function(){
if (':' == this.peek().type) {
this.advance();
return new nodes.Block(this.parseExpr());
} else {
return this.block();
}
},
/**
* case
*/
parseCase: function(){
var val = this.expect('case').val;
var node = new nodes.Case(val);
node.line = this.line();
var block = new nodes.Block;
block.line = this.line();
block.filename = this.filename;
this.expect('indent');
while ('outdent' != this.peek().type) {
switch (this.peek().type) {
case 'comment':
case 'newline':
this.advance();
break;
case 'when':
block.push(this.parseWhen());
break;
case 'default':
block.push(this.parseDefault());
break;
default:
throw new Error('Unexpected token "' + this.peek().type
+ '", expected "when", "default" or "newline"');
}
}
this.expect('outdent');
node.block = block;
return node;
},
/**
* when
*/
parseWhen: function(){
var val = this.expect('when').val;
if (this.peek().type !== 'newline')
return new nodes.Case.When(val, this.parseBlockExpansion());
else
return new nodes.Case.When(val);
},
/**
* default
*/
parseDefault: function(){
this.expect('default');
return new nodes.Case.When('default', this.parseBlockExpansion());
},
/**
* code
*/
parseCode: function(afterIf){
var tok = this.expect('code');
var node = new nodes.Code(tok.val, tok.buffer, tok.escape);
var block;
node.line = this.line();
// throw an error if an else does not have an if
if (tok.isElse && !tok.hasIf) {
throw new Error('Unexpected else without if');
}
// handle block
block = 'indent' == this.peek().type;
if (block) {
node.block = this.block();
}
// handle missing block
if (tok.requiresBlock && !block) {
node.block = new nodes.Block();
}
// mark presense of if for future elses
if (tok.isIf && this.peek().isElse) {
this.peek().hasIf = true;
} else if (tok.isIf && this.peek().type === 'newline' && this.lookahead(2).isElse) {
this.lookahead(2).hasIf = true;
}
return node;
},
/**
* block code
*/
parseBlockCode: function(){
var tok = this.expect('blockCode');
var node;
var body = this.peek();
var text;
if (body.type === 'pipeless-text') {
this.advance();
text = body.val.join('\n');
} else {
text = '';
}
node = new nodes.Code(text, false, false);
return node;
},
/**
* comment
*/
parseComment: function(){
var tok = this.expect('comment');
var node;
var block;
if (block = this.parseTextBlock()) {
node = new nodes.BlockComment(tok.val, block, tok.buffer);
} else {
node = new nodes.Comment(tok.val, tok.buffer);
}
node.line = this.line();
return node;
},
/**
* doctype
*/
parseDoctype: function(){
var tok = this.expect('doctype');
var node = new nodes.Doctype(tok.val);
node.line = this.line();
return node;
},
/**
* filter attrs? text-block
*/
parseFilter: function(){
var tok = this.expect('filter');
var attrs = this.accept('attrs');
var block;
block = this.parseTextBlock() || new nodes.Block();
var options = {};
if (attrs) {
attrs.attrs.forEach(function (attribute) {
options[attribute.name] = constantinople.toConstant(attribute.val);
});
}
var node = new nodes.Filter(tok.val, block, options);
node.line = this.line();
return node;
},
/**
* each block
*/
parseEach: function(){
var tok = this.expect('each');
var node = new nodes.Each(tok.code, tok.val, tok.key);
node.line = this.line();
node.block = this.block();
if (this.peek().type == 'code' && this.peek().val == 'else') {
this.advance();
node.alternative = this.block();
}
return node;
},
/**
* Resolves a path relative to the template for use in
* includes and extends
*
* @param {String} path
* @param {String} purpose Used in error messages.
* @return {String}
* @api private
*/
resolvePath: function (path, purpose) {
var p = require('path');
var dirname = p.dirname;
var basename = p.basename;
var join = p.join;
if (path[0] !== '/' && !this.filename)
throw new Error('the "filename" option is required to use "' + purpose + '" with "relative" paths');
if (path[0] === '/' && !this.options.basedir)
throw new Error('the "basedir" option is required to use "' + purpose + '" with "absolute" paths');
path = join(path[0] === '/' ? this.options.basedir : dirname(this.filename), path);
if (basename(path).indexOf('.') === -1) path += '.jade';
return path;
},
/**
* 'extends' name
*/
parseExtends: function(){
var fs = require('fs');
var path = this.resolvePath(this.expect('extends').val.trim(), 'extends');
if ('.jade' != path.substr(-5)) path += '.jade';
this.dependencies.push(path);
var str = fs.readFileSync(path, 'utf8');
var parser = new this.constructor(str, path, this.options);
parser.dependencies = this.dependencies;
parser.blocks = this.blocks;
parser.included = this.included;
parser.contexts = this.contexts;
this.extending = parser;
// TODO: null node
return new nodes.Literal('');
},
/**
* 'block' name block
*/
parseBlock: function(){
var block = this.expect('block');
var mode = block.mode;
var name = block.val.trim();
var line = block.line;
this.inBlock++;
block = 'indent' == this.peek().type
? this.block()
: new nodes.Block(new nodes.Literal(''));
this.inBlock--;
block.name = name;
block.line = line;
var prev = this.blocks[name] || {prepended: [], appended: []}
if (prev.mode === 'replace') return this.blocks[name] = prev;
var allNodes = prev.prepended.concat(block.nodes).concat(prev.appended);
switch (mode) {
case 'append':
prev.appended = prev.parser === this ?
prev.appended.concat(block.nodes) :
block.nodes.concat(prev.appended);
break;
case 'prepend':
prev.prepended = prev.parser === this ?
block.nodes.concat(prev.prepended) :
prev.prepended.concat(block.nodes);
break;
}
block.nodes = allNodes;
block.appended = prev.appended;
block.prepended = prev.prepended;
block.mode = mode;
block.parser = this;
block.isSubBlock = this.inBlock > 0;
return this.blocks[name] = block;
},
parseMixinBlock: function () {
var block = this.expect('mixin-block');
if (!this.inMixin) {
throw new Error('Anonymous blocks are not allowed unless they are part of a mixin.');
}
return new nodes.MixinBlock();
},
/**
* include block?
*/
parseInclude: function(){
var fs = require('fs');
var tok = this.expect('include');
var path = this.resolvePath(tok.val.trim(), 'include');
this.dependencies.push(path);
// has-filter
if (tok.filter) {
var str = fs.readFileSync(path, 'utf8').replace(/\r/g, '');
var options = {filename: path};
if (tok.attrs) {
tok.attrs.attrs.forEach(function (attribute) {
options[attribute.name] = constantinople.toConstant(attribute.val);
});
}
str = filters(tok.filter, str, options);
return new nodes.Literal(str);
}
// non-jade
if ('.jade' != path.substr(-5)) {
var str = fs.readFileSync(path, 'utf8').replace(/\r/g, '');
return new nodes.Literal(str);
}
var str = fs.readFileSync(path, 'utf8');
var parser = new this.constructor(str, path, this.options);
parser.dependencies = this.dependencies;
parser.blocks = utils.merge({}, this.blocks);
parser.included = true;
parser.mixins = this.mixins;
this.context(parser);
var ast = parser.parse();
this.context();
ast.filename = path;
if ('indent' == this.peek().type) {
ast.includeBlock().push(this.block());
}
return ast;
},
/**
* call ident block
*/
parseCall: function(){
var tok = this.expect('call');
var name = tok.val;
var args = tok.args;
var mixin = new nodes.Mixin(name, args, new nodes.Block, true);
this.tag(mixin);
if (mixin.code) {
mixin.block.push(mixin.code);
mixin.code = null;
}
if (mixin.block.isEmpty()) mixin.block = null;
return mixin;
},
/**
* mixin block
*/
parseMixin: function(){
var tok = this.expect('mixin');
var name = tok.val;
var args = tok.args;
var mixin;
// definition
if ('indent' == this.peek().type) {
this.inMixin++;
mixin = new nodes.Mixin(name, args, this.block(), false);
this.mixins[name] = mixin;
this.inMixin--;
return mixin;
// call
} else {
return new nodes.Mixin(name, args, null, true);
}
},
parseInlineTagsInText: function (str) {
var line = this.line();
var match = /(\\)?#\[((?:.|\n)*)$/.exec(str);
if (match) {
if (match[1]) { // escape
var text = new nodes.Text(str.substr(0, match.index) + '#[');
text.line = line;
var rest = this.parseInlineTagsInText(match[2]);
if (rest[0].type === 'Text') {
text.val += rest[0].val;
rest.shift();
}
return [text].concat(rest);
} else {
var text = new nodes.Text(str.substr(0, match.index));
text.line = line;
var buffer = [text];
var rest = match[2];
var range = parseJSExpression(rest);
var inner = new Parser(range.src, this.filename, this.options);
buffer.push(inner.parse());
return buffer.concat(this.parseInlineTagsInText(rest.substr(range.end + 1)));
}
} else {
var text = new nodes.Text(str);
text.line = line;
return [text];
}
},
/**
* indent (text | newline)* outdent
*/
parseTextBlock: function(){
var block = new nodes.Block;
block.line = this.line();
var body = this.peek();
if (body.type !== 'pipeless-text') return;
this.advance();
block.nodes = body.val.reduce(function (accumulator, text) {
return accumulator.concat(this.parseInlineTagsInText(text));
}.bind(this), []);
return block;
},
/**
* indent expr* outdent
*/
block: function(){
var block = new nodes.Block;
block.line = this.line();
block.filename = this.filename;
this.expect('indent');
while ('outdent' != this.peek().type) {
if ('newline' == this.peek().type) {
this.advance();
} else {
var expr = this.parseExpr();
expr.filename = this.filename;
block.push(expr);
}
}
this.expect('outdent');
return block;
},
/**
* interpolation (attrs | class | id)* (text | code | ':')? newline* block?
*/
parseInterpolation: function(){
var tok = this.advance();
var tag = new nodes.Tag(tok.val);
tag.buffer = true;
return this.tag(tag);
},
/**
* tag (attrs | class | id)* (text | code | ':')? newline* block?
*/
parseTag: function(){
var tok = this.advance();
var tag = new nodes.Tag(tok.val);
tag.selfClosing = tok.selfClosing;
return this.tag(tag);
},
/**
* Parse tag.
*/
tag: function(tag){
tag.line = this.line();
var seenAttrs = false;
// (attrs | class | id)*
out:
while (true) {
switch (this.peek().type) {
case 'id':
case 'class':
var tok = this.advance();
tag.setAttribute(tok.type, "'" + tok.val + "'");
continue;
case 'attrs':
if (seenAttrs) {
console.warn(this.filename + ', line ' + this.peek().line + ':\nYou should not have jade tags with multiple attributes.');
}
seenAttrs = true;
var tok = this.advance();
var attrs = tok.attrs;
if (tok.selfClosing) tag.selfClosing = true;
for (var i = 0; i < attrs.length; i++) {
tag.setAttribute(attrs[i].name, attrs[i].val, attrs[i].escaped);
}
continue;
case '&attributes':
var tok = this.advance();
tag.addAttributes(tok.val);
break;
default:
break out;
}
}
// check immediate '.'
if ('dot' == this.peek().type) {
tag.textOnly = true;
this.advance();
}
// (text | code | ':')?
switch (this.peek().type) {
case 'text':
tag.block.push(this.parseText());
break;
case 'code':
tag.code = this.parseCode();
break;
case ':':
this.advance();
tag.block = new nodes.Block;
tag.block.push(this.parseExpr());
break;
case 'newline':
case 'indent':
case 'outdent':
case 'eos':
case 'pipeless-text':
break;
default:
throw new Error('Unexpected token `' + this.peek().type + '` expected `text`, `code`, `:`, `newline` or `eos`')
}
// newline*
while ('newline' == this.peek().type) this.advance();
// block?
if (tag.textOnly) {
tag.block = this.parseTextBlock() || new nodes.Block();
} else if ('indent' == this.peek().type) {
var block = this.block();
for (var i = 0, len = block.nodes.length; i < len; ++i) {
tag.block.push(block.nodes[i]);
}
}
return tag;
}
};
+246
View File
@@ -0,0 +1,246 @@
'use strict';
/**
* Merge two attribute objects giving precedence
* to values in object `b`. Classes are special-cased
* allowing for arrays and merging/joining appropriately
* resulting in a string.
*
* @param {Object} a
* @param {Object} b
* @return {Object} a
* @api private
*/
exports.merge = function merge(a, b) {
if (arguments.length === 1) {
var attrs = a[0];
for (var i = 1; i < a.length; i++) {
attrs = merge(attrs, a[i]);
}
return attrs;
}
var ac = a['class'];
var bc = b['class'];
if (ac || bc) {
ac = ac || [];
bc = bc || [];
if (!Array.isArray(ac)) ac = [ac];
if (!Array.isArray(bc)) bc = [bc];
a['class'] = ac.concat(bc).filter(nulls);
}
for (var key in b) {
if (key != 'class') {
a[key] = b[key];
}
}
return a;
};
/**
* Filter null `val`s.
*
* @param {*} val
* @return {Boolean}
* @api private
*/
function nulls(val) {
return val != null && val !== '';
}
/**
* join array as classes.
*
* @param {*} val
* @return {String}
*/
exports.joinClasses = joinClasses;
function joinClasses(val) {
return (Array.isArray(val) ? val.map(joinClasses) :
(val && typeof val === 'object') ? Object.keys(val).filter(function (key) { return val[key]; }) :
[val]).filter(nulls).join(' ');
}
/**
* Render the given classes.
*
* @param {Array} classes
* @param {Array.<Boolean>} escaped
* @return {String}
*/
exports.cls = function cls(classes, escaped) {
var buf = [];
for (var i = 0; i < classes.length; i++) {
if (escaped && escaped[i]) {
buf.push(exports.escape(joinClasses([classes[i]])));
} else {
buf.push(joinClasses(classes[i]));
}
}
var text = joinClasses(buf);
if (text.length) {
return ' class="' + text + '"';
} else {
return '';
}
};
exports.style = function (val) {
if (val && typeof val === 'object') {
return Object.keys(val).map(function (style) {
return style + ':' + val[style];
}).join(';');
} else {
return val;
}
};
/**
* Render the given attribute.
*
* @param {String} key
* @param {String} val
* @param {Boolean} escaped
* @param {Boolean} terse
* @return {String}
*/
exports.attr = function attr(key, val, escaped, terse) {
if (key === 'style') {
val = exports.style(val);
}
if ('boolean' == typeof val || null == val) {
if (val) {
return ' ' + (terse ? key : key + '="' + key + '"');
} else {
return '';
}
} else if (0 == key.indexOf('data') && 'string' != typeof val) {
if (JSON.stringify(val).indexOf('&') !== -1) {
console.warn('Since Jade 2.0.0, ampersands (`&`) in data attributes ' +
'will be escaped to `&amp;`');
};
if (val && typeof val.toISOString === 'function') {
console.warn('Jade will eliminate the double quotes around dates in ' +
'ISO form after 2.0.0');
}
return ' ' + key + "='" + JSON.stringify(val).replace(/'/g, '&apos;') + "'";
} else if (escaped) {
if (val && typeof val.toISOString === 'function') {
console.warn('Jade will stringify dates in ISO form after 2.0.0');
}
return ' ' + key + '="' + exports.escape(val) + '"';
} else {
if (val && typeof val.toISOString === 'function') {
console.warn('Jade will stringify dates in ISO form after 2.0.0');
}
return ' ' + key + '="' + val + '"';
}
};
/**
* Render the given attributes object.
*
* @param {Object} obj
* @param {Object} escaped
* @return {String}
*/
exports.attrs = function attrs(obj, terse){
var buf = [];
var keys = Object.keys(obj);
if (keys.length) {
for (var i = 0; i < keys.length; ++i) {
var key = keys[i]
, val = obj[key];
if ('class' == key) {
if (val = joinClasses(val)) {
buf.push(' ' + key + '="' + val + '"');
}
} else {
buf.push(exports.attr(key, val, false, terse));
}
}
}
return buf.join('');
};
/**
* Escape the given string of `html`.
*
* @param {String} html
* @return {String}
* @api private
*/
var jade_encode_html_rules = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;'
};
var jade_match_html = /[&<>"]/g;
function jade_encode_char(c) {
return jade_encode_html_rules[c] || c;
}
exports.escape = jade_escape;
function jade_escape(html){
var result = String(html).replace(jade_match_html, jade_encode_char);
if (result === '' + html) return html;
else return result;
};
/**
* Re-throw the given `err` in context to the
* the jade in `filename` at the given `lineno`.
*
* @param {Error} err
* @param {String} filename
* @param {String} lineno
* @api private
*/
exports.rethrow = function rethrow(err, filename, lineno, str){
if (!(err instanceof Error)) throw err;
if ((typeof window != 'undefined' || !filename) && !str) {
err.message += ' on line ' + lineno;
throw err;
}
try {
str = str || require('fs').readFileSync(filename, 'utf8')
} catch (ex) {
rethrow(err, null, lineno)
}
var context = 3
, lines = str.split('\n')
, start = Math.max(lineno - context, 0)
, end = Math.min(lines.length, lineno + context);
// Error context
var context = lines.slice(start, end).map(function(line, i){
var curr = i + start + 1;
return (curr == lineno ? ' > ' : ' ')
+ curr
+ '| '
+ line;
}).join('\n');
// Alter exception message
err.path = filename;
err.message = (filename || 'Jade') + ':' + lineno
+ '\n' + context + '\n\n' + err.message;
throw err;
};
exports.DebugItem = function DebugItem(lineno, filename) {
this.lineno = lineno;
this.filename = filename;
}
+53
View File
@@ -0,0 +1,53 @@
'use strict';
/**
* Merge `b` into `a`.
*
* @param {Object} a
* @param {Object} b
* @return {Object}
* @api public
*/
exports.merge = function(a, b) {
for (var key in b) a[key] = b[key];
return a;
};
exports.stringify = function(str) {
return JSON.stringify(str)
.replace(/\u2028/g, '\\u2028')
.replace(/\u2029/g, '\\u2029');
};
exports.walkAST = function walkAST(ast, before, after) {
before && before(ast);
switch (ast.type) {
case 'Block':
ast.nodes.forEach(function (node) {
walkAST(node, before, after);
});
break;
case 'Case':
case 'Each':
case 'Mixin':
case 'Tag':
case 'When':
case 'Code':
ast.block && walkAST(ast.block, before, after);
break;
case 'Attrs':
case 'BlockComment':
case 'Comment':
case 'Doctype':
case 'Filter':
case 'Literal':
case 'MixinBlock':
case 'Text':
break;
default:
throw new Error('Unexpected node type ' + ast.type);
break;
}
after && after(ast);
};
+1
View File
@@ -0,0 +1 @@
../clean-css/bin/cleancss
+1
View File
@@ -0,0 +1 @@
../mkdirp/bin/cmd.js
+1
View File
@@ -0,0 +1 @@
../uglify-js/bin/uglifyjs
@@ -0,0 +1,2 @@
test/
.travis.yml
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2013 Forbes Lindesay
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+142
View File
@@ -0,0 +1,142 @@
# character-parser
Parse JavaScript one character at a time to look for snippets in Templates. This is not a validator, it's just designed to allow you to have sections of JavaScript delimited by brackets robustly.
[![Build Status](https://img.shields.io/travis/ForbesLindesay/character-parser/master.svg)](https://travis-ci.org/ForbesLindesay/character-parser)
## Installation
npm install character-parser
## Usage
Work out how much depth changes:
```js
var state = parse('foo(arg1, arg2, {\n foo: [a, b\n');
assert(state.roundDepth === 1);
assert(state.curlyDepth === 1);
assert(state.squareDepth === 1);
parse(' c, d]\n })', state);
assert(state.squareDepth === 0);
assert(state.curlyDepth === 0);
assert(state.roundDepth === 0);
```
### Bracketed Expressions
Find all the contents of a bracketed expression:
```js
var section = parser.parseMax('foo="(", bar="}") bing bong');
assert(section.start === 0);
assert(section.end === 16);//exclusive end of string
assert(section.src = 'foo="(", bar="}"');
var section = parser.parseMax('{foo="(", bar="}"} bing bong', {start: 1});
assert(section.start === 1);
assert(section.end === 17);//exclusive end of string
assert(section.src = 'foo="(", bar="}"');
```
The bracketed expression parsing simply parses up to but excluding the first unmatched closed bracket (`)`, `}`, `]`). It is clever enough to ignore brackets in comments or strings.
### Custom Delimited Expressions
Find code up to a custom delimiter:
```js
var section = parser.parseUntil('foo.bar("%>").baz%> bing bong', '%>');
assert(section.start === 0);
assert(section.end === 17);//exclusive end of string
assert(section.src = 'foo.bar("%>").baz');
var section = parser.parseUntil('<%foo.bar("%>").baz%> bing bong', '%>', {start: 2});
assert(section.start === 2);
assert(section.end === 19);//exclusive end of string
assert(section.src = 'foo.bar("%>").baz');
```
Delimiters are ignored if they are inside strings or comments.
## API
### parse(str, state = defaultState(), options = {start: 0, end: src.length})
Parse a string starting at the index start, and return the state after parsing that string.
If you want to parse one string in multiple sections you should keep passing the resulting state to the next parse operation.
Returns a `State` object.
### parseMax(src, options = {start: 0})
Parses the source until the first unmatched close bracket (any of `)`, `}`, `]`). It returns an object with the structure:
```js
{
start: 0,//index of first character of string
end: 13,//index of first character after the end of string
src: 'source string'
}
```
### parseUntil(src, delimiter, options = {start: 0, includeLineComment: false})
Parses the source until the first occurence of `delimiter` which is not in a string or a comment. If `includeLineComment` is `true`, it will still count if the delimiter occurs in a line comment, but not in a block comment. It returns an object with the structure:
```js
{
start: 0,//index of first character of string
end: 13,//index of first character after the end of string
src: 'source string'
}
```
### parseChar(character, state = defaultState())
Parses the single character and returns the state. See `parse` for the structure of the returned state object. N.B. character must be a single character not a multi character string.
### defaultState()
Get a default starting state.
### isPunctuator(character)
Returns `true` if `character` represents punctuation in JavaScript.
### isKeyword(name)
Returns `true` if `name` is a keyword in JavaScript.
## State
A state is an object with the following structure
```js
{
lineComment: false, //true if inside a line comment
blockComment: false, //true if inside a block comment
singleQuote: false, //true if inside a single quoted string
doubleQuote: false, //true if inside a double quoted string
regexp: false, //true if inside a regular expression
escaped: false, //true if in a string and the last character was an escape character
roundDepth: 0, //number of un-closed open `(` brackets
curlyDepth: 0, //number of un-closed open `{` brackets
squareDepth: 0 //number of un-closed open `[` brackets
}
```
It also has the following useful methods:
- `.isString()` returns `true` if the current location is inside a string.
- `.isComment()` returns `true` if the current location is inside a comment.
- `isNesting()` returns `true` if the current location is anything but at the top level, i.e. with no nesting.
## License
MIT
+231
View File
@@ -0,0 +1,231 @@
exports = (module.exports = parse);
exports.parse = parse;
function parse(src, state, options) {
options = options || {};
state = state || exports.defaultState();
var start = options.start || 0;
var end = options.end || src.length;
var index = start;
while (index < end) {
if (state.roundDepth < 0 || state.curlyDepth < 0 || state.squareDepth < 0) {
throw new SyntaxError('Mismatched Bracket: ' + src[index - 1]);
}
exports.parseChar(src[index++], state);
}
return state;
}
exports.parseMax = parseMax;
function parseMax(src, options) {
options = options || {};
var start = options.start || 0;
var index = start;
var state = exports.defaultState();
while (state.roundDepth >= 0 && state.curlyDepth >= 0 && state.squareDepth >= 0) {
if (index >= src.length) {
throw new Error('The end of the string was reached with no closing bracket found.');
}
exports.parseChar(src[index++], state);
}
var end = index - 1;
return {
start: start,
end: end,
src: src.substring(start, end)
};
}
exports.parseUntil = parseUntil;
function parseUntil(src, delimiter, options) {
options = options || {};
var includeLineComment = options.includeLineComment || false;
var start = options.start || 0;
var index = start;
var state = exports.defaultState();
while (state.isString() || state.regexp || state.blockComment ||
(!includeLineComment && state.lineComment) || !startsWith(src, delimiter, index)) {
exports.parseChar(src[index++], state);
}
var end = index;
return {
start: start,
end: end,
src: src.substring(start, end)
};
}
exports.parseChar = parseChar;
function parseChar(character, state) {
if (character.length !== 1) throw new Error('Character must be a string of length 1');
state = state || exports.defaultState();
state.src = state.src || '';
state.src += character;
var wasComment = state.blockComment || state.lineComment;
var lastChar = state.history ? state.history[0] : '';
if (state.regexpStart) {
if (character === '/' || character == '*') {
state.regexp = false;
}
state.regexpStart = false;
}
if (state.lineComment) {
if (character === '\n') {
state.lineComment = false;
}
} else if (state.blockComment) {
if (state.lastChar === '*' && character === '/') {
state.blockComment = false;
}
} else if (state.singleQuote) {
if (character === '\'' && !state.escaped) {
state.singleQuote = false;
} else if (character === '\\' && !state.escaped) {
state.escaped = true;
} else {
state.escaped = false;
}
} else if (state.doubleQuote) {
if (character === '"' && !state.escaped) {
state.doubleQuote = false;
} else if (character === '\\' && !state.escaped) {
state.escaped = true;
} else {
state.escaped = false;
}
} else if (state.regexp) {
if (character === '/' && !state.escaped) {
state.regexp = false;
} else if (character === '\\' && !state.escaped) {
state.escaped = true;
} else {
state.escaped = false;
}
} else if (lastChar === '/' && character === '/') {
state.history = state.history.substr(1);
state.lineComment = true;
} else if (lastChar === '/' && character === '*') {
state.history = state.history.substr(1);
state.blockComment = true;
} else if (character === '/' && isRegexp(state.history)) {
state.regexp = true;
state.regexpStart = true;
} else if (character === '\'') {
state.singleQuote = true;
} else if (character === '"') {
state.doubleQuote = true;
} else if (character === '(') {
state.roundDepth++;
} else if (character === ')') {
state.roundDepth--;
} else if (character === '{') {
state.curlyDepth++;
} else if (character === '}') {
state.curlyDepth--;
} else if (character === '[') {
state.squareDepth++;
} else if (character === ']') {
state.squareDepth--;
}
if (!state.blockComment && !state.lineComment && !wasComment) state.history = character + state.history;
state.lastChar = character; // store last character for ending block comments
return state;
}
exports.defaultState = function () { return new State() };
function State() {
this.lineComment = false;
this.blockComment = false;
this.singleQuote = false;
this.doubleQuote = false;
this.regexp = false;
this.escaped = false;
this.roundDepth = 0;
this.curlyDepth = 0;
this.squareDepth = 0;
this.history = ''
this.lastChar = ''
}
State.prototype.isString = function () {
return this.singleQuote || this.doubleQuote;
}
State.prototype.isComment = function () {
return this.lineComment || this.blockComment;
}
State.prototype.isNesting = function () {
return this.isString() || this.isComment() || this.regexp || this.roundDepth > 0 || this.curlyDepth > 0 || this.squareDepth > 0
}
function startsWith(str, start, i) {
return str.substr(i || 0, start.length) === start;
}
exports.isPunctuator = isPunctuator
function isPunctuator(c) {
if (!c) return true; // the start of a string is a punctuator
var code = c.charCodeAt(0)
switch (code) {
case 46: // . dot
case 40: // ( open bracket
case 41: // ) close bracket
case 59: // ; semicolon
case 44: // , comma
case 123: // { open curly brace
case 125: // } close curly brace
case 91: // [
case 93: // ]
case 58: // :
case 63: // ?
case 126: // ~
case 37: // %
case 38: // &
case 42: // *:
case 43: // +
case 45: // -
case 47: // /
case 60: // <
case 62: // >
case 94: // ^
case 124: // |
case 33: // !
case 61: // =
return true;
default:
return false;
}
}
exports.isKeyword = isKeyword
function isKeyword(id) {
return (id === 'if') || (id === 'in') || (id === 'do') || (id === 'var') || (id === 'for') || (id === 'new') ||
(id === 'try') || (id === 'let') || (id === 'this') || (id === 'else') || (id === 'case') ||
(id === 'void') || (id === 'with') || (id === 'enum') || (id === 'while') || (id === 'break') || (id === 'catch') ||
(id === 'throw') || (id === 'const') || (id === 'yield') || (id === 'class') || (id === 'super') ||
(id === 'return') || (id === 'typeof') || (id === 'delete') || (id === 'switch') || (id === 'export') ||
(id === 'import') || (id === 'default') || (id === 'finally') || (id === 'extends') || (id === 'function') ||
(id === 'continue') || (id === 'debugger') || (id === 'package') || (id === 'private') || (id === 'interface') ||
(id === 'instanceof') || (id === 'implements') || (id === 'protected') || (id === 'public') || (id === 'static') ||
(id === 'yield') || (id === 'let');
}
function isRegexp(history) {
//could be start of regexp or divide sign
history = history.replace(/^\s*/, '');
//unless its an `if`, `while`, `for` or `with` it's a divide, so we assume it's a divide
if (history[0] === ')') return false;
//unless it's a function expression, it's a regexp, so we assume it's a regexp
if (history[0] === '}') return true;
//any punctuation means it's a regexp
if (isPunctuator(history[0])) return true;
//if the last thing was a keyword then it must be a regexp (e.g. `typeof /foo/`)
if (/^\w+\b/.test(history) && isKeyword(/^\w+\b/.exec(history)[0].split('').reverse().join(''))) return true;
return false;
}
@@ -0,0 +1,43 @@
{
"name": "character-parser",
"version": "1.2.1",
"description": "Parse JavaScript one character at a time to look for snippets in Templates. This is not a validator, it's just designed to allow you to have sections of JavaScript delimited by brackets robustly.",
"main": "index.js",
"scripts": {
"test": "mocha -R spec"
},
"repository": {
"type": "git",
"url": "https://github.com/ForbesLindesay/character-parser.git"
},
"keywords": [
"parser",
"JavaScript",
"bracket",
"nesting",
"comment",
"string",
"escape",
"escaping"
],
"author": {
"name": "ForbesLindesay"
},
"license": "MIT",
"devDependencies": {
"better-assert": "~1.0.0",
"mocha": "~1.9.0"
},
"readme": "# character-parser\r\n\r\nParse JavaScript one character at a time to look for snippets in Templates. This is not a validator, it's just designed to allow you to have sections of JavaScript delimited by brackets robustly.\r\n\r\n[![Build Status](https://img.shields.io/travis/ForbesLindesay/character-parser/master.svg)](https://travis-ci.org/ForbesLindesay/character-parser)\r\n\r\n## Installation\r\n\r\n npm install character-parser\r\n\r\n## Usage\r\n\r\nWork out how much depth changes:\r\n\r\n```js\r\nvar state = parse('foo(arg1, arg2, {\\n foo: [a, b\\n');\r\nassert(state.roundDepth === 1);\r\nassert(state.curlyDepth === 1);\r\nassert(state.squareDepth === 1);\r\nparse(' c, d]\\n })', state);\r\nassert(state.squareDepth === 0);\r\nassert(state.curlyDepth === 0);\r\nassert(state.roundDepth === 0);\r\n```\r\n\r\n### Bracketed Expressions\r\n\r\nFind all the contents of a bracketed expression:\r\n\r\n```js\r\nvar section = parser.parseMax('foo=\"(\", bar=\"}\") bing bong');\r\nassert(section.start === 0);\r\nassert(section.end === 16);//exclusive end of string\r\nassert(section.src = 'foo=\"(\", bar=\"}\"');\r\n\r\n\r\nvar section = parser.parseMax('{foo=\"(\", bar=\"}\"} bing bong', {start: 1});\r\nassert(section.start === 1);\r\nassert(section.end === 17);//exclusive end of string\r\nassert(section.src = 'foo=\"(\", bar=\"}\"');\r\n```\r\n\r\nThe bracketed expression parsing simply parses up to but excluding the first unmatched closed bracket (`)`, `}`, `]`). It is clever enough to ignore brackets in comments or strings.\r\n\r\n\r\n### Custom Delimited Expressions\r\n\r\nFind code up to a custom delimiter:\r\n\r\n```js\r\nvar section = parser.parseUntil('foo.bar(\"%>\").baz%> bing bong', '%>');\r\nassert(section.start === 0);\r\nassert(section.end === 17);//exclusive end of string\r\nassert(section.src = 'foo.bar(\"%>\").baz');\r\n\r\nvar section = parser.parseUntil('<%foo.bar(\"%>\").baz%> bing bong', '%>', {start: 2});\r\nassert(section.start === 2);\r\nassert(section.end === 19);//exclusive end of string\r\nassert(section.src = 'foo.bar(\"%>\").baz');\r\n```\r\n\r\nDelimiters are ignored if they are inside strings or comments.\r\n\r\n## API\r\n\r\n### parse(str, state = defaultState(), options = {start: 0, end: src.length})\r\n\r\nParse a string starting at the index start, and return the state after parsing that string.\r\n\r\nIf you want to parse one string in multiple sections you should keep passing the resulting state to the next parse operation.\r\n\r\nReturns a `State` object.\r\n\r\n### parseMax(src, options = {start: 0})\r\n\r\nParses the source until the first unmatched close bracket (any of `)`, `}`, `]`). It returns an object with the structure:\r\n\r\n```js\r\n{\r\n start: 0,//index of first character of string\r\n end: 13,//index of first character after the end of string\r\n src: 'source string'\r\n}\r\n```\r\n\r\n### parseUntil(src, delimiter, options = {start: 0, includeLineComment: false})\r\n\r\nParses the source until the first occurence of `delimiter` which is not in a string or a comment. If `includeLineComment` is `true`, it will still count if the delimiter occurs in a line comment, but not in a block comment. It returns an object with the structure:\r\n\r\n```js\r\n{\r\n start: 0,//index of first character of string\r\n end: 13,//index of first character after the end of string\r\n src: 'source string'\r\n}\r\n```\r\n\r\n### parseChar(character, state = defaultState())\r\n\r\nParses the single character and returns the state. See `parse` for the structure of the returned state object. N.B. character must be a single character not a multi character string.\r\n\r\n### defaultState()\r\n\r\nGet a default starting state.\r\n\r\n### isPunctuator(character)\r\n\r\nReturns `true` if `character` represents punctuation in JavaScript.\r\n\r\n### isKeyword(name)\r\n\r\nReturns `true` if `name` is a keyword in JavaScript.\r\n\r\n## State\r\n\r\nA state is an object with the following structure\r\n\r\n```js\r\n{\r\n lineComment: false, //true if inside a line comment\r\n blockComment: false, //true if inside a block comment\r\n\r\n singleQuote: false, //true if inside a single quoted string\r\n doubleQuote: false, //true if inside a double quoted string\r\n regexp: false, //true if inside a regular expression\r\n escaped: false, //true if in a string and the last character was an escape character\r\n\r\n roundDepth: 0, //number of un-closed open `(` brackets\r\n curlyDepth: 0, //number of un-closed open `{` brackets\r\n squareDepth: 0 //number of un-closed open `[` brackets\r\n}\r\n```\r\n\r\nIt also has the following useful methods:\r\n\r\n- `.isString()` returns `true` if the current location is inside a string.\r\n- `.isComment()` returns `true` if the current location is inside a comment.\r\n- `isNesting()` returns `true` if the current location is anything but at the top level, i.e. with no nesting.\r\n\r\n## License\r\n\r\nMIT",
"readmeFilename": "README.md",
"bugs": {
"url": "https://github.com/ForbesLindesay/character-parser/issues"
},
"homepage": "https://github.com/ForbesLindesay/character-parser",
"_id": "character-parser@1.2.1",
"dist": {
"shasum": "22653d21bf582cb85ab9b5ce021007ebdec93e07"
},
"_from": "character-parser@1.2.1",
"_resolved": "https://registry.npmjs.org/character-parser/-/character-parser-1.2.1.tgz"
}
File diff suppressed because it is too large Load Diff
+19
View File
@@ -0,0 +1,19 @@
Copyright (C) 2015 JakubPawlowicz.com
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
+369
View File
@@ -0,0 +1,369 @@
[![NPM version](https://img.shields.io/npm/v/clean-css.svg?style=flat)](https://www.npmjs.com/package/clean-css)
[![Linux Build Status](https://img.shields.io/travis/jakubpawlowicz/clean-css/master.svg?style=flat&label=Linux%20build)](https://travis-ci.org/jakubpawlowicz/clean-css)
[![Windows Build status](https://img.shields.io/appveyor/ci/jakubpawlowicz/clean-css/master.svg?style=flat&label=Windows%20build)](https://ci.appveyor.com/project/jakubpawlowicz/clean-css/branch/master)
[![Dependency Status](https://img.shields.io/david/jakubpawlowicz/clean-css.svg?style=flat)](https://david-dm.org/jakubpawlowicz/clean-css)
[![devDependency Status](https://img.shields.io/david/dev/jakubpawlowicz/clean-css.svg?style=flat)](https://david-dm.org/jakubpawlowicz/clean-css#info=devDependencies)
## What is clean-css?
Clean-css is a fast and efficient [Node.js](http://nodejs.org/) library for minifying CSS files.
According to [tests](http://goalsmashers.github.io/css-minification-benchmark/) it is one of the best available.
## Usage
### What are the requirements?
```
Node.js 0.10+ (tested on CentOS, Ubuntu, OS X 10.6+, and Windows 7+) or io.js 3.0+
```
### How to install clean-css?
```
npm install clean-css
```
### How to use clean-css CLI?
Clean-css accepts the following command line arguments (please make sure
you use `<source-file>` as the very last argument to avoid potential issues):
```
cleancss [options] source-file, [source-file, ...]
-h, --help output usage information
-v, --version output the version number
-b, --keep-line-breaks Keep line breaks
-c, --compatibility [ie7|ie8] Force compatibility mode (see Readme for advanced examples)
-d, --debug Shows debug information (minification time & compression efficiency)
-o, --output [output-file] Use [output-file] as output instead of STDOUT
-r, --root [root-path] Set a root path to which resolve absolute @import rules
-s, --skip-import Disable @import processing
-t, --timeout [seconds] Per connection timeout when fetching remote @imports (defaults to 5 seconds)
--rounding-precision [n] Rounds to `N` decimal places. Defaults to 2. -1 disables rounding
--s0 Remove all special comments, i.e. /*! comment */
--s1 Remove all special comments but the first one
--semantic-merging Enables unsafe mode by assuming BEM-like semantic stylesheets (warning, this may break your styling!)
--skip-advanced Disable advanced optimizations - ruleset reordering & merging
--skip-aggressive-merging Disable properties merging based on their order
--skip-import-from [rules] Disable @import processing for specified rules
--skip-media-merging Disable @media merging
--skip-rebase Disable URLs rebasing
--skip-restructuring Disable restructuring optimizations
--skip-shorthand-compacting Disable shorthand compacting
--source-map Enables building input's source map
--source-map-inline-sources Enables inlining sources inside source maps
```
#### Examples:
To minify a **public.css** file into **public-min.css** do:
```
cleancss -o public-min.css public.css
```
To minify the same **public.css** into the standard output skip the `-o` parameter:
```
cleancss public.css
```
More likely you would like to concatenate a couple of files.
If you are on a Unix-like system:
```bash
cat one.css two.css three.css | cleancss -o merged-and-minified.css
```
On Windows:
```bat
type one.css two.css three.css | cleancss -o merged-and-minified.css
```
Or even gzip the result at once:
```bash
cat one.css two.css three.css | cleancss | gzip -9 -c > merged-minified-and-gzipped.css.gz
```
### How to use clean-css API?
```js
var CleanCSS = require('clean-css');
var source = 'a{font-weight:bold;}';
var minified = new CleanCSS().minify(source).styles;
```
CleanCSS constructor accepts a hash as a parameter, i.e.,
`new CleanCSS(options)` with the following options available:
* `advanced` - set to false to disable advanced optimizations - selector & property merging, reduction, etc.
* `aggressiveMerging` - set to false to disable aggressive merging of properties.
* `benchmark` - turns on benchmarking mode measuring time spent on cleaning up (run `npm run bench` to see example)
* `compatibility` - enables compatibility mode, see [below for more examples](#how-to-set-compatibility-mode)
* `debug` - set to true to get minification statistics under `stats` property (see `test/custom-test.js` for examples)
* `inliner` - a hash of options for `@import` inliner, see [test/protocol-imports-test.js](https://github.com/jakubpawlowicz/clean-css/blob/master/test/protocol-imports-test.js#L372) for examples, or [this comment](https://github.com/jakubpawlowicz/clean-css/issues/612#issuecomment-119594185) for a proxy use case.
* `keepBreaks` - whether to keep line breaks (default is false)
* `keepSpecialComments` - `*` for keeping all (default), `1` for keeping first one only, `0` for removing all
* `mediaMerging` - whether to merge `@media` at-rules (default is true)
* `processImport` - whether to process `@import` rules
* `processImportFrom` - a list of `@import` rules, can be `['all']` (default), `['local']`, `['remote']`, or a blacklisted path e.g. `['!fonts.googleapis.com']`
* `rebase` - set to false to skip URL rebasing
* `relativeTo` - path to **resolve** relative `@import` rules and URLs
* `restructuring` - set to false to disable restructuring in advanced optimizations
* `root` - path to **resolve** absolute `@import` rules and **rebase** relative URLs
* `roundingPrecision` - rounding precision; defaults to `2`; `-1` disables rounding
* `semanticMerging` - set to true to enable semantic merging mode which assumes BEM-like content (default is false as it's highly likely this will break your stylesheets - **use with caution**!)
* `shorthandCompacting` - set to false to skip shorthand compacting (default is true unless sourceMap is set when it's false)
* `sourceMap` - exposes source map under `sourceMap` property, e.g. `new CleanCSS().minify(source).sourceMap` (default is false)
If input styles are a product of CSS preprocessor (Less, Sass) an input source map can be passed as a string.
* `sourceMapInlineSources` - set to true to inline sources inside a source map's `sourcesContent` field (defaults to false)
It is also required to process inlined sources from input source maps.
* `target` - path to a folder or an output file to which **rebase** all URLs
The output of `minify` method (or the 2nd argument to passed callback) is a hash containing the following fields:
* `styles` - optimized output CSS as a string
* `sourceMap` - output source map (if requested with `sourceMap` option)
* `errors` - a list of errors raised
* `warnings` - a list of warnings raised
* `stats` - a hash of statistic information (if requested with `debug` option):
* `originalSize` - original content size (after import inlining)
* `minifiedSize` - optimized content size
* `timeSpent` - time spent on optimizations
* `efficiency` - a ratio of output size to input size (e.g. 25% if content was reduced from 100 bytes to 75 bytes)
#### How to make sure remote `@import`s are processed correctly?
In order to inline remote `@import` statements you need to provide a callback to minify method, e.g.:
```js
var CleanCSS = require('clean-css');
var source = '@import url(http://path/to/remote/styles);';
new CleanCSS().minify(source, function (errors, minified) {
// minified.styles
});
```
This is due to a fact, that, while local files can be read synchronously, remote resources can only be processed asynchronously.
If you don't provide a callback, then remote `@import`s will be left intact.
### How to use clean-css with build tools?
* [Broccoli](https://github.com/broccolijs/broccoli#broccoli): [broccoli-clean-css](https://github.com/shinnn/broccoli-clean-css)
* [Brunch](http://brunch.io/): [clean-css-brunch](https://github.com/brunch/clean-css-brunch)
* [Grunt](http://gruntjs.com): [grunt-contrib-cssmin](https://github.com/gruntjs/grunt-contrib-cssmin)
* [Gulp](http://gulpjs.com/): [gulp-minify-css](https://github.com/jonathanepollack/gulp-minify-css)
* [Gulp](http://gulpjs.com/): [using vinyl-map as a wrapper - courtesy of @sogko](https://github.com/jakubpawlowicz/clean-css/issues/342)
* [component-builder2](https://github.com/component/builder2.js): [builder-clean-css](https://github.com/poying/builder-clean-css)
* [Metalsmith](http://metalsmith.io): [metalsmith-clean-css](https://github.com/aymericbeaumet/metalsmith-clean-css)
* [Lasso](https://github.com/lasso-js/lasso): [lasso-clean-css](https://github.com/yomed/lasso-clean-css)
### What are the clean-css' dev commands?
First clone the source, then run:
* `npm run bench` for clean-css benchmarks (see [test/bench.js](https://github.com/jakubpawlowicz/clean-css/blob/master/test/bench.js) for details)
* `npm run browserify` to create the browser-ready clean-css version
* `npm run check` to check JS sources with [JSHint](https://github.com/jshint/jshint/)
* `npm test` for the test suite
## How to contribute to clean-css?
See [CONTRIBUTING.md](https://github.com/jakubpawlowicz/clean-css/blob/master/CONTRIBUTING.md).
## Tips & Tricks
### How to preserve a comment block?
Use the `/*!` notation instead of the standard one `/*`:
```css
/*!
Important comments included in minified output.
*/
```
### How to rebase relative image URLs?
Clean-css will handle it automatically for you (since version 1.1) in the following cases:
* When using the CLI:
1. Use an output path via `-o`/`--output` to rebase URLs as relative to the output file.
2. Use a root path via `-r`/`--root` to rebase URLs as absolute from the given root path.
3. If you specify both then `-r`/`--root` takes precendence.
* When using clean-css as a library:
1. Use a combination of `relativeTo` and `target` options for relative rebase (same as 1 in CLI).
2. Use a combination of `relativeTo` and `root` options for absolute rebase (same as 2 in CLI).
3. `root` takes precendence over `target` as in CLI.
### How to generate source maps?
Source maps are supported since version 3.0.
Additionally to mapping original CSS files, clean-css also supports input source maps, so minified styles can be mapped into their [Less](http://lesscss.org/) or [Sass](http://sass-lang.com/) sources directly.
Source maps are generated using [source-map](https://github.com/mozilla/source-map/) module from Mozilla.
#### Using CLI
To generate a source map, use `--source-map` switch, e.g.:
```
cleancss --source-map --output styles.min.css styles.css
```
Name of the output file is required, so a map file, named by adding `.map` suffix to output file name, can be created (styles.min.css.map in this case).
#### Using API
To generate a source map, use `sourceMap: true` option, e.g.:
```js
new CleanCSS({ sourceMap: true, target: pathToOutputDirectory })
.minify(source, function (minified) {
// access minified.sourceMap for SourceMapGenerator object
// see https://github.com/mozilla/source-map/#sourcemapgenerator for more details
// see https://github.com/jakubpawlowicz/clean-css/blob/master/bin/cleancss#L114 on how it's used in clean-css' CLI
});
```
Using API you can also pass an input source map directly:
```js
new CleanCSS({ sourceMap: inputSourceMapAsString, target: pathToOutputDirectory })
.minify(source, function (minified) {
// access minified.sourceMap to access SourceMapGenerator object
// see https://github.com/mozilla/source-map/#sourcemapgenerator for more details
// see https://github.com/jakubpawlowicz/clean-css/blob/master/bin/cleancss#L114 on how it's used in clean-css' CLI
});
```
Or even multiple input source maps at once (available since version 3.1):
```js
new CleanCSS({ sourceMap: true, target: pathToOutputDirectory }).minify({
'path/to/source/1': {
styles: '...styles...',
sourceMap: '...source-map...'
},
'path/to/source/2': {
styles: '...styles...',
sourceMap: '...source-map...'
}
}, function (minified) {
// access minified.sourceMap as above
});
```
### How to minify multiple files with API?
#### Passing an array
```js
new CleanCSS().minify(['path/to/file/one', 'path/to/file/two']);
```
#### Passing a hash
```js
new CleanCSS().minify({
'path/to/file/one': {
styles: 'contents of file one'
},
'path/to/file/two': {
styles: 'contents of file two'
}
});
```
### How to set a compatibility mode?
Compatibility settings are controlled by `--compatibility` switch (CLI) and `compatibility` option (library mode).
In both modes the following values are allowed:
* `'ie7'` - Internet Explorer 7 compatibility mode
* `'ie8'` - Internet Explorer 8 compatibility mode
* `''` or `'*'` (default) - Internet Explorer 9+ compatibility mode
Since clean-css 3 a fine grained control is available over
[those settings](https://github.com/jakubpawlowicz/clean-css/blob/master/lib/utils/compatibility.js),
with the following options available:
* `'[+-]colors.opacity'` - - turn on (+) / off (-) `rgba()` / `hsla()` declarations removal
* `'[+-]properties.backgroundClipMerging'` - turn on / off background-clip merging into shorthand
* `'[+-]properties.backgroundOriginMerging'` - turn on / off background-origin merging into shorthand
* `'[+-]properties.backgroundSizeMerging'` - turn on / off background-size merging into shorthand
* `'[+-]properties.colors'` - turn on / off any color optimizations
* `'[+-]properties.ieBangHack'` - turn on / off IE bang hack removal
* `'[+-]properties.iePrefixHack'` - turn on / off IE prefix hack removal
* `'[+-]properties.ieSuffixHack'` - turn on / off IE suffix hack removal
* `'[+-]properties.merging'` - turn on / off property merging based on understandability
* `'[+-]properties.spaceAfterClosingBrace'` - turn on / off removing space after closing brace - `url() no-repeat` into `url()no-repeat`
* `'[+-]properties.urlQuotes'` - turn on / off `url()` quoting
* `'[+-]properties.zeroUnits'` - turn on / off units removal after a `0` value
* `'[+-]selectors.adjacentSpace'` - turn on / off extra space before `nav` element
* `'[+-]selectors.ie7Hack'` - turn on / off IE7 selector hack removal (`*+html...`)
* `'[+-]selectors.special'` - a regular expression with all special, unmergeable selectors (leave it empty unless you know what you are doing)
* `'[+-]units.ch'` - turn on / off treating `ch` as a proper unit
* `'[+-]units.in'` - turn on / off treating `in` as a proper unit
* `'[+-]units.pc'` - turn on / off treating `pc` as a proper unit
* `'[+-]units.pt'` - turn on / off treating `pt` as a proper unit
* `'[+-]units.rem'` - turn on / off treating `rem` as a proper unit
* `'[+-]units.vh'` - turn on / off treating `vh` as a proper unit
* `'[+-]units.vm'` - turn on / off treating `vm` as a proper unit
* `'[+-]units.vmax'` - turn on / off treating `vmax` as a proper unit
* `'[+-]units.vmin'` - turn on / off treating `vmin` as a proper unit
* `'[+-]units.vm'` - turn on / off treating `vm` as a proper unit
For example, using `--compatibility 'ie8,+units.rem'` will ensure IE8 compatibility while enabling `rem` units so the following style `margin:0px 0rem` can be shortened to `margin:0`, while in pure IE8 mode it can't be.
To pass a single off (-) switch in CLI please use the following syntax `--compatibility *,-units.rem`.
In library mode you can also pass `compatibility` as a hash of options.
### What advanced optimizations are applied?
All advanced optimizations are dispatched [here](https://github.com/jakubpawlowicz/clean-css/blob/master/lib/selectors/advanced.js#L59), and this is what they do:
* `recursivelyOptimizeBlocks` - does all the following operations on a block (think `@media` or `@keyframe` at-rules);
* `recursivelyOptimizeProperties` - optimizes properties in rulesets and "flat at-rules" (like @font-face) by splitting them into components (e.g. `margin` into `margin-(*)`), optimizing, and rebuilding them back. You may want to use `shorthandCompacting` option to control whether you want to turn multiple (long-hand) properties into a shorthand ones;
* `removeDuplicates` - gets rid of duplicate rulesets with exactly the same set of properties (think of including the same Sass / Less partial twice for no good reason);
* `mergeAdjacent` - merges adjacent rulesets with the same selector or rules;
* `reduceNonAdjacent` - identifies which properties are overridden in same-selector non-adjacent rulesets, and removes them;
* `mergeNonAdjacentBySelector` - identifies same-selector non-adjacent rulesets which can be moved (!) to be merged, requires all intermediate rulesets to not redefine the moved properties, or if redefined to be either more coarse grained (e.g. `margin` vs `margin-top`) or have the same value;
* `mergeNonAdjacentByBody` - same as the one above but for same-rules non-adjacent rulesets;
* `restructure` - tries to reorganize different-selector different-rules rulesets so they take less space, e.g. `.one{padding:0}.two{margin:0}.one{margin-bottom:3px}` into `.two{margin:0}.one{padding:0;margin-bottom:3px}`;
* `removeDuplicateMediaQueries` - removes duplicated `@media` at-rules;
* `mergeMediaQueries` - merges non-adjacent `@media` at-rules by same rules as `mergeNonAdjacentBy*` above;
## Acknowledgments (sorted alphabetically)
* Anthony Barre ([@abarre](https://github.com/abarre)) for improvements to
`@import` processing, namely introducing the `--skip-import` /
`processImport` options.
* Simon Altschuler ([@altschuler](https://github.com/altschuler)) for fixing
`@import` processing inside comments.
* Isaac ([@facelessuser](https://github.com/facelessuser)) for pointing out
a flaw in clean-css' stateless mode.
* Jan Michael Alonzo ([@jmalonzo](https://github.com/jmalonzo)) for a patch
removing node.js' old `sys` package.
* Luke Page ([@lukeapage](https://github.com/lukeapage)) for suggestions and testing the source maps feature.
Plus everyone else involved in [#125](https://github.com/jakubpawlowicz/clean-css/issues/125) for pushing it forward.
* Timur Kristóf ([@Venemo](https://github.com/Venemo)) for an outstanding
contribution of advanced property optimizer for 2.2 release.
* Vincent Voyer ([@vvo](https://github.com/vvo)) for a patch with better
empty element regex and for inspiring us to do many performance improvements
in 0.4 release.
* [@XhmikosR](https://github.com/XhmikosR) for suggesting new features
(option to remove special comments and strip out URLs quotation) and
pointing out numerous improvements (JSHint, media queries).
## License
Clean-css is released under the [MIT License](https://github.com/jakubpawlowicz/clean-css/blob/master/LICENSE).
+168
View File
@@ -0,0 +1,168 @@
#!/usr/bin/env node
var fs = require('fs');
var path = require('path');
var CleanCSS = require('../index');
var commands = require('commander');
var packageConfig = fs.readFileSync(path.join(path.dirname(fs.realpathSync(process.argv[1])), '../package.json'));
var buildVersion = JSON.parse(packageConfig).version;
var isWindows = process.platform == 'win32';
// Specify commander options to parse command line params correctly
commands
.version(buildVersion, '-v, --version')
.usage('[options] source-file, [source-file, ...]')
.option('-b, --keep-line-breaks', 'Keep line breaks')
.option('-c, --compatibility [ie7|ie8]', 'Force compatibility mode (see Readme for advanced examples)')
.option('-d, --debug', 'Shows debug information (minification time & compression efficiency)')
.option('-o, --output [output-file]', 'Use [output-file] as output instead of STDOUT')
.option('-r, --root [root-path]', 'Set a root path to which resolve absolute @import rules')
.option('-s, --skip-import', 'Disable @import processing')
.option('-t, --timeout [seconds]', 'Per connection timeout when fetching remote @imports (defaults to 5 seconds)')
.option('--rounding-precision [n]', 'Rounds to `N` decimal places. Defaults to 2. -1 disables rounding', parseInt)
.option('--s0', 'Remove all special comments, i.e. /*! comment */')
.option('--s1', 'Remove all special comments but the first one')
.option('--semantic-merging', 'Enables unsafe mode by assuming BEM-like semantic stylesheets (warning, this may break your styling!)')
.option('--skip-advanced', 'Disable advanced optimizations - ruleset reordering & merging')
.option('--skip-aggressive-merging', 'Disable properties merging based on their order')
.option('--skip-import-from [rules]', 'Disable @import processing for specified rules', function (val) { return val.split(','); }, [])
.option('--skip-media-merging', 'Disable @media merging')
.option('--skip-rebase', 'Disable URLs rebasing')
.option('--skip-restructuring', 'Disable restructuring optimizations')
.option('--skip-shorthand-compacting', 'Disable shorthand compacting')
.option('--source-map', 'Enables building input\'s source map')
.option('--source-map-inline-sources', 'Enables inlining sources inside source maps');
commands.on('--help', function () {
console.log(' Examples:\n');
console.log(' %> cleancss one.css');
console.log(' %> cleancss -o one-min.css one.css');
if (isWindows) {
console.log(' %> type one.css two.css three.css | cleancss -o merged-and-minified.css');
} else {
console.log(' %> cat one.css two.css three.css | cleancss -o merged-and-minified.css');
console.log(' %> cat one.css two.css three.css | cleancss | gzip -9 -c > merged-minified-and-gzipped.css.gz');
}
console.log('');
process.exit();
});
commands.parse(process.argv);
// If no sensible data passed in just print help and exit
var fromStdin = !process.env.__DIRECT__ && !process.stdin.isTTY;
if (!fromStdin && commands.args.length === 0) {
commands.outputHelp();
return 0;
}
// Now coerce commands into CleanCSS configuration...
var options = {
advanced: commands.skipAdvanced ? false : true,
aggressiveMerging: commands.skipAggressiveMerging ? false : true,
compatibility: commands.compatibility,
debug: commands.debug,
inliner: commands.timeout ? { timeout: parseFloat(commands.timeout) * 1000 } : undefined,
keepBreaks: !!commands.keepLineBreaks,
keepSpecialComments: commands.s0 ? 0 : (commands.s1 ? 1 : '*'),
mediaMerging: commands.skipMediaMerging ? false : true,
processImport: commands.skipImport ? false : true,
processImportFrom: processImportFrom(commands.skipImportFrom),
rebase: commands.skipRebase ? false : true,
restructuring: commands.skipRestructuring ? false : true,
root: commands.root,
roundingPrecision: commands.roundingPrecision,
semanticMerging: commands.semanticMerging ? true : false,
shorthandCompacting: commands.skipShorthandCompacting ? false : true,
sourceMap: commands.sourceMap,
sourceMapInlineSources: commands.sourceMapInlineSources,
target: commands.output
};
if (options.root || commands.args.length > 0)
options.relativeTo = path.dirname(path.resolve(options.root || commands.args[0]));
if (options.sourceMap && !options.target) {
outputFeedback(['Source maps will not be built because you have not specified an output file.'], true);
options.sourceMap = false;
}
// ... and do the magic!
if (commands.args.length > 0) {
minify(commands.args);
} else {
var stdin = process.openStdin();
stdin.setEncoding('utf-8');
var data = '';
stdin.on('data', function (chunk) {
data += chunk;
});
stdin.on('end', function () {
minify(data);
});
}
function processImportFrom(rules) {
if (rules.length === 0) {
return ['all'];
} else if (rules.length == 1 && rules[0] == 'all') {
return [];
} else {
return rules.map(function (rule) {
if (rule == 'local')
return 'remote';
else if (rule == 'remote')
return 'local';
else
return '!' + rule;
});
}
}
function minify(data) {
new CleanCSS(options).minify(data, function (errors, minified) {
if (options.debug) {
console.error('Original: %d bytes', minified.stats.originalSize);
console.error('Minified: %d bytes', minified.stats.minifiedSize);
console.error('Efficiency: %d%', ~~(minified.stats.efficiency * 10000) / 100.0);
console.error('Time spent: %dms', minified.stats.timeSpent);
}
outputFeedback(minified.errors, true);
outputFeedback(minified.warnings);
if (minified.errors.length > 0)
process.exit(1);
if (minified.sourceMap) {
var mapFilename = path.basename(options.target) + '.map';
output(minified.styles + '/*# sourceMappingURL=' + mapFilename + ' */');
outputMap(minified.sourceMap, mapFilename);
} else {
output(minified.styles);
}
});
}
function output(minified) {
if (options.target)
fs.writeFileSync(options.target, minified, 'utf8');
else
process.stdout.write(minified);
}
function outputMap(sourceMap, mapFilename) {
var mapPath = path.join(path.dirname(options.target), mapFilename);
fs.writeFileSync(mapPath, sourceMap.toString(), 'utf-8');
}
function outputFeedback(messages, isError) {
var prefix = isError ? '\x1B[31mERROR\x1B[39m:' : 'WARNING:';
messages.forEach(function (message) {
console.error('%s %s', prefix, message);
});
}
+1
View File
@@ -0,0 +1 @@
module.exports = require('./lib/clean');
+231
View File
@@ -0,0 +1,231 @@
/**
* Clean-css - https://github.com/jakubpawlowicz/clean-css
* Released under the terms of MIT license
*
* Copyright (C) 2015 JakubPawlowicz.com
*/
var ImportInliner = require('./imports/inliner');
var rebaseUrls = require('./urls/rebase');
var tokenize = require('./tokenizer/tokenize');
var simpleOptimize = require('./selectors/simple');
var advancedOptimize = require('./selectors/advanced');
var simpleStringify = require('./stringifier/simple');
var sourceMapStringify = require('./stringifier/source-maps');
var CommentsProcessor = require('./text/comments-processor');
var ExpressionsProcessor = require('./text/expressions-processor');
var FreeTextProcessor = require('./text/free-text-processor');
var UrlsProcessor = require('./text/urls-processor');
var Compatibility = require('./utils/compatibility');
var InputSourceMapTracker = require('./utils/input-source-map-tracker');
var SourceTracker = require('./utils/source-tracker');
var SourceReader = require('./utils/source-reader');
var Validator = require('./properties/validator');
var fs = require('fs');
var path = require('path');
var url = require('url');
var override = require('./utils/object').override;
var DEFAULT_TIMEOUT = 5000;
var CleanCSS = module.exports = function CleanCSS(options) {
options = options || {};
this.options = {
advanced: undefined === options.advanced ? true : !!options.advanced,
aggressiveMerging: undefined === options.aggressiveMerging ? true : !!options.aggressiveMerging,
benchmark: options.benchmark,
compatibility: new Compatibility(options.compatibility).toOptions(),
debug: options.debug,
explicitRoot: !!options.root,
explicitTarget: !!options.target,
inliner: options.inliner || {},
keepBreaks: options.keepBreaks || false,
keepSpecialComments: 'keepSpecialComments' in options ? options.keepSpecialComments : '*',
mediaMerging: undefined === options.mediaMerging ? true : !!options.mediaMerging,
processImport: undefined === options.processImport ? true : !!options.processImport,
processImportFrom: importOptionsFrom(options.processImportFrom),
rebase: undefined === options.rebase ? true : !!options.rebase,
relativeTo: options.relativeTo,
restructuring: undefined === options.restructuring ? true : !!options.restructuring,
root: options.root || process.cwd(),
roundingPrecision: options.roundingPrecision,
semanticMerging: undefined === options.semanticMerging ? false : !!options.semanticMerging,
shorthandCompacting: undefined === options.shorthandCompacting ? true : !!options.shorthandCompacting,
sourceMap: options.sourceMap,
sourceMapInlineSources: !!options.sourceMapInlineSources,
target: !options.target || missingDirectory(options.target) || presentDirectory(options.target) ? options.target : path.dirname(options.target)
};
this.options.inliner.timeout = this.options.inliner.timeout || DEFAULT_TIMEOUT;
this.options.inliner.request = override(
/* jshint camelcase: false */
proxyOptionsFrom(process.env.HTTP_PROXY || process.env.http_proxy),
this.options.inliner.request || {}
);
};
function importOptionsFrom(rules) {
return undefined === rules ? ['all'] : rules;
}
function missingDirectory(filepath) {
return !fs.existsSync(filepath) && !/\.css$/.test(filepath);
}
function presentDirectory(filepath) {
return fs.existsSync(filepath) && fs.statSync(filepath).isDirectory();
}
function proxyOptionsFrom(httpProxy) {
return httpProxy ?
{
hostname: url.parse(httpProxy).hostname,
port: parseInt(url.parse(httpProxy).port)
} :
{};
}
CleanCSS.prototype.minify = function (data, callback) {
var context = {
stats: {},
errors: [],
warnings: [],
options: this.options,
debug: this.options.debug,
localOnly: !callback,
sourceTracker: new SourceTracker(),
validator: new Validator(this.options.compatibility)
};
if (context.options.sourceMap)
context.inputSourceMapTracker = new InputSourceMapTracker(context);
context.sourceReader = new SourceReader(context, data);
data = context.sourceReader.toString();
if (context.options.processImport || data.indexOf('@shallow') > 0) {
// inline all imports
var runner = callback ?
process.nextTick :
function (callback) { return callback(); };
return runner(function () {
return new ImportInliner(context).process(data, {
localOnly: context.localOnly,
imports: context.options.processImportFrom,
whenDone: runMinifier(callback, context)
});
});
} else {
return runMinifier(callback, context)(data);
}
};
function runMinifier(callback, context) {
function whenSourceMapReady (data) {
data = context.options.debug ?
minifyWithDebug(context, data) :
minify(context, data);
data = withMetadata(context, data);
return callback ?
callback.call(null, context.errors.length > 0 ? context.errors : null, data) :
data;
}
return function (data) {
if (context.options.sourceMap) {
return context.inputSourceMapTracker.track(data, function () {
if (context.options.sourceMapInlineSources) {
return context.inputSourceMapTracker.resolveSources(function () {
return whenSourceMapReady(data);
});
} else {
return whenSourceMapReady(data);
}
});
} else {
return whenSourceMapReady(data);
}
};
}
function withMetadata(context, data) {
data.stats = context.stats;
data.errors = context.errors;
data.warnings = context.warnings;
return data;
}
function minifyWithDebug(context, data) {
var startedAt = process.hrtime();
context.stats.originalSize = context.sourceTracker.removeAll(data).length;
data = minify(context, data);
var elapsed = process.hrtime(startedAt);
context.stats.timeSpent = ~~(elapsed[0] * 1e3 + elapsed[1] / 1e6);
context.stats.efficiency = 1 - data.styles.length / context.stats.originalSize;
context.stats.minifiedSize = data.styles.length;
return data;
}
function benchmark(runner) {
return function (processor, action) {
var name = processor.constructor.name + '#' + action;
var start = process.hrtime();
runner(processor, action);
var itTook = process.hrtime(start);
console.log('%d ms: ' + name, 1000 * itTook[0] + itTook[1] / 1000000);
};
}
function minify(context, data) {
var options = context.options;
var commentsProcessor = new CommentsProcessor(context, options.keepSpecialComments, options.keepBreaks, options.sourceMap);
var expressionsProcessor = new ExpressionsProcessor(options.sourceMap);
var freeTextProcessor = new FreeTextProcessor(options.sourceMap);
var urlsProcessor = new UrlsProcessor(context, options.sourceMap, options.compatibility.properties.urlQuotes);
var stringify = options.sourceMap ? sourceMapStringify : simpleStringify;
var run = function (processor, action) {
data = typeof processor == 'function' ?
processor(data) :
processor[action](data);
};
if (options.benchmark)
run = benchmark(run);
run(commentsProcessor, 'escape');
run(expressionsProcessor, 'escape');
run(urlsProcessor, 'escape');
run(freeTextProcessor, 'escape');
function restoreEscapes(data, prefixContent) {
data = freeTextProcessor.restore(data, prefixContent);
data = urlsProcessor.restore(data);
data = options.rebase ? rebaseUrls(data, context) : data;
data = expressionsProcessor.restore(data);
return commentsProcessor.restore(data);
}
var tokens = tokenize(data, context);
simpleOptimize(tokens, options);
if (options.advanced)
advancedOptimize(tokens, options, context.validator, true);
return stringify(tokens, options, restoreEscapes, context.inputSourceMapTracker);
}
@@ -0,0 +1,186 @@
var HexNameShortener = {};
var COLORS = {
aliceblue: '#f0f8ff',
antiquewhite: '#faebd7',
aqua: '#0ff',
aquamarine: '#7fffd4',
azure: '#f0ffff',
beige: '#f5f5dc',
bisque: '#ffe4c4',
black: '#000',
blanchedalmond: '#ffebcd',
blue: '#00f',
blueviolet: '#8a2be2',
brown: '#a52a2a',
burlywood: '#deb887',
cadetblue: '#5f9ea0',
chartreuse: '#7fff00',
chocolate: '#d2691e',
coral: '#ff7f50',
cornflowerblue: '#6495ed',
cornsilk: '#fff8dc',
crimson: '#dc143c',
cyan: '#0ff',
darkblue: '#00008b',
darkcyan: '#008b8b',
darkgoldenrod: '#b8860b',
darkgray: '#a9a9a9',
darkgreen: '#006400',
darkgrey: '#a9a9a9',
darkkhaki: '#bdb76b',
darkmagenta: '#8b008b',
darkolivegreen: '#556b2f',
darkorange: '#ff8c00',
darkorchid: '#9932cc',
darkred: '#8b0000',
darksalmon: '#e9967a',
darkseagreen: '#8fbc8f',
darkslateblue: '#483d8b',
darkslategray: '#2f4f4f',
darkslategrey: '#2f4f4f',
darkturquoise: '#00ced1',
darkviolet: '#9400d3',
deeppink: '#ff1493',
deepskyblue: '#00bfff',
dimgray: '#696969',
dimgrey: '#696969',
dodgerblue: '#1e90ff',
firebrick: '#b22222',
floralwhite: '#fffaf0',
forestgreen: '#228b22',
fuchsia: '#f0f',
gainsboro: '#dcdcdc',
ghostwhite: '#f8f8ff',
gold: '#ffd700',
goldenrod: '#daa520',
gray: '#808080',
green: '#008000',
greenyellow: '#adff2f',
grey: '#808080',
honeydew: '#f0fff0',
hotpink: '#ff69b4',
indianred: '#cd5c5c',
indigo: '#4b0082',
ivory: '#fffff0',
khaki: '#f0e68c',
lavender: '#e6e6fa',
lavenderblush: '#fff0f5',
lawngreen: '#7cfc00',
lemonchiffon: '#fffacd',
lightblue: '#add8e6',
lightcoral: '#f08080',
lightcyan: '#e0ffff',
lightgoldenrodyellow: '#fafad2',
lightgray: '#d3d3d3',
lightgreen: '#90ee90',
lightgrey: '#d3d3d3',
lightpink: '#ffb6c1',
lightsalmon: '#ffa07a',
lightseagreen: '#20b2aa',
lightskyblue: '#87cefa',
lightslategray: '#778899',
lightslategrey: '#778899',
lightsteelblue: '#b0c4de',
lightyellow: '#ffffe0',
lime: '#0f0',
limegreen: '#32cd32',
linen: '#faf0e6',
magenta: '#ff00ff',
maroon: '#800000',
mediumaquamarine: '#66cdaa',
mediumblue: '#0000cd',
mediumorchid: '#ba55d3',
mediumpurple: '#9370db',
mediumseagreen: '#3cb371',
mediumslateblue: '#7b68ee',
mediumspringgreen: '#00fa9a',
mediumturquoise: '#48d1cc',
mediumvioletred: '#c71585',
midnightblue: '#191970',
mintcream: '#f5fffa',
mistyrose: '#ffe4e1',
moccasin: '#ffe4b5',
navajowhite: '#ffdead',
navy: '#000080',
oldlace: '#fdf5e6',
olive: '#808000',
olivedrab: '#6b8e23',
orange: '#ffa500',
orangered: '#ff4500',
orchid: '#da70d6',
palegoldenrod: '#eee8aa',
palegreen: '#98fb98',
paleturquoise: '#afeeee',
palevioletred: '#db7093',
papayawhip: '#ffefd5',
peachpuff: '#ffdab9',
peru: '#cd853f',
pink: '#ffc0cb',
plum: '#dda0dd',
powderblue: '#b0e0e6',
purple: '#800080',
rebeccapurple: '#663399',
red: '#f00',
rosybrown: '#bc8f8f',
royalblue: '#4169e1',
saddlebrown: '#8b4513',
salmon: '#fa8072',
sandybrown: '#f4a460',
seagreen: '#2e8b57',
seashell: '#fff5ee',
sienna: '#a0522d',
silver: '#c0c0c0',
skyblue: '#87ceeb',
slateblue: '#6a5acd',
slategray: '#708090',
slategrey: '#708090',
snow: '#fffafa',
springgreen: '#00ff7f',
steelblue: '#4682b4',
tan: '#d2b48c',
teal: '#008080',
thistle: '#d8bfd8',
tomato: '#ff6347',
turquoise: '#40e0d0',
violet: '#ee82ee',
wheat: '#f5deb3',
white: '#fff',
whitesmoke: '#f5f5f5',
yellow: '#ff0',
yellowgreen: '#9acd32'
};
var toHex = {};
var toName = {};
for (var name in COLORS) {
var hex = COLORS[name];
if (name.length < hex.length)
toName[hex] = name;
else
toHex[name] = hex;
}
var toHexPattern = new RegExp('(^| |,|\\))(' + Object.keys(toHex).join('|') + ')( |,|\\)|$)', 'ig');
var toNamePattern = new RegExp('(' + Object.keys(toName).join('|') + ')([^a-f0-9]|$)', 'ig');
function hexConverter(match, prefix, colorValue, suffix) {
return prefix + toHex[colorValue.toLowerCase()] + suffix;
}
function nameConverter(match, colorValue, suffix) {
return toName[colorValue.toLowerCase()] + suffix;
}
HexNameShortener.shorten = function (value) {
var hasHex = value.indexOf('#') > -1;
var shortened = value.replace(toHexPattern, hexConverter);
if (shortened != value)
shortened = shortened.replace(toHexPattern, hexConverter);
return hasHex ? shortened.replace(toNamePattern, nameConverter) : shortened;
};
module.exports = HexNameShortener;
@@ -0,0 +1,67 @@
// HSL to RGB converter. Both methods adapted from:
// http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript
function HSLColor(hue, saturation, lightness) {
this.hue = hue;
this.saturation = saturation;
this.lightness = lightness;
}
function hslToRgb(h, s, l) {
var r, g, b;
// normalize hue orientation b/w 0 and 360 degrees
h = h % 360;
if (h < 0)
h += 360;
h = ~~h / 360;
if (s < 0)
s = 0;
else if (s > 100)
s = 100;
s = ~~s / 100;
if (l < 0)
l = 0;
else if (l > 100)
l = 100;
l = ~~l / 100;
if (s === 0) {
r = g = b = l; // achromatic
} else {
var q = l < 0.5 ?
l * (1 + s) :
l + s - l * s;
var p = 2 * l - q;
r = hueToRgb(p, q, h + 1/3);
g = hueToRgb(p, q, h);
b = hueToRgb(p, q, h - 1/3);
}
return [~~(r * 255), ~~(g * 255), ~~(b * 255)];
}
function hueToRgb(p, q, t) {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1/6) return p + (q - p) * 6 * t;
if (t < 1/2) return q;
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
}
HSLColor.prototype.toHex = function () {
var asRgb = hslToRgb(this.hue, this.saturation, this.lightness);
var redAsHex = asRgb[0].toString(16);
var greenAsHex = asRgb[1].toString(16);
var blueAsHex = asRgb[2].toString(16);
return '#' +
((redAsHex.length == 1 ? '0' : '') + redAsHex) +
((greenAsHex.length == 1 ? '0' : '') + greenAsHex) +
((blueAsHex.length == 1 ? '0' : '') + blueAsHex);
};
module.exports = HSLColor;
@@ -0,0 +1,16 @@
function RGB(red, green, blue) {
this.red = red;
this.green = green;
this.blue = blue;
}
RGB.prototype.toHex = function () {
var red = Math.max(0, Math.min(~~this.red, 255));
var green = Math.max(0, Math.min(~~this.green, 255));
var blue = Math.max(0, Math.min(~~this.blue, 255));
// Credit: Asen http://jsbin.com/UPUmaGOc/2/edit?js,console
return '#' + ('00000' + (red << 16 | green << 8 | blue).toString(16)).slice(-6);
};
module.exports = RGB;
@@ -0,0 +1,393 @@
var fs = require('fs');
var path = require('path');
var http = require('http');
var https = require('https');
var url = require('url');
var rewriteUrls = require('../urls/rewrite');
var split = require('../utils/split');
var override = require('../utils/object.js').override;
var MAP_MARKER = /\/\*# sourceMappingURL=(\S+) \*\//;
var REMOTE_RESOURCE = /^(https?:)?\/\//;
var NO_PROTOCOL_RESOURCE = /^\/\//;
function ImportInliner (context) {
this.outerContext = context;
}
ImportInliner.prototype.process = function (data, context) {
var root = this.outerContext.options.root;
context = override(context, {
baseRelativeTo: this.outerContext.options.relativeTo || root,
debug: this.outerContext.options.debug,
done: [],
errors: this.outerContext.errors,
left: [],
inliner: this.outerContext.options.inliner,
rebase: this.outerContext.options.rebase,
relativeTo: this.outerContext.options.relativeTo || root,
root: root,
sourceReader: this.outerContext.sourceReader,
sourceTracker: this.outerContext.sourceTracker,
warnings: this.outerContext.warnings,
visited: []
});
return importFrom(data, context);
};
function importFrom(data, context) {
if (context.shallow) {
context.shallow = false;
context.done.push(data);
return processNext(context);
}
var nextStart = 0;
var nextEnd = 0;
var cursor = 0;
var isComment = commentScanner(data);
for (; nextEnd < data.length;) {
nextStart = nextImportAt(data, cursor);
if (nextStart == -1)
break;
if (isComment(nextStart)) {
cursor = nextStart + 1;
continue;
}
nextEnd = data.indexOf(';', nextStart);
if (nextEnd == -1) {
cursor = data.length;
data = '';
break;
}
var noImportPart = data.substring(0, nextStart);
context.done.push(noImportPart);
context.left.unshift([data.substring(nextEnd + 1), context]);
context.afterContent = hasContent(noImportPart);
return inline(data, nextStart, nextEnd, context);
}
// no @import matched in current data
context.done.push(data);
return processNext(context);
}
function rebaseMap(data, source) {
return data.replace(MAP_MARKER, function (match, sourceMapUrl) {
return REMOTE_RESOURCE.test(sourceMapUrl) ?
match :
match.replace(sourceMapUrl, url.resolve(source, sourceMapUrl));
});
}
function nextImportAt(data, cursor) {
var nextLowerCase = data.indexOf('@import', cursor);
var nextUpperCase = data.indexOf('@IMPORT', cursor);
if (nextLowerCase > -1 && nextUpperCase == -1)
return nextLowerCase;
else if (nextLowerCase == -1 && nextUpperCase > -1)
return nextUpperCase;
else
return Math.min(nextLowerCase, nextUpperCase);
}
function processNext(context) {
return context.left.length > 0 ?
importFrom.apply(null, context.left.shift()) :
context.whenDone(context.done.join(''));
}
function commentScanner(data) {
var commentRegex = /(\/\*(?!\*\/)[\s\S]*?\*\/)/;
var lastStartIndex = 0;
var lastEndIndex = 0;
var noComments = false;
// test whether an index is located within a comment
return function scanner(idx) {
var comment;
var localStartIndex = 0;
var localEndIndex = 0;
var globalStartIndex = 0;
var globalEndIndex = 0;
// return if we know there are no more comments
if (noComments)
return false;
// idx can be still within last matched comment (many @import statements inside one comment)
if (idx > lastStartIndex && idx < lastEndIndex)
return true;
comment = data.match(commentRegex);
if (!comment) {
noComments = true;
return false;
}
// get the indexes relative to the current data chunk
lastStartIndex = localStartIndex = comment.index;
localEndIndex = localStartIndex + comment[0].length;
// calculate the indexes relative to the full original data
globalEndIndex = localEndIndex + lastEndIndex;
globalStartIndex = globalEndIndex - comment[0].length;
// chop off data up to and including current comment block
data = data.substring(localEndIndex);
lastEndIndex = globalEndIndex;
// re-run scan if comment ended before the idx
if (globalEndIndex < idx)
return scanner(idx);
return globalEndIndex > idx && idx > globalStartIndex;
};
}
function hasContent(data) {
var isComment = commentScanner(data);
var firstContentIdx = -1;
while (true) {
firstContentIdx = data.indexOf('{', firstContentIdx + 1);
if (firstContentIdx == -1 || !isComment(firstContentIdx))
break;
}
return firstContentIdx > -1;
}
function inline(data, nextStart, nextEnd, context) {
context.shallow = data.indexOf('@shallow') > 0;
var importDeclaration = data
.substring(nextImportAt(data, nextStart) + '@import'.length + 1, nextEnd)
.replace(/@shallow\)$/, ')')
.trim();
var viaUrl = importDeclaration.indexOf('url(') === 0;
var urlStartsAt = viaUrl ? 4 : 0;
var isQuoted = /^['"]/.exec(importDeclaration.substring(urlStartsAt, urlStartsAt + 2));
var urlEndsAt = isQuoted ?
importDeclaration.indexOf(isQuoted[0], urlStartsAt + 1) :
split(importDeclaration, ' ')[0].length - (viaUrl ? 1 : 0);
var importedFile = importDeclaration
.substring(urlStartsAt, urlEndsAt)
.replace(/['"]/g, '')
.replace(/\)$/, '')
.trim();
var mediaQuery = importDeclaration
.substring(urlEndsAt + 1)
.replace(/^\)/, '')
.trim();
var isRemote = context.isRemote || REMOTE_RESOURCE.test(importedFile);
if (isRemote && (context.localOnly || !allowedResource(importedFile, true, context.imports))) {
if (context.afterContent || hasContent(context.done.join('')))
context.warnings.push('Ignoring remote @import of "' + importedFile + '" as no callback given.');
else
restoreImport(importedFile, mediaQuery, context);
return processNext(context);
}
if (!isRemote && !allowedResource(importedFile, false, context.imports)) {
if (context.afterImport)
context.warnings.push('Ignoring local @import of "' + importedFile + '" as after other inlined content.');
else
restoreImport(importedFile, mediaQuery, context);
return processNext(context);
}
if (!isRemote && context.afterContent) {
context.warnings.push('Ignoring local @import of "' + importedFile + '" as after other CSS content.');
return processNext(context);
}
var method = isRemote ? inlineRemoteResource : inlineLocalResource;
return method(importedFile, mediaQuery, context);
}
function allowedResource(importedFile, isRemote, rules) {
if (rules.length === 0)
return false;
if (isRemote && NO_PROTOCOL_RESOURCE.test(importedFile))
importedFile = 'http:' + importedFile;
var match = isRemote ?
url.parse(importedFile).host :
importedFile;
var allowed = true;
for (var i = 0; i < rules.length; i++) {
var rule = rules[i];
if (rule == 'all')
allowed = true;
else if (isRemote && rule == 'local')
allowed = false;
else if (isRemote && rule == 'remote')
allowed = true;
else if (!isRemote && rule == 'remote')
allowed = false;
else if (!isRemote && rule == 'local')
allowed = true;
else if (rule[0] == '!' && rule.substring(1) === match)
allowed = false;
}
return allowed;
}
function inlineRemoteResource(importedFile, mediaQuery, context) {
var importedUrl = REMOTE_RESOURCE.test(importedFile) ?
importedFile :
url.resolve(context.relativeTo, importedFile);
var originalUrl = importedUrl;
if (NO_PROTOCOL_RESOURCE.test(importedUrl))
importedUrl = 'http:' + importedUrl;
if (context.visited.indexOf(importedUrl) > -1)
return processNext(context);
if (context.debug)
console.error('Inlining remote stylesheet: ' + importedUrl);
context.visited.push(importedUrl);
var get = importedUrl.indexOf('http://') === 0 ?
http.get :
https.get;
var errorHandled = false;
function handleError(message) {
if (errorHandled)
return;
errorHandled = true;
context.errors.push('Broken @import declaration of "' + importedUrl + '" - ' + message);
restoreImport(importedUrl, mediaQuery, context);
process.nextTick(function () {
processNext(context);
});
}
var requestOptions = override(url.parse(importedUrl), context.inliner.request);
if (context.inliner.request.hostname !== undefined)
requestOptions.path = requestOptions.href;
get(requestOptions, function (res) {
if (res.statusCode < 200 || res.statusCode > 399) {
return handleError('error ' + res.statusCode);
} else if (res.statusCode > 299) {
var movedUrl = url.resolve(importedUrl, res.headers.location);
return inlineRemoteResource(movedUrl, mediaQuery, context);
}
var chunks = [];
var parsedUrl = url.parse(importedUrl);
res.on('data', function (chunk) {
chunks.push(chunk.toString());
});
res.on('end', function () {
var importedData = chunks.join('');
if (context.rebase)
importedData = rewriteUrls(importedData, { toBase: originalUrl }, context);
context.sourceReader.trackSource(importedUrl, importedData);
importedData = context.sourceTracker.store(importedUrl, importedData);
importedData = rebaseMap(importedData, importedUrl);
if (mediaQuery.length > 0)
importedData = '@media ' + mediaQuery + '{' + importedData + '}';
context.afterImport = true;
var newContext = override(context, {
isRemote: true,
relativeTo: parsedUrl.protocol + '//' + parsedUrl.host + parsedUrl.pathname
});
process.nextTick(function () {
importFrom(importedData, newContext);
});
});
})
.on('error', function (res) {
handleError(res.message);
})
.on('timeout', function () {
handleError('timeout');
})
.setTimeout(context.inliner.timeout);
}
function inlineLocalResource(importedFile, mediaQuery, context) {
var relativeTo = importedFile[0] == '/' ?
context.root :
context.relativeTo;
var fullPath = path.resolve(path.join(relativeTo, importedFile));
if (!fs.existsSync(fullPath) || !fs.statSync(fullPath).isFile()) {
context.errors.push('Broken @import declaration of "' + importedFile + '"');
return processNext(context);
}
if (context.visited.indexOf(fullPath) > -1)
return processNext(context);
if (context.debug)
console.error('Inlining local stylesheet: ' + fullPath);
context.visited.push(fullPath);
var importRelativeTo = path.dirname(fullPath);
var importedData = fs.readFileSync(fullPath, 'utf8');
if (context.rebase) {
var rewriteOptions = {
relative: true,
fromBase: importRelativeTo,
toBase: context.baseRelativeTo
};
importedData = rewriteUrls(importedData, rewriteOptions, context);
}
var relativePath = path.relative(context.root, fullPath);
context.sourceReader.trackSource(relativePath, importedData);
importedData = context.sourceTracker.store(relativePath, importedData);
if (mediaQuery.length > 0)
importedData = '@media ' + mediaQuery + '{' + importedData + '}';
context.afterImport = true;
var newContext = override(context, {
relativeTo: importRelativeTo
});
return importFrom(importedData, newContext);
}
function restoreImport(importedUrl, mediaQuery, context) {
var restoredImport = '@import url(' + importedUrl + ')' + (mediaQuery.length > 0 ? ' ' + mediaQuery : '') + ';';
context.done.push(restoredImport);
}
module.exports = ImportInliner;
@@ -0,0 +1,329 @@
var wrapSingle = require('./wrap-for-optimizing').single;
var split = require('../utils/split');
var MULTIPLEX_SEPARATOR = ',';
function _colorFilter(validator) {
return function (value) {
return value[0] == 'invert' || validator.isValidColor(value[0]);
};
}
function _styleFilter(validator) {
return function (value) {
return value[0] != 'inherit' && validator.isValidStyle(value[0]);
};
}
function _wrapDefault(name, property, compactable) {
var descriptor = compactable[name];
if (descriptor.doubleValues && descriptor.defaultValue.length == 2)
return wrapSingle([[name, property.important], [descriptor.defaultValue[0]], [descriptor.defaultValue[1]]]);
else if (descriptor.doubleValues && descriptor.defaultValue.length == 1)
return wrapSingle([[name, property.important], [descriptor.defaultValue[0]]]);
else
return wrapSingle([[name, property.important], [descriptor.defaultValue]]);
}
function _widthFilter(validator) {
return function (value) {
return value[0] != 'inherit' && validator.isValidWidth(value[0]);
};
}
function background(property, compactable, validator) {
var image = _wrapDefault('background-image', property, compactable);
var position = _wrapDefault('background-position', property, compactable);
var size = _wrapDefault('background-size', property, compactable);
var repeat = _wrapDefault('background-repeat', property, compactable);
var attachment = _wrapDefault('background-attachment', property, compactable);
var origin = _wrapDefault('background-origin', property, compactable);
var clip = _wrapDefault('background-clip', property, compactable);
var color = _wrapDefault('background-color', property, compactable);
var components = [image, position, size, repeat, attachment, origin, clip, color];
var values = property.value;
var positionSet = false;
var clipSet = false;
var originSet = false;
var repeatSet = false;
if (property.value.length == 1 && property.value[0][0] == 'inherit') {
// NOTE: 'inherit' is not a valid value for background-attachment
color.value = image.value = repeat.value = position.value = size.value = origin.value = clip.value = property.value;
return components;
}
for (var i = values.length - 1; i >= 0; i--) {
var value = values[i];
if (validator.isValidBackgroundAttachment(value[0])) {
attachment.value = [value];
} else if (validator.isValidBackgroundBox(value[0])) {
if (clipSet) {
origin.value = [value];
originSet = true;
} else {
clip.value = [value];
clipSet = true;
}
} else if (validator.isValidBackgroundRepeat(value[0])) {
if (repeatSet) {
repeat.value.unshift(value);
} else {
repeat.value = [value];
repeatSet = true;
}
} else if (validator.isValidBackgroundPositionPart(value[0]) || validator.isValidBackgroundSizePart(value[0])) {
if (i > 0) {
var previousValue = values[i - 1];
if (previousValue[0].indexOf('/') > 0) {
var twoParts = split(previousValue[0], '/');
// NOTE: we do this slicing as value may contain metadata too, like for source maps
size.value = [[twoParts.pop()].concat(previousValue.slice(1)), value];
values[i - 1] = [twoParts.pop()].concat(previousValue.slice(1));
} else if (i > 1 && values[i - 2] == '/') {
size.value = [previousValue, value];
i -= 2;
} else if (previousValue[0] == '/') {
size.value = [value];
} else {
if (!positionSet)
position.value = [];
position.value.unshift(value);
positionSet = true;
}
} else {
if (!positionSet)
position.value = [];
position.value.unshift(value);
positionSet = true;
}
} else if (validator.isValidBackgroundPositionAndSize(value[0])) {
var sizeValue = split(value[0], '/');
// NOTE: we do this slicing as value may contain metadata too, like for source maps
size.value = [[sizeValue.pop()].concat(value.slice(1))];
position.value = [[sizeValue.pop()].concat(value.slice(1))];
} else if ((color.value[0][0] == compactable[color.name].defaultValue || color.value[0][0] == 'none') && validator.isValidColor(value[0])) {
color.value = [value];
} else if (validator.isValidUrl(value[0]) || validator.isValidFunction(value[0])) {
image.value = [value];
}
}
if (clipSet && !originSet)
origin.value = clip.value.slice(0);
return components;
}
function borderRadius(property, compactable) {
var values = property.value;
var splitAt = -1;
for (var i = 0, l = values.length; i < l; i++) {
if (values[i][0] == '/') {
splitAt = i;
break;
}
}
if (splitAt == -1)
return fourValues(property, compactable);
var target = _wrapDefault(property.name, property, compactable);
target.value = values.slice(0, splitAt);
target.components = fourValues(target, compactable);
var remainder = _wrapDefault(property.name, property, compactable);
remainder.value = values.slice(splitAt + 1);
remainder.components = fourValues(remainder, compactable);
for (var j = 0; j < 4; j++) {
target.components[j].multiplex = true;
target.components[j].value = target.components[j].value.concat([['/']]).concat(remainder.components[j].value);
}
return target.components;
}
function fourValues(property, compactable) {
var componentNames = compactable[property.name].components;
var components = [];
var value = property.value;
if (value.length < 1)
return [];
if (value.length < 2)
value[1] = value[0].slice(0);
if (value.length < 3)
value[2] = value[0].slice(0);
if (value.length < 4)
value[3] = value[1].slice(0);
for (var i = componentNames.length - 1; i >= 0; i--) {
var component = wrapSingle([[componentNames[i], property.important]]);
component.value = [value[i]];
components.unshift(component);
}
return components;
}
function multiplex(splitWith) {
return function (property, compactable, validator) {
var splitsAt = [];
var values = property.value;
var i, j, l, m;
// find split commas
for (i = 0, l = values.length; i < l; i++) {
if (values[i][0] == ',')
splitsAt.push(i);
}
if (splitsAt.length === 0)
return splitWith(property, compactable, validator);
var splitComponents = [];
// split over commas, and into components
for (i = 0, l = splitsAt.length; i <= l; i++) {
var from = i === 0 ? 0 : splitsAt[i - 1] + 1;
var to = i < l ? splitsAt[i] : values.length;
var _property = _wrapDefault(property.name, property, compactable);
_property.value = values.slice(from, to);
splitComponents.push(splitWith(_property, compactable, validator));
}
var components = splitComponents[0];
// group component values from each split
for (i = 0, l = components.length; i < l; i++) {
components[i].multiplex = true;
for (j = 1, m = splitComponents.length; j < m; j++) {
components[i].value.push([MULTIPLEX_SEPARATOR]);
Array.prototype.push.apply(components[i].value, splitComponents[j][i].value);
}
}
return components;
};
}
function listStyle(property, compactable, validator) {
var type = _wrapDefault('list-style-type', property, compactable);
var position = _wrapDefault('list-style-position', property, compactable);
var image = _wrapDefault('list-style-image', property, compactable);
var components = [type, position, image];
if (property.value.length == 1 && property.value[0][0] == 'inherit') {
type.value = position.value = image.value = [property.value[0]];
return components;
}
var values = property.value.slice(0);
var total = values.length;
var index = 0;
// `image` first...
for (index = 0, total = values.length; index < total; index++) {
if (validator.isValidUrl(values[index][0]) || values[index][0] == '0') {
image.value = [values[index]];
values.splice(index, 1);
break;
}
}
// ... then `type`...
for (index = 0, total = values.length; index < total; index++) {
if (validator.isValidListStyleType(values[index][0])) {
type.value = [values[index]];
values.splice(index, 1);
break;
}
}
// ... and what's left is a `position`
if (values.length > 0 && validator.isValidListStylePosition(values[0][0]))
position.value = [values[0]];
return components;
}
function widthStyleColor(property, compactable, validator) {
var descriptor = compactable[property.name];
var components = [
_wrapDefault(descriptor.components[0], property, compactable),
_wrapDefault(descriptor.components[1], property, compactable),
_wrapDefault(descriptor.components[2], property, compactable)
];
var color, style, width;
for (var i = 0; i < 3; i++) {
var component = components[i];
if (component.name.indexOf('color') > 0)
color = component;
else if (component.name.indexOf('style') > 0)
style = component;
else
width = component;
}
if ((property.value.length == 1 && property.value[0][0] == 'inherit') ||
(property.value.length == 3 && property.value[0][0] == 'inherit' && property.value[1][0] == 'inherit' && property.value[2][0] == 'inherit')) {
color.value = style.value = width.value = [property.value[0]];
return components;
}
var values = property.value.slice(0);
var match, matches;
// NOTE: usually users don't follow the required order of parts in this shorthand,
// so we'll try to parse it caring as little about order as possible
if (values.length > 0) {
matches = values.filter(_widthFilter(validator));
match = matches.length > 1 && (matches[0][0] == 'none' || matches[0][0] == 'auto') ? matches[1] : matches[0];
if (match) {
width.value = [match];
values.splice(values.indexOf(match), 1);
}
}
if (values.length > 0) {
match = values.filter(_styleFilter(validator))[0];
if (match) {
style.value = [match];
values.splice(values.indexOf(match), 1);
}
}
if (values.length > 0) {
match = values.filter(_colorFilter(validator))[0];
if (match) {
color.value = [match];
values.splice(values.indexOf(match), 1);
}
}
return components;
}
module.exports = {
background: background,
border: widthStyleColor,
borderRadius: borderRadius,
fourValues: fourValues,
listStyle: listStyle,
multiplex: multiplex,
outline: widthStyleColor
};
@@ -0,0 +1,120 @@
// Functions that decide what value can override what.
// The main purpose is to disallow removing CSS fallbacks.
// A separate implementation is needed for every different kind of CSS property.
// -----
// The generic idea is that properties that have wider browser support are 'more understandable'
// than others and that 'less understandable' values can't override more understandable ones.
// Use when two tokens of the same property can always be merged
function always() {
return true;
}
function backgroundImage(property1, property2, validator) {
// The idea here is that 'more understandable' values override 'less understandable' values, but not vice versa
// Understandability: (none | url | inherit) > (same function) > (same value)
// (none | url)
var image1 = property1.value[0][0];
var image2 = property2.value[0][0];
if (image2 == 'none' || image2 == 'inherit' || validator.isValidUrl(image2))
return true;
if (image1 == 'none' || image1 == 'inherit' || validator.isValidUrl(image1))
return false;
// Functions with the same name can override each other; same values can override each other
return sameFunctionOrValue(property1, property2, validator);
}
function border(property1, property2, validator) {
return color(property1.components[2], property2.components[2], validator);
}
// Use for color properties (color, background-color, border-color, etc.)
function color(property1, property2, validator) {
// The idea here is that 'more understandable' values override 'less understandable' values, but not vice versa
// Understandability: (hex | named) > (rgba | hsla) > (same function name) > anything else
// NOTE: at this point rgb and hsl are replaced by hex values by clean-css
var color1 = property1.value[0][0];
var color2 = property2.value[0][0];
// (hex | named)
if (validator.isValidNamedColor(color2) || validator.isValidHexColor(color2))
return true;
if (validator.isValidNamedColor(color1) || validator.isValidHexColor(color1))
return false;
// (rgba|hsla)
if (validator.isValidRgbaColor(color2) || validator.isValidHslaColor(color2))
return true;
if (validator.isValidRgbaColor(color1) || validator.isValidHslaColor(color1))
return false;
// Functions with the same name can override each other; same values can override each other
return sameFunctionOrValue(property1, property2, validator);
}
function twoOptionalFunctions(property1, property2, validator) {
var value1 = property1.value[0][0];
var value2 = property2.value[0][0];
return !(validator.isValidFunction(value1) ^ validator.isValidFunction(value2));
}
function sameValue(property1, property2) {
var value1 = property1.value[0][0];
var value2 = property2.value[0][0];
return value1 === value2;
}
function sameFunctionOrValue(property1, property2, validator) {
var value1 = property1.value[0][0];
var value2 = property2.value[0][0];
// Functions with the same name can override each other
if (validator.areSameFunction(value1, value2))
return true;
return value1 === value2;
}
// Use for properties containing CSS units (margin-top, padding-left, etc.)
function unit(property1, property2, validator) {
// The idea here is that 'more understandable' values override 'less understandable' values, but not vice versa
// Understandability: (unit without functions) > (same functions | standard functions) > anything else
// NOTE: there is no point in having different vendor-specific functions override each other or standard functions,
// or having standard functions override vendor-specific functions, but standard functions can override each other
// NOTE: vendor-specific property values are not taken into consideration here at the moment
var value1 = property1.value[0][0];
var value2 = property2.value[0][0];
if (validator.isValidAndCompatibleUnitWithoutFunction(value1) && !validator.isValidAndCompatibleUnitWithoutFunction(value2))
return false;
if (validator.isValidUnitWithoutFunction(value2))
return true;
if (validator.isValidUnitWithoutFunction(value1))
return false;
// Standard non-vendor-prefixed functions can override each other
if (validator.isValidFunctionWithoutVendorPrefix(value2) && validator.isValidFunctionWithoutVendorPrefix(value1)) {
return true;
}
// Functions with the same name can override each other; same values can override each other
return sameFunctionOrValue(property1, property2, validator);
}
module.exports = {
always: always,
backgroundImage: backgroundImage,
border: border,
color: color,
sameValue: sameValue,
sameFunctionOrValue: sameFunctionOrValue,
twoOptionalFunctions: twoOptionalFunctions,
unit: unit
};
@@ -0,0 +1,26 @@
var wrapSingle = require('./wrap-for-optimizing').single;
function deep(property) {
var cloned = shallow(property);
for (var i = property.components.length - 1; i >= 0; i--) {
var component = shallow(property.components[i]);
component.value = property.components[i].value.slice(0);
cloned.components.unshift(component);
}
cloned.dirty = true;
cloned.value = property.value.slice(0);
return cloned;
}
function shallow(property) {
var cloned = wrapSingle([[property.name, property.important, property.hack]]);
cloned.unused = false;
return cloned;
}
module.exports = {
deep: deep,
shallow: shallow
};
@@ -0,0 +1,285 @@
// Contains the interpretation of CSS properties, as used by the property optimizer
var breakUp = require('./break-up');
var canOverride = require('./can-override');
var restore = require('./restore');
// Properties to process
// Extend this object in order to add support for more properties in the optimizer.
//
// Each key in this object represents a CSS property and should be an object.
// Such an object contains properties that describe how the represented CSS property should be handled.
// Possible options:
//
// * components: array (Only specify for shorthand properties.)
// Contains the names of the granular properties this shorthand compacts.
//
// * canOverride: function (Default is canOverride.sameValue - meaning that they'll only be merged if they have the same value.)
// Returns whether two tokens of this property can be merged with each other.
// This property has no meaning for shorthands.
//
// * defaultValue: string
// Specifies the default value of the property according to the CSS standard.
// For shorthand, this is used when every component is set to its default value, therefore it should be the shortest possible default value of all the components.
//
// * shortestValue: string
// Specifies the shortest possible value the property can possibly have.
// (Falls back to defaultValue if unspecified.)
//
// * breakUp: function (Only specify for shorthand properties.)
// Breaks the shorthand up to its components.
//
// * restore: function (Only specify for shorthand properties.)
// Puts the shorthand together from its components.
//
var compactable = {
'color': {
canOverride: canOverride.color,
defaultValue: 'transparent',
shortestValue: 'red'
},
'background': {
components: [
'background-image',
'background-position',
'background-size',
'background-repeat',
'background-attachment',
'background-origin',
'background-clip',
'background-color'
],
breakUp: breakUp.multiplex(breakUp.background),
defaultValue: '0 0',
restore: restore.multiplex(restore.background),
shortestValue: '0',
shorthand: true
},
'background-clip': {
canOverride: canOverride.always,
defaultValue: 'border-box',
shortestValue: 'border-box'
},
'background-color': {
canOverride: canOverride.color,
defaultValue: 'transparent',
multiplexLastOnly: true,
nonMergeableValue: 'none',
shortestValue: 'red'
},
'background-image': {
canOverride: canOverride.backgroundImage,
defaultValue: 'none'
},
'background-origin': {
canOverride: canOverride.always,
defaultValue: 'padding-box',
shortestValue: 'border-box'
},
'background-repeat': {
canOverride: canOverride.always,
defaultValue: ['repeat'],
doubleValues: true
},
'background-position': {
canOverride: canOverride.always,
defaultValue: ['0', '0'],
doubleValues: true,
shortestValue: '0'
},
'background-size': {
canOverride: canOverride.always,
defaultValue: ['auto'],
doubleValues: true,
shortestValue: '0 0'
},
'background-attachment': {
canOverride: canOverride.always,
defaultValue: 'scroll'
},
'border': {
breakUp: breakUp.border,
canOverride: canOverride.border,
components: [
'border-width',
'border-style',
'border-color'
],
defaultValue: 'none',
restore: restore.withoutDefaults,
shorthand: true
},
'border-color': {
canOverride: canOverride.color,
defaultValue: 'none',
shorthand: true
},
'border-style': {
canOverride: canOverride.always,
defaultValue: 'none',
shorthand: true
},
'border-width': {
canOverride: canOverride.unit,
defaultValue: 'medium',
shortestValue: '0',
shorthand: true
},
'list-style': {
components: [
'list-style-type',
'list-style-position',
'list-style-image'
],
canOverride: canOverride.always,
breakUp: breakUp.listStyle,
restore: restore.withoutDefaults,
defaultValue: 'outside', // can't use 'disc' because that'd override default 'decimal' for <ol>
shortestValue: 'none',
shorthand: true
},
'list-style-type' : {
canOverride: canOverride.always,
defaultValue: '__hack',
// NOTE: we can't tell the real default value here, it's 'disc' for <ul> and 'decimal' for <ol>
// -- this is a hack, but it doesn't matter because this value will be either overridden or it will disappear at the final step anyway
shortestValue: 'none'
},
'list-style-position' : {
canOverride: canOverride.always,
defaultValue: 'outside',
shortestValue: 'inside'
},
'list-style-image' : {
canOverride: canOverride.always,
defaultValue: 'none'
},
'outline': {
components: [
'outline-color',
'outline-style',
'outline-width'
],
breakUp: breakUp.outline,
restore: restore.withoutDefaults,
defaultValue: '0',
shorthand: true
},
'outline-color': {
canOverride: canOverride.color,
defaultValue: 'invert',
shortestValue: 'red'
},
'outline-style': {
canOverride: canOverride.always,
defaultValue: 'none'
},
'outline-width': {
canOverride: canOverride.unit,
defaultValue: 'medium',
shortestValue: '0'
},
'-moz-transform': {
canOverride: canOverride.sameFunctionOrValue
},
'-ms-transform': {
canOverride: canOverride.sameFunctionOrValue
},
'-webkit-transform': {
canOverride: canOverride.sameFunctionOrValue
},
'transform': {
canOverride: canOverride.sameFunctionOrValue
}
};
var addFourValueShorthand = function (prop, components, options) {
options = options || {};
compactable[prop] = {
canOverride: options.canOverride,
components: components,
breakUp: options.breakUp || breakUp.fourValues,
defaultValue: options.defaultValue || '0',
restore: options.restore || restore.fourValues,
shortestValue: options.shortestValue,
shorthand: true
};
for (var i = 0; i < components.length; i++) {
compactable[components[i]] = {
breakUp: options.breakUp || breakUp.fourValues,
canOverride: options.canOverride || canOverride.unit,
defaultValue: options.defaultValue || '0',
shortestValue: options.shortestValue
};
}
};
['', '-moz-', '-o-', '-webkit-'].forEach(function (prefix) {
addFourValueShorthand(prefix + 'border-radius', [
prefix + 'border-top-left-radius',
prefix + 'border-top-right-radius',
prefix + 'border-bottom-right-radius',
prefix + 'border-bottom-left-radius'
], {
breakUp: breakUp.borderRadius,
restore: restore.borderRadius
});
});
addFourValueShorthand('border-color', [
'border-top-color',
'border-right-color',
'border-bottom-color',
'border-left-color'
], {
breakUp: breakUp.fourValues,
canOverride: canOverride.color,
defaultValue: 'none',
shortestValue: 'red'
});
addFourValueShorthand('border-style', [
'border-top-style',
'border-right-style',
'border-bottom-style',
'border-left-style'
], {
breakUp: breakUp.fourValues,
canOverride: canOverride.always,
defaultValue: 'none'
});
addFourValueShorthand('border-width', [
'border-top-width',
'border-right-width',
'border-bottom-width',
'border-left-width'
], {
defaultValue: 'medium',
shortestValue: '0'
});
addFourValueShorthand('padding', [
'padding-top',
'padding-right',
'padding-bottom',
'padding-left'
]);
addFourValueShorthand('margin', [
'margin-top',
'margin-right',
'margin-bottom',
'margin-left'
]);
// Adds `componentOf` field to all longhands
for (var property in compactable) {
if (compactable[property].shorthand) {
for (var i = 0, l = compactable[property].components.length; i < l; i++) {
compactable[compactable[property].components[i]].componentOf = property;
}
}
}
module.exports = compactable;
@@ -0,0 +1,24 @@
var shallowClone = require('./clone').shallow;
var MULTIPLEX_SEPARATOR = ',';
function everyCombination(fn, left, right, validator) {
var _left = shallowClone(left);
var _right = shallowClone(right);
for (var i = 0, l = left.value.length; i < l; i++) {
for (var j = 0, m = right.value.length; j < m; j++) {
if (left.value[i][0] == MULTIPLEX_SEPARATOR || right.value[j][0] == MULTIPLEX_SEPARATOR)
continue;
_left.value = [left.value[i]];
_right.value = [right.value[j]];
if (!fn(_left, _right, validator))
return false;
}
}
return true;
}
module.exports = everyCombination;
@@ -0,0 +1,10 @@
function hasInherit(property) {
for (var i = property.value.length - 1; i >= 0; i--) {
if (property.value[i][0] == 'inherit')
return true;
}
return false;
}
module.exports = hasInherit;
@@ -0,0 +1,212 @@
var compactable = require('./compactable');
var wrapForOptimizing = require('./wrap-for-optimizing').all;
var populateComponents = require('./populate-components');
var compactOverrides = require('./override-compactor');
var compactShorthands = require('./shorthand-compactor');
var removeUnused = require('./remove-unused');
var restoreFromOptimizing = require('./restore-from-optimizing');
var stringifyProperty = require('../stringifier/one-time').property;
var shorthands = {
'animation-delay': ['animation'],
'animation-direction': ['animation'],
'animation-duration': ['animation'],
'animation-fill-mode': ['animation'],
'animation-iteration-count': ['animation'],
'animation-name': ['animation'],
'animation-play-state': ['animation'],
'animation-timing-function': ['animation'],
'-moz-animation-delay': ['-moz-animation'],
'-moz-animation-direction': ['-moz-animation'],
'-moz-animation-duration': ['-moz-animation'],
'-moz-animation-fill-mode': ['-moz-animation'],
'-moz-animation-iteration-count': ['-moz-animation'],
'-moz-animation-name': ['-moz-animation'],
'-moz-animation-play-state': ['-moz-animation'],
'-moz-animation-timing-function': ['-moz-animation'],
'-o-animation-delay': ['-o-animation'],
'-o-animation-direction': ['-o-animation'],
'-o-animation-duration': ['-o-animation'],
'-o-animation-fill-mode': ['-o-animation'],
'-o-animation-iteration-count': ['-o-animation'],
'-o-animation-name': ['-o-animation'],
'-o-animation-play-state': ['-o-animation'],
'-o-animation-timing-function': ['-o-animation'],
'-webkit-animation-delay': ['-webkit-animation'],
'-webkit-animation-direction': ['-webkit-animation'],
'-webkit-animation-duration': ['-webkit-animation'],
'-webkit-animation-fill-mode': ['-webkit-animation'],
'-webkit-animation-iteration-count': ['-webkit-animation'],
'-webkit-animation-name': ['-webkit-animation'],
'-webkit-animation-play-state': ['-webkit-animation'],
'-webkit-animation-timing-function': ['-webkit-animation'],
'border-color': ['border'],
'border-style': ['border'],
'border-width': ['border'],
'border-bottom': ['border'],
'border-bottom-color': ['border-bottom', 'border-color', 'border'],
'border-bottom-style': ['border-bottom', 'border-style', 'border'],
'border-bottom-width': ['border-bottom', 'border-width', 'border'],
'border-left': ['border'],
'border-left-color': ['border-left', 'border-color', 'border'],
'border-left-style': ['border-left', 'border-style', 'border'],
'border-left-width': ['border-left', 'border-width', 'border'],
'border-right': ['border'],
'border-right-color': ['border-right', 'border-color', 'border'],
'border-right-style': ['border-right', 'border-style', 'border'],
'border-right-width': ['border-right', 'border-width', 'border'],
'border-top': ['border'],
'border-top-color': ['border-top', 'border-color', 'border'],
'border-top-style': ['border-top', 'border-style', 'border'],
'border-top-width': ['border-top', 'border-width', 'border'],
'font-family': ['font'],
'font-size': ['font'],
'font-style': ['font'],
'font-variant': ['font'],
'font-weight': ['font'],
'transition-delay': ['transition'],
'transition-duration': ['transition'],
'transition-property': ['transition'],
'transition-timing-function': ['transition'],
'-moz-transition-delay': ['-moz-transition'],
'-moz-transition-duration': ['-moz-transition'],
'-moz-transition-property': ['-moz-transition'],
'-moz-transition-timing-function': ['-moz-transition'],
'-o-transition-delay': ['-o-transition'],
'-o-transition-duration': ['-o-transition'],
'-o-transition-property': ['-o-transition'],
'-o-transition-timing-function': ['-o-transition'],
'-webkit-transition-delay': ['-webkit-transition'],
'-webkit-transition-duration': ['-webkit-transition'],
'-webkit-transition-property': ['-webkit-transition'],
'-webkit-transition-timing-function': ['-webkit-transition']
};
function _optimize(properties, mergeAdjacent, aggressiveMerging, validator) {
var overrideMapping = {};
var lastName = null;
var lastProperty;
var j;
function mergeablePosition(position) {
if (mergeAdjacent === false || mergeAdjacent === true)
return mergeAdjacent;
return mergeAdjacent.indexOf(position) > -1;
}
function sameValue(position) {
var left = properties[position - 1];
var right = properties[position];
return stringifyProperty(left.all, left.position) == stringifyProperty(right.all, right.position);
}
propertyLoop:
for (var position = 0, total = properties.length; position < total; position++) {
var property = properties[position];
var _name = (property.name == '-ms-filter' || property.name == 'filter') ?
(lastName == 'background' || lastName == 'background-image' ? lastName : property.name) :
property.name;
var isImportant = property.important;
var isHack = property.hack;
if (property.unused)
continue;
if (position > 0 && lastProperty && _name == lastName && isImportant == lastProperty.important && isHack == lastProperty.hack && sameValue(position) && !lastProperty.unused) {
property.unused = true;
continue;
}
// comment is necessary - we assume that if two properties are one after another
// then it is intentional way of redefining property which may not be widely supported
// e.g. a{display:inline-block;display:-moz-inline-box}
// however if `mergeablePosition` yields true then the rule does not apply
// (e.g merging two adjacent selectors: `a{display:block}a{display:block}`)
if (_name in overrideMapping && (aggressiveMerging && _name != lastName || mergeablePosition(position))) {
var toOverridePositions = overrideMapping[_name];
var canOverride = compactable[_name] && compactable[_name].canOverride;
var anyRemoved = false;
for (j = toOverridePositions.length - 1; j >= 0; j--) {
var toRemove = properties[toOverridePositions[j]];
var longhandToShorthand = toRemove.name != _name;
var wasImportant = toRemove.important;
var wasHack = toRemove.hack;
if (toRemove.unused)
continue;
if (longhandToShorthand && wasImportant)
continue;
if (!wasImportant && (wasHack && !isHack || !wasHack && isHack))
continue;
if (wasImportant && (isHack == 'star' || isHack == 'underscore'))
continue;
if (!wasHack && !isHack && !longhandToShorthand && canOverride && !canOverride(toRemove, property, validator))
continue;
if (wasImportant && !isImportant || wasImportant && isHack) {
property.unused = true;
lastProperty = property;
continue propertyLoop;
} else {
anyRemoved = true;
toRemove.unused = true;
}
}
if (anyRemoved) {
position = -1;
lastProperty = null;
lastName = null;
overrideMapping = {};
continue;
}
} else {
overrideMapping[_name] = overrideMapping[_name] || [];
overrideMapping[_name].push(position);
// TODO: to be removed with
// certain shorthand (see values of `shorthands`) should trigger removal of
// longhand properties (see keys of `shorthands`)
var _shorthands = shorthands[_name];
if (_shorthands) {
for (j = _shorthands.length - 1; j >= 0; j--) {
var shorthand = _shorthands[j];
overrideMapping[shorthand] = overrideMapping[shorthand] || [];
overrideMapping[shorthand].push(position);
}
}
}
lastName = _name;
lastProperty = property;
}
}
function optimize(selector, properties, mergeAdjacent, withCompacting, options, validator) {
var _properties = wrapForOptimizing(properties);
populateComponents(_properties, validator);
_optimize(_properties, mergeAdjacent, options.aggressiveMerging, validator);
for (var i = 0, l = _properties.length; i < l; i++) {
var _property = _properties[i];
if (_property.variable && _property.block)
optimize(selector, _property.value[0], mergeAdjacent, withCompacting, options, validator);
}
if (withCompacting && options.shorthandCompacting) {
compactOverrides(_properties, options.compatibility, validator);
compactShorthands(_properties, options.sourceMap, validator);
}
restoreFromOptimizing(_properties);
removeUnused(_properties);
}
module.exports = optimize;
@@ -0,0 +1,376 @@
var canOverride = require('./can-override');
var compactable = require('./compactable');
var deepClone = require('./clone').deep;
var shallowClone = require('./clone').shallow;
var hasInherit = require('./has-inherit');
var restoreFromOptimizing = require('./restore-from-optimizing');
var everyCombination = require('./every-combination');
var sameVendorPrefixesIn = require('./vendor-prefixes').same;
var stringifyProperty = require('../stringifier/one-time').property;
var MULTIPLEX_SEPARATOR = ',';
// Used when searching for a component that matches property
function nameMatchFilter(to) {
return function (property) {
return to.name === property.name;
};
}
function wouldBreakCompatibility(property, validator) {
for (var i = 0; i < property.components.length; i++) {
var component = property.components[i];
var descriptor = compactable[component.name];
var canOverride = descriptor && descriptor.canOverride || canOverride.sameValue;
var _component = shallowClone(component);
_component.value = [[descriptor.defaultValue]];
if (!canOverride(_component, component, validator))
return true;
}
return false;
}
function isComponentOf(shorthand, longhand) {
return compactable[shorthand.name].components.indexOf(longhand.name) > -1;
}
function overrideIntoMultiplex(property, by) {
by.unused = true;
turnIntoMultiplex(by, multiplexSize(property));
property.value = by.value;
}
function overrideByMultiplex(property, by) {
by.unused = true;
property.multiplex = true;
property.value = by.value;
}
function overrideSimple(property, by) {
by.unused = true;
property.value = by.value;
}
function override(property, by) {
if (by.multiplex)
overrideByMultiplex(property, by);
else if (property.multiplex)
overrideIntoMultiplex(property, by);
else
overrideSimple(property, by);
}
function overrideShorthand(property, by) {
by.unused = true;
for (var i = 0, l = property.components.length; i < l; i++) {
override(property.components[i], by.components[i], property.multiplex);
}
}
function turnIntoMultiplex(property, size) {
property.multiplex = true;
for (var i = 0, l = property.components.length; i < l; i++) {
var component = property.components[i];
if (component.multiplex)
continue;
var value = component.value.slice(0);
for (var j = 1; j < size; j++) {
component.value.push([MULTIPLEX_SEPARATOR]);
Array.prototype.push.apply(component.value, value);
}
}
}
function multiplexSize(component) {
var size = 0;
for (var i = 0, l = component.value.length; i < l; i++) {
if (component.value[i][0] == MULTIPLEX_SEPARATOR)
size++;
}
return size + 1;
}
function lengthOf(property) {
var fakeAsArray = [[property.name]].concat(property.value);
return stringifyProperty([fakeAsArray], 0).length;
}
function moreSameShorthands(properties, startAt, name) {
// Since we run the main loop in `compactOverrides` backwards, at this point some
// properties may not be marked as unused.
// We should consider reverting the order if possible
var count = 0;
for (var i = startAt; i >= 0; i--) {
if (properties[i].name == name && !properties[i].unused)
count++;
if (count > 1)
break;
}
return count > 1;
}
function mergingIntoFunction(left, right, validator) {
for (var i = 0, l = left.components.length; i < l; i++) {
if (anyValue(validator.isValidFunction, left.components[i]))
return true;
}
return false;
}
function anyValue(fn, property) {
for (var i = 0, l = property.value.length; i < l; i++) {
if (property.value[i][0] == MULTIPLEX_SEPARATOR)
continue;
if (fn(property.value[i][0]))
return true;
}
return false;
}
function wouldResultInLongerValue(left, right) {
if (!left.multiplex && !right.multiplex || left.multiplex && right.multiplex)
return false;
var multiplex = left.multiplex ? left : right;
var simple = left.multiplex ? right : left;
var component;
var multiplexClone = deepClone(multiplex);
restoreFromOptimizing([multiplexClone]);
var simpleClone = deepClone(simple);
restoreFromOptimizing([simpleClone]);
var lengthBefore = lengthOf(multiplexClone) + 1 + lengthOf(simpleClone);
if (left.multiplex) {
component = multiplexClone.components.filter(nameMatchFilter(simpleClone))[0];
overrideIntoMultiplex(component, simpleClone);
} else {
component = simpleClone.components.filter(nameMatchFilter(multiplexClone))[0];
turnIntoMultiplex(simpleClone, multiplexSize(multiplexClone));
overrideByMultiplex(component, multiplexClone);
}
restoreFromOptimizing([simpleClone]);
var lengthAfter = lengthOf(simpleClone);
return lengthBefore < lengthAfter;
}
function isCompactable(property) {
return property.name in compactable;
}
function noneOverrideHack(left, right) {
return !left.multiplex &&
(left.name == 'background' || left.name == 'background-image') &&
right.multiplex &&
(right.name == 'background' || right.name == 'background-image') &&
anyLayerIsNone(right.value);
}
function anyLayerIsNone(values) {
var layers = intoLayers(values);
for (var i = 0, l = layers.length; i < l; i++) {
if (layers[i].length == 1 && layers[i][0][0] == 'none')
return true;
}
return false;
}
function intoLayers(values) {
var layers = [];
for (var i = 0, layer = [], l = values.length; i < l; i++) {
var value = values[i];
if (value[0] == MULTIPLEX_SEPARATOR) {
layers.push(layer);
layer = [];
} else {
layer.push(value);
}
}
layers.push(layer);
return layers;
}
function compactOverrides(properties, compatibility, validator) {
var mayOverride, right, left, component;
var i, j, k;
propertyLoop:
for (i = properties.length - 1; i >= 0; i--) {
right = properties[i];
if (!isCompactable(right))
continue;
if (right.variable)
continue;
mayOverride = compactable[right.name].canOverride || canOverride.sameValue;
for (j = i - 1; j >= 0; j--) {
left = properties[j];
if (!isCompactable(left))
continue;
if (left.variable)
continue;
if (left.unused || right.unused)
continue;
if (left.hack && !right.hack || !left.hack && right.hack)
continue;
if (hasInherit(right))
continue;
if (noneOverrideHack(left, right))
continue;
if (!left.shorthand && right.shorthand && isComponentOf(right, left)) {
// maybe `left` can be overridden by `right` which is a shorthand?
if (!right.important && left.important)
continue;
if (!sameVendorPrefixesIn([left], right.components))
continue;
component = right.components.filter(nameMatchFilter(left))[0];
mayOverride = (compactable[left.name] && compactable[left.name].canOverride) || canOverride.sameValue;
if (everyCombination(mayOverride, left, component, validator)) {
left.unused = true;
}
} else if (left.shorthand && !right.shorthand && isComponentOf(left, right)) {
// maybe `right` can be pulled into `left` which is a shorthand?
if (right.important && !left.important)
continue;
// Pending more clever algorithm in #527
if (moreSameShorthands(properties, i - 1, left.name))
continue;
if (mergingIntoFunction(left, right, validator))
continue;
component = left.components.filter(nameMatchFilter(right))[0];
if (everyCombination(mayOverride, component, right, validator)) {
var disabledBackgroundMerging =
!compatibility.properties.backgroundClipMerging && component.name.indexOf('background-clip') > -1 ||
!compatibility.properties.backgroundOriginMerging && component.name.indexOf('background-origin') > -1 ||
!compatibility.properties.backgroundSizeMerging && component.name.indexOf('background-size') > -1;
var nonMergeableValue = compactable[right.name].nonMergeableValue === right.value[0][0];
if (disabledBackgroundMerging || nonMergeableValue)
continue;
if (!compatibility.properties.merging && wouldBreakCompatibility(left, validator))
continue;
if (component.value[0][0] != right.value[0][0] && (hasInherit(left) || hasInherit(right)))
continue;
if (wouldResultInLongerValue(left, right))
continue;
if (!left.multiplex && right.multiplex)
turnIntoMultiplex(left, multiplexSize(right));
override(component, right);
left.dirty = true;
}
} else if (left.shorthand && right.shorthand && left.name == right.name) {
// merge if all components can be merged
if (!left.multiplex && right.multiplex)
continue;
if (!right.important && left.important) {
right.unused = true;
continue propertyLoop;
}
if (right.important && !left.important) {
left.unused = true;
continue;
}
for (k = left.components.length - 1; k >= 0; k--) {
var leftComponent = left.components[k];
var rightComponent = right.components[k];
mayOverride = compactable[leftComponent.name].canOverride || canOverride.sameValue;
if (!everyCombination(mayOverride, leftComponent, rightComponent, validator))
continue propertyLoop;
if (!everyCombination(canOverride.twoOptionalFunctions, leftComponent, rightComponent, validator) && validator.isValidFunction(rightComponent))
continue propertyLoop;
}
overrideShorthand(left, right);
left.dirty = true;
} else if (left.shorthand && right.shorthand && isComponentOf(left, right)) {
// border is a shorthand but any of its components is a shorthand too
if (!left.important && right.important)
continue;
component = left.components.filter(nameMatchFilter(right))[0];
mayOverride = compactable[right.name].canOverride || canOverride.sameValue;
if (!everyCombination(mayOverride, component, right, validator))
continue;
if (left.important && !right.important) {
right.unused = true;
continue;
}
var rightRestored = compactable[right.name].restore(right, compactable);
if (rightRestored.length > 1)
continue;
component = left.components.filter(nameMatchFilter(right))[0];
override(component, right);
right.dirty = true;
} else if (left.name == right.name) {
// two non-shorthands should be merged based on understandability
if (left.important && !right.important) {
right.unused = true;
continue;
}
mayOverride = compactable[right.name].canOverride || canOverride.sameValue;
if (!everyCombination(mayOverride, left, right, validator))
continue;
left.unused = true;
}
}
}
}
module.exports = compactOverrides;
@@ -0,0 +1,21 @@
var compactable = require('./compactable');
function populateComponents(properties, validator) {
for (var i = properties.length - 1; i >= 0; i--) {
var property = properties[i];
var descriptor = compactable[property.name];
if (descriptor && descriptor.shorthand) {
property.shorthand = true;
property.dirty = true;
property.components = descriptor.breakUp(property, compactable, validator);
if (property.components.length > 0)
property.multiplex = property.components[0].multiplex;
else
property.unused = true;
}
}
}
module.exports = populateComponents;
@@ -0,0 +1,10 @@
function removeUnused(properties) {
for (var i = properties.length - 1; i >= 0; i--) {
var property = properties[i];
if (property.unused)
property.all.splice(property.position, 1);
}
}
module.exports = removeUnused;
@@ -0,0 +1,60 @@
var compactable = require('./compactable');
var BACKSLASH_HACK = '\\9';
var IMPORTANT_TOKEN = '!important';
var STAR_HACK = '*';
var UNDERSCORE_HACK = '_';
var BANG_HACK = '!ie';
function restoreImportant(property) {
property.value[property.value.length - 1][0] += IMPORTANT_TOKEN;
}
function restoreHack(property) {
if (property.hack == 'underscore')
property.name = UNDERSCORE_HACK + property.name;
else if (property.hack == 'star')
property.name = STAR_HACK + property.name;
else if (property.hack == 'backslash')
property.value[property.value.length - 1][0] += BACKSLASH_HACK;
else if (property.hack == 'bang')
property.value[property.value.length - 1][0] += ' ' + BANG_HACK;
}
function restoreFromOptimizing(properties, simpleMode) {
for (var i = properties.length - 1; i >= 0; i--) {
var property = properties[i];
var descriptor = compactable[property.name];
var restored;
if (property.unused)
continue;
if (!property.dirty && !property.important && !property.hack)
continue;
if (!simpleMode && descriptor && descriptor.shorthand) {
restored = descriptor.restore(property, compactable);
property.value = restored;
} else {
restored = property.value;
}
if (property.important)
restoreImportant(property);
if (property.hack)
restoreHack(property);
if (!('all' in property))
continue;
var current = property.all[property.position];
current[0][0] = property.name;
current.splice(1, current.length - 1);
Array.prototype.push.apply(current, restored);
}
}
module.exports = restoreFromOptimizing;
@@ -0,0 +1,229 @@
var shallowClone = require('./clone').shallow;
var MULTIPLEX_SEPARATOR = ',';
var SIZE_POSITION_SEPARATOR = '/';
function isInheritOnly(values) {
for (var i = 0, l = values.length; i < l; i++) {
var value = values[i][0];
if (value != 'inherit' && value != MULTIPLEX_SEPARATOR && value != SIZE_POSITION_SEPARATOR)
return false;
}
return true;
}
function background(property, compactable, lastInMultiplex) {
var components = property.components;
var restored = [];
var needsOne, needsBoth;
function restoreValue(component) {
Array.prototype.unshift.apply(restored, component.value);
}
function isDefaultValue(component) {
var descriptor = compactable[component.name];
if (descriptor.doubleValues) {
if (descriptor.defaultValue.length == 1)
return component.value[0][0] == descriptor.defaultValue[0] && (component.value[1] ? component.value[1][0] == descriptor.defaultValue[0] : true);
else
return component.value[0][0] == descriptor.defaultValue[0] && (component.value[1] ? component.value[1][0] : component.value[0][0]) == descriptor.defaultValue[1];
} else {
return component.value[0][0] == descriptor.defaultValue;
}
}
for (var i = components.length - 1; i >= 0; i--) {
var component = components[i];
var isDefault = isDefaultValue(component);
if (component.name == 'background-clip') {
var originComponent = components[i - 1];
var isOriginDefault = isDefaultValue(originComponent);
needsOne = component.value[0][0] == originComponent.value[0][0];
needsBoth = !needsOne && (
(isOriginDefault && !isDefault) ||
(!isOriginDefault && !isDefault) ||
(!isOriginDefault && isDefault && component.value[0][0] != originComponent.value[0][0]));
if (needsOne) {
restoreValue(originComponent);
} else if (needsBoth) {
restoreValue(component);
restoreValue(originComponent);
}
i--;
} else if (component.name == 'background-size') {
var positionComponent = components[i - 1];
var isPositionDefault = isDefaultValue(positionComponent);
needsOne = !isPositionDefault && isDefault;
needsBoth = !needsOne &&
(isPositionDefault && !isDefault || !isPositionDefault && !isDefault);
if (needsOne) {
restoreValue(positionComponent);
} else if (needsBoth) {
restoreValue(component);
restored.unshift([SIZE_POSITION_SEPARATOR]);
restoreValue(positionComponent);
} else if (positionComponent.value.length == 1) {
restoreValue(positionComponent);
}
i--;
} else {
if (isDefault || compactable[component.name].multiplexLastOnly && !lastInMultiplex)
continue;
restoreValue(component);
}
}
if (restored.length === 0 && property.value.length == 1 && property.value[0][0] == '0')
restored.push(property.value[0]);
if (restored.length === 0)
restored.push([compactable[property.name].defaultValue]);
if (isInheritOnly(restored))
return [restored[0]];
return restored;
}
function borderRadius(property, compactable) {
if (property.multiplex) {
var horizontal = shallowClone(property);
var vertical = shallowClone(property);
for (var i = 0; i < 4; i++) {
var component = property.components[i];
var horizontalComponent = shallowClone(property);
horizontalComponent.value = [component.value[0]];
horizontal.components.push(horizontalComponent);
var verticalComponent = shallowClone(property);
verticalComponent.value = [component.value[2]];
vertical.components.push(verticalComponent);
}
var horizontalValues = fourValues(horizontal, compactable);
var verticalValues = fourValues(vertical, compactable);
if (horizontalValues.length == verticalValues.length &&
horizontalValues[0][0] == verticalValues[0][0] &&
(horizontalValues.length > 1 ? horizontalValues[1][0] == verticalValues[1][0] : true) &&
(horizontalValues.length > 2 ? horizontalValues[2][0] == verticalValues[2][0] : true) &&
(horizontalValues.length > 3 ? horizontalValues[3][0] == verticalValues[3][0] : true)) {
return horizontalValues;
} else {
return horizontalValues.concat([['/']]).concat(verticalValues);
}
} else {
return fourValues(property, compactable);
}
}
function fourValues(property) {
var components = property.components;
var value1 = components[0].value[0];
var value2 = components[1].value[0];
var value3 = components[2].value[0];
var value4 = components[3].value[0];
if (value1[0] == value2[0] && value1[0] == value3[0] && value1[0] == value4[0]) {
return [value1];
} else if (value1[0] == value3[0] && value2[0] == value4[0]) {
return [value1, value2];
} else if (value2[0] == value4[0]) {
return [value1, value2, value3];
} else {
return [value1, value2, value3, value4];
}
}
function multiplex(restoreWith) {
return function (property, compactable) {
if (!property.multiplex)
return restoreWith(property, compactable, true);
var multiplexSize = 0;
var restored = [];
var componentMultiplexSoFar = {};
var i, l;
// At this point we don't know what's the multiplex size, e.g. how many background layers are there
for (i = 0, l = property.components[0].value.length; i < l; i++) {
if (property.components[0].value[i][0] == MULTIPLEX_SEPARATOR)
multiplexSize++;
}
for (i = 0; i <= multiplexSize; i++) {
var _property = shallowClone(property);
// We split multiplex into parts and restore them one by one
for (var j = 0, m = property.components.length; j < m; j++) {
var componentToClone = property.components[j];
var _component = shallowClone(componentToClone);
_property.components.push(_component);
// The trick is some properties has more than one value, so we iterate over values looking for
// a multiplex separator - a comma
for (var k = componentMultiplexSoFar[_component.name] || 0, n = componentToClone.value.length; k < n; k++) {
if (componentToClone.value[k][0] == MULTIPLEX_SEPARATOR) {
componentMultiplexSoFar[_component.name] = k + 1;
break;
}
_component.value.push(componentToClone.value[k]);
}
}
// No we can restore shorthand value
var lastInMultiplex = i == multiplexSize;
var _restored = restoreWith(_property, compactable, lastInMultiplex);
Array.prototype.push.apply(restored, _restored);
if (i < multiplexSize)
restored.push([',']);
}
return restored;
};
}
function withoutDefaults(property, compactable) {
var components = property.components;
var restored = [];
for (var i = components.length - 1; i >= 0; i--) {
var component = components[i];
var descriptor = compactable[component.name];
if (component.value[0][0] != descriptor.defaultValue)
restored.unshift(component.value[0]);
}
if (restored.length === 0)
restored.push([compactable[property.name].defaultValue]);
if (isInheritOnly(restored))
return [restored[0]];
return restored;
}
module.exports = {
background: background,
borderRadius: borderRadius,
fourValues: fourValues,
multiplex: multiplex,
withoutDefaults: withoutDefaults
};
@@ -0,0 +1,134 @@
var compactable = require('./compactable');
var deepClone = require('./clone').deep;
var hasInherit = require('./has-inherit');
var populateComponents = require('./populate-components');
var wrapSingle = require('./wrap-for-optimizing').single;
var everyCombination = require('./every-combination');
function mixedImportance(components) {
var important;
for (var name in components) {
if (undefined !== important && components[name].important != important)
return true;
important = components[name].important;
}
return false;
}
function componentSourceMaps(components) {
var sourceMapping = [];
for (var name in components) {
var component = components[name];
var originalValue = component.all[component.position];
var mapping = originalValue[0][originalValue[0].length - 1];
if (Array.isArray(mapping))
Array.prototype.push.apply(sourceMapping, mapping);
}
return sourceMapping;
}
function replaceWithShorthand(properties, candidateComponents, name, sourceMaps, validator) {
var descriptor = compactable[name];
var newValuePlaceholder = [[name], [descriptor.defaultValue]];
var all;
var newProperty = wrapSingle(newValuePlaceholder);
newProperty.shorthand = true;
newProperty.dirty = true;
populateComponents([newProperty], validator);
for (var i = 0, l = descriptor.components.length; i < l; i++) {
var component = candidateComponents[descriptor.components[i]];
var canOverride = compactable[component.name].canOverride;
if (hasInherit(component))
return;
if (!everyCombination(canOverride, newProperty.components[i], component, validator))
return;
newProperty.components[i] = deepClone(component);
newProperty.important = component.important;
all = component.all;
}
for (var componentName in candidateComponents) {
candidateComponents[componentName].unused = true;
}
if (sourceMaps) {
var sourceMapping = componentSourceMaps(candidateComponents);
if (sourceMapping.length > 0)
newValuePlaceholder[0].push(sourceMapping);
}
newProperty.position = all.length;
newProperty.all = all;
newProperty.all.push(newValuePlaceholder);
properties.push(newProperty);
}
function invalidateOrCompact(properties, position, candidates, sourceMaps, validator) {
var property = properties[position];
for (var name in candidates) {
if (undefined !== property && name == property.name)
continue;
var descriptor = compactable[name];
var candidateComponents = candidates[name];
if (descriptor.components.length > Object.keys(candidateComponents).length) {
delete candidates[name];
continue;
}
if (mixedImportance(candidateComponents))
continue;
replaceWithShorthand(properties, candidateComponents, name, sourceMaps, validator);
}
}
function compactShortands(properties, sourceMaps, validator) {
var candidates = {};
if (properties.length < 3)
return;
for (var i = 0, l = properties.length; i < l; i++) {
var property = properties[i];
if (property.unused)
continue;
if (property.hack)
continue;
if (property.variable)
continue;
var descriptor = compactable[property.name];
if (!descriptor || !descriptor.componentOf)
continue;
if (property.shorthand) {
invalidateOrCompact(properties, i, candidates, sourceMaps, validator);
} else {
var componentOf = descriptor.componentOf;
candidates[componentOf] = candidates[componentOf] || {};
candidates[componentOf][property.name] = property;
}
}
invalidateOrCompact(properties, i, candidates, sourceMaps, validator);
}
module.exports = compactShortands;
@@ -0,0 +1,183 @@
// Validates various CSS property values
var split = require('../utils/split');
var widthKeywords = ['thin', 'thick', 'medium', 'inherit', 'initial'];
var allUnits = ['px', '%', 'em', 'in', 'cm', 'mm', 'ex', 'pt', 'pc', 'ch', 'rem', 'vh', 'vm', 'vmin', 'vmax', 'vw'];
var cssUnitRegexStr = '(\\-?\\.?\\d+\\.?\\d*(' + allUnits.join('|') + '|)|auto|inherit)';
var cssCalcRegexStr = '(\\-moz\\-|\\-webkit\\-)?calc\\([^\\)]+\\)';
var cssFunctionNoVendorRegexStr = '[A-Z]+(\\-|[A-Z]|[0-9])+\\(.*?\\)';
var cssFunctionVendorRegexStr = '\\-(\\-|[A-Z]|[0-9])+\\(.*?\\)';
var cssVariableRegexStr = 'var\\(\\-\\-[^\\)]+\\)';
var cssFunctionAnyRegexStr = '(' + cssVariableRegexStr + '|' + cssFunctionNoVendorRegexStr + '|' + cssFunctionVendorRegexStr + ')';
var cssUnitOrCalcRegexStr = '(' + cssUnitRegexStr + '|' + cssCalcRegexStr + ')';
var cssUnitAnyRegexStr = '(none|' + widthKeywords.join('|') + '|' + cssUnitRegexStr + '|' + cssVariableRegexStr + '|' + cssFunctionNoVendorRegexStr + '|' + cssFunctionVendorRegexStr + ')';
var cssFunctionNoVendorRegex = new RegExp('^' + cssFunctionNoVendorRegexStr + '$', 'i');
var cssFunctionVendorRegex = new RegExp('^' + cssFunctionVendorRegexStr + '$', 'i');
var cssVariableRegex = new RegExp('^' + cssVariableRegexStr + '$', 'i');
var cssFunctionAnyRegex = new RegExp('^' + cssFunctionAnyRegexStr + '$', 'i');
var cssUnitRegex = new RegExp('^' + cssUnitRegexStr + '$', 'i');
var cssUnitOrCalcRegex = new RegExp('^' + cssUnitOrCalcRegexStr + '$', 'i');
var cssUnitAnyRegex = new RegExp('^' + cssUnitAnyRegexStr + '$', 'i');
var backgroundRepeatKeywords = ['repeat', 'no-repeat', 'repeat-x', 'repeat-y', 'inherit'];
var backgroundAttachmentKeywords = ['inherit', 'scroll', 'fixed', 'local'];
var backgroundPositionKeywords = ['center', 'top', 'bottom', 'left', 'right'];
var backgroundSizeKeywords = ['contain', 'cover'];
var backgroundBoxKeywords = ['border-box', 'content-box', 'padding-box'];
var styleKeywords = ['auto', 'inherit', 'hidden', 'none', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset'];
var listStyleTypeKeywords = ['armenian', 'circle', 'cjk-ideographic', 'decimal', 'decimal-leading-zero', 'disc', 'georgian', 'hebrew', 'hiragana', 'hiragana-iroha', 'inherit', 'katakana', 'katakana-iroha', 'lower-alpha', 'lower-greek', 'lower-latin', 'lower-roman', 'none', 'square', 'upper-alpha', 'upper-latin', 'upper-roman'];
var listStylePositionKeywords = ['inside', 'outside', 'inherit'];
function Validator(compatibility) {
var validUnits = allUnits.slice(0).filter(function (value) {
return !(value in compatibility.units) || compatibility.units[value] === true;
});
var compatibleCssUnitRegexStr = '(\\-?\\.?\\d+\\.?\\d*(' + validUnits.join('|') + '|)|auto|inherit)';
this.compatibleCssUnitRegex = new RegExp('^' + compatibleCssUnitRegexStr + '$', 'i');
this.compatibleCssUnitAnyRegex = new RegExp('^(none|' + widthKeywords.join('|') + '|' + compatibleCssUnitRegexStr + '|' + cssVariableRegexStr + '|' + cssFunctionNoVendorRegexStr + '|' + cssFunctionVendorRegexStr + ')$', 'i');
}
Validator.prototype.isValidHexColor = function (s) {
return (s.length === 4 || s.length === 7) && s[0] === '#';
};
Validator.prototype.isValidRgbaColor = function (s) {
s = s.split(' ').join('');
return s.length > 0 && s.indexOf('rgba(') === 0 && s.indexOf(')') === s.length - 1;
};
Validator.prototype.isValidHslaColor = function (s) {
s = s.split(' ').join('');
return s.length > 0 && s.indexOf('hsla(') === 0 && s.indexOf(')') === s.length - 1;
};
Validator.prototype.isValidNamedColor = function (s) {
// We don't really check if it's a valid color value, but allow any letters in it
return s !== 'auto' && (s === 'transparent' || s === 'inherit' || /^[a-zA-Z]+$/.test(s));
};
Validator.prototype.isValidVariable = function (s) {
return cssVariableRegex.test(s);
};
Validator.prototype.isValidColor = function (s) {
return this.isValidNamedColor(s) ||
this.isValidHexColor(s) ||
this.isValidRgbaColor(s) ||
this.isValidHslaColor(s) ||
this.isValidVariable(s) ||
this.isValidVendorPrefixedValue(s);
};
Validator.prototype.isValidUrl = function (s) {
// NOTE: at this point all URLs are replaced with placeholders by clean-css, so we check for those placeholders
return s.indexOf('__ESCAPED_URL_CLEAN_CSS') === 0;
};
Validator.prototype.isValidUnit = function (s) {
return cssUnitAnyRegex.test(s);
};
Validator.prototype.isValidUnitWithoutFunction = function (s) {
return cssUnitRegex.test(s);
};
Validator.prototype.isValidAndCompatibleUnit = function (s) {
return this.compatibleCssUnitAnyRegex.test(s);
};
Validator.prototype.isValidAndCompatibleUnitWithoutFunction = function (s) {
return this.compatibleCssUnitRegex.test(s);
};
Validator.prototype.isValidFunctionWithoutVendorPrefix = function (s) {
return cssFunctionNoVendorRegex.test(s);
};
Validator.prototype.isValidFunctionWithVendorPrefix = function (s) {
return cssFunctionVendorRegex.test(s);
};
Validator.prototype.isValidFunction = function (s) {
return cssFunctionAnyRegex.test(s);
};
Validator.prototype.isValidBackgroundRepeat = function (s) {
return backgroundRepeatKeywords.indexOf(s) >= 0 || this.isValidVariable(s);
};
Validator.prototype.isValidBackgroundAttachment = function (s) {
return backgroundAttachmentKeywords.indexOf(s) >= 0 || this.isValidVariable(s);
};
Validator.prototype.isValidBackgroundBox = function (s) {
return backgroundBoxKeywords.indexOf(s) >= 0 || this.isValidVariable(s);
};
Validator.prototype.isValidBackgroundPositionPart = function (s) {
return backgroundPositionKeywords.indexOf(s) >= 0 || cssUnitOrCalcRegex.test(s) || this.isValidVariable(s);
};
Validator.prototype.isValidBackgroundPosition = function (s) {
if (s === 'inherit')
return true;
var parts = s.split(' ');
for (var i = 0, l = parts.length; i < l; i++) {
if (parts[i] === '')
continue;
if (this.isValidBackgroundPositionPart(parts[i]) || this.isValidVariable(parts[i]))
continue;
return false;
}
return true;
};
Validator.prototype.isValidBackgroundSizePart = function (s) {
return backgroundSizeKeywords.indexOf(s) >= 0 || cssUnitRegex.test(s) || this.isValidVariable(s);
};
Validator.prototype.isValidBackgroundPositionAndSize = function (s) {
if (s.indexOf('/') < 0)
return false;
var twoParts = split(s, '/');
return this.isValidBackgroundSizePart(twoParts.pop()) && this.isValidBackgroundPositionPart(twoParts.pop());
};
Validator.prototype.isValidListStyleType = function (s) {
return listStyleTypeKeywords.indexOf(s) >= 0 || this.isValidVariable(s);
};
Validator.prototype.isValidListStylePosition = function (s) {
return listStylePositionKeywords.indexOf(s) >= 0 || this.isValidVariable(s);
};
Validator.prototype.isValidStyle = function (s) {
return styleKeywords.indexOf(s) >= 0 || this.isValidVariable(s);
};
Validator.prototype.isValidWidth = function (s) {
return this.isValidUnit(s) || widthKeywords.indexOf(s) >= 0 || this.isValidVariable(s);
};
Validator.prototype.isValidVendorPrefixedValue = function (s) {
return /^-([A-Za-z0-9]|-)*$/gi.test(s);
};
Validator.prototype.areSameFunction = function (a, b) {
if (!this.isValidFunction(a) || !this.isValidFunction(b))
return false;
var f1name = a.substring(0, a.indexOf('('));
var f2name = b.substring(0, b.indexOf('('));
return f1name === f2name;
};
module.exports = Validator;
@@ -0,0 +1,26 @@
var VENDOR_PREFIX_PATTERN = /$\-moz\-|\-ms\-|\-o\-|\-webkit\-/;
function prefixesIn(tokens) {
var prefixes = [];
for (var i = 0, l = tokens.length; i < l; i++) {
var token = tokens[i];
for (var j = 0, m = token.value.length; j < m; j++) {
var match = VENDOR_PREFIX_PATTERN.exec(token.value[j][0]);
if (match && prefixes.indexOf(match[0]) == -1)
prefixes.push(match[0]);
}
}
return prefixes;
}
function same(left, right) {
return prefixesIn(left).sort().join(',') == prefixesIn(right).sort().join(',');
}
module.exports = {
same: same
};
@@ -0,0 +1,111 @@
var BACKSLASH_HACK = '\\';
var IMPORTANT_TOKEN = '!important';
var STAR_HACK = '*';
var UNDERSCORE_HACK = '_';
var BANG_HACK = '!';
function wrapAll(properties) {
var wrapped = [];
for (var i = properties.length - 1; i >= 0; i--) {
if (typeof properties[i][0] == 'string')
continue;
var single = wrapSingle(properties[i]);
single.all = properties;
single.position = i;
wrapped.unshift(single);
}
return wrapped;
}
function isMultiplex(property) {
for (var i = 1, l = property.length; i < l; i++) {
if (property[i][0] == ',' || property[i][0] == '/')
return true;
}
return false;
}
function hackType(property) {
var type = false;
var name = property[0][0];
var lastValue = property[property.length - 1];
if (name[0] == UNDERSCORE_HACK) {
type = 'underscore';
} else if (name[0] == STAR_HACK) {
type = 'star';
} else if (lastValue[0][0] == BANG_HACK && lastValue[0].indexOf('important') == -1) {
type = 'bang';
} else if (lastValue[0].indexOf(BANG_HACK) > 0 && lastValue[0].indexOf('important') == -1) {
type = 'bang';
} else if (lastValue[0].indexOf(BACKSLASH_HACK) > 0 && lastValue[0].indexOf(BACKSLASH_HACK) == lastValue[0].length - BACKSLASH_HACK.length - 1) {
type = 'backslash';
} else if (lastValue[0].indexOf(BACKSLASH_HACK) === 0 && lastValue[0].length == 2) {
type = 'backslash';
}
return type;
}
function isImportant(property) {
return property.length > 1 ?
property[property.length - 1][0].indexOf(IMPORTANT_TOKEN) > 0 :
false;
}
function stripImportant(property) {
if (property.length > 0)
property[property.length - 1][0] = property[property.length - 1][0].replace(IMPORTANT_TOKEN, '');
}
function stripPrefixHack(property) {
property[0][0] = property[0][0].substring(1);
}
function stripSuffixHack(property, hackType) {
var lastValue = property[property.length - 1];
lastValue[0] = lastValue[0]
.substring(0, lastValue[0].indexOf(hackType == 'backslash' ? BACKSLASH_HACK : BANG_HACK))
.trim();
if (lastValue[0].length === 0)
property.pop();
}
function wrapSingle(property) {
var _isImportant = isImportant(property);
if (_isImportant)
stripImportant(property);
var _hackType = hackType(property);
if (_hackType == 'star' || _hackType == 'underscore')
stripPrefixHack(property);
else if (_hackType == 'backslash' || _hackType == 'bang')
stripSuffixHack(property, _hackType);
var isVariable = property[0][0].indexOf('--') === 0;
return {
block: isVariable && property[1] && Array.isArray(property[1][0][0]),
components: [],
dirty: false,
hack: _hackType,
important: _isImportant,
name: property[0][0],
multiplex: property.length > 2 ? isMultiplex(property) : false,
position: 0,
shorthand: false,
unused: property.length < 2,
value: property.slice(1),
variable: isVariable
};
}
module.exports = {
all: wrapAll,
single: wrapSingle
};
@@ -0,0 +1,86 @@
var optimizeProperties = require('../properties/optimizer');
var removeDuplicates = require('./remove-duplicates');
var mergeAdjacent = require('./merge-adjacent');
var reduceNonAdjacent = require('./reduce-non-adjacent');
var mergeNonAdjacentBySelector = require('./merge-non-adjacent-by-selector');
var mergeNonAdjacentByBody = require('./merge-non-adjacent-by-body');
var restructure = require('./restructure');
var removeDuplicateMediaQueries = require('./remove-duplicate-media-queries');
var mergeMediaQueries = require('./merge-media-queries');
function removeEmpty(tokens) {
for (var i = 0, l = tokens.length; i < l; i++) {
var token = tokens[i];
var isEmpty = false;
switch (token[0]) {
case 'selector':
isEmpty = token[1].length === 0 || token[2].length === 0;
break;
case 'block':
removeEmpty(token[2]);
isEmpty = token[2].length === 0;
}
if (isEmpty) {
tokens.splice(i, 1);
i--;
l--;
}
}
}
function recursivelyOptimizeBlocks(tokens, options, validator) {
for (var i = 0, l = tokens.length; i < l; i++) {
var token = tokens[i];
if (token[0] == 'block') {
var isKeyframes = /@(-moz-|-o-|-webkit-)?keyframes/.test(token[1][0]);
optimize(token[2], options, validator, !isKeyframes);
}
}
}
function recursivelyOptimizeProperties(tokens, options, validator) {
for (var i = 0, l = tokens.length; i < l; i++) {
var token = tokens[i];
switch (token[0]) {
case 'selector':
optimizeProperties(token[1], token[2], false, true, options, validator);
break;
case 'block':
recursivelyOptimizeProperties(token[2], options, validator);
}
}
}
function optimize(tokens, options, validator, withRestructuring) {
recursivelyOptimizeBlocks(tokens, options, validator);
recursivelyOptimizeProperties(tokens, options, validator);
removeDuplicates(tokens);
mergeAdjacent(tokens, options, validator);
reduceNonAdjacent(tokens, options, validator);
mergeNonAdjacentBySelector(tokens, options, validator);
mergeNonAdjacentByBody(tokens, options);
if (options.restructuring && withRestructuring) {
restructure(tokens, options);
mergeAdjacent(tokens, options, validator);
}
if (options.mediaMerging) {
removeDuplicateMediaQueries(tokens);
var reduced = mergeMediaQueries(tokens);
for (var i = reduced.length - 1; i >= 0; i--) {
optimize(reduced[i][2], options, validator, false);
}
}
removeEmpty(tokens);
}
module.exports = optimize;
@@ -0,0 +1,80 @@
function removeWhitespace(match, value) {
return '[' + value.replace(/ /g, '') + ']';
}
function selectorSorter(s1, s2) {
return s1[0] > s2[0] ? 1 : -1;
}
var CleanUp = {
selectors: function (selectors, removeUnsupported, adjacentSpace) {
var list = [];
var repeated = [];
for (var i = 0, l = selectors.length; i < l; i++) {
var selector = selectors[i];
var reduced = selector[0]
.replace(/\s+/g, ' ')
.replace(/ ?, ?/g, ',')
.replace(/\s*([>\+\~])\s*/g, '$1')
.trim();
if (adjacentSpace && reduced.indexOf('nav') > 0)
reduced = reduced.replace(/\+nav(\S|$)/, '+ nav$1');
if (removeUnsupported && (reduced.indexOf('*+html ') != -1 || reduced.indexOf('*:first-child+html ') != -1))
continue;
if (reduced.indexOf('*') > -1) {
reduced = reduced
.replace(/\*([:#\.\[])/g, '$1')
.replace(/^(\:first\-child)?\+html/, '*$1+html');
}
if (reduced.indexOf('[') > -1)
reduced = reduced.replace(/\[([^\]]+)\]/g, removeWhitespace);
if (repeated.indexOf(reduced) == -1) {
selector[0] = reduced;
repeated.push(reduced);
list.push(selector);
}
}
return list.sort(selectorSorter);
},
selectorDuplicates: function (selectors) {
var list = [];
var repeated = [];
for (var i = 0, l = selectors.length; i < l; i++) {
var selector = selectors[i];
if (repeated.indexOf(selector[0]) == -1) {
repeated.push(selector[0]);
list.push(selector);
}
}
return list.sort(selectorSorter);
},
block: function (values, spaceAfterClosingBrace) {
values[0] = values[0]
.replace(/\s+/g, ' ')
.replace(/(,|:|\() /g, '$1')
.replace(/ \)/g, ')');
if (!spaceAfterClosingBrace)
values[0] = values[0].replace(/\) /g, ')');
},
atRule: function (values) {
values[0] = values[0]
.replace(/\s+/g, ' ')
.trim();
}
};
module.exports = CleanUp;
@@ -0,0 +1,69 @@
// This extractor is used in advanced optimizations
// IMPORTANT: Mind Token class and this code is not related!
// Properties will be tokenized in one step, see #429
var stringifySelectors = require('../stringifier/one-time').selectors;
var stringifyValue = require('../stringifier/one-time').value;
var AT_RULE = 'at-rule';
function extract(token) {
var properties = [];
if (token[0] == 'selector') {
var inSpecificSelector = !/[\.\+>~]/.test(stringifySelectors(token[1]));
for (var i = 0, l = token[2].length; i < l; i++) {
var property = token[2][i];
if (property.indexOf('__ESCAPED') === 0)
continue;
if (property[0] == AT_RULE)
continue;
var name = token[2][i][0][0];
if (name.length === 0)
continue;
if (name.indexOf('--') === 0)
continue;
var value = stringifyValue(token[2], i);
properties.push([
name,
value,
findNameRoot(name),
token[2][i],
name + ':' + value,
token[1],
inSpecificSelector
]);
}
} else if (token[0] == 'block') {
for (var j = 0, k = token[2].length; j < k; j++) {
properties = properties.concat(extract(token[2][j]));
}
}
return properties;
}
function findNameRoot(name) {
if (name == 'list-style')
return name;
if (name.indexOf('-radius') > 0)
return 'border-radius';
if (name == 'border-collapse' || name == 'border-spacing' || name == 'border-image')
return name;
if (name.indexOf('border-') === 0 && /^border\-\w+\-\w+$/.test(name))
return name.match(/border\-\w+/)[0];
if (name.indexOf('border-') === 0 && /^border\-\w+$/.test(name))
return 'border';
if (name.indexOf('text-') === 0)
return name;
return name.replace(/^\-\w+\-/, '').match(/([a-zA-Z]+)/)[0].toLowerCase();
}
module.exports = extract;
@@ -0,0 +1,5 @@
function isSpecial(options, selector) {
return options.compatibility.selectors.special.test(selector);
}
module.exports = isSpecial;
@@ -0,0 +1,35 @@
var optimizeProperties = require('../properties/optimizer');
var stringifyBody = require('../stringifier/one-time').body;
var stringifySelectors = require('../stringifier/one-time').selectors;
var cleanUpSelectors = require('./clean-up').selectors;
var isSpecial = require('./is-special');
function mergeAdjacent(tokens, options, validator) {
var lastToken = [null, [], []];
var adjacentSpace = options.compatibility.selectors.adjacentSpace;
for (var i = 0, l = tokens.length; i < l; i++) {
var token = tokens[i];
if (token[0] != 'selector') {
lastToken = [null, [], []];
continue;
}
if (lastToken[0] == 'selector' && stringifySelectors(token[1]) == stringifySelectors(lastToken[1])) {
var joinAt = [lastToken[2].length];
Array.prototype.push.apply(lastToken[2], token[2]);
optimizeProperties(token[1], lastToken[2], joinAt, true, options, validator);
token[2] = [];
} else if (lastToken[0] == 'selector' && stringifyBody(token[2]) == stringifyBody(lastToken[2]) &&
!isSpecial(options, stringifySelectors(token[1])) && !isSpecial(options, stringifySelectors(lastToken[1]))) {
lastToken[1] = cleanUpSelectors(lastToken[1].concat(token[1]), false, adjacentSpace);
token[2] = [];
} else {
lastToken = token;
}
}
}
module.exports = mergeAdjacent;
@@ -0,0 +1,64 @@
var canReorder = require('./reorderable').canReorder;
var extractProperties = require('./extractor');
function mergeMediaQueries(tokens) {
var candidates = {};
var reduced = [];
for (var i = tokens.length - 1; i >= 0; i--) {
var token = tokens[i];
if (token[0] != 'block')
continue;
var candidate = candidates[token[1][0]];
if (!candidate) {
candidate = [];
candidates[token[1][0]] = candidate;
}
candidate.push(i);
}
for (var name in candidates) {
var positions = candidates[name];
positionLoop:
for (var j = positions.length - 1; j > 0; j--) {
var positionOne = positions[j];
var tokenOne = tokens[positionOne];
var positionTwo = positions[j - 1];
var tokenTwo = tokens[positionTwo];
directionLoop:
for (var direction = 1; direction >= -1; direction -= 2) {
var topToBottom = direction == 1;
var from = topToBottom ? positionOne + 1 : positionTwo - 1;
var to = topToBottom ? positionTwo : positionOne;
var delta = topToBottom ? 1 : -1;
var source = topToBottom ? tokenOne : tokenTwo;
var target = topToBottom ? tokenTwo : tokenOne;
var movedProperties = extractProperties(source);
while (from != to) {
var traversedProperties = extractProperties(tokens[from]);
from += delta;
if (!canReorder(movedProperties, traversedProperties))
continue directionLoop;
}
target[2] = topToBottom ?
source[2].concat(target[2]) :
target[2].concat(source[2]);
source[2] = [];
reduced.push(target);
continue positionLoop;
}
}
}
return reduced;
}
module.exports = mergeMediaQueries;
@@ -0,0 +1,58 @@
var stringifyBody = require('../stringifier/one-time').body;
var stringifySelectors = require('../stringifier/one-time').selectors;
var cleanUpSelectors = require('./clean-up').selectors;
var isSpecial = require('./is-special');
function unsafeSelector(value) {
return /\.|\*| :/.test(value);
}
function isBemElement(token) {
var asString = stringifySelectors(token[1]);
return asString.indexOf('__') > -1 || asString.indexOf('--') > -1;
}
function withoutModifier(selector) {
return selector.replace(/--[^ ,>\+~:]+/g, '');
}
function removeAnyUnsafeElements(left, candidates) {
var leftSelector = withoutModifier(stringifySelectors(left[1]));
for (var body in candidates) {
var right = candidates[body];
var rightSelector = withoutModifier(stringifySelectors(right[1]));
if (rightSelector.indexOf(leftSelector) > -1 || leftSelector.indexOf(rightSelector) > -1)
delete candidates[body];
}
}
function mergeNonAdjacentByBody(tokens, options) {
var candidates = {};
var adjacentSpace = options.compatibility.selectors.adjacentSpace;
for (var i = tokens.length - 1; i >= 0; i--) {
var token = tokens[i];
if (token[0] != 'selector')
continue;
if (token[2].length > 0 && (!options.semanticMerging && unsafeSelector(stringifySelectors(token[1]))))
candidates = {};
if (token[2].length > 0 && options.semanticMerging && isBemElement(token))
removeAnyUnsafeElements(token, candidates);
var oldToken = candidates[stringifyBody(token[2])];
if (oldToken && !isSpecial(options, stringifySelectors(token[1])) && !isSpecial(options, stringifySelectors(oldToken[1]))) {
token[1] = cleanUpSelectors(oldToken[1].concat(token[1]), false, adjacentSpace);
oldToken[2] = [];
candidates[stringifyBody(token[2])] = null;
}
candidates[stringifyBody(token[2])] = token;
}
}
module.exports = mergeNonAdjacentByBody;
@@ -0,0 +1,76 @@
var optimizeProperties = require('../properties/optimizer');
var stringifySelectors = require('../stringifier/one-time').selectors;
var extractProperties = require('./extractor');
var canReorder = require('./reorderable').canReorder;
function mergeNonAdjacentBySelector(tokens, options, validator) {
var allSelectors = {};
var repeatedSelectors = [];
var i;
for (i = tokens.length - 1; i >= 0; i--) {
if (tokens[i][0] != 'selector')
continue;
if (tokens[i][2].length === 0)
continue;
var selector = stringifySelectors(tokens[i][1]);
allSelectors[selector] = [i].concat(allSelectors[selector] || []);
if (allSelectors[selector].length == 2)
repeatedSelectors.push(selector);
}
for (i = repeatedSelectors.length - 1; i >= 0; i--) {
var positions = allSelectors[repeatedSelectors[i]];
selectorIterator:
for (var j = positions.length - 1; j > 0; j--) {
var positionOne = positions[j - 1];
var tokenOne = tokens[positionOne];
var positionTwo = positions[j];
var tokenTwo = tokens[positionTwo];
directionIterator:
for (var direction = 1; direction >= -1; direction -= 2) {
var topToBottom = direction == 1;
var from = topToBottom ? positionOne + 1 : positionTwo - 1;
var to = topToBottom ? positionTwo : positionOne;
var delta = topToBottom ? 1 : -1;
var moved = topToBottom ? tokenOne : tokenTwo;
var target = topToBottom ? tokenTwo : tokenOne;
var movedProperties = extractProperties(moved);
var joinAt;
while (from != to) {
var traversedProperties = extractProperties(tokens[from]);
from += delta;
// traversed then moved as we move selectors towards the start
var reorderable = topToBottom ?
canReorder(movedProperties, traversedProperties) :
canReorder(traversedProperties, movedProperties);
if (!reorderable && !topToBottom)
continue selectorIterator;
if (!reorderable && topToBottom)
continue directionIterator;
}
if (topToBottom) {
joinAt = [moved[2].length];
Array.prototype.push.apply(moved[2], target[2]);
target[2] = moved[2];
} else {
joinAt = [target[2].length];
Array.prototype.push.apply(target[2], moved[2]);
}
optimizeProperties(target[1], target[2], joinAt, true, options, validator);
moved[2] = [];
}
}
}
}
module.exports = mergeNonAdjacentBySelector;
@@ -0,0 +1,172 @@
var optimizeProperties = require('../properties/optimizer');
var stringifyBody = require('../stringifier/one-time').body;
var stringifySelectors = require('../stringifier/one-time').selectors;
var isSpecial = require('./is-special');
var cloneArray = require('../utils/clone-array');
function reduceNonAdjacent(tokens, options, validator) {
var candidates = {};
var repeated = [];
for (var i = tokens.length - 1; i >= 0; i--) {
var token = tokens[i];
if (token[0] != 'selector')
continue;
if (token[2].length === 0)
continue;
var selectorAsString = stringifySelectors(token[1]);
var isComplexAndNotSpecial = token[1].length > 1 && !isSpecial(options, selectorAsString);
var wrappedSelectors = options.sourceMap ? wrappedSelectorsFrom(token[1]) : token[1];
var selectors = isComplexAndNotSpecial ?
[selectorAsString].concat(wrappedSelectors) :
[selectorAsString];
for (var j = 0, m = selectors.length; j < m; j++) {
var selector = selectors[j];
if (!candidates[selector])
candidates[selector] = [];
else
repeated.push(selector);
candidates[selector].push({
where: i,
list: wrappedSelectors,
isPartial: isComplexAndNotSpecial && j > 0,
isComplex: isComplexAndNotSpecial && j === 0
});
}
}
reduceSimpleNonAdjacentCases(tokens, repeated, candidates, options, validator);
reduceComplexNonAdjacentCases(tokens, candidates, options, validator);
}
function wrappedSelectorsFrom(list) {
var wrapped = [];
for (var i = 0; i < list.length; i++) {
wrapped.push([list[i][0]]);
}
return wrapped;
}
function reduceSimpleNonAdjacentCases(tokens, repeated, candidates, options, validator) {
function filterOut(idx, bodies) {
return data[idx].isPartial && bodies.length === 0;
}
function reduceBody(token, newBody, processedCount, tokenIdx) {
if (!data[processedCount - tokenIdx - 1].isPartial)
token[2] = newBody;
}
for (var i = 0, l = repeated.length; i < l; i++) {
var selector = repeated[i];
var data = candidates[selector];
reduceSelector(tokens, selector, data, {
filterOut: filterOut,
callback: reduceBody
}, options, validator);
}
}
function reduceComplexNonAdjacentCases(tokens, candidates, options, validator) {
var localContext = {};
function filterOut(idx) {
return localContext.data[idx].where < localContext.intoPosition;
}
function collectReducedBodies(token, newBody, processedCount, tokenIdx) {
if (tokenIdx === 0)
localContext.reducedBodies.push(newBody);
}
allSelectors:
for (var complexSelector in candidates) {
var into = candidates[complexSelector];
if (!into[0].isComplex)
continue;
var intoPosition = into[into.length - 1].where;
var intoToken = tokens[intoPosition];
var reducedBodies = [];
var selectors = isSpecial(options, complexSelector) ?
[complexSelector] :
into[0].list;
localContext.intoPosition = intoPosition;
localContext.reducedBodies = reducedBodies;
for (var j = 0, m = selectors.length; j < m; j++) {
var selector = selectors[j];
var data = candidates[selector];
if (data.length < 2)
continue allSelectors;
localContext.data = data;
reduceSelector(tokens, selector, data, {
filterOut: filterOut,
callback: collectReducedBodies
}, options, validator);
if (stringifyBody(reducedBodies[reducedBodies.length - 1]) != stringifyBody(reducedBodies[0]))
continue allSelectors;
}
intoToken[2] = reducedBodies[0];
}
}
function reduceSelector(tokens, selector, data, context, options, validator) {
var bodies = [];
var bodiesAsList = [];
var joinsAt = [];
var processedTokens = [];
for (var j = data.length - 1, m = 0; j >= 0; j--) {
if (context.filterOut(j, bodies))
continue;
var where = data[j].where;
var token = tokens[where];
var clonedBody = cloneArray(token[2]);
bodies = bodies.concat(clonedBody);
bodiesAsList.push(clonedBody);
processedTokens.push(where);
}
for (j = 0, m = bodiesAsList.length; j < m; j++) {
if (bodiesAsList[j].length > 0)
joinsAt.push((joinsAt[j - 1] || 0) + bodiesAsList[j].length);
}
optimizeProperties(selector, bodies, joinsAt, false, options, validator);
var processedCount = processedTokens.length;
var propertyIdx = bodies.length - 1;
var tokenIdx = processedCount - 1;
while (tokenIdx >= 0) {
if ((tokenIdx === 0 || (bodies[propertyIdx] && bodiesAsList[tokenIdx].indexOf(bodies[propertyIdx]) > -1)) && propertyIdx > -1) {
propertyIdx--;
continue;
}
var newBody = bodies.splice(propertyIdx + 1);
context.callback(tokens[processedTokens[tokenIdx]], newBody, processedCount, tokenIdx);
tokenIdx--;
}
}
module.exports = reduceNonAdjacent;
@@ -0,0 +1,21 @@
var stringifyAll = require('../stringifier/one-time').all;
function removeDuplicateMediaQueries(tokens) {
var candidates = {};
for (var i = 0, l = tokens.length; i < l; i++) {
var token = tokens[i];
if (token[0] != 'block')
continue;
var key = token[1][0] + '%' + stringifyAll(token[2]);
var candidate = candidates[key];
if (candidate)
candidate[2] = [];
candidates[key] = token;
}
}
module.exports = removeDuplicateMediaQueries;
@@ -0,0 +1,41 @@
var stringifyBody = require('../stringifier/one-time').body;
var stringifySelectors = require('../stringifier/one-time').selectors;
function removeDuplicates(tokens) {
var matched = {};
var moreThanOnce = [];
var id, token;
var body, bodies;
for (var i = 0, l = tokens.length; i < l; i++) {
token = tokens[i];
if (token[0] != 'selector')
continue;
id = stringifySelectors(token[1]);
if (matched[id] && matched[id].length == 1)
moreThanOnce.push(id);
else
matched[id] = matched[id] || [];
matched[id].push(i);
}
for (i = 0, l = moreThanOnce.length; i < l; i++) {
id = moreThanOnce[i];
bodies = [];
for (var j = matched[id].length - 1; j >= 0; j--) {
token = tokens[matched[id][j]];
body = stringifyBody(token[2]);
if (bodies.indexOf(body) > -1)
token[2] = [];
else
bodies.push(body);
}
}
}
module.exports = removeDuplicates;
@@ -0,0 +1,95 @@
// TODO: it'd be great to merge it with the other canReorder functionality
var FLEX_PROPERTIES = /align\-items|box\-align|box\-pack|flex|justify/;
var BORDER_PROPERTIES = /^border\-(top|right|bottom|left|color|style|width|radius)/;
function canReorder(left, right) {
for (var i = right.length - 1; i >= 0; i--) {
for (var j = left.length - 1; j >= 0; j--) {
if (!canReorderSingle(left[j], right[i]))
return false;
}
}
return true;
}
function canReorderSingle(left, right) {
var leftName = left[0];
var leftValue = left[1];
var leftNameRoot = left[2];
var leftSelector = left[5];
var leftInSpecificSelector = left[6];
var rightName = right[0];
var rightValue = right[1];
var rightNameRoot = right[2];
var rightSelector = right[5];
var rightInSpecificSelector = right[6];
if (leftName == 'font' && rightName == 'line-height' || rightName == 'font' && leftName == 'line-height')
return false;
if (FLEX_PROPERTIES.test(leftName) && FLEX_PROPERTIES.test(rightName))
return false;
if (leftNameRoot == rightNameRoot && unprefixed(leftName) == unprefixed(rightName) && (vendorPrefixed(leftName) ^ vendorPrefixed(rightName)))
return false;
if (leftNameRoot == 'border' && BORDER_PROPERTIES.test(rightNameRoot) && (leftName == 'border' || leftName == rightNameRoot))
return false;
if (rightNameRoot == 'border' && BORDER_PROPERTIES.test(leftNameRoot) && (rightName == 'border' || rightName == leftNameRoot))
return false;
if (leftNameRoot == 'border' && rightNameRoot == 'border' && leftName != rightName && (isSideBorder(leftName) && isStyleBorder(rightName) || isStyleBorder(leftName) && isSideBorder(rightName)))
return false;
if (leftNameRoot != rightNameRoot)
return true;
if (leftName == rightName && leftNameRoot == rightNameRoot && (leftValue == rightValue || withDifferentVendorPrefix(leftValue, rightValue)))
return true;
if (leftName != rightName && leftNameRoot == rightNameRoot && leftName != leftNameRoot && rightName != rightNameRoot)
return true;
if (leftName != rightName && leftNameRoot == rightNameRoot && leftValue == rightValue)
return true;
if (rightInSpecificSelector && leftInSpecificSelector && !inheritable(leftNameRoot) && !inheritable(rightNameRoot) && selectorsDoNotOverlap(rightSelector, leftSelector))
return true;
return false;
}
function vendorPrefixed(name) {
return /^\-(?:moz|webkit|ms|o)\-/.test(name);
}
function unprefixed(name) {
return name.replace(/^\-(?:moz|webkit|ms|o)\-/, '');
}
function isSideBorder(name) {
return name == 'border-top' || name == 'border-right' || name == 'border-bottom' || name == 'border-left';
}
function isStyleBorder(name) {
return name == 'border-color' || name == 'border-style' || name == 'border-width';
}
function withDifferentVendorPrefix(value1, value2) {
return vendorPrefixed(value1) && vendorPrefixed(value2) && value1.split('-')[1] != value2.split('-')[2];
}
function selectorsDoNotOverlap(s1, s2) {
for (var i = 0, l = s1.length; i < l; i++) {
for (var j = 0, m = s2.length; j < m; j++) {
if (s1[i][0] == s2[j][0])
return false;
}
}
return true;
}
function inheritable(name) {
// According to http://www.w3.org/TR/CSS21/propidx.html
// Others will be catched by other, preceeding rules
return name == 'font' || name == 'line-height' || name == 'list-style';
}
module.exports = {
canReorder: canReorder,
canReorderSingle: canReorderSingle
};
@@ -0,0 +1,352 @@
var extractProperties = require('./extractor');
var canReorderSingle = require('./reorderable').canReorderSingle;
var stringifyBody = require('../stringifier/one-time').body;
var stringifySelectors = require('../stringifier/one-time').selectors;
var cleanUpSelectorDuplicates = require('./clean-up').selectorDuplicates;
var isSpecial = require('./is-special');
function naturalSorter(a, b) {
return a > b;
}
function restructure(tokens, options) {
var movableTokens = {};
var movedProperties = [];
var multiPropertyMoveCache = {};
var movedToBeDropped = [];
var maxCombinationsLevel = 2;
var ID_JOIN_CHARACTER = '%';
function sendToMultiPropertyMoveCache(position, movedProperty, allFits) {
for (var i = allFits.length - 1; i >= 0; i--) {
var fit = allFits[i][0];
var id = addToCache(movedProperty, fit);
if (multiPropertyMoveCache[id].length > 1 && processMultiPropertyMove(position, multiPropertyMoveCache[id])) {
removeAllMatchingFromCache(id);
break;
}
}
}
function addToCache(movedProperty, fit) {
var id = cacheId(fit);
multiPropertyMoveCache[id] = multiPropertyMoveCache[id] || [];
multiPropertyMoveCache[id].push([movedProperty, fit]);
return id;
}
function removeAllMatchingFromCache(matchId) {
var matchSelectors = matchId.split(ID_JOIN_CHARACTER);
var forRemoval = [];
var i;
for (var id in multiPropertyMoveCache) {
var selectors = id.split(ID_JOIN_CHARACTER);
for (i = selectors.length - 1; i >= 0; i--) {
if (matchSelectors.indexOf(selectors[i]) > -1) {
forRemoval.push(id);
break;
}
}
}
for (i = forRemoval.length - 1; i >= 0; i--) {
delete multiPropertyMoveCache[forRemoval[i]];
}
}
function cacheId(cachedTokens) {
var id = [];
for (var i = 0, l = cachedTokens.length; i < l; i++) {
id.push(stringifySelectors(cachedTokens[i][1]));
}
return id.join(ID_JOIN_CHARACTER);
}
function tokensToMerge(sourceTokens) {
var uniqueTokensWithBody = [];
var mergeableTokens = [];
for (var i = sourceTokens.length - 1; i >= 0; i--) {
if (isSpecial(options, stringifySelectors(sourceTokens[i][1])))
continue;
mergeableTokens.unshift(sourceTokens[i]);
if (sourceTokens[i][2].length > 0 && uniqueTokensWithBody.indexOf(sourceTokens[i]) == -1)
uniqueTokensWithBody.push(sourceTokens[i]);
}
return uniqueTokensWithBody.length > 1 ?
mergeableTokens :
[];
}
function shortenIfPossible(position, movedProperty) {
var name = movedProperty[0];
var value = movedProperty[1];
var key = movedProperty[4];
var valueSize = name.length + value.length + 1;
var allSelectors = [];
var qualifiedTokens = [];
var mergeableTokens = tokensToMerge(movableTokens[key]);
if (mergeableTokens.length < 2)
return;
var allFits = findAllFits(mergeableTokens, valueSize, 1);
var bestFit = allFits[0];
if (bestFit[1] > 0)
return sendToMultiPropertyMoveCache(position, movedProperty, allFits);
for (var i = bestFit[0].length - 1; i >=0; i--) {
allSelectors = bestFit[0][i][1].concat(allSelectors);
qualifiedTokens.unshift(bestFit[0][i]);
}
allSelectors = cleanUpSelectorDuplicates(allSelectors);
dropAsNewTokenAt(position, [movedProperty], allSelectors, qualifiedTokens);
}
function fitSorter(fit1, fit2) {
return fit1[1] > fit2[1];
}
function findAllFits(mergeableTokens, propertySize, propertiesCount) {
var combinations = allCombinations(mergeableTokens, propertySize, propertiesCount, maxCombinationsLevel - 1);
return combinations.sort(fitSorter);
}
function allCombinations(tokensVariant, propertySize, propertiesCount, level) {
var differenceVariants = [[tokensVariant, sizeDifference(tokensVariant, propertySize, propertiesCount)]];
if (tokensVariant.length > 2 && level > 0) {
for (var i = tokensVariant.length - 1; i >= 0; i--) {
var subVariant = Array.prototype.slice.call(tokensVariant, 0);
subVariant.splice(i, 1);
differenceVariants = differenceVariants.concat(allCombinations(subVariant, propertySize, propertiesCount, level - 1));
}
}
return differenceVariants;
}
function sizeDifference(tokensVariant, propertySize, propertiesCount) {
var allSelectorsSize = 0;
for (var i = tokensVariant.length - 1; i >= 0; i--) {
allSelectorsSize += tokensVariant[i][2].length > propertiesCount ? stringifySelectors(tokensVariant[i][1]).length : -1;
}
return allSelectorsSize - (tokensVariant.length - 1) * propertySize + 1;
}
function dropAsNewTokenAt(position, properties, allSelectors, mergeableTokens) {
var i, j, k, m;
var allProperties = [];
for (i = mergeableTokens.length - 1; i >= 0; i--) {
var mergeableToken = mergeableTokens[i];
for (j = mergeableToken[2].length - 1; j >= 0; j--) {
var mergeableProperty = mergeableToken[2][j];
for (k = 0, m = properties.length; k < m; k++) {
var property = properties[k];
var mergeablePropertyName = mergeableProperty[0][0];
var propertyName = property[0];
var propertyBody = property[4];
if (mergeablePropertyName == propertyName && stringifyBody([mergeableProperty]) == propertyBody) {
mergeableToken[2].splice(j, 1);
break;
}
}
}
}
for (i = properties.length - 1; i >= 0; i--) {
allProperties.unshift(properties[i][3]);
}
var newToken = ['selector', allSelectors, allProperties];
tokens.splice(position, 0, newToken);
}
function dropPropertiesAt(position, movedProperty) {
var key = movedProperty[4];
var toMove = movableTokens[key];
if (toMove && toMove.length > 1) {
if (!shortenMultiMovesIfPossible(position, movedProperty))
shortenIfPossible(position, movedProperty);
}
}
function shortenMultiMovesIfPossible(position, movedProperty) {
var candidates = [];
var propertiesAndMergableTokens = [];
var key = movedProperty[4];
var j, k;
var mergeableTokens = tokensToMerge(movableTokens[key]);
if (mergeableTokens.length < 2)
return;
movableLoop:
for (var value in movableTokens) {
var tokensList = movableTokens[value];
for (j = mergeableTokens.length - 1; j >= 0; j--) {
if (tokensList.indexOf(mergeableTokens[j]) == -1)
continue movableLoop;
}
candidates.push(value);
}
if (candidates.length < 2)
return false;
for (j = candidates.length - 1; j >= 0; j--) {
for (k = movedProperties.length - 1; k >= 0; k--) {
if (movedProperties[k][4] == candidates[j]) {
propertiesAndMergableTokens.unshift([movedProperties[k], mergeableTokens]);
break;
}
}
}
return processMultiPropertyMove(position, propertiesAndMergableTokens);
}
function processMultiPropertyMove(position, propertiesAndMergableTokens) {
var valueSize = 0;
var properties = [];
var property;
for (var i = propertiesAndMergableTokens.length - 1; i >= 0; i--) {
property = propertiesAndMergableTokens[i][0];
var fullValue = property[4];
valueSize += fullValue.length + (i > 0 ? 1 : 0);
properties.push(property);
}
var mergeableTokens = propertiesAndMergableTokens[0][1];
var bestFit = findAllFits(mergeableTokens, valueSize, properties.length)[0];
if (bestFit[1] > 0)
return false;
var allSelectors = [];
var qualifiedTokens = [];
for (i = bestFit[0].length - 1; i >= 0; i--) {
allSelectors = bestFit[0][i][1].concat(allSelectors);
qualifiedTokens.unshift(bestFit[0][i]);
}
allSelectors = cleanUpSelectorDuplicates(allSelectors);
dropAsNewTokenAt(position, properties, allSelectors, qualifiedTokens);
for (i = properties.length - 1; i >= 0; i--) {
property = properties[i];
var index = movedProperties.indexOf(property);
delete movableTokens[property[4]];
if (index > -1 && movedToBeDropped.indexOf(index) == -1)
movedToBeDropped.push(index);
}
return true;
}
function boundToAnotherPropertyInCurrrentToken(property, movedProperty, token) {
var propertyName = property[0];
var movedPropertyName = movedProperty[0];
if (propertyName != movedPropertyName)
return false;
var key = movedProperty[4];
var toMove = movableTokens[key];
return toMove && toMove.indexOf(token) > -1;
}
for (var i = tokens.length - 1; i >= 0; i--) {
var token = tokens[i];
var isSelector;
var j, k, m;
if (token[0] == 'selector') {
isSelector = true;
} else if (token[0] == 'block') {
isSelector = false;
} else {
continue;
}
// We cache movedProperties.length as it may change in the loop
var movedCount = movedProperties.length;
var properties = extractProperties(token);
movedToBeDropped = [];
var unmovableInCurrentToken = [];
for (j = properties.length - 1; j >= 0; j--) {
for (k = j - 1; k >= 0; k--) {
if (!canReorderSingle(properties[j], properties[k])) {
unmovableInCurrentToken.push(j);
break;
}
}
}
for (j = properties.length - 1; j >= 0; j--) {
var property = properties[j];
var movedSameProperty = false;
for (k = 0; k < movedCount; k++) {
var movedProperty = movedProperties[k];
if (movedToBeDropped.indexOf(k) == -1 && !canReorderSingle(property, movedProperty) && !boundToAnotherPropertyInCurrrentToken(property, movedProperty, token)) {
dropPropertiesAt(i + 1, movedProperty, token);
if (movedToBeDropped.indexOf(k) == -1) {
movedToBeDropped.push(k);
delete movableTokens[movedProperty[4]];
}
}
if (!movedSameProperty)
movedSameProperty = property[0] == movedProperty[0] && property[1] == movedProperty[1];
}
if (!isSelector || unmovableInCurrentToken.indexOf(j) > -1)
continue;
var key = property[4];
movableTokens[key] = movableTokens[key] || [];
movableTokens[key].push(token);
if (!movedSameProperty)
movedProperties.push(property);
}
movedToBeDropped = movedToBeDropped.sort(naturalSorter);
for (j = 0, m = movedToBeDropped.length; j < m; j++) {
var dropAt = movedToBeDropped[j] - j;
movedProperties.splice(dropAt, 1);
}
}
var position = tokens[0] && tokens[0][0] == 'at-rule' && tokens[0][1][0].indexOf('@charset') === 0 ? 1 : 0;
for (; position < tokens.length - 1; position++) {
var isImportRule = tokens[position][0] === 'at-rule' && tokens[position][1][0].indexOf('@import') === 0;
var isEscapedCommentSpecial = tokens[position][0] === 'text' && tokens[position][1][0].indexOf('__ESCAPED_COMMENT_SPECIAL') === 0;
if (!(isImportRule || isEscapedCommentSpecial))
break;
}
for (i = 0; i < movedProperties.length; i++) {
dropPropertiesAt(position, movedProperties[i]);
}
}
module.exports = restructure;
@@ -0,0 +1,444 @@
var cleanUpSelectors = require('./clean-up').selectors;
var cleanUpBlock = require('./clean-up').block;
var cleanUpAtRule = require('./clean-up').atRule;
var split = require('../utils/split');
var RGB = require('../colors/rgb');
var HSL = require('../colors/hsl');
var HexNameShortener = require('../colors/hex-name-shortener');
var wrapForOptimizing = require('../properties/wrap-for-optimizing').all;
var restoreFromOptimizing = require('../properties/restore-from-optimizing');
var removeUnused = require('../properties/remove-unused');
var DEFAULT_ROUNDING_PRECISION = 2;
var CHARSET_TOKEN = '@charset';
var CHARSET_REGEXP = new RegExp('^' + CHARSET_TOKEN, 'i');
var FONT_NUMERAL_WEIGHTS = ['100', '200', '300', '400', '500', '600', '700', '800', '900'];
var FONT_NAME_WEIGHTS = ['normal', 'bold', 'bolder', 'lighter'];
var FONT_NAME_WEIGHTS_WITHOUT_NORMAL = ['bold', 'bolder', 'lighter'];
var WHOLE_PIXEL_VALUE = /(?:^|\s|\()(-?\d+)px/;
var TIME_VALUE = /^(\-?[\d\.]+)(m?s)$/;
var valueMinifiers = {
'background': function (value, index, total) {
return index === 0 && total == 1 && (value == 'none' || value == 'transparent') ? '0 0' : value;
},
'font-weight': function (value) {
if (value == 'normal')
return '400';
else if (value == 'bold')
return '700';
else
return value;
},
'outline': function (value, index, total) {
return index === 0 && total == 1 && value == 'none' ? '0' : value;
}
};
function isNegative(property, idx) {
return property.value[idx] && property.value[idx][0][0] == '-' && parseFloat(property.value[idx][0]) < 0;
}
function zeroMinifier(name, value) {
if (value.indexOf('0') == -1)
return value;
if (value.indexOf('-') > -1) {
value = value
.replace(/([^\w\d\-]|^)\-0([^\.]|$)/g, '$10$2')
.replace(/([^\w\d\-]|^)\-0([^\.]|$)/g, '$10$2');
}
return value
.replace(/(^|\s)0+([1-9])/g, '$1$2')
.replace(/(^|\D)\.0+(\D|$)/g, '$10$2')
.replace(/(^|\D)\.0+(\D|$)/g, '$10$2')
.replace(/\.([1-9]*)0+(\D|$)/g, function (match, nonZeroPart, suffix) {
return (nonZeroPart.length > 0 ? '.' : '') + nonZeroPart + suffix;
})
.replace(/(^|\D)0\.(\d)/g, '$1.$2');
}
function zeroDegMinifier(_, value) {
if (value.indexOf('0deg') == -1)
return value;
return value.replace(/\(0deg\)/g, '(0)');
}
function whitespaceMinifier(name, value) {
if (name.indexOf('filter') > -1 || value.indexOf(' ') == -1)
return value;
value = value.replace(/\s+/g, ' ');
if (value.indexOf('calc') > -1)
value = value.replace(/\) ?\/ ?/g, ')/ ');
return value
.replace(/\( /g, '(')
.replace(/ \)/g, ')')
.replace(/, /g, ',');
}
function precisionMinifier(_, value, precisionOptions) {
if (precisionOptions.value === -1 || value.indexOf('.') === -1)
return value;
return value
.replace(precisionOptions.regexp, function (match, number) {
return Math.round(parseFloat(number) * precisionOptions.multiplier) / precisionOptions.multiplier + 'px';
})
.replace(/(\d)\.($|\D)/g, '$1$2');
}
function unitMinifier(name, value, unitsRegexp) {
if (/^(?:\-moz\-calc|\-webkit\-calc|calc)\(/.test(value))
return value;
if (name == 'flex' || name == '-ms-flex' || name == '-webkit-flex' || name == 'flex-basis' || name == '-webkit-flex-basis')
return value;
if (value.indexOf('%') > 0 && (name == 'height' || name == 'max-height'))
return value;
return value
.replace(unitsRegexp, '$1' + '0' + '$2')
.replace(unitsRegexp, '$1' + '0' + '$2');
}
function multipleZerosMinifier(property) {
var values = property.value;
var spliceAt;
if (values.length == 4 && values[0][0] === '0' && values[1][0] === '0' && values[2][0] === '0' && values[3][0] === '0') {
if (property.name.indexOf('box-shadow') > -1)
spliceAt = 2;
else
spliceAt = 1;
}
if (spliceAt) {
property.value.splice(spliceAt);
property.dirty = true;
}
}
function colorMininifier(_, value, compatibility) {
if (value.indexOf('#') === -1 && value.indexOf('rgb') == -1 && value.indexOf('hsl') == -1)
return HexNameShortener.shorten(value);
value = value
.replace(/rgb\((\-?\d+),(\-?\d+),(\-?\d+)\)/g, function (match, red, green, blue) {
return new RGB(red, green, blue).toHex();
})
.replace(/hsl\((-?\d+),(-?\d+)%?,(-?\d+)%?\)/g, function (match, hue, saturation, lightness) {
return new HSL(hue, saturation, lightness).toHex();
})
.replace(/(^|[^='"])#([0-9a-f]{6})/gi, function (match, prefix, color) {
if (color[0] == color[1] && color[2] == color[3] && color[4] == color[5])
return prefix + '#' + color[0] + color[2] + color[4];
else
return prefix + '#' + color;
})
.replace(/(rgb|rgba|hsl|hsla)\(([^\)]+)\)/g, function (match, colorFunction, colorDef) {
var tokens = colorDef.split(',');
var applies = (colorFunction == 'hsl' && tokens.length == 3) ||
(colorFunction == 'hsla' && tokens.length == 4) ||
(colorFunction == 'rgb' && tokens.length == 3 && colorDef.indexOf('%') > 0) ||
(colorFunction == 'rgba' && tokens.length == 4 && colorDef.indexOf('%') > 0);
if (!applies)
return match;
if (tokens[1].indexOf('%') == -1)
tokens[1] += '%';
if (tokens[2].indexOf('%') == -1)
tokens[2] += '%';
return colorFunction + '(' + tokens.join(',') + ')';
});
if (compatibility.colors.opacity) {
value = value.replace(/(?:rgba|hsla)\(0,0%?,0%?,0\)/g, function (match) {
if (split(value, ',').pop().indexOf('gradient(') > -1)
return match;
return 'transparent';
});
}
return HexNameShortener.shorten(value);
}
function pixelLengthMinifier(_, value, compatibility) {
if (!WHOLE_PIXEL_VALUE.test(value))
return value;
return value.replace(WHOLE_PIXEL_VALUE, function (match, val) {
var newValue;
var intVal = parseInt(val);
if (intVal === 0)
return match;
if (compatibility.properties.shorterLengthUnits && compatibility.units.pt && intVal * 3 % 4 === 0)
newValue = intVal * 3 / 4 + 'pt';
if (compatibility.properties.shorterLengthUnits && compatibility.units.pc && intVal % 16 === 0)
newValue = intVal / 16 + 'pc';
if (compatibility.properties.shorterLengthUnits && compatibility.units.in && intVal % 96 === 0)
newValue = intVal / 96 + 'in';
if (newValue)
newValue = match.substring(0, match.indexOf(val)) + newValue;
return newValue && newValue.length < match.length ? newValue : match;
});
}
function timeUnitMinifier(_, value) {
if (!TIME_VALUE.test(value))
return value;
return value.replace(TIME_VALUE, function (match, val, unit) {
var newValue;
if (unit == 'ms') {
newValue = parseInt(val) / 1000 + 's';
} else if (unit == 's') {
newValue = parseFloat(val) * 1000 + 'ms';
}
return newValue.length < match.length ? newValue : match;
});
}
function minifyBorderRadius(property) {
var values = property.value;
var spliceAt;
if (values.length == 3 && values[1][0] == '/' && values[0][0] == values[2][0])
spliceAt = 1;
else if (values.length == 5 && values[2][0] == '/' && values[0][0] == values[3][0] && values[1][0] == values[4][0])
spliceAt = 2;
else if (values.length == 7 && values[3][0] == '/' && values[0][0] == values[4][0] && values[1][0] == values[5][0] && values[2][0] == values[6][0])
spliceAt = 3;
else if (values.length == 9 && values[4][0] == '/' && values[0][0] == values[5][0] && values[1][0] == values[6][0] && values[2][0] == values[7][0] && values[3][0] == values[8][0])
spliceAt = 4;
if (spliceAt) {
property.value.splice(spliceAt);
property.dirty = true;
}
}
function minifyFilter(property) {
if (property.value.length == 1) {
property.value[0][0] = property.value[0][0].replace(/progid:DXImageTransform\.Microsoft\.(Alpha|Chroma)(\W)/, function (match, filter, suffix) {
return filter.toLowerCase() + suffix;
});
}
property.value[0][0] = property.value[0][0]
.replace(/,(\S)/g, ', $1')
.replace(/ ?= ?/g, '=');
}
function minifyFont(property) {
var values = property.value;
var hasNumeral = FONT_NUMERAL_WEIGHTS.indexOf(values[0][0]) > -1 ||
values[1] && FONT_NUMERAL_WEIGHTS.indexOf(values[1][0]) > -1 ||
values[2] && FONT_NUMERAL_WEIGHTS.indexOf(values[2][0]) > -1;
if (hasNumeral)
return;
if (values[1] == '/')
return;
var normalCount = 0;
if (values[0][0] == 'normal')
normalCount++;
if (values[1] && values[1][0] == 'normal')
normalCount++;
if (values[2] && values[2][0] == 'normal')
normalCount++;
if (normalCount > 1)
return;
var toOptimize;
if (FONT_NAME_WEIGHTS_WITHOUT_NORMAL.indexOf(values[0][0]) > -1)
toOptimize = 0;
else if (values[1] && FONT_NAME_WEIGHTS_WITHOUT_NORMAL.indexOf(values[1][0]) > -1)
toOptimize = 1;
else if (values[2] && FONT_NAME_WEIGHTS_WITHOUT_NORMAL.indexOf(values[2][0]) > -1)
toOptimize = 2;
else if (FONT_NAME_WEIGHTS.indexOf(values[0][0]) > -1)
toOptimize = 0;
else if (values[1] && FONT_NAME_WEIGHTS.indexOf(values[1][0]) > -1)
toOptimize = 1;
else if (values[2] && FONT_NAME_WEIGHTS.indexOf(values[2][0]) > -1)
toOptimize = 2;
if (toOptimize !== undefined) {
property.value[toOptimize][0] = valueMinifiers['font-weight'](values[toOptimize][0]);
property.dirty = true;
}
}
function optimizeBody(properties, options) {
var property, name, value;
var _properties = wrapForOptimizing(properties);
for (var i = 0, l = _properties.length; i < l; i++) {
property = _properties[i];
name = property.name;
if (property.hack && (
(property.hack == 'star' || property.hack == 'underscore') && !options.compatibility.properties.iePrefixHack ||
property.hack == 'backslash' && !options.compatibility.properties.ieSuffixHack ||
property.hack == 'bang' && !options.compatibility.properties.ieBangHack))
property.unused = true;
if (name.indexOf('padding') === 0 && (isNegative(property, 0) || isNegative(property, 1) || isNegative(property, 2) || isNegative(property, 3)))
property.unused = true;
if (property.unused)
continue;
if (property.variable) {
if (property.block)
optimizeBody(property.value[0], options);
continue;
}
for (var j = 0, m = property.value.length; j < m; j++) {
value = property.value[j][0];
if (valueMinifiers[name])
value = valueMinifiers[name](value, j, m);
value = whitespaceMinifier(name, value);
value = precisionMinifier(name, value, options.precision);
value = pixelLengthMinifier(name, value, options.compatibility);
value = timeUnitMinifier(name, value);
value = zeroMinifier(name, value);
if (options.compatibility.properties.zeroUnits) {
value = zeroDegMinifier(name, value);
value = unitMinifier(name, value, options.unitsRegexp);
}
if (options.compatibility.properties.colors)
value = colorMininifier(name, value, options.compatibility);
property.value[j][0] = value;
}
multipleZerosMinifier(property);
if (name.indexOf('border') === 0 && name.indexOf('radius') > 0)
minifyBorderRadius(property);
else if (name == 'filter')
minifyFilter(property);
else if (name == 'font')
minifyFont(property);
}
restoreFromOptimizing(_properties, true);
removeUnused(_properties);
}
function cleanupCharsets(tokens) {
var hasCharset = false;
for (var i = 0, l = tokens.length; i < l; i++) {
var token = tokens[i];
if (token[0] != 'at-rule')
continue;
if (!CHARSET_REGEXP.test(token[1][0]))
continue;
if (hasCharset || token[1][0].indexOf(CHARSET_TOKEN) == -1) {
tokens.splice(i, 1);
i--;
l--;
} else {
hasCharset = true;
tokens.splice(i, 1);
tokens.unshift(['at-rule', [token[1][0].replace(CHARSET_REGEXP, CHARSET_TOKEN)]]);
}
}
}
function buildUnitRegexp(options) {
var units = ['px', 'em', 'ex', 'cm', 'mm', 'in', 'pt', 'pc', '%'];
var otherUnits = ['ch', 'rem', 'vh', 'vm', 'vmax', 'vmin', 'vw'];
otherUnits.forEach(function (unit) {
if (options.compatibility.units[unit])
units.push(unit);
});
return new RegExp('(^|\\s|\\(|,)0(?:' + units.join('|') + ')(\\W|$)', 'g');
}
function buildPrecision(options) {
var precision = {};
precision.value = options.roundingPrecision === undefined ?
DEFAULT_ROUNDING_PRECISION :
options.roundingPrecision;
precision.multiplier = Math.pow(10, precision.value);
precision.regexp = new RegExp('(\\d*\\.\\d{' + (precision.value + 1) + ',})px', 'g');
return precision;
}
function optimize(tokens, options) {
var ie7Hack = options.compatibility.selectors.ie7Hack;
var adjacentSpace = options.compatibility.selectors.adjacentSpace;
var spaceAfterClosingBrace = options.compatibility.properties.spaceAfterClosingBrace;
var mayHaveCharset = false;
options.unitsRegexp = buildUnitRegexp(options);
options.precision = buildPrecision(options);
for (var i = 0, l = tokens.length; i < l; i++) {
var token = tokens[i];
switch (token[0]) {
case 'selector':
token[1] = cleanUpSelectors(token[1], !ie7Hack, adjacentSpace);
optimizeBody(token[2], options);
break;
case 'block':
cleanUpBlock(token[1], spaceAfterClosingBrace);
optimize(token[2], options);
break;
case 'flat-block':
cleanUpBlock(token[1], spaceAfterClosingBrace);
optimizeBody(token[2], options);
break;
case 'at-rule':
cleanUpAtRule(token[1]);
mayHaveCharset = true;
}
if (token[1].length === 0 || (token[2] && token[2].length === 0)) {
tokens.splice(i, 1);
i--;
l--;
}
}
if (mayHaveCharset)
cleanupCharsets(tokens);
}
module.exports = optimize;
@@ -0,0 +1,119 @@
var escapePrefix = '__ESCAPED_';
function trackPrefix(value, context, interestingContent) {
if (!interestingContent && value.indexOf('\n') == -1) {
if (value.indexOf(escapePrefix) === 0) {
return value;
} else {
context.column += value.length;
return;
}
}
var withoutContent = 0;
var split = value.split('\n');
var total = split.length;
var shift = 0;
while (true) {
if (withoutContent == total - 1)
break;
var part = split[withoutContent];
if (/\S/.test(part))
break;
shift += part.length + 1;
withoutContent++;
}
context.line += withoutContent;
context.column = withoutContent > 0 ? 0 : context.column;
context.column += /^(\s)*/.exec(split[withoutContent])[0].length;
return value.substring(shift).trimLeft();
}
function sourceFor(originalMetadata, contextMetadata, context) {
var source = originalMetadata.source || contextMetadata.source;
if (source && context.resolvePath)
return context.resolvePath(contextMetadata.source, source);
return source;
}
function snapshot(data, context, fallbacks) {
var metadata = {
line: context.line,
column: context.column,
source: context.source
};
var sourceContent = null;
var sourceMetadata = context.sourceMapTracker.isTracking(metadata.source) ?
context.sourceMapTracker.originalPositionFor(metadata, data, fallbacks || 0) :
{};
metadata.line = sourceMetadata.line || metadata.line;
metadata.column = sourceMetadata.column || metadata.column;
metadata.source = sourceMetadata.sourceResolved ?
sourceMetadata.source :
sourceFor(sourceMetadata, metadata, context);
if (context.sourceMapInlineSources) {
var sourceMapSourcesContent = context.sourceMapTracker.sourcesContentFor(context.source);
sourceContent = sourceMapSourcesContent && sourceMapSourcesContent[metadata.source] ?
sourceMapSourcesContent :
context.sourceReader.sourceAt(context.source);
}
return sourceContent ?
[metadata.line, metadata.column, metadata.source, sourceContent] :
[metadata.line, metadata.column, metadata.source];
}
function trackSuffix(data, context) {
var parts = data.split('\n');
for (var i = 0, l = parts.length; i < l; i++) {
var part = parts[i];
var cursor = 0;
if (i > 0) {
context.line++;
context.column = 0;
}
while (true) {
var next = part.indexOf(escapePrefix, cursor);
if (next == -1) {
context.column += part.substring(cursor).length;
break;
}
context.column += next - cursor;
cursor += next - cursor;
var escaped = part.substring(next, part.indexOf('__', next + 1) + 2);
var encodedValues = escaped.substring(escaped.indexOf('(') + 1, escaped.indexOf(')')).split(',');
context.line += ~~encodedValues[0];
context.column = (~~encodedValues[0] === 0 ? context.column : 0) + ~~encodedValues[1];
cursor += escaped.length;
}
}
}
function track(data, context, snapshotMetadata, fallbacks) {
var untracked = trackPrefix(data, context, snapshotMetadata);
var metadata = snapshotMetadata ?
snapshot(untracked, context, fallbacks) :
[];
if (untracked)
trackSuffix(untracked, context);
return metadata;
}
module.exports = track;
@@ -0,0 +1,161 @@
var lineBreak = require('os').EOL;
var AT_RULE = 'at-rule';
var PROPERTY_SEPARATOR = ';';
function hasMoreProperties(tokens, index) {
for (var i = index, l = tokens.length; i < l; i++) {
if (typeof tokens[i] != 'string')
return true;
}
return false;
}
function supportsAfterClosingBrace(token) {
return token[0][0] == 'background' || token[0][0] == 'transform' || token[0][0] == 'src';
}
function isVariable(token, valueIndex) {
return token[valueIndex][0].indexOf('var(') === 0;
}
function afterClosingBrace(token, valueIndex) {
return token[valueIndex][0][token[valueIndex][0].length - 1] == ')' || token[valueIndex][0].indexOf('__ESCAPED_URL_CLEAN_CSS') === 0;
}
function afterComma(token, valueIndex) {
return token[valueIndex][0] == ',';
}
function afterSlash(token, valueIndex) {
return token[valueIndex][0] == '/';
}
function beforeComma(token, valueIndex) {
return token[valueIndex + 1] && token[valueIndex + 1][0] == ',';
}
function beforeSlash(token, valueIndex) {
return token[valueIndex + 1] && token[valueIndex + 1][0] == '/';
}
function inFilter(token) {
return token[0][0] == 'filter' || token[0][0] == '-ms-filter';
}
function inSpecialContext(token, valueIndex, context) {
return (!context.spaceAfterClosingBrace && supportsAfterClosingBrace(token) || isVariable(token, valueIndex)) && afterClosingBrace(token, valueIndex) ||
beforeSlash(token, valueIndex) ||
afterSlash(token, valueIndex) ||
beforeComma(token, valueIndex) ||
afterComma(token, valueIndex);
}
function selectors(tokens, context) {
var store = context.store;
for (var i = 0, l = tokens.length; i < l; i++) {
store(tokens[i], context);
if (i < l - 1)
store(',', context);
}
}
function body(tokens, context) {
for (var i = 0, l = tokens.length; i < l; i++) {
property(tokens, i, i == l - 1, context);
}
}
function property(tokens, position, isLast, context) {
var store = context.store;
var token = tokens[position];
if (typeof token == 'string') {
store(token, context);
} else if (token[0] == AT_RULE) {
propertyAtRule(token[1], isLast, context);
} else {
store(token[0], context);
store(':', context);
value(tokens, position, isLast, context);
}
}
function propertyAtRule(value, isLast, context) {
var store = context.store;
store(value, context);
if (!isLast)
store(PROPERTY_SEPARATOR, context);
}
function value(tokens, position, isLast, context) {
var store = context.store;
var token = tokens[position];
var isVariableDeclaration = token[0][0].indexOf('--') === 0;
if (isVariableDeclaration && Array.isArray(token[1][0][0])) {
store('{', context);
body(token[1], context);
store('};', context);
return;
}
for (var j = 1, m = token.length; j < m; j++) {
store(token[j], context);
if (j < m - 1 && (inFilter(token) || !inSpecialContext(token, j, context))) {
store(' ', context);
} else if (j == m - 1 && !isLast && hasMoreProperties(tokens, position + 1)) {
store(PROPERTY_SEPARATOR, context);
}
}
}
function all(tokens, context) {
var joinCharacter = context.keepBreaks ? lineBreak : '';
var store = context.store;
for (var i = 0, l = tokens.length; i < l; i++) {
var token = tokens[i];
switch (token[0]) {
case 'at-rule':
case 'text':
store(token[1][0], context);
store(joinCharacter, context);
break;
case 'block':
selectors([token[1]], context);
store('{', context);
all(token[2], context);
store('}', context);
store(joinCharacter, context);
break;
case 'flat-block':
selectors([token[1]], context);
store('{', context);
body(token[2], context);
store('}', context);
store(joinCharacter, context);
break;
default:
selectors(token[1], context);
store('{', context);
body(token[2], context);
store('}', context);
store(joinCharacter, context);
}
}
}
module.exports = {
all: all,
body: body,
property: property,
selectors: selectors,
value: value
};
@@ -0,0 +1,50 @@
var helpers = require('./helpers');
function store(token, context) {
context.output.push(typeof token == 'string' ? token : token[0]);
}
function context() {
return {
output: [],
store: store
};
}
function all(tokens) {
var fakeContext = context();
helpers.all(tokens, fakeContext);
return fakeContext.output.join('');
}
function body(tokens) {
var fakeContext = context();
helpers.body(tokens, fakeContext);
return fakeContext.output.join('');
}
function property(tokens, position) {
var fakeContext = context();
helpers.property(tokens, position, true, fakeContext);
return fakeContext.output.join('');
}
function selectors(tokens) {
var fakeContext = context();
helpers.selectors(tokens, fakeContext);
return fakeContext.output.join('');
}
function value(tokens, position) {
var fakeContext = context();
helpers.value(tokens, position, true, fakeContext);
return fakeContext.output.join('');
}
module.exports = {
all: all,
body: body,
property: property,
selectors: selectors,
value: value
};
@@ -0,0 +1,22 @@
var all = require('./helpers').all;
function store(token, context) {
context.output.push(typeof token == 'string' ? token : token[0]);
}
function stringify(tokens, options, restoreCallback) {
var context = {
keepBreaks: options.keepBreaks,
output: [],
spaceAfterClosingBrace: options.compatibility.properties.spaceAfterClosingBrace,
store: store
};
all(tokens, context, false);
return {
styles: restoreCallback(context.output.join('')).trim()
};
}
module.exports = stringify;
@@ -0,0 +1,96 @@
var SourceMapGenerator = require('source-map').SourceMapGenerator;
var all = require('./helpers').all;
var isWindows = process.platform == 'win32';
var unknownSource = '$stdin';
function store(element, context) {
var fromString = typeof element == 'string';
var value = fromString ? element : element[0];
if (value.indexOf('_') > -1)
value = context.restore(value, prefixContentFrom(context.output));
track(value, fromString ? null : element, context);
context.output.push(value);
}
function prefixContentFrom(values) {
var content = [];
for (var i = values.length - 1; i >= 0; i--) {
var value = values[i];
content.unshift(value);
if (value == '{' || value == ';')
break;
}
return content.join('');
}
function track(value, element, context) {
if (element)
trackAllMappings(element, context);
var parts = value.split('\n');
context.line += parts.length - 1;
context.column = parts.length > 1 ? 0 : (context.column + parts.pop().length);
}
function trackAllMappings(element, context) {
var mapping = element[element.length - 1];
if (!Array.isArray(mapping))
return;
for (var i = 0, l = mapping.length; i < l; i++) {
trackMapping(mapping[i], context);
}
}
function trackMapping(mapping, context) {
var source = mapping[2] || unknownSource;
if (isWindows)
source = source.replace(/\\/g, '/');
context.outputMap.addMapping({
generated: {
line: context.line,
column: context.column
},
source: source,
original: {
line: mapping[0],
column: mapping[1]
}
});
if (mapping[3])
context.outputMap.setSourceContent(source, mapping[3][mapping[2]]);
}
function stringify(tokens, options, restoreCallback, inputMapTracker) {
var context = {
column: 0,
inputMapTracker: inputMapTracker,
keepBreaks: options.keepBreaks,
line: 1,
output: [],
outputMap: new SourceMapGenerator(),
restore: restoreCallback,
sourceMapInlineSources: options.sourceMapInlineSources,
spaceAfterClosingBrace: options.compatibility.properties.spaceAfterClosingBrace,
store: store
};
all(tokens, context, false);
return {
sourceMap: context.outputMap,
styles: context.output.join('').trim()
};
}
module.exports = stringify;
@@ -0,0 +1,131 @@
var EscapeStore = require('./escape-store');
var QuoteScanner = require('../utils/quote-scanner');
var SPECIAL_COMMENT_PREFIX = '/*!';
var COMMENT_PREFIX = '/*';
var COMMENT_SUFFIX = '*/';
var lineBreak = require('os').EOL;
function CommentsProcessor(context, keepSpecialComments, keepBreaks, saveWaypoints) {
this.comments = new EscapeStore('COMMENT');
this.specialComments = new EscapeStore('COMMENT_SPECIAL');
this.context = context;
this.restored = 0;
this.keepAll = keepSpecialComments == '*';
this.keepOne = keepSpecialComments == '1' || keepSpecialComments === 1;
this.keepBreaks = keepBreaks;
this.saveWaypoints = saveWaypoints;
}
function quoteScannerFor(data) {
var quoteMap = [];
new QuoteScanner(data).each(function (quotedString, _, startsAt) {
quoteMap.push([startsAt, startsAt + quotedString.length]);
});
return function (position) {
for (var i = 0, l = quoteMap.length; i < l; i++) {
if (quoteMap[i][0] < position && quoteMap[i][1] > position)
return true;
}
return false;
};
}
CommentsProcessor.prototype.escape = function (data) {
var tempData = [];
var nextStart = 0;
var nextEnd = 0;
var cursor = 0;
var indent = 0;
var breaksCount;
var lastBreakAt;
var newIndent;
var isQuotedAt = quoteScannerFor(data);
var saveWaypoints = this.saveWaypoints;
for (; nextEnd < data.length;) {
nextStart = data.indexOf(COMMENT_PREFIX, cursor);
if (nextStart == -1)
break;
if (isQuotedAt(nextStart)) {
tempData.push(data.substring(cursor, nextStart + COMMENT_PREFIX.length));
cursor = nextStart + COMMENT_PREFIX.length;
continue;
}
nextEnd = data.indexOf(COMMENT_SUFFIX, nextStart + COMMENT_PREFIX.length);
if (nextEnd == -1) {
this.context.warnings.push('Broken comment: \'' + data.substring(nextStart) + '\'.');
nextEnd = data.length - 2;
}
tempData.push(data.substring(cursor, nextStart));
var comment = data.substring(nextStart, nextEnd + COMMENT_SUFFIX.length);
var isSpecialComment = comment.indexOf(SPECIAL_COMMENT_PREFIX) === 0;
if (saveWaypoints) {
breaksCount = comment.split(lineBreak).length - 1;
lastBreakAt = comment.lastIndexOf(lineBreak);
newIndent = lastBreakAt > 0 ?
comment.substring(lastBreakAt + lineBreak.length).length :
indent + comment.length;
}
if (saveWaypoints || isSpecialComment) {
var metadata = saveWaypoints ? [breaksCount, newIndent] : null;
var placeholder = isSpecialComment ?
this.specialComments.store(comment, metadata) :
this.comments.store(comment, metadata);
tempData.push(placeholder);
}
if (saveWaypoints)
indent = newIndent + 1;
cursor = nextEnd + COMMENT_SUFFIX.length;
}
return tempData.length > 0 ?
tempData.join('') + data.substring(cursor, data.length) :
data;
};
function restore(context, data, from, isSpecial) {
var tempData = [];
var cursor = 0;
for (; cursor < data.length;) {
var nextMatch = from.nextMatch(data, cursor);
if (nextMatch.start < 0)
break;
tempData.push(data.substring(cursor, nextMatch.start));
var comment = from.restore(nextMatch.match);
if (isSpecial && (context.keepAll || (context.keepOne && context.restored === 0))) {
context.restored++;
tempData.push(comment);
cursor = nextMatch.end;
} else {
cursor = nextMatch.end + (context.keepBreaks && data.substring(nextMatch.end, nextMatch.end + lineBreak.length) == lineBreak ? lineBreak.length : 0);
}
}
return tempData.length > 0 ?
tempData.join('') + data.substring(cursor, data.length) :
data;
}
CommentsProcessor.prototype.restore = function (data) {
data = restore(this, data, this.comments, false);
data = restore(this, data, this.specialComments, true);
return data;
};
module.exports = CommentsProcessor;
@@ -0,0 +1,53 @@
var placeholderBrace = '__';
function EscapeStore(placeholderRoot) {
this.placeholderRoot = 'ESCAPED_' + placeholderRoot + '_CLEAN_CSS';
this.placeholderToData = {};
this.dataToPlaceholder = {};
this.count = 0;
this.restoreMatcher = new RegExp(this.placeholderRoot + '(\\d+)');
}
EscapeStore.prototype._nextPlaceholder = function (metadata) {
return {
index: this.count,
value: placeholderBrace + this.placeholderRoot + this.count++ + metadata + placeholderBrace
};
};
EscapeStore.prototype.store = function (data, metadata) {
var encodedMetadata = metadata ?
'(' + metadata.join(',') + ')' :
'';
var placeholder = this.dataToPlaceholder[data];
if (!placeholder) {
var nextPlaceholder = this._nextPlaceholder(encodedMetadata);
placeholder = nextPlaceholder.value;
this.placeholderToData[nextPlaceholder.index] = data;
this.dataToPlaceholder[data] = nextPlaceholder.value;
}
if (metadata)
placeholder = placeholder.replace(/\([^\)]+\)/, encodedMetadata);
return placeholder;
};
EscapeStore.prototype.nextMatch = function (data, cursor) {
var next = {};
next.start = data.indexOf(this.placeholderRoot, cursor) - placeholderBrace.length;
next.end = data.indexOf(placeholderBrace, next.start + placeholderBrace.length) + placeholderBrace.length;
if (next.start > -1 && next.end > -1)
next.match = data.substring(next.start, next.end);
return next;
};
EscapeStore.prototype.restore = function (placeholder) {
var index = this.restoreMatcher.exec(placeholder)[1];
return this.placeholderToData[index];
};
module.exports = EscapeStore;
@@ -0,0 +1,117 @@
var EscapeStore = require('./escape-store');
var EXPRESSION_NAME = 'expression';
var EXPRESSION_START = '(';
var EXPRESSION_END = ')';
var EXPRESSION_PREFIX = EXPRESSION_NAME + EXPRESSION_START;
var BODY_START = '{';
var BODY_END = '}';
var lineBreak = require('os').EOL;
function findEnd(data, start) {
var end = start + EXPRESSION_NAME.length;
var level = 0;
var quoted = false;
var braced = false;
while (true) {
var current = data[end++];
if (quoted) {
quoted = current != '\'' && current != '"';
} else {
quoted = current == '\'' || current == '"';
if (current == EXPRESSION_START)
level++;
if (current == EXPRESSION_END)
level--;
if (current == BODY_START)
braced = true;
if (current == BODY_END && !braced && level == 1) {
end--;
level--;
}
}
if (level === 0 && current == EXPRESSION_END)
break;
if (!current) {
end = data.substring(0, end).lastIndexOf(BODY_END);
break;
}
}
return end;
}
function ExpressionsProcessor(saveWaypoints) {
this.expressions = new EscapeStore('EXPRESSION');
this.saveWaypoints = saveWaypoints;
}
ExpressionsProcessor.prototype.escape = function (data) {
var nextStart = 0;
var nextEnd = 0;
var cursor = 0;
var tempData = [];
var indent = 0;
var breaksCount;
var lastBreakAt;
var newIndent;
var saveWaypoints = this.saveWaypoints;
for (; nextEnd < data.length;) {
nextStart = data.indexOf(EXPRESSION_PREFIX, nextEnd);
if (nextStart == -1)
break;
nextEnd = findEnd(data, nextStart);
var expression = data.substring(nextStart, nextEnd);
if (saveWaypoints) {
breaksCount = expression.split(lineBreak).length - 1;
lastBreakAt = expression.lastIndexOf(lineBreak);
newIndent = lastBreakAt > 0 ?
expression.substring(lastBreakAt + lineBreak.length).length :
indent + expression.length;
}
var metadata = saveWaypoints ? [breaksCount, newIndent] : null;
var placeholder = this.expressions.store(expression, metadata);
tempData.push(data.substring(cursor, nextStart));
tempData.push(placeholder);
if (saveWaypoints)
indent = newIndent + 1;
cursor = nextEnd;
}
return tempData.length > 0 ?
tempData.join('') + data.substring(cursor, data.length) :
data;
};
ExpressionsProcessor.prototype.restore = function (data) {
var tempData = [];
var cursor = 0;
for (; cursor < data.length;) {
var nextMatch = this.expressions.nextMatch(data, cursor);
if (nextMatch.start < 0)
break;
tempData.push(data.substring(cursor, nextMatch.start));
var comment = this.expressions.restore(nextMatch.match);
tempData.push(comment);
cursor = nextMatch.end;
}
return tempData.length > 0 ?
tempData.join('') + data.substring(cursor, data.length) :
data;
};
module.exports = ExpressionsProcessor;
@@ -0,0 +1,98 @@
var EscapeStore = require('./escape-store');
var QuoteScanner = require('../utils/quote-scanner');
var lineBreak = require('os').EOL;
function FreeTextProcessor(saveWaypoints) {
this.matches = new EscapeStore('FREE_TEXT');
this.saveWaypoints = saveWaypoints;
}
// Strip content tags by replacing them by the a special
// marker for further restoring. It's done via string scanning
// instead of regexps to speed up the process.
FreeTextProcessor.prototype.escape = function (data) {
var self = this;
var breaksCount;
var lastBreakAt;
var indent;
var metadata;
var saveWaypoints = this.saveWaypoints;
return new QuoteScanner(data).each(function (match, store) {
if (saveWaypoints) {
breaksCount = match.split(lineBreak).length - 1;
lastBreakAt = match.lastIndexOf(lineBreak);
indent = lastBreakAt > 0 ?
match.substring(lastBreakAt + lineBreak.length).length :
match.length;
metadata = [breaksCount, indent];
}
var placeholder = self.matches.store(match, metadata);
store.push(placeholder);
});
};
function normalize(text, data, prefixContext, cursor) {
// FIXME: this is even a bigger hack now - see #407
var searchIn = data;
if (prefixContext) {
searchIn = prefixContext + data.substring(0, data.indexOf('__ESCAPED_FREE_TEXT_CLEAN_CSS'));
cursor = searchIn.length;
}
var lastSemicolon = searchIn.lastIndexOf(';', cursor);
var lastOpenBrace = searchIn.lastIndexOf('{', cursor);
var lastOne = 0;
if (lastSemicolon > -1 && lastOpenBrace > -1)
lastOne = Math.max(lastSemicolon, lastOpenBrace);
else if (lastSemicolon == -1)
lastOne = lastOpenBrace;
else
lastOne = lastSemicolon;
var context = searchIn.substring(lastOne + 1, cursor);
if (/\[[\w\d\-]+[\*\|\~\^\$]?=$/.test(context)) {
text = text
.replace(/\\\n|\\\r\n/g, '')
.replace(/\n|\r\n/g, '');
}
if (/^['"][a-zA-Z][a-zA-Z\d\-_]+['"]$/.test(text) && !/format\($/.test(context)) {
var isFont = /^(font|font\-family):/.test(context);
var isAttribute = /\[[\w\d\-]+[\*\|\~\^\$]?=$/.test(context);
var isKeyframe = /@(-moz-|-o-|-webkit-)?keyframes /.test(context);
var isAnimation = /^(-moz-|-o-|-webkit-)?animation(-name)?:/.test(context);
if (isFont || isAttribute || isKeyframe || isAnimation)
text = text.substring(1, text.length - 1);
}
return text;
}
FreeTextProcessor.prototype.restore = function (data, prefixContext) {
var tempData = [];
var cursor = 0;
for (; cursor < data.length;) {
var nextMatch = this.matches.nextMatch(data, cursor);
if (nextMatch.start < 0)
break;
tempData.push(data.substring(cursor, nextMatch.start));
var text = normalize(this.matches.restore(nextMatch.match), data, prefixContext, nextMatch.start);
tempData.push(text);
cursor = nextMatch.end;
}
return tempData.length > 0 ?
tempData.join('') + data.substring(cursor, data.length) :
data;
};
module.exports = FreeTextProcessor;
@@ -0,0 +1,72 @@
var EscapeStore = require('./escape-store');
var reduceUrls = require('../urls/reduce');
var lineBreak = require('os').EOL;
function UrlsProcessor(context, saveWaypoints, keepUrlQuotes) {
this.urls = new EscapeStore('URL');
this.context = context;
this.saveWaypoints = saveWaypoints;
this.keepUrlQuotes = keepUrlQuotes;
}
// Strip urls by replacing them by a special
// marker for further restoring. It's done via string scanning
// instead of regexps to speed up the process.
UrlsProcessor.prototype.escape = function (data) {
var breaksCount;
var lastBreakAt;
var indent;
var saveWaypoints = this.saveWaypoints;
var self = this;
return reduceUrls(data, this.context, function (url, tempData) {
if (saveWaypoints) {
breaksCount = url.split(lineBreak).length - 1;
lastBreakAt = url.lastIndexOf(lineBreak);
indent = lastBreakAt > 0 ?
url.substring(lastBreakAt + lineBreak.length).length :
url.length;
}
var placeholder = self.urls.store(url, saveWaypoints ? [breaksCount, indent] : null);
tempData.push(placeholder);
});
};
function normalize(url, keepUrlQuotes) {
url = url
.replace(/^url/gi, 'url')
.replace(/\\?\n|\\?\r\n/g, '')
.replace(/(\s{2,}|\s)/g, ' ')
.replace(/^url\((['"])? /, 'url($1')
.replace(/ (['"])?\)$/, '$1)');
if (!keepUrlQuotes && !/^['"].+['"]$/.test(url) && !/url\(.*[\s\(\)].*\)/.test(url) && !/url\(['"]data:[^;]+;charset/.test(url))
url = url.replace(/["']/g, '');
return url;
}
UrlsProcessor.prototype.restore = function (data) {
var tempData = [];
var cursor = 0;
for (; cursor < data.length;) {
var nextMatch = this.urls.nextMatch(data, cursor);
if (nextMatch.start < 0)
break;
tempData.push(data.substring(cursor, nextMatch.start));
var url = normalize(this.urls.restore(nextMatch.match), this.keepUrlQuotes);
tempData.push(url);
cursor = nextMatch.end;
}
return tempData.length > 0 ?
tempData.join('') + data.substring(cursor, data.length) :
data;
};
module.exports = UrlsProcessor;
@@ -0,0 +1,188 @@
var split = require('../utils/split');
var COMMA = ',';
var FORWARD_SLASH = '/';
var AT_RULE = 'at-rule';
function selectorName(value) {
return value[0];
}
function noop() {}
function withoutComments(string, into, heading, context) {
var matcher = heading ? /^__ESCAPED_COMMENT_/ : /__ESCAPED_COMMENT_/;
var track = heading ? context.track : noop; // don't track when comment not in a heading as we do it later in `trackComments`
while (matcher.test(string)) {
var startOfComment = string.indexOf('__');
var endOfComment = string.indexOf('__', startOfComment + 1) + 2;
var comment = string.substring(startOfComment, endOfComment);
string = string.substring(0, startOfComment) + string.substring(endOfComment);
track(comment);
into.push(comment);
}
return string;
}
function withoutHeadingComments(string, into, context) {
return withoutComments(string, into, true, context);
}
function withoutInnerComments(string, into, context) {
return withoutComments(string, into, false, context);
}
function trackComments(comments, into, context) {
for (var i = 0, l = comments.length; i < l; i++) {
context.track(comments[i]);
into.push(comments[i]);
}
}
function extractProperties(string, selectors, context) {
var list = [];
var innerComments = [];
var valueSeparator = /[ ,\/]/;
if (typeof string != 'string')
return [];
if (string.indexOf(')') > -1)
string = string.replace(/\)([^\s_;:,\)])/g, context.sourceMap ? ') __ESCAPED_COMMENT_CLEAN_CSS(0,-1)__ $1' : ') $1');
if (string.indexOf('ESCAPED_URL_CLEAN_CSS') > -1)
string = string.replace(/(ESCAPED_URL_CLEAN_CSS[^_]+?__)/g, context.sourceMap ? '$1 __ESCAPED_COMMENT_CLEAN_CSS(0,-1)__ ' : '$1 ');
var candidates = split(string, ';', false, '{', '}');
for (var i = 0, l = candidates.length; i < l; i++) {
var candidate = candidates[i];
var firstColonAt = candidate.indexOf(':');
var atRule = candidate.trim()[0] == '@';
if (atRule) {
context.track(candidate);
list.push([AT_RULE, candidate.trim()]);
continue;
}
if (firstColonAt == -1) {
context.track(candidate);
if (candidate.indexOf('__ESCAPED_COMMENT_SPECIAL') > -1)
list.push(candidate.trim());
continue;
}
if (candidate.indexOf('{') > 0 && candidate.indexOf('{') < firstColonAt) {
context.track(candidate);
continue;
}
var body = [];
var name = candidate.substring(0, firstColonAt);
innerComments = [];
if (name.indexOf('__ESCAPED_COMMENT') > -1)
name = withoutHeadingComments(name, list, context);
if (name.indexOf('__ESCAPED_COMMENT') > -1)
name = withoutInnerComments(name, innerComments, context);
body.push([name.trim()].concat(context.track(name, true)));
context.track(':');
trackComments(innerComments, list, context);
var firstBraceAt = candidate.indexOf('{');
var isVariable = name.trim().indexOf('--') === 0;
if (isVariable && firstBraceAt > 0) {
var blockPrefix = candidate.substring(firstColonAt + 1, firstBraceAt + 1);
var blockSuffix = candidate.substring(candidate.indexOf('}'));
var blockContent = candidate.substring(firstBraceAt + 1, candidate.length - blockSuffix.length);
context.track(blockPrefix);
body.push(extractProperties(blockContent, selectors, context));
list.push(body);
context.track(blockSuffix);
context.track(i < l - 1 ? ';' : '');
continue;
}
var values = split(candidate.substring(firstColonAt + 1), valueSeparator, true);
if (values.length == 1 && values[0] === '') {
context.warnings.push('Empty property \'' + name + '\' inside \'' + selectors.filter(selectorName).join(',') + '\' selector. Ignoring.');
continue;
}
for (var j = 0, m = values.length; j < m; j++) {
var value = values[j];
var trimmed = value.trim();
if (trimmed.length === 0)
continue;
var lastCharacter = trimmed[trimmed.length - 1];
var endsWithNonSpaceSeparator = trimmed.length > 1 && (lastCharacter == COMMA || lastCharacter == FORWARD_SLASH);
if (endsWithNonSpaceSeparator)
trimmed = trimmed.substring(0, trimmed.length - 1);
if (trimmed.indexOf('__ESCAPED_COMMENT_CLEAN_CSS(0,-') > -1) {
context.track(trimmed);
continue;
}
innerComments = [];
if (trimmed.indexOf('__ESCAPED_COMMENT') > -1)
trimmed = withoutHeadingComments(trimmed, list, context);
if (trimmed.indexOf('__ESCAPED_COMMENT') > -1)
trimmed = withoutInnerComments(trimmed, innerComments, context);
if (trimmed.length === 0) {
trackComments(innerComments, list, context);
continue;
}
var pos = body.length - 1;
if (trimmed == 'important' && body[pos][0] == '!') {
context.track(trimmed);
body[pos - 1][0] += '!important';
body.pop();
continue;
}
if (trimmed == '!important' || (trimmed == 'important' && body[pos][0][body[pos][0].length - 1] == '!')) {
context.track(trimmed);
body[pos][0] += trimmed;
continue;
}
body.push([trimmed].concat(context.track(value, true)));
trackComments(innerComments, list, context);
if (endsWithNonSpaceSeparator) {
body.push([lastCharacter]);
context.track(lastCharacter);
}
}
if (i < l - 1)
context.track(';');
list.push(body);
}
return list;
}
module.exports = extractProperties;
@@ -0,0 +1,17 @@
var split = require('../utils/split');
function extractSelectors(string, context) {
var list = [];
var metadata;
var selectors = split(string, ',');
for (var i = 0, l = selectors.length; i < l; i++) {
metadata = context.track(selectors[i], true, i);
context.track(',');
list.push([selectors[i].trim()].concat(metadata));
}
return list;
}
module.exports = extractSelectors;
@@ -0,0 +1,264 @@
var extractProperties = require('./extract-properties');
var extractSelectors = require('./extract-selectors');
var track = require('../source-maps/track');
var split = require('../utils/split');
var path = require('path');
var flatBlock = /(@(font\-face|page|\-ms\-viewport|\-o\-viewport|viewport|counter\-style)|\\@.+?)/;
function tokenize(data, outerContext) {
var chunks = split(normalize(data), '}', true, '{', '}');
if (chunks.length === 0)
return [];
var context = {
chunk: chunks.shift(),
chunks: chunks,
column: 0,
cursor: 0,
line: 1,
mode: 'top',
resolvePath: outerContext.options.explicitTarget ?
relativePathResolver(outerContext.options.root, outerContext.options.target) :
null,
source: undefined,
sourceMap: outerContext.options.sourceMap,
sourceMapInlineSources: outerContext.options.sourceMapInlineSources,
sourceMapTracker: outerContext.inputSourceMapTracker,
sourceReader: outerContext.sourceReader,
sourceTracker: outerContext.sourceTracker,
state: [],
track: outerContext.options.sourceMap ?
function (data, snapshotMetadata, fallbacks) { return [[track(data, context, snapshotMetadata, fallbacks)]]; } :
function () { return []; },
warnings: outerContext.warnings
};
return intoTokens(context);
}
function normalize(data) {
return data.replace(/\r\n/g, '\n');
}
function relativePathResolver(root, target) {
var rebaseTo = path.relative(root, target);
return function (relativeTo, sourcePath) {
return relativeTo != sourcePath ?
path.normalize(path.join(path.relative(rebaseTo, path.dirname(relativeTo)), sourcePath)) :
sourcePath;
};
}
function whatsNext(context) {
var mode = context.mode;
var chunk = context.chunk;
var closest;
if (chunk.length == context.cursor) {
if (context.chunks.length === 0)
return null;
context.chunk = chunk = context.chunks.shift();
context.cursor = 0;
}
if (mode == 'body') {
if (chunk[context.cursor] == '}')
return [context.cursor, 'bodyEnd'];
if (chunk.indexOf('}', context.cursor) == -1)
return null;
closest = context.cursor + split(chunk.substring(context.cursor - 1), '}', true, '{', '}')[0].length - 2;
return [closest, 'bodyEnd'];
}
var nextSpecial = chunk.indexOf('@', context.cursor);
var nextEscape = chunk.indexOf('__ESCAPED_', context.cursor);
var nextBodyStart = chunk.indexOf('{', context.cursor);
var nextBodyEnd = chunk.indexOf('}', context.cursor);
if (nextEscape > -1 && /\S/.test(chunk.substring(context.cursor, nextEscape)))
nextEscape = -1;
closest = nextSpecial;
if (closest == -1 || (nextEscape > -1 && nextEscape < closest))
closest = nextEscape;
if (closest == -1 || (nextBodyStart > -1 && nextBodyStart < closest))
closest = nextBodyStart;
if (closest == -1 || (nextBodyEnd > -1 && nextBodyEnd < closest))
closest = nextBodyEnd;
if (closest == -1)
return;
if (nextEscape === closest)
return [closest, 'escape'];
if (nextBodyStart === closest)
return [closest, 'bodyStart'];
if (nextBodyEnd === closest)
return [closest, 'bodyEnd'];
if (nextSpecial === closest)
return [closest, 'special'];
}
function intoTokens(context) {
var chunk = context.chunk;
var tokenized = [];
var newToken;
var value;
while (true) {
var next = whatsNext(context);
if (!next) {
var whatsLeft = context.chunk.substring(context.cursor);
if (whatsLeft.trim().length > 0) {
if (context.mode == 'body') {
context.warnings.push('Missing \'}\' after \'' + whatsLeft + '\'. Ignoring.');
} else {
tokenized.push(['text', [whatsLeft]]);
}
context.cursor += whatsLeft.length;
}
break;
}
var nextSpecial = next[0];
var what = next[1];
var nextEnd;
var oldMode;
chunk = context.chunk;
if (context.cursor != nextSpecial && what != 'bodyEnd') {
var spacing = chunk.substring(context.cursor, nextSpecial);
var leadingWhitespace = /^\s+/.exec(spacing);
if (leadingWhitespace) {
context.cursor += leadingWhitespace[0].length;
context.track(leadingWhitespace[0]);
}
}
if (what == 'special') {
var firstOpenBraceAt = chunk.indexOf('{', nextSpecial);
var firstSemicolonAt = chunk.indexOf(';', nextSpecial);
var isSingle = firstSemicolonAt > -1 && (firstOpenBraceAt == -1 || firstSemicolonAt < firstOpenBraceAt);
var isBroken = firstOpenBraceAt == -1 && firstSemicolonAt == -1;
if (isBroken) {
context.warnings.push('Broken declaration: \'' + chunk.substring(context.cursor) + '\'.');
context.cursor = chunk.length;
} else if (isSingle) {
nextEnd = chunk.indexOf(';', nextSpecial + 1);
value = chunk.substring(context.cursor, nextEnd + 1);
tokenized.push([
'at-rule',
[value].concat(context.track(value, true))
]);
context.track(';');
context.cursor = nextEnd + 1;
} else {
nextEnd = chunk.indexOf('{', nextSpecial + 1);
value = chunk.substring(context.cursor, nextEnd);
var trimmedValue = value.trim();
var isFlat = flatBlock.test(trimmedValue);
oldMode = context.mode;
context.cursor = nextEnd + 1;
context.mode = isFlat ? 'body' : 'block';
newToken = [
isFlat ? 'flat-block' : 'block'
];
newToken.push([trimmedValue].concat(context.track(value, true)));
context.track('{');
newToken.push(intoTokens(context));
if (typeof newToken[2] == 'string')
newToken[2] = extractProperties(newToken[2], [[trimmedValue]], context);
context.mode = oldMode;
context.track('}');
tokenized.push(newToken);
}
} else if (what == 'escape') {
nextEnd = chunk.indexOf('__', nextSpecial + 1);
var escaped = chunk.substring(context.cursor, nextEnd + 2);
var isStartSourceMarker = !!context.sourceTracker.nextStart(escaped);
var isEndSourceMarker = !!context.sourceTracker.nextEnd(escaped);
if (isStartSourceMarker) {
context.track(escaped);
context.state.push({
source: context.source,
line: context.line,
column: context.column
});
context.source = context.sourceTracker.nextStart(escaped).filename;
context.line = 1;
context.column = 0;
} else if (isEndSourceMarker) {
var oldState = context.state.pop();
context.source = oldState.source;
context.line = oldState.line;
context.column = oldState.column;
context.track(escaped);
} else {
if (escaped.indexOf('__ESCAPED_COMMENT_SPECIAL') === 0)
tokenized.push(['text', [escaped]]);
context.track(escaped);
}
context.cursor = nextEnd + 2;
} else if (what == 'bodyStart') {
var selectors = extractSelectors(chunk.substring(context.cursor, nextSpecial), context);
oldMode = context.mode;
context.cursor = nextSpecial + 1;
context.mode = 'body';
var body = extractProperties(intoTokens(context), selectors, context);
context.track('{');
context.mode = oldMode;
tokenized.push([
'selector',
selectors,
body
]);
} else if (what == 'bodyEnd') {
// extra closing brace at the top level can be safely ignored
if (context.mode == 'top') {
var at = context.cursor;
var warning = chunk[context.cursor] == '}' ?
'Unexpected \'}\' in \'' + chunk.substring(at - 20, at + 20) + '\'. Ignoring.' :
'Unexpected content: \'' + chunk.substring(at, nextSpecial + 1) + '\'. Ignoring.';
context.warnings.push(warning);
context.cursor = nextSpecial + 1;
continue;
}
if (context.mode == 'block')
context.track(chunk.substring(context.cursor, nextSpecial));
if (context.mode != 'block')
tokenized = chunk.substring(context.cursor, nextSpecial);
context.cursor = nextSpecial + 1;
break;
}
}
return tokenized;
}
module.exports = tokenize;
@@ -0,0 +1,30 @@
var path = require('path');
var rewriteUrls = require('./rewrite');
function rebaseUrls(data, context) {
var rebaseOpts = {
absolute: context.options.explicitRoot,
relative: !context.options.explicitRoot && context.options.explicitTarget,
fromBase: context.options.relativeTo
};
if (!rebaseOpts.absolute && !rebaseOpts.relative)
return data;
if (rebaseOpts.absolute && context.options.explicitTarget)
context.warnings.push('Both \'root\' and output file given so rebasing URLs as absolute paths');
if (rebaseOpts.absolute)
rebaseOpts.toBase = path.resolve(context.options.root);
if (rebaseOpts.relative)
rebaseOpts.toBase = path.resolve(context.options.target);
if (!rebaseOpts.fromBase || !rebaseOpts.toBase)
return data;
return rewriteUrls(data, rebaseOpts, context);
}
module.exports = rebaseUrls;
@@ -0,0 +1,152 @@
var URL_PREFIX = 'url(';
var UPPERCASE_URL_PREFIX = 'URL(';
var URL_SUFFIX = ')';
var DATA_URI_PREFIX = 'data:';
var IMPORT_URL_PREFIX = '@import';
var UPPERCASE_IMPORT_URL_PREFIX = '@IMPORT';
var COMMENT_END_MARKER = /\*\//;
function byUrl(data, context, callback) {
var nextStart = 0;
var nextStartUpperCase = 0;
var nextEnd = 0;
var nextEndAhead = 0;
var isDataURI = false;
var cursor = 0;
var tempData = [];
var hasUppercaseUrl = data.indexOf(UPPERCASE_URL_PREFIX) > -1;
for (; nextEnd < data.length;) {
nextStart = data.indexOf(URL_PREFIX, nextEnd);
nextStartUpperCase = hasUppercaseUrl ? data.indexOf(UPPERCASE_URL_PREFIX, nextEnd) : -1;
if (nextStart == -1 && nextStartUpperCase == -1)
break;
if (nextStart == -1 && nextStartUpperCase > -1)
nextStart = nextStartUpperCase;
if (data[nextStart + URL_PREFIX.length] == '"') {
nextEnd = data.indexOf('"', nextStart + URL_PREFIX.length + 1);
} else if (data[nextStart + URL_PREFIX.length] == '\'') {
nextEnd = data.indexOf('\'', nextStart + URL_PREFIX.length + 1);
} else {
isDataURI = data.substring(nextStart + URL_PREFIX.length).trim().indexOf(DATA_URI_PREFIX) === 0;
nextEnd = data.indexOf(URL_SUFFIX, nextStart);
if (isDataURI) {
// this is a fuzzy matching logic for unqoted data URIs
while (true) {
nextEndAhead = data.indexOf(URL_SUFFIX, nextEnd + 1);
// if it has whitespace then we should be out of URL, otherwise keep iterating
// if it has not but content is not escaped, it has to be quoted so it will be captured
// by either of two clauses above
if (nextEndAhead == -1 || /\s/.test(data.substring(nextEnd, nextEndAhead)))
break;
nextEnd = nextEndAhead;
}
}
}
// Following lines are a safety mechanism to ensure
// incorrectly terminated urls are processed correctly.
if (nextEnd == -1) {
nextEnd = data.indexOf('}', nextStart);
if (nextEnd == -1)
nextEnd = data.length;
else
nextEnd--;
context.warnings.push('Broken URL declaration: \'' + data.substring(nextStart, nextEnd + 1) + '\'.');
} else {
if (data[nextEnd] != URL_SUFFIX)
nextEnd = data.indexOf(URL_SUFFIX, nextEnd);
}
tempData.push(data.substring(cursor, nextStart));
var url = data.substring(nextStart, nextEnd + 1);
callback(url, tempData);
cursor = nextEnd + 1;
}
return tempData.length > 0 ?
tempData.join('') + data.substring(cursor, data.length) :
data;
}
function byImport(data, context, callback) {
var nextImport = 0;
var nextImportUpperCase = 0;
var nextStart = 0;
var nextEnd = 0;
var cursor = 0;
var tempData = [];
var nextSingleQuote = 0;
var nextDoubleQuote = 0;
var untilNextQuote;
var withQuote;
var SINGLE_QUOTE = '\'';
var DOUBLE_QUOTE = '"';
for (; nextEnd < data.length;) {
nextImport = data.indexOf(IMPORT_URL_PREFIX, nextEnd);
nextImportUpperCase = data.indexOf(UPPERCASE_IMPORT_URL_PREFIX, nextEnd);
if (nextImport == -1 && nextImportUpperCase == -1)
break;
if (nextImport > -1 && nextImportUpperCase > -1 && nextImportUpperCase < nextImport)
nextImport = nextImportUpperCase;
nextSingleQuote = data.indexOf(SINGLE_QUOTE, nextImport);
nextDoubleQuote = data.indexOf(DOUBLE_QUOTE, nextImport);
if (nextSingleQuote > -1 && nextDoubleQuote > -1 && nextSingleQuote < nextDoubleQuote) {
nextStart = nextSingleQuote;
withQuote = SINGLE_QUOTE;
} else if (nextSingleQuote > -1 && nextDoubleQuote > -1 && nextSingleQuote > nextDoubleQuote) {
nextStart = nextDoubleQuote;
withQuote = DOUBLE_QUOTE;
} else if (nextSingleQuote > -1) {
nextStart = nextSingleQuote;
withQuote = SINGLE_QUOTE;
} else if (nextDoubleQuote > -1) {
nextStart = nextDoubleQuote;
withQuote = DOUBLE_QUOTE;
} else {
break;
}
tempData.push(data.substring(cursor, nextStart));
nextEnd = data.indexOf(withQuote, nextStart + 1);
untilNextQuote = data.substring(nextImport, nextEnd);
if (nextEnd == -1 || /^@import\s+(url\(|__ESCAPED)/i.test(untilNextQuote) || COMMENT_END_MARKER.test(untilNextQuote)) {
cursor = nextStart;
break;
}
var url = data.substring(nextStart, nextEnd + 1);
callback(url, tempData);
cursor = nextEnd + 1;
}
return tempData.length > 0 ?
tempData.join('') + data.substring(cursor, data.length) :
data;
}
function reduceAll(data, context, callback) {
data = byUrl(data, context, callback);
data = byImport(data, context, callback);
return data;
}
module.exports = reduceAll;
@@ -0,0 +1,100 @@
var path = require('path');
var url = require('url');
var reduceUrls = require('./reduce');
var isWindows = process.platform == 'win32';
function isAbsolute(uri) {
return uri[0] == '/';
}
function isSVGMarker(uri) {
return uri[0] == '#';
}
function isEscaped(uri) {
return uri.indexOf('__ESCAPED_URL_CLEAN_CSS__') === 0;
}
function isInternal(uri) {
return /^\w+:\w+/.test(uri);
}
function isRemote(uri) {
return /^[^:]+?:\/\//.test(uri) || uri.indexOf('//') === 0;
}
function isSameOrigin(uri1, uri2) {
return url.parse(uri1).protocol == url.parse(uri2).protocol &&
url.parse(uri1).host == url.parse(uri2).host;
}
function isImport(uri) {
return uri.lastIndexOf('.css') === uri.length - 4;
}
function isData(uri) {
return uri.indexOf('data:') === 0;
}
function absolute(uri, options) {
return path
.resolve(path.join(options.fromBase || '', uri))
.replace(options.toBase, '');
}
function relative(uri, options) {
return path.relative(options.toBase, path.join(options.fromBase || '', uri));
}
function normalize(uri) {
return isWindows ? uri.replace(/\\/g, '/') : uri;
}
function rebase(uri, options) {
if (isAbsolute(uri) || isSVGMarker(uri) || isEscaped(uri) || isInternal(uri))
return uri;
if (options.rebase === false && !isImport(uri))
return uri;
if (!options.imports && isImport(uri))
return uri;
if (isData(uri))
return '\'' + uri + '\'';
if (isRemote(uri) && !isRemote(options.toBase))
return uri;
if (isRemote(uri) && !isSameOrigin(uri, options.toBase))
return uri;
if (!isRemote(uri) && isRemote(options.toBase))
return url.resolve(options.toBase, uri);
return options.absolute ?
normalize(absolute(uri, options)) :
normalize(relative(uri, options));
}
function quoteFor(url) {
if (url.indexOf('\'') > -1)
return '"';
else if (url.indexOf('"') > -1)
return '\'';
else if (/\s/.test(url))
return '\'';
else
return '';
}
function rewriteUrls(data, options, context) {
return reduceUrls(data, context, function (url, tempData) {
url = url.replace(/^(url\()?\s*['"]?|['"]?\s*\)?$/g, '');
tempData.push('url(' + quoteFor(url) + rebase(url, options) + quoteFor(url) + ')');
});
}
module.exports = rewriteUrls;
@@ -0,0 +1,12 @@
function cloneArray(array) {
var cloned = array.slice(0);
for (var i = 0, l = cloned.length; i < l; i++) {
if (Array.isArray(cloned[i]))
cloned[i] = cloneArray(cloned[i]);
}
return cloned;
}
module.exports = cloneArray;

Some files were not shown because too many files have changed in this diff Show More