chushihua
This commit is contained in:
+22
@@ -0,0 +1,22 @@
|
||||
Copyright (c) Ben Briggs <beneb.info@gmail.com> (http://beneb.info)
|
||||
|
||||
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.
|
||||
+297
@@ -0,0 +1,297 @@
|
||||
# cssnano-preset-default
|
||||
|
||||
> Safe defaults for cssnano which require minimal configuration.
|
||||
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
|
||||
- [Usage](#usage)
|
||||
|
||||
- [Install](#install)
|
||||
- [Configuration](#configuration)
|
||||
|
||||
- [Plugins](#plugins)
|
||||
|
||||
- [cssnano-util-raw-cache](#cssnano-util-raw-cache)
|
||||
- [postcss-calc (external)](#postcss-calc-external)
|
||||
- [postcss-colormin](#postcss-colormin)
|
||||
- [postcss-convert-values](#postcss-convert-values)
|
||||
- [postcss-discard-comments](#postcss-discard-comments)
|
||||
- [postcss-discard-duplicates](#postcss-discard-duplicates)
|
||||
- [postcss-discard-empty](#postcss-discard-empty)
|
||||
- [postcss-discard-overridden](#postcss-discard-overridden)
|
||||
- [postcss-merge-longhand](#postcss-merge-longhand)
|
||||
- [postcss-merge-rules](#postcss-merge-rules)
|
||||
- [postcss-minify-font-values](#postcss-minify-font-values)
|
||||
- [postcss-minify-gradients](#postcss-minify-gradients)
|
||||
- [postcss-minify-params](#postcss-minify-params)
|
||||
- [postcss-minify-selectors](#postcss-minify-selectors)
|
||||
- [postcss-normalize-charset](#postcss-normalize-charset)
|
||||
- [postcss-normalize-display-values](#postcss-normalize-display-values)
|
||||
- [postcss-normalize-positions](#postcss-normalize-positions)
|
||||
- [postcss-normalize-repeat-style](#postcss-normalize-repeat-style)
|
||||
- [postcss-normalize-string](#postcss-normalize-string)
|
||||
- [postcss-normalize-timing-functions](#postcss-normalize-timing-functions)
|
||||
- [postcss-normalize-unicode](#postcss-normalize-unicode)
|
||||
- [postcss-normalize-url](#postcss-normalize-url)
|
||||
- [postcss-normalize-whitespace](#postcss-normalize-whitespace)
|
||||
- [postcss-ordered-values](#postcss-ordered-values)
|
||||
- [postcss-reduce-initial](#postcss-reduce-initial)
|
||||
- [postcss-reduce-transforms](#postcss-reduce-transforms)
|
||||
- [postcss-svgo](#postcss-svgo)
|
||||
- [postcss-unique-selectors](#postcss-unique-selectors)
|
||||
|
||||
- [Contributors](#contributors)
|
||||
|
||||
- [License](#license)
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
This default preset for cssnano only includes transforms that make no
|
||||
assumptions about your CSS other than what is passed in. In previous
|
||||
iterations of cssnano, assumptions were made about your CSS which caused
|
||||
output to look different in certain use cases, but not others. These
|
||||
transforms have been moved from the defaults to other presets, to make
|
||||
this preset require only minimal configuration.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
### Install
|
||||
|
||||
Note that this preset comes bundled with cssnano _by default_, so you don't need to install it separately.
|
||||
|
||||
### Configuration
|
||||
|
||||
If you would like to use the default configuration, then you don't need to add anything to your `package.json`.
|
||||
|
||||
But should you wish to customise this, you can pass an array with the second parameter as the options object to use. For example, to remove all comments:
|
||||
|
||||
```diff
|
||||
{
|
||||
"name": "awesome-application",
|
||||
+ "cssnano": {
|
||||
+ "preset": [
|
||||
+ "default",
|
||||
+ {"discardComments": {"removeAll": true}}
|
||||
+ ]
|
||||
+ }
|
||||
}
|
||||
```
|
||||
|
||||
Depending on your usage, the JSON configuration might not work for you, such as in cases where you would like to use options with customisable function parameters. For this use case, we recommend a `cssnano.config.js` at the same location as your `package.json`. You can then load a preset and export it with your custom parameters:
|
||||
|
||||
```js
|
||||
const defaultPreset = require('cssnano-preset-default');
|
||||
|
||||
module.exports = defaultPreset({
|
||||
discardComments: {
|
||||
remove: comment => comment[0] === "@"
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Note that you may wish to publish your own preset to npm for reusability, should it differ a lot from this one. This is highly encouraged!
|
||||
|
||||
|
||||
## Plugins
|
||||
|
||||
### [`css-declaration-sorter`](https://github.com/Siilwyn/css-declaration-sorter) (external)
|
||||
|
||||
> Sorts CSS declarations fast and automatically in a certain order.
|
||||
|
||||
This plugin is loaded with its default configuration.
|
||||
|
||||
### [`cssnano-util-raw-cache`](https://github.com/cssnano/cssnano/tree/master/packages/cssnano-util-raw-cache)
|
||||
|
||||
> Manages the raw value formatting for generated AST nodes.
|
||||
|
||||
This plugin is loaded with its default configuration.
|
||||
|
||||
### [`postcss-calc`](https://github.com/postcss/postcss-calc) (external)
|
||||
|
||||
> PostCSS plugin to reduce calc()
|
||||
|
||||
This plugin is loaded with its default configuration.
|
||||
|
||||
### [`postcss-colormin`](https://github.com/cssnano/cssnano/tree/master/packages/postcss-colormin)
|
||||
|
||||
> Minify colors in your CSS files with PostCSS.
|
||||
|
||||
This plugin is loaded with its default configuration.
|
||||
|
||||
### [`postcss-convert-values`](https://github.com/cssnano/cssnano/tree/master/packages/postcss-convert-values)
|
||||
|
||||
> Convert values with PostCSS (e.g. ms -> s)
|
||||
|
||||
This plugin is loaded with the following configuration:
|
||||
|
||||
```js
|
||||
{
|
||||
length: false
|
||||
}
|
||||
```
|
||||
|
||||
### [`postcss-discard-comments`](https://github.com/cssnano/cssnano/tree/master/packages/postcss-discard-comments)
|
||||
|
||||
> Discard comments in your CSS files with PostCSS.
|
||||
|
||||
This plugin is loaded with its default configuration.
|
||||
|
||||
### [`postcss-discard-duplicates`](https://github.com/cssnano/cssnano/tree/master/packages/postcss-discard-duplicates)
|
||||
|
||||
> Discard duplicate rules in your CSS files with PostCSS.
|
||||
|
||||
This plugin is loaded with its default configuration.
|
||||
|
||||
### [`postcss-discard-empty`](https://github.com/cssnano/cssnano/tree/master/packages/postcss-discard-empty)
|
||||
|
||||
> Discard empty rules and values with PostCSS.
|
||||
|
||||
This plugin is loaded with its default configuration.
|
||||
|
||||
### [`postcss-discard-overridden`](https://github.com/cssnano/cssnano/tree/master/packages/postcss-discard-overridden)
|
||||
|
||||
> PostCSS plugin to discard overridden @keyframes or @counter-style.
|
||||
|
||||
This plugin is loaded with its default configuration.
|
||||
|
||||
### [`postcss-merge-longhand`](https://github.com/cssnano/cssnano/tree/master/packages/postcss-merge-longhand)
|
||||
|
||||
> Merge longhand properties into shorthand with PostCSS.
|
||||
|
||||
This plugin is loaded with its default configuration.
|
||||
|
||||
### [`postcss-merge-rules`](https://github.com/cssnano/cssnano/tree/master/packages/postcss-merge-rules)
|
||||
|
||||
> Merge CSS rules with PostCSS.
|
||||
|
||||
This plugin is loaded with its default configuration.
|
||||
|
||||
### [`postcss-minify-font-values`](https://github.com/cssnano/cssnano/tree/master/packages/postcss-minify-font-values)
|
||||
|
||||
> Minify font declarations with PostCSS
|
||||
|
||||
This plugin is loaded with its default configuration.
|
||||
|
||||
### [`postcss-minify-gradients`](https://github.com/cssnano/cssnano/tree/master/packages/postcss-minify-gradients)
|
||||
|
||||
> Minify gradient parameters with PostCSS.
|
||||
|
||||
This plugin is loaded with its default configuration.
|
||||
|
||||
### [`postcss-minify-params`](https://github.com/cssnano/cssnano/tree/master/packages/postcss-minify-params)
|
||||
|
||||
> Minify at-rule params with PostCSS
|
||||
|
||||
This plugin is loaded with its default configuration.
|
||||
|
||||
### [`postcss-minify-selectors`](https://github.com/cssnano/cssnano/tree/master/packages/postcss-minify-selectors)
|
||||
|
||||
> Minify selectors with PostCSS.
|
||||
|
||||
This plugin is loaded with its default configuration.
|
||||
|
||||
### [`postcss-normalize-charset`](https://github.com/cssnano/cssnano/tree/master/packages/postcss-normalize-charset)
|
||||
|
||||
> Add necessary or remove extra charset with PostCSS
|
||||
|
||||
This plugin is loaded with the following configuration:
|
||||
|
||||
```js
|
||||
{
|
||||
add: false
|
||||
}
|
||||
```
|
||||
|
||||
### [`postcss-normalize-display-values`](https://github.com/cssnano/cssnano/tree/master/packages/postcss-normalize-display-values)
|
||||
|
||||
> Normalize multiple value display syntaxes into single values.
|
||||
|
||||
This plugin is loaded with its default configuration.
|
||||
|
||||
### [`postcss-normalize-positions`](https://github.com/cssnano/cssnano/tree/master/packages/postcss-normalize-positions)
|
||||
|
||||
> Normalize keyword values for position into length values.
|
||||
|
||||
This plugin is loaded with its default configuration.
|
||||
|
||||
### [`postcss-normalize-repeat-style`](https://github.com/cssnano/cssnano/tree/master/packages/postcss-normalize-repeat-style)
|
||||
|
||||
> Convert two value syntax for repeat-style into one value.
|
||||
|
||||
This plugin is loaded with its default configuration.
|
||||
|
||||
### [`postcss-normalize-string`](https://github.com/cssnano/cssnano/tree/master/packages/postcss-normalize-string)
|
||||
|
||||
> Normalize wrapping quotes for CSS string literals.
|
||||
|
||||
This plugin is loaded with its default configuration.
|
||||
|
||||
### [`postcss-normalize-timing-functions`](https://github.com/cssnano/cssnano/tree/master/packages/postcss-normalize-timing-functions)
|
||||
|
||||
> Normalize CSS animation/transition timing functions.
|
||||
|
||||
This plugin is loaded with its default configuration.
|
||||
|
||||
### [`postcss-normalize-unicode`](https://github.com/cssnano/cssnano/tree/master/packages/postcss-normalize-unicode)
|
||||
|
||||
> Normalize unicode-range descriptors, and can convert to wildcard ranges.
|
||||
|
||||
This plugin is loaded with its default configuration.
|
||||
|
||||
### [`postcss-normalize-url`](https://github.com/cssnano/cssnano/tree/master/packages/postcss-normalize-url)
|
||||
|
||||
> Normalize URLs with PostCSS
|
||||
|
||||
This plugin is loaded with its default configuration.
|
||||
|
||||
### [`postcss-normalize-whitespace`](https://github.com/cssnano/cssnano/tree/master/packages/postcss-normalize-whitespace)
|
||||
|
||||
> Trim whitespace inside and around CSS rules & declarations.
|
||||
|
||||
This plugin is loaded with its default configuration.
|
||||
|
||||
### [`postcss-ordered-values`](https://github.com/cssnano/cssnano/tree/master/packages/postcss-ordered-values)
|
||||
|
||||
> Ensure values are ordered consistently in your CSS.
|
||||
|
||||
This plugin is loaded with its default configuration.
|
||||
|
||||
### [`postcss-reduce-initial`](https://github.com/cssnano/cssnano/tree/master/packages/postcss-reduce-initial)
|
||||
|
||||
> Reduce initial definitions to the actual initial value, where possible.
|
||||
|
||||
This plugin is loaded with its default configuration.
|
||||
|
||||
### [`postcss-reduce-transforms`](https://github.com/cssnano/cssnano/tree/master/packages/postcss-reduce-transforms)
|
||||
|
||||
> Reduce transform functions with PostCSS.
|
||||
|
||||
This plugin is loaded with its default configuration.
|
||||
|
||||
### [`postcss-svgo`](https://github.com/cssnano/cssnano/tree/master/packages/postcss-svgo)
|
||||
|
||||
> Optimise inline SVG with PostCSS.
|
||||
|
||||
This plugin is loaded with its default configuration.
|
||||
|
||||
### [`postcss-unique-selectors`](https://github.com/cssnano/cssnano/tree/master/packages/postcss-unique-selectors)
|
||||
|
||||
> Ensure CSS selectors are unique.
|
||||
|
||||
This plugin is loaded with its default configuration.
|
||||
|
||||
|
||||
## Contributors
|
||||
|
||||
See [CONTRIBUTORS.md](https://github.com/cssnano/cssnano/blob/master/CONTRIBUTORS.md).
|
||||
|
||||
|
||||
## License
|
||||
|
||||
MIT © [Ben Briggs](http://beneb.info)
|
||||
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
'use strict';
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.default = defaultPreset;
|
||||
|
||||
var _cssDeclarationSorter = require('css-declaration-sorter');
|
||||
|
||||
var _cssDeclarationSorter2 = _interopRequireDefault(_cssDeclarationSorter);
|
||||
|
||||
var _postcssDiscardComments = require('postcss-discard-comments');
|
||||
|
||||
var _postcssDiscardComments2 = _interopRequireDefault(_postcssDiscardComments);
|
||||
|
||||
var _postcssReduceInitial = require('postcss-reduce-initial');
|
||||
|
||||
var _postcssReduceInitial2 = _interopRequireDefault(_postcssReduceInitial);
|
||||
|
||||
var _postcssMinifyGradients = require('postcss-minify-gradients');
|
||||
|
||||
var _postcssMinifyGradients2 = _interopRequireDefault(_postcssMinifyGradients);
|
||||
|
||||
var _postcssSvgo = require('postcss-svgo');
|
||||
|
||||
var _postcssSvgo2 = _interopRequireDefault(_postcssSvgo);
|
||||
|
||||
var _postcssReduceTransforms = require('postcss-reduce-transforms');
|
||||
|
||||
var _postcssReduceTransforms2 = _interopRequireDefault(_postcssReduceTransforms);
|
||||
|
||||
var _postcssConvertValues = require('postcss-convert-values');
|
||||
|
||||
var _postcssConvertValues2 = _interopRequireDefault(_postcssConvertValues);
|
||||
|
||||
var _postcssCalc = require('postcss-calc');
|
||||
|
||||
var _postcssCalc2 = _interopRequireDefault(_postcssCalc);
|
||||
|
||||
var _postcssColormin = require('postcss-colormin');
|
||||
|
||||
var _postcssColormin2 = _interopRequireDefault(_postcssColormin);
|
||||
|
||||
var _postcssOrderedValues = require('postcss-ordered-values');
|
||||
|
||||
var _postcssOrderedValues2 = _interopRequireDefault(_postcssOrderedValues);
|
||||
|
||||
var _postcssMinifySelectors = require('postcss-minify-selectors');
|
||||
|
||||
var _postcssMinifySelectors2 = _interopRequireDefault(_postcssMinifySelectors);
|
||||
|
||||
var _postcssMinifyParams = require('postcss-minify-params');
|
||||
|
||||
var _postcssMinifyParams2 = _interopRequireDefault(_postcssMinifyParams);
|
||||
|
||||
var _postcssNormalizeCharset = require('postcss-normalize-charset');
|
||||
|
||||
var _postcssNormalizeCharset2 = _interopRequireDefault(_postcssNormalizeCharset);
|
||||
|
||||
var _postcssMinifyFontValues = require('postcss-minify-font-values');
|
||||
|
||||
var _postcssMinifyFontValues2 = _interopRequireDefault(_postcssMinifyFontValues);
|
||||
|
||||
var _postcssNormalizeUrl = require('postcss-normalize-url');
|
||||
|
||||
var _postcssNormalizeUrl2 = _interopRequireDefault(_postcssNormalizeUrl);
|
||||
|
||||
var _postcssMergeLonghand = require('postcss-merge-longhand');
|
||||
|
||||
var _postcssMergeLonghand2 = _interopRequireDefault(_postcssMergeLonghand);
|
||||
|
||||
var _postcssDiscardDuplicates = require('postcss-discard-duplicates');
|
||||
|
||||
var _postcssDiscardDuplicates2 = _interopRequireDefault(_postcssDiscardDuplicates);
|
||||
|
||||
var _postcssDiscardOverridden = require('postcss-discard-overridden');
|
||||
|
||||
var _postcssDiscardOverridden2 = _interopRequireDefault(_postcssDiscardOverridden);
|
||||
|
||||
var _postcssNormalizeRepeatStyle = require('postcss-normalize-repeat-style');
|
||||
|
||||
var _postcssNormalizeRepeatStyle2 = _interopRequireDefault(_postcssNormalizeRepeatStyle);
|
||||
|
||||
var _postcssMergeRules = require('postcss-merge-rules');
|
||||
|
||||
var _postcssMergeRules2 = _interopRequireDefault(_postcssMergeRules);
|
||||
|
||||
var _postcssDiscardEmpty = require('postcss-discard-empty');
|
||||
|
||||
var _postcssDiscardEmpty2 = _interopRequireDefault(_postcssDiscardEmpty);
|
||||
|
||||
var _postcssUniqueSelectors = require('postcss-unique-selectors');
|
||||
|
||||
var _postcssUniqueSelectors2 = _interopRequireDefault(_postcssUniqueSelectors);
|
||||
|
||||
var _postcssNormalizeString = require('postcss-normalize-string');
|
||||
|
||||
var _postcssNormalizeString2 = _interopRequireDefault(_postcssNormalizeString);
|
||||
|
||||
var _postcssNormalizePositions = require('postcss-normalize-positions');
|
||||
|
||||
var _postcssNormalizePositions2 = _interopRequireDefault(_postcssNormalizePositions);
|
||||
|
||||
var _postcssNormalizeWhitespace = require('postcss-normalize-whitespace');
|
||||
|
||||
var _postcssNormalizeWhitespace2 = _interopRequireDefault(_postcssNormalizeWhitespace);
|
||||
|
||||
var _postcssNormalizeUnicode = require('postcss-normalize-unicode');
|
||||
|
||||
var _postcssNormalizeUnicode2 = _interopRequireDefault(_postcssNormalizeUnicode);
|
||||
|
||||
var _postcssNormalizeDisplayValues = require('postcss-normalize-display-values');
|
||||
|
||||
var _postcssNormalizeDisplayValues2 = _interopRequireDefault(_postcssNormalizeDisplayValues);
|
||||
|
||||
var _postcssNormalizeTimingFunctions = require('postcss-normalize-timing-functions');
|
||||
|
||||
var _postcssNormalizeTimingFunctions2 = _interopRequireDefault(_postcssNormalizeTimingFunctions);
|
||||
|
||||
var _cssnanoUtilRawCache = require('cssnano-util-raw-cache');
|
||||
|
||||
var _cssnanoUtilRawCache2 = _interopRequireDefault(_cssnanoUtilRawCache);
|
||||
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||
|
||||
const defaultOpts = {
|
||||
convertValues: {
|
||||
length: false
|
||||
},
|
||||
normalizeCharset: {
|
||||
add: false
|
||||
},
|
||||
cssDeclarationSorter: {
|
||||
exclude: true
|
||||
}
|
||||
}; /**
|
||||
* @author Ben Briggs
|
||||
* @license MIT
|
||||
* @module cssnano:preset:default
|
||||
* @overview
|
||||
*
|
||||
* This default preset for cssnano only includes transforms that make no
|
||||
* assumptions about your CSS other than what is passed in. In previous
|
||||
* iterations of cssnano, assumptions were made about your CSS which caused
|
||||
* output to look different in certain use cases, but not others. These
|
||||
* transforms have been moved from the defaults to other presets, to make
|
||||
* this preset require only minimal configuration.
|
||||
*/
|
||||
|
||||
function defaultPreset(opts = {}) {
|
||||
const options = Object.assign({}, defaultOpts, opts);
|
||||
|
||||
const plugins = [[_postcssDiscardComments2.default, options.discardComments], [_postcssMinifyGradients2.default, options.minifyGradients], [_postcssReduceInitial2.default, options.reduceInitial], [_postcssSvgo2.default, options.svgo], [_postcssNormalizeDisplayValues2.default, options.normalizeDisplayValues], [_postcssReduceTransforms2.default, options.reduceTransforms], [_postcssColormin2.default, options.colormin], [_postcssNormalizeTimingFunctions2.default, options.normalizeTimingFunctions], [_postcssCalc2.default, options.calc], [_postcssConvertValues2.default, options.convertValues], [_postcssOrderedValues2.default, options.orderedValues], [_postcssMinifySelectors2.default, options.minifySelectors], [_postcssMinifyParams2.default, options.minifyParams], [_postcssNormalizeCharset2.default, options.normalizeCharset], [_postcssDiscardOverridden2.default, options.discardOverridden], [_postcssNormalizeString2.default, options.normalizeString], [_postcssNormalizeUnicode2.default, options.normalizeUnicode], [_postcssMinifyFontValues2.default, options.minifyFontValues], [_postcssNormalizeUrl2.default, options.normalizeUrl], [_postcssNormalizeRepeatStyle2.default, options.normalizeRepeatStyle], [_postcssNormalizePositions2.default, options.normalizePositions], [_postcssNormalizeWhitespace2.default, options.normalizeWhitespace], [_postcssMergeLonghand2.default, options.mergeLonghand], [_postcssDiscardDuplicates2.default, options.discardDuplicates], [_postcssMergeRules2.default, options.mergeRules], [_postcssDiscardEmpty2.default, options.discardEmpty], [_postcssUniqueSelectors2.default, options.uniqueSelectors], [_cssDeclarationSorter2.default, options.cssDeclarationSorter], [_cssnanoUtilRawCache2.default, options.rawCache]];
|
||||
|
||||
return { plugins };
|
||||
}
|
||||
module.exports = exports['default'];
|
||||
+1
@@ -0,0 +1 @@
|
||||
../esprima/bin/esparse.js
|
||||
+1
@@ -0,0 +1 @@
|
||||
../esprima/bin/esvalidate.js
|
||||
+1
@@ -0,0 +1 @@
|
||||
../js-yaml/bin/js-yaml.js
|
||||
+1
@@ -0,0 +1 @@
|
||||
../svgo/bin/svgo
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
# 3.0.0 - 2018-07-10
|
||||
|
||||
- Upgraded: browserslist
|
||||
|
||||
# 2.1.0 - 2018-06-06 (never released to npm)
|
||||
|
||||
- Upgraded: browserslist, caniuse-lite
|
||||
|
||||
# 2.0.0 - 2017-05-03
|
||||
|
||||
- Changed: we now use caniuse-lite instead if caniuse-db
|
||||
([#59](https://github.com/Nyalab/caniuse-api/pull/59))
|
||||
|
||||
# 1.6.1 - 2017-04-07
|
||||
|
||||
- Added: export the feature list
|
||||
([#48](https://github.com/Nyalab/caniuse-api/pull/48))
|
||||
|
||||
# 1.5.3 - 2017-02-01
|
||||
|
||||
- Removed unused dependency
|
||||
([#54](https://github.com/Nyalab/caniuse-api/pull/54) - @wtgtybhertgeghgtwtg)
|
||||
|
||||
# 1.5.2 - 2016-09-05
|
||||
|
||||
- Fixed: no more generation `postinstall` hook ``\o/``.
|
||||
([#47](https://github.com/Nyalab/caniuse-api/pull/47) - @alexisvincent)
|
||||
|
||||
# 1.5.1 - 2016-08-06
|
||||
|
||||
- Fixed: Do not fail when browserslist gives a browser that caniuse-api doesn't
|
||||
know about
|
||||
([#45](https://github.com/Nyalab/caniuse-api/pull/45) - @onigoetz)
|
||||
|
||||
# 1.5.0 - 2016-06-01
|
||||
|
||||
- Added: JSPM support with explicit file extensions ([#40](https://github.com/Nyalab/caniuse-api/issues/40))
|
||||
- Upgraded: dependecies (lodash.memoize, lodash.uniq, shelljs, babel-tape-runner, tape, tap-spec)
|
||||
- Upgraded: ask travis to only test node stable
|
||||
- Upgraded: some tests fixed, some tests added
|
||||
|
||||
# 1.4.1 - 2015-10-18
|
||||
|
||||
- Fixed: `generator.js` was missing
|
||||
|
||||
# 1.4.0 - 2015-10-18
|
||||
|
||||
- Upgraded: browserslist 1.x
|
||||
- Upgraded: shelljs 0.5.x
|
||||
- Added: output to notify if generation has been made or not
|
||||
(related to [#25](https://github.com/Nyalab/caniuse-api/issues/25))
|
||||
|
||||
# 1.3.2 - 2015-06-23
|
||||
|
||||
- Fixed: lodash.uniq dep
|
||||
([#31](https://github.com/Nyalab/caniuse-api/issues/31))
|
||||
|
||||
# 1.3.1 - 2015-03-31
|
||||
|
||||
- Fixed: Windows support
|
||||
|
||||
# 1.3.0 - 2015-03-30
|
||||
|
||||
- Added: better exception messages
|
||||
- Added: full browserify compatibility (by avoiding dynamic require)
|
||||
|
||||
# 1.2.2 - 2015-02-06
|
||||
|
||||
- Fixed: postinstall hook for Windows
|
||||
|
||||
# 1.2.1 - 2015-02-04
|
||||
|
||||
- Changed: Allow in browser usage by avoiding `require.resolve` and using a generated json instead or reading a directory ([#20](https://github.com/Nyalab/caniuse-api/pull/20)]
|
||||
|
||||
# 1.2.0 [YANKED]
|
||||
|
||||
# 1.1.0 - 2015-02-03
|
||||
|
||||
- Fixed: usage of caniuse-db outside the package itself
|
||||
- Changed: upgrade to browserslist 0.2.x
|
||||
|
||||
# 1.0.0 - 2014-12-16
|
||||
|
||||
- Added: package is now automatically tested by [Travis-CI](https://travis-ci.org/Nyalab/caniuse-api)
|
||||
|
||||
# 0.1.0 - 2014-12-15
|
||||
|
||||
- Changed: complete API changes, released as `caniuse-api` package
|
||||
|
||||
# 0.0.1 - 2014-12-09
|
||||
|
||||
✨Initial release
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Sébastien Balayn
|
||||
|
||||
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.
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
# caniuse-api [](https://travis-ci.org/Nyalab/caniuse-api) [](https://ci.appveyor.com/project/MoOx/caniuse-api/branch/master)
|
||||
|
||||
request the caniuse data to check browsers compatibilities
|
||||
|
||||
## Installation
|
||||
|
||||
```console
|
||||
$ yarn add caniuse-api
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
const caniuse = require('caniuse-api')
|
||||
|
||||
caniuse.getSupport('border-radius')
|
||||
caniuse.isSupported('border-radius', 'ie 8, ie 9')
|
||||
caniuse.setBrowserScope('> 5%, last 1 version')
|
||||
caniuse.getSupport('border-radius')
|
||||
// ...
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
#### `caniuse.getSupport(feature)`
|
||||
|
||||
_ask since which browsers versions a feature is available_
|
||||
|
||||
* `y`: Since which browser version the feature is available
|
||||
* `n`: Up to which browser version the feature is unavailable
|
||||
* `a`: Up to which browser version the feature is partially supported
|
||||
* `x`: Up to which browser version the feature is prefixed
|
||||
|
||||
```js
|
||||
caniuse.getSupport('border-radius', true)
|
||||
/*
|
||||
{ and_chr: { y: 67 },
|
||||
and_ff: { y: 60 },
|
||||
and_qq: { y: 1.2 },
|
||||
and_uc: { y: 11.8 },
|
||||
android: { y: 2.1, x: 2.1 },
|
||||
baidu: { y: 7.12 },
|
||||
chrome: { y: 4, x: 4 },
|
||||
edge: { y: 12 },
|
||||
firefox: { a: 2, x: 3.6, y: 3 },
|
||||
ie: { n: 8, y: 9 },
|
||||
ie_mob: { y: 10 },
|
||||
ios_saf: { y: 3.2, x: 3.2 },
|
||||
op_mini: {},
|
||||
op_mob: { n: 10, y: 11 },
|
||||
opera: { n: 10, y: 10.5 },
|
||||
safari: { y: 3.1, x: 4 },
|
||||
samsung: { y: 4 } }
|
||||
*/
|
||||
```
|
||||
|
||||
#### `caniuse.isSupported(feature, browsers)`
|
||||
|
||||
_ask if a feature is supported by some browsers_
|
||||
|
||||
```js
|
||||
caniuse.isSupported('border-radius', 'ie 8, ie 9') // false
|
||||
caniuse.isSupported('border-radius', 'ie 9') // true
|
||||
```
|
||||
|
||||
#### `caniuse.find(query)`
|
||||
|
||||
_search for a caniuse feature name_
|
||||
|
||||
Ex:
|
||||
|
||||
```js
|
||||
caniuse.find('radius') // ['border-radius']
|
||||
caniuse.find('nothingness') // []
|
||||
caniuse.find('css3')
|
||||
/*
|
||||
[ 'css3-attr',
|
||||
'css3-boxsizing',
|
||||
'css3-colors',
|
||||
'css3-cursors-grab',
|
||||
'css3-cursors-newer',
|
||||
'css3-cursors',
|
||||
'css3-tabsize' ]
|
||||
*/
|
||||
```
|
||||
|
||||
#### `caniuse.getLatestStableBrowsers()`
|
||||
|
||||
_get the current version for each browser_
|
||||
|
||||
```js
|
||||
caniuse.getLatestStableBrowsers()
|
||||
/*
|
||||
[ 'and_chr 67',
|
||||
'and_ff 60',
|
||||
'and_qq 1.2',
|
||||
'and_uc 11.8',
|
||||
'android 67',
|
||||
'baidu 7.12',
|
||||
'bb 10',
|
||||
'chrome 67',
|
||||
'edge 17',
|
||||
'firefox 61',
|
||||
'ie 11',
|
||||
'ie_mob 11',
|
||||
'ios_saf 11.3-11.4',
|
||||
'op_mini all',
|
||||
'op_mob 46',
|
||||
'opera 53',
|
||||
'safari 11.1',
|
||||
'samsung 7.2' ]
|
||||
*/
|
||||
```
|
||||
|
||||
#### `caniuse.getBrowserScope()`
|
||||
|
||||
_returns a list of browsers currently used for the scope of operations_
|
||||
|
||||
```js
|
||||
caniuse.getBrowserScope()
|
||||
/*
|
||||
[ 'and_chr',
|
||||
'and_ff',
|
||||
'and_qq',
|
||||
'and_uc',
|
||||
'android',
|
||||
'baidu',
|
||||
'chrome',
|
||||
'edge',
|
||||
'firefox',
|
||||
'ie',
|
||||
'ie_mob',
|
||||
'ios_saf',
|
||||
'op_mini',
|
||||
'op_mob',
|
||||
'opera',
|
||||
'safari',
|
||||
'samsung' ]
|
||||
*/
|
||||
```
|
||||
|
||||
#### `caniuse.setBrowserScope(browserscope)`
|
||||
|
||||
_if you do not like the default browser scope, you can set it globally by using this method_
|
||||
|
||||
* browserscope should be a 'autoprefixer' formatted string
|
||||
|
||||
```js
|
||||
caniuse.setBrowserScope('> 5%, last 2 versions, Firefox ESR, Opera 12.1')
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
## [Changelog](CHANGELOG.md)
|
||||
|
||||
## [License](LICENSE)
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.getBrowserScope = exports.setBrowserScope = exports.getLatestStableBrowsers = exports.find = exports.isSupported = exports.getSupport = exports.features = undefined;
|
||||
|
||||
var _lodash = require("lodash.memoize");
|
||||
|
||||
var _lodash2 = _interopRequireDefault(_lodash);
|
||||
|
||||
var _browserslist = require("browserslist");
|
||||
|
||||
var _browserslist2 = _interopRequireDefault(_browserslist);
|
||||
|
||||
var _caniuseLite = require("caniuse-lite");
|
||||
|
||||
var _utils = require("./utils");
|
||||
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||
|
||||
var featuresList = Object.keys(_caniuseLite.features);
|
||||
|
||||
var browsers = void 0;
|
||||
function setBrowserScope(browserList) {
|
||||
browsers = (0, _utils.cleanBrowsersList)(browserList);
|
||||
}
|
||||
|
||||
function getBrowserScope() {
|
||||
return browsers;
|
||||
}
|
||||
|
||||
var parse = (0, _lodash2.default)(_utils.parseCaniuseData, function (feat, browsers) {
|
||||
return feat.title + browsers;
|
||||
});
|
||||
|
||||
function getSupport(query) {
|
||||
var feature = void 0;
|
||||
try {
|
||||
feature = (0, _caniuseLite.feature)(_caniuseLite.features[query]);
|
||||
} catch (e) {
|
||||
var res = find(query);
|
||||
if (res.length === 1) return getSupport(res[0]);
|
||||
throw new ReferenceError("Please provide a proper feature name. Cannot find " + query);
|
||||
}
|
||||
return parse(feature, browsers);
|
||||
}
|
||||
|
||||
function isSupported(feature, browsers) {
|
||||
var data = void 0;
|
||||
try {
|
||||
data = (0, _caniuseLite.feature)(_caniuseLite.features[feature]);
|
||||
} catch (e) {
|
||||
var res = find(feature);
|
||||
if (res.length === 1) {
|
||||
data = _caniuseLite.features[res[0]];
|
||||
} else {
|
||||
throw new ReferenceError("Please provide a proper feature name. Cannot find " + feature);
|
||||
}
|
||||
}
|
||||
|
||||
return (0, _browserslist2.default)(browsers, { ignoreUnknownVersions: true }).map(function (browser) {
|
||||
return browser.split(" ");
|
||||
}).every(function (browser) {
|
||||
return data.stats[browser[0]] && data.stats[browser[0]][browser[1]] === "y";
|
||||
});
|
||||
}
|
||||
|
||||
function find(query) {
|
||||
if (typeof query !== "string") {
|
||||
throw new TypeError("The `query` parameter should be a string.");
|
||||
}
|
||||
|
||||
if (~featuresList.indexOf(query)) {
|
||||
// exact match
|
||||
return query;
|
||||
}
|
||||
|
||||
return featuresList.filter(function (file) {
|
||||
return (0, _utils.contains)(file, query);
|
||||
});
|
||||
}
|
||||
|
||||
function getLatestStableBrowsers() {
|
||||
return (0, _browserslist2.default)("last 1 version");
|
||||
}
|
||||
|
||||
setBrowserScope();
|
||||
|
||||
exports.features = featuresList;
|
||||
exports.getSupport = getSupport;
|
||||
exports.isSupported = isSupported;
|
||||
exports.find = find;
|
||||
exports.getLatestStableBrowsers = getLatestStableBrowsers;
|
||||
exports.setBrowserScope = setBrowserScope;
|
||||
exports.getBrowserScope = getBrowserScope;
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.contains = contains;
|
||||
exports.parseCaniuseData = parseCaniuseData;
|
||||
exports.cleanBrowsersList = cleanBrowsersList;
|
||||
|
||||
var _lodash = require("lodash.uniq");
|
||||
|
||||
var _lodash2 = _interopRequireDefault(_lodash);
|
||||
|
||||
var _browserslist = require("browserslist");
|
||||
|
||||
var _browserslist2 = _interopRequireDefault(_browserslist);
|
||||
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||
|
||||
function contains(str, substr) {
|
||||
return !!~str.indexOf(substr);
|
||||
}
|
||||
|
||||
function parseCaniuseData(feature, browsers) {
|
||||
var support = {};
|
||||
var letters;
|
||||
var letter;
|
||||
|
||||
browsers.forEach(function (browser) {
|
||||
support[browser] = {};
|
||||
for (var info in feature.stats[browser]) {
|
||||
letters = feature.stats[browser][info].replace(/#\d+/, "").trim().split(" ");
|
||||
info = parseFloat(info.split("-")[0]); //if info is a range, take the left
|
||||
if (isNaN(info)) continue;
|
||||
for (var i = 0; i < letters.length; i++) {
|
||||
letter = letters[i];
|
||||
if (letter === "d") {
|
||||
// skip this letter, we don't support it yet
|
||||
continue;
|
||||
} else if (letter === "y") {
|
||||
// min support asked, need to find the min value
|
||||
if (typeof support[browser][letter] === "undefined" || info < support[browser][letter]) {
|
||||
support[browser][letter] = info;
|
||||
}
|
||||
} else {
|
||||
// any other support, need to find the max value
|
||||
if (typeof support[browser][letter] === "undefined" || info > support[browser][letter]) {
|
||||
support[browser][letter] = info;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return support;
|
||||
}
|
||||
|
||||
function cleanBrowsersList(browserList) {
|
||||
return (0, _lodash2.default)((0, _browserslist2.default)(browserList).map(function (browser) {
|
||||
return browser.split(" ")[0];
|
||||
}));
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "caniuse-api",
|
||||
"version": "3.0.0",
|
||||
"description": "request the caniuse data to check browsers compatibilities",
|
||||
"repository": "https://github.com/nyalab/caniuse-api.git",
|
||||
"keywords": [
|
||||
"caniuse",
|
||||
"browserslist"
|
||||
],
|
||||
"authors": [
|
||||
"nyalab",
|
||||
"MoOx"
|
||||
],
|
||||
"license": "MIT",
|
||||
"main": "dist/index.js",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"dependencies": {
|
||||
"browserslist": "^4.0.0",
|
||||
"caniuse-lite": "^1.0.0",
|
||||
"lodash.memoize": "^4.1.2",
|
||||
"lodash.uniq": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.22.2",
|
||||
"babel-eslint": "^5.0.0",
|
||||
"babel-preset-latest": "^6.22.0",
|
||||
"babel-tape-runner": "^2.0.1",
|
||||
"jshint": "^2.5.10",
|
||||
"npmpub": "^3.1.0",
|
||||
"tap-spec": "^4.1.1",
|
||||
"tape": "^4.6.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "babel src --out-dir dist",
|
||||
"lint": "jshint src",
|
||||
"prepublish": "npm run build",
|
||||
"test": "npm run lint && babel-tape-runner test/*.js | tap-spec",
|
||||
"release": "npmpub"
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
"babel-preset-latest"
|
||||
]
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Sergey Berezhnoy <veged@ya.ru>
|
||||
|
||||
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.
|
||||
+340
@@ -0,0 +1,340 @@
|
||||
# Command-Option-Argument
|
||||
|
||||
Yet another parser for command line options.
|
||||
|
||||
[![NPM Status][npm-img]][npm]
|
||||
[![Travis Status][test-img]][travis]
|
||||
[![AppVeyor Status][appveyor-img]][appveyor]
|
||||
[![Coverage Status][coverage-img]][coveralls]
|
||||
[![Dependency Status][dependency-img]][david]
|
||||
|
||||
[npm]: https://www.npmjs.org/package/coa
|
||||
[npm-img]: https://img.shields.io/npm/v/coa.svg
|
||||
[travis]: https://travis-ci.org/veged/coa
|
||||
[test-img]: https://img.shields.io/travis/veged/coa.svg
|
||||
[appveyor]: https://ci.appveyor.com/project/zxqfox/coa
|
||||
[appveyor-img]: https://ci.appveyor.com/api/projects/status/github/veged/coa?svg=true
|
||||
[coveralls]: https://coveralls.io/r/veged/coa
|
||||
[coverage-img]: https://img.shields.io/coveralls/veged/coa.svg
|
||||
[david]: https://david-dm.org/veged/coa
|
||||
[dependency-img]: http://img.shields.io/david/veged/coa.svg
|
||||
|
||||
## What is it?
|
||||
|
||||
COA is a parser for command line options that aim to get maximum profit from formalization your program API.
|
||||
Once you write definition in terms of commands, options and arguments you automaticaly get:
|
||||
|
||||
* Command line help text
|
||||
* Program API for use COA-based programs as modules
|
||||
* Shell completion
|
||||
|
||||
### Other features
|
||||
|
||||
* Rich types for options and arguments, such as arrays, boolean flags and required
|
||||
* Commands can be async throught using promising (powered by [Q](https://github.com/kriskowal/q))
|
||||
* Easy submoduling some existing commands to new top-level one
|
||||
* Combined validation and complex parsing of values
|
||||
|
||||
### TODO
|
||||
|
||||
* Localization
|
||||
* Shell-mode
|
||||
* Configs
|
||||
* Aliases
|
||||
* Defaults
|
||||
|
||||
## Examples
|
||||
|
||||
````javascript
|
||||
require('coa').Cmd() // main (top level) command declaration
|
||||
.name(process.argv[1]) // set top level command name from program name
|
||||
.title('My awesome command line util') // title for use in text messages
|
||||
.helpful() // make command "helpful", i.e. options -h --help with usage message
|
||||
.opt() // add some option
|
||||
.name('version') // name for use in API
|
||||
.title('Version') // title for use in text messages
|
||||
.short('v') // short key: -v
|
||||
.long('version') // long key: --version
|
||||
.flag() // for options without value
|
||||
.act(function(opts) { // add action for option
|
||||
// return message as result of action
|
||||
return JSON.parse(require('fs').readFileSync(__dirname + '/package.json'))
|
||||
.version;
|
||||
})
|
||||
.end() // end option chain and return to main command
|
||||
.cmd().name('subcommand').apply(require('./subcommand').COA).end() // load subcommand from module
|
||||
.cmd() // inplace subcommand declaration
|
||||
.name('othercommand').title('Awesome other subcommand').helpful()
|
||||
.opt()
|
||||
.name('input').title('input file, required')
|
||||
.short('i').long('input')
|
||||
.val(function(v) { // validator function, also for translate simple values
|
||||
return require('fs').createReadStream(v) })
|
||||
.req() // make option required
|
||||
.end() // end option chain and return to command
|
||||
.end() // end subcommand chain and return to parent command
|
||||
.run(process.argv.slice(2)); // parse and run on process.argv
|
||||
````
|
||||
|
||||
````javascript
|
||||
// subcommand.js
|
||||
exports.COA = function() {
|
||||
this
|
||||
.title('Awesome subcommand').helpful()
|
||||
.opt()
|
||||
.name('output').title('output file')
|
||||
.short('o').long('output')
|
||||
.output() // use default preset for "output" option declaration
|
||||
.end()
|
||||
};
|
||||
````
|
||||
|
||||
## API reference
|
||||
|
||||
### Cmd
|
||||
Command is a top level entity. Commands may have options and arguments.
|
||||
|
||||
#### Cmd.api
|
||||
Returns object containing all its subcommands as methods to use from other programs.<br>
|
||||
**@returns** *{Object}*
|
||||
|
||||
#### Cmd.name
|
||||
Set a canonical command identifier to be used anywhere in the API.<br>
|
||||
**@param** *String* `_name` command name<br>
|
||||
**@returns** *COA.Cmd* `this` instance (for chainability)
|
||||
|
||||
#### Cmd.title
|
||||
Set a long description for command to be used anywhere in text messages.<br>
|
||||
**@param** *String* `_title` command title<br>
|
||||
**@returns** *COA.Cmd* `this` instance (for chainability)
|
||||
|
||||
#### Cmd.cmd
|
||||
Create new or add existing subcommand for current command.<br>
|
||||
**@param** *COA.Cmd* `[cmd]` existing command instance<br>
|
||||
**@returns** *COA.Cmd* new or added subcommand instance
|
||||
|
||||
#### Cmd.opt
|
||||
Create option for current command.<br>
|
||||
**@returns** *COA.Opt* `new` option instance
|
||||
|
||||
#### Cmd.arg
|
||||
Create argument for current command.<br>
|
||||
**@returns** *COA.Opt* `new` argument instance
|
||||
|
||||
#### Cmd.act
|
||||
Add (or set) action for current command.<br>
|
||||
**@param** *Function* `act` action function,
|
||||
invoked in the context of command instance
|
||||
and has the parameters:<br>
|
||||
- *Object* `opts` parsed options<br>
|
||||
- *Array* `args` parsed arguments<br>
|
||||
- *Object* `res` actions result accumulator<br>
|
||||
It can return rejected promise by Cmd.reject (in case of error)
|
||||
or any other value treated as result.<br>
|
||||
**@param** *{Boolean}* [force=false] flag for set action instead add to existings<br>
|
||||
**@returns** *COA.Cmd* `this` instance (for chainability)
|
||||
|
||||
#### Cmd.apply
|
||||
Apply function with arguments in context of command instance.<br>
|
||||
**@param** *Function* `fn`<br>
|
||||
**@param** *Array* `args`<br>
|
||||
**@returns** *COA.Cmd* `this` instance (for chainability)
|
||||
|
||||
#### Cmd.comp
|
||||
Set custom additional completion for current command.<br>
|
||||
**@param** *Function* `fn` completion generation function,
|
||||
invoked in the context of command instance.
|
||||
Accepts parameters:<br>
|
||||
- *Object* `opts` completion options<br>
|
||||
It can return promise or any other value treated as result.<br>
|
||||
**@returns** *COA.Cmd* `this` instance (for chainability)
|
||||
|
||||
#### Cmd.helpful
|
||||
Make command "helpful", i.e. add -h --help flags for print usage.<br>
|
||||
**@returns** *COA.Cmd* `this` instance (for chainability)
|
||||
|
||||
#### Cmd.completable
|
||||
Adds shell completion to command, adds "completion" subcommand, that makes all the magic.<br>
|
||||
Must be called only on root command.<br>
|
||||
**@returns** *COA.Cmd* `this` instance (for chainability)
|
||||
|
||||
#### Cmd.usage
|
||||
Build full usage text for current command instance.<br>
|
||||
**@returns** *String* `usage` text
|
||||
|
||||
#### Cmd.run
|
||||
Parse arguments from simple format like NodeJS process.argv
|
||||
and run ahead current program, i.e. call process.exit when all actions done.<br>
|
||||
**@param** *Array* `argv`<br>
|
||||
**@returns** *COA.Cmd* `this` instance (for chainability)
|
||||
|
||||
#### Cmd.invoke
|
||||
Invoke specified (or current) command using provided options and arguments.<br>
|
||||
**@param** *String|Array* `cmds` subcommand to invoke (optional)<br>
|
||||
**@param** *Object* `opts` command options (optional)<br>
|
||||
**@param** *Object* `args` command arguments (optional)<br>
|
||||
**@returns** *Q.Promise*
|
||||
|
||||
#### Cmd.reject
|
||||
Return reject of actions results promise.<br>
|
||||
Use in .act() for return with error.<br>
|
||||
**@param** *Object* `reason` reject reason<br>
|
||||
You can customize toString() method and exitCode property
|
||||
of reason object.<br>
|
||||
**@returns** *Q.promise* rejected promise
|
||||
|
||||
#### Cmd.end
|
||||
Finish chain for current subcommand and return parent command instance.<br>
|
||||
**@returns** *COA.Cmd* `parent` command
|
||||
|
||||
### Opt
|
||||
Option is a named entity. Options may have short and long keys for use from command line.<br>
|
||||
**@namespace**<br>
|
||||
**@class** Presents option
|
||||
|
||||
#### Opt.name
|
||||
Set a canonical option identifier to be used anywhere in the API.<br>
|
||||
**@param** *String* `_name` option name<br>
|
||||
**@returns** *COA.Opt* `this` instance (for chainability)
|
||||
|
||||
#### Opt.title
|
||||
Set a long description for option to be used anywhere in text messages.<br>
|
||||
**@param** *String* `_title` option title<br>
|
||||
**@returns** *COA.Opt* `this` instance (for chainability)
|
||||
|
||||
#### Opt.short
|
||||
Set a short key for option to be used with one hyphen from command line.<br>
|
||||
**@param** *String* `_short`<br>
|
||||
**@returns** *COA.Opt* `this` instance (for chainability)
|
||||
|
||||
#### Opt.long
|
||||
Set a short key for option to be used with double hyphens from command line.<br>
|
||||
**@param** *String* `_long`<br>
|
||||
**@returns** *COA.Opt* `this` instance (for chainability)
|
||||
|
||||
#### Opt.flag
|
||||
Make an option boolean, i.e. option without value.<br>
|
||||
**@returns** *COA.Opt* `this` instance (for chainability)
|
||||
|
||||
#### Opt.arr
|
||||
Makes an option accepts multiple values.<br>
|
||||
Otherwise, the value will be used by the latter passed.<br>
|
||||
**@returns** *COA.Opt* `this` instance (for chainability)
|
||||
|
||||
#### Opt.req
|
||||
Makes an option req.<br>
|
||||
**@returns** *COA.Opt* `this` instance (for chainability)
|
||||
|
||||
#### Opt.only
|
||||
Makes an option to act as a command,
|
||||
i.e. program will exit just after option action.<br>
|
||||
**@returns** *COA.Opt* `this` instance (for chainability)
|
||||
|
||||
#### Opt.val
|
||||
Set a validation (or value) function for argument.<br>
|
||||
Value from command line passes through before becoming available from API.<br>
|
||||
Using for validation and convertion simple types to any values.<br>
|
||||
**@param** *Function* `_val` validating function,
|
||||
invoked in the context of option instance
|
||||
and has one parameter with value from command line<br>
|
||||
**@returns** *COA.Opt* `this` instance (for chainability)
|
||||
|
||||
#### Opt.def
|
||||
Set a default value for option.
|
||||
Default value passed through validation function as ordinary value.<br>
|
||||
**@param** *Object* `_def`<br>
|
||||
**@returns** *COA.Opt* `this` instance (for chainability)
|
||||
|
||||
#### Opt.input
|
||||
Make option value inputting stream.
|
||||
It's add useful validation and shortcut for STDIN.
|
||||
**@returns** *{COA.Opt}* `this` instance (for chainability)
|
||||
|
||||
#### Opt.output
|
||||
Make option value outputing stream.<br>
|
||||
It's add useful validation and shortcut for STDOUT.<br>
|
||||
**@returns** *COA.Opt* `this` instance (for chainability)
|
||||
|
||||
#### Opt.act
|
||||
Add action for current option command.
|
||||
This action is performed if the current option
|
||||
is present in parsed options (with any value).<br>
|
||||
**@param** *Function* `act` action function,
|
||||
invoked in the context of command instance
|
||||
and has the parameters:<br>
|
||||
- *Object* `opts` parsed options<br>
|
||||
- *Array* `args` parsed arguments<br>
|
||||
- *Object* `res` actions result accumulator<br>
|
||||
It can return rejected promise by Cmd.reject (in case of error)
|
||||
or any other value treated as result.<br>
|
||||
**@returns** *COA.Opt* `this` instance (for chainability)
|
||||
|
||||
#### Opt.comp
|
||||
Set custom additional completion for current option.<br>
|
||||
**@param** *Function* `fn` completion generation function,
|
||||
invoked in the context of command instance.
|
||||
Accepts parameters:<br>
|
||||
- *Object* `opts` completion options<br>
|
||||
It can return promise or any other value treated as result.<br>
|
||||
**@returns** *COA.Opt* `this` instance (for chainability)
|
||||
|
||||
#### Opt.end
|
||||
Finish chain for current option and return parent command instance.<br>
|
||||
**@returns** *COA.Cmd* `parent` command
|
||||
|
||||
|
||||
### Arg
|
||||
Argument is a unnamed entity.<br>
|
||||
From command line arguments passed as list of unnamed values.
|
||||
|
||||
#### Arg.name
|
||||
Set a canonical argument identifier to be used anywhere in text messages.<br>
|
||||
**@param** *String* `_name` argument name<br>
|
||||
**@returns** *COA.Arg* `this` instance (for chainability)
|
||||
|
||||
#### Arg.title
|
||||
Set a long description for argument to be used anywhere in text messages.<br>
|
||||
**@param** *String* `_title` argument title<br>
|
||||
**@returns** *COA.Arg* `this` instance (for chainability)
|
||||
|
||||
#### Arg.arr
|
||||
Makes an argument accepts multiple values.<br>
|
||||
Otherwise, the value will be used by the latter passed.<br>
|
||||
**@returns** *COA.Arg* `this` instance (for chainability)
|
||||
|
||||
#### Arg.req
|
||||
Makes an argument req.<br>
|
||||
**@returns** *COA.Arg* `this` instance (for chainability)
|
||||
|
||||
#### Arg.val
|
||||
Set a validation (or value) function for argument.<br>
|
||||
Value from command line passes through before becoming available from API.<br>
|
||||
Using for validation and convertion simple types to any values.<br>
|
||||
**@param** *Function* `_val` validating function,
|
||||
invoked in the context of argument instance
|
||||
and has one parameter with value from command line<br>
|
||||
**@returns** *COA.Arg* `this` instance (for chainability)
|
||||
|
||||
#### Arg.def
|
||||
Set a default value for argument.
|
||||
Default value passed through validation function as ordinary value.<br>
|
||||
**@param** *Object* `_def`<br>
|
||||
**@returns** *COA.Arg* `this` instance (for chainability)
|
||||
|
||||
#### Arg.output
|
||||
Make argument value outputing stream.<br>
|
||||
It's add useful validation and shortcut for STDOUT.<br>
|
||||
**@returns** *COA.Arg* `this` instance (for chainability)
|
||||
|
||||
#### Arg.comp
|
||||
Set custom additional completion for current argument.<br>
|
||||
**@param** *Function* `fn` completion generation function,
|
||||
invoked in the context of command instance.
|
||||
Accepts parameters:<br>
|
||||
- *Object* `opts` completion options<br>
|
||||
It can return promise or any other value treated as result.<br>
|
||||
**@returns** *COA.Arg* `this` instance (for chainability)
|
||||
|
||||
#### Arg.end
|
||||
Finish chain for current option and return parent command instance.<br>
|
||||
**@returns** *COA.Cmd* `parent` command
|
||||
+316
@@ -0,0 +1,316 @@
|
||||
# Command-Option-Argument
|
||||
[](http://travis-ci.org/veged/coa)
|
||||
|
||||
## Что это?
|
||||
|
||||
COA — парсер параметров командной строки, позволяющий извлечь максимум пользы от формального API вашей программы.
|
||||
Как только вы опишете определение в терминах команд, параметров и аргументов, вы автоматически получите:
|
||||
|
||||
* Справку для командной строки
|
||||
* API для использования программы как модуля в COA-совместимых программах
|
||||
* Автодополнение для командной строки
|
||||
|
||||
### Прочие возможности
|
||||
|
||||
* Широкий выбор настроек для параметров и аргументов, включая множественные значения, логические значения и обязательность параметров
|
||||
* Возможность асинхронного исполнения команд, используя промисы (используется библиотека [Q](https://github.com/kriskowal/q))
|
||||
* Простота использования существующих команд как подмодулей для новых команд
|
||||
* Комбинированная валидация и анализ сложных значений
|
||||
|
||||
## Примеры
|
||||
|
||||
````javascript
|
||||
require('coa').Cmd() // декларация команды верхнего уровня
|
||||
.name(process.argv[1]) // имя команды верхнего уровня, берем из имени программы
|
||||
.title('Жутко полезная утилита для командной строки') // название для использования в справке и сообщениях
|
||||
.helpful() // добавляем поддержку справки командной строки (-h, --help)
|
||||
.opt() // добавляем параметр
|
||||
.name('version') // имя параметра для использования в API
|
||||
.title('Version') // текст для вывода в сообщениях
|
||||
.short('v') // короткое имя параметра: -v
|
||||
.long('version') // длинное имя параметра: --version
|
||||
.flag() // параметр не требует ввода значения
|
||||
.act(function(opts) { // действия при вызове аргумента
|
||||
// результатом является вывод текстового сообщения
|
||||
return JSON.parse(require('fs').readFileSync(__dirname + '/package.json'))
|
||||
.version;
|
||||
})
|
||||
.end() // завершаем определение параметра и возвращаемся к определению верхнего уровня
|
||||
.cmd().name('subcommand').apply(require('./subcommand').COA).end() // загрузка подкоманды из модуля
|
||||
.cmd() // добавляем еще одну подкоманду
|
||||
.name('othercommand').title('Еще одна полезная подпрограмма').helpful()
|
||||
.opt()
|
||||
.name('input').title('input file, required')
|
||||
.short('i').long('input')
|
||||
.val(function(v) { // функция-валидатор, также может использоваться для трансформации значений параметров
|
||||
return require('fs').createReadStream(v) })
|
||||
.req() // параметр является обязательным
|
||||
.end() // завершаем определение параметра и возвращаемся к определению команды
|
||||
.end() // завершаем определение подкоманды и возвращаемся к определению команды верхнего уровня
|
||||
.run(process.argv.slice(2)); // разбираем process.argv и запускаем
|
||||
````
|
||||
|
||||
````javascript
|
||||
// subcommand.js
|
||||
exports.COA = function() {
|
||||
this
|
||||
.title('Полезная подпрограмма').helpful()
|
||||
.opt()
|
||||
.name('output').title('output file')
|
||||
.short('o').long('output')
|
||||
.output() // использовать стандартную настройку для параметра вывода
|
||||
.end()
|
||||
};
|
||||
````
|
||||
|
||||
## API
|
||||
|
||||
### Cmd
|
||||
Команда — сущность верхнего уровня. У команды могут быть определены параметры и аргументы.
|
||||
|
||||
#### Cmd.api
|
||||
Возвращает объект, который можно использовать в других программах. Подкоманды являются методами этого объекта.<br>
|
||||
**@returns** *{Object}*
|
||||
|
||||
#### Cmd.name
|
||||
Определяет канонический идентификатор команды, используемый в вызовах API.<br>
|
||||
**@param** *String* `_name` имя команды<br>
|
||||
**@returns** *COA.Cmd* `this` экземпляр команды (для поддержки цепочки методов)
|
||||
|
||||
#### Cmd.title
|
||||
Определяет название команды, используемый в текстовых сообщениях.<br>
|
||||
**@param** *String* `_title` название команды<br>
|
||||
**@returns** *COA.Cmd* `this` экземпляр команды (для поддержки цепочки методов)
|
||||
|
||||
#### Cmd.cmd
|
||||
Создает новую подкоманду или добавляет ранее определенную подкоманду к текущей команде.<br>
|
||||
**@param** *COA.Cmd* `[cmd]` экземпляр ранее определенной подкоманды<br>
|
||||
**@returns** *COA.Cmd* экземпляр новой или ранее определенной подкоманды
|
||||
|
||||
#### Cmd.opt
|
||||
Создает параметр для текущей команды.<br>
|
||||
**@returns** *COA.Opt* `new` экземпляр параметра
|
||||
|
||||
#### Cmd.arg
|
||||
Создает аргумент для текущей команды.<br>
|
||||
**@returns** *COA.Opt* `new` экземпляр аргумента
|
||||
|
||||
#### Cmd.act
|
||||
Добавляет (или создает) действие для текущей команды.<br>
|
||||
**@param** *Function* `act` функция,
|
||||
выполняемая в контексте экземпляра текущей команды
|
||||
и принимающая следующие параметры:<br>
|
||||
- *Object* `opts` параметры команды<br>
|
||||
- *Array* `args` аргументы команды<br>
|
||||
- *Object* `res` объект-аккумулятор результатов<br>
|
||||
Функция может вернуть проваленный промис из Cmd.reject (в случае ошибки)
|
||||
или любое другое значение, рассматриваемое как результат.<br>
|
||||
**@param** *{Boolean}* [force=false] флаг, назначающий немедленное исполнение вместо добавления к списку существующих действий<br>
|
||||
**@returns** *COA.Cmd* `this` экземпляр команды (для поддержки цепочки методов)
|
||||
|
||||
#### Cmd.apply
|
||||
Исполняет функцию с переданными аргументами в контексте экземпляра текущей команды.<br>
|
||||
**@param** *Function* `fn`<br>
|
||||
**@param** *Array* `args`<br>
|
||||
**@returns** *COA.Cmd* `this` экземпляр команды (для поддержки цепочки методов)
|
||||
|
||||
#### Cmd.comp
|
||||
Назначает кастомную функцию автодополнения для текущей команды.<br>
|
||||
**@param** *Function* `fn` функция-генератор автодополнения,
|
||||
исполняемая в контексте текущей команды.
|
||||
Принимает параметры:<br>
|
||||
- *Object* `opts` параметры<br>
|
||||
Может возвращать промис или любое другое значение, рассматриваемое как результат исполнения команды.<br>
|
||||
**@returns** *COA.Cmd* `this` экземпляр команды (для поддержки цепочки методов)
|
||||
|
||||
#### Cmd.helpful
|
||||
Ставит флаг поддержки справки командной строки, т.е. вызов команды с параметрами -h --help выводит справку по работе с командой.<br>
|
||||
**@returns** *COA.Cmd* `this` экземпляр команды (для поддержки цепочки методов)
|
||||
|
||||
#### Cmd.completable
|
||||
Добавляет поддержку автодополнения командной строки. Добавляется подкоманда "completion", которая выполняет все необходимые действия.<br>
|
||||
Может быть добавлен только для главной команды.<br>
|
||||
**@returns** *COA.Cmd* `this` экземпляр команды (для поддержки цепочки методов)
|
||||
|
||||
#### Cmd.usage
|
||||
Возвращает текст справки по использованию команды для текущего экземпляра.<br>
|
||||
**@returns** *String* `usage` Текст справки по использованию
|
||||
|
||||
#### Cmd.run
|
||||
Разбирает аргументы из значения, возвращаемого NodeJS process.argv,
|
||||
и запускает текущую программу, т.е. вызывает process.exit после завершения
|
||||
всех действий.<br>
|
||||
**@param** *Array* `argv`<br>
|
||||
**@returns** *COA.Cmd* `this` экземпляр команды (для поддержки цепочки методов)
|
||||
|
||||
#### Cmd.invoke
|
||||
Исполняет переданную (или текущую) команду с указанными параметрами и аргументами.<br>
|
||||
**@param** *String|Array* `cmds` подкоманда для исполнения (необязательно)<br>
|
||||
**@param** *Object* `opts` параметры, передаваемые команде (необязательно)<br>
|
||||
**@param** *Object* `args` аргументы, передаваемые команде (необязательно)<br>
|
||||
**@returns** *Q.Promise*
|
||||
|
||||
#### Cmd.reject
|
||||
Проваливает промисы, возращенные в действиях.<br>
|
||||
Используется в .act() для возврата с ошибкой.<br>
|
||||
**@param** *Object* `reason` причина провала<br>
|
||||
Вы можете определить метод toString() и свойство toString()
|
||||
объекта причины провала.<br>
|
||||
**@returns** *Q.promise* проваленный промис
|
||||
|
||||
#### Cmd.end
|
||||
Завершает цепочку методов текущей подкоманды и возвращает экземпляр родительской команды.<br>
|
||||
**@returns** *COA.Cmd* `parent` родительская команда
|
||||
|
||||
### Opt
|
||||
Параметр — именованная сущность. У параметра может быть определено короткое или длинное имя для использования из командной строки.<br>
|
||||
**@namespace**<br>
|
||||
**@class** Переданный параметр
|
||||
|
||||
#### Opt.name
|
||||
Определяет канонический идентификатор параметра, используемый в вызовах API.<br>
|
||||
**@param** *String* `_name` имя параметра<br>
|
||||
**@returns** *COA.Opt* `this` экземпляр параметра (для поддержки цепочки методов)
|
||||
|
||||
#### Opt.title
|
||||
Определяет описание для параметра, используемое в текстовых сообщениях.<br>
|
||||
**@param** *String* `_title` название параметра<br>
|
||||
**@returns** *COA.Opt* `this` экземпляр параметра (для поддержки цепочки методов)
|
||||
|
||||
#### Opt.short
|
||||
Назначает ключ для короткого имени параметра, передаваемого из командной строки с одинарным дефисом (например, `-v`).<br>
|
||||
**@param** *String* `_short`<br>
|
||||
**@returns** *COA.Opt* `this` экземпляр параметра (для поддержки цепочки методов)
|
||||
|
||||
#### Opt.long
|
||||
Назначает ключ для длинного имени параметра, передаваемого из командной строки с двойным дефисом (например, `--version`).<br>
|
||||
**@param** *String* `_long`<br>
|
||||
**@returns** *COA.Opt* `this` экземпляр параметра (для поддержки цепочки методов)
|
||||
|
||||
#### Opt.flag
|
||||
Помечает параметр как логический, т.е. параметр не имеющий значения.<br>
|
||||
**@returns** *COA.Opt* `this` экземпляр параметра (для поддержки цепочки методов)
|
||||
|
||||
#### Opt.arr
|
||||
Помечает параметр как принимающий множественные значения.<br>
|
||||
Иначе будет использовано последнее переданное значение параметра.<br>
|
||||
**@returns** *COA.Opt* `this` экземпляр параметра (для поддержки цепочки методов)
|
||||
|
||||
#### Opt.req
|
||||
Помечает параметр как обязательный.<br>
|
||||
**@returns** *COA.Opt* `this` экземпляр параметра (для поддержки цепочки методов)
|
||||
|
||||
#### Opt.only
|
||||
Интерпретирует параметр как команду,
|
||||
т.е. программа будет завершена сразу после выполнения параметра.<br>
|
||||
**@returns** *COA.Opt* `this` экземпляр параметра (для поддержки цепочки методов)
|
||||
|
||||
#### Opt.val
|
||||
Назначает функцию валидации (или трансформации значения) для значения параметра.<br>
|
||||
Значение, полученное из командной строки, передается в функцию-валидатор прежде чем оно станет доступно из API.<br>
|
||||
Используется для валидации и трансформации введенных данных.<br>
|
||||
**@param** *Function* `_val` функция валидации,
|
||||
исполняемая в контексте экземпляра параметра
|
||||
и принимающая в качестве единственного параметра значение, полученное
|
||||
из командной строки<br>
|
||||
**@returns** *COA.Opt* `this` экземпляр параметра (для поддержки цепочки методов)
|
||||
|
||||
#### Opt.def
|
||||
Назначает значение параметра по умолчанию. Это значение также передается
|
||||
в функцию валидации как обычное значение.<br>
|
||||
**@param** *Object* `_def`<br>
|
||||
**@returns** *COA.Opt* `this` экземпляр параметра (для поддержки цепочки методов)
|
||||
|
||||
#### Opt.input
|
||||
Помечает параметр как принимающий ввод пользователя. <br>
|
||||
Позволяет использовать валидацию для STDIN.<br>
|
||||
**@returns** *{COA.Opt}* `this` экземпляр параметра (для поддержки цепочки методов)
|
||||
|
||||
#### Opt.output
|
||||
Помечает параметр как вывод.<br>
|
||||
Позволяет использовать валидацию для STDOUT.<br>
|
||||
**@returns** *COA.Opt* `this` экземпляр параметра (для поддержки цепочки методов)
|
||||
|
||||
#### Opt.act
|
||||
Добавляет (или создает) действие для текущего параметра команды.
|
||||
Это действие будет выполнено, если текущий параметр есть
|
||||
в списке полученных параметров (с любым значением).<br>
|
||||
**@param** *Function* `act` функция, выполняемая в контексте
|
||||
экземпляра текущей команды и принимающая следующие параметры:<br>
|
||||
- *Object* `opts` параметры команды<br>
|
||||
- *Array* `args` аргументы команды<br>
|
||||
- *Object* `res` объект-аккумулятор результатов<br>
|
||||
Функция может вернуть проваленный промис из Cmd.reject (в случае ошибки)
|
||||
или любое другое значение, рассматриваемое как результат.<br>
|
||||
**@returns** *COA.Opt* `this` экземпляр параметра (для поддержки цепочки методов)
|
||||
|
||||
#### Opt.comp
|
||||
Назначает кастомную функцию автодополнения для текущей команды.<br>
|
||||
**@param** *Function* `fn` функция-генератор автодоплнения, исполняемая в
|
||||
контексте экземпляра команды.
|
||||
Принимает параметры:<br>
|
||||
- *Object* `opts` параметры автодополнения<br>
|
||||
Может возвращать промис или любое другое значение, рассматриваемое как результат исполнения команды.<br>
|
||||
**@returns** *COA.Opt* `this` экземпляр параметра (для поддержки цепочки методов)
|
||||
|
||||
#### Opt.end
|
||||
Завершает цепочку методов текущего параметра и возвращает экземпляр родительской команды.<br>
|
||||
**@returns** *COA.Cmd* `parent` родительская команда
|
||||
|
||||
|
||||
### Arg
|
||||
Аргумент — неименованная сущность.<br>
|
||||
Аргументы передаются из командной строки как список неименованных значений.
|
||||
|
||||
#### Arg.name
|
||||
Определяет канонический идентификатор аргумента, используемый в вызовах API.<br>
|
||||
**@param** *String* `_name` имя аргумента<br>
|
||||
**@returns** *COA.Arg* `this` экземпляр аргумента (для поддержки цепочки методов)
|
||||
|
||||
#### Arg.title
|
||||
Определяет описание для аргумента, используемое в текстовых сообщениях.<br>
|
||||
**@param** *String* `_title` описание аргумента<br>
|
||||
**@returns** *COA.Arg* `this` экземпляр аргумента (для поддержки цепочки методов)
|
||||
|
||||
#### Arg.arr
|
||||
Помечает аргумент как принимающий множественные значения.<br>
|
||||
Иначе будет использовано последнее переданное значение аргумента.<br>
|
||||
**@returns** *COA.Arg* `this` экземпляр аргумента (для поддержки цепочки методов)
|
||||
|
||||
#### Arg.req
|
||||
Помечает аргумент как обязательный.<br>
|
||||
**@returns** *COA.Arg* `this` экземпляр аргумента (для поддержки цепочки методов)
|
||||
|
||||
#### Arg.val
|
||||
Назначает функцию валидации (или трансформации значения) для аргумента.<br>
|
||||
Значение, полученное из командной строки, передается в функцию-валидатор прежде чем оно станет доступно из API.<br>
|
||||
Используется для валидации и трансформации введенных данных.<br>
|
||||
**@param** *Function* `_val` функция валидации,
|
||||
исполняемая в контексте экземпляра аргумента
|
||||
и принимающая в качестве единственного параметра значение, полученное
|
||||
из командной строки<br>
|
||||
**@returns** *COA.Opt* `this` экземпляр аргумента (для поддержки цепочки методов)
|
||||
|
||||
#### Arg.def
|
||||
Назначает дефолтное значение для аргумента. Дефолтное значение передается
|
||||
в функцию валидации как обычное значение.<br>
|
||||
**@param** *Object* `_def`<br>
|
||||
**@returns** *COA.Arg* `this` экземпляр аргумента (для поддержки цепочки методов)
|
||||
|
||||
#### Arg.output
|
||||
Помечает параметр как вывод.<br>
|
||||
Позволяет назначить валидацию для STDOUT.<br>
|
||||
**@returns** *COA.Arg* `this` экземпляр аргумента (для поддержки цепочки методов)
|
||||
|
||||
#### Arg.comp
|
||||
Назначает кастомную функцию автодополнения для текущего аргумента.<br>
|
||||
**@param** *Function* `fn` функция-генератор автодоплнения,
|
||||
исполняемая в контексте текущей команды.
|
||||
Принимает параметры:<br>
|
||||
- *Object* `opts` параметры
|
||||
Может возвращать промис или любое другое значение, рассматриваемое как результат исполнения команды.<br>
|
||||
**@returns** *COA.Arg* `this` экземпляр аргумента (для поддержки цепочки методов)
|
||||
|
||||
#### Arg.end
|
||||
Завершает цепочку методов текущего аргумента и возвращает экземпляр родительской команды.<br>
|
||||
**@returns** *COA.Cmd* `parent` родительская команда
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
/// <reference types="q"/>
|
||||
|
||||
export const Arg: undefined;
|
||||
|
||||
export const Opt: undefined;
|
||||
|
||||
export function Cmd(cmd?: classes.Cmd): classes.Cmd;
|
||||
|
||||
export namespace classes {
|
||||
class Arg {
|
||||
constructor(cmd: Cmd);
|
||||
name(name: string): Arg;
|
||||
title(title: string): Arg;
|
||||
arr(): Arg;
|
||||
req(): Arg;
|
||||
val(validation: (this: Arg, value: any) => boolean): Arg;
|
||||
def(def: any): Arg;
|
||||
output(): Arg;
|
||||
comp(fn: (opts: any) => any): Arg;
|
||||
end(): Cmd;
|
||||
apply(...args: any[]): Arg;
|
||||
input(): Arg;
|
||||
reject(...args: any[]): Arg;
|
||||
}
|
||||
|
||||
class Cmd {
|
||||
constructor(cmd?: Cmd);
|
||||
static create(cmd?: Cmd): Cmd;
|
||||
api(): any;
|
||||
name(name: string): Cmd;
|
||||
title(title: string): Cmd;
|
||||
cmd(cmd?: Cmd): Cmd;
|
||||
opt(): Opt;
|
||||
arg(): Arg;
|
||||
act(act: (opts: any, args: any[], res: any) => any, force?: boolean): Cmd;
|
||||
apply(fn: Function, args?: any[]): Cmd;
|
||||
comp(fs: (opts: any) => any): Cmd;
|
||||
helpful(): Cmd;
|
||||
completable(): Cmd;
|
||||
usage(): string;
|
||||
run(argv: string[]): Cmd;
|
||||
invoke(cmds?: string|string[], opts?: any, args?: any): Q.Promise<any>;
|
||||
reject(reason: any): Q.Promise<any>;
|
||||
end(): Cmd;
|
||||
do(argv: string[]): any;
|
||||
extendable(pattern?: string): Cmd;
|
||||
}
|
||||
|
||||
class Opt {
|
||||
constructor(cmd?: Cmd);
|
||||
name(name: string): Opt;
|
||||
title(title: string): Opt;
|
||||
short(short: string): Opt;
|
||||
long(long: string): Opt;
|
||||
flag(): Opt;
|
||||
arr(): Opt;
|
||||
req(): Opt;
|
||||
only(): Opt;
|
||||
val(validation: (this: Opt, value: any) => boolean): Opt;
|
||||
def(def: any): Opt;
|
||||
input(): Opt;
|
||||
output(): Opt;
|
||||
act(act: (opts: any, args: any[], res: any) => any): Opt;
|
||||
comp(fn: (opts: any) => any): Opt;
|
||||
end(): Cmd;
|
||||
apply(...args: any[]): void;
|
||||
reject(...args: any[]): void;
|
||||
}
|
||||
}
|
||||
|
||||
export namespace shell {
|
||||
function escape(w: string): string;
|
||||
function unescape(w: string): string;
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
module.exports = require('./lib');
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
'use strict';
|
||||
|
||||
const
|
||||
CoaParam = require('./coaparam'),
|
||||
chalk = require('chalk');
|
||||
|
||||
/**
|
||||
* Argument
|
||||
*
|
||||
* Unnamed entity. From command line arguments passed as list of unnamed values.
|
||||
*
|
||||
* @class Arg
|
||||
* @extends CoaParam
|
||||
*/
|
||||
module.exports = class Arg extends CoaParam {
|
||||
/**
|
||||
* @constructs
|
||||
* @param {COA.Cmd} cmd - parent command
|
||||
*/
|
||||
constructor(cmd) {
|
||||
super(cmd);
|
||||
|
||||
this._cmd._args.push(this);
|
||||
}
|
||||
|
||||
_saveVal(args, val) {
|
||||
this._val && (val = this._val(val));
|
||||
|
||||
const name = this._name;
|
||||
this._arr
|
||||
? (args[name] || (args[name] = [])).push(val)
|
||||
: (args[name] = val);
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
_parse(arg, args) {
|
||||
return this._saveVal(args, arg);
|
||||
}
|
||||
|
||||
_checkParsed(opts, args) {
|
||||
return !args.hasOwnProperty(this._name);
|
||||
}
|
||||
|
||||
_usage() {
|
||||
const res = [];
|
||||
|
||||
res.push(chalk.magentaBright(this._name.toUpperCase()), ' : ', this._title);
|
||||
|
||||
this._req && res.push(' ', chalk.redBright('(required)'));
|
||||
|
||||
return res.join('');
|
||||
}
|
||||
|
||||
_requiredText() {
|
||||
return `Missing required argument:\n ${this._usage()}`;
|
||||
}
|
||||
};
|
||||
+493
@@ -0,0 +1,493 @@
|
||||
/* eslint-disable class-methods-use-this */
|
||||
'use strict';
|
||||
|
||||
const
|
||||
UTIL = require('util'),
|
||||
PATH = require('path'),
|
||||
EOL = require('os').EOL,
|
||||
|
||||
Q = require('q'),
|
||||
chalk = require('chalk'),
|
||||
|
||||
CoaObject = require('./coaobject'),
|
||||
Opt = require('./opt'),
|
||||
Arg = require('./arg'),
|
||||
completion = require('./completion');
|
||||
|
||||
/**
|
||||
* Command
|
||||
*
|
||||
* Top level entity. Commands may have options and arguments.
|
||||
*
|
||||
* @namespace
|
||||
* @class Cmd
|
||||
* @extends CoaObject
|
||||
*/
|
||||
class Cmd extends CoaObject {
|
||||
/**
|
||||
* @constructs
|
||||
* @param {COA.Cmd} [cmd] parent command
|
||||
*/
|
||||
constructor(cmd) {
|
||||
super(cmd);
|
||||
|
||||
this._parent(cmd);
|
||||
this._cmds = [];
|
||||
this._cmdsByName = {};
|
||||
this._opts = [];
|
||||
this._optsByKey = {};
|
||||
this._args = [];
|
||||
this._api = null;
|
||||
this._ext = false;
|
||||
}
|
||||
|
||||
static create(cmd) {
|
||||
return new Cmd(cmd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns object containing all its subcommands as methods
|
||||
* to use from other programs.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
get api() {
|
||||
// Need _this here because of passed arguments into _api
|
||||
const _this = this;
|
||||
this._api || (this._api = function () {
|
||||
return _this.invoke.apply(_this, arguments);
|
||||
});
|
||||
|
||||
const cmds = this._cmdsByName;
|
||||
Object.keys(cmds).forEach(cmd => { this._api[cmd] = cmds[cmd].api; });
|
||||
|
||||
return this._api;
|
||||
}
|
||||
|
||||
_parent(cmd) {
|
||||
this._cmd = cmd || this;
|
||||
|
||||
this.isRootCmd ||
|
||||
cmd._cmds.push(this) &&
|
||||
this._name &&
|
||||
(this._cmd._cmdsByName[this._name] = this);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
get isRootCmd() {
|
||||
return this._cmd === this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a canonical command identifier to be used anywhere in the API.
|
||||
*
|
||||
* @param {String} name - command name
|
||||
* @returns {COA.Cmd} - this instance (for chainability)
|
||||
*/
|
||||
name(name) {
|
||||
super.name(name);
|
||||
|
||||
this.isRootCmd ||
|
||||
(this._cmd._cmdsByName[name] = this);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new or add existing subcommand for current command.
|
||||
*
|
||||
* @param {COA.Cmd} [cmd] existing command instance
|
||||
* @returns {COA.Cmd} new subcommand instance
|
||||
*/
|
||||
cmd(cmd) {
|
||||
return cmd?
|
||||
cmd._parent(this)
|
||||
: new Cmd(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create option for current command.
|
||||
*
|
||||
* @returns {COA.Opt} new option instance
|
||||
*/
|
||||
opt() {
|
||||
return new Opt(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create argument for current command.
|
||||
*
|
||||
* @returns {COA.Opt} new argument instance
|
||||
*/
|
||||
arg() {
|
||||
return new Arg(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add (or set) action for current command.
|
||||
*
|
||||
* @param {Function} act - action function,
|
||||
* invoked in the context of command instance
|
||||
* and has the parameters:
|
||||
* - {Object} opts - parsed options
|
||||
* - {String[]} args - parsed arguments
|
||||
* - {Object} res - actions result accumulator
|
||||
* It can return rejected promise by Cmd.reject (in case of error)
|
||||
* or any other value treated as result.
|
||||
* @param {Boolean} [force=false] flag for set action instead add to existings
|
||||
* @returns {COA.Cmd} - this instance (for chainability)
|
||||
*/
|
||||
act(act, force) {
|
||||
if(!act) return this;
|
||||
|
||||
(!this._act || force) && (this._act = []);
|
||||
this._act.push(act);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make command "helpful", i.e. add -h --help flags for print usage.
|
||||
*
|
||||
* @returns {COA.Cmd} - this instance (for chainability)
|
||||
*/
|
||||
helpful() {
|
||||
return this.opt()
|
||||
.name('help')
|
||||
.title('Help')
|
||||
.short('h')
|
||||
.long('help')
|
||||
.flag()
|
||||
.only()
|
||||
.act(function() {
|
||||
return this.usage();
|
||||
})
|
||||
.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds shell completion to command, adds "completion" subcommand,
|
||||
* that makes all the magic.
|
||||
* Must be called only on root command.
|
||||
*
|
||||
* @returns {COA.Cmd} - this instance (for chainability)
|
||||
*/
|
||||
completable() {
|
||||
return this.cmd()
|
||||
.name('completion')
|
||||
.apply(completion)
|
||||
.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow command to be extendable by external node.js modules.
|
||||
*
|
||||
* @param {String} [pattern] Pattern of node.js module to find subcommands at.
|
||||
* @returns {COA.Cmd} - this instance (for chainability)
|
||||
*/
|
||||
extendable(pattern) {
|
||||
this._ext = pattern || true;
|
||||
return this;
|
||||
}
|
||||
|
||||
_exit(msg, code) {
|
||||
return process.once('exit', function(exitCode) {
|
||||
msg && console[code === 0 ? 'log' : 'error'](msg);
|
||||
process.exit(code || exitCode || 0);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Build full usage text for current command instance.
|
||||
*
|
||||
* @returns {String} usage text
|
||||
*/
|
||||
usage() {
|
||||
const res = [];
|
||||
|
||||
this._title && res.push(this._fullTitle());
|
||||
|
||||
res.push('', 'Usage:');
|
||||
|
||||
this._cmds.length
|
||||
&& res.push([
|
||||
'', '', chalk.redBright(this._fullName()), chalk.blueBright('COMMAND'),
|
||||
chalk.greenBright('[OPTIONS]'), chalk.magentaBright('[ARGS]')
|
||||
].join(' '));
|
||||
|
||||
(this._opts.length + this._args.length)
|
||||
&& res.push([
|
||||
'', '', chalk.redBright(this._fullName()),
|
||||
chalk.greenBright('[OPTIONS]'), chalk.magentaBright('[ARGS]')
|
||||
].join(' '));
|
||||
|
||||
res.push(
|
||||
this._usages(this._cmds, 'Commands'),
|
||||
this._usages(this._opts, 'Options'),
|
||||
this._usages(this._args, 'Arguments')
|
||||
);
|
||||
|
||||
return res.join(EOL);
|
||||
}
|
||||
|
||||
_usage() {
|
||||
return chalk.blueBright(this._name) + ' : ' + this._title;
|
||||
}
|
||||
|
||||
_usages(os, title) {
|
||||
if(!os.length) return;
|
||||
|
||||
return ['', title + ':']
|
||||
.concat(os.map(o => ` ${o._usage()}`))
|
||||
.join(EOL);
|
||||
}
|
||||
|
||||
_fullTitle() {
|
||||
return `${this.isRootCmd? '' : this._cmd._fullTitle() + EOL}${this._title}`;
|
||||
}
|
||||
|
||||
_fullName() {
|
||||
return `${this.isRootCmd? '' : this._cmd._fullName() + ' '}${PATH.basename(this._name)}`;
|
||||
}
|
||||
|
||||
_ejectOpt(opts, opt) {
|
||||
const pos = opts.indexOf(opt);
|
||||
if(pos === -1) return;
|
||||
|
||||
return opts[pos]._arr?
|
||||
opts[pos] :
|
||||
opts.splice(pos, 1)[0];
|
||||
}
|
||||
|
||||
_checkRequired(opts, args) {
|
||||
if(this._opts.some(opt => opt._only && opts.hasOwnProperty(opt._name))) return;
|
||||
|
||||
const all = this._opts.concat(this._args);
|
||||
let i;
|
||||
while(i = all.shift())
|
||||
if(i._req && i._checkParsed(opts, args))
|
||||
return this.reject(i._requiredText());
|
||||
}
|
||||
|
||||
_parseCmd(argv, unparsed) {
|
||||
unparsed || (unparsed = []);
|
||||
|
||||
let i,
|
||||
optSeen = false;
|
||||
while(i = argv.shift()) {
|
||||
i.indexOf('-') || (optSeen = true);
|
||||
|
||||
if(optSeen || !/^\w[\w-_]*$/.test(i)) {
|
||||
unparsed.push(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
let pkg, cmd = this._cmdsByName[i];
|
||||
if(!cmd && this._ext) {
|
||||
if(this._ext === true) {
|
||||
pkg = i;
|
||||
let c = this;
|
||||
while(true) { // eslint-disable-line
|
||||
pkg = c._name + '-' + pkg;
|
||||
if(c.isRootCmd) break;
|
||||
c = c._cmd;
|
||||
}
|
||||
} else if(typeof this._ext === 'string')
|
||||
pkg = ~this._ext.indexOf('%s')?
|
||||
UTIL.format(this._ext, i) :
|
||||
this._ext + i;
|
||||
|
||||
let cmdDesc;
|
||||
try {
|
||||
cmdDesc = require(pkg);
|
||||
} catch(e) {
|
||||
// Dummy
|
||||
}
|
||||
|
||||
if(cmdDesc) {
|
||||
if(typeof cmdDesc === 'function') {
|
||||
this.cmd().name(i).apply(cmdDesc).end();
|
||||
} else if(typeof cmdDesc === 'object') {
|
||||
this.cmd(cmdDesc);
|
||||
cmdDesc.name(i);
|
||||
} else throw new Error('Error: Unsupported command declaration type, '
|
||||
+ 'should be a function or COA.Cmd() object');
|
||||
|
||||
cmd = this._cmdsByName[i];
|
||||
}
|
||||
}
|
||||
|
||||
if(cmd) return cmd._parseCmd(argv, unparsed);
|
||||
|
||||
unparsed.push(i);
|
||||
}
|
||||
|
||||
return { cmd : this, argv : unparsed };
|
||||
}
|
||||
|
||||
_parseOptsAndArgs(argv) {
|
||||
const opts = {},
|
||||
args = {},
|
||||
nonParsedOpts = this._opts.concat(),
|
||||
nonParsedArgs = this._args.concat();
|
||||
|
||||
let res, i;
|
||||
while(i = argv.shift()) {
|
||||
if(i !== '--' && i[0] === '-') {
|
||||
const m = i.match(/^(--\w[\w-_]*)=(.*)$/);
|
||||
if(m) {
|
||||
i = m[1];
|
||||
this._optsByKey[i]._flag || argv.unshift(m[2]);
|
||||
}
|
||||
|
||||
const opt = this._ejectOpt(nonParsedOpts, this._optsByKey[i]);
|
||||
if(!opt) return this.reject(`Unknown option: ${i}`);
|
||||
|
||||
if(Q.isRejected(res = opt._parse(argv, opts))) return res;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
i === '--' && (i = argv.splice(0));
|
||||
Array.isArray(i) || (i = [i]);
|
||||
|
||||
let a;
|
||||
while(a = i.shift()) {
|
||||
let arg = nonParsedArgs.shift();
|
||||
if(!arg) return this.reject(`Unknown argument: ${a}`);
|
||||
|
||||
arg._arr && nonParsedArgs.unshift(arg);
|
||||
if(Q.isRejected(res = arg._parse(a, args))) return res;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
opts : this._setDefaults(opts, nonParsedOpts),
|
||||
args : this._setDefaults(args, nonParsedArgs)
|
||||
};
|
||||
}
|
||||
|
||||
_setDefaults(params, desc) {
|
||||
for(const item of desc)
|
||||
item._def !== undefined &&
|
||||
!params.hasOwnProperty(item._name) &&
|
||||
item._saveVal(params, item._def);
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
_processParams(params, desc) {
|
||||
const notExists = [];
|
||||
|
||||
for(const item of desc) {
|
||||
const n = item._name;
|
||||
|
||||
if(!params.hasOwnProperty(n)) {
|
||||
notExists.push(item);
|
||||
continue;
|
||||
}
|
||||
|
||||
const vals = Array.isArray(params[n])? params[n] : [params[n]];
|
||||
delete params[n];
|
||||
|
||||
let res;
|
||||
for(const v of vals)
|
||||
if(Q.isRejected(res = item._saveVal(params, v)))
|
||||
return res;
|
||||
}
|
||||
|
||||
return this._setDefaults(params, notExists);
|
||||
}
|
||||
|
||||
_parseArr(argv) {
|
||||
return Q.when(this._parseCmd(argv), p =>
|
||||
Q.when(p.cmd._parseOptsAndArgs(p.argv), r => ({
|
||||
cmd : p.cmd,
|
||||
opts : r.opts,
|
||||
args : r.args
|
||||
})));
|
||||
}
|
||||
|
||||
_do(inputPromise) {
|
||||
return Q.when(inputPromise, input => {
|
||||
return [this._checkRequired]
|
||||
.concat(input.cmd._act || [])
|
||||
.reduce((res, act) =>
|
||||
Q.when(res, prev => act.call(input.cmd, input.opts, input.args, prev)),
|
||||
undefined);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse arguments from simple format like NodeJS process.argv
|
||||
* and run ahead current program, i.e. call process.exit when all actions done.
|
||||
*
|
||||
* @param {String[]} argv - arguments
|
||||
* @returns {COA.Cmd} - this instance (for chainability)
|
||||
*/
|
||||
run(argv) {
|
||||
argv || (argv = process.argv.slice(2));
|
||||
|
||||
const cb = code =>
|
||||
res => res?
|
||||
this._exit(res.stack || res.toString(), (res.hasOwnProperty('exitCode')? res.exitCode : code) || 0) :
|
||||
this._exit();
|
||||
|
||||
Q.when(this.do(argv), cb(0), cb(1)).done();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke specified (or current) command using provided
|
||||
* options and arguments.
|
||||
*
|
||||
* @param {String|String[]} [cmds] - subcommand to invoke (optional)
|
||||
* @param {Object} [opts] - command options (optional)
|
||||
* @param {Object} [args] - command arguments (optional)
|
||||
* @returns {Q.Promise}
|
||||
*/
|
||||
invoke(cmds, opts, args) {
|
||||
cmds || (cmds = []);
|
||||
opts || (opts = {});
|
||||
args || (args = {});
|
||||
typeof cmds === 'string' && (cmds = cmds.split(' '));
|
||||
|
||||
if(arguments.length < 3 && !Array.isArray(cmds)) {
|
||||
args = opts;
|
||||
opts = cmds;
|
||||
cmds = [];
|
||||
}
|
||||
|
||||
return Q.when(this._parseCmd(cmds), p => {
|
||||
if(p.argv.length)
|
||||
return this.reject(`Unknown command: ${cmds.join(' ')}`);
|
||||
|
||||
return Q.all([
|
||||
this._processParams(opts, this._opts),
|
||||
this._processParams(args, this._args)
|
||||
]).spread((_opts, _args) =>
|
||||
this._do({
|
||||
cmd : p.cmd,
|
||||
opts : _opts,
|
||||
args : _args
|
||||
})
|
||||
.fail(res => (res && res.exitCode === 0)?
|
||||
res.toString() :
|
||||
this.reject(res)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenient function to run command from tests.
|
||||
*
|
||||
* @param {String[]} argv - arguments
|
||||
* @returns {Q.Promise}
|
||||
*/
|
||||
Cmd.prototype.do = function(argv) {
|
||||
return this._do(this._parseArr(argv || []));
|
||||
};
|
||||
|
||||
module.exports = Cmd;
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
/* eslint-disable class-methods-use-this */
|
||||
'use strict';
|
||||
|
||||
const Q = require('q');
|
||||
|
||||
/**
|
||||
* COA Object
|
||||
*
|
||||
* Base class for all COA-related objects
|
||||
*
|
||||
* --------|-----|-----|-----
|
||||
* | Cmd | Opt | Arg
|
||||
* --------|-----|-----|-----
|
||||
* name | ✓ | ✓ | ✓
|
||||
* title | ✓ | ✓ | ✓
|
||||
* comp | ✓ | ✓ | ✓
|
||||
* reject | ✓ | ✓ | ✓
|
||||
* end | ✓ | ✓ | ✓
|
||||
* apply | ✓ | ✓ | ✓
|
||||
*
|
||||
* @class CoaObject
|
||||
*/
|
||||
module.exports = class CoaObject {
|
||||
constructor(cmd) {
|
||||
this._cmd = cmd;
|
||||
this._name = null;
|
||||
this._title = null;
|
||||
this._comp = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a canonical identifier to be used anywhere in the API.
|
||||
*
|
||||
* @param {String} name - command, option or argument name
|
||||
* @returns {COA.CoaObject} - this instance (for chainability)
|
||||
*/
|
||||
name(name) {
|
||||
this._name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a long description to be used anywhere in text messages.
|
||||
* @param {String} title - human readable entity title
|
||||
* @returns {COA.CoaObject} - this instance (for chainability)
|
||||
*/
|
||||
title(title) {
|
||||
this._title = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set custom additional completion for current object.
|
||||
*
|
||||
* @param {Function} comp - completion generation function,
|
||||
* invoked in the context of object instance.
|
||||
* Accepts parameters:
|
||||
* - {Object} opts - completion options
|
||||
* It can return promise or any other value threated as a result.
|
||||
* @returns {COA.CoaObject} - this instance (for chainability)
|
||||
*/
|
||||
comp(comp) {
|
||||
this._comp = comp;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply function with arguments in a context of object instance.
|
||||
*
|
||||
* @param {Function} fn - body
|
||||
* @param {Array.<*>} args... - arguments
|
||||
* @returns {COA.CoaObject} - this instance (for chainability)
|
||||
*/
|
||||
apply(fn) {
|
||||
arguments.length > 1?
|
||||
fn.apply(this, [].slice.call(arguments, 1))
|
||||
: fn.call(this);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return reject of actions results promise with error code.
|
||||
* Use in .act() for return with error.
|
||||
* @param {Object} reason - reject reason
|
||||
* You can customize toString() method and exitCode property
|
||||
* of reason object.
|
||||
* @returns {Q.promise} rejected promise
|
||||
*/
|
||||
reject(reason) {
|
||||
return Q.reject(reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish chain for current subcommand and return parent command instance.
|
||||
* @returns {COA.Cmd} parent command
|
||||
*/
|
||||
end() {
|
||||
return this._cmd;
|
||||
}
|
||||
};
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
const CoaObject = require('./coaobject');
|
||||
|
||||
/**
|
||||
* COA Parameter
|
||||
*
|
||||
* Base class for options and arguments
|
||||
*
|
||||
* --------|-----|-----|-----
|
||||
* | Cmd | Opt | Arg
|
||||
* --------|-----|-----|-----
|
||||
* arr | | ✓ | ✓
|
||||
* req | | ✓ | ✓
|
||||
* val | | ✓ | ✓
|
||||
* def | | ✓ | ✓
|
||||
* input | | ✓ | ✓
|
||||
* output | | ✓ | ✓
|
||||
*
|
||||
* @class CoaParam
|
||||
* @extends CoaObject
|
||||
*/
|
||||
module.exports = class CoaParam extends CoaObject {
|
||||
constructor(cmd) {
|
||||
super(cmd);
|
||||
|
||||
this._arr = false;
|
||||
this._req = false;
|
||||
this._val = undefined;
|
||||
this._def = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a param accepts multiple values.
|
||||
* Otherwise, the value will be used by the latter passed.
|
||||
*
|
||||
* @returns {COA.CoaParam} - this instance (for chainability)
|
||||
*/
|
||||
arr() {
|
||||
this._arr = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a param required.
|
||||
*
|
||||
* @returns {COA.CoaParam} - this instance (for chainability)
|
||||
*/
|
||||
req() {
|
||||
this._req = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a validation (or value) function for param.
|
||||
* Value from command line passes through before becoming available from API.
|
||||
* Using for validation and convertion simple types to any values.
|
||||
*
|
||||
* @param {Function} val - validating function,
|
||||
* invoked in the context of option instance
|
||||
* and has one parameter with value from command line.
|
||||
* @returns {COA.CoaParam} - this instance (for chainability)
|
||||
*/
|
||||
val(val) {
|
||||
this._val = val;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a default value for param.
|
||||
* Default value passed through validation function as ordinary value.
|
||||
*
|
||||
* @param {*} def - default value of function generator
|
||||
* @returns {COA.CoaParam} - this instance (for chainability)
|
||||
*/
|
||||
def(def) {
|
||||
this._def = def;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make option value inputting stream.
|
||||
* It's add useful validation and shortcut for STDIN.
|
||||
*
|
||||
* @returns {COA.CoaParam} - this instance (for chainability)
|
||||
*/
|
||||
input() {
|
||||
process.stdin.pause();
|
||||
return this
|
||||
.def(process.stdin)
|
||||
.val(function(v) {
|
||||
if(typeof v !== 'string')
|
||||
return v;
|
||||
|
||||
if(v === '-')
|
||||
return process.stdin;
|
||||
|
||||
const s = fs.createReadStream(v, { encoding : 'utf8' });
|
||||
s.pause();
|
||||
return s;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Make option value outputing stream.
|
||||
* It's add useful validation and shortcut for STDOUT.
|
||||
*
|
||||
* @returns {COA.CoaParam} - this instance (for chainability)
|
||||
*/
|
||||
output() {
|
||||
return this
|
||||
.def(process.stdout)
|
||||
.val(function(v) {
|
||||
if(typeof v !== 'string')
|
||||
return v;
|
||||
|
||||
if(v === '-')
|
||||
return process.stdout;
|
||||
|
||||
return fs.createWriteStream(v, { encoding : 'utf8' });
|
||||
});
|
||||
}
|
||||
};
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
'use strict';
|
||||
|
||||
const constants = require('constants');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const Q = require('q');
|
||||
|
||||
const shell = require('./shell');
|
||||
const escape = shell.escape;
|
||||
const unescape = shell.unescape;
|
||||
|
||||
/**
|
||||
* Most of the code adopted from the npm package shell completion code.
|
||||
* See https://github.com/isaacs/npm/blob/master/lib/completion.js
|
||||
*
|
||||
* @returns {COA.CoaObject}
|
||||
*/
|
||||
module.exports = function completion() {
|
||||
return this
|
||||
.title('Shell completion')
|
||||
.helpful()
|
||||
.arg()
|
||||
.name('raw')
|
||||
.title('Completion words')
|
||||
.arr()
|
||||
.end()
|
||||
.act((opts, args) => {
|
||||
if(process.platform === 'win32') {
|
||||
const e = new Error('shell completion not supported on windows');
|
||||
e.code = 'ENOTSUP';
|
||||
e.errno = constants.ENOTSUP;
|
||||
return this.reject(e);
|
||||
}
|
||||
|
||||
// if the COMP_* isn't in the env, then just dump the script
|
||||
if((process.env.COMP_CWORD == null)
|
||||
|| (process.env.COMP_LINE == null)
|
||||
|| (process.env.COMP_POINT == null)) {
|
||||
return dumpScript(this._cmd._name);
|
||||
}
|
||||
|
||||
console.error('COMP_LINE: %s', process.env.COMP_LINE);
|
||||
console.error('COMP_CWORD: %s', process.env.COMP_CWORD);
|
||||
console.error('COMP_POINT: %s', process.env.COMP_POINT);
|
||||
console.error('args: %j', args.raw);
|
||||
|
||||
// completion opts
|
||||
opts = getOpts(args.raw);
|
||||
|
||||
// cmd
|
||||
const parsed = this._cmd._parseCmd(opts.partialWords);
|
||||
return Q.when(complete(parsed.cmd, parsed.opts), compls => {
|
||||
console.error('filtered: %j', compls);
|
||||
return console.log(compls.map(escape).join('\n'));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function dumpScript(name) {
|
||||
const defer = Q.defer();
|
||||
|
||||
fs.readFile(path.resolve(__dirname, 'completion.sh'), 'utf8', function(err, d) {
|
||||
if(err) return defer.reject(err);
|
||||
d = d.replace(/{{cmd}}/g, path.basename(name)).replace(/^#!.*?\n/, '');
|
||||
|
||||
process.stdout.on('error', onError);
|
||||
process.stdout.write(d, () => defer.resolve());
|
||||
});
|
||||
|
||||
return defer.promise;
|
||||
|
||||
function onError(err) {
|
||||
// Darwin is a real dick sometimes.
|
||||
//
|
||||
// This is necessary because the "source" or "." program in
|
||||
// bash on OS X closes its file argument before reading
|
||||
// from it, meaning that you get exactly 1 write, which will
|
||||
// work most of the time, and will always raise an EPIPE.
|
||||
//
|
||||
// Really, one should not be tossing away EPIPE errors, or any
|
||||
// errors, so casually. But, without this, `. <(cmd completion)`
|
||||
// can never ever work on OS X.
|
||||
if(err.errno !== constants.EPIPE) return defer.reject(err);
|
||||
process.stdout.removeListener('error', onError);
|
||||
return defer.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
function getOpts(argv) {
|
||||
// get the partial line and partial word, if the point isn't at the end
|
||||
// ie, tabbing at: cmd foo b|ar
|
||||
const line = process.env.COMP_LINE;
|
||||
const w = +process.env.COMP_CWORD;
|
||||
const point = +process.env.COMP_POINT;
|
||||
const words = argv.map(unescape);
|
||||
const word = words[w];
|
||||
const partialLine = line.substr(0, point);
|
||||
const partialWords = words.slice(0, w);
|
||||
|
||||
// figure out where in that last word the point is
|
||||
let partialWord = argv[w] || '';
|
||||
let i = partialWord.length;
|
||||
while(partialWord.substr(0, i) !== partialLine.substr(-1 * i) && i > 0) i--;
|
||||
|
||||
partialWord = unescape(partialWord.substr(0, i));
|
||||
partialWord && partialWords.push(partialWord);
|
||||
|
||||
return {
|
||||
line,
|
||||
w,
|
||||
point,
|
||||
words,
|
||||
word,
|
||||
partialLine,
|
||||
partialWords,
|
||||
partialWord
|
||||
};
|
||||
}
|
||||
|
||||
function complete(cmd, opts) {
|
||||
let optWord, optPrefix,
|
||||
compls = [];
|
||||
|
||||
// Complete on cmds
|
||||
if(opts.partialWord.indexOf('-'))
|
||||
compls = Object.keys(cmd._cmdsByName);
|
||||
// Complete on required opts without '-' in last partial word
|
||||
// (if required not already specified)
|
||||
//
|
||||
// Commented out because of uselessness:
|
||||
// -b, --block suggest results in '-' on cmd line;
|
||||
// next completion suggest all options, because of '-'
|
||||
//.concat Object.keys(cmd._optsByKey).filter (v) -> cmd._optsByKey[v]._req
|
||||
else {
|
||||
// complete on opt values: --opt=| case
|
||||
const m = opts.partialWord.match(/^(--\w[\w-_]*)=(.*)$/);
|
||||
if(m) {
|
||||
optWord = m[1];
|
||||
optPrefix = optWord + '=';
|
||||
} else
|
||||
// complete on opts
|
||||
// don't complete on opts in case of --opt=val completion
|
||||
// TODO: don't complete on opts in case of unknown arg after commands
|
||||
// TODO: complete only on opts with arr() or not already used
|
||||
// TODO: complete only on full opts?
|
||||
compls = Object.keys(cmd._optsByKey);
|
||||
}
|
||||
|
||||
// complete on opt values: next arg case
|
||||
opts.partialWords[opts.w - 1].indexOf('-') || (optWord = opts.partialWords[opts.w - 1]);
|
||||
|
||||
// complete on opt values: completion
|
||||
let opt;
|
||||
optWord
|
||||
&& (opt = cmd._optsByKey[optWord])
|
||||
&& !opt._flag
|
||||
&& opt._comp
|
||||
&& (compls = Q.join(compls,
|
||||
Q.when(opt._comp(opts),
|
||||
(c, o) => c.concat(o.map(v => (optPrefix || '') + v)))));
|
||||
|
||||
// TODO: complete on args values (context aware, custom completion?)
|
||||
|
||||
// custom completion on cmds
|
||||
cmd._comp && (compls = Q.join(compls, Q.when(cmd._comp(opts)), (c, o) => c.concat(o)));
|
||||
|
||||
// TODO: context aware custom completion on cmds, opts and args
|
||||
// (can depend on already entered values, especially options)
|
||||
|
||||
return Q.when(compls, complitions => {
|
||||
console.error('partialWord: %s', opts.partialWord);
|
||||
console.error('compls: %j', complitions);
|
||||
return compls.filter(c => c.indexOf(opts.partialWord) === 0);
|
||||
});
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env bash
|
||||
###-begin-{{cmd}}-completion-###
|
||||
#
|
||||
# {{cmd}} command completion script
|
||||
#
|
||||
# Installation: {{cmd}} completion >> ~/.bashrc (or ~/.zshrc)
|
||||
# Or, maybe: {{cmd}} completion > /usr/local/etc/bash_completion.d/{{cmd}}
|
||||
#
|
||||
|
||||
COMP_WORDBREAKS=${COMP_WORDBREAKS/=/}
|
||||
COMP_WORDBREAKS=${COMP_WORDBREAKS/@/}
|
||||
export COMP_WORDBREAKS
|
||||
|
||||
if complete &>/dev/null; then
|
||||
_{{cmd}}_completion () {
|
||||
local si="$IFS"
|
||||
IFS=$'\n' COMPREPLY=($(COMP_CWORD="$COMP_CWORD" \
|
||||
COMP_LINE="$COMP_LINE" \
|
||||
COMP_POINT="$COMP_POINT" \
|
||||
{{cmd}} completion -- "${COMP_WORDS[@]}" \
|
||||
2>/dev/null)) || return $?
|
||||
IFS="$si"
|
||||
}
|
||||
complete -F _{{cmd}}_completion {{cmd}}
|
||||
elif compctl &>/dev/null; then
|
||||
_{{cmd}}_completion () {
|
||||
local cword line point words si
|
||||
read -Ac words
|
||||
read -cn cword
|
||||
let cword-=1
|
||||
read -l line
|
||||
read -ln point
|
||||
si="$IFS"
|
||||
IFS=$'\n' reply=($(COMP_CWORD="$cword" \
|
||||
COMP_LINE="$line" \
|
||||
COMP_POINT="$point" \
|
||||
{{cmd}} completion -- "${words[@]}" \
|
||||
2>/dev/null)) || return $?
|
||||
IFS="$si"
|
||||
}
|
||||
compctl -K _{{cmd}}_completion {{cmd}}
|
||||
fi
|
||||
###-end-{{cmd}}-completion-###
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
const
|
||||
Cmd = require('./cmd'),
|
||||
Opt = require('./opt'),
|
||||
Arg = require('./arg'),
|
||||
shell = require('./shell');
|
||||
|
||||
module.exports = {
|
||||
Cmd : Cmd.create,
|
||||
Opt : Opt.create,
|
||||
Arg : Arg.create,
|
||||
classes : { Cmd, Opt, Arg },
|
||||
shell,
|
||||
require
|
||||
};
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
'use strict';
|
||||
|
||||
const
|
||||
Q = require('q'),
|
||||
|
||||
CoaParam = require('./coaparam'),
|
||||
chalk = require('chalk');
|
||||
|
||||
/**
|
||||
* Option
|
||||
*
|
||||
* Named entity. Options may have short and long keys for use from command line.
|
||||
*
|
||||
* @namespace
|
||||
* @class Opt
|
||||
* @extends CoaParam
|
||||
*/
|
||||
module.exports = class Opt extends CoaParam {
|
||||
/**
|
||||
* @constructs
|
||||
* @param {COA.Cmd} cmd - parent command
|
||||
*/
|
||||
constructor(cmd) {
|
||||
super(cmd);
|
||||
|
||||
this._short = null;
|
||||
this._long = null;
|
||||
this._flag = false;
|
||||
this._only = false;
|
||||
this._cmd._opts.push(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a short key for option to be used with one hyphen from command line.
|
||||
*
|
||||
* @param {String} short - short name
|
||||
* @returns {COA.Opt} - this instance (for chainability)
|
||||
*/
|
||||
short(short) {
|
||||
this._short = short;
|
||||
this._cmd._optsByKey[`-${short}`] = this;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a short key for option to be used with double hyphens from command line.
|
||||
*
|
||||
* @param {String} long - long name
|
||||
* @returns {COA.Opt} - this instance (for chainability)
|
||||
*/
|
||||
long(long) {
|
||||
this._long = long;
|
||||
this._cmd._optsByKey[`--${long}`] = this;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an option boolean, i.e. option without value.
|
||||
*
|
||||
* @returns {COA.Opt} - this instance (for chainability)
|
||||
*/
|
||||
flag() {
|
||||
this._flag = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes an option to act as a command,
|
||||
* i.e. program will exit just after option action.
|
||||
*
|
||||
* @returns {COA.Opt} - this instance (for chainability)
|
||||
*/
|
||||
only() {
|
||||
this._only = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add action for current option command.
|
||||
* This action is performed if the current option
|
||||
* is present in parsed options (with any value).
|
||||
*
|
||||
* @param {Function} act - action function,
|
||||
* invoked in the context of command instance
|
||||
* and has the parameters:
|
||||
* - {Object} opts - parsed options
|
||||
* - {Array} args - parsed arguments
|
||||
* - {Object} res - actions result accumulator
|
||||
* It can return rejected promise by Cmd.reject (in case of error)
|
||||
* or any other value treated as result.
|
||||
* @returns {COA.Opt} - this instance (for chainability)
|
||||
*/
|
||||
act(act) {
|
||||
// Need function here for arguments
|
||||
const opt = this;
|
||||
this._cmd.act(function(opts) {
|
||||
if(!opts.hasOwnProperty(opt._name)) return;
|
||||
|
||||
const res = act.apply(this, arguments);
|
||||
if(!opt._only) return res;
|
||||
|
||||
return Q.when(res, out => this.reject({
|
||||
toString : () => out.toString(),
|
||||
exitCode : 0
|
||||
}));
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
_saveVal(opts, val) {
|
||||
this._val && (val = this._val(val));
|
||||
|
||||
const name = this._name;
|
||||
this._arr
|
||||
? (opts[name] || (opts[name] = [])).push(val)
|
||||
: (opts[name] = val);
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
_parse(argv, opts) {
|
||||
return this._saveVal(opts, this._flag ? true : argv.shift());
|
||||
}
|
||||
|
||||
_checkParsed(opts) {
|
||||
return !opts.hasOwnProperty(this._name);
|
||||
}
|
||||
|
||||
_usage() {
|
||||
const res = [],
|
||||
nameStr = this._name.toUpperCase();
|
||||
|
||||
if(this._short) {
|
||||
res.push('-', chalk.greenBright(this._short));
|
||||
this._flag || res.push(' ' + nameStr);
|
||||
res.push(', ');
|
||||
}
|
||||
|
||||
if(this._long) {
|
||||
res.push('--', chalk.green(this._long));
|
||||
this._flag || res.push('=' + nameStr);
|
||||
}
|
||||
|
||||
res.push(' : ', this._title);
|
||||
|
||||
this._req && res.push(' ', chalk.redBright('(required)'));
|
||||
|
||||
return res.join('');
|
||||
}
|
||||
|
||||
_requiredText() {
|
||||
return `Missing required option:\n ${this._usage()}`;
|
||||
}
|
||||
};
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
module.exports = { escape, unescape };
|
||||
|
||||
function unescape(w) {
|
||||
w = w.charAt(0) === '"'
|
||||
? w.replace(/^"|([^\\])"$/g, '$1')
|
||||
: w.replace(/\\ /g, ' ');
|
||||
|
||||
return w.replace(/\\("|'|\$|`|\\)/g, '$1');
|
||||
}
|
||||
|
||||
function escape(w) {
|
||||
w = w.replace(/(["'$`\\])/g,'\\$1');
|
||||
return w.match(/\s+/) ? `"${w}"` : w;
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "coa",
|
||||
"description": "Command-Option-Argument: Yet another parser for command line options.",
|
||||
"version": "2.0.2",
|
||||
"homepage": "http://github.com/veged/coa",
|
||||
"author": "Sergey Berezhnoy <veged@ya.ru> (http://github.com/veged)",
|
||||
"maintainers": [
|
||||
"Sergey Berezhnoy <veged@ya.ru> (http://github.com/veged)",
|
||||
"Sergey Belov <peimei@ya.ru> (http://github.com/arikon)"
|
||||
],
|
||||
"contributors": [
|
||||
"Sergey Belov <peimei@ya.ru> (http://github.com/arikon)"
|
||||
],
|
||||
"files": [
|
||||
"lib/",
|
||||
"index.js",
|
||||
"coa.d.ts",
|
||||
"README.ru.md"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/veged/coa.git"
|
||||
},
|
||||
"directories": {
|
||||
"lib": "./lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/q": "^1.5.1",
|
||||
"chalk": "^2.4.1",
|
||||
"q": "^1.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "~1.7.2",
|
||||
"coveralls": "^2.11.16",
|
||||
"eslint": "^4.15.0",
|
||||
"eslint-config-pedant": "^1.0.0",
|
||||
"mocha": "~1.21.4",
|
||||
"nyc": "^10.1.2"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rm -r .nyc_output coverage",
|
||||
"coverage": "nyc --reporter=text --reporter=html mocha; echo; echo 'Open coverage/index.html file in your browser'",
|
||||
"coveralls": "nyc report --reporter=text-lcov | coveralls",
|
||||
"lint": "eslint .",
|
||||
"pretest": "npm run lint",
|
||||
"test": "nyc mocha"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4.0"
|
||||
},
|
||||
"types": "./coa.d.ts",
|
||||
"license": "MIT"
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
Copyright (c) 2011 Heather Arthur <fayearthur@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
# color-string
|
||||
|
||||
> library for parsing and generating CSS color strings.
|
||||
|
||||
## Install
|
||||
|
||||
With [npm](http://npmjs.org/):
|
||||
|
||||
```console
|
||||
$ npm install color-string
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Parsing
|
||||
|
||||
```js
|
||||
colorString.get('#FFF') // {model: 'rgb', value: [255, 255, 255, 1]}
|
||||
colorString.get('#FFFA') // {model: 'rgb', value: [255, 255, 255, 0.67]}
|
||||
colorString.get('#FFFFFFAA') // {model: 'rgb', value: [255, 255, 255, 0.67]}
|
||||
colorString.get('hsl(360, 100%, 50%)') // {model: 'hsl', value: [0, 100, 50, 1]}
|
||||
colorString.get('hsl(360 100% 50%)') // {model: 'hsl', value: [0, 100, 50, 1]}
|
||||
colorString.get('hwb(60, 3%, 60%)') // {model: 'hwb', value: [60, 3, 60, 1]}
|
||||
|
||||
colorString.get.rgb('#FFF') // [255, 255, 255, 1]
|
||||
colorString.get.rgb('blue') // [0, 0, 255, 1]
|
||||
colorString.get.rgb('rgba(200, 60, 60, 0.3)') // [200, 60, 60, 0.3]
|
||||
colorString.get.rgb('rgba(200 60 60 / 0.3)') // [200, 60, 60, 0.3]
|
||||
colorString.get.rgb('rgba(200 60 60 / 30%)') // [200, 60, 60, 0.3]
|
||||
colorString.get.rgb('rgb(200, 200, 200)') // [200, 200, 200, 1]
|
||||
colorString.get.rgb('rgb(200 200 200)') // [200, 200, 200, 1]
|
||||
|
||||
colorString.get.hsl('hsl(360, 100%, 50%)') // [0, 100, 50, 1]
|
||||
colorString.get.hsl('hsl(360 100% 50%)') // [0, 100, 50, 1]
|
||||
colorString.get.hsl('hsla(360, 60%, 50%, 0.4)') // [0, 60, 50, 0.4]
|
||||
colorString.get.hsl('hsl(360 60% 50% / 0.4)') // [0, 60, 50, 0.4]
|
||||
|
||||
colorString.get.hwb('hwb(60, 3%, 60%)') // [60, 3, 60, 1]
|
||||
colorString.get.hwb('hwb(60, 3%, 60%, 0.6)') // [60, 3, 60, 0.6]
|
||||
|
||||
colorString.get.rgb('invalid color string') // null
|
||||
```
|
||||
|
||||
### Generation
|
||||
|
||||
```js
|
||||
colorString.to.hex([255, 255, 255]) // "#FFFFFF"
|
||||
colorString.to.hex([0, 0, 255, 0.4]) // "#0000FF66"
|
||||
colorString.to.hex([0, 0, 255], 0.4) // "#0000FF66"
|
||||
colorString.to.rgb([255, 255, 255]) // "rgb(255, 255, 255)"
|
||||
colorString.to.rgb([0, 0, 255, 0.4]) // "rgba(0, 0, 255, 0.4)"
|
||||
colorString.to.rgb([0, 0, 255], 0.4) // "rgba(0, 0, 255, 0.4)"
|
||||
colorString.to.rgb.percent([0, 0, 255]) // "rgb(0%, 0%, 100%)"
|
||||
colorString.to.keyword([255, 255, 0]) // "yellow"
|
||||
colorString.to.hsl([360, 100, 100]) // "hsl(360, 100%, 100%)"
|
||||
colorString.to.hwb([50, 3, 15]) // "hwb(50, 3%, 15%)"
|
||||
|
||||
// all functions also support swizzling
|
||||
colorString.to.rgb(0, [0, 255], 0.4) // "rgba(0, 0, 255, 0.4)"
|
||||
colorString.to.rgb([0, 0], [255], 0.4) // "rgba(0, 0, 255, 0.4)"
|
||||
colorString.to.rgb([0], 0, [255, 0.4]) // "rgba(0, 0, 255, 0.4)"
|
||||
```
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
/* MIT license */
|
||||
var colorNames = require('color-name');
|
||||
var swizzle = require('simple-swizzle');
|
||||
var hasOwnProperty = Object.hasOwnProperty;
|
||||
|
||||
var reverseNames = Object.create(null);
|
||||
|
||||
// create a list of reverse color names
|
||||
for (var name in colorNames) {
|
||||
if (hasOwnProperty.call(colorNames, name)) {
|
||||
reverseNames[colorNames[name]] = name;
|
||||
}
|
||||
}
|
||||
|
||||
var cs = module.exports = {
|
||||
to: {},
|
||||
get: {}
|
||||
};
|
||||
|
||||
cs.get = function (string) {
|
||||
var prefix = string.substring(0, 3).toLowerCase();
|
||||
var val;
|
||||
var model;
|
||||
switch (prefix) {
|
||||
case 'hsl':
|
||||
val = cs.get.hsl(string);
|
||||
model = 'hsl';
|
||||
break;
|
||||
case 'hwb':
|
||||
val = cs.get.hwb(string);
|
||||
model = 'hwb';
|
||||
break;
|
||||
default:
|
||||
val = cs.get.rgb(string);
|
||||
model = 'rgb';
|
||||
break;
|
||||
}
|
||||
|
||||
if (!val) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {model: model, value: val};
|
||||
};
|
||||
|
||||
cs.get.rgb = function (string) {
|
||||
if (!string) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var abbr = /^#([a-f0-9]{3,4})$/i;
|
||||
var hex = /^#([a-f0-9]{6})([a-f0-9]{2})?$/i;
|
||||
var rgba = /^rgba?\(\s*([+-]?\d+)(?=[\s,])\s*(?:,\s*)?([+-]?\d+)(?=[\s,])\s*(?:,\s*)?([+-]?\d+)\s*(?:[,|\/]\s*([+-]?[\d\.]+)(%?)\s*)?\)$/;
|
||||
var per = /^rgba?\(\s*([+-]?[\d\.]+)\%\s*,?\s*([+-]?[\d\.]+)\%\s*,?\s*([+-]?[\d\.]+)\%\s*(?:[,|\/]\s*([+-]?[\d\.]+)(%?)\s*)?\)$/;
|
||||
var keyword = /^(\w+)$/;
|
||||
|
||||
var rgb = [0, 0, 0, 1];
|
||||
var match;
|
||||
var i;
|
||||
var hexAlpha;
|
||||
|
||||
if (match = string.match(hex)) {
|
||||
hexAlpha = match[2];
|
||||
match = match[1];
|
||||
|
||||
for (i = 0; i < 3; i++) {
|
||||
// https://jsperf.com/slice-vs-substr-vs-substring-methods-long-string/19
|
||||
var i2 = i * 2;
|
||||
rgb[i] = parseInt(match.slice(i2, i2 + 2), 16);
|
||||
}
|
||||
|
||||
if (hexAlpha) {
|
||||
rgb[3] = parseInt(hexAlpha, 16) / 255;
|
||||
}
|
||||
} else if (match = string.match(abbr)) {
|
||||
match = match[1];
|
||||
hexAlpha = match[3];
|
||||
|
||||
for (i = 0; i < 3; i++) {
|
||||
rgb[i] = parseInt(match[i] + match[i], 16);
|
||||
}
|
||||
|
||||
if (hexAlpha) {
|
||||
rgb[3] = parseInt(hexAlpha + hexAlpha, 16) / 255;
|
||||
}
|
||||
} else if (match = string.match(rgba)) {
|
||||
for (i = 0; i < 3; i++) {
|
||||
rgb[i] = parseInt(match[i + 1], 0);
|
||||
}
|
||||
|
||||
if (match[4]) {
|
||||
if (match[5]) {
|
||||
rgb[3] = parseFloat(match[4]) * 0.01;
|
||||
} else {
|
||||
rgb[3] = parseFloat(match[4]);
|
||||
}
|
||||
}
|
||||
} else if (match = string.match(per)) {
|
||||
for (i = 0; i < 3; i++) {
|
||||
rgb[i] = Math.round(parseFloat(match[i + 1]) * 2.55);
|
||||
}
|
||||
|
||||
if (match[4]) {
|
||||
if (match[5]) {
|
||||
rgb[3] = parseFloat(match[4]) * 0.01;
|
||||
} else {
|
||||
rgb[3] = parseFloat(match[4]);
|
||||
}
|
||||
}
|
||||
} else if (match = string.match(keyword)) {
|
||||
if (match[1] === 'transparent') {
|
||||
return [0, 0, 0, 0];
|
||||
}
|
||||
|
||||
if (!hasOwnProperty.call(colorNames, match[1])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
rgb = colorNames[match[1]];
|
||||
rgb[3] = 1;
|
||||
|
||||
return rgb;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (i = 0; i < 3; i++) {
|
||||
rgb[i] = clamp(rgb[i], 0, 255);
|
||||
}
|
||||
rgb[3] = clamp(rgb[3], 0, 1);
|
||||
|
||||
return rgb;
|
||||
};
|
||||
|
||||
cs.get.hsl = function (string) {
|
||||
if (!string) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var hsl = /^hsla?\(\s*([+-]?(?:\d{0,3}\.)?\d+)(?:deg)?\s*,?\s*([+-]?[\d\.]+)%\s*,?\s*([+-]?[\d\.]+)%\s*(?:[,|\/]\s*([+-]?(?=\.\d|\d)(?:0|[1-9]\d*)?(?:\.\d*)?(?:[eE][+-]?\d+)?)\s*)?\)$/;
|
||||
var match = string.match(hsl);
|
||||
|
||||
if (match) {
|
||||
var alpha = parseFloat(match[4]);
|
||||
var h = ((parseFloat(match[1]) % 360) + 360) % 360;
|
||||
var s = clamp(parseFloat(match[2]), 0, 100);
|
||||
var l = clamp(parseFloat(match[3]), 0, 100);
|
||||
var a = clamp(isNaN(alpha) ? 1 : alpha, 0, 1);
|
||||
|
||||
return [h, s, l, a];
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
cs.get.hwb = function (string) {
|
||||
if (!string) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var hwb = /^hwb\(\s*([+-]?\d{0,3}(?:\.\d+)?)(?:deg)?\s*,\s*([+-]?[\d\.]+)%\s*,\s*([+-]?[\d\.]+)%\s*(?:,\s*([+-]?(?=\.\d|\d)(?:0|[1-9]\d*)?(?:\.\d*)?(?:[eE][+-]?\d+)?)\s*)?\)$/;
|
||||
var match = string.match(hwb);
|
||||
|
||||
if (match) {
|
||||
var alpha = parseFloat(match[4]);
|
||||
var h = ((parseFloat(match[1]) % 360) + 360) % 360;
|
||||
var w = clamp(parseFloat(match[2]), 0, 100);
|
||||
var b = clamp(parseFloat(match[3]), 0, 100);
|
||||
var a = clamp(isNaN(alpha) ? 1 : alpha, 0, 1);
|
||||
return [h, w, b, a];
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
cs.to.hex = function () {
|
||||
var rgba = swizzle(arguments);
|
||||
|
||||
return (
|
||||
'#' +
|
||||
hexDouble(rgba[0]) +
|
||||
hexDouble(rgba[1]) +
|
||||
hexDouble(rgba[2]) +
|
||||
(rgba[3] < 1
|
||||
? (hexDouble(Math.round(rgba[3] * 255)))
|
||||
: '')
|
||||
);
|
||||
};
|
||||
|
||||
cs.to.rgb = function () {
|
||||
var rgba = swizzle(arguments);
|
||||
|
||||
return rgba.length < 4 || rgba[3] === 1
|
||||
? 'rgb(' + Math.round(rgba[0]) + ', ' + Math.round(rgba[1]) + ', ' + Math.round(rgba[2]) + ')'
|
||||
: 'rgba(' + Math.round(rgba[0]) + ', ' + Math.round(rgba[1]) + ', ' + Math.round(rgba[2]) + ', ' + rgba[3] + ')';
|
||||
};
|
||||
|
||||
cs.to.rgb.percent = function () {
|
||||
var rgba = swizzle(arguments);
|
||||
|
||||
var r = Math.round(rgba[0] / 255 * 100);
|
||||
var g = Math.round(rgba[1] / 255 * 100);
|
||||
var b = Math.round(rgba[2] / 255 * 100);
|
||||
|
||||
return rgba.length < 4 || rgba[3] === 1
|
||||
? 'rgb(' + r + '%, ' + g + '%, ' + b + '%)'
|
||||
: 'rgba(' + r + '%, ' + g + '%, ' + b + '%, ' + rgba[3] + ')';
|
||||
};
|
||||
|
||||
cs.to.hsl = function () {
|
||||
var hsla = swizzle(arguments);
|
||||
return hsla.length < 4 || hsla[3] === 1
|
||||
? 'hsl(' + hsla[0] + ', ' + hsla[1] + '%, ' + hsla[2] + '%)'
|
||||
: 'hsla(' + hsla[0] + ', ' + hsla[1] + '%, ' + hsla[2] + '%, ' + hsla[3] + ')';
|
||||
};
|
||||
|
||||
// hwb is a bit different than rgb(a) & hsl(a) since there is no alpha specific syntax
|
||||
// (hwb have alpha optional & 1 is default value)
|
||||
cs.to.hwb = function () {
|
||||
var hwba = swizzle(arguments);
|
||||
|
||||
var a = '';
|
||||
if (hwba.length >= 4 && hwba[3] !== 1) {
|
||||
a = ', ' + hwba[3];
|
||||
}
|
||||
|
||||
return 'hwb(' + hwba[0] + ', ' + hwba[1] + '%, ' + hwba[2] + '%' + a + ')';
|
||||
};
|
||||
|
||||
cs.to.keyword = function (rgb) {
|
||||
return reverseNames[rgb.slice(0, 3)];
|
||||
};
|
||||
|
||||
// helpers
|
||||
function clamp(num, min, max) {
|
||||
return Math.min(Math.max(min, num), max);
|
||||
}
|
||||
|
||||
function hexDouble(num) {
|
||||
var str = Math.round(num).toString(16).toUpperCase();
|
||||
return (str.length < 2) ? '0' + str : str;
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "color-string",
|
||||
"description": "Parser and generator for CSS color strings",
|
||||
"version": "1.9.1",
|
||||
"author": "Heather Arthur <fayearthur@gmail.com>",
|
||||
"contributors": [
|
||||
"Maxime Thirouin",
|
||||
"Dyma Ywanov <dfcreative@gmail.com>",
|
||||
"Josh Junon"
|
||||
],
|
||||
"repository": "Qix-/color-string",
|
||||
"scripts": {
|
||||
"pretest": "xo",
|
||||
"test": "node test/basic.js"
|
||||
},
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"index.js"
|
||||
],
|
||||
"xo": {
|
||||
"rules": {
|
||||
"no-cond-assign": 0,
|
||||
"operator-linebreak": 0
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"color-name": "^1.0.0",
|
||||
"simple-swizzle": "^0.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"xo": "^0.12.1"
|
||||
},
|
||||
"keywords": [
|
||||
"color",
|
||||
"colour",
|
||||
"rgb",
|
||||
"css"
|
||||
]
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
Copyright (c) 2012 Heather Arthur
|
||||
|
||||
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.
|
||||
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
# color [](https://travis-ci.org/Qix-/color)
|
||||
|
||||
> JavaScript library for immutable color conversion and manipulation with support for CSS color strings.
|
||||
|
||||
```js
|
||||
var color = Color('#7743CE').alpha(0.5).lighten(0.5);
|
||||
console.log(color.hsl().string()); // 'hsla(262, 59%, 81%, 0.5)'
|
||||
|
||||
console.log(color.cmyk().round().array()); // [ 16, 25, 0, 8, 0.5 ]
|
||||
|
||||
console.log(color.ansi256().object()); // { ansi256: 183, alpha: 0.5 }
|
||||
```
|
||||
|
||||
## Install
|
||||
```console
|
||||
$ npm install color
|
||||
```
|
||||
|
||||
## Usage
|
||||
```js
|
||||
var Color = require('color');
|
||||
```
|
||||
|
||||
### Constructors
|
||||
```js
|
||||
var color = Color('rgb(255, 255, 255)')
|
||||
var color = Color({r: 255, g: 255, b: 255})
|
||||
var color = Color.rgb(255, 255, 255)
|
||||
var color = Color.rgb([255, 255, 255])
|
||||
```
|
||||
|
||||
Set the values for individual channels with `alpha`, `red`, `green`, `blue`, `hue`, `saturationl` (hsl), `saturationv` (hsv), `lightness`, `whiteness`, `blackness`, `cyan`, `magenta`, `yellow`, `black`
|
||||
|
||||
String constructors are handled by [color-string](https://www.npmjs.com/package/color-string)
|
||||
|
||||
### Getters
|
||||
```js
|
||||
color.hsl();
|
||||
```
|
||||
Convert a color to a different space (`hsl()`, `cmyk()`, etc.).
|
||||
|
||||
```js
|
||||
color.object(); // {r: 255, g: 255, b: 255}
|
||||
```
|
||||
Get a hash of the color value. Reflects the color's current model (see above).
|
||||
|
||||
```js
|
||||
color.rgb().array() // [255, 255, 255]
|
||||
```
|
||||
Get an array of the values with `array()`. Reflects the color's current model (see above).
|
||||
|
||||
```js
|
||||
color.rgbNumber() // 16777215 (0xffffff)
|
||||
```
|
||||
Get the rgb number value.
|
||||
|
||||
```js
|
||||
color.hex() // #ffffff
|
||||
```
|
||||
Get the hex value.
|
||||
|
||||
```js
|
||||
color.red() // 255
|
||||
```
|
||||
Get the value for an individual channel.
|
||||
|
||||
### CSS Strings
|
||||
```js
|
||||
color.hsl().string() // 'hsl(320, 50%, 100%)'
|
||||
```
|
||||
|
||||
Calling `.string()` with a number rounds the numbers to that decimal place. It defaults to 1.
|
||||
|
||||
### Luminosity
|
||||
```js
|
||||
color.luminosity(); // 0.412
|
||||
```
|
||||
The [WCAG luminosity](http://www.w3.org/TR/WCAG20/#relativeluminancedef) of the color. 0 is black, 1 is white.
|
||||
|
||||
```js
|
||||
color.contrast(Color("blue")) // 12
|
||||
```
|
||||
The [WCAG contrast ratio](http://www.w3.org/TR/WCAG20/#contrast-ratiodef) to another color, from 1 (same color) to 21 (contrast b/w white and black).
|
||||
|
||||
```js
|
||||
color.isLight(); // true
|
||||
color.isDark(); // false
|
||||
```
|
||||
Get whether the color is "light" or "dark", useful for deciding text color.
|
||||
|
||||
### Manipulation
|
||||
```js
|
||||
color.negate() // rgb(0, 100, 255) -> rgb(255, 155, 0)
|
||||
|
||||
color.lighten(0.5) // hsl(100, 50%, 50%) -> hsl(100, 50%, 75%)
|
||||
color.lighten(0.5) // hsl(100, 50%, 0) -> hsl(100, 50%, 0)
|
||||
color.darken(0.5) // hsl(100, 50%, 50%) -> hsl(100, 50%, 25%)
|
||||
color.darken(0.5) // hsl(100, 50%, 0) -> hsl(100, 50%, 0)
|
||||
|
||||
color.lightness(50) // hsl(100, 50%, 10%) -> hsl(100, 50%, 50%)
|
||||
|
||||
color.saturate(0.5) // hsl(100, 50%, 50%) -> hsl(100, 75%, 50%)
|
||||
color.desaturate(0.5) // hsl(100, 50%, 50%) -> hsl(100, 25%, 50%)
|
||||
color.grayscale() // #5CBF54 -> #969696
|
||||
|
||||
color.whiten(0.5) // hwb(100, 50%, 50%) -> hwb(100, 75%, 50%)
|
||||
color.blacken(0.5) // hwb(100, 50%, 50%) -> hwb(100, 50%, 75%)
|
||||
|
||||
color.fade(0.5) // rgba(10, 10, 10, 0.8) -> rgba(10, 10, 10, 0.4)
|
||||
color.opaquer(0.5) // rgba(10, 10, 10, 0.8) -> rgba(10, 10, 10, 1.0)
|
||||
|
||||
color.rotate(180) // hsl(60, 20%, 20%) -> hsl(240, 20%, 20%)
|
||||
color.rotate(-90) // hsl(60, 20%, 20%) -> hsl(330, 20%, 20%)
|
||||
|
||||
color.mix(Color("yellow")) // cyan -> rgb(128, 255, 128)
|
||||
color.mix(Color("yellow"), 0.3) // cyan -> rgb(77, 255, 179)
|
||||
|
||||
// chaining
|
||||
color.green(100).grayscale().lighten(0.6)
|
||||
```
|
||||
|
||||
## Propers
|
||||
The API was inspired by [color-js](https://github.com/brehaut/color-js). Manipulation functions by CSS tools like Sass, LESS, and Stylus.
|
||||
+482
@@ -0,0 +1,482 @@
|
||||
'use strict';
|
||||
|
||||
var colorString = require('color-string');
|
||||
var convert = require('color-convert');
|
||||
|
||||
var _slice = [].slice;
|
||||
|
||||
var skippedModels = [
|
||||
// to be honest, I don't really feel like keyword belongs in color convert, but eh.
|
||||
'keyword',
|
||||
|
||||
// gray conflicts with some method names, and has its own method defined.
|
||||
'gray',
|
||||
|
||||
// shouldn't really be in color-convert either...
|
||||
'hex'
|
||||
];
|
||||
|
||||
var hashedModelKeys = {};
|
||||
Object.keys(convert).forEach(function (model) {
|
||||
hashedModelKeys[_slice.call(convert[model].labels).sort().join('')] = model;
|
||||
});
|
||||
|
||||
var limiters = {};
|
||||
|
||||
function Color(obj, model) {
|
||||
if (!(this instanceof Color)) {
|
||||
return new Color(obj, model);
|
||||
}
|
||||
|
||||
if (model && model in skippedModels) {
|
||||
model = null;
|
||||
}
|
||||
|
||||
if (model && !(model in convert)) {
|
||||
throw new Error('Unknown model: ' + model);
|
||||
}
|
||||
|
||||
var i;
|
||||
var channels;
|
||||
|
||||
if (obj == null) { // eslint-disable-line no-eq-null,eqeqeq
|
||||
this.model = 'rgb';
|
||||
this.color = [0, 0, 0];
|
||||
this.valpha = 1;
|
||||
} else if (obj instanceof Color) {
|
||||
this.model = obj.model;
|
||||
this.color = obj.color.slice();
|
||||
this.valpha = obj.valpha;
|
||||
} else if (typeof obj === 'string') {
|
||||
var result = colorString.get(obj);
|
||||
if (result === null) {
|
||||
throw new Error('Unable to parse color from string: ' + obj);
|
||||
}
|
||||
|
||||
this.model = result.model;
|
||||
channels = convert[this.model].channels;
|
||||
this.color = result.value.slice(0, channels);
|
||||
this.valpha = typeof result.value[channels] === 'number' ? result.value[channels] : 1;
|
||||
} else if (obj.length) {
|
||||
this.model = model || 'rgb';
|
||||
channels = convert[this.model].channels;
|
||||
var newArr = _slice.call(obj, 0, channels);
|
||||
this.color = zeroArray(newArr, channels);
|
||||
this.valpha = typeof obj[channels] === 'number' ? obj[channels] : 1;
|
||||
} else if (typeof obj === 'number') {
|
||||
// this is always RGB - can be converted later on.
|
||||
obj &= 0xFFFFFF;
|
||||
this.model = 'rgb';
|
||||
this.color = [
|
||||
(obj >> 16) & 0xFF,
|
||||
(obj >> 8) & 0xFF,
|
||||
obj & 0xFF
|
||||
];
|
||||
this.valpha = 1;
|
||||
} else {
|
||||
this.valpha = 1;
|
||||
|
||||
var keys = Object.keys(obj);
|
||||
if ('alpha' in obj) {
|
||||
keys.splice(keys.indexOf('alpha'), 1);
|
||||
this.valpha = typeof obj.alpha === 'number' ? obj.alpha : 0;
|
||||
}
|
||||
|
||||
var hashedKeys = keys.sort().join('');
|
||||
if (!(hashedKeys in hashedModelKeys)) {
|
||||
throw new Error('Unable to parse color from object: ' + JSON.stringify(obj));
|
||||
}
|
||||
|
||||
this.model = hashedModelKeys[hashedKeys];
|
||||
|
||||
var labels = convert[this.model].labels;
|
||||
var color = [];
|
||||
for (i = 0; i < labels.length; i++) {
|
||||
color.push(obj[labels[i]]);
|
||||
}
|
||||
|
||||
this.color = zeroArray(color);
|
||||
}
|
||||
|
||||
// perform limitations (clamping, etc.)
|
||||
if (limiters[this.model]) {
|
||||
channels = convert[this.model].channels;
|
||||
for (i = 0; i < channels; i++) {
|
||||
var limit = limiters[this.model][i];
|
||||
if (limit) {
|
||||
this.color[i] = limit(this.color[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.valpha = Math.max(0, Math.min(1, this.valpha));
|
||||
|
||||
if (Object.freeze) {
|
||||
Object.freeze(this);
|
||||
}
|
||||
}
|
||||
|
||||
Color.prototype = {
|
||||
toString: function () {
|
||||
return this.string();
|
||||
},
|
||||
|
||||
toJSON: function () {
|
||||
return this[this.model]();
|
||||
},
|
||||
|
||||
string: function (places) {
|
||||
var self = this.model in colorString.to ? this : this.rgb();
|
||||
self = self.round(typeof places === 'number' ? places : 1);
|
||||
var args = self.valpha === 1 ? self.color : self.color.concat(this.valpha);
|
||||
return colorString.to[self.model](args);
|
||||
},
|
||||
|
||||
percentString: function (places) {
|
||||
var self = this.rgb().round(typeof places === 'number' ? places : 1);
|
||||
var args = self.valpha === 1 ? self.color : self.color.concat(this.valpha);
|
||||
return colorString.to.rgb.percent(args);
|
||||
},
|
||||
|
||||
array: function () {
|
||||
return this.valpha === 1 ? this.color.slice() : this.color.concat(this.valpha);
|
||||
},
|
||||
|
||||
object: function () {
|
||||
var result = {};
|
||||
var channels = convert[this.model].channels;
|
||||
var labels = convert[this.model].labels;
|
||||
|
||||
for (var i = 0; i < channels; i++) {
|
||||
result[labels[i]] = this.color[i];
|
||||
}
|
||||
|
||||
if (this.valpha !== 1) {
|
||||
result.alpha = this.valpha;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
unitArray: function () {
|
||||
var rgb = this.rgb().color;
|
||||
rgb[0] /= 255;
|
||||
rgb[1] /= 255;
|
||||
rgb[2] /= 255;
|
||||
|
||||
if (this.valpha !== 1) {
|
||||
rgb.push(this.valpha);
|
||||
}
|
||||
|
||||
return rgb;
|
||||
},
|
||||
|
||||
unitObject: function () {
|
||||
var rgb = this.rgb().object();
|
||||
rgb.r /= 255;
|
||||
rgb.g /= 255;
|
||||
rgb.b /= 255;
|
||||
|
||||
if (this.valpha !== 1) {
|
||||
rgb.alpha = this.valpha;
|
||||
}
|
||||
|
||||
return rgb;
|
||||
},
|
||||
|
||||
round: function (places) {
|
||||
places = Math.max(places || 0, 0);
|
||||
return new Color(this.color.map(roundToPlace(places)).concat(this.valpha), this.model);
|
||||
},
|
||||
|
||||
alpha: function (val) {
|
||||
if (arguments.length) {
|
||||
return new Color(this.color.concat(Math.max(0, Math.min(1, val))), this.model);
|
||||
}
|
||||
|
||||
return this.valpha;
|
||||
},
|
||||
|
||||
// rgb
|
||||
red: getset('rgb', 0, maxfn(255)),
|
||||
green: getset('rgb', 1, maxfn(255)),
|
||||
blue: getset('rgb', 2, maxfn(255)),
|
||||
|
||||
hue: getset(['hsl', 'hsv', 'hsl', 'hwb', 'hcg'], 0, function (val) { return ((val % 360) + 360) % 360; }), // eslint-disable-line brace-style
|
||||
|
||||
saturationl: getset('hsl', 1, maxfn(100)),
|
||||
lightness: getset('hsl', 2, maxfn(100)),
|
||||
|
||||
saturationv: getset('hsv', 1, maxfn(100)),
|
||||
value: getset('hsv', 2, maxfn(100)),
|
||||
|
||||
chroma: getset('hcg', 1, maxfn(100)),
|
||||
gray: getset('hcg', 2, maxfn(100)),
|
||||
|
||||
white: getset('hwb', 1, maxfn(100)),
|
||||
wblack: getset('hwb', 2, maxfn(100)),
|
||||
|
||||
cyan: getset('cmyk', 0, maxfn(100)),
|
||||
magenta: getset('cmyk', 1, maxfn(100)),
|
||||
yellow: getset('cmyk', 2, maxfn(100)),
|
||||
black: getset('cmyk', 3, maxfn(100)),
|
||||
|
||||
x: getset('xyz', 0, maxfn(100)),
|
||||
y: getset('xyz', 1, maxfn(100)),
|
||||
z: getset('xyz', 2, maxfn(100)),
|
||||
|
||||
l: getset('lab', 0, maxfn(100)),
|
||||
a: getset('lab', 1),
|
||||
b: getset('lab', 2),
|
||||
|
||||
keyword: function (val) {
|
||||
if (arguments.length) {
|
||||
return new Color(val);
|
||||
}
|
||||
|
||||
return convert[this.model].keyword(this.color);
|
||||
},
|
||||
|
||||
hex: function (val) {
|
||||
if (arguments.length) {
|
||||
return new Color(val);
|
||||
}
|
||||
|
||||
return colorString.to.hex(this.rgb().round().color);
|
||||
},
|
||||
|
||||
rgbNumber: function () {
|
||||
var rgb = this.rgb().color;
|
||||
return ((rgb[0] & 0xFF) << 16) | ((rgb[1] & 0xFF) << 8) | (rgb[2] & 0xFF);
|
||||
},
|
||||
|
||||
luminosity: function () {
|
||||
// http://www.w3.org/TR/WCAG20/#relativeluminancedef
|
||||
var rgb = this.rgb().color;
|
||||
|
||||
var lum = [];
|
||||
for (var i = 0; i < rgb.length; i++) {
|
||||
var chan = rgb[i] / 255;
|
||||
lum[i] = (chan <= 0.03928) ? chan / 12.92 : Math.pow(((chan + 0.055) / 1.055), 2.4);
|
||||
}
|
||||
|
||||
return 0.2126 * lum[0] + 0.7152 * lum[1] + 0.0722 * lum[2];
|
||||
},
|
||||
|
||||
contrast: function (color2) {
|
||||
// http://www.w3.org/TR/WCAG20/#contrast-ratiodef
|
||||
var lum1 = this.luminosity();
|
||||
var lum2 = color2.luminosity();
|
||||
|
||||
if (lum1 > lum2) {
|
||||
return (lum1 + 0.05) / (lum2 + 0.05);
|
||||
}
|
||||
|
||||
return (lum2 + 0.05) / (lum1 + 0.05);
|
||||
},
|
||||
|
||||
level: function (color2) {
|
||||
var contrastRatio = this.contrast(color2);
|
||||
if (contrastRatio >= 7.1) {
|
||||
return 'AAA';
|
||||
}
|
||||
|
||||
return (contrastRatio >= 4.5) ? 'AA' : '';
|
||||
},
|
||||
|
||||
isDark: function () {
|
||||
// YIQ equation from http://24ways.org/2010/calculating-color-contrast
|
||||
var rgb = this.rgb().color;
|
||||
var yiq = (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000;
|
||||
return yiq < 128;
|
||||
},
|
||||
|
||||
isLight: function () {
|
||||
return !this.isDark();
|
||||
},
|
||||
|
||||
negate: function () {
|
||||
var rgb = this.rgb();
|
||||
for (var i = 0; i < 3; i++) {
|
||||
rgb.color[i] = 255 - rgb.color[i];
|
||||
}
|
||||
return rgb;
|
||||
},
|
||||
|
||||
lighten: function (ratio) {
|
||||
var hsl = this.hsl();
|
||||
hsl.color[2] += hsl.color[2] * ratio;
|
||||
return hsl;
|
||||
},
|
||||
|
||||
darken: function (ratio) {
|
||||
var hsl = this.hsl();
|
||||
hsl.color[2] -= hsl.color[2] * ratio;
|
||||
return hsl;
|
||||
},
|
||||
|
||||
saturate: function (ratio) {
|
||||
var hsl = this.hsl();
|
||||
hsl.color[1] += hsl.color[1] * ratio;
|
||||
return hsl;
|
||||
},
|
||||
|
||||
desaturate: function (ratio) {
|
||||
var hsl = this.hsl();
|
||||
hsl.color[1] -= hsl.color[1] * ratio;
|
||||
return hsl;
|
||||
},
|
||||
|
||||
whiten: function (ratio) {
|
||||
var hwb = this.hwb();
|
||||
hwb.color[1] += hwb.color[1] * ratio;
|
||||
return hwb;
|
||||
},
|
||||
|
||||
blacken: function (ratio) {
|
||||
var hwb = this.hwb();
|
||||
hwb.color[2] += hwb.color[2] * ratio;
|
||||
return hwb;
|
||||
},
|
||||
|
||||
grayscale: function () {
|
||||
// http://en.wikipedia.org/wiki/Grayscale#Converting_color_to_grayscale
|
||||
var rgb = this.rgb().color;
|
||||
var val = rgb[0] * 0.3 + rgb[1] * 0.59 + rgb[2] * 0.11;
|
||||
return Color.rgb(val, val, val);
|
||||
},
|
||||
|
||||
fade: function (ratio) {
|
||||
return this.alpha(this.valpha - (this.valpha * ratio));
|
||||
},
|
||||
|
||||
opaquer: function (ratio) {
|
||||
return this.alpha(this.valpha + (this.valpha * ratio));
|
||||
},
|
||||
|
||||
rotate: function (degrees) {
|
||||
var hsl = this.hsl();
|
||||
var hue = hsl.color[0];
|
||||
hue = (hue + degrees) % 360;
|
||||
hue = hue < 0 ? 360 + hue : hue;
|
||||
hsl.color[0] = hue;
|
||||
return hsl;
|
||||
},
|
||||
|
||||
mix: function (mixinColor, weight) {
|
||||
// ported from sass implementation in C
|
||||
// https://github.com/sass/libsass/blob/0e6b4a2850092356aa3ece07c6b249f0221caced/functions.cpp#L209
|
||||
if (!mixinColor || !mixinColor.rgb) {
|
||||
throw new Error('Argument to "mix" was not a Color instance, but rather an instance of ' + typeof mixinColor);
|
||||
}
|
||||
var color1 = mixinColor.rgb();
|
||||
var color2 = this.rgb();
|
||||
var p = weight === undefined ? 0.5 : weight;
|
||||
|
||||
var w = 2 * p - 1;
|
||||
var a = color1.alpha() - color2.alpha();
|
||||
|
||||
var w1 = (((w * a === -1) ? w : (w + a) / (1 + w * a)) + 1) / 2.0;
|
||||
var w2 = 1 - w1;
|
||||
|
||||
return Color.rgb(
|
||||
w1 * color1.red() + w2 * color2.red(),
|
||||
w1 * color1.green() + w2 * color2.green(),
|
||||
w1 * color1.blue() + w2 * color2.blue(),
|
||||
color1.alpha() * p + color2.alpha() * (1 - p));
|
||||
}
|
||||
};
|
||||
|
||||
// model conversion methods and static constructors
|
||||
Object.keys(convert).forEach(function (model) {
|
||||
if (skippedModels.indexOf(model) !== -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
var channels = convert[model].channels;
|
||||
|
||||
// conversion methods
|
||||
Color.prototype[model] = function () {
|
||||
if (this.model === model) {
|
||||
return new Color(this);
|
||||
}
|
||||
|
||||
if (arguments.length) {
|
||||
return new Color(arguments, model);
|
||||
}
|
||||
|
||||
var newAlpha = typeof arguments[channels] === 'number' ? channels : this.valpha;
|
||||
return new Color(assertArray(convert[this.model][model].raw(this.color)).concat(newAlpha), model);
|
||||
};
|
||||
|
||||
// 'static' construction methods
|
||||
Color[model] = function (color) {
|
||||
if (typeof color === 'number') {
|
||||
color = zeroArray(_slice.call(arguments), channels);
|
||||
}
|
||||
return new Color(color, model);
|
||||
};
|
||||
});
|
||||
|
||||
function roundTo(num, places) {
|
||||
return Number(num.toFixed(places));
|
||||
}
|
||||
|
||||
function roundToPlace(places) {
|
||||
return function (num) {
|
||||
return roundTo(num, places);
|
||||
};
|
||||
}
|
||||
|
||||
function getset(model, channel, modifier) {
|
||||
model = Array.isArray(model) ? model : [model];
|
||||
|
||||
model.forEach(function (m) {
|
||||
(limiters[m] || (limiters[m] = []))[channel] = modifier;
|
||||
});
|
||||
|
||||
model = model[0];
|
||||
|
||||
return function (val) {
|
||||
var result;
|
||||
|
||||
if (arguments.length) {
|
||||
if (modifier) {
|
||||
val = modifier(val);
|
||||
}
|
||||
|
||||
result = this[model]();
|
||||
result.color[channel] = val;
|
||||
return result;
|
||||
}
|
||||
|
||||
result = this[model]().color[channel];
|
||||
if (modifier) {
|
||||
result = modifier(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
function maxfn(max) {
|
||||
return function (v) {
|
||||
return Math.max(0, Math.min(max, v));
|
||||
};
|
||||
}
|
||||
|
||||
function assertArray(val) {
|
||||
return Array.isArray(val) ? val : [val];
|
||||
}
|
||||
|
||||
function zeroArray(arr, length) {
|
||||
for (var i = 0; i < length; i++) {
|
||||
if (typeof arr[i] !== 'number') {
|
||||
arr[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
module.exports = Color;
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "color",
|
||||
"version": "3.2.1",
|
||||
"description": "Color conversion and manipulation with CSS string support",
|
||||
"keywords": [
|
||||
"color",
|
||||
"colour",
|
||||
"css"
|
||||
],
|
||||
"authors": [
|
||||
"Josh Junon <i.am.qix@gmail.com>",
|
||||
"Heather Arthur <fayearthur@gmail.com>",
|
||||
"Maxime Thirouin"
|
||||
],
|
||||
"license": "MIT",
|
||||
"repository": "Qix-/color",
|
||||
"xo": {
|
||||
"rules": {
|
||||
"no-cond-assign": 0,
|
||||
"new-cap": 0
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"CHANGELOG.md",
|
||||
"LICENSE",
|
||||
"index.js"
|
||||
],
|
||||
"scripts": {
|
||||
"pretest": "xo",
|
||||
"test": "mocha"
|
||||
},
|
||||
"dependencies": {
|
||||
"color-convert": "^1.9.3",
|
||||
"color-string": "^1.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "9.0.2",
|
||||
"xo": "0.12.1"
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
Copyright (c) Felix Böhm
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
|
||||
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
|
||||
THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS,
|
||||
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
# css-select [](https://npmjs.org/package/css-select) [](http://travis-ci.org/fb55/css-select) [](https://npmjs.org/package/css-select) [](https://coveralls.io/r/fb55/css-select)
|
||||
|
||||
a CSS selector compiler/engine
|
||||
|
||||
## What?
|
||||
|
||||
css-select turns CSS selectors into functions that tests if elements match them. When searching for elements, testing is executed "from the top", similar to how browsers execute CSS selectors.
|
||||
|
||||
In its default configuration, css-select queries the DOM structure of the [`domhandler`](https://github.com/fb55/domhandler) module (also known as htmlparser2 DOM).
|
||||
It uses [`domutils`](https://github.com/fb55/domutils) as its default adapter over the DOM structure. See Options below for details on querying alternative DOM structures.
|
||||
|
||||
__Features:__
|
||||
|
||||
- Full implementation of CSS3 selectors
|
||||
- Partial implementation of jQuery/Sizzle extensions
|
||||
- Very high test coverage
|
||||
- Pretty good performance
|
||||
|
||||
## Why?
|
||||
|
||||
The traditional approach of executing CSS selectors, named left-to-right execution, is to execute every component of the selector in order, from left to right _(duh)_. The execution of the selector `a b` for example will first query for `a` elements, then search these for `b` elements. (That's the approach of eg. [`Sizzle`](https://github.com/jquery/sizzle), [`nwmatcher`](https://github.com/dperini/nwmatcher/) and [`qwery`](https://github.com/ded/qwery).)
|
||||
|
||||
While this works, it has some downsides: Children of `a`s will be checked multiple times; first, to check if they are also `a`s, then, for every superior `a` once, if they are `b`s. Using [Big O notation](http://en.wikipedia.org/wiki/Big_O_notation), that would be `O(n^(k+1))`, where `k` is the number of descendant selectors (that's the space in the example above).
|
||||
|
||||
The far more efficient approach is to first look for `b` elements, then check if they have superior `a` elements: Using big O notation again, that would be `O(n)`. That's called right-to-left execution.
|
||||
|
||||
And that's what css-select does – and why it's quite performant.
|
||||
|
||||
## How does it work?
|
||||
|
||||
By building a stack of functions.
|
||||
|
||||
_Wait, what?_
|
||||
|
||||
Okay, so let's suppose we want to compile the selector `a b` again, for right-to-left execution. We start by _parsing_ the selector, which means we turn the selector into an array of the building-blocks of the selector, so we can distinguish them easily. That's what the [`css-what`](https://github.com/fb55/css-what) module is for, if you want to have a look.
|
||||
|
||||
Anyway, after parsing, we end up with an array like this one:
|
||||
|
||||
```js
|
||||
[
|
||||
{ type: 'tag', name: 'a' },
|
||||
{ type: 'descendant' },
|
||||
{ type: 'tag', name: 'b' }
|
||||
]
|
||||
```
|
||||
|
||||
Actually, this array is wrapped in another array, but that's another story (involving commas in selectors).
|
||||
|
||||
Now that we know the meaning of every part of the selector, we can compile it. That's where it becomes interesting.
|
||||
|
||||
The basic idea is to turn every part of the selector into a function, which takes an element as its only argument. The function checks whether a passed element matches its part of the selector: If it does, the element is passed to the next turned-into-a-function part of the selector, which does the same. If an element is accepted by all parts of the selector, it _matches_ the selector and double rainbow ALL THE WAY.
|
||||
|
||||
As said before, we want to do right-to-left execution with all the big O improvements nonsense, so elements are passed from the rightmost part of the selector (`b` in our example) to the leftmost (~~which would be `c`~~ of course `a`).
|
||||
|
||||
_//TODO: More in-depth description. Implementation details. Build a spaceship._
|
||||
|
||||
## API
|
||||
|
||||
```js
|
||||
const CSSselect = require("css-select");
|
||||
```
|
||||
|
||||
__Note:__ css-select throws errors when invalid selectors are passed to it, contrary to the behavior in browsers, which swallow them. This is done to aid with writing css selectors, but can be unexpected when processing arbitrary strings.
|
||||
|
||||
#### `CSSselect(query, elems, options)`
|
||||
|
||||
Queries `elems`, returns an array containing all matches.
|
||||
|
||||
- `query` can be either a CSS selector or a function.
|
||||
- `elems` can be either an array of elements, or a single element. If it is an element, its children will be queried.
|
||||
- `options` is described below.
|
||||
|
||||
Aliases: `CSSselect.selectAll(query, elems)`, `CSSselect.iterate(query, elems)`.
|
||||
|
||||
#### `CSSselect.compile(query)`
|
||||
|
||||
Compiles the query, returns a function.
|
||||
|
||||
#### `CSSselect.is(elem, query, options)`
|
||||
|
||||
Tests whether or not an element is matched by `query`. `query` can be either a CSS selector or a function.
|
||||
|
||||
#### `CSSselect.selectOne(query, elems, options)`
|
||||
|
||||
Arguments are the same as for `CSSselect(query, elems)`. Only returns the first match, or `null` if there was no match.
|
||||
|
||||
### Options
|
||||
|
||||
- `xmlMode`: When enabled, tag names will be case-sensitive. Default: `false`.
|
||||
- `strict`: Limits the module to only use CSS3 selectors. Default: `false`.
|
||||
- `rootFunc`: The last function in the stack, will be called with the last element that's looked at. Should return `true`.
|
||||
- `adapter`: The adapter to use when interacting with the backing DOM structure. By default it uses [`domutils`](https://github.com/fb55/domutils).
|
||||
|
||||
#### Custom Adapters
|
||||
|
||||
A custom adapter must implement the following functions:
|
||||
|
||||
```
|
||||
isTag, existsOne, getAttributeValue, getChildren, getName, getParent,
|
||||
getSiblings, getText, hasAttrib, removeSubsets, findAll, findOne
|
||||
```
|
||||
|
||||
The method signature notation used below should be fairly intuitive - if not,
|
||||
see the [`rtype`](https://github.com/ericelliott/rtype) or
|
||||
[`TypeScript`](https://www.typescriptlang.org/) docs, as it is very similar to
|
||||
both of those. You may also want to look at
|
||||
-[`domutils`](https://github.com/fb55/domutils) to see the default
|
||||
-implementation, or at
|
||||
-[`css-select-browser-adapter`](https://github.com/nrkn/css-select-browser-adapter/blob/master/index.js)
|
||||
-for an implementation backed by the DOM.
|
||||
|
||||
```ts
|
||||
{
|
||||
// is the node a tag?
|
||||
isTag: ( node:Node ) => isTag:Boolean,
|
||||
|
||||
// does at least one of passed element nodes pass the test predicate?
|
||||
existsOne: ( test:Predicate, elems:[ElementNode] ) => existsOne:Boolean,
|
||||
|
||||
// get the attribute value
|
||||
getAttributeValue: ( elem:ElementNode, name:String ) => value:String,
|
||||
|
||||
// get the node's children
|
||||
getChildren: ( node:Node ) => children:[Node],
|
||||
|
||||
// get the name of the tag
|
||||
getName: ( elem:ElementNode ) => tagName:String,
|
||||
|
||||
// get the parent of the node
|
||||
getParent: ( node:Node ) => parentNode:Node,
|
||||
|
||||
/*
|
||||
get the siblings of the node. Note that unlike jQuery's `siblings` method,
|
||||
this is expected to include the current node as well
|
||||
*/
|
||||
getSiblings: ( node:Node ) => siblings:[Node],
|
||||
|
||||
// get the text content of the node, and its children if it has any
|
||||
getText: ( node:Node ) => text:String,
|
||||
|
||||
// does the element have the named attribute?
|
||||
hasAttrib: ( elem:ElementNode, name:String ) => hasAttrib:Boolean,
|
||||
|
||||
// takes an array of nodes, and removes any duplicates, as well as any nodes
|
||||
// whose ancestors are also in the array
|
||||
removeSubsets: ( nodes:[Node] ) => unique:[Node],
|
||||
|
||||
// finds all of the element nodes in the array that match the test predicate,
|
||||
// as well as any of their children that match it
|
||||
findAll: ( test:Predicate, nodes:[Node] ) => elems:[ElementNode],
|
||||
|
||||
// finds the first node in the array that matches the test predicate, or one
|
||||
// of its children
|
||||
findOne: ( test:Predicate, elems:[ElementNode] ) => findOne:ElementNode,
|
||||
|
||||
/*
|
||||
The adapter can also optionally include an equals method, if your DOM
|
||||
structure needs a custom equality test to compare two objects which refer
|
||||
to the same underlying node. If not provided, `css-select` will fall back to
|
||||
`a === b`.
|
||||
*/
|
||||
equals: ( a:Node, b:Node ) => Boolean
|
||||
}
|
||||
```
|
||||
|
||||
## Supported selectors
|
||||
|
||||
_As defined by CSS 4 and / or jQuery._
|
||||
|
||||
* Universal (`*`)
|
||||
* Tag (`<tagname>`)
|
||||
* Descendant (` `)
|
||||
* Child (`>`)
|
||||
* Parent (`<`) *
|
||||
* Sibling (`+`)
|
||||
* Adjacent (`~`)
|
||||
* Attribute (`[attr=foo]`), with supported comparisons:
|
||||
* `[attr]` (existential)
|
||||
* `=`
|
||||
* `~=`
|
||||
* `|=`
|
||||
* `*=`
|
||||
* `^=`
|
||||
* `$=`
|
||||
* `!=` *
|
||||
* Also, `i` can be added after the comparison to make the comparison case-insensitive (eg. `[attr=foo i]`) *
|
||||
* Pseudos:
|
||||
* `:not`
|
||||
* `:contains` *
|
||||
* `:icontains` * (case-insensitive version of `:contains`)
|
||||
* `:has` *
|
||||
* `:root`
|
||||
* `:empty`
|
||||
* `:parent` *
|
||||
* `:[first|last]-child[-of-type]`
|
||||
* `:only-of-type`, `:only-child`
|
||||
* `:nth-[last-]child[-of-type]`
|
||||
* `:link`
|
||||
* `:visited`, `:hover`, `:active` * (these depend on optional Adapter methods, so these will work only if implemented in Adapter)
|
||||
* `:selected` *, `:checked`
|
||||
* `:enabled`, `:disabled`
|
||||
* `:required`, `:optional`
|
||||
* `:header`, `:button`, `:input`, `:text`, `:checkbox`, `:file`, `:password`, `:reset`, `:radio` etc. *
|
||||
* `:matches` *
|
||||
|
||||
__*__: Not part of CSS3
|
||||
|
||||
---
|
||||
|
||||
License: BSD-2-Clause
|
||||
|
||||
## Security contact information
|
||||
|
||||
To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security).
|
||||
Tidelift will coordinate the fix and disclosure.
|
||||
|
||||
## `css-select` for enterprise
|
||||
|
||||
Available as part of the Tidelift Subscription
|
||||
|
||||
The maintainers of `css-select` and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. [Learn more.](https://tidelift.com/subscription/pkg/npm-css-select?utm_source=npm-css-select&utm_medium=referral&utm_campaign=enterprise&utm_term=repo)
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
export = CSSselect;
|
||||
|
||||
/**
|
||||
* Alias for CSSselect.selectAll(query, elems, options).
|
||||
* @see [CSSselect.compile] for supported selector queries.
|
||||
*/
|
||||
declare function CSSselect<Node, ElementNode extends Node>(
|
||||
query: CSSselect.Query,
|
||||
elems: Array<ElementNode> | ElementNode,
|
||||
options?: CSSselect.Options<Node, ElementNode>
|
||||
): Array<ElementNode>;
|
||||
|
||||
declare namespace CSSselect {
|
||||
type Predicate<Value> = (v: Value) => boolean;
|
||||
interface Adapter<Node, ElementNode extends Node> {
|
||||
/**
|
||||
* is the node a tag?
|
||||
*/
|
||||
isTag(node: Node): node is ElementNode;
|
||||
|
||||
/**
|
||||
* Does at least one of passed element nodes pass the test predicate?
|
||||
*/
|
||||
existsOne(test: Predicate<ElementNode>, elems: Array<ElementNode>): boolean;
|
||||
|
||||
/**
|
||||
* get the attribute value.
|
||||
*/
|
||||
getAttributeValue(elem: ElementNode, name: string): string;
|
||||
|
||||
/**
|
||||
* get the node's children
|
||||
*/
|
||||
getChildren(node: Node): Array<Node>;
|
||||
|
||||
/**
|
||||
* get the name of the tag
|
||||
*/
|
||||
getName(elem: ElementNode): string;
|
||||
|
||||
/**
|
||||
* get the parent of the node
|
||||
*/
|
||||
getParent(node: Node): Node;
|
||||
|
||||
/*
|
||||
Get the siblings of the node. Note that unlike jQuery's `siblings` method,
|
||||
this is expected to include the current node as well
|
||||
*/
|
||||
getSiblings(node: Node): Array<Node>;
|
||||
|
||||
/*
|
||||
* Get the text content of the node, and its children if it has any.
|
||||
*/
|
||||
getText(node: Node): string;
|
||||
|
||||
/**
|
||||
* Does the element have the named attribute?
|
||||
*/
|
||||
hasAttrib(elem: ElementNode, name: string): boolean;
|
||||
|
||||
/**
|
||||
* takes an array of nodes, and removes any duplicates, as well as any
|
||||
* nodes whose ancestors are also in the array.
|
||||
*/
|
||||
removeSubsets(nodes: Array<Node>): Array<Node>;
|
||||
|
||||
/**
|
||||
* finds all of the element nodes in the array that match the test predicate,
|
||||
* as well as any of their children that match it.
|
||||
*/
|
||||
findAll(test: Predicate<ElementNode>, nodes: Array<Node>): Array<ElementNode>;
|
||||
|
||||
/**
|
||||
* finds the first node in the array that matches the test predicate, or one
|
||||
* of its children.
|
||||
*/
|
||||
findOne(test: Predicate<ElementNode>, elems: Array<ElementNode>): ElementNode | undefined,
|
||||
|
||||
/**
|
||||
The adapter can also optionally include an equals method, if your DOM
|
||||
structure needs a custom equality test to compare two objects which refer
|
||||
to the same underlying node. If not provided, `css-select` will fall back to
|
||||
`a === b`.
|
||||
*/
|
||||
equals?: (a: Node, b: Node) => boolean;
|
||||
|
||||
/**
|
||||
* is the element in hovered state?
|
||||
*/
|
||||
isHovered?: (elem: ElementNode) => boolean;
|
||||
|
||||
/**
|
||||
* is the element in visited state?
|
||||
*/
|
||||
isVisited?: (elem: ElementNode) => boolean;
|
||||
|
||||
/**
|
||||
* is the element in active state?
|
||||
*/
|
||||
isActive?: (elem: ElementNode) => boolean;
|
||||
}
|
||||
|
||||
// TODO default types to the domutil/httpparser2 types
|
||||
interface Options<Node, ElementNode extends Node> {
|
||||
/**
|
||||
* When enabled, tag names will be case-sensitive. Default: false.
|
||||
*/
|
||||
xmlMode?: boolean;
|
||||
/**
|
||||
* Limits the module to only use CSS3 selectors. Default: false.
|
||||
*/
|
||||
strict?: boolean;
|
||||
/**
|
||||
* The last function in the stack, will be called with the last element
|
||||
* that's looked at. Should return true.
|
||||
*/
|
||||
rootFunc?: (element: ElementNode) => true;
|
||||
/**
|
||||
* The adapter to use when interacting with the backing DOM structure. By
|
||||
* default it uses domutils.
|
||||
*/
|
||||
adapter?: Adapter<Node, ElementNode>;
|
||||
}
|
||||
|
||||
type CompiledQuery = (node: any) => boolean;
|
||||
type Query = string | CompiledQuery;
|
||||
|
||||
/**
|
||||
* Compiles the query, returns a function.
|
||||
*
|
||||
* Supported simple selectors:
|
||||
* * Universal (*)
|
||||
* * Tag (<tagname>)
|
||||
* * Attribute ([attr=foo]), with supported comparisons:
|
||||
* * [attr] (existential)
|
||||
* * =
|
||||
* * ~=
|
||||
* * |=
|
||||
* * *=
|
||||
* * ^=
|
||||
* * $=
|
||||
* * !=
|
||||
* * Can be case insensitive (E.g. [attr=foo i])
|
||||
* * Pseudos:
|
||||
* * :not
|
||||
* * :root
|
||||
* * :empty
|
||||
* * :[first|last]-child[-of-type]
|
||||
* * :only-of-type, :only-child
|
||||
* * :nth-[last-]child[-of-type]
|
||||
* * :link, :visited (the latter doesn't match any elements)
|
||||
* * :checked
|
||||
* * :enabled, :disabled
|
||||
* * :required, :optional
|
||||
* * Nonstandard Pseudos (available when strict mode is not enabled):
|
||||
* * `:contains`
|
||||
* * `:icontains` (case-insensitive version of :contains)
|
||||
* * `:has`
|
||||
* * `:parent`
|
||||
* * `:selected`
|
||||
* * `:header, :button, :input, :text, :checkbox, :file, :password, :reset, :radio etc.
|
||||
* * :matches
|
||||
*
|
||||
* Supported Combinators:
|
||||
*
|
||||
* * Descendant (` `)
|
||||
* * Child (`>`)
|
||||
* * Parent (`<`) (when strict mode is not enabled)
|
||||
* * Sibling (`~`)
|
||||
* * Adjacent (`+`)
|
||||
*/
|
||||
function compile(query: string): CompiledQuery;
|
||||
/**
|
||||
* @template Node The generic Node type for the DOM adapter being used.
|
||||
* @template ElementNode The Node type for elements for the DOM adapter being used.
|
||||
* @param elems Elements to query. If it is an element, its children will be queried..
|
||||
* @param query can be either a CSS selector string or a compiled query function.
|
||||
* @param [options] options for querying the document.
|
||||
* @see CSSselect.compile for supported selector queries.
|
||||
* @returns All matching elements.
|
||||
*/
|
||||
function selectAll<Node, ElementNode extends Node>(
|
||||
query: Query,
|
||||
elems: Array<ElementNode> | ElementNode,
|
||||
options?: Options<Node, ElementNode>
|
||||
): Array<ElementNode>;
|
||||
/**
|
||||
* @template Node The generic Node type for the DOM adapter being used.
|
||||
* @template ElementNode The Node type for elements for the DOM adapter being used.
|
||||
* @param elems Elements to query. If it is an element, its children will be queried..
|
||||
* @param query can be either a CSS selector string or a compiled query function.
|
||||
* @param [options] options for querying the document.
|
||||
* @see CSSselect.compile for supported selector queries.
|
||||
* @returns the first match, or null if there was no match.
|
||||
*/
|
||||
function selectOne<Node, ElementNode extends Node>(
|
||||
query: Query,
|
||||
elems: Array<ElementNode> | ElementNode,
|
||||
options?: Options<Node, ElementNode>
|
||||
): ElementNode | null;
|
||||
|
||||
/**
|
||||
* Tests whether or not an element is matched by query.
|
||||
*
|
||||
* @template Node The generic Node type for the DOM adapter being used.
|
||||
* @template ElementNode The Node type for elements for the DOM adapter being used.
|
||||
* @param elem The element to test if it matches the query.
|
||||
* @param query can be either a CSS selector string or a compiled query function.
|
||||
* @param [options] options for querying the document.
|
||||
* @see CSSselect.compile for supported selector queries.
|
||||
* @returns
|
||||
*/
|
||||
function is<Node, ElementNode extends Node>(
|
||||
elem: ElementNode,
|
||||
query: Query,
|
||||
options?: Options<Node, ElementNode>
|
||||
): boolean;
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = CSSselect;
|
||||
|
||||
var DomUtils = require("domutils");
|
||||
var falseFunc = require("boolbase").falseFunc;
|
||||
var compileRaw = require("./lib/compile.js");
|
||||
|
||||
function wrapCompile(func) {
|
||||
return function addAdapter(selector, options, context) {
|
||||
options = options || {};
|
||||
options.adapter = options.adapter || DomUtils;
|
||||
|
||||
return func(selector, options, context);
|
||||
};
|
||||
}
|
||||
|
||||
var compile = wrapCompile(compileRaw);
|
||||
var compileUnsafe = wrapCompile(compileRaw.compileUnsafe);
|
||||
|
||||
function getSelectorFunc(searchFunc) {
|
||||
return function select(query, elems, options) {
|
||||
options = options || {};
|
||||
options.adapter = options.adapter || DomUtils;
|
||||
|
||||
if (typeof query !== "function") {
|
||||
query = compileUnsafe(query, options, elems);
|
||||
}
|
||||
if (query.shouldTestNextSiblings) {
|
||||
elems = appendNextSiblings((options && options.context) || elems, options.adapter);
|
||||
}
|
||||
if (!Array.isArray(elems)) elems = options.adapter.getChildren(elems);
|
||||
else elems = options.adapter.removeSubsets(elems);
|
||||
return searchFunc(query, elems, options);
|
||||
};
|
||||
}
|
||||
|
||||
function getNextSiblings(elem, adapter) {
|
||||
var siblings = adapter.getSiblings(elem);
|
||||
if (!Array.isArray(siblings)) return [];
|
||||
siblings = siblings.slice(0);
|
||||
while (siblings.shift() !== elem);
|
||||
return siblings;
|
||||
}
|
||||
|
||||
function appendNextSiblings(elems, adapter) {
|
||||
// Order matters because jQuery seems to check the children before the siblings
|
||||
if (!Array.isArray(elems)) elems = [elems];
|
||||
var newElems = elems.slice(0);
|
||||
|
||||
for (var i = 0, len = elems.length; i < len; i++) {
|
||||
var nextSiblings = getNextSiblings(newElems[i], adapter);
|
||||
newElems.push.apply(newElems, nextSiblings);
|
||||
}
|
||||
return newElems;
|
||||
}
|
||||
|
||||
var selectAll = getSelectorFunc(function selectAll(query, elems, options) {
|
||||
return query === falseFunc || !elems || elems.length === 0 ? [] : options.adapter.findAll(query, elems);
|
||||
});
|
||||
|
||||
var selectOne = getSelectorFunc(function selectOne(query, elems, options) {
|
||||
return query === falseFunc || !elems || elems.length === 0 ? null : options.adapter.findOne(query, elems);
|
||||
});
|
||||
|
||||
function is(elem, query, options) {
|
||||
options = options || {};
|
||||
options.adapter = options.adapter || DomUtils;
|
||||
return (typeof query === "function" ? query : compile(query, options))(elem);
|
||||
}
|
||||
|
||||
/*
|
||||
the exported interface
|
||||
*/
|
||||
function CSSselect(query, elems, options) {
|
||||
return selectAll(query, elems, options);
|
||||
}
|
||||
|
||||
CSSselect.compile = compile;
|
||||
CSSselect.filters = compileRaw.Pseudos.filters;
|
||||
CSSselect.pseudos = compileRaw.Pseudos.pseudos;
|
||||
|
||||
CSSselect.selectAll = selectAll;
|
||||
CSSselect.selectOne = selectOne;
|
||||
|
||||
CSSselect.is = is;
|
||||
|
||||
//legacy methods (might be removed)
|
||||
CSSselect.parse = compile;
|
||||
CSSselect.iterate = selectAll;
|
||||
|
||||
//hooks
|
||||
CSSselect._compileUnsafe = compileUnsafe;
|
||||
CSSselect._compileToken = compileRaw.compileToken;
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
var falseFunc = require("boolbase").falseFunc;
|
||||
|
||||
//https://github.com/slevithan/XRegExp/blob/master/src/xregexp.js#L469
|
||||
var reChars = /[-[\]{}()*+?.,\\^$|#\s]/g;
|
||||
|
||||
/*
|
||||
attribute selectors
|
||||
*/
|
||||
var attributeRules = {
|
||||
__proto__: null,
|
||||
equals: function(next, data, options) {
|
||||
var name = data.name;
|
||||
var value = data.value;
|
||||
var adapter = options.adapter;
|
||||
|
||||
if (data.ignoreCase) {
|
||||
value = value.toLowerCase();
|
||||
|
||||
return function equalsIC(elem) {
|
||||
var attr = adapter.getAttributeValue(elem, name);
|
||||
return attr != null && attr.toLowerCase() === value && next(elem);
|
||||
};
|
||||
}
|
||||
|
||||
return function equals(elem) {
|
||||
return adapter.getAttributeValue(elem, name) === value && next(elem);
|
||||
};
|
||||
},
|
||||
hyphen: function(next, data, options) {
|
||||
var name = data.name;
|
||||
var value = data.value;
|
||||
var len = value.length;
|
||||
var adapter = options.adapter;
|
||||
|
||||
if (data.ignoreCase) {
|
||||
value = value.toLowerCase();
|
||||
|
||||
return function hyphenIC(elem) {
|
||||
var attr = adapter.getAttributeValue(elem, name);
|
||||
return (
|
||||
attr != null &&
|
||||
(attr.length === len || attr.charAt(len) === "-") &&
|
||||
attr.substr(0, len).toLowerCase() === value &&
|
||||
next(elem)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
return function hyphen(elem) {
|
||||
var attr = adapter.getAttributeValue(elem, name);
|
||||
return (
|
||||
attr != null &&
|
||||
attr.substr(0, len) === value &&
|
||||
(attr.length === len || attr.charAt(len) === "-") &&
|
||||
next(elem)
|
||||
);
|
||||
};
|
||||
},
|
||||
element: function(next, data, options) {
|
||||
var name = data.name;
|
||||
var value = data.value;
|
||||
var adapter = options.adapter;
|
||||
|
||||
if (/\s/.test(value)) {
|
||||
return falseFunc;
|
||||
}
|
||||
|
||||
value = value.replace(reChars, "\\$&");
|
||||
|
||||
var pattern = "(?:^|\\s)" + value + "(?:$|\\s)",
|
||||
flags = data.ignoreCase ? "i" : "",
|
||||
regex = new RegExp(pattern, flags);
|
||||
|
||||
return function element(elem) {
|
||||
var attr = adapter.getAttributeValue(elem, name);
|
||||
return attr != null && regex.test(attr) && next(elem);
|
||||
};
|
||||
},
|
||||
exists: function(next, data, options) {
|
||||
var name = data.name;
|
||||
var adapter = options.adapter;
|
||||
|
||||
return function exists(elem) {
|
||||
return adapter.hasAttrib(elem, name) && next(elem);
|
||||
};
|
||||
},
|
||||
start: function(next, data, options) {
|
||||
var name = data.name;
|
||||
var value = data.value;
|
||||
var len = value.length;
|
||||
var adapter = options.adapter;
|
||||
|
||||
if (len === 0) {
|
||||
return falseFunc;
|
||||
}
|
||||
|
||||
if (data.ignoreCase) {
|
||||
value = value.toLowerCase();
|
||||
|
||||
return function startIC(elem) {
|
||||
var attr = adapter.getAttributeValue(elem, name);
|
||||
return attr != null && attr.substr(0, len).toLowerCase() === value && next(elem);
|
||||
};
|
||||
}
|
||||
|
||||
return function start(elem) {
|
||||
var attr = adapter.getAttributeValue(elem, name);
|
||||
return attr != null && attr.substr(0, len) === value && next(elem);
|
||||
};
|
||||
},
|
||||
end: function(next, data, options) {
|
||||
var name = data.name;
|
||||
var value = data.value;
|
||||
var len = -value.length;
|
||||
var adapter = options.adapter;
|
||||
|
||||
if (len === 0) {
|
||||
return falseFunc;
|
||||
}
|
||||
|
||||
if (data.ignoreCase) {
|
||||
value = value.toLowerCase();
|
||||
|
||||
return function endIC(elem) {
|
||||
var attr = adapter.getAttributeValue(elem, name);
|
||||
return attr != null && attr.substr(len).toLowerCase() === value && next(elem);
|
||||
};
|
||||
}
|
||||
|
||||
return function end(elem) {
|
||||
var attr = adapter.getAttributeValue(elem, name);
|
||||
return attr != null && attr.substr(len) === value && next(elem);
|
||||
};
|
||||
},
|
||||
any: function(next, data, options) {
|
||||
var name = data.name;
|
||||
var value = data.value;
|
||||
var adapter = options.adapter;
|
||||
|
||||
if (value === "") {
|
||||
return falseFunc;
|
||||
}
|
||||
|
||||
if (data.ignoreCase) {
|
||||
var regex = new RegExp(value.replace(reChars, "\\$&"), "i");
|
||||
|
||||
return function anyIC(elem) {
|
||||
var attr = adapter.getAttributeValue(elem, name);
|
||||
return attr != null && regex.test(attr) && next(elem);
|
||||
};
|
||||
}
|
||||
|
||||
return function any(elem) {
|
||||
var attr = adapter.getAttributeValue(elem, name);
|
||||
return attr != null && attr.indexOf(value) >= 0 && next(elem);
|
||||
};
|
||||
},
|
||||
not: function(next, data, options) {
|
||||
var name = data.name;
|
||||
var value = data.value;
|
||||
var adapter = options.adapter;
|
||||
|
||||
if (value === "") {
|
||||
return function notEmpty(elem) {
|
||||
return !!adapter.getAttributeValue(elem, name) && next(elem);
|
||||
};
|
||||
} else if (data.ignoreCase) {
|
||||
value = value.toLowerCase();
|
||||
|
||||
return function notIC(elem) {
|
||||
var attr = adapter.getAttributeValue(elem, name);
|
||||
return attr != null && attr.toLowerCase() !== value && next(elem);
|
||||
};
|
||||
}
|
||||
|
||||
return function not(elem) {
|
||||
return adapter.getAttributeValue(elem, name) !== value && next(elem);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
compile: function(next, data, options) {
|
||||
if (options && options.strict && (data.ignoreCase || data.action === "not")) {
|
||||
throw new Error("Unsupported attribute selector");
|
||||
}
|
||||
return attributeRules[data.action](next, data, options);
|
||||
},
|
||||
rules: attributeRules
|
||||
};
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
compiles a selector to an executable function
|
||||
*/
|
||||
|
||||
module.exports = compile;
|
||||
|
||||
var parse = require("css-what").parse;
|
||||
var BaseFuncs = require("boolbase");
|
||||
var sortRules = require("./sort.js");
|
||||
var procedure = require("./procedure.json");
|
||||
var Rules = require("./general.js");
|
||||
var Pseudos = require("./pseudos.js");
|
||||
var trueFunc = BaseFuncs.trueFunc;
|
||||
var falseFunc = BaseFuncs.falseFunc;
|
||||
|
||||
var filters = Pseudos.filters;
|
||||
|
||||
function compile(selector, options, context) {
|
||||
var next = compileUnsafe(selector, options, context);
|
||||
return wrap(next, options);
|
||||
}
|
||||
|
||||
function wrap(next, options) {
|
||||
var adapter = options.adapter;
|
||||
|
||||
return function base(elem) {
|
||||
return adapter.isTag(elem) && next(elem);
|
||||
};
|
||||
}
|
||||
|
||||
function compileUnsafe(selector, options, context) {
|
||||
var token = parse(selector, options);
|
||||
return compileToken(token, options, context);
|
||||
}
|
||||
|
||||
function includesScopePseudo(t) {
|
||||
return (
|
||||
t.type === "pseudo" &&
|
||||
(t.name === "scope" ||
|
||||
(Array.isArray(t.data) &&
|
||||
t.data.some(function(data) {
|
||||
return data.some(includesScopePseudo);
|
||||
})))
|
||||
);
|
||||
}
|
||||
|
||||
var DESCENDANT_TOKEN = { type: "descendant" };
|
||||
var FLEXIBLE_DESCENDANT_TOKEN = { type: "_flexibleDescendant" };
|
||||
var SCOPE_TOKEN = { type: "pseudo", name: "scope" };
|
||||
var PLACEHOLDER_ELEMENT = {};
|
||||
|
||||
//CSS 4 Spec (Draft): 3.3.1. Absolutizing a Scope-relative Selector
|
||||
//http://www.w3.org/TR/selectors4/#absolutizing
|
||||
function absolutize(token, options, context) {
|
||||
var adapter = options.adapter;
|
||||
|
||||
//TODO better check if context is document
|
||||
var hasContext =
|
||||
!!context &&
|
||||
!!context.length &&
|
||||
context.every(function(e) {
|
||||
return e === PLACEHOLDER_ELEMENT || !!adapter.getParent(e);
|
||||
});
|
||||
|
||||
token.forEach(function(t) {
|
||||
if (t.length > 0 && isTraversal(t[0]) && t[0].type !== "descendant") {
|
||||
//don't return in else branch
|
||||
} else if (hasContext && !(Array.isArray(t) ? t.some(includesScopePseudo) : includesScopePseudo(t))) {
|
||||
t.unshift(DESCENDANT_TOKEN);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
t.unshift(SCOPE_TOKEN);
|
||||
});
|
||||
}
|
||||
|
||||
function compileToken(token, options, context) {
|
||||
token = token.filter(function(t) {
|
||||
return t.length > 0;
|
||||
});
|
||||
|
||||
token.forEach(sortRules);
|
||||
|
||||
var isArrayContext = Array.isArray(context);
|
||||
|
||||
context = (options && options.context) || context;
|
||||
|
||||
if (context && !isArrayContext) context = [context];
|
||||
|
||||
absolutize(token, options, context);
|
||||
|
||||
var shouldTestNextSiblings = false;
|
||||
|
||||
var query = token
|
||||
.map(function(rules) {
|
||||
if (rules[0] && rules[1] && rules[0].name === "scope") {
|
||||
var ruleType = rules[1].type;
|
||||
if (isArrayContext && ruleType === "descendant") {
|
||||
rules[1] = FLEXIBLE_DESCENDANT_TOKEN;
|
||||
} else if (ruleType === "adjacent" || ruleType === "sibling") {
|
||||
shouldTestNextSiblings = true;
|
||||
}
|
||||
}
|
||||
return compileRules(rules, options, context);
|
||||
})
|
||||
.reduce(reduceRules, falseFunc);
|
||||
|
||||
query.shouldTestNextSiblings = shouldTestNextSiblings;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
function isTraversal(t) {
|
||||
return procedure[t.type] < 0;
|
||||
}
|
||||
|
||||
function compileRules(rules, options, context) {
|
||||
return rules.reduce(function(func, rule) {
|
||||
if (func === falseFunc) return func;
|
||||
|
||||
if (!(rule.type in Rules)) {
|
||||
throw new Error("Rule type " + rule.type + " is not supported by css-select");
|
||||
}
|
||||
|
||||
return Rules[rule.type](func, rule, options, context);
|
||||
}, (options && options.rootFunc) || trueFunc);
|
||||
}
|
||||
|
||||
function reduceRules(a, b) {
|
||||
if (b === falseFunc || a === trueFunc) {
|
||||
return a;
|
||||
}
|
||||
if (a === falseFunc || b === trueFunc) {
|
||||
return b;
|
||||
}
|
||||
|
||||
return function combine(elem) {
|
||||
return a(elem) || b(elem);
|
||||
};
|
||||
}
|
||||
|
||||
function containsTraversal(t) {
|
||||
return t.some(isTraversal);
|
||||
}
|
||||
|
||||
//:not, :has and :matches have to compile selectors
|
||||
//doing this in lib/pseudos.js would lead to circular dependencies,
|
||||
//so we add them here
|
||||
filters.not = function(next, token, options, context) {
|
||||
var opts = {
|
||||
xmlMode: !!(options && options.xmlMode),
|
||||
strict: !!(options && options.strict),
|
||||
adapter: options.adapter
|
||||
};
|
||||
|
||||
if (opts.strict) {
|
||||
if (token.length > 1 || token.some(containsTraversal)) {
|
||||
throw new Error("complex selectors in :not aren't allowed in strict mode");
|
||||
}
|
||||
}
|
||||
|
||||
var func = compileToken(token, opts, context);
|
||||
|
||||
if (func === falseFunc) return next;
|
||||
if (func === trueFunc) return falseFunc;
|
||||
|
||||
return function not(elem) {
|
||||
return !func(elem) && next(elem);
|
||||
};
|
||||
};
|
||||
|
||||
filters.has = function(next, token, options) {
|
||||
var adapter = options.adapter;
|
||||
var opts = {
|
||||
xmlMode: !!(options && options.xmlMode),
|
||||
strict: !!(options && options.strict),
|
||||
adapter: adapter
|
||||
};
|
||||
|
||||
//FIXME: Uses an array as a pointer to the current element (side effects)
|
||||
var context = token.some(containsTraversal) ? [PLACEHOLDER_ELEMENT] : null;
|
||||
|
||||
var func = compileToken(token, opts, context);
|
||||
|
||||
if (func === falseFunc) return falseFunc;
|
||||
if (func === trueFunc) {
|
||||
return function hasChild(elem) {
|
||||
return adapter.getChildren(elem).some(adapter.isTag) && next(elem);
|
||||
};
|
||||
}
|
||||
|
||||
func = wrap(func, options);
|
||||
|
||||
if (context) {
|
||||
return function has(elem) {
|
||||
return next(elem) && ((context[0] = elem), adapter.existsOne(func, adapter.getChildren(elem)));
|
||||
};
|
||||
}
|
||||
|
||||
return function has(elem) {
|
||||
return next(elem) && adapter.existsOne(func, adapter.getChildren(elem));
|
||||
};
|
||||
};
|
||||
|
||||
filters.matches = function(next, token, options, context) {
|
||||
var opts = {
|
||||
xmlMode: !!(options && options.xmlMode),
|
||||
strict: !!(options && options.strict),
|
||||
rootFunc: next,
|
||||
adapter: options.adapter
|
||||
};
|
||||
|
||||
return compileToken(token, opts, context);
|
||||
};
|
||||
|
||||
compile.compileToken = compileToken;
|
||||
compile.compileUnsafe = compileUnsafe;
|
||||
compile.Pseudos = Pseudos;
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
var attributes = require("./attributes.js");
|
||||
var Pseudos = require("./pseudos");
|
||||
|
||||
/*
|
||||
all available rules
|
||||
*/
|
||||
module.exports = {
|
||||
__proto__: null,
|
||||
|
||||
attribute: attributes.compile,
|
||||
pseudo: Pseudos.compile,
|
||||
|
||||
//tags
|
||||
tag: function(next, data, options) {
|
||||
var name = data.name;
|
||||
var adapter = options.adapter;
|
||||
|
||||
return function tag(elem) {
|
||||
return adapter.getName(elem) === name && next(elem);
|
||||
};
|
||||
},
|
||||
|
||||
//traversal
|
||||
descendant: function(next, data, options) {
|
||||
// eslint-disable-next-line no-undef
|
||||
var isFalseCache = typeof WeakSet !== "undefined" ? new WeakSet() : null;
|
||||
var adapter = options.adapter;
|
||||
|
||||
return function descendant(elem) {
|
||||
var found = false;
|
||||
|
||||
while (!found && (elem = adapter.getParent(elem))) {
|
||||
if (!isFalseCache || !isFalseCache.has(elem)) {
|
||||
found = next(elem);
|
||||
if (!found && isFalseCache) {
|
||||
isFalseCache.add(elem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
};
|
||||
},
|
||||
_flexibleDescendant: function(next, data, options) {
|
||||
var adapter = options.adapter;
|
||||
|
||||
// Include element itself, only used while querying an array
|
||||
return function descendant(elem) {
|
||||
var found = next(elem);
|
||||
|
||||
while (!found && (elem = adapter.getParent(elem))) {
|
||||
found = next(elem);
|
||||
}
|
||||
|
||||
return found;
|
||||
};
|
||||
},
|
||||
parent: function(next, data, options) {
|
||||
if (options && options.strict) {
|
||||
throw new Error("Parent selector isn't part of CSS3");
|
||||
}
|
||||
|
||||
var adapter = options.adapter;
|
||||
|
||||
return function parent(elem) {
|
||||
return adapter.getChildren(elem).some(test);
|
||||
};
|
||||
|
||||
function test(elem) {
|
||||
return adapter.isTag(elem) && next(elem);
|
||||
}
|
||||
},
|
||||
child: function(next, data, options) {
|
||||
var adapter = options.adapter;
|
||||
|
||||
return function child(elem) {
|
||||
var parent = adapter.getParent(elem);
|
||||
return !!parent && next(parent);
|
||||
};
|
||||
},
|
||||
sibling: function(next, data, options) {
|
||||
var adapter = options.adapter;
|
||||
|
||||
return function sibling(elem) {
|
||||
var siblings = adapter.getSiblings(elem);
|
||||
|
||||
for (var i = 0; i < siblings.length; i++) {
|
||||
if (adapter.isTag(siblings[i])) {
|
||||
if (siblings[i] === elem) break;
|
||||
if (next(siblings[i])) return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
},
|
||||
adjacent: function(next, data, options) {
|
||||
var adapter = options.adapter;
|
||||
|
||||
return function adjacent(elem) {
|
||||
var siblings = adapter.getSiblings(elem),
|
||||
lastElement;
|
||||
|
||||
for (var i = 0; i < siblings.length; i++) {
|
||||
if (adapter.isTag(siblings[i])) {
|
||||
if (siblings[i] === elem) break;
|
||||
lastElement = siblings[i];
|
||||
}
|
||||
}
|
||||
|
||||
return !!lastElement && next(lastElement);
|
||||
};
|
||||
},
|
||||
universal: function(next) {
|
||||
return next;
|
||||
}
|
||||
};
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"universal": 50,
|
||||
"tag": 30,
|
||||
"attribute": 1,
|
||||
"pseudo": 0,
|
||||
"descendant": -1,
|
||||
"child": -1,
|
||||
"parent": -1,
|
||||
"sibling": -1,
|
||||
"adjacent": -1
|
||||
}
|
||||
+453
@@ -0,0 +1,453 @@
|
||||
/*
|
||||
pseudo selectors
|
||||
|
||||
---
|
||||
|
||||
they are available in two forms:
|
||||
* filters called when the selector
|
||||
is compiled and return a function
|
||||
that needs to return next()
|
||||
* pseudos get called on execution
|
||||
they need to return a boolean
|
||||
*/
|
||||
|
||||
var getNCheck = require("nth-check");
|
||||
var BaseFuncs = require("boolbase");
|
||||
var attributes = require("./attributes.js");
|
||||
var trueFunc = BaseFuncs.trueFunc;
|
||||
var falseFunc = BaseFuncs.falseFunc;
|
||||
|
||||
var checkAttrib = attributes.rules.equals;
|
||||
|
||||
function getAttribFunc(name, value) {
|
||||
var data = { name: name, value: value };
|
||||
return function attribFunc(next, rule, options) {
|
||||
return checkAttrib(next, data, options);
|
||||
};
|
||||
}
|
||||
|
||||
function getChildFunc(next, adapter) {
|
||||
return function(elem) {
|
||||
return !!adapter.getParent(elem) && next(elem);
|
||||
};
|
||||
}
|
||||
|
||||
var filters = {
|
||||
contains: function(next, text, options) {
|
||||
var adapter = options.adapter;
|
||||
|
||||
return function contains(elem) {
|
||||
return next(elem) && adapter.getText(elem).indexOf(text) >= 0;
|
||||
};
|
||||
},
|
||||
icontains: function(next, text, options) {
|
||||
var itext = text.toLowerCase();
|
||||
var adapter = options.adapter;
|
||||
|
||||
return function icontains(elem) {
|
||||
return (
|
||||
next(elem) &&
|
||||
adapter
|
||||
.getText(elem)
|
||||
.toLowerCase()
|
||||
.indexOf(itext) >= 0
|
||||
);
|
||||
};
|
||||
},
|
||||
|
||||
//location specific methods
|
||||
"nth-child": function(next, rule, options) {
|
||||
var func = getNCheck(rule);
|
||||
var adapter = options.adapter;
|
||||
|
||||
if (func === falseFunc) return func;
|
||||
if (func === trueFunc) return getChildFunc(next, adapter);
|
||||
|
||||
return function nthChild(elem) {
|
||||
var siblings = adapter.getSiblings(elem);
|
||||
|
||||
for (var i = 0, pos = 0; i < siblings.length; i++) {
|
||||
if (adapter.isTag(siblings[i])) {
|
||||
if (siblings[i] === elem) break;
|
||||
else pos++;
|
||||
}
|
||||
}
|
||||
|
||||
return func(pos) && next(elem);
|
||||
};
|
||||
},
|
||||
"nth-last-child": function(next, rule, options) {
|
||||
var func = getNCheck(rule);
|
||||
var adapter = options.adapter;
|
||||
|
||||
if (func === falseFunc) return func;
|
||||
if (func === trueFunc) return getChildFunc(next, adapter);
|
||||
|
||||
return function nthLastChild(elem) {
|
||||
var siblings = adapter.getSiblings(elem);
|
||||
|
||||
for (var pos = 0, i = siblings.length - 1; i >= 0; i--) {
|
||||
if (adapter.isTag(siblings[i])) {
|
||||
if (siblings[i] === elem) break;
|
||||
else pos++;
|
||||
}
|
||||
}
|
||||
|
||||
return func(pos) && next(elem);
|
||||
};
|
||||
},
|
||||
"nth-of-type": function(next, rule, options) {
|
||||
var func = getNCheck(rule);
|
||||
var adapter = options.adapter;
|
||||
|
||||
if (func === falseFunc) return func;
|
||||
if (func === trueFunc) return getChildFunc(next, adapter);
|
||||
|
||||
return function nthOfType(elem) {
|
||||
var siblings = adapter.getSiblings(elem);
|
||||
|
||||
for (var pos = 0, i = 0; i < siblings.length; i++) {
|
||||
if (adapter.isTag(siblings[i])) {
|
||||
if (siblings[i] === elem) break;
|
||||
if (adapter.getName(siblings[i]) === adapter.getName(elem)) pos++;
|
||||
}
|
||||
}
|
||||
|
||||
return func(pos) && next(elem);
|
||||
};
|
||||
},
|
||||
"nth-last-of-type": function(next, rule, options) {
|
||||
var func = getNCheck(rule);
|
||||
var adapter = options.adapter;
|
||||
|
||||
if (func === falseFunc) return func;
|
||||
if (func === trueFunc) return getChildFunc(next, adapter);
|
||||
|
||||
return function nthLastOfType(elem) {
|
||||
var siblings = adapter.getSiblings(elem);
|
||||
|
||||
for (var pos = 0, i = siblings.length - 1; i >= 0; i--) {
|
||||
if (adapter.isTag(siblings[i])) {
|
||||
if (siblings[i] === elem) break;
|
||||
if (adapter.getName(siblings[i]) === adapter.getName(elem)) pos++;
|
||||
}
|
||||
}
|
||||
|
||||
return func(pos) && next(elem);
|
||||
};
|
||||
},
|
||||
|
||||
//TODO determine the actual root element
|
||||
root: function(next, rule, options) {
|
||||
var adapter = options.adapter;
|
||||
|
||||
return function(elem) {
|
||||
return !adapter.getParent(elem) && next(elem);
|
||||
};
|
||||
},
|
||||
|
||||
scope: function(next, rule, options, context) {
|
||||
var adapter = options.adapter;
|
||||
|
||||
if (!context || context.length === 0) {
|
||||
//equivalent to :root
|
||||
return filters.root(next, rule, options);
|
||||
}
|
||||
|
||||
function equals(a, b) {
|
||||
if (typeof adapter.equals === "function") return adapter.equals(a, b);
|
||||
|
||||
return a === b;
|
||||
}
|
||||
|
||||
if (context.length === 1) {
|
||||
//NOTE: can't be unpacked, as :has uses this for side-effects
|
||||
return function(elem) {
|
||||
return equals(context[0], elem) && next(elem);
|
||||
};
|
||||
}
|
||||
|
||||
return function(elem) {
|
||||
return context.indexOf(elem) >= 0 && next(elem);
|
||||
};
|
||||
},
|
||||
|
||||
//jQuery extensions (others follow as pseudos)
|
||||
checkbox: getAttribFunc("type", "checkbox"),
|
||||
file: getAttribFunc("type", "file"),
|
||||
password: getAttribFunc("type", "password"),
|
||||
radio: getAttribFunc("type", "radio"),
|
||||
reset: getAttribFunc("type", "reset"),
|
||||
image: getAttribFunc("type", "image"),
|
||||
submit: getAttribFunc("type", "submit"),
|
||||
|
||||
//dynamic state pseudos. These depend on optional Adapter methods.
|
||||
hover: function(next, rule, options) {
|
||||
var adapter = options.adapter;
|
||||
|
||||
if (typeof adapter.isHovered === 'function') {
|
||||
return function hover(elem) {
|
||||
return next(elem) && adapter.isHovered(elem);
|
||||
};
|
||||
}
|
||||
|
||||
return falseFunc;
|
||||
},
|
||||
visited: function(next, rule, options) {
|
||||
var adapter = options.adapter;
|
||||
|
||||
if (typeof adapter.isVisited === 'function') {
|
||||
return function visited(elem) {
|
||||
return next(elem) && adapter.isVisited(elem);
|
||||
};
|
||||
}
|
||||
|
||||
return falseFunc;
|
||||
},
|
||||
active: function(next, rule, options) {
|
||||
var adapter = options.adapter;
|
||||
|
||||
if (typeof adapter.isActive === 'function') {
|
||||
return function active(elem) {
|
||||
return next(elem) && adapter.isActive(elem);
|
||||
};
|
||||
}
|
||||
|
||||
return falseFunc;
|
||||
}
|
||||
};
|
||||
|
||||
//helper methods
|
||||
function getFirstElement(elems, adapter) {
|
||||
for (var i = 0; elems && i < elems.length; i++) {
|
||||
if (adapter.isTag(elems[i])) return elems[i];
|
||||
}
|
||||
}
|
||||
|
||||
//while filters are precompiled, pseudos get called when they are needed
|
||||
var pseudos = {
|
||||
empty: function(elem, adapter) {
|
||||
return !adapter.getChildren(elem).some(function(elem) {
|
||||
return adapter.isTag(elem) || elem.type === "text";
|
||||
});
|
||||
},
|
||||
|
||||
"first-child": function(elem, adapter) {
|
||||
return getFirstElement(adapter.getSiblings(elem), adapter) === elem;
|
||||
},
|
||||
"last-child": function(elem, adapter) {
|
||||
var siblings = adapter.getSiblings(elem);
|
||||
|
||||
for (var i = siblings.length - 1; i >= 0; i--) {
|
||||
if (siblings[i] === elem) return true;
|
||||
if (adapter.isTag(siblings[i])) break;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
"first-of-type": function(elem, adapter) {
|
||||
var siblings = adapter.getSiblings(elem);
|
||||
|
||||
for (var i = 0; i < siblings.length; i++) {
|
||||
if (adapter.isTag(siblings[i])) {
|
||||
if (siblings[i] === elem) return true;
|
||||
if (adapter.getName(siblings[i]) === adapter.getName(elem)) break;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
"last-of-type": function(elem, adapter) {
|
||||
var siblings = adapter.getSiblings(elem);
|
||||
|
||||
for (var i = siblings.length - 1; i >= 0; i--) {
|
||||
if (adapter.isTag(siblings[i])) {
|
||||
if (siblings[i] === elem) return true;
|
||||
if (adapter.getName(siblings[i]) === adapter.getName(elem)) break;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
"only-of-type": function(elem, adapter) {
|
||||
var siblings = adapter.getSiblings(elem);
|
||||
|
||||
for (var i = 0, j = siblings.length; i < j; i++) {
|
||||
if (adapter.isTag(siblings[i])) {
|
||||
if (siblings[i] === elem) continue;
|
||||
if (adapter.getName(siblings[i]) === adapter.getName(elem)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
"only-child": function(elem, adapter) {
|
||||
var siblings = adapter.getSiblings(elem);
|
||||
|
||||
for (var i = 0; i < siblings.length; i++) {
|
||||
if (adapter.isTag(siblings[i]) && siblings[i] !== elem) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
//:matches(a, area, link)[href]
|
||||
link: function(elem, adapter) {
|
||||
return adapter.hasAttrib(elem, "href");
|
||||
},
|
||||
//TODO: :any-link once the name is finalized (as an alias of :link)
|
||||
|
||||
//forms
|
||||
//to consider: :target
|
||||
|
||||
//:matches([selected], select:not([multiple]):not(> option[selected]) > option:first-of-type)
|
||||
selected: function(elem, adapter) {
|
||||
if (adapter.hasAttrib(elem, "selected")) return true;
|
||||
else if (adapter.getName(elem) !== "option") return false;
|
||||
|
||||
//the first <option> in a <select> is also selected
|
||||
var parent = adapter.getParent(elem);
|
||||
|
||||
if (!parent || adapter.getName(parent) !== "select" || adapter.hasAttrib(parent, "multiple")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var siblings = adapter.getChildren(parent);
|
||||
var sawElem = false;
|
||||
|
||||
for (var i = 0; i < siblings.length; i++) {
|
||||
if (adapter.isTag(siblings[i])) {
|
||||
if (siblings[i] === elem) {
|
||||
sawElem = true;
|
||||
} else if (!sawElem) {
|
||||
return false;
|
||||
} else if (adapter.hasAttrib(siblings[i], "selected")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sawElem;
|
||||
},
|
||||
//https://html.spec.whatwg.org/multipage/scripting.html#disabled-elements
|
||||
//:matches(
|
||||
// :matches(button, input, select, textarea, menuitem, optgroup, option)[disabled],
|
||||
// optgroup[disabled] > option),
|
||||
// fieldset[disabled] * //TODO not child of first <legend>
|
||||
//)
|
||||
disabled: function(elem, adapter) {
|
||||
return adapter.hasAttrib(elem, "disabled");
|
||||
},
|
||||
enabled: function(elem, adapter) {
|
||||
return !adapter.hasAttrib(elem, "disabled");
|
||||
},
|
||||
//:matches(:matches(:radio, :checkbox)[checked], :selected) (TODO menuitem)
|
||||
checked: function(elem, adapter) {
|
||||
return adapter.hasAttrib(elem, "checked") || pseudos.selected(elem, adapter);
|
||||
},
|
||||
//:matches(input, select, textarea)[required]
|
||||
required: function(elem, adapter) {
|
||||
return adapter.hasAttrib(elem, "required");
|
||||
},
|
||||
//:matches(input, select, textarea):not([required])
|
||||
optional: function(elem, adapter) {
|
||||
return !adapter.hasAttrib(elem, "required");
|
||||
},
|
||||
|
||||
//jQuery extensions
|
||||
|
||||
//:not(:empty)
|
||||
parent: function(elem, adapter) {
|
||||
return !pseudos.empty(elem, adapter);
|
||||
},
|
||||
//:matches(h1, h2, h3, h4, h5, h6)
|
||||
header: namePseudo(["h1", "h2", "h3", "h4", "h5", "h6"]),
|
||||
|
||||
//:matches(button, input[type=button])
|
||||
button: function(elem, adapter) {
|
||||
var name = adapter.getName(elem);
|
||||
return (
|
||||
name === "button" || (name === "input" && adapter.getAttributeValue(elem, "type") === "button")
|
||||
);
|
||||
},
|
||||
//:matches(input, textarea, select, button)
|
||||
input: namePseudo(["input", "textarea", "select", "button"]),
|
||||
//input:matches(:not([type!='']), [type='text' i])
|
||||
text: function(elem, adapter) {
|
||||
var attr;
|
||||
return (
|
||||
adapter.getName(elem) === "input" &&
|
||||
(!(attr = adapter.getAttributeValue(elem, "type")) || attr.toLowerCase() === "text")
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function namePseudo(names) {
|
||||
if (typeof Set !== "undefined") {
|
||||
// eslint-disable-next-line no-undef
|
||||
var nameSet = new Set(names);
|
||||
|
||||
return function(elem, adapter) {
|
||||
return nameSet.has(adapter.getName(elem));
|
||||
};
|
||||
}
|
||||
|
||||
return function(elem, adapter) {
|
||||
return names.indexOf(adapter.getName(elem)) >= 0;
|
||||
};
|
||||
}
|
||||
|
||||
function verifyArgs(func, name, subselect) {
|
||||
if (subselect === null) {
|
||||
if (func.length > 2 && name !== "scope") {
|
||||
throw new Error("pseudo-selector :" + name + " requires an argument");
|
||||
}
|
||||
} else {
|
||||
if (func.length === 2) {
|
||||
throw new Error("pseudo-selector :" + name + " doesn't have any arguments");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//FIXME this feels hacky
|
||||
var re_CSS3 = /^(?:(?:nth|last|first|only)-(?:child|of-type)|root|empty|(?:en|dis)abled|checked|not)$/;
|
||||
|
||||
module.exports = {
|
||||
compile: function(next, data, options, context) {
|
||||
var name = data.name;
|
||||
var subselect = data.data;
|
||||
var adapter = options.adapter;
|
||||
|
||||
if (options && options.strict && !re_CSS3.test(name)) {
|
||||
throw new Error(":" + name + " isn't part of CSS3");
|
||||
}
|
||||
|
||||
if (typeof filters[name] === "function") {
|
||||
return filters[name](next, subselect, options, context);
|
||||
} else if (typeof pseudos[name] === "function") {
|
||||
var func = pseudos[name];
|
||||
|
||||
verifyArgs(func, name, subselect);
|
||||
|
||||
if (func === falseFunc) {
|
||||
return func;
|
||||
}
|
||||
|
||||
if (next === trueFunc) {
|
||||
return function pseudoRoot(elem) {
|
||||
return func(elem, adapter, subselect);
|
||||
};
|
||||
}
|
||||
|
||||
return function pseudoArgs(elem) {
|
||||
return func(elem, adapter, subselect) && next(elem);
|
||||
};
|
||||
} else {
|
||||
throw new Error("unmatched pseudo-class :" + name);
|
||||
}
|
||||
},
|
||||
filters: filters,
|
||||
pseudos: pseudos
|
||||
};
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
module.exports = sortByProcedure;
|
||||
|
||||
/*
|
||||
sort the parts of the passed selector,
|
||||
as there is potential for optimization
|
||||
(some types of selectors are faster than others)
|
||||
*/
|
||||
|
||||
var procedure = require("./procedure.json");
|
||||
|
||||
var attributes = {
|
||||
__proto__: null,
|
||||
exists: 10,
|
||||
equals: 8,
|
||||
not: 7,
|
||||
start: 6,
|
||||
end: 6,
|
||||
any: 5,
|
||||
hyphen: 4,
|
||||
element: 4
|
||||
};
|
||||
|
||||
function sortByProcedure(arr) {
|
||||
var procs = arr.map(getProcedure);
|
||||
for (var i = 1; i < arr.length; i++) {
|
||||
var procNew = procs[i];
|
||||
|
||||
if (procNew < 0) continue;
|
||||
|
||||
for (var j = i - 1; j >= 0 && procNew < procs[j]; j--) {
|
||||
var token = arr[j + 1];
|
||||
arr[j + 1] = arr[j];
|
||||
arr[j] = token;
|
||||
procs[j + 1] = procs[j];
|
||||
procs[j] = procNew;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getProcedure(token) {
|
||||
var proc = procedure[token.type];
|
||||
|
||||
if (proc === procedure.attribute) {
|
||||
proc = attributes[token.action];
|
||||
|
||||
if (proc === attributes.equals && token.name === "id") {
|
||||
//prefer ID selectors (eg. #ID)
|
||||
proc = 9;
|
||||
}
|
||||
|
||||
if (token.ignoreCase) {
|
||||
//ignoreCase adds some overhead, prefer "normal" token
|
||||
//this is a binary operation, to ensure it's still an int
|
||||
proc >>= 1;
|
||||
}
|
||||
} else if (proc === procedure.pseudo) {
|
||||
if (!token.data) {
|
||||
proc = 3;
|
||||
} else if (token.name === "has" || token.name === "contains") {
|
||||
proc = 0; //expensive in any case
|
||||
} else if (token.name === "matches" || token.name === "not") {
|
||||
proc = 0;
|
||||
for (var i = 0; i < token.data.length; i++) {
|
||||
//TODO better handling of complex selectors
|
||||
if (token.data[i].length !== 1) continue;
|
||||
var cur = getProcedure(token.data[i][0]);
|
||||
//avoid executing :has or :contains
|
||||
if (cur === 0) {
|
||||
proc = 0;
|
||||
break;
|
||||
}
|
||||
if (cur > proc) proc = cur;
|
||||
}
|
||||
if (token.data.length > 1 && proc > 0) proc -= 1;
|
||||
} else {
|
||||
proc = 1;
|
||||
}
|
||||
}
|
||||
return proc;
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "css-select",
|
||||
"version": "2.1.0",
|
||||
"description": "a CSS selector compiler/engine",
|
||||
"author": "Felix Boehm <me@feedic.com>",
|
||||
"keywords": [
|
||||
"css",
|
||||
"selector",
|
||||
"sizzle"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/fb55/css-select.git"
|
||||
},
|
||||
"files": [
|
||||
"index.js",
|
||||
"index.d.ts",
|
||||
"lib"
|
||||
],
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0",
|
||||
"css-what": "^3.2.1",
|
||||
"domutils": "^1.7.0",
|
||||
"nth-check": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cheerio-soupselect": "^0.1.1",
|
||||
"coveralls": "^3.0.2",
|
||||
"eslint": "^6.0.0",
|
||||
"expect.js": "^0.3.1",
|
||||
"htmlparser2": "^4.0.0",
|
||||
"istanbul": "^0.4.5",
|
||||
"mocha": "^6.0.0",
|
||||
"mocha-lcov-reporter": "^1.3.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "mocha && npm run lint",
|
||||
"lint": "eslint index.js lib/*.js test/*.js",
|
||||
"lcov": "istanbul cover _mocha --report lcovonly -- -R spec",
|
||||
"coveralls": "npm run lint && npm run lcov && (cat coverage/lcov.info | coveralls || exit 0)"
|
||||
},
|
||||
"license": "BSD-2-Clause",
|
||||
"types": "index.d.ts",
|
||||
"prettier": {
|
||||
"tabWidth": 4
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
Copyright (c) Felix Böhm
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
|
||||
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
|
||||
THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS,
|
||||
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export * from "./parse";
|
||||
export { default as parse } from "./parse";
|
||||
export { default as stringify } from "./stringify";
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,aAAa,CAAC"}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
||||
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
||||
};
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.stringify = exports.parse = void 0;
|
||||
__exportStar(require("./parse"), exports);
|
||||
var parse_1 = require("./parse");
|
||||
Object.defineProperty(exports, "parse", { enumerable: true, get: function () { return __importDefault(parse_1).default; } });
|
||||
var stringify_1 = require("./stringify");
|
||||
Object.defineProperty(exports, "stringify", { enumerable: true, get: function () { return __importDefault(stringify_1).default; } });
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
export default parse;
|
||||
export interface Options {
|
||||
lowerCaseAttributeNames?: boolean;
|
||||
lowerCaseTags?: boolean;
|
||||
xmlMode?: boolean;
|
||||
}
|
||||
export declare type Selector = PseudoSelector | PseudoElement | AttributeSelector | TagSelector | UniversalSelector | Traversal;
|
||||
export interface AttributeSelector {
|
||||
type: "attribute";
|
||||
name: string;
|
||||
action: AttributeAction;
|
||||
value: string;
|
||||
ignoreCase: boolean;
|
||||
}
|
||||
declare type DataType = Selector[][] | null | string;
|
||||
export interface PseudoSelector {
|
||||
type: "pseudo";
|
||||
name: string;
|
||||
data: DataType;
|
||||
}
|
||||
export interface PseudoElement {
|
||||
type: "pseudo-element";
|
||||
name: string;
|
||||
}
|
||||
export interface TagSelector {
|
||||
type: "tag";
|
||||
name: string;
|
||||
}
|
||||
export interface UniversalSelector {
|
||||
type: "universal";
|
||||
}
|
||||
export interface Traversal {
|
||||
type: TraversalType;
|
||||
}
|
||||
export declare type AttributeAction = "any" | "element" | "end" | "equals" | "exists" | "hyphen" | "not" | "start";
|
||||
export declare type TraversalType = "adjacent" | "child" | "descendant" | "parent" | "sibling";
|
||||
declare function parse(selector: string, options?: Options): Selector[][];
|
||||
//# sourceMappingURL=parse.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"AAEA,eAAe,KAAK,CAAC;AAErB,MAAM,WAAW,OAAO;IACpB,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,OAAO,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,oBAAY,QAAQ,GACd,cAAc,GACd,aAAa,GACb,iBAAiB,GACjB,WAAW,GACX,iBAAiB,GACjB,SAAS,CAAC;AAEhB,MAAM,WAAW,iBAAiB;IAC9B,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,eAAe,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,OAAO,CAAC;CACvB;AAED,aAAK,QAAQ,GAAG,QAAQ,EAAE,EAAE,GAAG,IAAI,GAAG,MAAM,CAAC;AAE7C,MAAM,WAAW,cAAc;IAC3B,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,QAAQ,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC1B,IAAI,EAAE,gBAAgB,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,WAAW;IACxB,IAAI,EAAE,KAAK,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,iBAAiB;IAC9B,IAAI,EAAE,WAAW,CAAC;CACrB;AAED,MAAM,WAAW,SAAS;IACtB,IAAI,EAAE,aAAa,CAAC;CACvB;AAED,oBAAY,eAAe,GACrB,KAAK,GACL,SAAS,GACT,KAAK,GACL,QAAQ,GACR,QAAQ,GACR,QAAQ,GACR,KAAK,GACL,OAAO,CAAC;AAEd,oBAAY,aAAa,GACnB,UAAU,GACV,OAAO,GACP,YAAY,GACZ,QAAQ,GACR,SAAS,CAAC;AAkEhB,iBAAS,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,QAAQ,EAAE,EAAE,CAUhE"}
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.default = parse;
|
||||
var reName = /^[^\\]?(?:\\(?:[\da-f]{1,6}\s?|.)|[\w\-\u00b0-\uFFFF])+/;
|
||||
var reEscape = /\\([\da-f]{1,6}\s?|(\s)|.)/gi;
|
||||
// Modified version of https://github.com/jquery/sizzle/blob/master/src/sizzle.js#L87
|
||||
var reAttr = /^\s*((?:\\.|[\w\u00b0-\uFFFF-])+)\s*(?:(\S?)=\s*(?:(['"])((?:[^\\]|\\[^])*?)\3|(#?(?:\\.|[\w\u00b0-\uFFFF-])*)|)|)\s*(i)?\]/;
|
||||
var actionTypes = {
|
||||
undefined: "exists",
|
||||
"": "equals",
|
||||
"~": "element",
|
||||
"^": "start",
|
||||
$: "end",
|
||||
"*": "any",
|
||||
"!": "not",
|
||||
"|": "hyphen",
|
||||
};
|
||||
var Traversals = {
|
||||
">": "child",
|
||||
"<": "parent",
|
||||
"~": "sibling",
|
||||
"+": "adjacent",
|
||||
};
|
||||
var attribSelectors = {
|
||||
"#": ["id", "equals"],
|
||||
".": ["class", "element"],
|
||||
};
|
||||
// Pseudos, whose data property is parsed as well.
|
||||
var unpackPseudos = new Set([
|
||||
"has",
|
||||
"not",
|
||||
"matches",
|
||||
"is",
|
||||
"host",
|
||||
"host-context",
|
||||
]);
|
||||
var stripQuotesFromPseudos = new Set(["contains", "icontains"]);
|
||||
var quotes = new Set(['"', "'"]);
|
||||
// Unescape function taken from https://github.com/jquery/sizzle/blob/master/src/sizzle.js#L152
|
||||
function funescape(_, escaped, escapedWhitespace) {
|
||||
var high = parseInt(escaped, 16) - 0x10000;
|
||||
// NaN means non-codepoint
|
||||
return high !== high || escapedWhitespace
|
||||
? escaped
|
||||
: high < 0
|
||||
? // BMP codepoint
|
||||
String.fromCharCode(high + 0x10000)
|
||||
: // Supplemental Plane codepoint (surrogate pair)
|
||||
String.fromCharCode((high >> 10) | 0xd800, (high & 0x3ff) | 0xdc00);
|
||||
}
|
||||
function unescapeCSS(str) {
|
||||
return str.replace(reEscape, funescape);
|
||||
}
|
||||
function isWhitespace(c) {
|
||||
return c === " " || c === "\n" || c === "\t" || c === "\f" || c === "\r";
|
||||
}
|
||||
function parse(selector, options) {
|
||||
var subselects = [];
|
||||
selector = parseSelector(subselects, "" + selector, options);
|
||||
if (selector !== "") {
|
||||
throw new Error("Unmatched selector: " + selector);
|
||||
}
|
||||
return subselects;
|
||||
}
|
||||
function parseSelector(subselects, selector, options) {
|
||||
var _a, _b;
|
||||
if (options === void 0) { options = {}; }
|
||||
var tokens = [];
|
||||
var sawWS = false;
|
||||
function getName() {
|
||||
var match = selector.match(reName);
|
||||
if (!match) {
|
||||
throw new Error("Expected name, found " + selector);
|
||||
}
|
||||
var sub = match[0];
|
||||
selector = selector.substr(sub.length);
|
||||
return unescapeCSS(sub);
|
||||
}
|
||||
function stripWhitespace(start) {
|
||||
while (isWhitespace(selector.charAt(start)))
|
||||
start++;
|
||||
selector = selector.substr(start);
|
||||
}
|
||||
function isEscaped(pos) {
|
||||
var slashCount = 0;
|
||||
while (selector.charAt(--pos) === "\\")
|
||||
slashCount++;
|
||||
return (slashCount & 1) === 1;
|
||||
}
|
||||
stripWhitespace(0);
|
||||
while (selector !== "") {
|
||||
var firstChar = selector.charAt(0);
|
||||
if (isWhitespace(firstChar)) {
|
||||
sawWS = true;
|
||||
stripWhitespace(1);
|
||||
}
|
||||
else if (firstChar in Traversals) {
|
||||
tokens.push({ type: Traversals[firstChar] });
|
||||
sawWS = false;
|
||||
stripWhitespace(1);
|
||||
}
|
||||
else if (firstChar === ",") {
|
||||
if (tokens.length === 0) {
|
||||
throw new Error("Empty sub-selector");
|
||||
}
|
||||
subselects.push(tokens);
|
||||
tokens = [];
|
||||
sawWS = false;
|
||||
stripWhitespace(1);
|
||||
}
|
||||
else {
|
||||
if (sawWS) {
|
||||
if (tokens.length > 0) {
|
||||
tokens.push({ type: "descendant" });
|
||||
}
|
||||
sawWS = false;
|
||||
}
|
||||
if (firstChar === "*") {
|
||||
selector = selector.substr(1);
|
||||
tokens.push({ type: "universal" });
|
||||
}
|
||||
else if (firstChar in attribSelectors) {
|
||||
var _c = attribSelectors[firstChar], name_1 = _c[0], action = _c[1];
|
||||
selector = selector.substr(1);
|
||||
tokens.push({
|
||||
type: "attribute",
|
||||
name: name_1,
|
||||
action: action,
|
||||
value: getName(),
|
||||
ignoreCase: false,
|
||||
});
|
||||
}
|
||||
else if (firstChar === "[") {
|
||||
selector = selector.substr(1);
|
||||
var attributeMatch = selector.match(reAttr);
|
||||
if (!attributeMatch) {
|
||||
throw new Error("Malformed attribute selector: " + selector);
|
||||
}
|
||||
var completeSelector = attributeMatch[0], baseName = attributeMatch[1], actionType = attributeMatch[2], _d = attributeMatch[4], quotedValue = _d === void 0 ? "" : _d, _e = attributeMatch[5], value = _e === void 0 ? quotedValue : _e, ignoreCase = attributeMatch[6];
|
||||
selector = selector.substr(completeSelector.length);
|
||||
var name_2 = unescapeCSS(baseName);
|
||||
if ((_a = options.lowerCaseAttributeNames) !== null && _a !== void 0 ? _a : !options.xmlMode) {
|
||||
name_2 = name_2.toLowerCase();
|
||||
}
|
||||
tokens.push({
|
||||
type: "attribute",
|
||||
name: name_2,
|
||||
action: actionTypes[actionType],
|
||||
value: unescapeCSS(value),
|
||||
ignoreCase: !!ignoreCase,
|
||||
});
|
||||
}
|
||||
else if (firstChar === ":") {
|
||||
if (selector.charAt(1) === ":") {
|
||||
selector = selector.substr(2);
|
||||
tokens.push({
|
||||
type: "pseudo-element",
|
||||
name: getName().toLowerCase(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
selector = selector.substr(1);
|
||||
var name_3 = getName().toLowerCase();
|
||||
var data = null;
|
||||
if (selector.startsWith("(")) {
|
||||
if (unpackPseudos.has(name_3)) {
|
||||
var quot = selector.charAt(1);
|
||||
var quoted = quotes.has(quot);
|
||||
selector = selector.substr(quoted ? 2 : 1);
|
||||
data = [];
|
||||
selector = parseSelector(data, selector, options);
|
||||
if (quoted) {
|
||||
if (!selector.startsWith(quot)) {
|
||||
throw new Error("Unmatched quotes in :" + name_3);
|
||||
}
|
||||
else {
|
||||
selector = selector.substr(1);
|
||||
}
|
||||
}
|
||||
if (!selector.startsWith(")")) {
|
||||
throw new Error("Missing closing parenthesis in :" + name_3 + " (" + selector + ")");
|
||||
}
|
||||
selector = selector.substr(1);
|
||||
}
|
||||
else {
|
||||
var pos = 1;
|
||||
var counter = 1;
|
||||
for (; counter > 0 && pos < selector.length; pos++) {
|
||||
if (selector.charAt(pos) === "(" &&
|
||||
!isEscaped(pos)) {
|
||||
counter++;
|
||||
}
|
||||
else if (selector.charAt(pos) === ")" &&
|
||||
!isEscaped(pos)) {
|
||||
counter--;
|
||||
}
|
||||
}
|
||||
if (counter) {
|
||||
throw new Error("Parenthesis not matched");
|
||||
}
|
||||
data = selector.substr(1, pos - 2);
|
||||
selector = selector.substr(pos);
|
||||
if (stripQuotesFromPseudos.has(name_3)) {
|
||||
var quot = data.charAt(0);
|
||||
if (quot === data.slice(-1) && quotes.has(quot)) {
|
||||
data = data.slice(1, -1);
|
||||
}
|
||||
data = unescapeCSS(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
tokens.push({ type: "pseudo", name: name_3, data: data });
|
||||
}
|
||||
else if (reName.test(selector)) {
|
||||
var name_4 = getName();
|
||||
if ((_b = options.lowerCaseTags) !== null && _b !== void 0 ? _b : !options.xmlMode) {
|
||||
name_4 = name_4.toLowerCase();
|
||||
}
|
||||
tokens.push({ type: "tag", name: name_4 });
|
||||
}
|
||||
else {
|
||||
if (tokens.length &&
|
||||
tokens[tokens.length - 1].type === "descendant") {
|
||||
tokens.pop();
|
||||
}
|
||||
addToken(subselects, tokens);
|
||||
return selector;
|
||||
}
|
||||
}
|
||||
}
|
||||
addToken(subselects, tokens);
|
||||
return selector;
|
||||
}
|
||||
function addToken(subselects, tokens) {
|
||||
if (subselects.length > 0 && tokens.length === 0) {
|
||||
throw new Error("Empty sub-selector");
|
||||
}
|
||||
subselects.push(tokens);
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
import { Selector } from "./parse";
|
||||
export default function stringify(token: Selector[][]): string;
|
||||
//# sourceMappingURL=stringify.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"stringify.d.ts","sourceRoot":"","sources":["../src/stringify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAuBnC,MAAM,CAAC,OAAO,UAAU,SAAS,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,GAAG,MAAM,CAE7D"}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
"use strict";
|
||||
var __spreadArrays = (this && this.__spreadArrays) || function () {
|
||||
for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;
|
||||
for (var r = Array(s), k = 0, i = 0; i < il; i++)
|
||||
for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)
|
||||
r[k] = a[j];
|
||||
return r;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var actionTypes = {
|
||||
equals: "",
|
||||
element: "~",
|
||||
start: "^",
|
||||
end: "$",
|
||||
any: "*",
|
||||
not: "!",
|
||||
hyphen: "|",
|
||||
};
|
||||
var charsToEscape = new Set(__spreadArrays(Object.keys(actionTypes)
|
||||
.map(function (typeKey) { return actionTypes[typeKey]; })
|
||||
.filter(Boolean), [
|
||||
":",
|
||||
"[",
|
||||
"]",
|
||||
" ",
|
||||
"\\",
|
||||
]));
|
||||
function stringify(token) {
|
||||
return token.map(stringifySubselector).join(", ");
|
||||
}
|
||||
exports.default = stringify;
|
||||
function stringifySubselector(token) {
|
||||
return token.map(stringifyToken).join("");
|
||||
}
|
||||
function stringifyToken(token) {
|
||||
switch (token.type) {
|
||||
// Simple types
|
||||
case "child":
|
||||
return " > ";
|
||||
case "parent":
|
||||
return " < ";
|
||||
case "sibling":
|
||||
return " ~ ";
|
||||
case "adjacent":
|
||||
return " + ";
|
||||
case "descendant":
|
||||
return " ";
|
||||
case "universal":
|
||||
return "*";
|
||||
case "tag":
|
||||
return escapeName(token.name);
|
||||
case "pseudo-element":
|
||||
return "::" + escapeName(token.name);
|
||||
case "pseudo":
|
||||
if (token.data === null)
|
||||
return ":" + escapeName(token.name);
|
||||
if (typeof token.data === "string") {
|
||||
return ":" + escapeName(token.name) + "(" + token.data + ")";
|
||||
}
|
||||
return ":" + escapeName(token.name) + "(" + stringify(token.data) + ")";
|
||||
case "attribute":
|
||||
if (token.action === "exists") {
|
||||
return "[" + escapeName(token.name) + "]";
|
||||
}
|
||||
if (token.name === "id" &&
|
||||
token.action === "equals" &&
|
||||
!token.ignoreCase) {
|
||||
return "#" + escapeName(token.value);
|
||||
}
|
||||
if (token.name === "class" &&
|
||||
token.action === "element" &&
|
||||
!token.ignoreCase) {
|
||||
return "." + escapeName(token.value);
|
||||
}
|
||||
return "[" + escapeName(token.name) + actionTypes[token.action] + "='" + escapeName(token.value) + "'" + (token.ignoreCase ? "i" : "") + "]";
|
||||
}
|
||||
}
|
||||
function escapeName(str) {
|
||||
return str
|
||||
.split("")
|
||||
.map(function (c) { return (charsToEscape.has(c) ? "\\" + c : c); })
|
||||
.join("");
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"author": "Felix Böhm <me@feedic.com> (http://feedic.com)",
|
||||
"name": "css-what",
|
||||
"description": "a CSS selector parser",
|
||||
"version": "3.4.2",
|
||||
"funding": "https://github.com/sponsors/fb55",
|
||||
"repository": {
|
||||
"url": "https://github.com/fb55/css-what"
|
||||
},
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"files": [
|
||||
"lib/**/*"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "jest --coverage -u && npm run lint",
|
||||
"coverage": "cat coverage/lcov.info | coveralls",
|
||||
"lint": "npm run lint:es && npm run lint:prettier",
|
||||
"lint:es": "eslint src",
|
||||
"lint:prettier": "npm run prettier -- --check",
|
||||
"format": "npm run format:es && npm run format:prettier",
|
||||
"format:es": "npm run lint:es -- --fix",
|
||||
"format:prettier": "npm run prettier -- --write",
|
||||
"prettier": "prettier '**/*.{ts,md,json,yml}'",
|
||||
"build": "tsc",
|
||||
"prepare": "npm run build"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^26.0.3",
|
||||
"@types/node": "^14.0.5",
|
||||
"@typescript-eslint/eslint-plugin": "^4.1.0",
|
||||
"@typescript-eslint/parser": "^4.1.0",
|
||||
"coveralls": "^3.0.5",
|
||||
"eslint": "^7.0.0",
|
||||
"eslint-config-prettier": "^6.0.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"jest": "^26.0.1",
|
||||
"prettier": "^2.0.5",
|
||||
"ts-jest": "^26.0.0",
|
||||
"typescript": "^4.0.2"
|
||||
},
|
||||
"optionalDependencies": {},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
},
|
||||
"license": "BSD-2-Clause",
|
||||
"jest": {
|
||||
"preset": "ts-jest",
|
||||
"testEnvironment": "node"
|
||||
},
|
||||
"prettier": {
|
||||
"tabWidth": 4
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
# css-what [](http://travis-ci.org/fb55/css-what)
|
||||
|
||||
a CSS selector parser
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
const CSSwhat = require("css-what")
|
||||
CSSwhat.parse("foo[bar]:baz")
|
||||
|
||||
~> [
|
||||
[
|
||||
{ type: "tag", name: "foo" },
|
||||
{
|
||||
type: "attribute",
|
||||
name: "bar",
|
||||
action: "exists",
|
||||
value: "",
|
||||
ignoreCase: false
|
||||
},
|
||||
{ type: "pseudo", name: "baz", data: null }
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
**`CSSwhat.parse(str, options)` - Parses `str`, optionally with the passed `options`.**
|
||||
|
||||
The function returns a two-dimensional array. The first array represents selectors separated by commas (eg. `sub1, sub2`), the second contains the relevant tokens for that selector. Possible token types are:
|
||||
|
||||
| name | attributes | example | output |
|
||||
| ---------------- | --------------------------------------- | ------------- | ---------------------------------------------------------------------------------------- |
|
||||
| `tag` | `name` | `div` | `{ type: 'tag', name: 'div' }` |
|
||||
| `universal` | - | `*` | `{ type: 'universal' }` |
|
||||
| `pseudo` | `name`, `data` | `:name(data)` | `{ type: 'pseudo', name: 'name', data: 'data' }` |
|
||||
| `pseudo` | `name`, `data` | `:name` | `{ type: 'pseudo', name: 'name', data: null }` |
|
||||
| `pseudo-element` | `name` | `::name` | `{ type: 'pseudo-element', name: 'name' }` |
|
||||
| `attribute` | `name`, `action`, `value`, `ignoreCase` | `[attr]` | `{ type: 'attribute', name: 'attr', action: 'exists', value: '', ignoreCase: false }` |
|
||||
| `attribute` | `name`, `action`, `value`, `ignoreCase` | `[attr=val]` | `{ type: 'attribute', name: 'attr', action: 'equals', value: 'val', ignoreCase: false }` |
|
||||
| `attribute` | `name`, `action`, `value`, `ignoreCase` | `[attr^=val]` | `{ type: 'attribute', name: 'attr', action: 'start', value: 'val', ignoreCase: false }` |
|
||||
| `attribute` | `name`, `action`, `value`, `ignoreCase` | `[attr$=val]` | `{ type: 'attribute', name: 'attr', action: 'end', value: 'val', ignoreCase: false }` |
|
||||
| `child` | - | `>` | `{ type: 'child' }` |
|
||||
| `parent` | - | `<` | `{ type: 'parent' }` |
|
||||
| `sibling` | - | `~` | `{ type: 'sibling' }` |
|
||||
| `adjacent` | - | `+` | `{ type: 'adjacent' }` |
|
||||
| `descendant` | - | | `{ type: 'descendant' }` |
|
||||
|
||||
**Options:**
|
||||
|
||||
- `lowerCaseTags`: When false, tag names will not be lowercased. Defaults to `true`.
|
||||
- `lowerCaseAttributeNames`: When false, attribute names will not be lowercased. Defaults to `true`.
|
||||
- `xmlMode`: When `true`, `xmlMode` implies both `lowerCaseTags` and `lowerCaseAttributeNames` are set to `false`.
|
||||
|
||||
**`CSSwhat.stringify(selector)` - Turns `selector` back into a string.**
|
||||
|
||||
---
|
||||
|
||||
License: BSD-2-Clause
|
||||
|
||||
## Security contact information
|
||||
|
||||
To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security).
|
||||
Tidelift will coordinate the fix and disclosure.
|
||||
|
||||
## `css-what` for enterprise
|
||||
|
||||
Available as part of the Tidelift Subscription
|
||||
|
||||
The maintainers of `css-what` and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. [Learn more.](https://tidelift.com/subscription/pkg/npm-css-what?utm_source=npm-css-what&utm_medium=referral&utm_campaign=enterprise&utm_term=repo)
|
||||
+421
@@ -0,0 +1,421 @@
|
||||
## 4.2.0 (November 26, 2020)
|
||||
|
||||
- Trim Custom Property values when possible (#393)
|
||||
- Fixed removing unit for zero-length dimentions in `min()`, `max()` and `clamp()` functions (#426)
|
||||
- Fixed crash on bad value in TRBL declaration value (#412)
|
||||
|
||||
## 4.1.1 (November 15, 2020)
|
||||
|
||||
- Fixed build setup to exclude full `mdn/data` that reduced the lib size:
|
||||
* dist/csso.js: 794.5Kb -> 255.2Kb
|
||||
* dist/csso.min.js: 394.4Kb -> 194.2Kb
|
||||
* package size: 237.8 kB -> 156.1 kB
|
||||
* package unpacked size: 1.3 MB -> 586.8 kB
|
||||
|
||||
## 4.1.0 (October 27, 2020)
|
||||
|
||||
- Bumped [CSSTree](https://github.com/csstree/csstree) to `^1.0.0`
|
||||
- Fixed wrongly merging of TRBL values when one of them contains `var()` (#420)
|
||||
- Fixed wrongly merging of pseudo class and element with the same name, e.g. `:-ms-input-placeholder` and `::-ms-input-placeholder` (#383, #416)
|
||||
- Fixed wrongly merging of `overflow` fallback (#415)
|
||||
|
||||
## 4.0.3 (March 24, 2020)
|
||||
|
||||
- Prevented percent sign removal in `flex`/`-ms-flex` (#410)
|
||||
- Fixed restructuring optimisation in some cases (@charlessuh & @chsuh, #358, #411)
|
||||
- Bumped dependencies (@AviVahl, #409)
|
||||
|
||||
## 4.0.2 (October 28, 2019)
|
||||
|
||||
- Fixed clean stage to avoid exceptions when source has unparsed or bad parts (#380)
|
||||
- Fixed wrong percentage sign removal for zero values (#395)
|
||||
|
||||
## 4.0.1 (October 22, 2019)
|
||||
|
||||
- Bumped CSSTree to [`1.0.0-alpha.37`](https://github.com/csstree/csstree/releases/tag/v1.0.0-alpha.37) to avoid source map generation inconsistency across Node.js versions
|
||||
|
||||
## 4.0.0 (October 21, 2019)
|
||||
|
||||
- Dropped support for Node.js < 8
|
||||
- Refreshed dev dependencies and scripts
|
||||
- Bumped [CSSTree](https://github.com/csstree/csstree) to `1.0.0-alpha.36` (#399)
|
||||
- Changed bundle files: `dist/csso.js` and `dist/csso.min.js` instead single `dist/csso-browser.js` (min version)
|
||||
- Expose `compress()` as `syntax.compress()`
|
||||
|
||||
## 3.5.1 (June 7, 2018)
|
||||
|
||||
- Bumped [CSSTree](https://github.com/csstree/csstree) to `1.0.0-alpha.29` (fixes some issues)
|
||||
|
||||
## 3.5.0 (January 14, 2018)
|
||||
|
||||
- Migrated to [CSSTree](https://github.com/csstree/csstree) `1.0.0-alpha.27`
|
||||
|
||||
## 3.4.0 (November 3, 2017)
|
||||
|
||||
- Added percent sign removal for zero percentages for some properties that is safe (@RubaXa, #286)
|
||||
- Removed unit removal for zero values in `-ms-flex` due it breaks flex in IE10/11 (#362)
|
||||
- Improved performance of selectors comparison (@smelukov, #343)
|
||||
|
||||
## 3.3.1 (October 17, 2017)
|
||||
|
||||
- Fixed merge of `position` declarations when `sticky` fallback is using (@gruzzilkin, #356)
|
||||
|
||||
## 3.3.0 (October 12, 2017)
|
||||
|
||||
- Migrated to [CSSTree](https://github.com/csstree/csstree) `1.0.0-alpha25`
|
||||
- Changed AST format (see [CSSTree change log](https://github.com/csstree/csstree/blob/master/HISTORY.md) for details)
|
||||
- Fixed performance issue when generate CSS with source map (quadratic increase in time depending on the size of the CSS)
|
||||
|
||||
## 3.2.0 (September 10, 2017)
|
||||
|
||||
- Fixed named color compression to apply only when an identifier is guaranteed to be a color
|
||||
- Added lifting of `@keyframes` to the beginning of style sheet (chunk), but after `@charset` and `@import` rules
|
||||
- Added removal of `@keyframes`, `@media` and `@supports` with no prelude
|
||||
- Added removal of duplicate `@keyframes` (#202)
|
||||
- Added new option `forceMediaMerge` to force media rules merging. It's unsafe in general, but works fine in many cases. Use it on your own risk (#350)
|
||||
- Bumped `CSSTree` to `1.0.0-alpha23`
|
||||
|
||||
## 3.1.1 (April 25, 2017)
|
||||
|
||||
- Fixed crash on a number processing when it used not in a list (#335)
|
||||
|
||||
## 3.1.0 (April 24, 2017)
|
||||
|
||||
- Implemented optimisation for `none` keyword in `border` and `outline` properties (@zoobestik, #41)
|
||||
- Implemented replacing `rgba(x, x, x, 0)` to `transparent`
|
||||
- Fixed plus sign omitting for numbers following identifier, hex color, number or unicode range, since it can change the meaning of CSS (e.g. `calc(1px+2px)` has been optimized to `calc(1px2px)` before, now it stays the same)
|
||||
- Improved usage filtering for nested selectors (i.e. for `:nth-*()`, `:has()`, `:matches` and other pseudos)
|
||||
- Implemented `blacklist` filtering in usage (#334, see [Black list filtering](https://github.com/css/csso#black-list-filtering))
|
||||
- Improved white space removing, now white spaces are removing in the beginning and at the ending of sequences, and between stylesheet and block nodes
|
||||
- Bumped `CSSTree` to `1.0.0-alpha19`
|
||||
|
||||
## 3.0.1 (March 14, 2017)
|
||||
|
||||
- Fixed declaration merging when declaration contains an `!important`
|
||||
|
||||
## 3.0.0 (March 13, 2017)
|
||||
|
||||
- Migrated to [CSSTree](https://github.com/csstree/csstree) as AST backend and exposed its API behind `syntax` property
|
||||
- Extracted CLI into standalone package [css/csso-cli](https://github.com/css/csso-cli)
|
||||
|
||||
## 2.3.1 (January 6, 2017)
|
||||
|
||||
- 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
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
Copyright (C) 2015-2019 by Roman Dvornov
|
||||
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.
|
||||
+372
@@ -0,0 +1,372 @@
|
||||
[](https://www.npmjs.com/package/csso)
|
||||
[](https://travis-ci.org/css/csso)
|
||||
[](https://coveralls.io/github/css/csso?branch=master)
|
||||
[](https://www.npmjs.com/package/csso)
|
||||
[](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.
|
||||
|
||||
[](https://www.yandex.com/)
|
||||
[](https://www.avito.ru/)
|
||||
|
||||
## Ready to use
|
||||
|
||||
- [Web interface](http://css.github.io/csso/csso.html)
|
||||
- [csso-cli](https://github.com/css/csso-cli) – command line interface
|
||||
- [gulp-csso](https://github.com/ben-eb/gulp-csso) – `Gulp` plugin
|
||||
- [grunt-csso](https://github.com/t32k/grunt-csso) – `Grunt` plugin
|
||||
- [broccoli-csso](https://github.com/sindresorhus/broccoli-csso) – `Broccoli` plugin
|
||||
- [postcss-csso](https://github.com/lahmatiy/postcss-csso) – `PostCSS` plugin
|
||||
- [csso-loader](https://github.com/sandark7/csso-loader) – `webpack` loader
|
||||
- [csso-webpack-plugin](https://github.com/zoobestik/csso-webpack-plugin) – `webpack` plugin
|
||||
- [CSSO Visual Studio Code plugin](https://marketplace.visualstudio.com/items?itemName=Aneryu.csso)
|
||||
|
||||
## Install
|
||||
|
||||
```
|
||||
npm install csso
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
<!-- TOC depthfrom:3 -->
|
||||
|
||||
- [minify(source[, options])](#minifysource-options)
|
||||
- [minifyBlock(source[, options])](#minifyblocksource-options)
|
||||
- [syntax.compress(ast[, options])](#syntaxcompressast-options)
|
||||
- [Source maps](#source-maps)
|
||||
- [Usage data](#usage-data)
|
||||
- [White list filtering](#white-list-filtering)
|
||||
- [Black list filtering](#black-list-filtering)
|
||||
- [Scopes](#scopes)
|
||||
|
||||
<!-- /TOC -->
|
||||
|
||||
Basic usage:
|
||||
|
||||
```js
|
||||
var csso = require('csso');
|
||||
|
||||
var minifiedCss = csso.minify('.test { color: #ff0000; }').css;
|
||||
|
||||
console.log(minifiedCss);
|
||||
// .test{color:red}
|
||||
```
|
||||
|
||||
CSSO is based on [CSSTree](https://github.com/csstree/csstree) to parse CSS into AST, AST traversal and to generate AST back to CSS. All `CSSTree` API is available behind `syntax` field. You may minify CSS step by step:
|
||||
|
||||
```js
|
||||
var csso = require('csso');
|
||||
var ast = csso.syntax.parse('.test { color: #ff0000; }');
|
||||
var compressedAst = csso.syntax.compress(ast).ast;
|
||||
var minifiedCss = csso.syntax.generate(compressedAst);
|
||||
|
||||
console.log(minifiedCss);
|
||||
// .test{color:red}
|
||||
```
|
||||
|
||||
> Warning: CSSO uses early versions of CSSTree that still in active development. CSSO doesn't guarantee API behind `syntax` field or AST format will not change in future releases of CSSO, since it's subject to change in CSSTree. Be careful with CSSO updates if you use `syntax` API until this warning removal.
|
||||
|
||||
### minify(source[, options])
|
||||
|
||||
Minify `source` CSS passed as `String`.
|
||||
|
||||
```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}
|
||||
```
|
||||
|
||||
Returns an object with properties:
|
||||
|
||||
- css `String` – resulting CSS
|
||||
- map `Object` – instance of [`SourceMapGenerator`](https://github.com/mozilla/source-map#sourcemapgenerator) or `null`
|
||||
|
||||
Options:
|
||||
|
||||
- sourceMap
|
||||
|
||||
Type: `Boolean`
|
||||
Default: `false`
|
||||
|
||||
Generate a source map when `true`.
|
||||
|
||||
- filename
|
||||
|
||||
Type: `String`
|
||||
Default: `'<unknown>'`
|
||||
|
||||
Filename of input CSS, uses for source map generation.
|
||||
|
||||
- debug
|
||||
|
||||
Type: `Boolean`
|
||||
Default: `false`
|
||||
|
||||
Output debug information to `stderr`.
|
||||
|
||||
- beforeCompress
|
||||
|
||||
Type: `function(ast, options)` or `Array<function(ast, options)>` or `null`
|
||||
Default: `null`
|
||||
|
||||
Called right after parse is run.
|
||||
|
||||
- afterCompress
|
||||
|
||||
Type: `function(compressResult, options)` or `Array<function(compressResult, options)>` or `null`
|
||||
Default: `null`
|
||||
|
||||
Called right after [`syntax.compress()`](#syntaxcompressast-options) is run.
|
||||
|
||||
- Other options are the same as for [`syntax.compress()`](#syntaxcompressast-options) function.
|
||||
|
||||
### minifyBlock(source[, options])
|
||||
|
||||
The same as `minify()` but for list of declarations. Usually it's a `style` attribute value.
|
||||
|
||||
```js
|
||||
var result = csso.minifyBlock('color: rgba(255, 0, 0, 1); color: #ff0000');
|
||||
|
||||
console.log(result.css);
|
||||
// > color:red
|
||||
```
|
||||
|
||||
### syntax.compress(ast[, options])
|
||||
|
||||
Does the main task – compress an AST. This is CSSO's extension in CSSTree syntax API.
|
||||
|
||||
> NOTE: `syntax.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.
|
||||
|
||||
Returns an object with properties:
|
||||
|
||||
- ast `Object` – resulting AST
|
||||
|
||||
Options:
|
||||
|
||||
- restructure
|
||||
|
||||
Type: `Boolean`
|
||||
Default: `true`
|
||||
|
||||
Disable or enable a structure optimisations.
|
||||
|
||||
- forceMediaMerge
|
||||
|
||||
Type: `Boolean`
|
||||
Default: `false`
|
||||
|
||||
Enables merging of `@media` rules with the same media query by splitted by other rules. The optimisation is unsafe in general, but should work fine in most cases. Use it on your own risk.
|
||||
|
||||
- clone
|
||||
|
||||
Type: `Boolean`
|
||||
Default: `false`
|
||||
|
||||
Transform a copy of input AST if `true`. Useful in case of AST reuse.
|
||||
|
||||
- comments
|
||||
|
||||
Type: `String` or `Boolean`
|
||||
Default: `true`
|
||||
|
||||
Specify what comments to leave:
|
||||
|
||||
- `'exclamation'` or `true` – leave all exclamation comments (i.e. `/*! .. */`)
|
||||
- `'first-exclamation'` – remove every comment except first one
|
||||
- `false` – remove all comments
|
||||
|
||||
- usage
|
||||
|
||||
Type: `Object` or `null`
|
||||
Default: `null`
|
||||
|
||||
Usage data for advanced optimisations (see [Usage data](#usage-data) for details)
|
||||
|
||||
- logger
|
||||
|
||||
Type: `Function` or `null`
|
||||
Default: `null`
|
||||
|
||||
Function to track every step of transformation.
|
||||
|
||||
### Source maps
|
||||
|
||||
To get a source map set `true` for `sourceMap` option. Additianaly `filename` option can be passed to specify source file. When `sourceMap` option is `true`, `map` field of result object will contain a [`SourceMapGenerator`](https://github.com/mozilla/source-map#sourcemapgenerator) instance. This object can be mixed with another source map or translated to string.
|
||||
|
||||
```js
|
||||
var csso = require('csso');
|
||||
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 .. }'
|
||||
```
|
||||
|
||||
Example of generating source map with respect of source map from input CSS:
|
||||
|
||||
```js
|
||||
var require('source-map');
|
||||
var csso = require('csso');
|
||||
var inputFile = 'path/to/my.css';
|
||||
var input = fs.readFileSync(inputFile, 'utf8');
|
||||
var inputMap = input.match(/\/\*# sourceMappingURL=(\S+)\s*\*\/\s*$/);
|
||||
var output = csso.minify(input, {
|
||||
filename: inputFile,
|
||||
sourceMap: true
|
||||
});
|
||||
|
||||
// apply input source map to output
|
||||
if (inputMap) {
|
||||
output.map.applySourceMap(
|
||||
new SourceMapConsumer(inputMap[1]),
|
||||
inputFile
|
||||
)
|
||||
}
|
||||
|
||||
// result CSS with source map
|
||||
console.log(
|
||||
output.css +
|
||||
'/*# sourceMappingURL=data:application/json;base64,' +
|
||||
new Buffer(output.map.toString()).toString('base64') +
|
||||
' */'
|
||||
);
|
||||
```
|
||||
|
||||
### Usage data
|
||||
|
||||
`CSSO` can use data about how `CSS` is used in a markup for better compression. File with this data (`JSON`) can be set using `usage` option. Usage data may contain following sections:
|
||||
|
||||
- `blacklist` – a set of black lists (see [Black list filtering](#black-list-filtering))
|
||||
- `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 the same element
|
||||
|
||||
All sections are optional. Value of `tags`, `ids` and `classes` should be an array of a string, value of `scopes` should be an array of arrays of strings. Other values are ignoring.
|
||||
|
||||
#### White list filtering
|
||||
|
||||
`tags`, `ids` and `classes` are using on clean stage to filter selectors that contain something not in the lists. 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 all type selectors in selector present in list or selector has no any type selector it isn't filter.
|
||||
|
||||
> `ids` and `classes` 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"]
|
||||
}
|
||||
```
|
||||
|
||||
Resulting CSS:
|
||||
|
||||
```css
|
||||
*{color:green}ul,li{color:blue}ul.foo{color:red}
|
||||
```
|
||||
|
||||
Filtering performs for nested selectors too. `:not()` pseudos content is ignoring since the result of matching is unpredictable. Example for the same usage data as above:
|
||||
|
||||
```css
|
||||
:nth-child(2n of ul, ol) { color: red }
|
||||
:nth-child(3n + 1 of img) { color: yellow }
|
||||
:not(div, ol, ul) { color: green }
|
||||
:has(:matches(ul, ol), ul, ol) { color: blue }
|
||||
```
|
||||
|
||||
Turns into:
|
||||
|
||||
```css
|
||||
:nth-child(2n of ul){color:red}:not(div,ol,ul){color:green}:has(:matches(ul),ul){color:blue}
|
||||
```
|
||||
|
||||
#### Black list filtering
|
||||
|
||||
Black list filtering performs the same as white list filtering, but filters things that mentioned in the lists. `blacklist` can contain the lists `tags`, `ids` and `classes`.
|
||||
|
||||
Black list has a higher priority, so when something mentioned in the white list and in the black list then white list occurrence is ignoring. The `:not()` pseudos content ignoring as well.
|
||||
|
||||
```css
|
||||
* { color: green; }
|
||||
ul, ol, li { color: blue; }
|
||||
UL.foo, li.bar { color: red; }
|
||||
```
|
||||
|
||||
Usage data:
|
||||
|
||||
```json
|
||||
{
|
||||
"blacklist": {
|
||||
"tags": ["ul"]
|
||||
},
|
||||
"tags": ["ul", "LI"]
|
||||
}
|
||||
```
|
||||
|
||||
Resulting CSS:
|
||||
|
||||
```css
|
||||
*{color:green}li{color:blue}li.bar{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 define lists of class names that exclusively used on some markup. This information allows the optimizer to move rules more agressive. Since it assumes selectors from different scopes don't match for the same element. This can improve rule 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 are never used with the second two on the same markup. But we can't say that for sure without a markup review. The optimizer doesn't know it either 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}
|
||||
```
|
||||
|
||||
With usage data `CSSO` can produce better output. If follow usage data is provided:
|
||||
|
||||
```json
|
||||
{
|
||||
"scopes": [
|
||||
["module1-foo", "module1-bar"],
|
||||
["module2-baz", "module2-qux"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The result will be (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 isn't mentioned in the `scopes` it belongs to default scope. `scopes` data doesn't affect `classes` whitelist. If class name mentioned in `scopes` but missed in `classes` (both sections are specified) it will be filtered.
|
||||
|
||||
Note that class name can't be set for several scopes. Also a selector can't have class names from different scopes. In both cases an exception will thrown.
|
||||
|
||||
Currently the optimizer doesn't care about changing order safety for out-of-bounds selectors (i.e. selectors that match to elements without class name, e.g. `.scope div` or `.scope ~ :last-child`). It assumes that scoped CSS modules doesn't relay on it's order. It may be fix in future if to be an issue.
|
||||
+3322
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+66
@@ -0,0 +1,66 @@
|
||||
var resolveKeyword = require('css-tree').keyword;
|
||||
var { hasNoChildren } = require('./utils');
|
||||
|
||||
module.exports = function cleanAtrule(node, item, list) {
|
||||
if (node.block) {
|
||||
// otherwise removed at-rule don't prevent @import for removal
|
||||
if (this.stylesheet !== null) {
|
||||
this.stylesheet.firstAtrulesAllowed = false;
|
||||
}
|
||||
|
||||
if (hasNoChildren(node.block)) {
|
||||
list.remove(item);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
switch (node.name) {
|
||||
case 'charset':
|
||||
if (hasNoChildren(node.prelude)) {
|
||||
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.stylesheet === null || !this.stylesheet.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;
|
||||
|
||||
default:
|
||||
var name = resolveKeyword(node.name).basename;
|
||||
if (name === 'keyframes' ||
|
||||
name === 'media' ||
|
||||
name === 'supports') {
|
||||
|
||||
// drop at-rule with no prelude
|
||||
if (hasNoChildren(node.prelude) || hasNoChildren(node.block)) {
|
||||
list.remove(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
module.exports = function cleanComment(data, item, list) {
|
||||
list.remove(item);
|
||||
};
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
var property = require('css-tree').property;
|
||||
|
||||
module.exports = function cleanDeclartion(node, item, list) {
|
||||
if (node.value.children && node.value.children.isEmpty()) {
|
||||
list.remove(item);
|
||||
return;
|
||||
}
|
||||
|
||||
if (property(node.property).custom) {
|
||||
if (/\S/.test(node.value.value)) {
|
||||
node.value.value = node.value.value.trim();
|
||||
}
|
||||
}
|
||||
};
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
var { isNodeChildrenList } = require('./utils');
|
||||
|
||||
module.exports = function cleanRaw(node, item, list) {
|
||||
// raw in stylesheet or block children
|
||||
if (isNodeChildrenList(this.stylesheet, list) ||
|
||||
isNodeChildrenList(this.block, list)) {
|
||||
list.remove(item);
|
||||
}
|
||||
};
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
var hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
var walk = require('css-tree').walk;
|
||||
var { hasNoChildren } = require('./utils');
|
||||
|
||||
function cleanUnused(selectorList, usageData) {
|
||||
selectorList.children.each(function(selector, item, list) {
|
||||
var shouldRemove = false;
|
||||
|
||||
walk(selector, function(node) {
|
||||
// ignore nodes in nested selectors
|
||||
if (this.selector === null || this.selector === selectorList) {
|
||||
switch (node.type) {
|
||||
case 'SelectorList':
|
||||
// TODO: remove toLowerCase when pseudo selectors will be normalized
|
||||
// ignore selectors inside :not()
|
||||
if (this.function === null || this.function.name.toLowerCase() !== 'not') {
|
||||
if (cleanUnused(node, usageData)) {
|
||||
shouldRemove = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ClassSelector':
|
||||
if (usageData.whitelist !== null &&
|
||||
usageData.whitelist.classes !== null &&
|
||||
!hasOwnProperty.call(usageData.whitelist.classes, node.name)) {
|
||||
shouldRemove = true;
|
||||
}
|
||||
if (usageData.blacklist !== null &&
|
||||
usageData.blacklist.classes !== null &&
|
||||
hasOwnProperty.call(usageData.blacklist.classes, node.name)) {
|
||||
shouldRemove = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'IdSelector':
|
||||
if (usageData.whitelist !== null &&
|
||||
usageData.whitelist.ids !== null &&
|
||||
!hasOwnProperty.call(usageData.whitelist.ids, node.name)) {
|
||||
shouldRemove = true;
|
||||
}
|
||||
if (usageData.blacklist !== null &&
|
||||
usageData.blacklist.ids !== null &&
|
||||
hasOwnProperty.call(usageData.blacklist.ids, node.name)) {
|
||||
shouldRemove = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'TypeSelector':
|
||||
// TODO: remove toLowerCase when type selectors will be normalized
|
||||
// ignore universal selectors
|
||||
if (node.name.charAt(node.name.length - 1) !== '*') {
|
||||
if (usageData.whitelist !== null &&
|
||||
usageData.whitelist.tags !== null &&
|
||||
!hasOwnProperty.call(usageData.whitelist.tags, node.name.toLowerCase())) {
|
||||
shouldRemove = true;
|
||||
}
|
||||
if (usageData.blacklist !== null &&
|
||||
usageData.blacklist.tags !== null &&
|
||||
hasOwnProperty.call(usageData.blacklist.tags, node.name.toLowerCase())) {
|
||||
shouldRemove = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (shouldRemove) {
|
||||
list.remove(item);
|
||||
}
|
||||
});
|
||||
|
||||
return selectorList.children.isEmpty();
|
||||
}
|
||||
|
||||
module.exports = function cleanRule(node, item, list, options) {
|
||||
if (hasNoChildren(node.prelude) || hasNoChildren(node.block)) {
|
||||
list.remove(item);
|
||||
return;
|
||||
}
|
||||
|
||||
var usageData = options.usage;
|
||||
|
||||
if (usageData && (usageData.whitelist !== null || usageData.blacklist !== null)) {
|
||||
cleanUnused(node.prelude, usageData);
|
||||
|
||||
if (hasNoChildren(node.prelude)) {
|
||||
list.remove(item);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
// remove useless universal selector
|
||||
module.exports = function cleanTypeSelector(node, item, list) {
|
||||
var name = item.data.name;
|
||||
|
||||
// check it's a non-namespaced universal selector
|
||||
if (name !== '*') {
|
||||
return;
|
||||
}
|
||||
|
||||
// remove when universal selector before other selectors
|
||||
var nextType = item.next && item.next.data.type;
|
||||
if (nextType === 'IdSelector' ||
|
||||
nextType === 'ClassSelector' ||
|
||||
nextType === 'AttributeSelector' ||
|
||||
nextType === 'PseudoClassSelector' ||
|
||||
nextType === 'PseudoElementSelector') {
|
||||
list.remove(item);
|
||||
}
|
||||
};
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
var { isNodeChildrenList } = require('./utils');
|
||||
|
||||
function isSafeOperator(node) {
|
||||
return node.type === 'Operator' && node.value !== '+' && node.value !== '-';
|
||||
}
|
||||
|
||||
module.exports = function cleanWhitespace(node, item, list) {
|
||||
// remove when first or last item in sequence
|
||||
if (item.next === null || item.prev === null) {
|
||||
list.remove(item);
|
||||
return;
|
||||
}
|
||||
|
||||
// white space in stylesheet or block children
|
||||
if (isNodeChildrenList(this.stylesheet, list) ||
|
||||
isNodeChildrenList(this.block, list)) {
|
||||
list.remove(item);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.next.data.type === 'WhiteSpace') {
|
||||
list.remove(item);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSafeOperator(item.prev.data) || isSafeOperator(item.next.data)) {
|
||||
list.remove(item);
|
||||
return;
|
||||
}
|
||||
};
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
var walk = require('css-tree').walk;
|
||||
var handlers = {
|
||||
Atrule: require('./Atrule'),
|
||||
Comment: require('./Comment'),
|
||||
Declaration: require('./Declaration'),
|
||||
Raw: require('./Raw'),
|
||||
Rule: require('./Rule'),
|
||||
TypeSelector: require('./TypeSelector'),
|
||||
WhiteSpace: require('./WhiteSpace')
|
||||
};
|
||||
|
||||
module.exports = function(ast, options) {
|
||||
walk(ast, {
|
||||
leave: function(node, item, list) {
|
||||
if (handlers.hasOwnProperty(node.type)) {
|
||||
handlers[node.type].call(this, node, item, list, options);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
hasNoChildren: function(node) {
|
||||
return !node || !node.children || node.children.isEmpty();
|
||||
},
|
||||
isNodeChildrenList: function(node, list) {
|
||||
return node !== null && node.children === list;
|
||||
}
|
||||
};
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
var List = require('css-tree').List;
|
||||
var clone = require('css-tree').clone;
|
||||
var usageUtils = require('./usage');
|
||||
var clean = require('./clean');
|
||||
var replace = require('./replace');
|
||||
var restructure = require('./restructure');
|
||||
var walk = require('css-tree').walk;
|
||||
|
||||
function readChunk(children, specialComments) {
|
||||
var buffer = new List();
|
||||
var nonSpaceTokenInBuffer = false;
|
||||
var protectedComment;
|
||||
|
||||
children.nextUntil(children.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 !== 'WhiteSpace') {
|
||||
nonSpaceTokenInBuffer = true;
|
||||
}
|
||||
|
||||
buffer.insert(list.remove(item));
|
||||
});
|
||||
|
||||
return {
|
||||
comment: protectedComment,
|
||||
stylesheet: {
|
||||
type: 'StyleSheet',
|
||||
loc: null,
|
||||
children: buffer
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function compressChunk(ast, firstAtrulesAllowed, num, options) {
|
||||
options.logger('Compress block #' + num, null, true);
|
||||
|
||||
var seed = 1;
|
||||
|
||||
if (ast.type === 'StyleSheet') {
|
||||
ast.firstAtrulesAllowed = firstAtrulesAllowed;
|
||||
ast.id = seed++;
|
||||
}
|
||||
|
||||
walk(ast, {
|
||||
visit: 'Atrule',
|
||||
enter: function markScopes(node) {
|
||||
if (node.block !== null) {
|
||||
node.block.id = seed++;
|
||||
}
|
||||
}
|
||||
});
|
||||
options.logger('init', ast);
|
||||
|
||||
// remove redundant
|
||||
clean(ast, options);
|
||||
options.logger('clean', ast);
|
||||
|
||||
// replace nodes for shortened forms
|
||||
replace(ast, options);
|
||||
options.logger('replace', ast);
|
||||
|
||||
// structure optimisations
|
||||
if (options.restructuring) {
|
||||
restructure(ast, options);
|
||||
}
|
||||
|
||||
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) {
|
||||
if ('restructure' in options) {
|
||||
return options.restructure;
|
||||
}
|
||||
|
||||
return 'restructuring' in options ? options.restructuring : true;
|
||||
}
|
||||
|
||||
function wrapBlock(block) {
|
||||
return new List().appendData({
|
||||
type: 'Rule',
|
||||
loc: null,
|
||||
prelude: {
|
||||
type: 'SelectorList',
|
||||
loc: null,
|
||||
children: new List().appendData({
|
||||
type: 'Selector',
|
||||
loc: null,
|
||||
children: new List().appendData({
|
||||
type: 'TypeSelector',
|
||||
loc: null,
|
||||
name: 'x'
|
||||
})
|
||||
})
|
||||
},
|
||||
block: block
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = function compress(ast, options) {
|
||||
ast = ast || { type: 'StyleSheet', loc: null, children: new List() };
|
||||
options = options || {};
|
||||
|
||||
var compressOptions = {
|
||||
logger: typeof options.logger === 'function' ? options.logger : function() {},
|
||||
restructuring: getRestructureOption(options),
|
||||
forceMediaMerge: Boolean(options.forceMediaMerge),
|
||||
usage: options.usage ? usageUtils.buildIndex(options.usage) : false
|
||||
};
|
||||
var specialComments = getCommentsOption(options);
|
||||
var firstAtrulesAllowed = true;
|
||||
var input;
|
||||
var output = new List();
|
||||
var chunk;
|
||||
var chunkNum = 1;
|
||||
var chunkChildren;
|
||||
|
||||
if (options.clone) {
|
||||
ast = clone(ast);
|
||||
}
|
||||
|
||||
if (ast.type === 'StyleSheet') {
|
||||
input = ast.children;
|
||||
ast.children = output;
|
||||
} else {
|
||||
input = wrapBlock(ast);
|
||||
}
|
||||
|
||||
do {
|
||||
chunk = readChunk(input, Boolean(specialComments));
|
||||
compressChunk(chunk.stylesheet, firstAtrulesAllowed, chunkNum++, compressOptions);
|
||||
chunkChildren = chunk.stylesheet.children;
|
||||
|
||||
if (chunk.comment) {
|
||||
// add \n before comment if there is another content in output
|
||||
if (!output.isEmpty()) {
|
||||
output.insert(List.createItem({
|
||||
type: 'Raw',
|
||||
value: '\n'
|
||||
}));
|
||||
}
|
||||
|
||||
output.insert(List.createItem(chunk.comment));
|
||||
|
||||
// add \n after comment if chunk is not empty
|
||||
if (!chunkChildren.isEmpty()) {
|
||||
output.insert(List.createItem({
|
||||
type: 'Raw',
|
||||
value: '\n'
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (firstAtrulesAllowed && !chunkChildren.isEmpty()) {
|
||||
var lastRule = chunkChildren.last();
|
||||
|
||||
if (lastRule.type !== 'Atrule' ||
|
||||
(lastRule.name !== 'import' && lastRule.name !== 'charset')) {
|
||||
firstAtrulesAllowed = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (specialComments !== 'exclamation') {
|
||||
specialComments = false;
|
||||
}
|
||||
|
||||
output.appendList(chunkChildren);
|
||||
} while (!input.isEmpty());
|
||||
|
||||
return {
|
||||
ast: ast
|
||||
};
|
||||
};
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
var csstree = require('css-tree');
|
||||
var parse = csstree.parse;
|
||||
var compress = require('./compress');
|
||||
var generate = csstree.generate;
|
||||
|
||||
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 = generate(ast);
|
||||
|
||||
// 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)
|
||||
);
|
||||
}
|
||||
|
||||
// generate
|
||||
if (options.sourceMap) {
|
||||
result = debugOutput('generate(sourceMap: true)', options, Date.now(), (function() {
|
||||
var tmp = generate(compressResult.ast, { sourceMap: true });
|
||||
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('generate', options, Date.now(), {
|
||||
css: generate(compressResult.ast),
|
||||
map: null
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function minifyStylesheet(source, options) {
|
||||
return minify('stylesheet', source, options);
|
||||
}
|
||||
|
||||
function minifyBlock(source, options) {
|
||||
return minify('declarationList', source, options);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
version: require('../package.json').version,
|
||||
|
||||
// main methods
|
||||
minify: minifyStylesheet,
|
||||
minifyBlock: minifyBlock,
|
||||
|
||||
// css syntax parser/walkers/generator/etc
|
||||
syntax: Object.assign({
|
||||
compress: compress
|
||||
}, csstree)
|
||||
};
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
var resolveKeyword = require('css-tree').keyword;
|
||||
var compressKeyframes = require('./atrule/keyframes');
|
||||
|
||||
module.exports = function(node) {
|
||||
// compress @keyframe selectors
|
||||
if (resolveKeyword(node.name).basename === 'keyframes') {
|
||||
compressKeyframes(node);
|
||||
}
|
||||
};
|
||||
Generated
Vendored
+33
@@ -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})(\r\n|[ \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',
|
||||
loc: attrValue.loc,
|
||||
name: unquotedValue
|
||||
};
|
||||
}
|
||||
};
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
var packNumber = require('./Number').pack;
|
||||
var MATH_FUNCTIONS = {
|
||||
'calc': true,
|
||||
'min': true,
|
||||
'max': true,
|
||||
'clamp': true
|
||||
};
|
||||
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, item);
|
||||
|
||||
node.value = value;
|
||||
|
||||
if (value === '0' && this.declaration !== null && this.atrulePrelude === null) {
|
||||
var unit = node.unit.toLowerCase();
|
||||
|
||||
// only length values can be compressed
|
||||
if (!LENGTH_UNIT.hasOwnProperty(unit)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// issue #362: shouldn't remove unit in -ms-flex since it breaks flex in IE10/11
|
||||
// issue #200: shouldn't remove unit in flex since it breaks flex in IE10/11
|
||||
if (this.declaration.property === '-ms-flex' ||
|
||||
this.declaration.property === 'flex') {
|
||||
return;
|
||||
}
|
||||
|
||||
// issue #222: don't remove units inside calc
|
||||
if (this.function && MATH_FUNCTIONS.hasOwnProperty(this.function.name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
item.data = {
|
||||
type: 'Number',
|
||||
loc: node.loc,
|
||||
value: value
|
||||
};
|
||||
}
|
||||
};
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
var OMIT_PLUSSIGN = /^(?:\+|(-))?0*(\d*)(?:\.0*|(\.\d*?)0*)?$/;
|
||||
var KEEP_PLUSSIGN = /^([\+\-])?0*(\d*)(?:\.0*|(\.\d*?)0*)?$/;
|
||||
var unsafeToRemovePlusSignAfter = {
|
||||
Dimension: true,
|
||||
Hash: true,
|
||||
Identifier: true,
|
||||
Number: true,
|
||||
Raw: true,
|
||||
UnicodeRange: true
|
||||
};
|
||||
|
||||
function packNumber(value, item) {
|
||||
// omit plus sign only if no prev or prev is safe type
|
||||
var regexp = item && item.prev !== null && unsafeToRemovePlusSignAfter.hasOwnProperty(item.prev.data.type)
|
||||
? KEEP_PLUSSIGN
|
||||
: OMIT_PLUSSIGN;
|
||||
|
||||
// 100 -> '100'
|
||||
// 00100 -> '100'
|
||||
// +100 -> '100' (only when safe, e.g. omitting plus sign for 1px+1px leads to single dimension instead of two)
|
||||
// -100 -> '-100'
|
||||
// 0.123 -> '.123'
|
||||
// 0.12300 -> '.123'
|
||||
// 0.0 -> ''
|
||||
// 0 -> ''
|
||||
// -0 -> '-'
|
||||
value = String(value).replace(regexp, '$1$2$3');
|
||||
|
||||
if (value === '' || value === '-') {
|
||||
value = '0';
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
module.exports = function(node, item) {
|
||||
node.value = packNumber(node.value, item);
|
||||
};
|
||||
module.exports.pack = packNumber;
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
var lexer = require('css-tree').lexer;
|
||||
var packNumber = require('./Number').pack;
|
||||
var blacklist = new Set([
|
||||
// see https://github.com/jakubpawlowicz/clean-css/issues/957
|
||||
'width',
|
||||
'min-width',
|
||||
'max-width',
|
||||
'height',
|
||||
'min-height',
|
||||
'max-height',
|
||||
|
||||
// issue #410: Don’t remove units in flex-basis value for (-ms-)flex shorthand
|
||||
// issue #362: shouldn't remove unit in -ms-flex since it breaks flex in IE10/11
|
||||
// issue #200: shouldn't remove unit in flex since it breaks flex in IE10/11
|
||||
'flex',
|
||||
'-ms-flex'
|
||||
]);
|
||||
|
||||
module.exports = function compressPercentage(node, item) {
|
||||
node.value = packNumber(node.value, item);
|
||||
|
||||
if (node.value === '0' && this.declaration && !blacklist.has(this.declaration.property)) {
|
||||
// try to convert a number
|
||||
item.data = {
|
||||
type: 'Number',
|
||||
loc: node.loc,
|
||||
value: node.value
|
||||
};
|
||||
|
||||
// that's ok only when new value matches on length
|
||||
if (!lexer.matchDeclaration(this.declaration).isType(item.data, 'length')) {
|
||||
// otherwise rollback changes
|
||||
item.data = node;
|
||||
}
|
||||
}
|
||||
};
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
module.exports = function(node) {
|
||||
var value = node.value;
|
||||
|
||||
// remove escaped newlines, i.e.
|
||||
// .a { content: "foo\
|
||||
// bar"}
|
||||
// ->
|
||||
// .a { content: "foobar" }
|
||||
value = value.replace(/\\(\r\n|\r|\n|\f)/g, '');
|
||||
|
||||
node.value = value;
|
||||
};
|
||||
+33
@@ -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',
|
||||
loc: node.value.loc,
|
||||
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;
|
||||
}
|
||||
};
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
var resolveName = require('css-tree').property;
|
||||
var handlers = {
|
||||
'font': require('./property/font'),
|
||||
'font-weight': require('./property/font-weight'),
|
||||
'background': require('./property/background'),
|
||||
'border': require('./property/border'),
|
||||
'outline': require('./property/border')
|
||||
};
|
||||
|
||||
module.exports = function compressValue(node) {
|
||||
if (!this.declaration) {
|
||||
return;
|
||||
}
|
||||
|
||||
var property = resolveName(this.declaration.property);
|
||||
|
||||
if (handlers.hasOwnProperty(property.basename)) {
|
||||
handlers[property.basename](node);
|
||||
}
|
||||
};
|
||||
Generated
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
module.exports = function(node) {
|
||||
node.block.children.each(function(rule) {
|
||||
rule.prelude.children.each(function(simpleselector) {
|
||||
simpleselector.children.each(function(data, item) {
|
||||
if (data.type === 'Percentage' && data.value === '100') {
|
||||
item.data = {
|
||||
type: 'TypeSelector',
|
||||
loc: data.loc,
|
||||
name: 'to'
|
||||
};
|
||||
} else if (data.type === 'TypeSelector' && data.name === 'from') {
|
||||
item.data = {
|
||||
type: 'Percentage',
|
||||
loc: data.loc,
|
||||
value: '0'
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
+510
@@ -0,0 +1,510 @@
|
||||
var lexer = require('css-tree').lexer;
|
||||
var packNumber = require('./Number').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 cursor = functionArgs.head;
|
||||
var args = [];
|
||||
var wasValue = false;
|
||||
|
||||
while (cursor !== null) {
|
||||
var node = cursor.data;
|
||||
var type = node.type;
|
||||
|
||||
switch (type) {
|
||||
case 'Number':
|
||||
case 'Percentage':
|
||||
if (wasValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
wasValue = true;
|
||||
args.push({
|
||||
type: type,
|
||||
value: Number(node.value)
|
||||
});
|
||||
break;
|
||||
|
||||
case 'Operator':
|
||||
if (node.value === ',') {
|
||||
if (!wasValue) {
|
||||
return;
|
||||
}
|
||||
wasValue = false;
|
||||
} else if (wasValue || node.value !== '+') {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// something we couldn't understand
|
||||
return;
|
||||
}
|
||||
|
||||
cursor = cursor.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.children, 4, functionName === 'rgba');
|
||||
|
||||
if (!args) {
|
||||
// something went wrong
|
||||
return;
|
||||
}
|
||||
|
||||
if (functionName === 'hsla') {
|
||||
args = hslToRgb.apply(null, args);
|
||||
node.name = 'rgba';
|
||||
}
|
||||
|
||||
if (args[3] === 0) {
|
||||
// try to replace `rgba(x, x, x, 0)` to `transparent`
|
||||
// always replace `rgba(0, 0, 0, 0)` to `transparent`
|
||||
// otherwise avoid replacement in gradients since it may break color transition
|
||||
// http://stackoverflow.com/questions/11829410/css3-gradient-rendering-issues-from-transparent-to-white
|
||||
var scopeFunctionName = this.function && this.function.name;
|
||||
if ((args[0] === 0 && args[1] === 0 && args[2] === 0) ||
|
||||
!/^(?:to|from|color-stop)$|gradient$/i.test(scopeFunctionName)) {
|
||||
|
||||
item.data = {
|
||||
type: 'Identifier',
|
||||
loc: node.loc,
|
||||
name: 'transparent'
|
||||
};
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (args[3] !== 1) {
|
||||
// replace argument values for normalized/interpolated
|
||||
node.children.each(function(node, item, list) {
|
||||
if (node.type === 'Operator') {
|
||||
if (node.value !== ',') {
|
||||
list.remove(item);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
item.data = {
|
||||
type: 'Number',
|
||||
loc: node.loc,
|
||||
value: packNumber(args.shift(), null)
|
||||
};
|
||||
});
|
||||
|
||||
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.children, 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.children, 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 !== 'WhiteSpace') {
|
||||
list.insert(list.createItem({
|
||||
type: 'WhiteSpace',
|
||||
value: ' '
|
||||
}), next);
|
||||
}
|
||||
|
||||
item.data = {
|
||||
type: 'Hash',
|
||||
loc: node.loc,
|
||||
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) &&
|
||||
lexer.matchDeclaration(this.declaration).isType(node, 'color')) {
|
||||
var hex = NAME_TO_HEX[color];
|
||||
|
||||
if (hex.length + 1 <= color.length) {
|
||||
// replace for shorter hex value
|
||||
item.data = {
|
||||
type: 'Hash',
|
||||
loc: node.loc,
|
||||
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',
|
||||
loc: node.loc,
|
||||
name: HEX_TO_NAME[color]
|
||||
};
|
||||
} else {
|
||||
node.value = color;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
compressFunction: compressFunction,
|
||||
compressIdent: compressIdent,
|
||||
compressHex: compressHex
|
||||
};
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
var walk = require('css-tree').walk;
|
||||
var handlers = {
|
||||
Atrule: require('./Atrule'),
|
||||
AttributeSelector: require('./AttributeSelector'),
|
||||
Value: require('./Value'),
|
||||
Dimension: require('./Dimension'),
|
||||
Percentage: require('./Percentage'),
|
||||
Number: require('./Number'),
|
||||
String: require('./String'),
|
||||
Url: require('./Url'),
|
||||
Hash: require('./color').compressHex,
|
||||
Identifier: require('./color').compressIdent,
|
||||
Function: require('./color').compressFunction
|
||||
};
|
||||
|
||||
module.exports = function(ast) {
|
||||
walk(ast, {
|
||||
leave: function(node, item, list) {
|
||||
if (handlers.hasOwnProperty(node.type)) {
|
||||
handlers[node.type].call(this, node, item, list);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
Generated
Vendored
+69
@@ -0,0 +1,69 @@
|
||||
var List = require('css-tree').List;
|
||||
|
||||
module.exports = function compressBackground(node) {
|
||||
function lastType() {
|
||||
if (buffer.length) {
|
||||
return buffer[buffer.length - 1].type;
|
||||
}
|
||||
}
|
||||
|
||||
function flush() {
|
||||
if (lastType() === 'WhiteSpace') {
|
||||
buffer.pop();
|
||||
}
|
||||
|
||||
if (!buffer.length) {
|
||||
buffer.unshift(
|
||||
{
|
||||
type: 'Number',
|
||||
loc: null,
|
||||
value: '0'
|
||||
},
|
||||
{
|
||||
type: 'WhiteSpace',
|
||||
value: ' '
|
||||
},
|
||||
{
|
||||
type: 'Number',
|
||||
loc: null,
|
||||
value: '0'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
newValue.push.apply(newValue, buffer);
|
||||
|
||||
buffer = [];
|
||||
}
|
||||
|
||||
var newValue = [];
|
||||
var buffer = [];
|
||||
|
||||
node.children.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 === 'WhiteSpace' && (!buffer.length || lastType() === 'WhiteSpace')) {
|
||||
return;
|
||||
}
|
||||
|
||||
buffer.push(node);
|
||||
});
|
||||
|
||||
flush();
|
||||
node.children = new List().fromArray(newValue);
|
||||
};
|
||||
Generated
Vendored
+31
@@ -0,0 +1,31 @@
|
||||
function removeItemAndRedundantWhiteSpace(list, item) {
|
||||
var prev = item.prev;
|
||||
var next = item.next;
|
||||
|
||||
if (next !== null) {
|
||||
if (next.data.type === 'WhiteSpace' && (prev === null || prev.data.type === 'WhiteSpace')) {
|
||||
list.remove(next);
|
||||
}
|
||||
} else if (prev !== null && prev.data.type === 'WhiteSpace') {
|
||||
list.remove(prev);
|
||||
}
|
||||
|
||||
list.remove(item);
|
||||
}
|
||||
|
||||
module.exports = function compressBorder(node) {
|
||||
node.children.each(function(node, item, list) {
|
||||
if (node.type === 'Identifier' && node.name.toLowerCase() === 'none') {
|
||||
if (list.head === list.tail) {
|
||||
// replace `none` for zero when `none` is a single term
|
||||
item.data = {
|
||||
type: 'Number',
|
||||
loc: node.loc,
|
||||
value: '0'
|
||||
};
|
||||
} else {
|
||||
removeItemAndRedundantWhiteSpace(list, item);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
Generated
Vendored
+22
@@ -0,0 +1,22 @@
|
||||
module.exports = function compressFontWeight(node) {
|
||||
var value = node.children.head.data;
|
||||
|
||||
if (value.type === 'Identifier') {
|
||||
switch (value.name) {
|
||||
case 'normal':
|
||||
node.children.head.data = {
|
||||
type: 'Number',
|
||||
loc: value.loc,
|
||||
value: '400'
|
||||
};
|
||||
break;
|
||||
case 'bold':
|
||||
node.children.head.data = {
|
||||
type: 'Number',
|
||||
loc: value.loc,
|
||||
value: '700'
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
Generated
Vendored
+45
@@ -0,0 +1,45 @@
|
||||
module.exports = function compressFont(node) {
|
||||
var list = node.children;
|
||||
|
||||
list.eachRight(function(node, item) {
|
||||
if (node.type === 'Identifier') {
|
||||
if (node.name === 'bold') {
|
||||
item.data = {
|
||||
type: 'Number',
|
||||
loc: node.loc,
|
||||
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 === 'WhiteSpace') {
|
||||
if (!item.prev || !item.next || item.next.data.type === 'WhiteSpace') {
|
||||
this.remove(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (list.isEmpty()) {
|
||||
list.insert(list.createItem({
|
||||
type: 'Identifier',
|
||||
name: 'normal'
|
||||
}));
|
||||
}
|
||||
};
|
||||
Generated
Vendored
+107
@@ -0,0 +1,107 @@
|
||||
var List = require('css-tree').List;
|
||||
var resolveKeyword = require('css-tree').keyword;
|
||||
var hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
var walk = require('css-tree').walk;
|
||||
|
||||
function addRuleToMap(map, item, list, single) {
|
||||
var node = item.data;
|
||||
var name = resolveKeyword(node.name).basename;
|
||||
var id = node.name.toLowerCase() + '/' + (node.prelude ? node.prelude.id : null);
|
||||
|
||||
if (!hasOwnProperty.call(map, name)) {
|
||||
map[name] = Object.create(null);
|
||||
}
|
||||
|
||||
if (single) {
|
||||
delete map[name][id];
|
||||
}
|
||||
|
||||
if (!hasOwnProperty.call(map[name], id)) {
|
||||
map[name][id] = new List();
|
||||
}
|
||||
|
||||
map[name][id].append(list.remove(item));
|
||||
}
|
||||
|
||||
function relocateAtrules(ast, options) {
|
||||
var collected = Object.create(null);
|
||||
var topInjectPoint = null;
|
||||
|
||||
ast.children.each(function(node, item, list) {
|
||||
if (node.type === 'Atrule') {
|
||||
var name = resolveKeyword(node.name).basename;
|
||||
|
||||
switch (name) {
|
||||
case 'keyframes':
|
||||
addRuleToMap(collected, item, list, true);
|
||||
return;
|
||||
|
||||
case 'media':
|
||||
if (options.forceMediaMerge) {
|
||||
addRuleToMap(collected, item, list, false);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (topInjectPoint === null &&
|
||||
name !== 'charset' &&
|
||||
name !== 'import') {
|
||||
topInjectPoint = item;
|
||||
}
|
||||
} else {
|
||||
if (topInjectPoint === null) {
|
||||
topInjectPoint = item;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (var atrule in collected) {
|
||||
for (var id in collected[atrule]) {
|
||||
ast.children.insertList(
|
||||
collected[atrule][id],
|
||||
atrule === 'media' ? null : topInjectPoint
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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.prelude &&
|
||||
prev.prelude &&
|
||||
node.prelude.id === prev.prelude.id) {
|
||||
prev.block.children.appendList(node.block.children);
|
||||
list.remove(item);
|
||||
|
||||
// TODO: use it when we can refer to several points in source
|
||||
// prev.loc = {
|
||||
// primary: prev.loc,
|
||||
// merged: node.loc
|
||||
// };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = function rejoinAtrule(ast, options) {
|
||||
relocateAtrules(ast, options);
|
||||
|
||||
walk(ast, {
|
||||
visit: 'Atrule',
|
||||
reverse: true,
|
||||
enter: processAtrule
|
||||
});
|
||||
};
|
||||
Generated
Vendored
+47
@@ -0,0 +1,47 @@
|
||||
var walk = require('css-tree').walk;
|
||||
var utils = require('./utils');
|
||||
|
||||
function processRule(node, item, list) {
|
||||
var selectors = node.prelude.children;
|
||||
var declarations = node.block.children;
|
||||
|
||||
list.prevUntil(item.prev, function(prev) {
|
||||
// skip non-ruleset node if safe
|
||||
if (prev.type !== 'Rule') {
|
||||
return utils.unsafeToSkipNode.call(selectors, prev);
|
||||
}
|
||||
|
||||
var prevSelectors = prev.prelude.children;
|
||||
var prevDeclarations = prev.block.children;
|
||||
|
||||
// try to join rulesets with equal pseudo signature
|
||||
if (node.pseudoSignature === prev.pseudoSignature) {
|
||||
// try to join by selectors
|
||||
if (utils.isEqualSelectors(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 initialMergeRule(ast) {
|
||||
walk(ast, {
|
||||
visit: 'Rule',
|
||||
enter: processRule
|
||||
});
|
||||
};
|
||||
Generated
Vendored
+42
@@ -0,0 +1,42 @@
|
||||
var List = require('css-tree').List;
|
||||
var walk = require('css-tree').walk;
|
||||
|
||||
function processRule(node, item, list) {
|
||||
var selectors = node.prelude.children;
|
||||
|
||||
// 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: 'Rule',
|
||||
loc: node.loc,
|
||||
prelude: {
|
||||
type: 'SelectorList',
|
||||
loc: node.prelude.loc,
|
||||
children: newSelectors
|
||||
},
|
||||
block: {
|
||||
type: 'Block',
|
||||
loc: node.block.loc,
|
||||
children: node.block.children.copy()
|
||||
},
|
||||
pseudoSignature: node.pseudoSignature
|
||||
}), item);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = function disjoinRule(ast) {
|
||||
walk(ast, {
|
||||
visit: 'Rule',
|
||||
reverse: true,
|
||||
enter: processRule
|
||||
});
|
||||
};
|
||||
Generated
Vendored
+432
@@ -0,0 +1,432 @@
|
||||
var List = require('css-tree').List;
|
||||
var generate = require('css-tree').generate;
|
||||
var walk = require('css-tree').walk;
|
||||
|
||||
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.loc = null;
|
||||
this.iehack = undefined;
|
||||
this.sides = {
|
||||
'top': null,
|
||||
'right': null,
|
||||
'bottom': null,
|
||||
'left': null
|
||||
};
|
||||
}
|
||||
|
||||
TRBL.prototype.getValueSequence = function(declaration, count) {
|
||||
var values = [];
|
||||
var iehack = '';
|
||||
var hasBadValues = declaration.value.type !== 'Value' || declaration.value.children.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':
|
||||
if (child.name === 'var') {
|
||||
return true;
|
||||
}
|
||||
|
||||
special = child.name;
|
||||
break;
|
||||
|
||||
case 'WhiteSpace':
|
||||
return false; // ignore space
|
||||
|
||||
default:
|
||||
return true; // bad value
|
||||
}
|
||||
|
||||
values.push({
|
||||
node: child,
|
||||
special: special,
|
||||
important: declaration.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, declaration) {
|
||||
function attemptToAdd() {
|
||||
var sides = this.sides;
|
||||
var side = SIDE[name];
|
||||
|
||||
if (side) {
|
||||
if (side in sides === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var values = this.getValueSequence(declaration, 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(declaration, 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;
|
||||
}
|
||||
|
||||
// TODO: use it when we can refer to several points in source
|
||||
// if (this.loc) {
|
||||
// this.loc = {
|
||||
// primary: this.loc,
|
||||
// merged: declaration.loc
|
||||
// };
|
||||
// } else {
|
||||
// this.loc = declaration.loc;
|
||||
// }
|
||||
if (!this.loc) {
|
||||
this.loc = declaration.loc;
|
||||
}
|
||||
|
||||
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 = new List();
|
||||
var sides = this.sides;
|
||||
var values = [
|
||||
sides.top,
|
||||
sides.right,
|
||||
sides.bottom,
|
||||
sides.left
|
||||
];
|
||||
var stringValues = [
|
||||
generate(sides.top.node),
|
||||
generate(sides.right.node),
|
||||
generate(sides.bottom.node),
|
||||
generate(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.appendData({ type: 'WhiteSpace', value: ' ' });
|
||||
}
|
||||
|
||||
result.appendData(values[i].node);
|
||||
}
|
||||
|
||||
if (this.iehack) {
|
||||
result.appendData({ type: 'WhiteSpace', value: ' ' });
|
||||
result.appendData({
|
||||
type: 'Identifier',
|
||||
loc: null,
|
||||
name: this.iehack
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'Value',
|
||||
loc: null,
|
||||
children: result
|
||||
};
|
||||
};
|
||||
|
||||
TRBL.prototype.getDeclaration = function() {
|
||||
return {
|
||||
type: 'Declaration',
|
||||
loc: this.loc,
|
||||
important: this.sides.top.important,
|
||||
property: this.name,
|
||||
value: this.getValue()
|
||||
};
|
||||
};
|
||||
|
||||
function processRule(rule, shorts, shortDeclarations, lastShortSelector) {
|
||||
var declarations = rule.block.children;
|
||||
var selector = rule.prelude.children.first().id;
|
||||
|
||||
rule.block.children.eachRight(function(declaration, item) {
|
||||
var property = declaration.property;
|
||||
|
||||
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)) {
|
||||
operation = REPLACE;
|
||||
shorthand = new TRBL(key);
|
||||
|
||||
// if can't parse value ignore it and break shorthand children
|
||||
if (!shorthand.add(property, declaration)) {
|
||||
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(shorthand.getDeclaration());
|
||||
} else {
|
||||
item.block.remove(item.item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = function restructBlock(ast, indexer) {
|
||||
var stylesheetMap = {};
|
||||
var shortDeclarations = [];
|
||||
|
||||
walk(ast, {
|
||||
visit: 'Rule',
|
||||
reverse: true,
|
||||
enter: function(node) {
|
||||
var stylesheet = this.block || this.stylesheet;
|
||||
var ruleId = (node.pseudoSignature || '') + '|' + node.prelude.children.first().id;
|
||||
var ruleMap;
|
||||
var shorts;
|
||||
|
||||
if (!stylesheetMap.hasOwnProperty(stylesheet.id)) {
|
||||
ruleMap = {
|
||||
lastShortSelector: null
|
||||
};
|
||||
stylesheetMap[stylesheet.id] = ruleMap;
|
||||
} else {
|
||||
ruleMap = stylesheetMap[stylesheet.id];
|
||||
}
|
||||
|
||||
if (ruleMap.hasOwnProperty(ruleId)) {
|
||||
shorts = ruleMap[ruleId];
|
||||
} else {
|
||||
shorts = {};
|
||||
ruleMap[ruleId] = shorts;
|
||||
}
|
||||
|
||||
ruleMap.lastShortSelector = processRule.call(this, node, shorts, shortDeclarations, ruleMap.lastShortSelector);
|
||||
}
|
||||
});
|
||||
|
||||
processShorthands(shortDeclarations, indexer.declaration);
|
||||
};
|
||||
Generated
Vendored
+300
@@ -0,0 +1,300 @@
|
||||
var resolveProperty = require('css-tree').property;
|
||||
var resolveKeyword = require('css-tree').keyword;
|
||||
var walk = require('css-tree').walk;
|
||||
var generate = require('css-tree').generate;
|
||||
var fingerprintId = 1;
|
||||
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 SAFE_VALUES = {
|
||||
cursor: [
|
||||
'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'
|
||||
],
|
||||
overflow: [
|
||||
'hidden', 'visible', 'scroll', 'auto'
|
||||
],
|
||||
position: [
|
||||
'static', 'relative', 'absolute', 'fixed'
|
||||
]
|
||||
};
|
||||
|
||||
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).basename;
|
||||
|
||||
if (realName === 'background') {
|
||||
return propertyName + ':' + generate(declaration.value);
|
||||
}
|
||||
|
||||
var declarationId = declaration.id;
|
||||
var fingerprint = fingerprints[declarationId];
|
||||
|
||||
if (!fingerprint) {
|
||||
switch (declaration.value.type) {
|
||||
case 'Value':
|
||||
var vendorId = '';
|
||||
var iehack = '';
|
||||
var special = {};
|
||||
var raw = false;
|
||||
|
||||
declaration.value.children.each(function walk(node) {
|
||||
switch (node.type) {
|
||||
case 'Value':
|
||||
case 'Brackets':
|
||||
case 'Parentheses':
|
||||
node.children.each(walk);
|
||||
break;
|
||||
|
||||
case 'Raw':
|
||||
raw = true;
|
||||
break;
|
||||
|
||||
case 'Identifier':
|
||||
var name = node.name;
|
||||
|
||||
if (!vendorId) {
|
||||
vendorId = resolveKeyword(name).vendor;
|
||||
}
|
||||
|
||||
if (/\\[09]/.test(name)) {
|
||||
iehack = RegExp.lastMatch;
|
||||
}
|
||||
|
||||
if (SAFE_VALUES.hasOwnProperty(realName)) {
|
||||
if (SAFE_VALUES[realName].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
|
||||
var hasComma = node.children.some(function(node) {
|
||||
return node.type === 'Operator' && node.value === ',';
|
||||
});
|
||||
if (!hasComma) {
|
||||
name = 'rect-backward';
|
||||
}
|
||||
}
|
||||
|
||||
special[name + '()'] = true;
|
||||
|
||||
// check nested tokens too
|
||||
node.children.each(walk);
|
||||
|
||||
break;
|
||||
|
||||
case 'Dimension':
|
||||
var unit = node.unit;
|
||||
|
||||
if (/\\[09]/.test(unit)) {
|
||||
iehack = RegExp.lastMatch;
|
||||
}
|
||||
|
||||
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 = raw
|
||||
? '!' + fingerprintId++
|
||||
: '!' + Object.keys(special).sort() + '|' + iehack + vendorId;
|
||||
break;
|
||||
|
||||
case 'Raw':
|
||||
fingerprint = '!' + declaration.value.value;
|
||||
break;
|
||||
|
||||
default:
|
||||
fingerprint = generate(declaration.value);
|
||||
}
|
||||
|
||||
fingerprints[declarationId] = fingerprint;
|
||||
}
|
||||
|
||||
return propertyName + fingerprint;
|
||||
}
|
||||
|
||||
function needless(props, declaration, fingerprints) {
|
||||
var property = resolveProperty(declaration.property);
|
||||
|
||||
if (NEEDLESS_TABLE.hasOwnProperty(property.basename)) {
|
||||
var table = NEEDLESS_TABLE[property.basename];
|
||||
|
||||
for (var i = 0; i < table.length; i++) {
|
||||
var ppre = getPropertyFingerprint(property.prefix + table[i], declaration, fingerprints);
|
||||
var prev = props.hasOwnProperty(ppre) ? props[ppre] : null;
|
||||
|
||||
if (prev && (!declaration.important || prev.item.data.important)) {
|
||||
return prev;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function processRule(rule, item, list, props, fingerprints) {
|
||||
var declarations = rule.block.children;
|
||||
|
||||
declarations.eachRight(function(declaration, declarationItem) {
|
||||
var property = declaration.property;
|
||||
var fingerprint = getPropertyFingerprint(property, declaration, fingerprints);
|
||||
var prev = props[fingerprint];
|
||||
|
||||
if (prev && !dontRestructure.hasOwnProperty(property)) {
|
||||
if (declaration.important && !prev.item.data.important) {
|
||||
props[fingerprint] = {
|
||||
block: declarations,
|
||||
item: declarationItem
|
||||
};
|
||||
|
||||
prev.block.remove(prev.item);
|
||||
|
||||
// TODO: use it when we can refer to several points in source
|
||||
// declaration.loc = {
|
||||
// primary: declaration.loc,
|
||||
// merged: prev.item.data.loc
|
||||
// };
|
||||
} else {
|
||||
declarations.remove(declarationItem);
|
||||
|
||||
// TODO: use it when we can refer to several points in source
|
||||
// prev.item.data.loc = {
|
||||
// primary: prev.item.data.loc,
|
||||
// merged: declaration.loc
|
||||
// };
|
||||
}
|
||||
} else {
|
||||
var prev = needless(props, declaration, fingerprints);
|
||||
|
||||
if (prev) {
|
||||
declarations.remove(declarationItem);
|
||||
|
||||
// TODO: use it when we can refer to several points in source
|
||||
// prev.item.data.loc = {
|
||||
// primary: prev.item.data.loc,
|
||||
// merged: declaration.loc
|
||||
// };
|
||||
} 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);
|
||||
|
||||
walk(ast, {
|
||||
visit: 'Rule',
|
||||
reverse: true,
|
||||
enter: function(node, item, list) {
|
||||
var stylesheet = this.block || this.stylesheet;
|
||||
var ruleId = (node.pseudoSignature || '') + '|' + node.prelude.children.first().id;
|
||||
var ruleMap;
|
||||
var props;
|
||||
|
||||
if (!stylesheetMap.hasOwnProperty(stylesheet.id)) {
|
||||
ruleMap = {};
|
||||
stylesheetMap[stylesheet.id] = ruleMap;
|
||||
} else {
|
||||
ruleMap = stylesheetMap[stylesheet.id];
|
||||
}
|
||||
|
||||
if (ruleMap.hasOwnProperty(ruleId)) {
|
||||
props = ruleMap[ruleId];
|
||||
} else {
|
||||
props = {};
|
||||
ruleMap[ruleId] = props;
|
||||
}
|
||||
|
||||
processRule.call(this, node, item, list, props, fingerprints);
|
||||
}
|
||||
});
|
||||
};
|
||||
Generated
Vendored
+86
@@ -0,0 +1,86 @@
|
||||
var walk = require('css-tree').walk;
|
||||
var utils = require('./utils');
|
||||
|
||||
/*
|
||||
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 processRule(node, item, list) {
|
||||
var selectors = node.prelude.children;
|
||||
var declarations = node.block.children;
|
||||
var nodeCompareMarker = selectors.first().compareMarker;
|
||||
var skippedCompareMarkers = {};
|
||||
|
||||
list.nextUntil(item.next, function(next, nextItem) {
|
||||
// skip non-ruleset node if safe
|
||||
if (next.type !== 'Rule') {
|
||||
return utils.unsafeToSkipNode.call(selectors, next);
|
||||
}
|
||||
|
||||
if (node.pseudoSignature !== next.pseudoSignature) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var nextFirstSelector = next.prelude.children.head;
|
||||
var nextDeclarations = next.block.children;
|
||||
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 mergeRule(ast) {
|
||||
walk(ast, {
|
||||
visit: 'Rule',
|
||||
enter: processRule
|
||||
});
|
||||
};
|
||||
Generated
Vendored
+177
@@ -0,0 +1,177 @@
|
||||
var List = require('css-tree').List;
|
||||
var walk = require('css-tree').walk;
|
||||
var utils = require('./utils');
|
||||
|
||||
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 processRule(node, item, list) {
|
||||
var avoidRulesMerge = this.block !== null ? this.block.avoidRulesMerge : false;
|
||||
var selectors = node.prelude.children;
|
||||
var block = node.block;
|
||||
var disallowDownMarkers = Object.create(null);
|
||||
var allowMergeUp = true;
|
||||
var allowMergeDown = true;
|
||||
|
||||
list.prevUntil(item.prev, function(prev, prevItem) {
|
||||
var prevBlock = prev.block;
|
||||
var prevType = prev.type;
|
||||
|
||||
if (prevType !== 'Rule') {
|
||||
var unsafe = utils.unsafeToSkipNode.call(selectors, prev);
|
||||
|
||||
if (!unsafe && prevType === 'Atrule' && prevBlock) {
|
||||
walk(prevBlock, {
|
||||
visit: 'Rule',
|
||||
enter: function(node) {
|
||||
node.prelude.children.each(function(data) {
|
||||
disallowDownMarkers[data.compareMarker] = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return unsafe;
|
||||
}
|
||||
|
||||
var prevSelectors = prev.prelude.children;
|
||||
|
||||
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.isEqualSelectors(prevSelectors, selectors)) {
|
||||
prevBlock.children.appendList(block.children);
|
||||
list.remove(item);
|
||||
return true;
|
||||
}
|
||||
|
||||
// try to join by properties
|
||||
var diff = utils.compareDeclarations(block.children, prevBlock.children);
|
||||
|
||||
// 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.children = new List().fromArray(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.children = new List().fromArray(diff.ne2);
|
||||
}
|
||||
} else {
|
||||
// diff.ne1.length && diff.ne2.length
|
||||
// extract equal block
|
||||
var newSelector = {
|
||||
type: 'SelectorList',
|
||||
loc: null,
|
||||
children: utils.addSelectors(prevSelectors.copy(), selectors)
|
||||
};
|
||||
var newBlockLength = calcSelectorLength(newSelector.children) + 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 (blockLength >= newBlockLength) {
|
||||
var newItem = list.createItem({
|
||||
type: 'Rule',
|
||||
loc: null,
|
||||
prelude: newSelector,
|
||||
block: {
|
||||
type: 'Block',
|
||||
loc: null,
|
||||
children: new List().fromArray(diff.eq)
|
||||
},
|
||||
pseudoSignature: node.pseudoSignature
|
||||
});
|
||||
|
||||
block.children = new List().fromArray(diff.ne1);
|
||||
prevBlock.children = new List().fromArray(diff.ne2overrided);
|
||||
|
||||
if (allowMergeUp) {
|
||||
list.insert(newItem, prevItem);
|
||||
} else {
|
||||
list.insert(newItem, item);
|
||||
}
|
||||
|
||||
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 restructRule(ast) {
|
||||
walk(ast, {
|
||||
visit: 'Rule',
|
||||
reverse: true,
|
||||
enter: processRule
|
||||
});
|
||||
};
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
var prepare = require('./prepare/index');
|
||||
var mergeAtrule = require('./1-mergeAtrule');
|
||||
var initialMergeRuleset = require('./2-initialMergeRuleset');
|
||||
var disjoinRuleset = require('./3-disjoinRuleset');
|
||||
var restructShorthand = require('./4-restructShorthand');
|
||||
var restructBlock = require('./6-restructBlock');
|
||||
var mergeRuleset = require('./7-mergeRuleset');
|
||||
var restructRuleset = require('./8-restructRuleset');
|
||||
|
||||
module.exports = function(ast, options) {
|
||||
// prepare ast for restructing
|
||||
var indexer = prepare(ast, options);
|
||||
options.logger('prepare', ast);
|
||||
|
||||
mergeAtrule(ast, options);
|
||||
options.logger('mergeAtrule', ast);
|
||||
|
||||
initialMergeRuleset(ast);
|
||||
options.logger('initialMergeRuleset', ast);
|
||||
|
||||
disjoinRuleset(ast);
|
||||
options.logger('disjoinRuleset', ast);
|
||||
|
||||
restructShorthand(ast, indexer);
|
||||
options.logger('restructShorthand', ast);
|
||||
|
||||
restructBlock(ast);
|
||||
options.logger('restructBlock', ast);
|
||||
|
||||
mergeRuleset(ast);
|
||||
options.logger('mergeRuleset', ast);
|
||||
|
||||
restructRuleset(ast);
|
||||
options.logger('restructRuleset', ast);
|
||||
};
|
||||
Generated
Vendored
+31
@@ -0,0 +1,31 @@
|
||||
var generate = require('css-tree').generate;
|
||||
|
||||
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 ids = new Index();
|
||||
|
||||
return function markDeclaration(node) {
|
||||
var id = generate(node);
|
||||
|
||||
node.id = ids.resolve(id);
|
||||
node.length = id.length;
|
||||
node.fingerprint = null;
|
||||
|
||||
return node;
|
||||
};
|
||||
};
|
||||
Generated
Vendored
+43
@@ -0,0 +1,43 @@
|
||||
var resolveKeyword = require('css-tree').keyword;
|
||||
var walk = require('css-tree').walk;
|
||||
var generate = require('css-tree').generate;
|
||||
var createDeclarationIndexer = require('./createDeclarationIndexer');
|
||||
var processSelector = require('./processSelector');
|
||||
|
||||
module.exports = function prepare(ast, options) {
|
||||
var markDeclaration = createDeclarationIndexer();
|
||||
|
||||
walk(ast, {
|
||||
visit: 'Rule',
|
||||
enter: function processRule(node) {
|
||||
node.block.children.each(markDeclaration);
|
||||
processSelector(node, options.usage);
|
||||
}
|
||||
});
|
||||
|
||||
walk(ast, {
|
||||
visit: 'Atrule',
|
||||
enter: function(node) {
|
||||
if (node.prelude) {
|
||||
node.prelude.id = null; // pre-init property to avoid multiple hidden class for generate
|
||||
node.prelude.id = generate(node.prelude);
|
||||
}
|
||||
|
||||
// compare keyframe selectors by its values
|
||||
// NOTE: still no clarification about problems with keyframes selector grouping (issue #197)
|
||||
if (resolveKeyword(node.name).basename === 'keyframes') {
|
||||
node.block.avoidRulesMerge = true; /* probably we don't need to prevent those merges for @keyframes
|
||||
TODO: need to be checked */
|
||||
node.block.children.each(function(rule) {
|
||||
rule.prelude.children.each(function(simpleselector) {
|
||||
simpleselector.compareMarker = simpleselector.id;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
declaration: markDeclaration
|
||||
};
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user