Added logging, changed some directory structure

This commit is contained in:
2018-01-13 21:33:40 -05:00
parent f079a5f067
commit 8e72ffb917
73656 changed files with 35284 additions and 53718 deletions

View File

@@ -0,0 +1,325 @@
## 2.3.2 (March 11, 2016)
- Fix infinite loop on value parse (#328)
## 2.3.1 (January 6, 2016)
- Added `\0` IE hack support (#320)
## 2.3.0 (October 25, 2016)
- Added `beforeCompress` and `afterCompress` options support (#316)
- Fixed crash on empty argument in function (#317)
## 2.2.1 (July 25, 2016)
- Fixed shorthand optimisation issue when value has a color value or something unknown (#311)
- Fixed `cursor` broken fallback (#306)
## 2.2.0 (June 23, 2016)
- Implement AST cloning by adding `clone()` [function](https://github.com/css/csso#cloneast) and `clone` [option](https://github.com/css/csso#compressast-options) for `compress()` function (#296)
- Fix parse and translate attribute selector with flags but w/o operator (i.e. `[attrName i]`)
- Don't merge rules with flagged attribute selectors with others (#291)
- Take in account functions when merge TRBL-properties (#297, thanks to @ArturAralin)
- Improve partial merge (#304)
- Tweak scanner, reduce code deoptimizations and other small improvements
## 2.1.1 (May 11, 2016)
- Fix wrong declaration with `\9` hack merge (#295)
## 2.1.0 (May 8, 2016)
- New option `comments` to specify what comments to left: `exclamation`, `first-exclamation` and `none`
- Add `offset` to CSS parse error details
- Fix token `offset` computation
## 2.0.0 (April 5, 2016)
- No more `gonzales` AST format and related code
- `minify()` and `minifyBlock()` is always return an object as result now (i.e. `{ css: String, map: SourceMapGenerator or null }`)
- `parse()`
- Returns AST in new format (so called `internal`)
- Dynamic scanner implemented
- New AST format + dynamic scanner = performance boost and less memory consumption
- No more `context` argument, context should be specified via `options`
- Supported contexts now: `stylesheet`, `atrule`, `atruleExpression`, `ruleset`, `selector`, `simpleSelector`, `block`, `declaration` and `value`
- Drop `needPositions` option, `positions` option should be used instead
- Drop `needInfo` option, `info` object is attaching to nodes when some information is requested by `options`
- `options` should be an object, otherwise it treats as empty object
- `compress()`
- No more AST converting (performance boost and less memory consumption)
- Drop `outputAst` option
- Returns an object as result instead of AST (i.e. `{ ast: Object }`)
- Drop methods: `justDoIt()`, `stringify()`, `cleanInfo()`
## 1.8.1 (March 30, 2016)
- Don't remove spaces after function/braces/urls since unsafe (#289)
## 1.8.0 (March 24, 2016)
- Usage data support:
- Filter rulesets by tag names, class names and ids white lists.
- More aggressive ruleset moving using class name scopes information.
- New CLI option `--usage` to pass usage data file.
- Improve initial ruleset merge
- Change order of ruleset processing, now it's left to right. Previously unmerged rulesets may prevent lookup and other rulesets merge.
- Difference in pseudo signature just prevents ruleset merging, but don't stop lookup.
- Simplify block comparison (performance).
- New method `csso.minifyBlock()` for css block compression (e.g. `style` attribute content).
- Ruleset merge improvement: at-rules with block (like `@media` or `@supports`) now can be skipped during ruleset merge lookup if doesn't contain something prevents it.
- FIX: Add negation (`:not()`) to pseudo signature to avoid unsafe merge (old browsers doesn't support it).
- FIX: Check nested parts of value when compute compatibility. It fixes unsafe property merging.
## 1.7.1 (March 16, 2016)
- pass block mode to tokenizer for correct parsing of declarations properties with `//` hack
- fix wrongly `@import` and `@charset` removal on double exclamation comment
## 1.7.0 (March 10, 2016)
- support for [CSS Custom Properties](https://www.w3.org/TR/css-variables/) (#279)
- rework RTBL properties merge better merge for values with special units and don't merge values with CSS-wide keywords (#255)
- remove redundant universal selectors (#178)
- take in account `!important` when check for property overriding (#280)
- don't merge `text-align` declarations with some values (#281)
- add spaces around `/deep/` combinator on translate, since it together with universal selector can produce a comment
- better keyword and property name resolving (tolerant to hacks and so on)
- integration improvements
- compression log function could be customized by `logger` option for `compress()` and `minify()`
- make possible to set initial line and column for parser
## 1.6.4 (March 1, 2016)
- `npm` publish issue (#276)
## 1.6.3 (February 29, 2016)
- add `file` to generated source map since other tools can relay on it in source map transform chain
## 1.6.2 (February 29, 2016)
- tweak some parse error messages and their positions
- fix `:not()` parsing and selector groups in `:not()` is supported now (#215)
- `needPosition` parser option is deprecated, `positions` option should be used instead (`needPosition` is used still if `positions` option omitted)
- expose internal AST API as `csso.internal.*`
- `minify()` adds `sourcesContent` by default when source map is generated
- bring back support for node.js `0.10` until major release (#275)
## 1.6.1 (February 28, 2016)
- fix exception on zero length dimension compress outside declaration (#273)
## 1.6.0 (February 27, 2016)
- **source maps support**
- parser remake:
- various parsing issues fixed
- fix unicode sequence processing in ident (#191)
- support for flags in attribute selector (#270)
- position (line and column) of parse error (#109)
- 4x performance boost, less memory consumption
- compressor refactoring
- internal AST is using doubly linked lists (with safe transformation support during iteration) instead of arrays
- rename `restructuring` to `restructure` option for `minify()`/`compress()` (`restructuring` is alias for `restructure` now, with lower priority)
- unquote urls when possible (#141, #60)
- setup code coverage and a number of related fixes
- add eslint to check unused things
## 1.5.4 (January 27, 2016)
- one more fix (in `restructRuleset` this time) with merge of rulesets when a ruleset with same specificity places between them (#264)
- disable partial merge of rulesets in `@keyframes` rulesets (until sure it's correct)
## 1.5.3 (January 25, 2016)
- don't override display values with different browser support (#259)
- fix publish issue (one of modules leak in development state)
## 1.5.2 (January 24, 2016)
- don't merge rulesets if between them a ruleset with same specificity (#264)
## 1.5.1 (January 14, 2016)
- ensure `-` is not used as an identifier in attribute selectors (thanks to @mathiasbynens)
- fix broken `justDoIt()` function
- various small fixes
## 1.5.0 (January 14, 2016)
### Parser
- attach minus to number
### Compressor
- split code base into small modules and related refactoring
- introduce internal AST format for compressor (`gonzales``internal` and `internal``gonzales` convertors, walkers, translator)
- various optimizations: no snapshots, using caches and indexes
- sort selectors, merge selectors in alphabet order
- compute selector's specificity
- better ruleset restructuring, improve compression of partially equal blocks
- better ruleset merge not only closest but also disjoined by other rulesets when safe
- join `@media` with same query
- `outputAst` new option to specify output AST format (`gonzales` by default for backward compatibility)
- remove quotes surrounding attribute values in attribute selectors when possible (#73)
- replace `from``0%` and `100%``to` at `@keyframes` (#205)
- prevent partial merge of rulesets at `@keyframes` (#80, #197)
### API
- walker for `gonzales` AST was implemented
### CLI
- new option `--stat` (output stat in `stderr`)
- new optional parameter `level` for `--debug` option
## 1.4.4 (December 10, 2015)
- prevent removal of spaces after braces that before identifier that breaking at-rules expressions (#258)
## 1.4.3 (December 4, 2015)
- fix unicode-range parsing that cause to wrong function detection (#250)
## 1.4.2 (November 9, 2015)
- allow spaces between `progid:` and rest part of value for IE's `filter` property as `autoprefixer` generates this kind of code (#249)
- fixes for Windows:
- correct processing new lines
- normalize file content in test suite
- fixes to work in strict mode (#252)
- init compressor dictionaries for every css block (#248, #251)
- bump uglify-js version
## 1.4.1 (October 20, 2015)
- allow merge for `display` property (#167, #244)
- more accurate `rect` (`clip` property value) merge
- fix typo when specifying options in cli (thanks to @Taritsyn)
- fix safe unit values merge with keyword values (#244)
- fix wrong descendant combinator removal (#246)
- build browser version on `prepublish` (thanks to @silentroach)
- parser: store whitespaces as single token (performance and reduce memory consumption)
- rearrange compress tests layout
## 1.4 (October 16, 2015)
Bringing project back to life. Changed files structure, cleaned up and refactored most of sources.
### Common
- single code base (no more `src` folder)
- build browser version with `browserify` (no more `make`, and `web` folder), browser version is available at `dist/csso-browser.js`
- main file is `lib/index.js` now
- minimal `node.js` version is `0.12` now
- restrict file list to publish on npm (no more useless folders and files in package)
- add `jscs` to control code style
- automate `gh-pages` update
- util functions reworked
- translator reworked
- test suite reworked
- compressor refactored
- initial parser refactoring
### API
- new method `minify(src, options)`, options:
- `restructuring` if set to `false`, disable structure optimisations (`true` by default)
- `debug` - outputs intermediate state of CSS during compression (`false` by default)
- deprecate `justDoIt()` method (use `minify` instead)
- rename `treeToString()` method to `stringify()`
- drop `printTree()` method
- AST node info
- `column` and `offset` added
- `ln` renamed to `line`
- fix line counting across multiple files and input with CR LF (#147)
### CLI
- completely reworked, use [clap](https://github.com/lahmatiy/clap) to parse argv
- add support for input from stdin (#128)
- drop undocumented and obsoleted options `--rule` and `--parser` (suppose nobody use it)
- drop `-off` alias for `--restructure-off` as incorrect (only one letter options should starts with single `-`)
- new option `--debug` that reflecting to `options.debug` for `minify`
### Parsing and optimizations
- keep all exclamation comments (#194)
- add `/deep/` combinator support (#209)
- attribute selector
- allow colon in attribute name (#237)
- support for namespaces (#233)
- color
- support all css/html colors
- convert `hsla` to `rgba` and `hls` to `rgb`
- convert `rgba` with 1 as alpha value to `rgb` (#122)
- interpolate `rgb` and `rgba` percentage values to absolute values
- replace percentage values in `rgba` for normalized/interpolated values
- lowercase hex colors and color names (#169)
- fix color minification when hex value replaced for color name (#176)
- fit rgb values to 0..255 range (#181)
- calc
- remove spaces for multiple operator in calc
- don't remove units inside calc (#222)
- fix wrong white space removal around `+` and `-` (#228)
- don't remove units in `flex` property as it could change value meaning (#200)
- don't merge `\9` hack values (#231)
- merge property values only if they have the same functions (#150, #227)
- don't merge property values with some sort of units (#140, #161)
- fix `!important` issue for `top-right-bottom-left` properties (#189)
- fix `top-right-bottom-left` properties merge (#139, #175)
- support for unicode-range (#148)
- don't crash on ruleset with no selector (#135)
- tolerant to class names that starts with digit (#99, #105)
- fix background compressing (#170)
## 1.3.12 (October 8, 2015)
- Case insensitive check for `!important` (#187)
- Fix problems with using `csso` as cli command on Windows (#83, #136, #142 and others)
- Remove byte order marker (the UTF-8 BOM) from input
- Don't strip space between funktion-funktion and funktion-vhash (#134)
- Don't merge TRBL values having \9 (hack for IE8 in bootstrap) (#159, #214, #230, #231 and others)
- Don't strip units off dimensions of non-length (#226, #229 and others)
## 1.3.7 (February 11, 2013)
- Gonzales 1.0.7.
## 1.3.6 (November 26, 2012)
- Gonzales 1.0.6.
## 1.3.5 (October 28, 2012)
- Gonzales 1.0.5.
- Protecting copyright notices in CSS: https://github.com/css/csso/issues/92
- Zero CSS throws an error: https://github.com/css/csso/issues/96
- Don't minify the second `0s` in Firefox for animations: https://github.com/css/csso/issues/100
- Japan manual
- BEM ready documentation
## 1.3.4 (October 10, 2012)
- @page inside @media Causes Error: https://github.com/css/csso/issues/90
## 1.3.3 (October 9, 2012)
- CSSO 1.3.2 compresses ".t-1" and ".t-01" as identical classes: https://github.com/css/csso/issues/88
## 1.3.2 (October 8, 2012)
- filter + important breaks CSSO v1.3.1: https://github.com/css/csso/issues/87
## 1.3.1 (October 8, 2012)
- "filter" IE property breaks CSSO v1.3.0: https://github.com/css/csso/issues/86
## 1.3.0 (October 4, 2012)
- PeCode CSS parser replaced by Gonzales CSS parser

View File

@@ -0,0 +1,19 @@
Copyright (C) 2011-2015 by Sergey Kryzhanovsky
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.

View File

@@ -0,0 +1,407 @@
[![NPM version](https://img.shields.io/npm/v/csso.svg)](https://www.npmjs.com/package/csso)
[![Build Status](https://travis-ci.org/css/csso.svg?branch=master)](https://travis-ci.org/css/csso)
[![Coverage Status](https://coveralls.io/repos/github/css/csso/badge.svg?branch=master)](https://coveralls.io/github/css/csso?branch=master)
[![NPM Downloads](https://img.shields.io/npm/dm/csso.svg)](https://www.npmjs.com/package/csso)
[![Twitter](https://img.shields.io/badge/Twitter-@cssoptimizer-blue.svg)](https://twitter.com/cssoptimizer)
CSSO (CSS Optimizer) is a CSS minifier. It performs three sort of transformations: cleaning (removing redundant), compression (replacement for shorter form) and restructuring (merge of declarations, rulesets and so on). As a result your CSS becomes much smaller.
[![Originated by Yandex](https://cdn.rawgit.com/css/csso/8d1b89211ac425909f735e7d5df87ee16c2feec6/docs/yandex.svg)](https://www.yandex.com/)
[![Sponsored by Avito](https://cdn.rawgit.com/css/csso/8d1b89211ac425909f735e7d5df87ee16c2feec6/docs/avito.svg)](https://www.avito.ru/)
## Usage
```
npm install -g csso
```
Or try out CSSO [right in your browser](http://css.github.io/csso/csso.html) (web interface).
### Runners
- Gulp: [gulp-csso](https://github.com/ben-eb/gulp-csso)
- Grunt: [grunt-csso](https://github.com/t32k/grunt-csso)
- Broccoli: [broccoli-csso](https://github.com/sindresorhus/broccoli-csso)
- PostCSS: [postcss-csso](https://github.com/lahmatiy/postcss-csso)
- Webpack: [csso-loader](https://github.com/sandark7/csso-loader)
### Command line
```
csso [input] [output] [options]
Options:
--comments <value> Comments to keep: exclamation (default), first-exclamation or none
--debug [level] Output intermediate state of CSS during compression
-h, --help Output usage information
-i, --input <filename> Input file
--input-map <source> Input source map: none, auto (default) or <filename>
-m, --map <destination> Generate source map: none (default), inline, file or <filename>
-o, --output <filename> Output file (result outputs to stdout if not set)
--restructure-off Turns structure minimization off
--stat Output statistics in stderr
-u, --usage <filenane> Usage data file
-v, --version Output version
```
Some examples:
```
> csso in.css
...output result in stdout...
> csso in.css --output out.css
> echo '.test { color: #ff0000; }' | csso
.test{color:red}
> cat source1.css source2.css | csso | gzip -9 -c > production.css.gz
```
### Source maps
Source map doesn't generate by default. To generate map use `--map` CLI option, that can be:
- `none` (default) don't generate source map
- `inline` add source map into result CSS (via `/*# sourceMappingURL=application/json;base64,... */`)
- `file` write source map into file with same name as output file, but with `.map` extension (in this case `--output` option is required)
- any other values treat as filename for generated source map
Examples:
```
> csso my.css --map inline
> csso my.css --output my.min.css --map file
> csso my.css --output my.min.css --map maps/my.min.map
```
Use `--input-map` option to specify input source map if needed. Possible values for option:
- `auto` (default) - attempt to fetch input source map by follow steps:
- try to fetch inline map from input
- try to fetch source map filename from input and read its content
- (when `--input` is specified) check file with same name as input file but with `.map` extension exists and read its content
- `none` - don't use input source map; actually it's using to disable `auto`-fetching
- any other values treat as filename for input source map
Generally you shouldn't care about input source map since defaults behaviour (`auto`) covers most use cases.
> NOTE: Input source map is using only if output source map is generating.
### Usage data
`CSSO` can use data about how `CSS` is using for better compression. File with this data (`JSON` format) can be set using `--usage` option. Usage data may contain follow sections:
- `tags` white list of tags
- `ids` white list of ids
- `classes` white list of classes
- `scopes` groups of classes which never used with classes from other groups on single element
All sections are optional. Value of `tags`, `ids` and `classes` should be array of strings, value of `scopes` should be an array of arrays of strings. Other values are ignoring.
#### Selector filtering
`tags`, `ids` and `classes` are using on clean stage to filter selectors that contains something that not in list. Selectors are filtering only by those kind of simple selector which white list is specified. For example, if only `tags` list is specified then type selectors are checking, and if selector hasn't any type selector (or even any type selector) it isn't filter.
> `ids` and `classes` names are case sensitive, `tags` is not.
Input CSS:
```css
* { color: green; }
ul, ol, li { color: blue; }
UL.foo, span.bar { color: red; }
```
Usage data:
```json
{
"tags": ["ul", "LI"]
}
```
Result CSS:
```css
*{color:green}ul,li{color:blue}ul.foo{color:red}
```
#### Scopes
Scopes is designed for CSS scope isolation solutions such as [css-modules](https://github.com/css-modules/css-modules). Scopes are similar to namespaces and defines lists of class names that exclusively used on some markup. This information allows the optimizer to move rulesets more agressive. Since it assumes selectors from different scopes can't to be matched on the same element. That leads to better ruleset merging.
Suppose we have a file:
```css
.module1-foo { color: red; }
.module1-bar { font-size: 1.5em; background: yellow; }
.module2-baz { color: red; }
.module2-qux { font-size: 1.5em; background: yellow; width: 50px; }
```
It can be assumed that first two rules never used with second two on the same markup. But we can't know that for sure without markup. The optimizer doesn't know it eather and will perform safe transformations only. The result will be the same as input but with no spaces and some semicolons:
```css
.module1-foo{color:red}.module1-bar{font-size:1.5em;background:#ff0}.module2-baz{color:red}.module2-qux{font-size:1.5em;background:#ff0;width:50px}
```
But with usage data `CSSO` can get better output. If follow usage data is provided:
```json
{
"scopes": [
["module1-foo", "module1-bar"],
["module2-baz", "module2-qux"]
]
}
```
New result (29 bytes extra saving):
```css
.module1-foo,.module2-baz{color:red}.module1-bar,.module2-qux{font-size:1.5em;background:#ff0}.module2-qux{width:50px}
```
If class name doesn't specified in `scopes` it belongs to default "scope". `scopes` doesn't affect `classes`. If class name presents in `scopes` but missed in `classes` (both sections specified) it will be filtered.
Note that class name can't be specified in several scopes. Also selector can't has classes from different scopes. In both cases an exception throws.
Currently the optimizer doesn't care about out-of-bounds selectors order changing safety (i.e. selectors that may be matched to elements with no class name of scope, e.g. `.scope div` or `.scope ~ :last-child`) since assumes scoped CSS modules doesn't relay on it's order. It may be fix in future if to be an issue.
### API
```js
var csso = require('csso');
var compressedCss = csso.minify('.test { color: #ff0000; }').css;
console.log(compressedCss);
// .test{color:red}
```
You may minify CSS by yourself step by step:
```js
var ast = csso.parse('.test { color: #ff0000; }');
var compressResult = csso.compress(ast);
var compressedCss = csso.translate(compressResult.ast);
console.log(compressedCss);
// .test{color:red}
```
Working with source maps:
```js
var css = fs.readFileSync('path/to/my.css', 'utf8');
var result = csso.minify(css, {
filename: 'path/to/my.css', // will be added to source map as reference to source file
sourceMap: true // generate source map
});
console.log(result);
// { css: '...minified...', map: SourceMapGenerator {} }
console.log(result.map.toString());
// '{ .. source map content .. }'
```
#### minify(source[, options])
Minify `source` CSS passed as `String`.
Options:
- sourceMap `Boolean` - generate source map if `true`
- filename `String` - filename of input, uses for source map
- debug `Boolean` - output debug information to `stderr`
- beforeCompress `function|array<function>` - called right after parse is run. Callbacks arguments are `ast, options`.
- afterCompress `function|array<function>` - called right after compress is run. Callbacks arguments are `compressResult, options`.
- other options are the same as for `compress()`
Returns an object with properties:
- css `String` resulting CSS
- map `Object` instance of `SourceMapGenerator` or `null`
```js
var result = csso.minify('.test { color: #ff0000; }', {
restructure: false, // don't change CSS structure, i.e. don't merge declarations, rulesets etc
debug: true // show additional debug information:
// true or number from 1 to 3 (greater number - more details)
});
console.log(result.css);
// > .test{color:red}
```
#### minifyBlock(source[, options])
The same as `minify()` but for style block. Usualy it's a `style` attribute content.
```js
var result = csso.minifyBlock('color: rgba(255, 0, 0, 1); color: #ff0000').css;
console.log(result.css);
// > color:red
```
#### parse(source[, options])
Parse CSS to AST.
> NOTE: Currenly parser omit redundant separators, spaces and comments (except exclamation comments, i.e. `/*! comment */`) on AST build, since those things are removing by compressor anyway.
Options:
- context `String` parsing context, useful when some part of CSS is parsing (see below)
- positions `Boolean` should AST contains node position or not, store data in `info` property of nodes (`false` by default)
- filename `String` filename of source that adds to info when `positions` is true, uses for source map generation (`<unknown>` by default)
- line `Number` initial line number, useful when parse fragment of CSS to compute correct positions
- column `Number` initial column number, useful when parse fragment of CSS to compute correct positions
Contexts:
- `stylesheet` (default) regular stylesheet, should be suitable in most cases
- `atrule` at-rule (e.g. `@media screen, print { ... }`)
- `atruleExpression` at-rule expression (`screen, print` for example above)
- `ruleset` rule (e.g. `.foo, .bar:hover { color: red; border: 1px solid black; }`)
- `selector` selector group (`.foo, .bar:hover` for ruleset example)
- `simpleSelector` selector (`.foo` or `.bar:hover` for ruleset example)
- `block` block content w/o curly braces (`color: red; border: 1px solid black;` for ruleset example)
- `declaration` declaration (`color: red` or `border: 1px solid black` for ruleset example)
- `value` declaration value (`red` or `1px solid black` for ruleset example)
```js
// simple parsing with no options
var ast = csso.parse('.example { color: red }');
// parse with options
var ast = csso.parse('.foo.bar', {
context: 'simpleSelector',
positions: true
});
```
#### compress(ast[, options])
Does the main task compress AST.
> NOTE: `compress` performs AST compression by transforming input AST by default (since AST cloning is expensive and needed in rare cases). Use `clone` option with truthy value in case you want to keep input AST untouched.
Options:
- restructure `Boolean` do the structure optimisations or not (`true` by default)
- clone `Boolean` - transform a copy of input AST if `true`, useful in case of AST reuse (`false` by default)
- comments `String` or `Boolean` specify what comments to left
- `'exclamation'` or `true` (default) left all exclamation comments (i.e. `/*! .. */`)
- `'first-exclamation'` remove every comments except first one
- `false` remove every comments
- usage `Object` - usage data for advanced optimisations (see [Usage data](#usage-data) for details)
- logger `Function` - function to track every step of transformations
#### clone(ast)
Make an AST node deep copy.
```js
var orig = csso.parse('.test { color: red }');
var copy = csso.clone(orig);
csso.walk(copy, function(node) {
if (node.type === 'Class') {
node.name = 'replaced';
}
});
console.log(csso.translate(orig));
// .test{color:red}
console.log(csso.translate(copy));
// .replaced{color:red}
```
#### translate(ast)
Converts AST to string.
```js
var ast = csso.parse('.test { color: red }');
console.log(csso.translate(ast));
// > .test{color:red}
```
#### translateWithSourceMap(ast)
The same as `translate()` but also generates source map (nodes should contain positions in `info` property).
```js
var ast = csso.parse('.test { color: red }', {
filename: 'my.css',
positions: true
});
console.log(csso.translateWithSourceMap(ast));
// { css: '.test{color:red}', map: SourceMapGenerator {} }
```
#### walk(ast, handler)
Visit all nodes of AST and call handler for each one. `handler` receives three arguments:
- node current AST node
- item node wrapper when node is a list member; this wrapper contains references to `prev` and `next` nodes in list
- list reference to list when node is a list member; it's useful for operations on list like `remove()` or `insert()`
Context for handler an object, that contains references to some parent nodes:
- root refers to `ast` or root node
- stylesheet refers to closest `StyleSheet` node, it may be a top-level or at-rule block stylesheet
- atruleExpression refers to `AtruleExpression` node if current node inside at-rule expression
- ruleset refers to `Ruleset` node if current node inside a ruleset
- selector refers to `Selector` node if current node inside a selector
- declaration refers to `Declaration` node if current node inside a declaration
- function refers to closest `Function` or `FunctionalPseudo` node if current node inside one of them
```js
// collect all urls in declarations
var csso = require('./lib/index.js');
var urls = [];
var ast = csso.parse(`
@import url(import.css);
.foo { background: url('foo.jpg'); }
.bar { background-image: url(bar.png); }
`);
csso.walk(ast, function(node) {
if (this.declaration !== null && node.type === 'Url') {
var value = node.value;
if (value.type === 'Raw') {
urls.push(value.value);
} else {
urls.push(value.value.substr(1, value.value.length - 2));
}
}
});
console.log(urls);
// [ 'foo.jpg', 'bar.png' ]
```
#### walkRules(ast, handler)
Same as `walk()` but visits `Ruleset` and `Atrule` nodes only.
#### walkRulesRight(ast, handler)
Same as `walkRules()` but visits nodes in reverse order (from last to first).
## More reading
- [Debugging](docs/debugging.md)
## License
MIT

View File

@@ -0,0 +1,16 @@
#!/usr/bin/env node
var cli = require('../lib/cli.js');
try {
cli.run();
} catch (e) {
// output user frendly message if cli error
if (cli.isCliError(e)) {
console.error(e.message || e);
process.exit(2);
}
// otherwise re-throw exception
throw e;
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,338 @@
var fs = require('fs');
var path = require('path');
var cli = require('clap');
var SourceMapConsumer = require('source-map').SourceMapConsumer;
var csso = require('./index.js');
function readFromStream(stream, minify) {
var buffer = [];
// FIXME: don't chain until node.js 0.10 drop, since setEncoding isn't chainable in 0.10
stream.setEncoding('utf8');
stream
.on('data', function(chunk) {
buffer.push(chunk);
})
.on('end', function() {
minify(buffer.join(''));
});
}
function showStat(filename, source, result, inputMap, map, time, mem) {
function fmt(size) {
return String(size).split('').reverse().reduce(function(size, digit, idx) {
if (idx && idx % 3 === 0) {
size = ' ' + size;
}
return digit + size;
}, '');
}
map = map || 0;
result -= map;
console.error('Source: ', filename === '<stdin>' ? filename : path.relative(process.cwd(), filename));
if (inputMap) {
console.error('Map source:', inputMap);
}
console.error('Original: ', fmt(source), 'bytes');
console.error('Compressed:', fmt(result), 'bytes', '(' + (100 * result / source).toFixed(2) + '%)');
console.error('Saving: ', fmt(source - result), 'bytes', '(' + (100 * (source - result) / source).toFixed(2) + '%)');
if (map) {
console.error('Source map:', fmt(map), 'bytes', '(' + (100 * map / (result + map)).toFixed(2) + '% of total)');
console.error('Total: ', fmt(map + result), 'bytes');
}
console.error('Time: ', time, 'ms');
console.error('Memory: ', (mem / (1024 * 1024)).toFixed(3), 'MB');
}
function showParseError(source, filename, details, message) {
function processLines(start, end) {
return lines.slice(start, end).map(function(line, idx) {
var num = String(start + idx + 1);
while (num.length < maxNumLength) {
num = ' ' + num;
}
return num + ' |' + line;
}).join('\n');
}
var lines = source.split(/\n|\r\n?|\f/);
var column = details.column;
var line = details.line;
var startLine = Math.max(1, line - 2);
var endLine = Math.min(line + 2, lines.length + 1);
var maxNumLength = Math.max(4, String(endLine).length) + 1;
console.error('\nParse error ' + filename + ': ' + message);
console.error(processLines(startLine - 1, line));
console.error(new Array(column + maxNumLength + 2).join('-') + '^');
console.error(processLines(line, endLine));
console.error();
}
function debugLevel(level) {
// level is undefined when no param -> 1
return isNaN(level) ? 1 : Math.max(Number(level), 0);
}
function resolveSourceMap(source, inputMap, map, inputFile, outputFile) {
var inputMapContent = null;
var inputMapFile = null;
var outputMapFile = null;
switch (map) {
case 'none':
// don't generate source map
map = false;
inputMap = 'none';
break;
case 'inline':
// nothing to do
break;
case 'file':
if (!outputFile) {
console.error('Output filename should be specified when `--map file` is used');
process.exit(2);
}
outputMapFile = outputFile + '.map';
break;
default:
// process filename
if (map) {
// check path is reachable
if (!fs.existsSync(path.dirname(map))) {
console.error('Directory for map file should exists:', path.dirname(path.resolve(map)));
process.exit(2);
}
// resolve to absolute path
outputMapFile = path.resolve(process.cwd(), map);
}
}
switch (inputMap) {
case 'none':
// nothing to do
break;
case 'auto':
if (map) {
// try fetch source map from source
var inputMapComment = source.match(/\/\*# sourceMappingURL=(\S+)\s*\*\/\s*$/);
if (inputFile === '<stdin>') {
inputFile = false;
}
if (inputMapComment) {
// if comment found value is filename or base64-encoded source map
inputMapComment = inputMapComment[1];
if (inputMapComment.substr(0, 5) === 'data:') {
// decode source map content from comment
inputMapContent = new Buffer(inputMapComment.substr(inputMapComment.indexOf('base64,') + 7), 'base64').toString();
} else {
// value is filename resolve it as absolute path
if (inputFile) {
inputMapFile = path.resolve(path.dirname(inputFile), inputMapComment);
}
}
} else {
// comment doesn't found - look up file with `.map` extension nearby input file
if (inputFile && fs.existsSync(inputFile + '.map')) {
inputMapFile = inputFile + '.map';
}
}
}
break;
default:
if (inputMap) {
inputMapFile = inputMap;
}
}
// source map placed in external file
if (inputMapFile) {
inputMapContent = fs.readFileSync(inputMapFile, 'utf8');
}
return {
input: inputMapContent,
inputFile: inputMapFile || (inputMapContent ? '<inline>' : false),
output: map,
outputFile: outputMapFile
};
}
function processCommentsOption(value) {
switch (value) {
case 'exclamation':
case 'first-exclamation':
case 'none':
return value;
}
console.error('Wrong value for `comments` option: %s', value);
process.exit(2);
}
var command = cli.create('csso', '[input] [output]')
.version(require('../package.json').version)
.option('-i, --input <filename>', 'Input file')
.option('-o, --output <filename>', 'Output file (result outputs to stdout if not set)')
.option('-m, --map <destination>', 'Generate source map: none (default), inline, file or <filename>', 'none')
.option('-u, --usage <filenane>', 'Usage data file')
.option('--input-map <source>', 'Input source map: none, auto (default) or <filename>', 'auto')
.option('--restructure-off', 'Turns structure minimization off')
.option('--comments <value>', 'Comments to keep: exclamation (default), first-exclamation or none', 'exclamation')
.option('--stat', 'Output statistics in stderr')
.option('--debug [level]', 'Output intermediate state of CSS during compression', debugLevel, 0)
.action(function(args) {
var options = this.values;
var inputFile = options.input || args[0];
var outputFile = options.output || args[1];
var usageFile = options.usage;
var usageData = false;
var map = options.map;
var inputMap = options.inputMap;
var structureOptimisationOff = options.restructureOff;
var comments = processCommentsOption(options.comments);
var debug = options.debug;
var statistics = options.stat;
var inputStream;
if (process.stdin.isTTY && !inputFile && !outputFile) {
this.showHelp();
return;
}
if (!inputFile) {
inputFile = '<stdin>';
inputStream = process.stdin;
} else {
inputFile = path.resolve(process.cwd(), inputFile);
inputStream = fs.createReadStream(inputFile);
}
if (outputFile) {
outputFile = path.resolve(process.cwd(), outputFile);
}
if (usageFile) {
if (!fs.existsSync(usageFile)) {
console.error('Usage data file doesn\'t found (%s)', usageFile);
process.exit(2);
}
usageData = fs.readFileSync(usageFile, 'utf-8');
try {
usageData = JSON.parse(usageData);
} catch (e) {
console.error('Usage data parse error (%s)', usageFile);
process.exit(2);
}
}
readFromStream(inputStream, function(source) {
var time = process.hrtime();
var mem = process.memoryUsage().heapUsed;
var sourceMap = resolveSourceMap(source, inputMap, map, inputFile, outputFile);
var sourceMapAnnotation = '';
var result;
// main action
try {
result = csso.minify(source, {
filename: inputFile,
sourceMap: sourceMap.output,
usage: usageData,
restructure: !structureOptimisationOff,
comments: comments,
debug: debug
});
// for backward capability minify returns a string
if (typeof result === 'string') {
result = {
css: result,
map: null
};
}
} catch (e) {
if (e.parseError) {
showParseError(source, inputFile, e.parseError, e.message);
if (!debug) {
process.exit(2);
}
}
throw e;
}
if (sourceMap.output && result.map) {
// apply input map
if (sourceMap.input) {
result.map.applySourceMap(
new SourceMapConsumer(sourceMap.input),
inputFile
);
}
// add source map to result
if (sourceMap.outputFile) {
// write source map to file
fs.writeFileSync(sourceMap.outputFile, result.map.toString(), 'utf-8');
sourceMapAnnotation = '\n' +
'/*# sourceMappingURL=' +
path.relative(outputFile ? path.dirname(outputFile) : process.cwd(), sourceMap.outputFile) +
' */';
} else {
// inline source map
sourceMapAnnotation = '\n' +
'/*# sourceMappingURL=data:application/json;base64,' +
new Buffer(result.map.toString()).toString('base64') +
' */';
}
result.css += sourceMapAnnotation;
}
// output result
if (outputFile) {
fs.writeFileSync(outputFile, result.css, 'utf-8');
} else {
console.log(result.css);
}
// output statistics
if (statistics) {
var timeDiff = process.hrtime(time);
showStat(
path.relative(process.cwd(), inputFile),
source.length,
result.css.length,
sourceMap.inputFile,
sourceMapAnnotation.length,
parseInt(timeDiff[0] * 1e3 + timeDiff[1] / 1e6),
process.memoryUsage().heapUsed - mem
);
}
});
});
module.exports = {
run: command.run.bind(command),
isCliError: function(err) {
return err instanceof cli.Error;
}
};

View File

@@ -0,0 +1,54 @@
module.exports = function cleanAtrule(node, item, list) {
if (node.block) {
// otherwise removed at-rule don't prevent @import for removal
this.root.firstAtrulesAllowed = false;
if (node.block.type === 'Block' && node.block.declarations.isEmpty()) {
list.remove(item);
return;
}
if (node.block.type === 'StyleSheet' && node.block.rules.isEmpty()) {
list.remove(item);
return;
}
}
switch (node.name) {
case 'charset':
if (node.expression.sequence.isEmpty()) {
list.remove(item);
return;
}
// if there is any rule before @charset -> remove it
if (item.prev) {
list.remove(item);
return;
}
break;
case 'import':
if (!this.root.firstAtrulesAllowed) {
list.remove(item);
return;
}
// if there are some rules that not an @import or @charset before @import
// remove it
list.prevUntil(item.prev, function(rule) {
if (rule.type === 'Atrule') {
if (rule.name === 'import' || rule.name === 'charset') {
return;
}
}
this.root.firstAtrulesAllowed = false;
list.remove(item);
return true;
}, this);
break;
}
};

View File

@@ -0,0 +1,3 @@
module.exports = function cleanComment(data, item, list) {
list.remove(item);
};

View File

@@ -0,0 +1,5 @@
module.exports = function cleanDeclartion(node, item, list) {
if (node.value.sequence.isEmpty()) {
list.remove(item);
}
};

View File

@@ -0,0 +1,9 @@
module.exports = function cleanIdentifier(node, item, list) {
// remove useless universal selector
if (this.selector !== null && node.name === '*') {
// remove when universal selector isn't last
if (item.next && item.next.data.type !== 'Combinator') {
list.remove(item);
}
}
};

View File

@@ -0,0 +1,39 @@
var hasOwnProperty = Object.prototype.hasOwnProperty;
function cleanUnused(node, usageData) {
return node.selector.selectors.each(function(selector, item, list) {
var hasUnused = selector.sequence.some(function(node) {
switch (node.type) {
case 'Class':
return usageData.classes && !hasOwnProperty.call(usageData.classes, node.name);
case 'Id':
return usageData.ids && !hasOwnProperty.call(usageData.ids, node.name);
case 'Identifier':
// ignore universal selector
if (node.name !== '*') {
// TODO: remove toLowerCase when type selectors will be normalized
return usageData.tags && !hasOwnProperty.call(usageData.tags, node.name.toLowerCase());
}
break;
}
});
if (hasUnused) {
list.remove(item);
}
});
}
module.exports = function cleanRuleset(node, item, list, usageData) {
if (usageData) {
cleanUnused(node, usageData);
}
if (node.selector.selectors.isEmpty() ||
node.block.declarations.isEmpty()) {
list.remove(item);
}
};

View File

@@ -0,0 +1,16 @@
function canCleanWhitespace(node) {
if (node.type !== 'Operator') {
return false;
}
return node.value !== '+' && node.value !== '-';
}
module.exports = function cleanWhitespace(node, item, list) {
var prev = item.prev && item.prev.data;
var next = item.next && item.next.data;
if (canCleanWhitespace(prev) || canCleanWhitespace(next)) {
list.remove(item);
}
};

View File

@@ -0,0 +1,17 @@
var walk = require('../../utils/walk.js').all;
var handlers = {
Space: require('./Space.js'),
Atrule: require('./Atrule.js'),
Ruleset: require('./Ruleset.js'),
Declaration: require('./Declaration.js'),
Identifier: require('./Identifier.js'),
Comment: require('./Comment.js')
};
module.exports = function(ast, usageData) {
walk(ast, function(node, item, list) {
if (handlers.hasOwnProperty(node.type)) {
handlers[node.type].call(this, node, item, list, usageData);
}
});
};

View File

@@ -0,0 +1,9 @@
var resolveKeyword = require('../../utils/names.js').keyword;
var compressKeyframes = require('./atrule/keyframes.js');
module.exports = function(node) {
// compress @keyframe selectors
if (resolveKeyword(node.name).name === 'keyframes') {
compressKeyframes(node);
}
};

View File

@@ -0,0 +1,33 @@
// Can unquote attribute detection
// Adopted implementation of Mathias Bynens
// https://github.com/mathiasbynens/mothereff.in/blob/master/unquoted-attributes/eff.js
var escapesRx = /\\([0-9A-Fa-f]{1,6})[ \t\n\f\r]?|\\./g;
var blockUnquoteRx = /^(-?\d|--)|[\u0000-\u002c\u002e\u002f\u003A-\u0040\u005B-\u005E\u0060\u007B-\u009f]/;
function canUnquote(value) {
if (value === '' || value === '-') {
return;
}
// Escapes are valid, so replace them with a valid non-empty string
value = value.replace(escapesRx, 'a');
return !blockUnquoteRx.test(value);
}
module.exports = function(node) {
var attrValue = node.value;
if (!attrValue || attrValue.type !== 'String') {
return;
}
var unquotedValue = attrValue.value.replace(/^(.)(.*)\1$/, '$2');
if (canUnquote(unquotedValue)) {
node.value = {
type: 'Identifier',
info: attrValue.info,
name: unquotedValue
};
}
};

View File

@@ -0,0 +1,54 @@
var packNumber = require('./Number.js').pack;
var LENGTH_UNIT = {
// absolute length units
'px': true,
'mm': true,
'cm': true,
'in': true,
'pt': true,
'pc': true,
// relative length units
'em': true,
'ex': true,
'ch': true,
'rem': true,
// viewport-percentage lengths
'vh': true,
'vw': true,
'vmin': true,
'vmax': true,
'vm': true
};
module.exports = function compressDimension(node, item) {
var value = packNumber(node.value);
node.value = value;
if (value === '0' && this.declaration) {
var unit = node.unit.toLowerCase();
// only length values can be compressed
if (!LENGTH_UNIT.hasOwnProperty(unit)) {
return;
}
// issue #200: don't remove units in flex property as it could change value meaning
if (this.declaration.property.name === 'flex') {
return;
}
// issue #222: don't remove units inside calc
if (this['function'] && this['function'].name === 'calc') {
return;
}
item.data = {
type: 'Number',
info: node.info,
value: value
};
}
};

View File

@@ -0,0 +1,22 @@
function packNumber(value) {
// 100 -> '100'
// 00100 -> '100'
// +100 -> '100'
// -100 -> '-100'
// 0.123 -> '.123'
// 0.12300 -> '.123'
// 0.0 -> ''
// 0 -> ''
value = String(value).replace(/^(?:\+|(-))?0*(\d*)(?:\.0*|(\.\d*?)0*)?$/, '$1$2$3');
if (value.length === 0 || value === '-') {
value = '0';
}
return value;
};
module.exports = function(node) {
node.value = packNumber(node.value);
};
module.exports.pack = packNumber;

View File

@@ -0,0 +1,12 @@
module.exports = function(node) {
var value = node.value;
// remove escaped \n, i.e.
// .a { content: "foo\
// bar"}
// ->
// .a { content: "foobar" }
value = value.replace(/\\\n/g, '');
node.value = value;
};

View File

@@ -0,0 +1,33 @@
var UNICODE = '\\\\[0-9a-f]{1,6}(\\r\\n|[ \\n\\r\\t\\f])?';
var ESCAPE = '(' + UNICODE + '|\\\\[^\\n\\r\\f0-9a-fA-F])';
var NONPRINTABLE = '\u0000\u0008\u000b\u000e-\u001f\u007f';
var SAFE_URL = new RegExp('^(' + ESCAPE + '|[^\"\'\\(\\)\\\\\\s' + NONPRINTABLE + '])*$', 'i');
module.exports = function(node) {
var value = node.value;
if (value.type !== 'String') {
return;
}
var quote = value.value[0];
var url = value.value.substr(1, value.value.length - 2);
// convert `\\` to `/`
url = url.replace(/\\\\/g, '/');
// remove quotes when safe
// https://www.w3.org/TR/css-syntax-3/#url-unquoted-diagram
if (SAFE_URL.test(url)) {
node.value = {
type: 'Raw',
info: node.value.info,
value: url
};
} else {
// use double quotes if string has no double quotes
// otherwise use original quotes
// TODO: make better quote type selection
node.value.value = url.indexOf('"') === -1 ? '"' + url + '"' : quote + url + quote;
}
};

View File

@@ -0,0 +1,18 @@
var resolveName = require('../../utils/names.js').property;
var handlers = {
'font': require('./property/font.js'),
'font-weight': require('./property/font-weight.js'),
'background': require('./property/background.js')
};
module.exports = function compressValue(node) {
if (!this.declaration) {
return;
}
var property = resolveName(this.declaration.property.name);
if (handlers.hasOwnProperty(property.name)) {
handlers[property.name](node);
}
};

View File

@@ -0,0 +1,21 @@
module.exports = function(node) {
node.block.rules.each(function(ruleset) {
ruleset.selector.selectors.each(function(simpleselector) {
simpleselector.sequence.each(function(data, item) {
if (data.type === 'Percentage' && data.value === '100') {
item.data = {
type: 'Identifier',
info: data.info,
name: 'to'
};
} else if (data.type === 'Identifier' && data.name === 'from') {
item.data = {
type: 'Percentage',
info: data.info,
value: '0'
};
}
});
});
});
};

View File

@@ -0,0 +1,489 @@
var List = require('../../utils/list.js');
var packNumber = require('./Number.js').pack;
// http://www.w3.org/TR/css3-color/#svg-color
var NAME_TO_HEX = {
'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',
'darkgrey': 'a9a9a9',
'darkgreen': '006400',
'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',
'grey': '808080',
'green': '008000',
'greenyellow': 'adff2f',
'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',
'lightgrey': 'd3d3d3',
'lightgreen': '90ee90',
'lightpink': 'ffb6c1',
'lightsalmon': 'ffa07a',
'lightseagreen': '20b2aa',
'lightskyblue': '87cefa',
'lightslategray': '789',
'lightslategrey': '789',
'lightsteelblue': 'b0c4de',
'lightyellow': 'ffffe0',
'lime': '0f0',
'limegreen': '32cd32',
'linen': 'faf0e6',
'magenta': 'f0f',
'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': '639',
'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 HEX_TO_NAME = {
'800000': 'maroon',
'800080': 'purple',
'808000': 'olive',
'808080': 'gray',
'00ffff': 'cyan',
'f0ffff': 'azure',
'f5f5dc': 'beige',
'ffe4c4': 'bisque',
'000000': 'black',
'0000ff': 'blue',
'a52a2a': 'brown',
'ff7f50': 'coral',
'ffd700': 'gold',
'008000': 'green',
'4b0082': 'indigo',
'fffff0': 'ivory',
'f0e68c': 'khaki',
'00ff00': 'lime',
'faf0e6': 'linen',
'000080': 'navy',
'ffa500': 'orange',
'da70d6': 'orchid',
'cd853f': 'peru',
'ffc0cb': 'pink',
'dda0dd': 'plum',
'f00': 'red',
'ff0000': 'red',
'fa8072': 'salmon',
'a0522d': 'sienna',
'c0c0c0': 'silver',
'fffafa': 'snow',
'd2b48c': 'tan',
'008080': 'teal',
'ff6347': 'tomato',
'ee82ee': 'violet',
'f5deb3': 'wheat',
'ffffff': 'white',
'ffff00': 'yellow'
};
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;
}
function hslToRgb(h, s, l, a) {
var r;
var g;
var b;
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 [
Math.round(r * 255),
Math.round(g * 255),
Math.round(b * 255),
a
];
}
function toHex(value) {
value = value.toString(16);
return value.length === 1 ? '0' + value : value;
}
function parseFunctionArgs(functionArgs, count, rgb) {
var argument = functionArgs.head;
var args = [];
while (argument !== null) {
var argumentPart = argument.data.sequence.head;
var wasValue = false;
while (argumentPart !== null) {
var value = argumentPart.data;
var type = value.type;
switch (type) {
case 'Number':
case 'Percentage':
if (wasValue) {
return;
}
wasValue = true;
args.push({
type: type,
value: Number(value.value)
});
break;
case 'Operator':
if (wasValue || value.value !== '+') {
return;
}
break;
default:
// something we couldn't understand
return;
}
argumentPart = argumentPart.next;
}
argument = argument.next;
}
if (args.length !== count) {
// invalid arguments count
// TODO: remove those tokens
return;
}
if (args.length === 4) {
if (args[3].type !== 'Number') {
// 4th argument should be a number
// TODO: remove those tokens
return;
}
args[3].type = 'Alpha';
}
if (rgb) {
if (args[0].type !== args[1].type || args[0].type !== args[2].type) {
// invalid color, numbers and percentage shouldn't be mixed
// TODO: remove those tokens
return;
}
} else {
if (args[0].type !== 'Number' ||
args[1].type !== 'Percentage' ||
args[2].type !== 'Percentage') {
// invalid color, for hsl values should be: number, percentage, percentage
// TODO: remove those tokens
return;
}
args[0].type = 'Angle';
}
return args.map(function(arg) {
var value = Math.max(0, arg.value);
switch (arg.type) {
case 'Number':
// fit value to [0..255] range
value = Math.min(value, 255);
break;
case 'Percentage':
// convert 0..100% to value in [0..255] range
value = Math.min(value, 100) / 100;
if (!rgb) {
return value;
}
value = 255 * value;
break;
case 'Angle':
// fit value to (-360..360) range
return (((value % 360) + 360) % 360) / 360;
case 'Alpha':
// fit value to [0..1] range
return Math.min(value, 1);
}
return Math.round(value);
});
}
function compressFunction(node, item, list) {
var functionName = node.name;
var args;
if (functionName === 'rgba' || functionName === 'hsla') {
args = parseFunctionArgs(node.arguments, 4, functionName === 'rgba');
if (!args) {
// something went wrong
return;
}
if (functionName === 'hsla') {
args = hslToRgb.apply(null, args);
node.name = 'rgba';
}
if (args[3] !== 1) {
// replace argument values for normalized/interpolated
node.arguments.each(function(argument) {
var item = argument.sequence.head;
if (item.data.type === 'Operator') {
item = item.next;
}
argument.sequence = new List([{
type: 'Number',
info: item.data.info,
value: packNumber(args.shift())
}]);
});
return;
}
// otherwise convert to rgb, i.e. rgba(255, 0, 0, 1) -> rgb(255, 0, 0)
functionName = 'rgb';
}
if (functionName === 'hsl') {
args = args || parseFunctionArgs(node.arguments, 3, false);
if (!args) {
// something went wrong
return;
}
// convert to rgb
args = hslToRgb.apply(null, args);
functionName = 'rgb';
}
if (functionName === 'rgb') {
args = args || parseFunctionArgs(node.arguments, 3, true);
if (!args) {
// something went wrong
return;
}
// check if color is not at the end and not followed by space
var next = item.next;
if (next && next.data.type !== 'Space') {
list.insert(list.createItem({
type: 'Space'
}), next);
}
item.data = {
type: 'Hash',
info: node.info,
value: toHex(args[0]) + toHex(args[1]) + toHex(args[2])
};
compressHex(item.data, item);
}
}
function compressIdent(node, item) {
if (this.declaration === null) {
return;
}
var color = node.name.toLowerCase();
if (NAME_TO_HEX.hasOwnProperty(color)) {
var hex = NAME_TO_HEX[color];
if (hex.length + 1 <= color.length) {
// replace for shorter hex value
item.data = {
type: 'Hash',
info: node.info,
value: hex
};
} else {
// special case for consistent colors
if (color === 'grey') {
color = 'gray';
}
// just replace value for lower cased name
node.name = color;
}
}
}
function compressHex(node, item) {
var color = node.value.toLowerCase();
// #112233 -> #123
if (color.length === 6 &&
color[0] === color[1] &&
color[2] === color[3] &&
color[4] === color[5]) {
color = color[0] + color[2] + color[4];
}
if (HEX_TO_NAME[color]) {
item.data = {
type: 'Identifier',
info: node.info,
name: HEX_TO_NAME[color]
};
} else {
node.value = color;
}
}
module.exports = {
compressFunction: compressFunction,
compressIdent: compressIdent,
compressHex: compressHex
};

View File

@@ -0,0 +1,22 @@
var walk = require('../../utils/walk.js').all;
var handlers = {
Atrule: require('./Atrule.js'),
Attribute: require('./Attribute.js'),
Value: require('./Value.js'),
Dimension: require('./Dimension.js'),
Percentage: require('./Number.js'),
Number: require('./Number.js'),
String: require('./String.js'),
Url: require('./Url.js'),
Hash: require('./color.js').compressHex,
Identifier: require('./color.js').compressIdent,
Function: require('./color.js').compressFunction
};
module.exports = function(ast) {
walk(ast, function(node, item, list) {
if (handlers.hasOwnProperty(node.type)) {
handlers[node.type].call(this, node, item, list);
}
});
};

View File

@@ -0,0 +1,66 @@
var List = require('../../../utils/list.js');
module.exports = function compressBackground(node) {
function lastType() {
if (buffer.length) {
return buffer[buffer.length - 1].type;
}
}
function flush() {
if (lastType() === 'Space') {
buffer.pop();
}
if (!buffer.length) {
buffer.unshift(
{
type: 'Number',
value: '0'
},
{
type: 'Space'
},
{
type: 'Number',
value: '0'
}
);
}
newValue.push.apply(newValue, buffer);
buffer = [];
}
var newValue = [];
var buffer = [];
node.sequence.each(function(node) {
if (node.type === 'Operator' && node.value === ',') {
flush();
newValue.push(node);
return;
}
// remove defaults
if (node.type === 'Identifier') {
if (node.name === 'transparent' ||
node.name === 'none' ||
node.name === 'repeat' ||
node.name === 'scroll') {
return;
}
}
// don't add redundant spaces
if (node.type === 'Space' && (!buffer.length || lastType() === 'Space')) {
return;
}
buffer.push(node);
});
flush();
node.sequence = new List(newValue);
};

View File

@@ -0,0 +1,22 @@
module.exports = function compressFontWeight(node) {
var value = node.sequence.head.data;
if (value.type === 'Identifier') {
switch (value.name) {
case 'normal':
node.sequence.head.data = {
type: 'Number',
info: value.info,
value: '400'
};
break;
case 'bold':
node.sequence.head.data = {
type: 'Number',
info: value.info,
value: '700'
};
break;
}
}
};

View File

@@ -0,0 +1,45 @@
module.exports = function compressFont(node) {
var list = node.sequence;
list.eachRight(function(node, item) {
if (node.type === 'Identifier') {
if (node.name === 'bold') {
item.data = {
type: 'Number',
info: node.info,
value: '700'
};
} else if (node.name === 'normal') {
var prev = item.prev;
if (prev && prev.data.type === 'Operator' && prev.data.value === '/') {
this.remove(prev);
}
this.remove(item);
} else if (node.name === 'medium') {
var next = item.next;
if (!next || next.data.type !== 'Operator') {
this.remove(item);
}
}
}
});
// remove redundant spaces
list.each(function(node, item) {
if (node.type === 'Space') {
if (!item.prev || !item.next || item.next.data.type === 'Space') {
this.remove(item);
}
}
});
if (list.isEmpty()) {
list.insert(list.createItem({
type: 'Identifier',
name: 'normal'
}));
}
};

View File

@@ -0,0 +1,186 @@
var List = require('../utils/list');
var clone = require('../utils/clone');
var usageUtils = require('./usage');
var clean = require('./clean');
var compress = require('./compress');
var restructureBlock = require('./restructure');
var walkRules = require('../utils/walk').rules;
function readRulesChunk(rules, specialComments) {
var buffer = new List();
var nonSpaceTokenInBuffer = false;
var protectedComment;
rules.nextUntil(rules.head, function(node, item, list) {
if (node.type === 'Comment') {
if (!specialComments || node.value.charAt(0) !== '!') {
list.remove(item);
return;
}
if (nonSpaceTokenInBuffer || protectedComment) {
return true;
}
list.remove(item);
protectedComment = node;
return;
}
if (node.type !== 'Space') {
nonSpaceTokenInBuffer = true;
}
buffer.insert(list.remove(item));
});
return {
comment: protectedComment,
stylesheet: {
type: 'StyleSheet',
info: null,
rules: buffer
}
};
}
function compressChunk(ast, firstAtrulesAllowed, usageData, num, logger) {
logger('Compress block #' + num, null, true);
var seed = 1;
walkRules(ast, function markStylesheets() {
if ('id' in this.stylesheet === false) {
this.stylesheet.firstAtrulesAllowed = firstAtrulesAllowed;
this.stylesheet.id = seed++;
}
});
logger('init', ast);
// remove redundant
clean(ast, usageData);
logger('clean', ast);
// compress nodes
compress(ast, usageData);
logger('compress', ast);
return ast;
}
function getCommentsOption(options) {
var comments = 'comments' in options ? options.comments : 'exclamation';
if (typeof comments === 'boolean') {
comments = comments ? 'exclamation' : false;
} else if (comments !== 'exclamation' && comments !== 'first-exclamation') {
comments = false;
}
return comments;
}
function getRestructureOption(options) {
return 'restructure' in options ? options.restructure :
'restructuring' in options ? options.restructuring :
true;
}
function wrapBlock(block) {
return new List([{
type: 'Ruleset',
selector: {
type: 'Selector',
selectors: new List([{
type: 'SimpleSelector',
sequence: new List([{
type: 'Identifier',
name: 'x'
}])
}])
},
block: block
}]);
}
module.exports = function compress(ast, options) {
ast = ast || { type: 'StyleSheet', info: null, rules: new List() };
options = options || {};
var logger = typeof options.logger === 'function' ? options.logger : Function();
var specialComments = getCommentsOption(options);
var restructuring = getRestructureOption(options);
var firstAtrulesAllowed = true;
var usageData = false;
var inputRules;
var outputRules = new List();
var chunk;
var chunkNum = 1;
var chunkRules;
if (options.clone) {
ast = clone(ast);
}
if (ast.type === 'StyleSheet') {
inputRules = ast.rules;
ast.rules = outputRules;
} else {
inputRules = wrapBlock(ast);
}
if (options.usage) {
usageData = usageUtils.buildIndex(options.usage);
}
do {
chunk = readRulesChunk(inputRules, Boolean(specialComments));
compressChunk(chunk.stylesheet, firstAtrulesAllowed, usageData, chunkNum++, logger);
// structure optimisations
if (restructuring) {
restructureBlock(chunk.stylesheet, usageData, logger);
}
chunkRules = chunk.stylesheet.rules;
if (chunk.comment) {
// add \n before comment if there is another content in outputRules
if (!outputRules.isEmpty()) {
outputRules.insert(List.createItem({
type: 'Raw',
value: '\n'
}));
}
outputRules.insert(List.createItem(chunk.comment));
// add \n after comment if chunk is not empty
if (!chunkRules.isEmpty()) {
outputRules.insert(List.createItem({
type: 'Raw',
value: '\n'
}));
}
}
if (firstAtrulesAllowed && !chunkRules.isEmpty()) {
var lastRule = chunkRules.last();
if (lastRule.type !== 'Atrule' ||
(lastRule.name !== 'import' && lastRule.name !== 'charset')) {
firstAtrulesAllowed = false;
}
}
if (specialComments !== 'exclamation') {
specialComments = false;
}
outputRules.appendList(chunkRules);
} while (!inputRules.isEmpty());
return {
ast: ast
};
};

View File

@@ -0,0 +1,48 @@
var utils = require('./utils.js');
var walkRules = require('../../utils/walk.js').rules;
function processRuleset(node, item, list) {
var selectors = node.selector.selectors;
var declarations = node.block.declarations;
list.prevUntil(item.prev, function(prev) {
// skip non-ruleset node if safe
if (prev.type !== 'Ruleset') {
return utils.unsafeToSkipNode.call(selectors, prev);
}
var prevSelectors = prev.selector.selectors;
var prevDeclarations = prev.block.declarations;
// try to join rulesets with equal pseudo signature
if (node.pseudoSignature === prev.pseudoSignature) {
// try to join by selectors
if (utils.isEqualLists(prevSelectors, selectors)) {
prevDeclarations.appendList(declarations);
list.remove(item);
return true;
}
// try to join by declarations
if (utils.isEqualDeclarations(declarations, prevDeclarations)) {
utils.addSelectors(prevSelectors, selectors);
list.remove(item);
return true;
}
}
// go to prev ruleset if has no selector similarities
return utils.hasSimilarSelectors(selectors, prevSelectors);
});
};
// NOTE: direction should be left to right, since rulesets merge to left
// ruleset. When direction right to left unmerged rulesets may prevent lookup
// TODO: remove initial merge
module.exports = function initialMergeRuleset(ast) {
walkRules(ast, function(node, item, list) {
if (node.type === 'Ruleset') {
processRuleset(node, item, list);
}
});
};

View File

@@ -0,0 +1,35 @@
var walkRulesRight = require('../../utils/walk.js').rulesRight;
function isMediaRule(node) {
return node.type === 'Atrule' && node.name === 'media';
}
function processAtrule(node, item, list) {
if (!isMediaRule(node)) {
return;
}
var prev = item.prev && item.prev.data;
if (!prev || !isMediaRule(prev)) {
return;
}
// merge @media with same query
if (node.expression.id === prev.expression.id) {
prev.block.rules.appendList(node.block.rules);
prev.info = {
primary: prev.info,
merged: node.info
};
list.remove(item);
}
};
module.exports = function rejoinAtrule(ast) {
walkRulesRight(ast, function(node, item, list) {
if (node.type === 'Atrule') {
processAtrule(node, item, list);
}
});
};

View File

@@ -0,0 +1,42 @@
var List = require('../../utils/list.js');
var walkRulesRight = require('../../utils/walk.js').rulesRight;
function processRuleset(node, item, list) {
var selectors = node.selector.selectors;
// generate new rule sets:
// .a, .b { color: red; }
// ->
// .a { color: red; }
// .b { color: red; }
// while there are more than 1 simple selector split for rulesets
while (selectors.head !== selectors.tail) {
var newSelectors = new List();
newSelectors.insert(selectors.remove(selectors.head));
list.insert(list.createItem({
type: 'Ruleset',
info: node.info,
pseudoSignature: node.pseudoSignature,
selector: {
type: 'Selector',
info: node.selector.info,
selectors: newSelectors
},
block: {
type: 'Block',
info: node.block.info,
declarations: node.block.declarations.copy()
}
}), item);
}
};
module.exports = function disjoinRuleset(ast) {
walkRulesRight(ast, function(node, item, list) {
if (node.type === 'Ruleset') {
processRuleset(node, item, list);
}
});
};

View File

@@ -0,0 +1,430 @@
var List = require('../../utils/list.js');
var translate = require('../../utils/translate.js');
var walkRulesRight = require('../../utils/walk.js').rulesRight;
var REPLACE = 1;
var REMOVE = 2;
var TOP = 0;
var RIGHT = 1;
var BOTTOM = 2;
var LEFT = 3;
var SIDES = ['top', 'right', 'bottom', 'left'];
var SIDE = {
'margin-top': 'top',
'margin-right': 'right',
'margin-bottom': 'bottom',
'margin-left': 'left',
'padding-top': 'top',
'padding-right': 'right',
'padding-bottom': 'bottom',
'padding-left': 'left',
'border-top-color': 'top',
'border-right-color': 'right',
'border-bottom-color': 'bottom',
'border-left-color': 'left',
'border-top-width': 'top',
'border-right-width': 'right',
'border-bottom-width': 'bottom',
'border-left-width': 'left',
'border-top-style': 'top',
'border-right-style': 'right',
'border-bottom-style': 'bottom',
'border-left-style': 'left'
};
var MAIN_PROPERTY = {
'margin': 'margin',
'margin-top': 'margin',
'margin-right': 'margin',
'margin-bottom': 'margin',
'margin-left': 'margin',
'padding': 'padding',
'padding-top': 'padding',
'padding-right': 'padding',
'padding-bottom': 'padding',
'padding-left': 'padding',
'border-color': 'border-color',
'border-top-color': 'border-color',
'border-right-color': 'border-color',
'border-bottom-color': 'border-color',
'border-left-color': 'border-color',
'border-width': 'border-width',
'border-top-width': 'border-width',
'border-right-width': 'border-width',
'border-bottom-width': 'border-width',
'border-left-width': 'border-width',
'border-style': 'border-style',
'border-top-style': 'border-style',
'border-right-style': 'border-style',
'border-bottom-style': 'border-style',
'border-left-style': 'border-style'
};
function TRBL(name) {
this.name = name;
this.info = null;
this.iehack = undefined;
this.sides = {
'top': null,
'right': null,
'bottom': null,
'left': null
};
}
TRBL.prototype.getValueSequence = function(value, count) {
var values = [];
var iehack = '';
var hasBadValues = value.sequence.some(function(child) {
var special = false;
switch (child.type) {
case 'Identifier':
switch (child.name) {
case '\\0':
case '\\9':
iehack = child.name;
return;
case 'inherit':
case 'initial':
case 'unset':
case 'revert':
special = child.name;
break;
}
break;
case 'Dimension':
switch (child.unit) {
// is not supported until IE11
case 'rem':
// v* units is too buggy across browsers and better
// don't merge values with those units
case 'vw':
case 'vh':
case 'vmin':
case 'vmax':
case 'vm': // IE9 supporting "vm" instead of "vmin".
special = child.unit;
break;
}
break;
case 'Hash': // color
case 'Number':
case 'Percentage':
break;
case 'Function':
special = child.name;
break;
case 'Space':
return false; // ignore space
default:
return true; // bad value
}
values.push({
node: child,
special: special,
important: value.important
});
});
if (hasBadValues || values.length > count) {
return false;
}
if (typeof this.iehack === 'string' && this.iehack !== iehack) {
return false;
}
this.iehack = iehack; // move outside
return values;
};
TRBL.prototype.canOverride = function(side, value) {
var currentValue = this.sides[side];
return !currentValue || (value.important && !currentValue.important);
};
TRBL.prototype.add = function(name, value, info) {
function attemptToAdd() {
var sides = this.sides;
var side = SIDE[name];
if (side) {
if (side in sides === false) {
return false;
}
var values = this.getValueSequence(value, 1);
if (!values || !values.length) {
return false;
}
// can mix only if specials are equal
for (var key in sides) {
if (sides[key] !== null && sides[key].special !== values[0].special) {
return false;
}
}
if (!this.canOverride(side, values[0])) {
return true;
}
sides[side] = values[0];
return true;
} else if (name === this.name) {
var values = this.getValueSequence(value, 4);
if (!values || !values.length) {
return false;
}
switch (values.length) {
case 1:
values[RIGHT] = values[TOP];
values[BOTTOM] = values[TOP];
values[LEFT] = values[TOP];
break;
case 2:
values[BOTTOM] = values[TOP];
values[LEFT] = values[RIGHT];
break;
case 3:
values[LEFT] = values[RIGHT];
break;
}
// can mix only if specials are equal
for (var i = 0; i < 4; i++) {
for (var key in sides) {
if (sides[key] !== null && sides[key].special !== values[i].special) {
return false;
}
}
}
for (var i = 0; i < 4; i++) {
if (this.canOverride(SIDES[i], values[i])) {
sides[SIDES[i]] = values[i];
}
}
return true;
}
}
if (!attemptToAdd.call(this)) {
return false;
}
if (this.info) {
this.info = {
primary: this.info,
merged: info
};
} else {
this.info = info;
}
return true;
};
TRBL.prototype.isOkToMinimize = function() {
var top = this.sides.top;
var right = this.sides.right;
var bottom = this.sides.bottom;
var left = this.sides.left;
if (top && right && bottom && left) {
var important =
top.important +
right.important +
bottom.important +
left.important;
return important === 0 || important === 4;
}
return false;
};
TRBL.prototype.getValue = function() {
var result = [];
var sides = this.sides;
var values = [
sides.top,
sides.right,
sides.bottom,
sides.left
];
var stringValues = [
translate(sides.top.node),
translate(sides.right.node),
translate(sides.bottom.node),
translate(sides.left.node)
];
if (stringValues[LEFT] === stringValues[RIGHT]) {
values.pop();
if (stringValues[BOTTOM] === stringValues[TOP]) {
values.pop();
if (stringValues[RIGHT] === stringValues[TOP]) {
values.pop();
}
}
}
for (var i = 0; i < values.length; i++) {
if (i) {
result.push({ type: 'Space' });
}
result.push(values[i].node);
}
if (this.iehack) {
result.push({ type: 'Space' }, {
type: 'Identifier',
info: {},
name: this.iehack
});
}
return {
type: 'Value',
info: {},
important: sides.top.important,
sequence: new List(result)
};
};
TRBL.prototype.getProperty = function() {
return {
type: 'Property',
info: {},
name: this.name
};
};
function processRuleset(ruleset, shorts, shortDeclarations, lastShortSelector) {
var declarations = ruleset.block.declarations;
var selector = ruleset.selector.selectors.first().id;
ruleset.block.declarations.eachRight(function(declaration, item) {
var property = declaration.property.name;
if (!MAIN_PROPERTY.hasOwnProperty(property)) {
return;
}
var key = MAIN_PROPERTY[property];
var shorthand;
var operation;
if (!lastShortSelector || selector === lastShortSelector) {
if (key in shorts) {
operation = REMOVE;
shorthand = shorts[key];
}
}
if (!shorthand || !shorthand.add(property, declaration.value, declaration.info)) {
operation = REPLACE;
shorthand = new TRBL(key);
// if can't parse value ignore it and break shorthand sequence
if (!shorthand.add(property, declaration.value, declaration.info)) {
lastShortSelector = null;
return;
}
}
shorts[key] = shorthand;
shortDeclarations.push({
operation: operation,
block: declarations,
item: item,
shorthand: shorthand
});
lastShortSelector = selector;
});
return lastShortSelector;
};
function processShorthands(shortDeclarations, markDeclaration) {
shortDeclarations.forEach(function(item) {
var shorthand = item.shorthand;
if (!shorthand.isOkToMinimize()) {
return;
}
if (item.operation === REPLACE) {
item.item.data = markDeclaration({
type: 'Declaration',
info: shorthand.info,
property: shorthand.getProperty(),
value: shorthand.getValue(),
id: 0,
length: 0,
fingerprint: null
});
} else {
item.block.remove(item.item);
}
});
};
module.exports = function restructBlock(ast, indexer) {
var stylesheetMap = {};
var shortDeclarations = [];
walkRulesRight(ast, function(node) {
if (node.type !== 'Ruleset') {
return;
}
var stylesheet = this.stylesheet;
var rulesetId = (node.pseudoSignature || '') + '|' + node.selector.selectors.first().id;
var rulesetMap;
var shorts;
if (!stylesheetMap.hasOwnProperty(stylesheet.id)) {
rulesetMap = {
lastShortSelector: null
};
stylesheetMap[stylesheet.id] = rulesetMap;
} else {
rulesetMap = stylesheetMap[stylesheet.id];
}
if (rulesetMap.hasOwnProperty(rulesetId)) {
shorts = rulesetMap[rulesetId];
} else {
shorts = {};
rulesetMap[rulesetId] = shorts;
}
rulesetMap.lastShortSelector = processRuleset.call(this, node, shorts, shortDeclarations, rulesetMap.lastShortSelector);
});
processShorthands(shortDeclarations, indexer.declaration);
};

View File

@@ -0,0 +1,261 @@
var resolveProperty = require('../../utils/names.js').property;
var resolveKeyword = require('../../utils/names.js').keyword;
var walkRulesRight = require('../../utils/walk.js').rulesRight;
var translate = require('../../utils/translate.js');
var dontRestructure = {
'src': 1 // https://github.com/afelix/csso/issues/50
};
var DONT_MIX_VALUE = {
// https://developer.mozilla.org/en-US/docs/Web/CSS/display#Browser_compatibility
'display': /table|ruby|flex|-(flex)?box$|grid|contents|run-in/i,
// https://developer.mozilla.org/en/docs/Web/CSS/text-align
'text-align': /^(start|end|match-parent|justify-all)$/i
};
var CURSOR_SAFE_VALUE = [
'auto', 'crosshair', 'default', 'move', 'text', 'wait', 'help',
'n-resize', 'e-resize', 's-resize', 'w-resize',
'ne-resize', 'nw-resize', 'se-resize', 'sw-resize',
'pointer', 'progress', 'not-allowed', 'no-drop', 'vertical-text', 'all-scroll',
'col-resize', 'row-resize'
];
var NEEDLESS_TABLE = {
'border-width': ['border'],
'border-style': ['border'],
'border-color': ['border'],
'border-top': ['border'],
'border-right': ['border'],
'border-bottom': ['border'],
'border-left': ['border'],
'border-top-width': ['border-top', 'border-width', 'border'],
'border-right-width': ['border-right', 'border-width', 'border'],
'border-bottom-width': ['border-bottom', 'border-width', 'border'],
'border-left-width': ['border-left', 'border-width', 'border'],
'border-top-style': ['border-top', 'border-style', 'border'],
'border-right-style': ['border-right', 'border-style', 'border'],
'border-bottom-style': ['border-bottom', 'border-style', 'border'],
'border-left-style': ['border-left', 'border-style', 'border'],
'border-top-color': ['border-top', 'border-color', 'border'],
'border-right-color': ['border-right', 'border-color', 'border'],
'border-bottom-color': ['border-bottom', 'border-color', 'border'],
'border-left-color': ['border-left', 'border-color', 'border'],
'margin-top': ['margin'],
'margin-right': ['margin'],
'margin-bottom': ['margin'],
'margin-left': ['margin'],
'padding-top': ['padding'],
'padding-right': ['padding'],
'padding-bottom': ['padding'],
'padding-left': ['padding'],
'font-style': ['font'],
'font-variant': ['font'],
'font-weight': ['font'],
'font-size': ['font'],
'font-family': ['font'],
'list-style-type': ['list-style'],
'list-style-position': ['list-style'],
'list-style-image': ['list-style']
};
function getPropertyFingerprint(propertyName, declaration, fingerprints) {
var realName = resolveProperty(propertyName).name;
if (realName === 'background' ||
(realName === 'filter' && declaration.value.sequence.first().type === 'Progid')) {
return propertyName + ':' + translate(declaration.value);
}
var declarationId = declaration.id;
var fingerprint = fingerprints[declarationId];
if (!fingerprint) {
var vendorId = '';
var iehack = '';
var special = {};
declaration.value.sequence.each(function walk(node) {
switch (node.type) {
case 'Argument':
case 'Value':
case 'Braces':
node.sequence.each(walk);
break;
case 'Identifier':
var name = node.name;
if (!vendorId) {
vendorId = resolveKeyword(name).vendor;
}
if (/\\[09]/.test(name)) {
iehack = RegExp.lastMatch;
}
if (realName === 'cursor') {
if (CURSOR_SAFE_VALUE.indexOf(name) === -1) {
special[name] = true;
}
} else if (DONT_MIX_VALUE.hasOwnProperty(realName)) {
if (DONT_MIX_VALUE[realName].test(name)) {
special[name] = true;
}
}
break;
case 'Function':
var name = node.name;
if (!vendorId) {
vendorId = resolveKeyword(name).vendor;
}
if (name === 'rect') {
// there are 2 forms of rect:
// rect(<top>, <right>, <bottom>, <left>) - standart
// rect(<top> <right> <bottom> <left>) backwards compatible syntax
// only the same form values can be merged
if (node.arguments.size < 4) {
name = 'rect-backward';
}
}
special[name + '()'] = true;
// check nested tokens too
node.arguments.each(walk);
break;
case 'Dimension':
var unit = node.unit;
switch (unit) {
// is not supported until IE11
case 'rem':
// v* units is too buggy across browsers and better
// don't merge values with those units
case 'vw':
case 'vh':
case 'vmin':
case 'vmax':
case 'vm': // IE9 supporting "vm" instead of "vmin".
special[unit] = true;
break;
}
break;
}
});
fingerprint = '|' + Object.keys(special).sort() + '|' + iehack + vendorId;
fingerprints[declarationId] = fingerprint;
}
return propertyName + fingerprint;
}
function needless(props, declaration, fingerprints) {
var property = resolveProperty(declaration.property.name);
if (NEEDLESS_TABLE.hasOwnProperty(property.name)) {
var table = NEEDLESS_TABLE[property.name];
for (var i = 0; i < table.length; i++) {
var ppre = getPropertyFingerprint(property.prefix + table[i], declaration, fingerprints);
var prev = props[ppre];
if (prev && (!declaration.value.important || prev.item.data.value.important)) {
return prev;
}
}
}
}
function processRuleset(ruleset, item, list, props, fingerprints) {
var declarations = ruleset.block.declarations;
declarations.eachRight(function(declaration, declarationItem) {
var property = declaration.property.name;
var fingerprint = getPropertyFingerprint(property, declaration, fingerprints);
var prev = props[fingerprint];
if (prev && !dontRestructure.hasOwnProperty(property)) {
if (declaration.value.important && !prev.item.data.value.important) {
props[fingerprint] = {
block: declarations,
item: declarationItem
};
prev.block.remove(prev.item);
declaration.info = {
primary: declaration.info,
merged: prev.item.data.info
};
} else {
declarations.remove(declarationItem);
prev.item.data.info = {
primary: prev.item.data.info,
merged: declaration.info
};
}
} else {
var prev = needless(props, declaration, fingerprints);
if (prev) {
declarations.remove(declarationItem);
prev.item.data.info = {
primary: prev.item.data.info,
merged: declaration.info
};
} else {
declaration.fingerprint = fingerprint;
props[fingerprint] = {
block: declarations,
item: declarationItem
};
}
}
});
if (declarations.isEmpty()) {
list.remove(item);
}
};
module.exports = function restructBlock(ast) {
var stylesheetMap = {};
var fingerprints = Object.create(null);
walkRulesRight(ast, function(node, item, list) {
if (node.type !== 'Ruleset') {
return;
}
var stylesheet = this.stylesheet;
var rulesetId = (node.pseudoSignature || '') + '|' + node.selector.selectors.first().id;
var rulesetMap;
var props;
if (!stylesheetMap.hasOwnProperty(stylesheet.id)) {
rulesetMap = {};
stylesheetMap[stylesheet.id] = rulesetMap;
} else {
rulesetMap = stylesheetMap[stylesheet.id];
}
if (rulesetMap.hasOwnProperty(rulesetId)) {
props = rulesetMap[rulesetId];
} else {
props = {};
rulesetMap[rulesetId] = props;
}
processRuleset.call(this, node, item, list, props, fingerprints);
});
};

View File

@@ -0,0 +1,87 @@
var utils = require('./utils.js');
var walkRules = require('../../utils/walk.js').rules;
/*
At this step all rules has single simple selector. We try to join by equal
declaration blocks to first rule, e.g.
.a { color: red }
b { ... }
.b { color: red }
->
.a, .b { color: red }
b { ... }
*/
function processRuleset(node, item, list) {
var selectors = node.selector.selectors;
var declarations = node.block.declarations;
var nodeCompareMarker = selectors.first().compareMarker;
var skippedCompareMarkers = {};
list.nextUntil(item.next, function(next, nextItem) {
// skip non-ruleset node if safe
if (next.type !== 'Ruleset') {
return utils.unsafeToSkipNode.call(selectors, next);
}
if (node.pseudoSignature !== next.pseudoSignature) {
return true;
}
var nextFirstSelector = next.selector.selectors.head;
var nextDeclarations = next.block.declarations;
var nextCompareMarker = nextFirstSelector.data.compareMarker;
// if next ruleset has same marked as one of skipped then stop joining
if (nextCompareMarker in skippedCompareMarkers) {
return true;
}
// try to join by selectors
if (selectors.head === selectors.tail) {
if (selectors.first().id === nextFirstSelector.data.id) {
declarations.appendList(nextDeclarations);
list.remove(nextItem);
return;
}
}
// try to join by properties
if (utils.isEqualDeclarations(declarations, nextDeclarations)) {
var nextStr = nextFirstSelector.data.id;
selectors.some(function(data, item) {
var curStr = data.id;
if (nextStr < curStr) {
selectors.insert(nextFirstSelector, item);
return true;
}
if (!item.next) {
selectors.insert(nextFirstSelector);
return true;
}
});
list.remove(nextItem);
return;
}
// go to next ruleset if current one can be skipped (has no equal specificity nor element selector)
if (nextCompareMarker === nodeCompareMarker) {
return true;
}
skippedCompareMarkers[nextCompareMarker] = true;
});
};
module.exports = function mergeRuleset(ast) {
walkRules(ast, function(node, item, list) {
if (node.type === 'Ruleset') {
processRuleset(node, item, list);
}
});
};

View File

@@ -0,0 +1,157 @@
var List = require('../../utils/list.js');
var utils = require('./utils.js');
var walkRulesRight = require('../../utils/walk.js').rulesRight;
function calcSelectorLength(list) {
var length = 0;
list.each(function(data) {
length += data.id.length + 1;
});
return length - 1;
}
function calcDeclarationsLength(tokens) {
var length = 0;
for (var i = 0; i < tokens.length; i++) {
length += tokens[i].length;
}
return (
length + // declarations
tokens.length - 1 // delimeters
);
}
function processRuleset(node, item, list) {
var avoidRulesMerge = this.stylesheet.avoidRulesMerge;
var selectors = node.selector.selectors;
var block = node.block;
var disallowDownMarkers = Object.create(null);
var allowMergeUp = true;
var allowMergeDown = true;
list.prevUntil(item.prev, function(prev, prevItem) {
// skip non-ruleset node if safe
if (prev.type !== 'Ruleset') {
return utils.unsafeToSkipNode.call(selectors, prev);
}
var prevSelectors = prev.selector.selectors;
var prevBlock = prev.block;
if (node.pseudoSignature !== prev.pseudoSignature) {
return true;
}
allowMergeDown = !prevSelectors.some(function(selector) {
return selector.compareMarker in disallowDownMarkers;
});
// try prev ruleset if simpleselectors has no equal specifity and element selector
if (!allowMergeDown && !allowMergeUp) {
return true;
}
// try to join by selectors
if (allowMergeUp && utils.isEqualLists(prevSelectors, selectors)) {
prevBlock.declarations.appendList(block.declarations);
list.remove(item);
return true;
}
// try to join by properties
var diff = utils.compareDeclarations(block.declarations, prevBlock.declarations);
// console.log(diff.eq, diff.ne1, diff.ne2);
if (diff.eq.length) {
if (!diff.ne1.length && !diff.ne2.length) {
// equal blocks
if (allowMergeDown) {
utils.addSelectors(selectors, prevSelectors);
list.remove(prevItem);
}
return true;
} else if (!avoidRulesMerge) { /* probably we don't need to prevent those merges for @keyframes
TODO: need to be checked */
if (diff.ne1.length && !diff.ne2.length) {
// prevBlock is subset block
var selectorLength = calcSelectorLength(selectors);
var blockLength = calcDeclarationsLength(diff.eq); // declarations length
if (allowMergeUp && selectorLength < blockLength) {
utils.addSelectors(prevSelectors, selectors);
block.declarations = new List(diff.ne1);
}
} else if (!diff.ne1.length && diff.ne2.length) {
// node is subset of prevBlock
var selectorLength = calcSelectorLength(prevSelectors);
var blockLength = calcDeclarationsLength(diff.eq); // declarations length
if (allowMergeDown && selectorLength < blockLength) {
utils.addSelectors(selectors, prevSelectors);
prevBlock.declarations = new List(diff.ne2);
}
} else {
// diff.ne1.length && diff.ne2.length
// extract equal block
var newSelector = {
type: 'Selector',
info: {},
selectors: utils.addSelectors(prevSelectors.copy(), selectors)
};
var newBlockLength = calcSelectorLength(newSelector.selectors) + 2; // selectors length + curly braces length
var blockLength = calcDeclarationsLength(diff.eq); // declarations length
// create new ruleset if declarations length greater than
// ruleset description overhead
if (allowMergeDown && blockLength >= newBlockLength) {
var newRuleset = {
type: 'Ruleset',
info: {},
pseudoSignature: node.pseudoSignature,
selector: newSelector,
block: {
type: 'Block',
info: {},
declarations: new List(diff.eq)
}
};
block.declarations = new List(diff.ne1);
prevBlock.declarations = new List(diff.ne2.concat(diff.ne2overrided));
list.insert(list.createItem(newRuleset), prevItem);
return true;
}
}
}
}
if (allowMergeUp) {
// TODO: disallow up merge only if any property interception only (i.e. diff.ne2overrided.length > 0);
// await property families to find property interception correctly
allowMergeUp = !prevSelectors.some(function(prevSelector) {
return selectors.some(function(selector) {
return selector.compareMarker === prevSelector.compareMarker;
});
});
}
prevSelectors.each(function(data) {
disallowDownMarkers[data.compareMarker] = true;
});
});
};
module.exports = function restructRuleset(ast) {
walkRulesRight(ast, function(node, item, list) {
if (node.type === 'Ruleset') {
processRuleset.call(this, node, item, list);
}
});
};

View File

@@ -0,0 +1,35 @@
var prepare = require('./prepare/index.js');
var initialMergeRuleset = require('./1-initialMergeRuleset.js');
var mergeAtrule = require('./2-mergeAtrule.js');
var disjoinRuleset = require('./3-disjoinRuleset.js');
var restructShorthand = require('./4-restructShorthand.js');
var restructBlock = require('./6-restructBlock.js');
var mergeRuleset = require('./7-mergeRuleset.js');
var restructRuleset = require('./8-restructRuleset.js');
module.exports = function(ast, usageData, debug) {
// prepare ast for restructing
var indexer = prepare(ast, usageData);
debug('prepare', ast);
initialMergeRuleset(ast);
debug('initialMergeRuleset', ast);
mergeAtrule(ast);
debug('mergeAtrule', ast);
disjoinRuleset(ast);
debug('disjoinRuleset', ast);
restructShorthand(ast, indexer);
debug('restructShorthand', ast);
restructBlock(ast);
debug('restructBlock', ast);
mergeRuleset(ast);
debug('mergeRuleset', ast);
restructRuleset(ast);
debug('restructRuleset', ast);
};

View File

@@ -0,0 +1,32 @@
var translate = require('../../../utils/translate.js');
function Index() {
this.seed = 0;
this.map = Object.create(null);
}
Index.prototype.resolve = function(str) {
var index = this.map[str];
if (!index) {
index = ++this.seed;
this.map[str] = index;
}
return index;
};
module.exports = function createDeclarationIndexer() {
var names = new Index();
var values = new Index();
return function markDeclaration(node) {
var property = node.property.name;
var value = translate(node.value);
node.id = names.resolve(property) + (values.resolve(value) << 12);
node.length = property.length + 1 + value.length;
return node;
};
};

View File

@@ -0,0 +1,44 @@
var resolveKeyword = require('../../../utils/names.js').keyword;
var walkRules = require('../../../utils/walk.js').rules;
var translate = require('../../../utils/translate.js');
var createDeclarationIndexer = require('./createDeclarationIndexer.js');
var processSelector = require('./processSelector.js');
function walk(node, markDeclaration, usageData) {
switch (node.type) {
case 'Ruleset':
node.block.declarations.each(markDeclaration);
processSelector(node, usageData);
break;
case 'Atrule':
if (node.expression) {
node.expression.id = translate(node.expression);
}
// compare keyframe selectors by its values
// NOTE: still no clarification about problems with keyframes selector grouping (issue #197)
if (resolveKeyword(node.name).name === 'keyframes') {
node.block.avoidRulesMerge = true; /* probably we don't need to prevent those merges for @keyframes
TODO: need to be checked */
node.block.rules.each(function(ruleset) {
ruleset.selector.selectors.each(function(simpleselector) {
simpleselector.compareMarker = simpleselector.id;
});
});
}
break;
}
};
module.exports = function prepare(ast, usageData) {
var markDeclaration = createDeclarationIndexer();
walkRules(ast, function(node) {
walk(node, markDeclaration, usageData);
});
return {
declaration: markDeclaration
};
};

View File

@@ -0,0 +1,99 @@
var translate = require('../../../utils/translate.js');
var specificity = require('./specificity.js');
var nonFreezePseudoElements = {
'first-letter': true,
'first-line': true,
'after': true,
'before': true
};
var nonFreezePseudoClasses = {
'link': true,
'visited': true,
'hover': true,
'active': true,
'first-letter': true,
'first-line': true,
'after': true,
'before': true
};
module.exports = function freeze(node, usageData) {
var pseudos = Object.create(null);
var hasPseudo = false;
node.selector.selectors.each(function(simpleSelector) {
var tagName = '*';
var scope = 0;
simpleSelector.sequence.some(function(node) {
switch (node.type) {
case 'Class':
if (usageData && usageData.scopes) {
var classScope = usageData.scopes[node.name] || 0;
if (scope !== 0 && classScope !== scope) {
throw new Error('Selector can\'t has classes from different scopes: ' + translate(simpleSelector));
}
scope = classScope;
}
break;
case 'PseudoClass':
if (!nonFreezePseudoClasses.hasOwnProperty(node.name)) {
pseudos[node.name] = true;
hasPseudo = true;
}
break;
case 'PseudoElement':
if (!nonFreezePseudoElements.hasOwnProperty(node.name)) {
pseudos[node.name] = true;
hasPseudo = true;
}
break;
case 'FunctionalPseudo':
pseudos[node.name] = true;
hasPseudo = true;
break;
case 'Negation':
pseudos.not = true;
hasPseudo = true;
break;
case 'Identifier':
tagName = node.name;
break;
case 'Attribute':
if (node.flags) {
pseudos['[' + node.flags + ']'] = true;
hasPseudo = true;
}
break;
case 'Combinator':
tagName = '*';
break;
}
});
simpleSelector.id = translate(simpleSelector);
simpleSelector.compareMarker = specificity(simpleSelector).toString();
if (scope) {
simpleSelector.compareMarker += ':' + scope;
}
if (tagName !== '*') {
simpleSelector.compareMarker += ',' + tagName;
}
});
if (hasPseudo) {
node.pseudoSignature = Object.keys(pseudos).sort().join(',');
}
};

View File

@@ -0,0 +1,48 @@
module.exports = function specificity(simpleSelector) {
var A = 0;
var B = 0;
var C = 0;
simpleSelector.sequence.each(function walk(data) {
switch (data.type) {
case 'SimpleSelector':
case 'Negation':
data.sequence.each(walk);
break;
case 'Id':
A++;
break;
case 'Class':
case 'Attribute':
case 'FunctionalPseudo':
B++;
break;
case 'Identifier':
if (data.name !== '*') {
C++;
}
break;
case 'PseudoElement':
C++;
break;
case 'PseudoClass':
var name = data.name.toLowerCase();
if (name === 'before' ||
name === 'after' ||
name === 'first-line' ||
name === 'first-letter') {
C++;
} else {
B++;
}
break;
}
});
return [A, B, C];
};

View File

@@ -0,0 +1,141 @@
var hasOwnProperty = Object.prototype.hasOwnProperty;
function isEqualLists(a, b) {
var cursor1 = a.head;
var cursor2 = b.head;
while (cursor1 !== null && cursor2 !== null && cursor1.data.id === cursor2.data.id) {
cursor1 = cursor1.next;
cursor2 = cursor2.next;
}
return cursor1 === null && cursor2 === null;
}
function isEqualDeclarations(a, b) {
var cursor1 = a.head;
var cursor2 = b.head;
while (cursor1 !== null && cursor2 !== null && cursor1.data.id === cursor2.data.id) {
cursor1 = cursor1.next;
cursor2 = cursor2.next;
}
return cursor1 === null && cursor2 === null;
}
function compareDeclarations(declarations1, declarations2) {
var result = {
eq: [],
ne1: [],
ne2: [],
ne2overrided: []
};
var fingerprints = Object.create(null);
var declarations2hash = Object.create(null);
for (var cursor = declarations2.head; cursor; cursor = cursor.next) {
declarations2hash[cursor.data.id] = true;
}
for (var cursor = declarations1.head; cursor; cursor = cursor.next) {
var data = cursor.data;
if (data.fingerprint) {
fingerprints[data.fingerprint] = data.value.important;
}
if (declarations2hash[data.id]) {
declarations2hash[data.id] = false;
result.eq.push(data);
} else {
result.ne1.push(data);
}
}
for (var cursor = declarations2.head; cursor; cursor = cursor.next) {
var data = cursor.data;
if (declarations2hash[data.id]) {
// if declarations1 has overriding declaration, this is not a difference
// but take in account !important - prev should be equal or greater than follow
if (hasOwnProperty.call(fingerprints, data.fingerprint) &&
Number(fingerprints[data.fingerprint]) >= Number(data.value.important)) {
result.ne2overrided.push(data);
} else {
result.ne2.push(data);
}
}
}
return result;
}
function addSelectors(dest, source) {
source.each(function(sourceData) {
var newStr = sourceData.id;
var cursor = dest.head;
while (cursor) {
var nextStr = cursor.data.id;
if (nextStr === newStr) {
return;
}
if (nextStr > newStr) {
break;
}
cursor = cursor.next;
}
dest.insert(dest.createItem(sourceData), cursor);
});
return dest;
}
// check if simpleselectors has no equal specificity and element selector
function hasSimilarSelectors(selectors1, selectors2) {
return selectors1.some(function(a) {
return selectors2.some(function(b) {
return a.compareMarker === b.compareMarker;
});
});
}
// test node can't to be skipped
function unsafeToSkipNode(node) {
switch (node.type) {
case 'Ruleset':
// unsafe skip ruleset with selector similarities
return hasSimilarSelectors(node.selector.selectors, this);
case 'Atrule':
// can skip at-rules with blocks
if (node.block) {
// non-stylesheet blocks are safe to skip since have no selectors
if (node.block.type !== 'StyleSheet') {
return false;
}
// unsafe skip at-rule if block contains something unsafe to skip
return node.block.rules.some(unsafeToSkipNode, this);
}
break;
}
// unsafe by default
return true;
}
module.exports = {
isEqualLists: isEqualLists,
isEqualDeclarations: isEqualDeclarations,
compareDeclarations: compareDeclarations,
addSelectors: addSelectors,
hasSimilarSelectors: hasSimilarSelectors,
unsafeToSkipNode: unsafeToSkipNode
};

View File

@@ -0,0 +1,58 @@
var hasOwnProperty = Object.prototype.hasOwnProperty;
function buildMap(list, caseInsensitive) {
var map = Object.create(null);
if (!Array.isArray(list)) {
return false;
}
for (var i = 0; i < list.length; i++) {
var name = list[i];
if (caseInsensitive) {
name = name.toLowerCase();
}
map[name] = true;
}
return map;
}
function buildIndex(data) {
var scopes = false;
if (data.scopes && Array.isArray(data.scopes)) {
scopes = Object.create(null);
for (var i = 0; i < data.scopes.length; i++) {
var list = data.scopes[i];
if (!list || !Array.isArray(list)) {
throw new Error('Wrong usage format');
}
for (var j = 0; j < list.length; j++) {
var name = list[j];
if (hasOwnProperty.call(scopes, name)) {
throw new Error('Class can\'t be used for several scopes: ' + name);
}
scopes[name] = i + 1;
}
}
}
return {
tags: buildMap(data.tags, true),
ids: buildMap(data.ids),
classes: buildMap(data.classes),
scopes: scopes
};
}
module.exports = {
buildIndex: buildIndex
};

View File

@@ -0,0 +1,156 @@
var parse = require('./parser');
var compress = require('./compressor');
var translate = require('./utils/translate');
var translateWithSourceMap = require('./utils/translateWithSourceMap');
var walkers = require('./utils/walk');
var clone = require('./utils/clone');
var List = require('./utils/list');
function debugOutput(name, options, startTime, data) {
if (options.debug) {
console.error('## ' + name + ' done in %d ms\n', Date.now() - startTime);
}
return data;
}
function createDefaultLogger(level) {
var lastDebug;
return function logger(title, ast) {
var line = title;
if (ast) {
line = '[' + ((Date.now() - lastDebug) / 1000).toFixed(3) + 's] ' + line;
}
if (level > 1 && ast) {
var css = translate(ast, true);
// when level 2, limit css to 256 symbols
if (level === 2 && css.length > 256) {
css = css.substr(0, 256) + '...';
}
line += '\n ' + css + '\n';
}
console.error(line);
lastDebug = Date.now();
};
}
function copy(obj) {
var result = {};
for (var key in obj) {
result[key] = obj[key];
}
return result;
}
function buildCompressOptions(options) {
options = copy(options);
if (typeof options.logger !== 'function' && options.debug) {
options.logger = createDefaultLogger(options.debug);
}
return options;
}
function runHandler(ast, options, handlers) {
if (!Array.isArray(handlers)) {
handlers = [handlers];
}
handlers.forEach(function(fn) {
fn(ast, options);
});
}
function minify(context, source, options) {
options = options || {};
var filename = options.filename || '<unknown>';
var result;
// parse
var ast = debugOutput('parsing', options, Date.now(),
parse(source, {
context: context,
filename: filename,
positions: Boolean(options.sourceMap)
})
);
// before compress handlers
if (options.beforeCompress) {
debugOutput('beforeCompress', options, Date.now(),
runHandler(ast, options, options.beforeCompress)
);
}
// compress
var compressResult = debugOutput('compress', options, Date.now(),
compress(ast, buildCompressOptions(options))
);
// after compress handlers
if (options.afterCompress) {
debugOutput('afterCompress', options, Date.now(),
runHandler(compressResult, options, options.afterCompress)
);
}
// translate
if (options.sourceMap) {
result = debugOutput('translateWithSourceMap', options, Date.now(), (function() {
var tmp = translateWithSourceMap(compressResult.ast);
tmp.map._file = filename; // since other tools can relay on file in source map transform chain
tmp.map.setSourceContent(filename, source);
return tmp;
})());
} else {
result = debugOutput('translate', options, Date.now(), {
css: translate(compressResult.ast),
map: null
});
}
return result;
}
function minifyStylesheet(source, options) {
return minify('stylesheet', source, options);
};
function minifyBlock(source, options) {
return minify('block', source, options);
}
module.exports = {
version: require('../package.json').version,
// classes
List: List,
// main methods
minify: minifyStylesheet,
minifyBlock: minifyBlock,
// step by step
parse: parse,
compress: compress,
translate: translate,
translateWithSourceMap: translateWithSourceMap,
// walkers
walk: walkers.all,
walkRules: walkers.rules,
walkRulesRight: walkers.rulesRight,
// utils
clone: clone
};

View File

@@ -0,0 +1,46 @@
exports.TokenType = {
String: 'String',
Comment: 'Comment',
Unknown: 'Unknown',
Newline: 'Newline',
Space: 'Space',
Tab: 'Tab',
ExclamationMark: 'ExclamationMark', // !
QuotationMark: 'QuotationMark', // "
NumberSign: 'NumberSign', // #
DollarSign: 'DollarSign', // $
PercentSign: 'PercentSign', // %
Ampersand: 'Ampersand', // &
Apostrophe: 'Apostrophe', // '
LeftParenthesis: 'LeftParenthesis', // (
RightParenthesis: 'RightParenthesis', // )
Asterisk: 'Asterisk', // *
PlusSign: 'PlusSign', // +
Comma: 'Comma', // ,
HyphenMinus: 'HyphenMinus', // -
FullStop: 'FullStop', // .
Solidus: 'Solidus', // /
Colon: 'Colon', // :
Semicolon: 'Semicolon', // ;
LessThanSign: 'LessThanSign', // <
EqualsSign: 'EqualsSign', // =
GreaterThanSign: 'GreaterThanSign', // >
QuestionMark: 'QuestionMark', // ?
CommercialAt: 'CommercialAt', // @
LeftSquareBracket: 'LeftSquareBracket', // [
ReverseSolidus: 'ReverseSolidus', // \
RightSquareBracket: 'RightSquareBracket', // ]
CircumflexAccent: 'CircumflexAccent', // ^
LowLine: 'LowLine', // _
LeftCurlyBracket: 'LeftCurlyBracket', // {
VerticalLine: 'VerticalLine', // |
RightCurlyBracket: 'RightCurlyBracket', // }
Tilde: 'Tilde', // ~
Identifier: 'Identifier',
DecimalNumber: 'DecimalNumber'
};
// var i = 1;
// for (var key in exports.TokenType) {
// exports.TokenType[key] = i++;
// }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,380 @@
'use strict';
var TokenType = require('./const.js').TokenType;
var TAB = 9;
var N = 10;
var F = 12;
var R = 13;
var SPACE = 32;
var DOUBLE_QUOTE = 34;
var QUOTE = 39;
var RIGHT_PARENTHESIS = 41;
var STAR = 42;
var SLASH = 47;
var BACK_SLASH = 92;
var UNDERSCORE = 95;
var LEFT_CURLY_BRACE = 123;
var RIGHT_CURLY_BRACE = 125;
var WHITESPACE = 1;
var PUNCTUATOR = 2;
var DIGIT = 3;
var STRING = 4;
var PUNCTUATION = {
9: TokenType.Tab, // '\t'
10: TokenType.Newline, // '\n'
13: TokenType.Newline, // '\r'
32: TokenType.Space, // ' '
33: TokenType.ExclamationMark, // '!'
34: TokenType.QuotationMark, // '"'
35: TokenType.NumberSign, // '#'
36: TokenType.DollarSign, // '$'
37: TokenType.PercentSign, // '%'
38: TokenType.Ampersand, // '&'
39: TokenType.Apostrophe, // '\''
40: TokenType.LeftParenthesis, // '('
41: TokenType.RightParenthesis, // ')'
42: TokenType.Asterisk, // '*'
43: TokenType.PlusSign, // '+'
44: TokenType.Comma, // ','
45: TokenType.HyphenMinus, // '-'
46: TokenType.FullStop, // '.'
47: TokenType.Solidus, // '/'
58: TokenType.Colon, // ':'
59: TokenType.Semicolon, // ';'
60: TokenType.LessThanSign, // '<'
61: TokenType.EqualsSign, // '='
62: TokenType.GreaterThanSign, // '>'
63: TokenType.QuestionMark, // '?'
64: TokenType.CommercialAt, // '@'
91: TokenType.LeftSquareBracket, // '['
93: TokenType.RightSquareBracket, // ']'
94: TokenType.CircumflexAccent, // '^'
95: TokenType.LowLine, // '_'
123: TokenType.LeftCurlyBracket, // '{'
124: TokenType.VerticalLine, // '|'
125: TokenType.RightCurlyBracket, // '}'
126: TokenType.Tilde // '~'
};
var SYMBOL_CATEGORY_LENGTH = Math.max.apply(null, Object.keys(PUNCTUATION)) + 1;
var SYMBOL_CATEGORY = new Uint32Array(SYMBOL_CATEGORY_LENGTH);
var IS_PUNCTUATOR = new Uint32Array(SYMBOL_CATEGORY_LENGTH);
// fill categories
Object.keys(PUNCTUATION).forEach(function(key) {
SYMBOL_CATEGORY[Number(key)] = PUNCTUATOR;
IS_PUNCTUATOR[Number(key)] = PUNCTUATOR;
}, SYMBOL_CATEGORY);
// don't treat as punctuator
IS_PUNCTUATOR[UNDERSCORE] = 0;
for (var i = 48; i <= 57; i++) {
SYMBOL_CATEGORY[i] = DIGIT;
}
SYMBOL_CATEGORY[SPACE] = WHITESPACE;
SYMBOL_CATEGORY[TAB] = WHITESPACE;
SYMBOL_CATEGORY[N] = WHITESPACE;
SYMBOL_CATEGORY[R] = WHITESPACE;
SYMBOL_CATEGORY[F] = WHITESPACE;
SYMBOL_CATEGORY[QUOTE] = STRING;
SYMBOL_CATEGORY[DOUBLE_QUOTE] = STRING;
//
// scanner
//
var Scanner = function(source, initBlockMode, initLine, initColumn) {
this.source = source;
this.pos = source.charCodeAt(0) === 0xFEFF ? 1 : 0;
this.eof = this.pos === this.source.length;
this.line = typeof initLine === 'undefined' ? 1 : initLine;
this.lineStartPos = typeof initColumn === 'undefined' ? -1 : -initColumn;
this.minBlockMode = initBlockMode ? 1 : 0;
this.blockMode = this.minBlockMode;
this.urlMode = false;
this.prevToken = null;
this.token = null;
this.buffer = [];
};
Scanner.prototype = {
lookup: function(offset) {
if (offset === 0) {
return this.token;
}
for (var i = this.buffer.length; !this.eof && i < offset; i++) {
this.buffer.push(this.getToken());
}
return offset <= this.buffer.length ? this.buffer[offset - 1] : null;
},
lookupType: function(offset, type) {
var token = this.lookup(offset);
return token !== null && token.type === type;
},
next: function() {
var newToken = null;
if (this.buffer.length !== 0) {
newToken = this.buffer.shift();
} else if (!this.eof) {
newToken = this.getToken();
}
this.prevToken = this.token;
this.token = newToken;
return newToken;
},
tokenize: function() {
var tokens = [];
for (; this.pos < this.source.length; this.pos++) {
tokens.push(this.getToken());
}
return tokens;
},
getToken: function() {
var code = this.source.charCodeAt(this.pos);
var line = this.line;
var column = this.pos - this.lineStartPos;
var offset = this.pos;
var next;
var type;
var value;
switch (code < SYMBOL_CATEGORY_LENGTH ? SYMBOL_CATEGORY[code] : 0) {
case DIGIT:
type = TokenType.DecimalNumber;
value = this.readDecimalNumber();
break;
case STRING:
type = TokenType.String;
value = this.readString(code);
break;
case WHITESPACE:
type = TokenType.Space;
value = this.readSpaces();
break;
case PUNCTUATOR:
if (code === SLASH) {
next = this.pos + 1 < this.source.length ? this.source.charCodeAt(this.pos + 1) : 0;
if (next === STAR) { // /*
type = TokenType.Comment;
value = this.readComment();
break;
} else if (next === SLASH && !this.urlMode) { // //
if (this.blockMode > 0) {
var skip = 2;
while (this.source.charCodeAt(this.pos + 2) === SLASH) {
skip++;
}
type = TokenType.Identifier;
value = this.readIdentifier(skip);
this.urlMode = this.urlMode || value === 'url';
} else {
type = TokenType.Unknown;
value = this.readUnknown();
}
break;
}
}
type = PUNCTUATION[code];
value = String.fromCharCode(code);
this.pos++;
if (code === RIGHT_PARENTHESIS) {
this.urlMode = false;
} else if (code === LEFT_CURLY_BRACE) {
this.blockMode++;
} else if (code === RIGHT_CURLY_BRACE) {
if (this.blockMode > this.minBlockMode) {
this.blockMode--;
}
}
break;
default:
type = TokenType.Identifier;
value = this.readIdentifier(0);
this.urlMode = this.urlMode || value === 'url';
}
this.eof = this.pos === this.source.length;
return {
type: type,
value: value,
offset: offset,
line: line,
column: column
};
},
isNewline: function(code) {
if (code === N || code === F || code === R) {
if (code === R && this.pos + 1 < this.source.length && this.source.charCodeAt(this.pos + 1) === N) {
this.pos++;
}
this.line++;
this.lineStartPos = this.pos;
return true;
}
return false;
},
readSpaces: function() {
var start = this.pos;
for (; this.pos < this.source.length; this.pos++) {
var code = this.source.charCodeAt(this.pos);
if (!this.isNewline(code) && code !== SPACE && code !== TAB) {
break;
}
}
return this.source.substring(start, this.pos);
},
readComment: function() {
var start = this.pos;
for (this.pos += 2; this.pos < this.source.length; this.pos++) {
var code = this.source.charCodeAt(this.pos);
if (code === STAR) { // */
if (this.source.charCodeAt(this.pos + 1) === SLASH) {
this.pos += 2;
break;
}
} else {
this.isNewline(code);
}
}
return this.source.substring(start, this.pos);
},
readUnknown: function() {
var start = this.pos;
for (this.pos += 2; this.pos < this.source.length; this.pos++) {
if (this.isNewline(this.source.charCodeAt(this.pos), this.source)) {
break;
}
}
return this.source.substring(start, this.pos);
},
readString: function(quote) {
var start = this.pos;
var res = '';
for (this.pos++; this.pos < this.source.length; this.pos++) {
var code = this.source.charCodeAt(this.pos);
if (code === BACK_SLASH) {
var end = this.pos++;
if (this.isNewline(this.source.charCodeAt(this.pos), this.source)) {
res += this.source.substring(start, end);
start = this.pos + 1;
}
} else if (code === quote) {
this.pos++;
break;
}
}
return res + this.source.substring(start, this.pos);
},
readDecimalNumber: function() {
var start = this.pos;
var code;
for (this.pos++; this.pos < this.source.length; this.pos++) {
code = this.source.charCodeAt(this.pos);
if (code < 48 || code > 57) { // 0 .. 9
break;
}
}
return this.source.substring(start, this.pos);
},
readIdentifier: function(skip) {
var start = this.pos;
for (this.pos += skip; this.pos < this.source.length; this.pos++) {
var code = this.source.charCodeAt(this.pos);
if (code === BACK_SLASH) {
this.pos++;
// skip escaped unicode sequence that can ends with space
// [0-9a-f]{1,6}(\r\n|[ \n\r\t\f])?
for (var i = 0; i < 7 && this.pos + i < this.source.length; i++) {
code = this.source.charCodeAt(this.pos + i);
if (i !== 6) {
if ((code >= 48 && code <= 57) || // 0 .. 9
(code >= 65 && code <= 70) || // A .. F
(code >= 97 && code <= 102)) { // a .. f
continue;
}
}
if (i > 0) {
this.pos += i - 1;
if (code === SPACE || code === TAB || this.isNewline(code)) {
this.pos++;
}
}
break;
}
} else if (code < SYMBOL_CATEGORY_LENGTH &&
IS_PUNCTUATOR[code] === PUNCTUATOR) {
break;
}
}
return this.source.substring(start, this.pos);
}
};
// warm up tokenizer to elimitate code branches that never execute
// fix soft deoptimizations (insufficient type feedback)
new Scanner('\n\r\r\n\f//""\'\'/**/1a;.{url(a)}').lookup(1e3);
module.exports = Scanner;

View File

@@ -0,0 +1,23 @@
var List = require('./list');
module.exports = function clone(node) {
var result = {};
for (var key in node) {
var value = node[key];
if (value) {
if (Array.isArray(value)) {
value = value.slice(0);
} else if (value instanceof List) {
value = new List(value.map(clone));
} else if (value.constructor === Object) {
value = clone(value);
}
}
result[key] = value;
}
return result;
};

View File

@@ -0,0 +1,389 @@
//
// item item item item
// /------\ /------\ /------\ /------\
// | data | | data | | data | | data |
// null <--+-prev |<---+-prev |<---+-prev |<---+-prev |
// | next-+--->| next-+--->| next-+--->| next-+--> null
// \------/ \------/ \------/ \------/
// ^ ^
// | list |
// | /------\ |
// \--------------+-head | |
// | tail-+--------------/
// \------/
//
function createItem(data) {
return {
data: data,
next: null,
prev: null
};
}
var List = function(values) {
this.cursor = null;
this.head = null;
this.tail = null;
if (Array.isArray(values)) {
var cursor = null;
for (var i = 0; i < values.length; i++) {
var item = createItem(values[i]);
if (cursor !== null) {
cursor.next = item;
} else {
this.head = item;
}
item.prev = cursor;
cursor = item;
}
this.tail = cursor;
}
};
Object.defineProperty(List.prototype, 'size', {
get: function() {
var size = 0;
var cursor = this.head;
while (cursor) {
size++;
cursor = cursor.next;
}
return size;
}
});
List.createItem = createItem;
List.prototype.createItem = createItem;
List.prototype.toArray = function() {
var cursor = this.head;
var result = [];
while (cursor) {
result.push(cursor.data);
cursor = cursor.next;
}
return result;
};
List.prototype.toJSON = function() {
return this.toArray();
};
List.prototype.isEmpty = function() {
return this.head === null;
};
List.prototype.first = function() {
return this.head && this.head.data;
};
List.prototype.last = function() {
return this.tail && this.tail.data;
};
List.prototype.each = function(fn, context) {
var item;
var cursor = {
prev: null,
next: this.head,
cursor: this.cursor
};
if (context === undefined) {
context = this;
}
// push cursor
this.cursor = cursor;
while (cursor.next !== null) {
item = cursor.next;
cursor.next = item.next;
fn.call(context, item.data, item, this);
}
// pop cursor
this.cursor = this.cursor.cursor;
};
List.prototype.eachRight = function(fn, context) {
var item;
var cursor = {
prev: this.tail,
next: null,
cursor: this.cursor
};
if (context === undefined) {
context = this;
}
// push cursor
this.cursor = cursor;
while (cursor.prev !== null) {
item = cursor.prev;
cursor.prev = item.prev;
fn.call(context, item.data, item, this);
}
// pop cursor
this.cursor = this.cursor.cursor;
};
List.prototype.nextUntil = function(start, fn, context) {
if (start === null) {
return;
}
var item;
var cursor = {
prev: null,
next: start,
cursor: this.cursor
};
if (context === undefined) {
context = this;
}
// push cursor
this.cursor = cursor;
while (cursor.next !== null) {
item = cursor.next;
cursor.next = item.next;
if (fn.call(context, item.data, item, this)) {
break;
}
}
// pop cursor
this.cursor = this.cursor.cursor;
};
List.prototype.prevUntil = function(start, fn, context) {
if (start === null) {
return;
}
var item;
var cursor = {
prev: start,
next: null,
cursor: this.cursor
};
if (context === undefined) {
context = this;
}
// push cursor
this.cursor = cursor;
while (cursor.prev !== null) {
item = cursor.prev;
cursor.prev = item.prev;
if (fn.call(context, item.data, item, this)) {
break;
}
}
// pop cursor
this.cursor = this.cursor.cursor;
};
List.prototype.some = function(fn, context) {
var cursor = this.head;
if (context === undefined) {
context = this;
}
while (cursor !== null) {
if (fn.call(context, cursor.data, cursor, this)) {
return true;
}
cursor = cursor.next;
}
return false;
};
List.prototype.map = function(fn, context) {
var result = [];
var cursor = this.head;
if (context === undefined) {
context = this;
}
while (cursor !== null) {
result.push(fn.call(context, cursor.data, cursor, this));
cursor = cursor.next;
}
return result;
};
List.prototype.copy = function() {
var result = new List();
var cursor = this.head;
while (cursor !== null) {
result.insert(createItem(cursor.data));
cursor = cursor.next;
}
return result;
};
List.prototype.updateCursors = function(prevOld, prevNew, nextOld, nextNew) {
var cursor = this.cursor;
while (cursor !== null) {
if (prevNew === true || cursor.prev === prevOld) {
cursor.prev = prevNew;
}
if (nextNew === true || cursor.next === nextOld) {
cursor.next = nextNew;
}
cursor = cursor.cursor;
}
};
List.prototype.insert = function(item, before) {
if (before !== undefined && before !== null) {
// prev before
// ^
// item
this.updateCursors(before.prev, item, before, item);
if (before.prev === null) {
// insert to the beginning of list
if (this.head !== before) {
throw new Error('before doesn\'t below to list');
}
// since head points to before therefore list doesn't empty
// no need to check tail
this.head = item;
before.prev = item;
item.next = before;
this.updateCursors(null, item);
} else {
// insert between two items
before.prev.next = item;
item.prev = before.prev;
before.prev = item;
item.next = before;
}
} else {
// tail
// ^
// item
this.updateCursors(this.tail, item, null, item);
// insert to end of the list
if (this.tail !== null) {
// if list has a tail, then it also has a head, but head doesn't change
// last item -> new item
this.tail.next = item;
// last item <- new item
item.prev = this.tail;
} else {
// if list has no a tail, then it also has no a head
// in this case points head to new item
this.head = item;
}
// tail always start point to new item
this.tail = item;
}
};
List.prototype.remove = function(item) {
// item
// ^
// prev next
this.updateCursors(item, item.prev, item, item.next);
if (item.prev !== null) {
item.prev.next = item.next;
} else {
if (this.head !== item) {
throw new Error('item doesn\'t below to list');
}
this.head = item.next;
}
if (item.next !== null) {
item.next.prev = item.prev;
} else {
if (this.tail !== item) {
throw new Error('item doesn\'t below to list');
}
this.tail = item.prev;
}
item.prev = null;
item.next = null;
return item;
};
List.prototype.appendList = function(list) {
// ignore empty lists
if (list.head === null) {
return;
}
this.updateCursors(this.tail, list.tail, null, list.head);
// insert to end of the list
if (this.tail !== null) {
// if destination list has a tail, then it also has a head,
// but head doesn't change
// dest tail -> source head
this.tail.next = list.head;
// dest tail <- source head
list.head.prev = this.tail;
} else {
// if list has no a tail, then it also has no a head
// in this case points head to new item
this.head = list.head;
}
// tail always start point to new item
this.tail = list.tail;
list.head = null;
list.tail = null;
};
module.exports = List;

View File

@@ -0,0 +1,73 @@
var hasOwnProperty = Object.prototype.hasOwnProperty;
var knownKeywords = Object.create(null);
var knownProperties = Object.create(null);
function getVendorPrefix(string) {
if (string[0] === '-') {
// skip 2 chars to avoid wrong match with variables names
var secondDashIndex = string.indexOf('-', 2);
if (secondDashIndex !== -1) {
return string.substr(0, secondDashIndex + 1);
}
}
return '';
}
function getKeywordInfo(keyword) {
if (hasOwnProperty.call(knownKeywords, keyword)) {
return knownKeywords[keyword];
}
var lowerCaseKeyword = keyword.toLowerCase();
var vendor = getVendorPrefix(lowerCaseKeyword);
var name = lowerCaseKeyword;
if (vendor) {
name = name.substr(vendor.length);
}
return knownKeywords[keyword] = Object.freeze({
vendor: vendor,
prefix: vendor,
name: name
});
}
function getPropertyInfo(property) {
if (hasOwnProperty.call(knownProperties, property)) {
return knownProperties[property];
}
var lowerCaseProperty = property.toLowerCase();
var hack = lowerCaseProperty[0];
if (hack === '*' || hack === '_' || hack === '$') {
lowerCaseProperty = lowerCaseProperty.substr(1);
} else if (hack === '/' && property[1] === '/') {
hack = '//';
lowerCaseProperty = lowerCaseProperty.substr(2);
} else {
hack = '';
}
var vendor = getVendorPrefix(lowerCaseProperty);
var name = lowerCaseProperty;
if (vendor) {
name = name.substr(vendor.length);
}
return knownProperties[property] = Object.freeze({
hack: hack,
vendor: vendor,
prefix: hack + vendor,
name: name
});
}
module.exports = {
keyword: getKeywordInfo,
property: getPropertyInfo
};

View File

@@ -0,0 +1,178 @@
function each(list) {
if (list.head === null) {
return '';
}
if (list.head === list.tail) {
return translate(list.head.data);
}
return list.map(translate).join('');
}
function eachDelim(list, delimeter) {
if (list.head === null) {
return '';
}
if (list.head === list.tail) {
return translate(list.head.data);
}
return list.map(translate).join(delimeter);
}
function translate(node) {
switch (node.type) {
case 'StyleSheet':
return each(node.rules);
case 'Atrule':
var nodes = ['@', node.name];
if (node.expression && !node.expression.sequence.isEmpty()) {
nodes.push(' ', translate(node.expression));
}
if (node.block) {
nodes.push('{', translate(node.block), '}');
} else {
nodes.push(';');
}
return nodes.join('');
case 'Ruleset':
return translate(node.selector) + '{' + translate(node.block) + '}';
case 'Selector':
return eachDelim(node.selectors, ',');
case 'SimpleSelector':
var nodes = node.sequence.map(function(node) {
// add extra spaces around /deep/ combinator since comment beginning/ending may to be produced
if (node.type === 'Combinator' && node.name === '/deep/') {
return ' ' + translate(node) + ' ';
}
return translate(node);
});
return nodes.join('');
case 'Block':
return eachDelim(node.declarations, ';');
case 'Declaration':
return translate(node.property) + ':' + translate(node.value);
case 'Property':
return node.name;
case 'Value':
return node.important
? each(node.sequence) + '!important'
: each(node.sequence);
case 'Attribute':
var result = translate(node.name);
var flagsPrefix = ' ';
if (node.operator !== null) {
result += node.operator;
if (node.value !== null) {
result += translate(node.value);
// space between string and flags is not required
if (node.value.type === 'String') {
flagsPrefix = '';
}
}
}
if (node.flags !== null) {
result += flagsPrefix + node.flags;
}
return '[' + result + ']';
case 'FunctionalPseudo':
return ':' + node.name + '(' + eachDelim(node.arguments, ',') + ')';
case 'Function':
return node.name + '(' + eachDelim(node.arguments, ',') + ')';
case 'Negation':
return ':not(' + eachDelim(node.sequence, ',') + ')';
case 'Braces':
return node.open + each(node.sequence) + node.close;
case 'Argument':
case 'AtruleExpression':
return each(node.sequence);
case 'Url':
return 'url(' + translate(node.value) + ')';
case 'Progid':
return translate(node.value);
case 'Combinator':
return node.name;
case 'Identifier':
return node.name;
case 'PseudoClass':
return ':' + node.name;
case 'PseudoElement':
return '::' + node.name;
case 'Class':
return '.' + node.name;
case 'Id':
return '#' + node.name;
case 'Hash':
return '#' + node.value;
case 'Dimension':
return node.value + node.unit;
case 'Nth':
return node.value;
case 'Number':
return node.value;
case 'String':
return node.value;
case 'Operator':
return node.value;
case 'Raw':
return node.value;
case 'Unknown':
return node.value;
case 'Percentage':
return node.value + '%';
case 'Space':
return ' ';
case 'Comment':
return '/*' + node.value + '*/';
default:
throw new Error('Unknown node type: ' + node.type);
}
}
module.exports = translate;

View File

@@ -0,0 +1,291 @@
var SourceMapGenerator = require('source-map').SourceMapGenerator;
var SourceNode = require('source-map').SourceNode;
// Our own implementation of SourceNode#toStringWithSourceMap,
// since SourceNode doesn't allow multiple references to original source.
// Also, as we know structure of result we could be optimize generation
// (currently it's ~40% faster).
function walk(node, fn) {
for (var chunk, i = 0; i < node.children.length; i++) {
chunk = node.children[i];
if (chunk instanceof SourceNode) {
// this is a hack, because source maps doesn't support for 1(generated):N(original)
// if (chunk.merged) {
// fn('', chunk);
// }
walk(chunk, fn);
} else {
fn(chunk, node);
}
}
}
function generateSourceMap(root) {
var map = new SourceMapGenerator();
var css = '';
var sourceMappingActive = false;
var lastOriginalLine = null;
var lastOriginalColumn = null;
var lastIndexOfNewline;
var generated = {
line: 1,
column: 0
};
var activatedMapping = {
generated: generated
};
walk(root, function(chunk, original) {
if (original.line !== null &&
original.column !== null) {
if (lastOriginalLine !== original.line ||
lastOriginalColumn !== original.column) {
map.addMapping({
source: original.source,
original: original,
generated: generated
});
}
lastOriginalLine = original.line;
lastOriginalColumn = original.column;
sourceMappingActive = true;
} else if (sourceMappingActive) {
map.addMapping(activatedMapping);
sourceMappingActive = false;
}
css += chunk;
lastIndexOfNewline = chunk.lastIndexOf('\n');
if (lastIndexOfNewline !== -1) {
generated.line += chunk.match(/\n/g).length;
generated.column = chunk.length - lastIndexOfNewline - 1;
} else {
generated.column += chunk.length;
}
});
return {
css: css,
map: map
};
}
function createAnonymousSourceNode(children) {
return new SourceNode(
null,
null,
null,
children
);
}
function createSourceNode(info, children) {
if (info.primary) {
// special marker node to add several references to original
// var merged = createSourceNode(info.merged, []);
// merged.merged = true;
// children.unshift(merged);
// use recursion, because primary can also has a primary/merged info
return createSourceNode(info.primary, children);
}
return new SourceNode(
info.line,
info.column - 1,
info.source,
children
);
}
function each(list) {
if (list.head === null) {
return '';
}
if (list.head === list.tail) {
return translate(list.head.data);
}
return list.map(translate).join('');
}
function eachDelim(list, delimeter) {
if (list.head === null) {
return '';
}
if (list.head === list.tail) {
return translate(list.head.data);
}
return list.map(translate).join(delimeter);
}
function translate(node) {
switch (node.type) {
case 'StyleSheet':
return createAnonymousSourceNode(node.rules.map(translate));
case 'Atrule':
var nodes = ['@', node.name];
if (node.expression && !node.expression.sequence.isEmpty()) {
nodes.push(' ', translate(node.expression));
}
if (node.block) {
nodes.push('{', translate(node.block), '}');
} else {
nodes.push(';');
}
return createSourceNode(node.info, nodes);
case 'Ruleset':
return createAnonymousSourceNode([
translate(node.selector), '{', translate(node.block), '}'
]);
case 'Selector':
return createAnonymousSourceNode(node.selectors.map(translate)).join(',');
case 'SimpleSelector':
var nodes = node.sequence.map(function(node) {
// add extra spaces around /deep/ combinator since comment beginning/ending may to be produced
if (node.type === 'Combinator' && node.name === '/deep/') {
return ' ' + translate(node) + ' ';
}
return translate(node);
});
return createSourceNode(node.info, nodes);
case 'Block':
return createAnonymousSourceNode(node.declarations.map(translate)).join(';');
case 'Declaration':
return createSourceNode(
node.info,
[translate(node.property), ':', translate(node.value)]
);
case 'Property':
return node.name;
case 'Value':
return node.important
? each(node.sequence) + '!important'
: each(node.sequence);
case 'Attribute':
var result = translate(node.name);
var flagsPrefix = ' ';
if (node.operator !== null) {
result += node.operator;
if (node.value !== null) {
result += translate(node.value);
// space between string and flags is not required
if (node.value.type === 'String') {
flagsPrefix = '';
}
}
}
if (node.flags !== null) {
result += flagsPrefix + node.flags;
}
return '[' + result + ']';
case 'FunctionalPseudo':
return ':' + node.name + '(' + eachDelim(node.arguments, ',') + ')';
case 'Function':
return node.name + '(' + eachDelim(node.arguments, ',') + ')';
case 'Negation':
return ':not(' + eachDelim(node.sequence, ',') + ')';
case 'Braces':
return node.open + each(node.sequence) + node.close;
case 'Argument':
case 'AtruleExpression':
return each(node.sequence);
case 'Url':
return 'url(' + translate(node.value) + ')';
case 'Progid':
return translate(node.value);
case 'Combinator':
return node.name;
case 'Identifier':
return node.name;
case 'PseudoClass':
return ':' + node.name;
case 'PseudoElement':
return '::' + node.name;
case 'Class':
return '.' + node.name;
case 'Id':
return '#' + node.name;
case 'Hash':
return '#' + node.value;
case 'Dimension':
return node.value + node.unit;
case 'Nth':
return node.value;
case 'Number':
return node.value;
case 'String':
return node.value;
case 'Operator':
return node.value;
case 'Raw':
return node.value;
case 'Unknown':
return node.value;
case 'Percentage':
return node.value + '%';
case 'Space':
return ' ';
case 'Comment':
return '/*' + node.value + '*/';
default:
throw new Error('Unknown node type: ' + node.type);
}
}
module.exports = function(node) {
return generateSourceMap(
createAnonymousSourceNode(translate(node))
);
};

View File

@@ -0,0 +1,189 @@
function walkRules(node, item, list) {
switch (node.type) {
case 'StyleSheet':
var oldStylesheet = this.stylesheet;
this.stylesheet = node;
node.rules.each(walkRules, this);
this.stylesheet = oldStylesheet;
break;
case 'Atrule':
if (node.block !== null) {
walkRules.call(this, node.block);
}
this.fn(node, item, list);
break;
case 'Ruleset':
this.fn(node, item, list);
break;
}
}
function walkRulesRight(node, item, list) {
switch (node.type) {
case 'StyleSheet':
var oldStylesheet = this.stylesheet;
this.stylesheet = node;
node.rules.eachRight(walkRulesRight, this);
this.stylesheet = oldStylesheet;
break;
case 'Atrule':
if (node.block !== null) {
walkRulesRight.call(this, node.block);
}
this.fn(node, item, list);
break;
case 'Ruleset':
this.fn(node, item, list);
break;
}
}
function walkAll(node, item, list) {
switch (node.type) {
case 'StyleSheet':
var oldStylesheet = this.stylesheet;
this.stylesheet = node;
node.rules.each(walkAll, this);
this.stylesheet = oldStylesheet;
break;
case 'Atrule':
if (node.expression !== null) {
walkAll.call(this, node.expression);
}
if (node.block !== null) {
walkAll.call(this, node.block);
}
break;
case 'Ruleset':
this.ruleset = node;
if (node.selector !== null) {
walkAll.call(this, node.selector);
}
walkAll.call(this, node.block);
this.ruleset = null;
break;
case 'Selector':
var oldSelector = this.selector;
this.selector = node;
node.selectors.each(walkAll, this);
this.selector = oldSelector;
break;
case 'Block':
node.declarations.each(walkAll, this);
break;
case 'Declaration':
this.declaration = node;
walkAll.call(this, node.property);
walkAll.call(this, node.value);
this.declaration = null;
break;
case 'Attribute':
walkAll.call(this, node.name);
if (node.value !== null) {
walkAll.call(this, node.value);
}
break;
case 'FunctionalPseudo':
case 'Function':
this['function'] = node;
node.arguments.each(walkAll, this);
this['function'] = null;
break;
case 'AtruleExpression':
this.atruleExpression = node;
node.sequence.each(walkAll, this);
this.atruleExpression = null;
break;
case 'Value':
case 'Argument':
case 'SimpleSelector':
case 'Braces':
case 'Negation':
node.sequence.each(walkAll, this);
break;
case 'Url':
case 'Progid':
walkAll.call(this, node.value);
break;
// nothig to do with
// case 'Property':
// case 'Combinator':
// case 'Dimension':
// case 'Hash':
// case 'Identifier':
// case 'Nth':
// case 'Class':
// case 'Id':
// case 'Percentage':
// case 'PseudoClass':
// case 'PseudoElement':
// case 'Space':
// case 'Number':
// case 'String':
// case 'Operator':
// case 'Raw':
}
this.fn(node, item, list);
}
function createContext(root, fn) {
var context = {
fn: fn,
root: root,
stylesheet: null,
atruleExpression: null,
ruleset: null,
selector: null,
declaration: null,
function: null
};
return context;
}
module.exports = {
all: function(root, fn) {
walkAll.call(createContext(root, fn), root);
},
rules: function(root, fn) {
walkRules.call(createContext(root, fn), root);
},
rulesRight: function(root, fn) {
walkRulesRight.call(createContext(root, fn), root);
}
};

View File

@@ -0,0 +1,118 @@
{
"_args": [
[
"csso@2.3.2",
"C:\\Users\\deranjer\\go\\src\\github.com\\deranjer\\goTorrent\\torrent-project"
]
],
"_from": "csso@2.3.2",
"_id": "csso@2.3.2",
"_inBundle": false,
"_integrity": "sha1-3dUsWHAz9J6Utx/FVWnyUuj/X4U=",
"_location": "/css-loader/csso",
"_phantomChildren": {},
"_requested": {
"type": "version",
"registry": true,
"raw": "csso@2.3.2",
"name": "csso",
"escapedName": "csso",
"rawSpec": "2.3.2",
"saveSpec": null,
"fetchSpec": "2.3.2"
},
"_requiredBy": [
"/css-loader/svgo"
],
"_resolved": "https://registry.npmjs.org/csso/-/csso-2.3.2.tgz",
"_spec": "2.3.2",
"_where": "C:\\Users\\deranjer\\go\\src\\github.com\\deranjer\\goTorrent\\torrent-project",
"author": {
"name": "Sergey Kryzhanovsky",
"email": "skryzhanovsky@ya.ru",
"url": "https://github.com/afelix"
},
"bin": {
"csso": "./bin/csso"
},
"bugs": {
"url": "https://github.com/css/csso/issues"
},
"dependencies": {
"clap": "^1.0.9",
"source-map": "^0.5.3"
},
"description": "CSSO (CSS Optimizer) is a CSS minifier with structural optimisations",
"devDependencies": {
"browserify": "^13.0.0",
"coveralls": "^2.11.6",
"eslint": "^2.2.0",
"istanbul": "^0.4.2",
"jscs": "~2.10.0",
"mocha": "~2.4.2",
"uglify-js": "^2.6.1"
},
"engines": {
"node": ">=0.10.0"
},
"eslintConfig": {
"env": {
"node": true,
"mocha": true,
"es6": true
},
"rules": {
"no-duplicate-case": 2,
"no-undef": 2,
"no-unused-vars": [
2,
{
"vars": "all",
"args": "after-used"
}
]
}
},
"files": [
"bin",
"dist/csso-browser.js",
"lib",
"HISTORY.md",
"LICENSE",
"README.md"
],
"homepage": "https://github.com/css/csso",
"keywords": [
"css",
"minifier",
"minify",
"compress",
"optimisation"
],
"license": "MIT",
"main": "./lib/index",
"maintainers": [
{
"name": "Roman Dvornov",
"email": "rdvornov@gmail.com"
}
],
"name": "csso",
"repository": {
"type": "git",
"url": "git+https://github.com/css/csso.git"
},
"scripts": {
"browserify": "browserify --standalone csso lib/index.js | uglifyjs --compress --mangle -o dist/csso-browser.js",
"codestyle": "jscs lib && eslint lib test",
"codestyle-and-test": "npm run codestyle && npm test",
"coverage": "istanbul cover _mocha -- -R dot",
"coveralls": "istanbul cover _mocha --report lcovonly -- -R dot && cat ./coverage/lcov.info | coveralls",
"gh-pages": "git clone -b gh-pages https://github.com/css/csso.git .gh-pages && npm run browserify && cp dist/csso-browser.js .gh-pages/ && cd .gh-pages && git commit -am \"update\" && git push && cd .. && rm -rf .gh-pages",
"hydrogen": "node --trace-hydrogen --trace-phase=Z --trace-deopt --code-comments --hydrogen-track-positions --redirect-code-traces --redirect-code-traces-to=code.asm --trace_hydrogen_file=code.cfg --print-opt-code bin/csso --stat -o /dev/null",
"prepublish": "npm run browserify",
"test": "mocha --reporter dot",
"travis": "npm run codestyle-and-test && npm run coveralls"
},
"version": "2.3.2"
}