draft: refactor project so it sits on top of a standalone app
This commit is contained in:
+75
@@ -0,0 +1,75 @@
|
||||
# replace default config
|
||||
|
||||
# multipass: true
|
||||
# full: true
|
||||
|
||||
plugins:
|
||||
|
||||
# - name
|
||||
#
|
||||
# or:
|
||||
# - name: false
|
||||
# - name: true
|
||||
#
|
||||
# or:
|
||||
# - name:
|
||||
# param1: 1
|
||||
# param2: 2
|
||||
|
||||
- removeDoctype
|
||||
- removeXMLProcInst
|
||||
- removeComments
|
||||
- removeMetadata
|
||||
- removeXMLNS
|
||||
- removeEditorsNSData
|
||||
- cleanupAttrs
|
||||
- inlineStyles
|
||||
- minifyStyles
|
||||
- convertStyleToAttrs
|
||||
- cleanupIDs
|
||||
- prefixIds
|
||||
- removeRasterImages
|
||||
- removeUselessDefs
|
||||
- cleanupNumericValues
|
||||
- cleanupListOfValues
|
||||
- convertColors
|
||||
- removeUnknownsAndDefaults
|
||||
- removeNonInheritableGroupAttrs
|
||||
- removeUselessStrokeAndFill
|
||||
- removeViewBox
|
||||
- cleanupEnableBackground
|
||||
- removeHiddenElems
|
||||
- removeEmptyText
|
||||
- convertShapeToPath
|
||||
- convertEllipseToCircle
|
||||
- moveElemsAttrsToGroup
|
||||
- moveGroupAttrsToElems
|
||||
- collapseGroups
|
||||
- convertPathData
|
||||
- convertTransform
|
||||
- removeEmptyAttrs
|
||||
- removeEmptyContainers
|
||||
- mergePaths
|
||||
- removeUnusedNS
|
||||
- sortAttrs
|
||||
- sortDefsChildren
|
||||
- removeTitle
|
||||
- removeDesc
|
||||
- removeDimensions
|
||||
- removeAttrs
|
||||
- removeAttributesBySelector
|
||||
- removeElementsByAttr
|
||||
- addClassesToSVGElement
|
||||
- removeStyleElement
|
||||
- removeScriptElement
|
||||
- addAttributesToSVGElement
|
||||
- removeOffCanvasPaths
|
||||
- reusePaths
|
||||
|
||||
# configure the indent (default 4 spaces) used by `--pretty` here:
|
||||
#
|
||||
# @see https://github.com/svg/svgo/blob/master/lib/svgo/js2svg.js#L6 for more config options
|
||||
#
|
||||
# js2svg:
|
||||
# pretty: true
|
||||
# indent: ' '
|
||||
+533
@@ -0,0 +1,533 @@
|
||||
### [ [>](https://github.com/svg/svgo/tree/v1.3.2) ] 1.3.2 / 30.10.2019
|
||||
* Fixed TypeError: Cannot set property 'multipassCount' of undefined
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v1.3.1) ] 1.3.1 / 29.10.2019
|
||||
* Updated CSSO version to 4.0.2 fixing the issue with empty semicolons ";;" in styles (thanks to @strarsis and @lahmatiy).
|
||||
* `prefixIds` plugin now runs only once with `--multipass` option (by @strarsis).
|
||||
* `cleanupIDs` plugin is prevented from producing a preserved ID, including one which matches a preserved prefix, when minifying (by @thomsj).
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v1.3.0) ] 1.3.0 / 14.07.2019
|
||||
* Custom plugins now can be loaded from external js through `path` plugin param.
|
||||
* New plugin `convertEllipseToCircle` to convert ellipse with equal radius measures to circle (by @tigt).
|
||||
* New plugin `sortDefsChildren` for improved compression (by @davidleston).
|
||||
* SVGO now removes unnecessary spaces after `arcto` path command flags.
|
||||
* `removeDimensions` plugin now adds `viewBox` if it's missing (by @adipascu).
|
||||
* Fixed `removeUnusedNS` not counting attributes in `<svg>` tag itself.
|
||||
* Fixed an issue with incorrect processing multiple images (by @cyberalien).
|
||||
* Fixed an error with incorrect converting multiple segmented curve to an arc.
|
||||
* Fixed an error with matrix decomposition in `convertTransform` due to rounding error leading to illegal value.
|
||||
* Added `force` option for `mergePaths` plugin (by @goyney).
|
||||
* Added options to `prefixIds` plugin for selectively prefixing IDs and/or classes (by @strarsis).
|
||||
* Exported config function (by @1000ch).
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v1.2.2) ] 1.2.2 / 16.04.2019
|
||||
* Update js-yaml for Code Injection warning (by @kaungst).
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v1.2.1) ] 1.2.1 / 04.04.2019
|
||||
Some goodness from pull-requests.
|
||||
* Bump up js-yaml version to fix DoS vulnerability (by @eugestarr).
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v1.2.0) ] 1.2.0 / 24.02.2019
|
||||
Some goodness from pull-requests.
|
||||
* Fixed extra blank lines when processing many files (by @panczarny).
|
||||
* Added `--recursive` option to process folders recursevely with option `-f` (by @dartess).
|
||||
* Added `removeAttributesBySelector` plugin to remove elements matching a css selector (by @bmease).
|
||||
* Added `removeOffCanvasPaths` plugin to remove elements outside of the viewbox (by @JoshyPHP).
|
||||
* `removeAttrs` plugin: added `preserveCurrentColor` color (by @roblevintennis) and 3rd optional filter for a value (by @Herman-Freund).
|
||||
* Added `reusePaths` plugin to replace duplicated elements with link (by @jhowcrof).
|
||||
* Added support of comma-separated plugins list in `--disable` and `--enable` options (by @jmwebservices).
|
||||
* Added option to preserve IDs based on prefix in `cleanupIDs` plugin (by @bkotzz).
|
||||
* Replaced `colors` dependency with `chalk` (by @xPaw).
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v1.1.1) ] 1.1.1 / 17.09.2018
|
||||
* Fixed crash in `SVGO.optimize()` when ‘info’ is absent.
|
||||
* Removed extra space after `cleanupListOfValues` plugin.
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v1.1.0) ] 1.1.0 / 16.09.2018
|
||||
* Fixed `collapseGroups` plugin removing property with a child having `inherit` value.
|
||||
* `version` attribute value is not more being rounded.
|
||||
* Fixed jsAPI `clone` method with respect to the introduced CSS classes.
|
||||
* Fixed scaling strokes with `vector-effect="non-scaling-stroke"` (by @alexjlockwood).
|
||||
* Fixed passing properties from groups in `collapseGroups` plugin if child have a filter (by @stristr).
|
||||
* Fixed arc path commands parsing without separators after flags, effectively producing a JS error.
|
||||
* Fixed `viewBox` separators parsing.
|
||||
* Fixed `removeNonInheritableGroupAttrs` plugin to work as intended.
|
||||
* Fixed removing path segments without length in presence of `stroke-linecap`.
|
||||
* Fixed `removeUnknownsAndDefaults` plugin removing attributes from elements with `id`.
|
||||
* Fixed converting to large arcs from nearly straight lines curves.
|
||||
* Fixed `collapseGroups` plugin affecting `<switch>` and its subgroups.
|
||||
* Fixed `convertTransform` plugin converting to `rotate()` with wrong sign in some case.
|
||||
* Fixed `cleanupListOfValues` plugin not preserving non-numeric values.
|
||||
* Fixed `!important` being passed to attributes in `convertStyleToAttrs` plugin.
|
||||
* Added option `keepImportant` to `convertStyleToAttrs` plugin to preserve styles with `!important`.
|
||||
* `removeHiddenElems` plugin now also removes elements with `visibility="hidden"` attribute (by @mikolaj92).
|
||||
* Added `forceAbsolutePath` option to `convertPathData` plugin to always use absolute coordinates (by @cool).
|
||||
* Added `keepRoleAttr` for `removeUnknownsAndDefaults` plugin to preserve `role-` attributes (by @himedlooff).
|
||||
* Added `xmlns` order option in `sortAttrs` plugin (by @hellatan).
|
||||
* Added an option to `prefixIds` plugin to pass prefix as false or as a function that returns false (by @vzaidman).
|
||||
* `prefixIds` plugin now adds prefix to every class (by @vzaidman).
|
||||
* Updated and improved docs a bit (multiple authors).
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v1.0.5) ] 1.0.5 / 26.02.2018
|
||||
* Fixed issue with prefixIDs plugin not replacing url() values correctly (by @harrisjose).
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v1.0.4) ] 1.0.4 / 30.01.2018
|
||||
* Fixed bug with removing groups that are direct child of "<switch>".
|
||||
* Fixed bug with shorthand path points counting (thanks @alexjlockwood for noticing).
|
||||
* Fixed crash on parsing invalid transform, e.g. without close parenthesis.
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v1.0.3) ] 1.0.3 / 08.11.2017
|
||||
* Fixed `removeViewBox` plugin to check for zero start coordinates.
|
||||
* Removed extra info from STDOUT when it set to output.
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v1.0.2) ] 1.0.2 / 03.11.2017
|
||||
* Fixed a couple of errors related to `inlineStyles` plugin.
|
||||
* Updated some lost details in documentation to reflect v1.0 changes.
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v1.0.1) ] 1.0.1 / 31.10.2017
|
||||
* Fixed error “Object.defineProperty called on non-object” in images with `<foreignObject/>`.
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v1.0.0) ] 1.0.0 / 30.10.2017
|
||||
* SVGO now requires Node 4 or higher.
|
||||
* Changed CLI syntax to treat filenames as input, thus allowing `svgo *.svg` syntax.
|
||||
* `SVGO.optimize()` now returns `Promise`.
|
||||
* Added `datauri` option to JS API.
|
||||
* Added support for SVG 2 `href` attribute.
|
||||
* `cleanupIDs` now don't removes IDs if an image consists only of `defs`.
|
||||
* New plugin `inlineStyles` for converting styles from `<style>` element to attributes if possible (by @strarsis).
|
||||
* `cleanupNumericValues` now rounds values in `viewBox` (by @caub).
|
||||
* New plugin: `removeScriptElement` (disabled by default) to align with `removeStyleElement` (by @pklingem).
|
||||
* `minifyStyles` now removes styles based on usage with controlling options (by @lahmatiy).
|
||||
* New option `except` in `cleanupIDs` to keep IDs (by @Velenir).
|
||||
* New option `force` in `cleanupIDs` to work even if SVG contains `style` or `script` elements (by @Velenir).
|
||||
* Fixed arcs transforming with different signed `scale` parameters (by @JoshyPHP).
|
||||
* Fixed `removeUselessStrokeAndFill` to check for `style` or `script` elements per file (by @caub).
|
||||
* New option `keepAriaAttrs` in `removeUnknownsAndDefaults` (by @davidtheclark).
|
||||
* Corrected parsing in `cleanupIDs` to account animation syntax (by @caub).
|
||||
* `#ff0000` now converts to `red` as well as `#f00` (by @davidleston).
|
||||
* Added “gray” variation to colors list per CSS Color Module Level 4 (by @ydaniv).
|
||||
* Fixed error on empty files.
|
||||
* A separator character in `removeAttrs` now can be changed per `elemSeparator` option (by @mikestreety).
|
||||
* `addAttributesToSVGElement` now can add values to attributes.
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.7.2) ] 0.7.2 / 29.01.2017
|
||||
* Extended `currentColor` match conditions (string, rx, bool) (by @AlimovSV)
|
||||
* Fixed removing `<animate>` in `<stop>`.
|
||||
* Fixed removing same transform in inner element in `removeUnknownsAndDefaults`.
|
||||
* Fixed collapsing groups with same non-inheritable attribute.
|
||||
* Corrected removing of leading zero in case of exponential notation.
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.7.1) ] 0.7.1 / 27.09.2016
|
||||
* Reverted the requirement of Node.js to version 0.10.
|
||||
* Added `addAttributesToSVGElement` to the default config to allow using it with `--enable` option.
|
||||
* Added korean translation of “How it works” doc (by @primeiros).
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.7.0) ] 0.7.0 / 25.08.2016
|
||||
* Required Node.js version has increased to 0.12.
|
||||
* New plugins: `removeElementsByAttr` (by IDs or classes) by @elidupuis,
|
||||
`addAttributesToSVGElement` by @gjjones,
|
||||
`removeXMLNS` (for SVG inlining) by @ricardobeat.
|
||||
* Tests now correctly pass in Windows with CRLF line endings. Pretty print now accounts system line endings.
|
||||
* Fixed bugs with collapsing groups with masks and transforms in `collapseGroups`.
|
||||
* Fixed bugs with erroneous removing IDs in `cleanupIDs`.
|
||||
* Improved attributes sorting in `sortAttrs` by @darktrojan.
|
||||
* `addClassesToSVGElement` no more repeats classes (by @ricardobeat).
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.6.6) ] 0.6.6 / 25.04.2016
|
||||
* Corrected CSSO API usage
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.6.5) ] 0.6.5 / 25.04.2016
|
||||
* Extra content inserted by editors are now being removed within `<foreignObject>` as well thus fixing bug “Namespace prefix … is not defined“ after applying SVGO.
|
||||
* Doctype with entities declaration is now also being removed since svgo correctly parses them starting from the version [0.6.2](https://github.com/svg/svgo/tree/v0.6.2).
|
||||
* Corrected `moveGroupAttrsToElems` not to move attributes to `g` content if it's referenced (has an `id`).
|
||||
* `collapseGroups` now don't collapse a group if it has an animated attribute (SMIL).
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.6.4) ] 0.6.4 / 05.04.2016
|
||||
* Fixed bug in “[convertStyleToAttrs](https://github.com/svg/svgo/blob/master/plugins/convertStyleToAttrs.js)” plugin with converting styling properties to non-existent attributes (which are normally removed later by `removeUnknownsAndDefaults`).
|
||||
* Added `--indent` option to style pretty-printed SVG. (e.g. `--indent 2`) (by @scurker).
|
||||
* Added `currentColor` param to `convertColors` plugin for converting values like `fill` and `stroke` to `currentColor` (by @scurker).
|
||||
* Bumped CSSO to the current version and used [its new shiny API](https://github.com/css/csso#api) (thanks to @lahmatiy).
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.6.3) ] 0.6.3 / 20.03.2016
|
||||
* Smart rounding (introduced in 0.4.5) now applies only when rounding is needed, thus making subsequent passes more stable.
|
||||
* Fixed regression in converting curves to arcs.
|
||||
* `xlink:href` references are now being checked by local name `href`, thus correctly working with another namespace prefix.
|
||||
* Fixed `id` removing with disabled `plugins/convertStyleToAttrs.js`.
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.6.2) ] 0.6.2 / 08.03.2016
|
||||
* Better error handling and messaging improvements.
|
||||
* SVG files with XML entities (e.g. from Adobe Illustrator) are now correctly being parsed.
|
||||
* Fixed error on converting curves to arcs.
|
||||
* Corrected rounding in subsequent passes with `--multipass` option.
|
||||
* Data URI option now handles charset (by @holymonson)
|
||||
* Transformations are no longer moved to group if there is a mask (`plugins/moveElemsAttrsToGroup.js`).
|
||||
* Fixed matrix decomposition losing sign in case like `[1, 0, 0, -1, 0, 0]` (`scale(1 -1)`).
|
||||
* Fixed crash on uppercased color name.
|
||||
* Paths with `id` and without `stroke-width` aren't being transformed now since `stroke-width` may be applied later.
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.6.1) ] 0.6.1 / 21.11.2015
|
||||
* Added option `--quiet` to suppress output (by @phihag).
|
||||
* Removed `lib-cov` folder from the package, which was erroneously included before.
|
||||
* Fixed errors in “[minifyStyles](https://github.com/svg/svgo/blob/master/plugins/minifyStyles.js)” when there are `<style>` elements with `CDATA` content or without content at all.
|
||||
* Amended transform functions parsing to prevent errors when there are no separators between numbers (which isn't allowed by syntax, but understood by browsers).
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.6.0) ] 0.6.0 / 08.11.2015
|
||||
* New optimization: circular curves now being converted to arcs. A notable improvement for circles within paths.
|
||||
* New plugin “[minifyStyles](https://github.com/svg/svgo/blob/master/plugins/minifyStyles.js)” which minifies `<style>` elments content with CSSO by @strarsis (svgo still doesn't understand its content)
|
||||
* New plugin “[removeStyleElement](https://github.com/svg/svgo/blob/master/plugins/removeStyleElement.js)” (disabled by default) by @betsydupuis.
|
||||
* Fixed issues with parsing numbers with exponent fraction (could happen with high precision >= 7).
|
||||
* Fixed rounding error due to incorrect preserving of precision in transformations.
|
||||
* Fixed shorthand curve distortion due to converted previous curve to not a curve.
|
||||
* Fixed interoperability issue with `precision` cli-option and `full` config.
|
||||
* Fixed an error produced by “[removeUnknownsAndDefaults](https://github.com/svg/svgo/blob/master/plugins/removeUnknownsAndDefaults.js)” by @thiakil
|
||||
* Another Inkscape prefix namespace is being removed.
|
||||
* Fixed an issue in [moveElemsAttrsToGroup“](https://github.com/svg/svgo/blob/master/plugins/moveElemsAttrsToGroup“.js)” with transforms moved around `clip-path`.
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.5.6) ] 0.5.6 / 13.08.2015
|
||||
* Fixed paths removing.
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.5.5) ] 0.5.5 / 05.08.2015
|
||||
* Reverted debugging changes.
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.5.4) ] 0.5.4 / 05.08.2015
|
||||
* New parameter `useShortTags` by @bradbarrow. Now svgo can produce correct non-selfclosing tags (useful in HTML in old browsers).
|
||||
* Fixed failing on empty transformation (which could be produced by two opposite).
|
||||
* Fixed removing paths which have numbers with exponent notation.
|
||||
* Fixed a bug with arc transformation.
|
||||
* Some typo fixes.
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.5.3) ] 0.5.3 / 21.06.2015
|
||||
* Fixed breaking related to rounding functions in “[convertTransform](https://github.com/svg/svgo/blob/master/plugins/convertTransform.js)”.
|
||||
* Fixed a bug with ID in animations not being worked on by “[cleanupIDs](https://github.com/svg/svgo/blob/master/plugins/cleanupIDs.js)”.
|
||||
* Fixed a bug with quoted reference in `url()`.
|
||||
* Now, if there are several same IDs in the document, then the first one is used and others are being removed.
|
||||
* New command-line option `--show-plugins` displaying list of plugins.
|
||||
* Two new optional plugins: “[removeDimensions](https://github.com/svg/svgo/blob/master/plugins/removeDimensions.js)” (removes `width` and `height` if there is `viewBox`) and “[removeAttrsPlugin](https://github.com/svg/svgo/blob/master/plugins/removeAttrs.js)” (by @bennyschudel).
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.5.2) ] 0.5.2 / 24.05.2015
|
||||
* Introduced new `transformPrecision` option for better image quality (defaults to 5) in “[convertTransform](https://github.com/svg/svgo/blob/master/plugins/convertTransform.js)” and “[convertPathData](https://github.com/svg/svgo/blob/master/plugins/convertPathData.js)” (for the purpose of applying transformations) plugins.
|
||||
* Matrix transformations now can be decomposed into a combination of few simple transforms like `translate`, `rotate`, `scale`.
|
||||
* Arcs (paths `arcto` command) are now correctly being transformed into another arcs without being converting to Bezier curves.
|
||||
* Fixed an issue with “[mergePaths](https://github.com/svg/svgo/blob/master/plugins/mergePaths.js)” failing to detect paths intersection in some cases.
|
||||
* Fixed a bug with “[removeUnknownsAndDefaults](https://github.com/svg/svgo/blob/master/plugins/removeUnknownsAndDefaults.js)” removing some paths, which was introduced in [v0.5.1](https://github.com/svg/svgo/tree/v0.5.1).
|
||||
* Fixed a bug with transformation having `rotate()` with optional parameters.
|
||||
* Patterns with inherited attributes are no longer being removed.
|
||||
* Styles are no longer being removed from `<desc>` (by @dennari).
|
||||
* SVGO no longer breaks during parsing.
|
||||
* Added `clone()` method to JSAPI (by @jakearchibald)
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.5.1) ] 0.5.1 / 30.03.2015
|
||||
* added new command-line option to set precision in floating point numbers.
|
||||
* fixed all known image-disruptive bugs
|
||||
* Notably [mergePaths](https://github.com/svg/svgo/blob/master/plugins/mergePaths.js) plugin now checks for possible intersections to avoid side-effects
|
||||
* new plugin [removeUselessDefs](https://github.com/svg/svgo/blob/master/plugins/removeUselessDefs.js) to remove elements in ``<defs>`` and similar non-rendering elements without an ``id`` and thus cannot be used
|
||||
* fix for ``--multipass`` command line option (by @dfilatov)
|
||||
* improved [cleanupEnableBackground](https://github.com/svg/svgo/blob/master/plugins/cleanupEnableBackground.js) and [convertColors](https://github.com/svg/svgo/blob/master/plugins/convertColors.js) plugins (by @YetiOr)
|
||||
* new plugin for image manipulation [cleanupListOfValues](https://github.com/svg/svgo/blob/master/plugins/cleanupListOfValues.js) (by @kiyopikko)
|
||||
* fixed fail on comments after closing root ``</svg>`` tag
|
||||
* updated parsing to account meaningful spaces in ``<text>``
|
||||
* ``data-*`` attributes are now preserved in [removeUnknownsAndDefaults](https://github.com/svg/svgo/blob/master/plugins/removeUnknownsAndDefaults.js)
|
||||
* prevented plugins from failing in ``<foreignObject>``
|
||||
* [cleanupNumericValues](https://github.com/svg/svgo/blob/master/plugins/cleanupNumericValues.js) plugin now converts other units to pixels (if it's better)
|
||||
* [removeUselessStrokeAndFill](https://github.com/svg/svgo/blob/master/plugins/removeUselessStrokeAndFill.js) plugin is enabled again with correct work in case of inherited attributes
|
||||
* fixed fail on images with incorrect paths like ``<path d="z"/>``
|
||||
* svgo now understands if an input is a folder (remember, you can set output to folder as well)
|
||||
* added support for some properties from SVG 2 like ``vector-effect="non-scaling-stroke"``
|
||||
* removed option to remove an ``id`` on root ``<svg>`` tag in [removeUnknownsAndDefaults](https://github.com/svg/svgo/blob/master/plugins/removeUnknownsAndDefaults.js) since it's already being done in [cleanupIDs](https://github.com/svg/svgo/blob/master/plugins/cleanupIDs.js)
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.5.0) ] 0.5.0 / 05.11.2014
|
||||
* added ``--multipass`` command line option which repeatedly applies optimizations like collapsing groups (by @dfilatov)
|
||||
* exposed JSAPI as a factory method (by @mistakster)
|
||||
* added removeDesc plugin (by @dwabyick), disabled by default
|
||||
* [removeUselessStrokeAndFill](https://github.com/svg/svgo/blob/master/plugins/removeUselessStrokeAndFill.js) plugin is disabled by default since it's unable to check inherited properties
|
||||
* transformations now apply to paths with arcs in [plugins/convertPathData](https://github.com/svg/svgo/blob/master/plugins/convertPathData.js)
|
||||
* a lot of bug fixes mostly related to transformations
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.4.5) ] 0.4.5 / 02.08.2014
|
||||
* significally improved plugin [plugins/convertPathData](https://github.com/svg/svgo/blob/master/plugins/convertPathData.js):
|
||||
- Now data is being written relative or absolute whichever is shorter. You can turn it off by setting ``utilizeAbsolute`` to ``false``.
|
||||
- Smarter rounding: values like 2.499 now rounds to 2.5. Rounding now takes in account accumulutive error meaning that points will not be misplaced due to rounding more than it neccessary.
|
||||
- Fixed couple bugs.
|
||||
* ``--output`` option now can be a folder along with ``--folder``, thanks to @mako-taco.
|
||||
* [plugins/cleanupIDs](https://github.com/svg/svgo/blob/master/plugins/cleanupIDs.js) now have ``prefix`` option in case you want to combine multiple svg later (by @DanielMazurkiewicz).
|
||||
* Quotes now being escaped in attributes (by @ditesh).
|
||||
* Minor bugfixes.
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.4.4) ] 0.4.4 / 14.01.2014
|
||||
* new plugin [plugins/removeTitle](https://github.com/svg/svgo/blob/master/plugins/removeTitle.js) (disabled by default, close [#159](https://github.com/svg/svgo/issues/159))
|
||||
* plugins/convertPathData: skip data concatenation for z instruction in collapseRepeated
|
||||
* plugins/removeUnknownsAndDefaults: do not remove overriden attributes with default values (fix [#161](https://github.com/svg/svgo/issues/161) and [#168](https://github.com/svg/svgo/issues/168))
|
||||
* plugins/removeViewBox: disable by default (fix [#139](https://github.com/svg/svgo/issues/139))
|
||||
* update README with [gulp task](https://github.com/ben-eb/gulp-svgmin)
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.4.3) ] 0.4.3 / 02.01.2014
|
||||
* new plugin [plugins/convertShapeToPath](https://github.com/svg/svgo/blob/master/plugins/convertShapeToPath.js) (close [#96](https://github.com/svg/svgo/issues/96))
|
||||
* update sax version to fix [#140](https://github.com/svg/svgo/issues/140)
|
||||
* update deps
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.4.2) ] 0.4.2 / 19.12.2013
|
||||
* add `lcov.info` to npmignore
|
||||
* fix `js-yaml` version to suppress deprecation warning in stdout
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.4.1) ] 0.4.1 / 18.11.2013
|
||||
* node >=0.8.0
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.4.0) ] 0.4.0 / 18.11.2013
|
||||
* merge almost all pull-requests
|
||||
* update dependencies
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.3.7) ] 0.3.7 / 24.06.2013
|
||||
* do not remove `result` attribute from filter primitives (fix [#122](https://github.com/svg/svgo/issues/122))
|
||||
* plugins/cleanupAttrs: replace newline with space when needed (fix [#119](https://github.com/svg/svgo/issues/119))
|
||||
* lib/coa: look for config file in current folder
|
||||
* lib/coa: always traverse all files in the given folder
|
||||
* deprecate svgo-grunt in favor of [grunt-svgmin](https://github.com/sindresorhus/grunt-svgmin)
|
||||
* re-enable node-coveralls
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.3.6) ] 0.3.6 / 06.06.2013
|
||||
* plugins/removeNonInheritableGroupAttrs: more attrs groups to exclude (fix [#116](https://github.com/svg/svgo/issues/116) & [#118](https://github.com/svg/svgo/issues/118))
|
||||
* lib/coa: optimize folder file by file (temp fix [#114](https://github.com/svg/svgo/issues/114))
|
||||
* `.jshintrc`: JSHint 2.0
|
||||
* temporarily disable node-coveralls
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.3.5) ] 0.3.5 / 07.05.2013
|
||||
* plugins/transformsWithOnePath: fix curves bounding box calculation
|
||||
* plugins/transformsWithOnePath: fix possible c+t or q+s bug
|
||||
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.3.4) ] 0.3.4 / 06.05.2013
|
||||
* plugins/convertPathData: fix m->M bug in some cases
|
||||
* plugins/transformsWithOnePath: fix last point calculation for C/S/Q/T
|
||||
* plugins/mergePaths: add space delimiter between z and m
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.3.3) ] 0.3.3 / 05.05.2013
|
||||
* plugins/convertPathData: convert very first m to M, fix applyTransforms with translate() (fix [#112](https://github.com/svg/svgo/issues/112))
|
||||
* plugins/transformsWithOnePath: fix real width/height rounding; fix scale transform origin; reorder transforms
|
||||
* plugins/transformsWithOnePath: ability to set new width or height independently with auto rescaling
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.3.2) ] 0.3.2 / 03.05.2013
|
||||
* new plugin [plugins/sortAttrs](https://github.com/svg/svgo/blob/master/plugins/sortAttrs.js)
|
||||
* plugins/transformsWithOnePath: buggy hcrop (fix [#111](https://github.com/svg/svgo/issues/111))
|
||||
* Impossible to set output presision to 0 (no fractional part) (fix [#110](https://github.com/svg/svgo/issues/110))
|
||||
* Istanbul + coveralls.io
|
||||
* update README with NPM version from badge.fury.io
|
||||
* update README with dependency status from gemnasium.com
|
||||
* npmignore unneeded files
|
||||
* reoptimized project logo
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.3.1) ] 0.3.1 / 15.04.2013
|
||||
* plugins/transformsWithOnePath: resize SVG and automatically rescale inner Path
|
||||
* better errors handling
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.3.0) ] 0.3.0 / 12.04.2013
|
||||
* global refactoring: getting rid of the many dependencies
|
||||
* new plugin [plugins/mergePaths](https://github.com/svg/svgo/blob/master/plugins/mergePaths.js)
|
||||
* new plugin [plugins/transformsWithOnePath](https://github.com/svg/svgo/blob/master/plugins/transformsWithOnePath.js) (renamed and featured `cropAndCenterAlongPath`)
|
||||
* config: replace default config with `full: true`
|
||||
* coa: JSON string as value of `--config`
|
||||
* coa: different types of Data URI strings (close [#105](https://github.com/svg/svgo/issues/105))
|
||||
* plugins/_transforms: allow spaces at the beginning of transform
|
||||
* Travis CI: Nodejs 0.10 & 0.11
|
||||
* `node.extend` → `whet.extend`
|
||||
* update `.gitignore`
|
||||
* update docs
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.2.4) ] 0.2.4 / 05.04.2013
|
||||
* new plugin [plugins/cropAndCenterAlongPath](https://github.com/svg/svgo/blob/master/plugins/cropAndCenterAlongPath.js) for the [Fontello](https://github.com/fontello) project
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.2.3) ] 0.2.3 / 22.02.2013
|
||||
* new plugin [plugins/removeNonInheritableGroupAttrs](https://github.com/svg/svgo/blob/master/plugins/removeNonInheritableGroupAttrs.js) (fix [#101](https://github.com/svg/svgo/issues/101))
|
||||
* new plugin [plugins/removeRasterImages](https://github.com/svg/svgo/blob/master/plugins/removeRasterImages.js) (close [#98](https://github.com/svg/svgo/issues/98))
|
||||
* plugins/convertTransform: bug with trailing spaces in transform value string (fix [#103](https://github.com/svg/svgo/issues/103))
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.2.2) ] 0.2.2 / 09.02.2013
|
||||
* plugins/convertTransforms: wrong translate() shorthand (fix [#94](https://github.com/svg/svgo/issues/94))
|
||||
* [yaml.js](https://github.com/jeremyfa/yaml.js) → [js-yaml](https://github.com/nodeca/js-yaml)
|
||||
* update outdated deps
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.2.1) ] 0.2.1 / 18.01.2013
|
||||
* plugins/moveElemsAttrsToGroup + plugins/moveGroupAttrsToElems: move or just leave transform attr from Group to the inner Path Elems (close [#86](https://github.com/svg/svgo/issues/86))
|
||||
* plugins/removeViewBox: doesn't catch floating-point numbers (fix [#88](https://github.com/svg/svgo/issues/88))
|
||||
* plugins/cleanupEnableBackground: doesn't catch floating-point numbers (fix [#89](https://github.com/svg/svgo/issues/89))
|
||||
* plugins/cleanupNumericValues: wrong floating-point numbers regexp (fix [#92](https://github.com/svg/svgo/issues/92))
|
||||
* SVG file generated by fontcustom.com not properly compressed (fix [#90](https://github.com/svg/svgo/issues/90))
|
||||
* `README.ru.md`: стилизация русского языка, улучшение языковых конструкций, правка ошибок (close [#91](https://github.com/svg/svgo/issues/91))
|
||||
* minor JSHint warning fix
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.2.0) ] 0.2.0 / 23.12.2012
|
||||
* plugins/convertPathData: apply transforms to Path pata (close [#33](https://github.com/svg/svgo/issues/33))
|
||||
* plugins/convertPathData: `-1.816-9.278.682-13.604` parsing error (fix [#85](https://github.com/svg/svgo/issues/85))
|
||||
* plugins/convertTransform: `translate(10, 0)` eq `translate(10)`, but not `translate(10, 10)` eq `translate(10)` (fix [#83](https://github.com/svg/svgo/issues/83))
|
||||
* run plugins/cleanupIDs before plugins/collapseGroups (fix [#84](https://github.com/svg/svgo/issues/84))
|
||||
* update `.gitignore`
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.1.9) ] 0.1.9 / 17.12.2012
|
||||
* plugins/cleanupIDs: renamed from removeUnusedIDs; minify used IDs (fix [#7](https://github.com/svg/svgo/issues/7))
|
||||
* lib/svgo/js2svg: restore HTML entities back (fix [#80](https://github.com/svg/svgo/issues/80) + [#81](https://github.com/svg/svgo/issues/81))
|
||||
* plugins/removeDoctype: do not remove if custom XML entities presents (fix [#77](https://github.com/svg/svgo/issues/77))
|
||||
* lib/svgo/coa: refactoring, colors and fix [#70](https://github.com/svg/svgo/issues/70)
|
||||
* lib/svgo: store elapsed time in result object
|
||||
* usage examples with SVGZ (close [#18](https://github.com/svg/svgo/issues/18))
|
||||
* more optimized logo
|
||||
* update `.gitignore`
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.1.8) ] 0.1.8 / 11.12.2012
|
||||
* new plugin [plugins/removeUselessStrokeAndFill](https://github.com/svg/svgo/blob/master/plugins/removeUselessStrokeAndFill.js) (close [#75](https://github.com/svg/svgo/issues/75))
|
||||
* new plugin [plugins/removeUnusedIDs](https://github.com/svg/svgo/blob/master/plugins/removeUnusedIDs.js) (close [#76](https://github.com/svg/svgo/issues/76))
|
||||
* plugins/convertPathData: wrong M interpretation in some cases (fix [#73](https://github.com/svg/svgo/issues/73))
|
||||
* plugins/cleanupAttrs: use `isElem()` API
|
||||
* `.travis.yml`: check all branches
|
||||
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.1.7) ] 0.1.7 / 08.12.2012
|
||||
* plugins/convertPathData: incorrect interpretation of `z + m` (fix [#69](https://github.com/svg/svgo/issues/69))
|
||||
* plugins/convertTransform: do a more accurate floating numbers rounding in `matrixToTransform()` (fix [#68](https://github.com/svg/svgo/issues/68))
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.1.6) ] 0.1.6 / 07.12.2012
|
||||
* plugins/convertPathData: collapse repeated instructions only after curveSmoothShorthands (fix [#64](https://github.com/svg/svgo/issues/64))
|
||||
* lib/svgo/coa: handle 'there is nothing to optimize' case and display a message about it (fix [#61](https://github.com/svg/svgo/issues/61))
|
||||
* plugins/cleanupSVGElem: delete as useless artefact
|
||||
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.1.5) ] 0.1.5 / 06.12.2012
|
||||
* E-notated numbers in paths not recognised (fix [#63](https://github.com/svg/svgo/issues/63))
|
||||
* update README with `svgo-grunt` and `svgo-osx-folder-action`
|
||||
* fix `mocha-as-promised` plug in node 0.6
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.1.4) ] 0.1.4 / 05.12.2012
|
||||
* plugins/_collections: more defaults
|
||||
* `README.ru.md`
|
||||
* `docs/how-it-works/ru.md`
|
||||
* mocha + mocha-as-promised + chai + chai-as-promised + should + istanbul = <3
|
||||
* update dependencies semvers in `package.json`
|
||||
* `v0.1.x` and `v0.2.x` milestones
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.1.3) ] 0.1.3 / 30.11.2012
|
||||
* new plugin [plugins/cleanupNumericValues](https://github.com/svg/svgo/blob/master/plugins/cleanupNumericValues.js) (close [#8](https://github.com/svg/svgo/issues/8))
|
||||
* plugins/removeDefaultPx functionality now included in plugins/removeUnknownsAndDefaults
|
||||
* plugins/removeUnknownsAndDefaults: refactoring and picking up the complete elems+attrs collection (close [#59](https://github.com/svg/svgo/issues/59))
|
||||
* plugins/convertTransform: error in matrices multiplication (fix [#58](https://github.com/svg/svgo/issues/58))
|
||||
* plugins/convertTransform: mark translate() and scale() as useless only with one param (fix [#57](https://github.com/svg/svgo/issues/57))
|
||||
* plugins/convertPathData: drastic speed improvement with huge Path data
|
||||
* plugins/convertPathData: fix the very first Mm with multiple points (fix [#56](https://github.com/svg/svgo/issues/56))
|
||||
* plugins/moveElemsAttrsToGroup: additional check for transform attr
|
||||
* brand-new project `logo.svg`
|
||||
* `.travis.yml`: build only master branch
|
||||
* global `'use strict'`
|
||||
* `.jshintignore`
|
||||
* README and CHANGELOG: minor corrections
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.1.2) ] 0.1.2 / 24.11.2012
|
||||
* lib/svgo/svg2js: correct 'onerror' failure (fix [#51](https://github.com/svg/svgo/issues/51))
|
||||
* config: disable sax-js position tracking by default (fix [#52](https://github.com/svg/svgo/issues/52))
|
||||
* lib/svgo: rename 'startBytes' to 'inBytes' and 'endBytes' to 'outBytes' (close [#53](https://github.com/svg/svgo/issues/53))
|
||||
* plugins/removeUnknownsAndDefaults: remove SVG id attr (close [#54](https://github.com/svg/svgo/issues/54))
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.1.1) ] 0.1.1 / 23.11.2012
|
||||
* plugins/moveElemsAttrsToGroup: fix inheitable only attrs array (fix [#47](https://github.com/svg/svgo/issues/47))
|
||||
* plugins/removeEmptyContainers: do not remove an empty 'svg' element (fix [#48](https://github.com/svg/svgo/issues/48))
|
||||
* plugins/removeDefaultPx: should also understand a floating-numbers too (fix [#49](https://github.com/svg/svgo/issues/49))
|
||||
* plugins/removeUnknownsAndDefaults: merge multiple groupDefaults attrs (close [#50](https://github.com/svg/svgo/issues/50))
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.1.0) ] 0.1.0 / 22.11.2012
|
||||
* new plugin [plugins/removeUnknownsAndDefaults](https://github.com/svg/svgo/blob/master/plugins/removeUnknownsAndDefaults.js) (close [#6](https://github.com/svg/svgo/issues/6))
|
||||
* plugins/convertPathData: convert straight curves into lines segments (close [#17](https://github.com/svg/svgo/issues/17)); remove an absolute coords conversions
|
||||
* plugins/convertPathData: convert quadratic Bézier curveto into smooth shorthand (close [#31](https://github.com/svg/svgo/issues/31))
|
||||
* plugins/convertPathData: convert curveto into smooth shorthand (close [#30](https://github.com/svg/svgo/issues/30))
|
||||
* lib/svgo: global API refactoring (close [#37](https://github.com/svg/svgo/issues/37))
|
||||
* lib/svgo: fatal and stupid error in stream chunks concatenation (fix [#40](https://github.com/svg/svgo/issues/40))
|
||||
* lib/coa: batch folder optimization (close [#29](https://github.com/svg/svgo/issues/29))
|
||||
* lib/coa: support arguments as aliases to `--input` and `--output` (close [#28](https://github.com/svg/svgo/issues/28))
|
||||
* project logo by [Egor Bolhshakov](http://xizzzy.ru/)
|
||||
* move modules to `./lib/svgo/`
|
||||
* rename and convert `config.json` to `.svgo.yml`
|
||||
* add [./docs/](https://github.com/svg/svgo/tree/master/docs)
|
||||
* plugins/convertPathData: don't remove first `M` even if it's `0,0`
|
||||
* plugins/convertPathData: stronger defense from infinite loop
|
||||
* plugins/moveElemsAttrsToGroup: should affect only inheritable attributes (fix [#46](https://github.com/svg/svgo/issues/46))*
|
||||
* plugins/removeComments: ignore comments which starts with '!' (close [#43](https://github.com/svg/svgo/issues/43))
|
||||
* config: `cleanupAttrs` should be before `convertStyleToAttrs` (fix [#44](https://github.com/svg/svgo/issues/44))*
|
||||
* lib/svgo/jsAPI: add `eachAttr()` optional context param
|
||||
* temporarily remove PhantomJS and `--test` (close [#38](https://github.com/svg/svgo/issues/38))
|
||||
* q@0.8.10 compatibility: 'end is deprecated, use done instead' fix
|
||||
* add [Istanbul](https://github.com/gotwarlost/istanbul) code coverage
|
||||
* update dependencies versions and gitignore
|
||||
* README: add TODO section with versions milestones
|
||||
* update README with License section
|
||||
* update LICENSE with russian translation
|
||||
* `.editorconfig`: 2 spaces for YAML
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.0.9) ] 0.0.9 / 29.10.2012
|
||||
* [plugins how-to](https://github.com/svg/svgo/tree/master/plugins#readme) (close [#27](https://github.com/svg/svgo/issues/27))
|
||||
* allow any plugin of any type to go in any order (close [#14](https://github.com/svg/svgo/issues/14))
|
||||
* allow to do a multiple optimizations with one init (close [#25](https://github.com/svg/svgo/issues/25))
|
||||
* plugins/convertPathData: global refactoring
|
||||
* plugins/convertPathData: do all the tricks with absolute coords too (fix [#22](https://github.com/svg/svgo/issues/22))
|
||||
* plugins/convertPathData: accumulation of rounding errors (fix [#23](https://github.com/svg/svgo/issues/23))
|
||||
* plugins/convertPathData: prevent an infinity loop on invalid path data (fix [#26](https://github.com/svg/svgo/issues/26))
|
||||
* plugins/convertPathData: do not remove very first M from the path data (fix [#24](https://github.com/svg/svgo/issues/24))
|
||||
* plugins/convertPathData: optimize path data in <glyph> and <missing-glyph> (close [#20](https://github.com/svg/svgo/issues/20))
|
||||
* plugins/convertTransform: add patternTransform attribute to the process (close [#15](https://github.com/svg/svgo/issues/15))
|
||||
* plugins/convertTransform: Firefox: removing extra space in front of negative number is alowed only in path data, but not in transform (fix [#12](https://github.com/svg/svgo/issues/12))
|
||||
* plugins/removeXMLProcInst: remove only 'xml' but not 'xml-stylesheet' (fix [#21](https://github.com/svg/svgo/issues/15))
|
||||
* plugins/collapseGroups: merge split-level transforms (fix [#13](https://github.com/svg/svgo/issues/13))
|
||||
* jsdoc corrections
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.0.8) ] 0.0.8 / 20.10.2012
|
||||
* new plugin [convertTransform](plugins/convertTransform.js) (close [#5](https://github.com/svg/svgo/issues/5))
|
||||
* new plugin [removeUnusedNS](plugins/removeUnusedNS.js)
|
||||
* plugins/convertPathData: remove useless segments
|
||||
* plugins/convertPathData: a lot of refactoring
|
||||
* plugins/convertPathData: round numbers before conditions because of exponential notation (fix [#3](https://github.com/svg/svgo/issues/3))
|
||||
* plugins/moveElemsAttrsToGroup: merge split-level transforms instead of replacing (fix [#10](https://github.com/svg/svgo/issues/10))
|
||||
* lib/svg2js: catch and output xml parser errors (fix [#4](https://github.com/svg/svgo/issues/4))
|
||||
* lib/coa: open file for writing only when we are ready (fix [#2](https://github.com/svg/svgo/issues/2))
|
||||
* lib/tools: node.extend module
|
||||
* lib/plugins: refactoring
|
||||
* lib/js2svg: refactoring
|
||||
* lib/jsAPI: simplification and refactoring
|
||||
* absolute urls in README
|
||||
* update .editorconfig
|
||||
* update .travis.yml with nodejs 0.9
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.0.7) ] 0.0.7 / 14.10.2012
|
||||
* new plugin [convertPathData](plugins/convertPathData.js)
|
||||
* --input data now can be a Data URI base64 string
|
||||
* --output data now can be a Data URI base64 string with --datauri flag
|
||||
* Travis CI
|
||||
* JSHint corrections + .jshintrc
|
||||
* [.editorconfig](http://editorconfig.org/)
|
||||
* display time spent on optimization
|
||||
* .svgo → config.json
|
||||
* lib/phantom_wrapper.js → lib/phantom.js
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.0.6) ] 0.0.6 / 04.10.2012
|
||||
* add --test option to make a visual comparison of two files (PhantomJS pre-required)
|
||||
* update README and CHANGELOG with the correct relative urls
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.0.5) ] 0.0.5 / 03.10.2012
|
||||
* every plugin now has [at least one test](plugins)
|
||||
* removeViewBox, cleanupEnableBackground, removeEditorsNSData, convertStyleToAttrs and collapseGroups plugins fixes
|
||||
* new --pretty option for the pretty printed SVG
|
||||
* lib/config refactoring
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.0.4) ] 0.0.4 / 30.09.2012
|
||||
* new plugin [removeViewBox](plugins/removeViewBox.js)
|
||||
* new plugin [cleanupEnableBackground](plugins/cleanupEnableBackground.js)
|
||||
* display useful info after successful optimization
|
||||
* 'npm test' with 'spec' mocha output by default
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.0.3) ] 0.0.3 / 29.09.2012
|
||||
* plugins/collapseGroups bugfix
|
||||
* plugins/moveElemsAttrsToGroup bugfix
|
||||
* svgo now display --help if running w/o arguments
|
||||
* massive jsdoc updates
|
||||
* plugins engine main filter function optimization
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.0.2) ] 0.0.2 / 28.09.2012
|
||||
* add --disable and --enable command line options
|
||||
* add an empty values rejecting to coa.js
|
||||
* update README
|
||||
|
||||
### [ [>](https://github.com/svg/svgo/tree/v0.0.1) ] 0.0.1 / 27.09.2012
|
||||
* initial public version
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
The MIT License
|
||||
|
||||
Copyright © 2012–2016 Kir Belevich
|
||||
|
||||
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.
|
||||
|
||||
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
Лицензия MIT
|
||||
|
||||
Copyright © 2012–2016 Кир Белевич
|
||||
|
||||
Данная лицензия разрешает лицам, получившим копию данного
|
||||
программного обеспечения и сопутствующей документации
|
||||
(в дальнейшем именуемыми «Программное Обеспечение»), безвозмездно
|
||||
использовать Программное Обеспечение без ограничений, включая
|
||||
неограниченное право на использование, копирование, изменение,
|
||||
добавление, публикацию, распространение, сублицензирование
|
||||
и/или продажу копий Программного Обеспечения, также как и лицам,
|
||||
которым предоставляется данное Программное Обеспечение,
|
||||
при соблюдении следующих условий:
|
||||
|
||||
Указанное выше уведомление об авторском праве и данные условия
|
||||
должны быть включены во все копии или значимые части данного
|
||||
Программного Обеспечения.
|
||||
|
||||
ДАННОЕ ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ПРЕДОСТАВЛЯЕТСЯ «КАК ЕСТЬ»,
|
||||
БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ, ЯВНО ВЫРАЖЕННЫХ ИЛИ ПОДРАЗУМЕВАЕМЫХ,
|
||||
ВКЛЮЧАЯ, НО НЕ ОГРАНИЧИВАЯСЬ ГАРАНТИЯМИ ТОВАРНОЙ ПРИГОДНОСТИ,
|
||||
СООТВЕТСТВИЯ ПО ЕГО КОНКРЕТНОМУ НАЗНАЧЕНИЮ И ОТСУТСТВИЯ НАРУШЕНИЙ
|
||||
ПРАВ. НИ В КАКОМ СЛУЧАЕ АВТОРЫ ИЛИ ПРАВООБЛАДАТЕЛИ НЕ НЕСУТ
|
||||
ОТВЕТСТВЕННОСТИ ПО ИСКАМ О ВОЗМЕЩЕНИИ УЩЕРБА, УБЫТКОВ ИЛИ ДРУГИХ
|
||||
ТРЕБОВАНИЙ ПО ДЕЙСТВУЮЩИМ КОНТРАКТАМ, ДЕЛИКТАМ ИЛИ ИНОМУ,
|
||||
ВОЗНИКШИМ ИЗ, ИМЕЮЩИМ ПРИЧИНОЙ ИЛИ СВЯЗАННЫМ С ПРОГРАММНЫМ
|
||||
ОБЕСПЕЧЕНИЕМ ИЛИ ИСПОЛЬЗОВАНИЕМ ПРОГРАММНОГО ОБЕСПЕЧЕНИЯ
|
||||
ИЛИ ИНЫМИ ДЕЙСТВИЯМИ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ.
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
test:
|
||||
@npm run test
|
||||
|
||||
lib-cov:
|
||||
@./node_modules/.bin/istanbul instrument --output lib-cov --no-compact --variable global.__coverage__ lib
|
||||
|
||||
coverage: lib-cov
|
||||
@COVERAGE=1 ISTANBUL_REPORTERS=text-summary ./node_modules/.bin/mocha --reporter mocha-istanbul
|
||||
@rm -rf lib-cov
|
||||
|
||||
coveralls: lib-cov
|
||||
@COVERAGE=1 ISTANBUL_REPORTERS=lcovonly ./node_modules/.bin/mocha --reporter mocha-istanbul
|
||||
@cat lcov.info | ./node_modules/.bin/coveralls
|
||||
@rm -rf lib-cov lcov.info
|
||||
|
||||
travis: lint test coveralls
|
||||
|
||||
lint:
|
||||
@npm run lint
|
||||
|
||||
.PHONY: test
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
**english** | [русский](https://github.com/svg/svgo/blob/master/README.ru.md)
|
||||
- - -
|
||||
|
||||
<img src="https://svg.github.io/svgo-logo.svg" width="200" height="200" alt="logo"/>
|
||||
|
||||
## SVGO [](https://npmjs.org/package/svgo) [](https://travis-ci.org/svg/svgo) [](https://coveralls.io/r/svg/svgo?branch=master)
|
||||
|
||||
**SVG O**ptimizer is a Nodejs-based tool for optimizing SVG vector graphics files.
|
||||

|
||||
|
||||
## Why?
|
||||
|
||||
SVG files, especially those exported from various editors, usually contain a lot of redundant and useless information. This can include editor metadata, comments, hidden elements, default or non-optimal values and other stuff that can be safely removed or converted without affecting the SVG rendering result.
|
||||
|
||||
## What it can do
|
||||
|
||||
SVGO has a plugin-based architecture, so almost every optimization is a separate plugin.
|
||||
|
||||
Today we have:
|
||||
|
||||
| Plugin | Description | Default |
|
||||
| ------ | ----------- | ------- |
|
||||
| [cleanupAttrs](https://github.com/svg/svgo/blob/master/plugins/cleanupAttrs.js) | cleanup attributes from newlines, trailing, and repeating spaces | `enabled` |
|
||||
| [inlineStyles](https://github.com/svg/svgo/blob/master/plugins/inlineStyles.js) | move and merge styles from `<style>` elements to element `style` attributes | `enabled` |
|
||||
| [removeDoctype](https://github.com/svg/svgo/blob/master/plugins/removeDoctype.js) | remove doctype declaration | `enabled` |
|
||||
| [removeXMLProcInst](https://github.com/svg/svgo/blob/master/plugins/removeXMLProcInst.js) | remove XML processing instructions | `enabled` |
|
||||
| [removeComments](https://github.com/svg/svgo/blob/master/plugins/removeComments.js) | remove comments | `enabled` |
|
||||
| [removeMetadata](https://github.com/svg/svgo/blob/master/plugins/removeMetadata.js) | remove `<metadata>` | `enabled` |
|
||||
| [removeTitle](https://github.com/svg/svgo/blob/master/plugins/removeTitle.js) | remove `<title>` | `enabled` |
|
||||
| [removeDesc](https://github.com/svg/svgo/blob/master/plugins/removeDesc.js) | remove `<desc>` | `enabled` |
|
||||
| [removeUselessDefs](https://github.com/svg/svgo/blob/master/plugins/removeUselessDefs.js) | remove elements of `<defs>` without `id` | `enabled` |
|
||||
| [removeXMLNS](https://github.com/svg/svgo/blob/master/plugins/removeXMLNS.js) | removes `xmlns` attribute (for inline svg) | `disabled` |
|
||||
| [removeEditorsNSData](https://github.com/svg/svgo/blob/master/plugins/removeEditorsNSData.js) | remove editors namespaces, elements, and attributes | `enabled` |
|
||||
| [removeEmptyAttrs](https://github.com/svg/svgo/blob/master/plugins/removeEmptyAttrs.js) | remove empty attributes | `enabled` |
|
||||
| [removeHiddenElems](https://github.com/svg/svgo/blob/master/plugins/removeHiddenElems.js) | remove hidden elements | `enabled` |
|
||||
| [removeEmptyText](https://github.com/svg/svgo/blob/master/plugins/removeEmptyText.js) | remove empty Text elements | `enabled` |
|
||||
| [removeEmptyContainers](https://github.com/svg/svgo/blob/master/plugins/removeEmptyContainers.js) | remove empty Container elements | `enabled` |
|
||||
| [removeViewBox](https://github.com/svg/svgo/blob/master/plugins/removeViewBox.js) | remove `viewBox` attribute when possible | `enabled` |
|
||||
| [cleanupEnableBackground](https://github.com/svg/svgo/blob/master/plugins/cleanupEnableBackground.js) | remove or cleanup `enable-background` attribute when possible | `enabled` |
|
||||
| [minifyStyles](https://github.com/svg/svgo/blob/master/plugins/minifyStyles.js) | minify `<style>` elements content with [CSSO](https://github.com/css/csso) | `enabled` |
|
||||
| [convertStyleToAttrs](https://github.com/svg/svgo/blob/master/plugins/convertStyleToAttrs.js) | convert styles into attributes | `enabled` |
|
||||
| [convertColors](https://github.com/svg/svgo/blob/master/plugins/convertColors.js) | convert colors (from `rgb()` to `#rrggbb`, from `#rrggbb` to `#rgb`) | `enabled` |
|
||||
| [convertPathData](https://github.com/svg/svgo/blob/master/plugins/convertPathData.js) | convert Path data to relative or absolute (whichever is shorter), convert one segment to another, trim useless delimiters, smart rounding, and much more | `enabled` |
|
||||
| [convertTransform](https://github.com/svg/svgo/blob/master/plugins/convertTransform.js) | collapse multiple transforms into one, convert matrices to the short aliases, and much more | `enabled` |
|
||||
| [removeUnknownsAndDefaults](https://github.com/svg/svgo/blob/master/plugins/removeUnknownsAndDefaults.js) | remove unknown elements content and attributes, remove attrs with default values | `enabled` |
|
||||
| [removeNonInheritableGroupAttrs](https://github.com/svg/svgo/blob/master/plugins/removeNonInheritableGroupAttrs.js) | remove non-inheritable group's "presentation" attributes | `enabled` |
|
||||
| [removeUselessStrokeAndFill](https://github.com/svg/svgo/blob/master/plugins/removeUselessStrokeAndFill.js) | remove useless `stroke` and `fill` attrs | `enabled` |
|
||||
| [removeUnusedNS](https://github.com/svg/svgo/blob/master/plugins/removeUnusedNS.js) | remove unused namespaces declaration | `enabled` |
|
||||
| [prefixIds](https://github.com/svg/svgo/blob/master/plugins/prefixIds.js) | prefix IDs and classes with the SVG filename or an arbitrary string | `disabled` |
|
||||
| [cleanupIDs](https://github.com/svg/svgo/blob/master/plugins/cleanupIDs.js) | remove unused and minify used IDs | `enabled` |
|
||||
| [cleanupNumericValues](https://github.com/svg/svgo/blob/master/plugins/cleanupNumericValues.js) | round numeric values to the fixed precision, remove default `px` units | `enabled` |
|
||||
| [cleanupListOfValues](https://github.com/svg/svgo/blob/master/plugins/cleanupListOfValues.js) | round numeric values in attributes that take a list of numbers (like `viewBox` or `enable-background`) | `disabled` |
|
||||
| [moveElemsAttrsToGroup](https://github.com/svg/svgo/blob/master/plugins/moveElemsAttrsToGroup.js) | move elements' attributes to their enclosing group | `enabled` |
|
||||
| [moveGroupAttrsToElems](https://github.com/svg/svgo/blob/master/plugins/moveGroupAttrsToElems.js) | move some group attributes to the contained elements | `enabled` |
|
||||
| [collapseGroups](https://github.com/svg/svgo/blob/master/plugins/collapseGroups.js) | collapse useless groups | `enabled` |
|
||||
| [removeRasterImages](https://github.com/svg/svgo/blob/master/plugins/removeRasterImages.js) | remove raster images | `disabled` |
|
||||
| [mergePaths](https://github.com/svg/svgo/blob/master/plugins/mergePaths.js) | merge multiple Paths into one | `enabled` |
|
||||
| [convertShapeToPath](https://github.com/svg/svgo/blob/master/plugins/convertShapeToPath.js) | convert some basic shapes to `<path>` | `enabled` |
|
||||
| [convertEllipseToCircle](https://github.com/svg/svgo/blob/master/plugins/convertEllipseToCircle.js) | convert non-eccentric `<ellipse>` to `<circle>` | `enabled` |
|
||||
| [sortAttrs](https://github.com/svg/svgo/blob/master/plugins/sortAttrs.js) | sort element attributes for epic readability | `disabled` |
|
||||
| [sortDefsChildren](https://github.com/svg/svgo/blob/master/plugins/sortDefsChildren.js) | sort children of `<defs>` in order to improve compression | `enabled` |
|
||||
| [removeDimensions](https://github.com/svg/svgo/blob/master/plugins/removeDimensions.js) | remove `width`/`height` and add `viewBox` if it's missing (opposite to removeViewBox, disable it first) | `disabled` |
|
||||
| [removeAttrs](https://github.com/svg/svgo/blob/master/plugins/removeAttrs.js) | remove attributes by pattern | `disabled` |
|
||||
| [removeAttributesBySelector](https://github.com/svg/svgo/blob/master/plugins/removeAttributesBySelector.js) | removes attributes of elements that match a css selector | `disabled` |
|
||||
| [removeElementsByAttr](https://github.com/svg/svgo/blob/master/plugins/removeElementsByAttr.js) | remove arbitrary elements by ID or className | `disabled` |
|
||||
| [addClassesToSVGElement](https://github.com/svg/svgo/blob/master/plugins/addClassesToSVGElement.js) | add classnames to an outer `<svg>` element | `disabled` |
|
||||
| [addAttributesToSVGElement](https://github.com/svg/svgo/blob/master/plugins/addAttributesToSVGElement.js) | adds attributes to an outer `<svg>` element | `disabled` |
|
||||
| [removeOffCanvasPaths](https://github.com/svg/svgo/blob/master/plugins/removeOffCanvasPaths.js) | removes elements that are drawn outside of the viewbox | `disabled` |
|
||||
| [removeStyleElement](https://github.com/svg/svgo/blob/master/plugins/removeStyleElement.js) | remove `<style>` elements | `disabled` |
|
||||
| [removeScriptElement](https://github.com/svg/svgo/blob/master/plugins/removeScriptElement.js) | remove `<script>` elements | `disabled` |
|
||||
| [reusePaths](https://github.com/svg/svgo/blob/master/plugins/reusePaths.js) | Find duplicated <path> elements and replace them with <use> links | `disabled` |
|
||||
|
||||
Want to know how it works and how to write your own plugin? [Of course you want to](https://github.com/svg/svgo/blob/master/docs/how-it-works/en.md). ([동작방법](https://github.com/svg/svgo/blob/master/docs/how-it-works/ko.md))
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
$ [sudo] npm install -g svgo
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### <abbr title="Command Line Interface">CLI</abbr>
|
||||
|
||||
```
|
||||
Usage:
|
||||
svgo [OPTIONS] [ARGS]
|
||||
|
||||
Options:
|
||||
-h, --help : Help
|
||||
-v, --version : Version
|
||||
-i INPUT, --input=INPUT : Input file, "-" for STDIN
|
||||
-s STRING, --string=STRING : Input SVG data string
|
||||
-f FOLDER, --folder=FOLDER : Input folder, optimize and rewrite all *.svg files
|
||||
-o OUTPUT, --output=OUTPUT : Output file or folder (by default the same as the input), "-" for STDOUT
|
||||
-p PRECISION, --precision=PRECISION : Set number of digits in the fractional part, overrides plugins params
|
||||
--config=CONFIG : Config file or JSON string to extend or replace default
|
||||
--disable=PLUGIN : Disable plugin by name, "--disable=PLUGIN1,PLUGIN2" for multiple plugins
|
||||
--enable=PLUGIN : Enable plugin by name, "--enable=PLUGIN3,PLUGIN4" for multiple plugins
|
||||
--datauri=DATAURI : Output as Data URI string (base64, URI encoded or unencoded)
|
||||
--multipass : Pass over SVGs multiple times to ensure all optimizations are applied
|
||||
--pretty : Make SVG pretty printed
|
||||
--indent=INDENT : Indent number when pretty printing SVGs
|
||||
-r, --recursive : Use with '-f'. Optimizes *.svg files in folders recursively.
|
||||
-q, --quiet : Only output error messages, not regular status messages
|
||||
--show-plugins : Show available plugins and exit
|
||||
|
||||
Arguments:
|
||||
INPUT : Alias to --input
|
||||
```
|
||||
|
||||
* with files:
|
||||
|
||||
```sh
|
||||
$ svgo test.svg
|
||||
```
|
||||
|
||||
or:
|
||||
|
||||
```sh
|
||||
$ svgo *.svg
|
||||
```
|
||||
|
||||
```sh
|
||||
$ svgo test.svg -o test.min.svg
|
||||
```
|
||||
|
||||
```sh
|
||||
$ svgo test.svg other.svg third.svg
|
||||
```
|
||||
|
||||
```sh
|
||||
$ svgo test.svg other.svg third.svg -o test.min.svg -o other.min.svg -o third.min.svg
|
||||
```
|
||||
|
||||
* with STDIN / STDOUT:
|
||||
|
||||
```sh
|
||||
$ cat test.svg | svgo -i - -o - > test.min.svg
|
||||
```
|
||||
|
||||
* with folder
|
||||
|
||||
```sh
|
||||
$ svgo -f ../path/to/folder/with/svg/files
|
||||
```
|
||||
|
||||
or:
|
||||
|
||||
```sh
|
||||
$ svgo -f ../path/to/folder/with/svg/files -o ../path/to/folder/with/svg/output
|
||||
```
|
||||
|
||||
```sh
|
||||
$ svgo *.svg -o ../path/to/folder/with/svg/output
|
||||
```
|
||||
|
||||
* with strings:
|
||||
|
||||
```sh
|
||||
$ svgo -s '<svg version="1.1">test</svg>' -o test.min.svg
|
||||
```
|
||||
|
||||
or even with Data URI base64:
|
||||
|
||||
```sh
|
||||
$ svgo -s 'data:image/svg+xml;base64,...' -o test.min.svg
|
||||
```
|
||||
|
||||
* with SVGZ:
|
||||
|
||||
from `.svgz` to `.svg`:
|
||||
|
||||
```sh
|
||||
$ gunzip -c test.svgz | svgo -i - -o test.min.svg
|
||||
```
|
||||
|
||||
from `.svg` to `.svgz`:
|
||||
|
||||
```sh
|
||||
$ svgo test.svg -o - | gzip -cfq9 > test.svgz
|
||||
```
|
||||
|
||||
### Other Ways to Use SVGO
|
||||
|
||||
* as a web app – [SVGOMG](https://jakearchibald.github.io/svgomg/)
|
||||
* as a Nodejs module – [examples](https://github.com/svg/svgo/tree/master/examples)
|
||||
* as a Grunt task – [grunt-svgmin](https://github.com/sindresorhus/grunt-svgmin)
|
||||
* as a Gulp task – [gulp-svgmin](https://github.com/ben-eb/gulp-svgmin)
|
||||
* as a Mimosa module – [mimosa-minify-svg](https://github.com/dbashford/mimosa-minify-svg)
|
||||
* as an OSX Folder Action – [svgo-osx-folder-action](https://github.com/svg/svgo-osx-folder-action)
|
||||
* as a webpack loader – [image-webpack-loader](https://github.com/tcoopman/image-webpack-loader)
|
||||
* as a Telegram Bot – [svgo_bot](https://github.com/maksugr/svgo_bot)
|
||||
* as a PostCSS plugin – [postcss-svgo](https://github.com/ben-eb/postcss-svgo)
|
||||
* as an Inkscape plugin – [inkscape-svgo](https://github.com/konsumer/inkscape-svgo)
|
||||
* as a Sketch plugin - [svgo-compressor](https://github.com/BohemianCoding/svgo-compressor)
|
||||
* as macOS app - [Image Shrinker](https://image-shrinker.com)
|
||||
* as a Rollup plugin - [rollup-plugin-svgo](https://github.com/porsager/rollup-plugin-svgo)
|
||||
|
||||
## Backers
|
||||
|
||||
| [<img src="https://sheetjs.com/sketch128.png" width="80">](https://sheetjs.com/) | [<img src="https://rawgithub.com/fontello/fontello/master/fontello-image.svg" width="80">](http://fontello.com/) |
|
||||
|:-:|:-:|
|
||||
| [SheetJS LLC](https://sheetjs.com/) | [Fontello](http://fontello.com/) |
|
||||
|
||||
## Donations
|
||||
|
||||
- PayPal: https://www.paypal.me/deepsweet
|
||||
|
||||
## License and Copyright
|
||||
|
||||
This software is released under the terms of the [MIT license](https://github.com/svg/svgo/blob/master/LICENSE).
|
||||
|
||||
Logo by [Yegor Bolshakov](http://xizzzy.ru/).
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
[english](https://github.com/svg/svgo/blob/master/README.md) | **русский**
|
||||
- - -
|
||||
|
||||
<img src="https://svg.github.io/svgo-logo.svg" width="200" height="200" alt="logo"/>
|
||||
|
||||
## SVGO [](https://npmjs.org/package/svgo) [](https://travis-ci.org/svg/svgo) [](https://coveralls.io/r/svg/svgo?branch=master)
|
||||
|
||||
**SVG** **O**ptimizer – это инструмент для оптимизации векторной графики в формате SVG, написанный на Node.js.
|
||||

|
||||
|
||||
## Зачем?
|
||||
|
||||
SVG-файлы, особенно экспортированные из редакторов, содержат много избыточной и бесполезной информации, комментариев, скрытых элементов, неоптимальные или стандартные значения и другой мусор, удаление которого безопасно и не влияет на конечный вид изображения.
|
||||
|
||||
## Возможности
|
||||
|
||||
SVGO имеет расширяемую архитектуру, в которой почти каждая оптимизация является отдельным расширением.
|
||||
|
||||
Что у нас есть:
|
||||
|
||||
| Plugin | Description | Default |
|
||||
| ------ | ----------- | ------- |
|
||||
| [cleanupAttrs](https://github.com/svg/svgo/blob/master/plugins/cleanupAttrs.js) | удаление переносов строк и лишних пробелов | `включен` |
|
||||
| [inlineStyles](https://github.com/svg/svgo/blob/master/plugins/inlineStyles.js) | перенос стилей из элементов `<style>` в атрибуты `style` | `включен` |
|
||||
| [removeDoctype](https://github.com/svg/svgo/blob/master/plugins/removeDoctype.js) | удаление doctype | `включен` |
|
||||
| [removeXMLProcInst](https://github.com/svg/svgo/blob/master/plugins/removeXMLProcInst.js) | удаление XML-инструкций | `включен` |
|
||||
| [removeComments](https://github.com/svg/svgo/blob/master/plugins/removeComments.js) | удаление комментариев | `включен` |
|
||||
| [removeMetadata](https://github.com/svg/svgo/blob/master/plugins/removeMetadata.js) | удаление `<metadata>` | `включен` |
|
||||
| [removeTitle](https://github.com/svg/svgo/blob/master/plugins/removeTitle.js) | удаление `<title>` | `включен` |
|
||||
| [removeDesc](https://github.com/svg/svgo/blob/master/plugins/removeDesc.js) | удаление `<desc>` | `включен` |
|
||||
| [removeUselessDefs](https://github.com/svg/svgo/blob/master/plugins/removeUselessDefs.js) | удаление элементов в `<defs>` без `id` | `включен` |
|
||||
| [removeXMLNS](https://github.com/svg/svgo/blob/master/plugins/removeXMLNS.js) | удаление атрибута xmlns (для заинлайненных svg) | `выключено` |
|
||||
| [removeEditorsNSData](https://github.com/svg/svgo/blob/master/plugins/removeEditorsNSData.js) | удаление пространств имён различных редакторов, их элементов и атрибутов | `включен` |
|
||||
| [removeEmptyAttrs](https://github.com/svg/svgo/blob/master/plugins/removeEmptyAttrs.js) | удаление пустых атрибутов | `включен` |
|
||||
| [removeHiddenElems](https://github.com/svg/svgo/blob/master/plugins/removeHiddenElems.js) | удаление скрытых элементов | `включен` |
|
||||
| [removeEmptyText](https://github.com/svg/svgo/blob/master/plugins/removeEmptyText.js) | удаление пустых текстовых элементов | `включен` |
|
||||
| [removeEmptyContainers](https://github.com/svg/svgo/blob/master/plugins/removeEmptyContainers.js) | удаление пустых элементов-контейнеров | `включен` |
|
||||
| [removeViewBox](https://github.com/svg/svgo/blob/master/plugins/removeViewBox.js) | удаление атрибута `viewBox`, когда это возможно | `включен` |
|
||||
| [cleanupEnableBackground](https://github.com/svg/svgo/blob/master/plugins/cleanupEnableBackground.js) | удаление или оптимизация атрибута `enable-background`, когда это возможно | `включен` |
|
||||
| [minifyStyles](https://github.com/svg/svgo/blob/master/plugins/minifyStyles.js) | уменьшает содержимое элементов `<style>` с помощью [CSSO](https://github.com/css/csso). | `включен` |
|
||||
| [convertStyleToAttrs](https://github.com/svg/svgo/blob/master/plugins/convertStyleToAttrs.js) | конвертирование стилей в атрибуте `style` в отдельные svg-атрибуты | `включен` |
|
||||
| [convertColors](https://github.com/svg/svgo/blob/master/plugins/convertColors.js) | конвертирование цветовых значений: из `rgb()` в `#rrggbb`, из `#rrggbb` в `#rgb` | `включен` |
|
||||
| [convertPathData](https://github.com/svg/svgo/blob/master/plugins/convertPathData.js) | конвертирование данных Path в относительные или абсолютные координаты, смотря что \|короче; конвертирование одних типов сегментов в другие; удаление ненужных разделителей; умное округление и тому подобное | `включен` |
|
||||
| [convertTransform](https://github.com/svg/svgo/blob/master/plugins/convertTransform.js) | схлопывание нескольких трансформаций в одну, конвертирование матриц в короткие алиасы \|и многое другое | `включен` |
|
||||
| [removeUnknownsAndDefaults](https://github.com/svg/svgo/blob/master/plugins/removeUnknownsAndDefaults.js) | удаление неизвестных элементов, контента и атрибутов | `включен` |
|
||||
| [removeNonInheritableGroupAttrs](https://github.com/svg/svgo/blob/master/plugins/removeNonInheritableGroupAttrs.js) | удаление ненаследуемых "презентационных" атрибутов групп | `включен` |
|
||||
| [removeUselessStrokeAndFill](https://github.com/svg/svgo/blob/master/plugins/removeUselessStrokeAndFill.js) | удаление неиспользуемых атрибутов stroke-* и fill-* | `включен` |
|
||||
| [removeUnusedNS](https://github.com/svg/svgo/blob/master/plugins/removeUnusedNS.js) | удаление деклараций неиспользуемых пространств имён | `включен` |
|
||||
| [prefixIds](https://github.com/svg/svgo/blob/master/plugins/prefixIds.js) | добавляет префикс в ID или классы в виде имени файла или произвольной строки | `выключено` |
|
||||
| [cleanupIDs](https://github.com/svg/svgo/blob/master/plugins/cleanupIDs.js) | удаление неиспользуемых и сокращение используемых ID | `включен` |
|
||||
| [cleanupNumericValues](https://github.com/svg/svgo/blob/master/plugins/cleanupNumericValues.js) | округление дробных чисел до заданной точности, удаление `px` как единицы \|измерения по-умолчанию | `включен` |
|
||||
| [cleanupListOfValues](https://github.com/svg/svgo/blob/master/plugins/cleanupListOfValues.js) | округление числовых значений в атрибутах со списком чисел, таких как `viewBox` \|или `enableBackground` | `выключено` |
|
||||
| [moveElemsAttrsToGroup](https://github.com/svg/svgo/blob/master/plugins/moveElemsAttrsToGroup.js) | перемещение совпадающих атрибутов у всех элементов внутри группы `<g>` | `включен` |
|
||||
| [moveGroupAttrsToElems](https://github.com/svg/svgo/blob/master/plugins/moveGroupAttrsToElems.js) | перемещение некоторых атрибутов группы на элементы внутри | `включен` |
|
||||
| [collapseGroups](https://github.com/svg/svgo/blob/master/plugins/collapseGroups.js) | схлопывание бесполезных групп `<g>` | `включен` |
|
||||
| [removeRasterImages](https://github.com/svg/svgo/blob/master/plugins/removeRasterImages.js) | удаление растровых изображений | `выключено` |
|
||||
| [mergePaths](https://github.com/svg/svgo/blob/master/plugins/mergePaths.js) | склеивание нескольких Path в одну кривую | `включен` |
|
||||
| [convertShapeToPath](https://github.com/svg/svgo/blob/master/plugins/convertShapeToPath.js) | конвертирование простых форм в Path | `включен` |
|
||||
| [convertEllipseToCircle](https://github.com/svg/svgo/blob/master/plugins/convertEllipseToCircle.js) | конвертирование вырожденного эллипса `<ellipse>` в круг `<circle>` | `включен` |
|
||||
| [sortAttrs](https://github.com/svg/svgo/blob/master/plugins/sortAttrs.js) | сортировка атрибутов элементов для удобочитаемости | `выключено` |
|
||||
| [sortDefsChildren](https://github.com/svg/svgo/blob/master/plugins/sortDefsChildren.js) | сортировка детей `<defs>` для лучшей компрессии | `включен` |
|
||||
| [removeDimensions](https://github.com/svg/svgo/blob/master/plugins/removeDimensions.js) | удаляет атрибуты width/height при наличии viewBox (противоречит removeViewBox — плагин должен быть выключен) | `выключено` |
|
||||
| [removeAttrs](https://github.com/svg/svgo/blob/master/plugins/removeAttrs.js) | удаляет атрибуты по указанному паттерну | `выключено` |
|
||||
| [removeAttributesBySelector](https://github.com/svg/svgo/blob/master/plugins/removeAttributesBySelector.js) | удаляет атрибуты по CSS-селектору | `выключено` |
|
||||
| [removeElementsByAttr](https://github.com/svg/svgo/blob/master/plugins/removeElementsByAttr.js) | удаляет элементы по указанным ID или классам | `выключено` |
|
||||
| [addClassesToSVGElement](https://github.com/svg/svgo/blob/master/plugins/addClassesToSVGElement.js) | добавляет имена классов корневому элементу `<svg>` | `выключено` |
|
||||
| [addAttributesToSVGElement](https://github.com/svg/svgo/blob/master/plugins/addAttributesToSVGElement.js) | добавляет атрибуты корневому элементу `<svg>` | `выключено` |
|
||||
| [removeOffCanvasPaths](https://github.com/svg/svgo/blob/master/plugins/removeOffCanvasPaths.js) | удаляет элементы вне отрисовываемой области | `выключено` |
|
||||
| [removeStyleElement](https://github.com/svg/svgo/blob/master/plugins/removeStyleElement.js) | удаляет элементы `<style>` | `выключено` |
|
||||
| [removeScriptElement](https://github.com/svg/svgo/blob/master/plugins/removeScriptElement.js) | удаляет элементы `<script>` | `выключено` |
|
||||
| [reusePaths](https://github.com/svg/svgo/blob/master/plugins/reusePaths.js) | Заменяет дублирующиеся элементы <path> ссылками <use> | `выключено` |
|
||||
|
||||
Хотите узнать принципы работы и как написать свой плагин? [Конечно же, да!](https://github.com/svg/svgo/blob/master/docs/how-it-works/ru.md)
|
||||
|
||||
|
||||
## Как использовать
|
||||
|
||||
```sh
|
||||
$ [sudo] npm install -g svgo
|
||||
```
|
||||
|
||||
## Выполнение:
|
||||
|
||||
### Командная строка
|
||||
|
||||
```
|
||||
Запуск:
|
||||
svgo [OPTIONS] [ARGS]
|
||||
|
||||
Параметры:
|
||||
-h, --help : Помощь
|
||||
-v, --version : Версия программы
|
||||
-i INPUT, --input=INPUT : Входной файл, "-" для STDIN
|
||||
-s STRING, --string=STRING : Входная строка SVG
|
||||
-f FOLDER, --folder=FOLDER : Входная папка, оптимизирует и перезаписывает все файлы *.svg
|
||||
-o OUTPUT, --output=OUTPUT : Выходной файл или папка (совпадает с входным по умолчанию), "-" для STDOUT
|
||||
-p PRECISION, --precision=PRECISION : Число цифр после запятой, переопределяет параметры плагинов
|
||||
--config=CONFIG : Файл конфигурации (или строка JSON) для расширения и замены настроек
|
||||
--disable=PLUGIN : Выключение плагина по имени, "--disable=PLUGIN1,PLUGIN2" для отключения нескольких плагинов
|
||||
--enable=PLUGIN : Включение плагина по имени, "--enable=PLUGIN3,PLUGIN4" для отключения нескольких плагинов
|
||||
--datauri=DATAURI : Результат в виде строки Data URI (base64, URI encoded или unencoded)
|
||||
--multipass : Оптимизация в несколько проходов для применения всех возможных оптимизаций
|
||||
--pretty : Удобочитаемое форматирование SVG
|
||||
--indent=INDENT : Размер отступа для удобочитаемого форматирования
|
||||
-r, --recursive : Совместно с '-f'. Рекурсивно обрабатывать *.svg файлы в папках.
|
||||
-q, --quiet : Подавляет вывод информации, выводятся только сообщения об ошибках
|
||||
--show-plugins : Доступные плагины
|
||||
|
||||
Аргументы:
|
||||
INPUT : Аналогично --input
|
||||
```
|
||||
|
||||
* с файлами:
|
||||
|
||||
```sh
|
||||
$ svgo test.svg
|
||||
```
|
||||
|
||||
или:
|
||||
|
||||
```sh
|
||||
$ svgo *.svg
|
||||
```
|
||||
|
||||
```sh
|
||||
$ svgo test.svg -o test.min.svg
|
||||
```
|
||||
|
||||
```sh
|
||||
$ svgo test.svg other.svg third.svg
|
||||
```
|
||||
|
||||
```sh
|
||||
$ svgo test.svg other.svg third.svg -o test.min.svg -o other.min.svg -o third.min.svg
|
||||
```
|
||||
|
||||
* со STDIN / STDOUT:
|
||||
|
||||
```sh
|
||||
$ cat test.svg | svgo -i - -o - > test.min.svg
|
||||
```
|
||||
|
||||
* с папками
|
||||
|
||||
```sh
|
||||
$ svgo -f ../path/to/folder/with/svg/files
|
||||
```
|
||||
|
||||
или:
|
||||
|
||||
```sh
|
||||
$ svgo -f ../path/to/folder/with/svg/files -o ../path/to/folder/with/svg/output
|
||||
```
|
||||
|
||||
```sh
|
||||
$ svgo *.svg -o ../path/to/folder/with/svg/output
|
||||
```
|
||||
|
||||
* со строками:
|
||||
|
||||
```sh
|
||||
$ svgo -s '<svg version="1.1">test</svg>' -o test.min.svg
|
||||
```
|
||||
|
||||
или даже с Data URI base64:
|
||||
|
||||
```sh
|
||||
$ svgo -s 'data:image/svg+xml;base64,…' -o test.min.svg
|
||||
```
|
||||
|
||||
* с SVGZ:
|
||||
|
||||
из `.svgz` в `.svg`:
|
||||
|
||||
```sh
|
||||
$ gunzip -c test.svgz | svgo -i - -o test.min.svg
|
||||
```
|
||||
|
||||
из `.svg` в `.svgz`:
|
||||
|
||||
```sh
|
||||
$ svgo test.svg -o - | gzip -cfq9 > test.svgz
|
||||
```
|
||||
|
||||
### Другие способы использования SVGO
|
||||
|
||||
* в виде веб-приложения - [SVGOMG](https://jakearchibald.github.io/svgomg/)
|
||||
* как модуль Node.js – [examples](https://github.com/svg/svgo/tree/master/examples)
|
||||
* как таск для Grunt – [grunt-svgmin](https://github.com/sindresorhus/grunt-svgmin)
|
||||
* как таск для Gulp – [gulp-svgmin](https://github.com/ben-eb/gulp-svgmin)
|
||||
* как таск для Mimosa – [mimosa-minify-svg](https://github.com/dbashford/mimosa-minify-svg)
|
||||
* как действие папки в OSX – [svgo-osx-folder-action](https://github.com/svg/svgo-osx-folder-action)
|
||||
* через загрузчик webpack – [image-webpack-loader](https://github.com/tcoopman/image-webpack-loader)
|
||||
* с помощью бота в Telegram – [svgo_bot](https://github.com/maksugr/svgo_bot)
|
||||
* как плагин PostCSS - [postcss-svgo](https://github.com/ben-eb/postcss-svgo)
|
||||
* как плагин для Inkscape – [inkscape-svgo](https://github.com/konsumer/inkscape-svgo)
|
||||
* как плагин для Sketch - [svgo-compressor](https://github.com/BohemianCoding/svgo-compressor)
|
||||
* в виде приложения macOS - [Image Shrinker](https://image-shrinker.com)
|
||||
* как плагин для Rollup - [rollup-plugin-svgo](https://github.com/porsager/rollup-plugin-svgo)
|
||||
|
||||
## Лицензия и копирайты
|
||||
|
||||
Данное программное обеспечение выпускается под [лицензией MIT](https://github.com/svg/svgo/blob/master/LICENSE).
|
||||
|
||||
Логотип – [Егор Большаков](http://xizzzy.ru/).
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
require('../lib/svgo/coa').run();
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
'use strict';
|
||||
|
||||
var csstree = require('css-tree'),
|
||||
List = csstree.List,
|
||||
stable = require('stable'),
|
||||
specificity = require('csso/lib/restructure/prepare/specificity');
|
||||
|
||||
|
||||
/**
|
||||
* Flatten a CSS AST to a selectors list.
|
||||
*
|
||||
* @param {Object} cssAst css-tree AST to flatten
|
||||
* @return {Array} selectors
|
||||
*/
|
||||
function flattenToSelectors(cssAst) {
|
||||
var selectors = [];
|
||||
|
||||
csstree.walk(cssAst, {visit: 'Rule', enter: function(node) {
|
||||
if (node.type !== 'Rule') {
|
||||
return;
|
||||
}
|
||||
|
||||
var atrule = this.atrule;
|
||||
var rule = node;
|
||||
|
||||
node.prelude.children.each(function(selectorNode, selectorItem) {
|
||||
var selector = {
|
||||
item: selectorItem,
|
||||
atrule: atrule,
|
||||
rule: rule,
|
||||
pseudos: []
|
||||
};
|
||||
|
||||
selectorNode.children.each(function(selectorChildNode, selectorChildItem, selectorChildList) {
|
||||
if (selectorChildNode.type === 'PseudoClassSelector' ||
|
||||
selectorChildNode.type === 'PseudoElementSelector') {
|
||||
selector.pseudos.push({
|
||||
item: selectorChildItem,
|
||||
list: selectorChildList
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
selectors.push(selector);
|
||||
});
|
||||
}});
|
||||
|
||||
return selectors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter selectors by Media Query.
|
||||
*
|
||||
* @param {Array} selectors to filter
|
||||
* @param {Array} useMqs Array with strings of media queries that should pass (<name> <expression>)
|
||||
* @return {Array} Filtered selectors that match the passed media queries
|
||||
*/
|
||||
function filterByMqs(selectors, useMqs) {
|
||||
return selectors.filter(function(selector) {
|
||||
if (selector.atrule === null) {
|
||||
return ~useMqs.indexOf('');
|
||||
}
|
||||
|
||||
var mqName = selector.atrule.name;
|
||||
var mqStr = mqName;
|
||||
if (selector.atrule.expression &&
|
||||
selector.atrule.expression.children.first().type === 'MediaQueryList') {
|
||||
var mqExpr = csstree.generate(selector.atrule.expression);
|
||||
mqStr = [mqName, mqExpr].join(' ');
|
||||
}
|
||||
|
||||
return ~useMqs.indexOf(mqStr);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter selectors by the pseudo-elements and/or -classes they contain.
|
||||
*
|
||||
* @param {Array} selectors to filter
|
||||
* @param {Array} usePseudos Array with strings of single or sequence of pseudo-elements and/or -classes that should pass
|
||||
* @return {Array} Filtered selectors that match the passed pseudo-elements and/or -classes
|
||||
*/
|
||||
function filterByPseudos(selectors, usePseudos) {
|
||||
return selectors.filter(function(selector) {
|
||||
var pseudoSelectorsStr = csstree.generate({
|
||||
type: 'Selector',
|
||||
children: new List().fromArray(selector.pseudos.map(function(pseudo) {
|
||||
return pseudo.item.data;
|
||||
}))
|
||||
});
|
||||
return ~usePseudos.indexOf(pseudoSelectorsStr);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove pseudo-elements and/or -classes from the selectors for proper matching.
|
||||
*
|
||||
* @param {Array} selectors to clean
|
||||
* @return {Array} Selectors without pseudo-elements and/or -classes
|
||||
*/
|
||||
function cleanPseudos(selectors) {
|
||||
selectors.forEach(function(selector) {
|
||||
selector.pseudos.forEach(function(pseudo) {
|
||||
pseudo.list.remove(pseudo.item);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Compares two selector specificities.
|
||||
* extracted from https://github.com/keeganstreet/specificity/blob/master/specificity.js#L211
|
||||
*
|
||||
* @param {Array} aSpecificity Specificity of selector A
|
||||
* @param {Array} bSpecificity Specificity of selector B
|
||||
* @return {Number} Score of selector specificity A compared to selector specificity B
|
||||
*/
|
||||
function compareSpecificity(aSpecificity, bSpecificity) {
|
||||
for (var i = 0; i < 4; i += 1) {
|
||||
if (aSpecificity[i] < bSpecificity[i]) {
|
||||
return -1;
|
||||
} else if (aSpecificity[i] > bSpecificity[i]) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Compare two simple selectors.
|
||||
*
|
||||
* @param {Object} aSimpleSelectorNode Simple selector A
|
||||
* @param {Object} bSimpleSelectorNode Simple selector B
|
||||
* @return {Number} Score of selector A compared to selector B
|
||||
*/
|
||||
function compareSimpleSelectorNode(aSimpleSelectorNode, bSimpleSelectorNode) {
|
||||
var aSpecificity = specificity(aSimpleSelectorNode),
|
||||
bSpecificity = specificity(bSimpleSelectorNode);
|
||||
return compareSpecificity(aSpecificity, bSpecificity);
|
||||
}
|
||||
|
||||
function _bySelectorSpecificity(selectorA, selectorB) {
|
||||
return compareSimpleSelectorNode(selectorA.item.data, selectorB.item.data);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sort selectors stably by their specificity.
|
||||
*
|
||||
* @param {Array} selectors to be sorted
|
||||
* @return {Array} Stable sorted selectors
|
||||
*/
|
||||
function sortSelectors(selectors) {
|
||||
return stable(selectors, _bySelectorSpecificity);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert a css-tree AST style declaration to CSSStyleDeclaration property.
|
||||
*
|
||||
* @param {Object} declaration css-tree style declaration
|
||||
* @return {Object} CSSStyleDeclaration property
|
||||
*/
|
||||
function csstreeToStyleDeclaration(declaration) {
|
||||
var propertyName = declaration.property,
|
||||
propertyValue = csstree.generate(declaration.value),
|
||||
propertyPriority = (declaration.important ? 'important' : '');
|
||||
return {
|
||||
name: propertyName,
|
||||
value: propertyValue,
|
||||
priority: propertyPriority
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the CSS string of a style element
|
||||
*
|
||||
* @param {Object} element style element
|
||||
* @return {String|Array} CSS string or empty array if no styles are set
|
||||
*/
|
||||
function getCssStr(elem) {
|
||||
return elem.content[0].text || elem.content[0].cdata || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the CSS string of a style element
|
||||
*
|
||||
* @param {Object} element style element
|
||||
* @param {String} CSS string to be set
|
||||
* @return {Object} reference to field with CSS
|
||||
*/
|
||||
function setCssStr(elem, css) {
|
||||
// in case of cdata field
|
||||
if(elem.content[0].cdata) {
|
||||
elem.content[0].cdata = css;
|
||||
return elem.content[0].cdata;
|
||||
}
|
||||
|
||||
// in case of text field + if nothing was set yet
|
||||
elem.content[0].text = css;
|
||||
return elem.content[0].text;
|
||||
}
|
||||
|
||||
|
||||
module.exports.flattenToSelectors = flattenToSelectors;
|
||||
|
||||
module.exports.filterByMqs = filterByMqs;
|
||||
module.exports.filterByPseudos = filterByPseudos;
|
||||
module.exports.cleanPseudos = cleanPseudos;
|
||||
|
||||
module.exports.compareSpecificity = compareSpecificity;
|
||||
module.exports.compareSimpleSelectorNode = compareSimpleSelectorNode;
|
||||
|
||||
module.exports.sortSelectors = sortSelectors;
|
||||
|
||||
module.exports.csstreeToStyleDeclaration = csstreeToStyleDeclaration;
|
||||
|
||||
module.exports.getCssStr = getCssStr;
|
||||
module.exports.setCssStr = setCssStr;
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* SVGO is a Nodejs-based tool for optimizing SVG vector graphics files.
|
||||
*
|
||||
* @see https://github.com/svg/svgo
|
||||
*
|
||||
* @author Kir Belevich <kir@soulshine.in> (https://github.com/deepsweet)
|
||||
* @copyright © 2012 Kir Belevich
|
||||
* @license MIT https://raw.githubusercontent.com/svg/svgo/master/LICENSE
|
||||
*/
|
||||
|
||||
var CONFIG = require('./svgo/config.js'),
|
||||
SVG2JS = require('./svgo/svg2js.js'),
|
||||
PLUGINS = require('./svgo/plugins.js'),
|
||||
JSAPI = require('./svgo/jsAPI.js'),
|
||||
encodeSVGDatauri = require('./svgo/tools.js').encodeSVGDatauri,
|
||||
JS2SVG = require('./svgo/js2svg.js');
|
||||
|
||||
var SVGO = function(config) {
|
||||
this.config = CONFIG(config);
|
||||
};
|
||||
|
||||
SVGO.prototype.optimize = function(svgstr, info) {
|
||||
info = info || {};
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.config.error) {
|
||||
reject(this.config.error);
|
||||
return;
|
||||
}
|
||||
|
||||
var config = this.config,
|
||||
maxPassCount = config.multipass ? 10 : 1,
|
||||
counter = 0,
|
||||
prevResultSize = Number.POSITIVE_INFINITY,
|
||||
optimizeOnceCallback = (svgjs) => {
|
||||
if (svgjs.error) {
|
||||
reject(svgjs.error);
|
||||
return;
|
||||
}
|
||||
|
||||
info.multipassCount = counter;
|
||||
if (++counter < maxPassCount && svgjs.data.length < prevResultSize) {
|
||||
prevResultSize = svgjs.data.length;
|
||||
this._optimizeOnce(svgjs.data, info, optimizeOnceCallback);
|
||||
} else {
|
||||
if (config.datauri) {
|
||||
svgjs.data = encodeSVGDatauri(svgjs.data, config.datauri);
|
||||
}
|
||||
if (info && info.path) {
|
||||
svgjs.path = info.path;
|
||||
}
|
||||
resolve(svgjs);
|
||||
}
|
||||
};
|
||||
|
||||
this._optimizeOnce(svgstr, info, optimizeOnceCallback);
|
||||
});
|
||||
};
|
||||
|
||||
SVGO.prototype._optimizeOnce = function(svgstr, info, callback) {
|
||||
var config = this.config;
|
||||
|
||||
SVG2JS(svgstr, function(svgjs) {
|
||||
if (svgjs.error) {
|
||||
callback(svgjs);
|
||||
return;
|
||||
}
|
||||
|
||||
svgjs = PLUGINS(svgjs, info, config.plugins);
|
||||
|
||||
callback(JS2SVG(svgjs, config.js2svg));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* The factory that creates a content item with the helper methods.
|
||||
*
|
||||
* @param {Object} data which passed to jsAPI constructor
|
||||
* @returns {JSAPI} content item
|
||||
*/
|
||||
SVGO.prototype.createContentItem = function(data) {
|
||||
return new JSAPI(data);
|
||||
};
|
||||
|
||||
SVGO.Config = CONFIG;
|
||||
|
||||
module.exports = SVGO;
|
||||
// Offer ES module interop compatibility.
|
||||
module.exports.default = SVGO;
|
||||
+579
@@ -0,0 +1,579 @@
|
||||
/* jshint quotmark: false */
|
||||
'use strict';
|
||||
|
||||
var FS = require('fs'),
|
||||
PATH = require('path'),
|
||||
chalk = require('chalk'),
|
||||
mkdirp = require('mkdirp'),
|
||||
promisify = require('util.promisify'),
|
||||
readdir = promisify(FS.readdir),
|
||||
readFile = promisify(FS.readFile),
|
||||
writeFile = promisify(FS.writeFile),
|
||||
SVGO = require('../svgo.js'),
|
||||
YAML = require('js-yaml'),
|
||||
PKG = require('../../package.json'),
|
||||
encodeSVGDatauri = require('./tools.js').encodeSVGDatauri,
|
||||
decodeSVGDatauri = require('./tools.js').decodeSVGDatauri,
|
||||
checkIsDir = require('./tools.js').checkIsDir,
|
||||
regSVGFile = /\.svg$/,
|
||||
noop = () => {},
|
||||
svgo;
|
||||
|
||||
/**
|
||||
* Command-Option-Argument.
|
||||
*
|
||||
* @see https://github.com/veged/coa
|
||||
*/
|
||||
module.exports = require('coa').Cmd()
|
||||
.helpful()
|
||||
.name(PKG.name)
|
||||
.title(PKG.description)
|
||||
.opt()
|
||||
.name('version').title('Version')
|
||||
.short('v').long('version')
|
||||
.only()
|
||||
.flag()
|
||||
.act(function() {
|
||||
// output the version to stdout instead of stderr if returned
|
||||
process.stdout.write(PKG.version + '\n');
|
||||
// coa will run `.toString` on the returned value and send it to stderr
|
||||
return '';
|
||||
})
|
||||
.end()
|
||||
.opt()
|
||||
.name('input').title('Input file, "-" for STDIN')
|
||||
.short('i').long('input')
|
||||
.arr()
|
||||
.val(function(val) {
|
||||
return val || this.reject("Option '--input' must have a value.");
|
||||
})
|
||||
.end()
|
||||
.opt()
|
||||
.name('string').title('Input SVG data string')
|
||||
.short('s').long('string')
|
||||
.end()
|
||||
.opt()
|
||||
.name('folder').title('Input folder, optimize and rewrite all *.svg files')
|
||||
.short('f').long('folder')
|
||||
.val(function(val) {
|
||||
return val || this.reject("Option '--folder' must have a value.");
|
||||
})
|
||||
.end()
|
||||
.opt()
|
||||
.name('output').title('Output file or folder (by default the same as the input), "-" for STDOUT')
|
||||
.short('o').long('output')
|
||||
.arr()
|
||||
.val(function(val) {
|
||||
return val || this.reject("Option '--output' must have a value.");
|
||||
})
|
||||
.end()
|
||||
.opt()
|
||||
.name('precision').title('Set number of digits in the fractional part, overrides plugins params')
|
||||
.short('p').long('precision')
|
||||
.val(function(val) {
|
||||
return !isNaN(val) ? val : this.reject("Option '--precision' must be an integer number");
|
||||
})
|
||||
.end()
|
||||
.opt()
|
||||
.name('config').title('Config file or JSON string to extend or replace default')
|
||||
.long('config')
|
||||
.val(function(val) {
|
||||
return val || this.reject("Option '--config' must have a value.");
|
||||
})
|
||||
.end()
|
||||
.opt()
|
||||
.name('disable').title('Disable plugin by name, "--disable={PLUGIN1,PLUGIN2}" for multiple plugins (*nix)')
|
||||
.long('disable')
|
||||
.arr()
|
||||
.val(function(val) {
|
||||
return val || this.reject("Option '--disable' must have a value.");
|
||||
})
|
||||
.end()
|
||||
.opt()
|
||||
.name('enable').title('Enable plugin by name, "--enable={PLUGIN3,PLUGIN4}" for multiple plugins (*nix)')
|
||||
.long('enable')
|
||||
.arr()
|
||||
.val(function(val) {
|
||||
return val || this.reject("Option '--enable' must have a value.");
|
||||
})
|
||||
.end()
|
||||
.opt()
|
||||
.name('datauri').title('Output as Data URI string (base64, URI encoded or unencoded)')
|
||||
.long('datauri')
|
||||
.val(function(val) {
|
||||
return val || this.reject("Option '--datauri' must have one of the following values: 'base64', 'enc' or 'unenc'");
|
||||
})
|
||||
.end()
|
||||
.opt()
|
||||
.name('multipass').title('Pass over SVGs multiple times to ensure all optimizations are applied')
|
||||
.long('multipass')
|
||||
.flag()
|
||||
.end()
|
||||
.opt()
|
||||
.name('pretty').title('Make SVG pretty printed')
|
||||
.long('pretty')
|
||||
.flag()
|
||||
.end()
|
||||
.opt()
|
||||
.name('indent').title('Indent number when pretty printing SVGs')
|
||||
.long('indent')
|
||||
.val(function(val) {
|
||||
return !isNaN(val) ? val : this.reject("Option '--indent' must be an integer number");
|
||||
})
|
||||
.end()
|
||||
.opt()
|
||||
.name('recursive').title('Use with \'-f\'. Optimizes *.svg files in folders recursively.')
|
||||
.short('r').long('recursive')
|
||||
.flag()
|
||||
.end()
|
||||
.opt()
|
||||
.name('quiet').title('Only output error messages, not regular status messages')
|
||||
.short('q').long('quiet')
|
||||
.flag()
|
||||
.end()
|
||||
.opt()
|
||||
.name('show-plugins').title('Show available plugins and exit')
|
||||
.long('show-plugins')
|
||||
.flag()
|
||||
.end()
|
||||
.arg()
|
||||
.name('input').title('Alias to --input')
|
||||
.arr()
|
||||
.end()
|
||||
.act(function(opts, args) {
|
||||
var input = opts.input || args.input,
|
||||
output = opts.output,
|
||||
config = {};
|
||||
|
||||
// --show-plugins
|
||||
if (opts['show-plugins']) {
|
||||
showAvailablePlugins();
|
||||
return;
|
||||
}
|
||||
|
||||
// w/o anything
|
||||
if (
|
||||
(!input || input[0] === '-') &&
|
||||
!opts.string &&
|
||||
!opts.stdin &&
|
||||
!opts.folder &&
|
||||
process.stdin.isTTY === true
|
||||
) return this.usage();
|
||||
|
||||
if (typeof process == 'object' && process.versions && process.versions.node && PKG && PKG.engines.node) {
|
||||
var nodeVersion = String(PKG.engines.node).match(/\d*(\.\d+)*/)[0];
|
||||
if (parseFloat(process.versions.node) < parseFloat(nodeVersion)) {
|
||||
return printErrorAndExit(`Error: ${PKG.name} requires Node.js version ${nodeVersion} or higher.`);
|
||||
}
|
||||
}
|
||||
|
||||
// --config
|
||||
if (opts.config) {
|
||||
// string
|
||||
if (opts.config.charAt(0) === '{') {
|
||||
try {
|
||||
config = JSON.parse(opts.config);
|
||||
} catch (e) {
|
||||
return printErrorAndExit(`Error: Couldn't parse config JSON.\n${String(e)}`);
|
||||
}
|
||||
// external file
|
||||
} else {
|
||||
var configPath = PATH.resolve(opts.config),
|
||||
configData;
|
||||
try {
|
||||
// require() adds some weird output on YML files
|
||||
configData = FS.readFileSync(configPath, 'utf8');
|
||||
config = JSON.parse(configData);
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
return printErrorAndExit(`Error: couldn't find config file '${opts.config}'.`);
|
||||
} else if (err.code === 'EISDIR') {
|
||||
return printErrorAndExit(`Error: directory '${opts.config}' is not a config file.`);
|
||||
}
|
||||
config = YAML.safeLoad(configData);
|
||||
config.__DIR = PATH.dirname(configPath); // will use it to resolve custom plugins defined via path
|
||||
|
||||
if (!config || Array.isArray(config)) {
|
||||
return printErrorAndExit(`Error: invalid config file '${opts.config}'.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --quiet
|
||||
if (opts.quiet) {
|
||||
config.quiet = opts.quiet;
|
||||
}
|
||||
|
||||
// --recursive
|
||||
if (opts.recursive) {
|
||||
config.recursive = opts.recursive;
|
||||
}
|
||||
|
||||
// --precision
|
||||
if (opts.precision) {
|
||||
var precision = Math.min(Math.max(0, parseInt(opts.precision)), 20);
|
||||
if (!isNaN(precision)) {
|
||||
config.floatPrecision = precision;
|
||||
}
|
||||
}
|
||||
|
||||
// --disable
|
||||
if (opts.disable) {
|
||||
changePluginsState(opts.disable, false, config);
|
||||
}
|
||||
|
||||
// --enable
|
||||
if (opts.enable) {
|
||||
changePluginsState(opts.enable, true, config);
|
||||
}
|
||||
|
||||
// --multipass
|
||||
if (opts.multipass) {
|
||||
config.multipass = true;
|
||||
}
|
||||
|
||||
// --pretty
|
||||
if (opts.pretty) {
|
||||
config.js2svg = config.js2svg || {};
|
||||
config.js2svg.pretty = true;
|
||||
var indent;
|
||||
if (opts.indent && !isNaN(indent = parseInt(opts.indent))) {
|
||||
config.js2svg.indent = indent;
|
||||
}
|
||||
}
|
||||
|
||||
svgo = new SVGO(config);
|
||||
|
||||
// --output
|
||||
if (output) {
|
||||
if (input && input[0] != '-') {
|
||||
if (output.length == 1 && checkIsDir(output[0])) {
|
||||
var dir = output[0];
|
||||
for (var i = 0; i < input.length; i++) {
|
||||
output[i] = checkIsDir(input[i]) ? input[i] : PATH.resolve(dir, PATH.basename(input[i]));
|
||||
}
|
||||
} else if (output.length < input.length) {
|
||||
output = output.concat(input.slice(output.length));
|
||||
}
|
||||
}
|
||||
} else if (input) {
|
||||
output = input;
|
||||
} else if (opts.string) {
|
||||
output = '-';
|
||||
}
|
||||
|
||||
if (opts.datauri) {
|
||||
config.datauri = opts.datauri;
|
||||
}
|
||||
|
||||
// --folder
|
||||
if (opts.folder) {
|
||||
var ouputFolder = output && output[0] || opts.folder;
|
||||
return optimizeFolder(config, opts.folder, ouputFolder).then(noop, printErrorAndExit);
|
||||
}
|
||||
|
||||
// --input
|
||||
if (input) {
|
||||
// STDIN
|
||||
if (input[0] === '-') {
|
||||
return new Promise((resolve, reject) => {
|
||||
var data = '',
|
||||
file = output[0];
|
||||
|
||||
process.stdin
|
||||
.on('data', chunk => data += chunk)
|
||||
.once('end', () => processSVGData(config, {input: 'string'}, data, file).then(resolve, reject));
|
||||
});
|
||||
// file
|
||||
} else {
|
||||
return Promise.all(input.map((file, n) => optimizeFile(config, file, output[n])))
|
||||
.then(noop, printErrorAndExit);
|
||||
}
|
||||
|
||||
// --string
|
||||
} else if (opts.string) {
|
||||
var data = decodeSVGDatauri(opts.string);
|
||||
|
||||
return processSVGData(config, {input: 'string'}, data, output[0]);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Change plugins state by names array.
|
||||
*
|
||||
* @param {Array} names plugins names
|
||||
* @param {Boolean} state active state
|
||||
* @param {Object} config original config
|
||||
* @return {Object} changed config
|
||||
*/
|
||||
function changePluginsState(names, state, config) {
|
||||
names.forEach(flattenPluginsCbk);
|
||||
|
||||
// extend config
|
||||
if (config.plugins) {
|
||||
for (var name of names) {
|
||||
var matched = false,
|
||||
key;
|
||||
|
||||
for (var plugin of config.plugins) {
|
||||
// get plugin name
|
||||
if (typeof plugin === 'object') {
|
||||
key = Object.keys(plugin)[0];
|
||||
} else {
|
||||
key = plugin;
|
||||
}
|
||||
|
||||
// if there is such a plugin name
|
||||
if (key === name) {
|
||||
// don't replace plugin's params with true
|
||||
if (typeof plugin[key] !== 'object' || !state) {
|
||||
plugin[key] = state;
|
||||
}
|
||||
// mark it as matched
|
||||
matched = true;
|
||||
}
|
||||
}
|
||||
|
||||
// if not matched and current config is not full
|
||||
if (!matched && !config.full) {
|
||||
// push new plugin Object
|
||||
config.plugins.push({ [name]: state });
|
||||
matched = true;
|
||||
}
|
||||
}
|
||||
// just push
|
||||
} else {
|
||||
config.plugins = names.map(name => ({ [name]: state }));
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten an array of plugins by invoking this callback on each element
|
||||
* whose value may be a comma separated list of plugins.
|
||||
*
|
||||
* @param {String} name Plugin name
|
||||
* @param {Number} index Plugin index
|
||||
* @param {Array} names Plugins being traversed
|
||||
*/
|
||||
function flattenPluginsCbk(name, index, names)
|
||||
{
|
||||
var split = name.split(',');
|
||||
|
||||
if(split.length > 1) {
|
||||
names[index] = split.shift();
|
||||
names.push.apply(names, split);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimize SVG files in a directory.
|
||||
* @param {Object} config options
|
||||
* @param {string} dir input directory
|
||||
* @param {string} output output directory
|
||||
* @return {Promise}
|
||||
*/
|
||||
function optimizeFolder(config, dir, output) {
|
||||
if (!config.quiet) {
|
||||
console.log(`Processing directory '${dir}':\n`);
|
||||
}
|
||||
return readdir(dir).then(files => processDirectory(config, dir, files, output));
|
||||
}
|
||||
|
||||
/**
|
||||
* Process given files, take only SVG.
|
||||
* @param {Object} config options
|
||||
* @param {string} dir input directory
|
||||
* @param {Array} files list of file names in the directory
|
||||
* @param {string} output output directory
|
||||
* @return {Promise}
|
||||
*/
|
||||
function processDirectory(config, dir, files, output) {
|
||||
// take only *.svg files, recursively if necessary
|
||||
var svgFilesDescriptions = getFilesDescriptions(config, dir, files, output);
|
||||
|
||||
return svgFilesDescriptions.length ?
|
||||
Promise.all(svgFilesDescriptions.map(fileDescription => optimizeFile(config, fileDescription.inputPath, fileDescription.outputPath))) :
|
||||
Promise.reject(new Error(`No SVG files have been found in '${dir}' directory.`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get svg files descriptions
|
||||
* @param {Object} config options
|
||||
* @param {string} dir input directory
|
||||
* @param {Array} files list of file names in the directory
|
||||
* @param {string} output output directory
|
||||
* @return {Array}
|
||||
*/
|
||||
function getFilesDescriptions(config, dir, files, output) {
|
||||
const filesInThisFolder = files
|
||||
.filter(name => regSVGFile.test(name))
|
||||
.map(name => ({
|
||||
inputPath: PATH.resolve(dir, name),
|
||||
outputPath: PATH.resolve(output, name),
|
||||
}));
|
||||
|
||||
return config.recursive ?
|
||||
[].concat(
|
||||
filesInThisFolder,
|
||||
files
|
||||
.filter(name => checkIsDir(PATH.resolve(dir, name)))
|
||||
.map(subFolderName => {
|
||||
const subFolderPath = PATH.resolve(dir, subFolderName);
|
||||
const subFolderFiles = FS.readdirSync(subFolderPath);
|
||||
const subFolderOutput = PATH.resolve(output, subFolderName);
|
||||
return getFilesDescriptions(config, subFolderPath, subFolderFiles, subFolderOutput);
|
||||
})
|
||||
.reduce((a, b) => [].concat(a, b), [])
|
||||
) :
|
||||
filesInThisFolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read SVG file and pass to processing.
|
||||
* @param {Object} config options
|
||||
* @param {string} file
|
||||
* @param {string} output
|
||||
* @return {Promise}
|
||||
*/
|
||||
function optimizeFile(config, file, output) {
|
||||
return readFile(file, 'utf8').then(
|
||||
data => processSVGData(config, {input: 'file', path: file}, data, output, file),
|
||||
error => checkOptimizeFileError(config, file, output, error)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimize SVG data.
|
||||
* @param {Object} config options
|
||||
* @param {string} data SVG content to optimize
|
||||
* @param {string} output where to write optimized file
|
||||
* @param {string} [input] input file name (being used if output is a directory)
|
||||
* @return {Promise}
|
||||
*/
|
||||
function processSVGData(config, info, data, output, input) {
|
||||
var startTime = Date.now(),
|
||||
prevFileSize = Buffer.byteLength(data, 'utf8');
|
||||
|
||||
return svgo.optimize(data, info).then(function(result) {
|
||||
if (config.datauri) {
|
||||
result.data = encodeSVGDatauri(result.data, config.datauri);
|
||||
}
|
||||
var resultFileSize = Buffer.byteLength(result.data, 'utf8'),
|
||||
processingTime = Date.now() - startTime;
|
||||
|
||||
return writeOutput(input, output, result.data).then(function() {
|
||||
if (!config.quiet && output != '-') {
|
||||
if (input) {
|
||||
console.log(`\n${PATH.basename(input)}:`);
|
||||
}
|
||||
printTimeInfo(processingTime);
|
||||
printProfitInfo(prevFileSize, resultFileSize);
|
||||
}
|
||||
},
|
||||
error => Promise.reject(new Error(error.code === 'ENOTDIR' ? `Error: output '${output}' is not a directory.` : error)));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Write result of an optimization.
|
||||
* @param {string} input
|
||||
* @param {string} output output file name. '-' for stdout
|
||||
* @param {string} data data to write
|
||||
* @return {Promise}
|
||||
*/
|
||||
function writeOutput(input, output, data) {
|
||||
if (output == '-') {
|
||||
console.log(data);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
mkdirp.sync(PATH.dirname(output));
|
||||
|
||||
return writeFile(output, data, 'utf8').catch(error => checkWriteFileError(input, output, data, error));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Write a time taken by optimization.
|
||||
* @param {number} time time in milliseconds.
|
||||
*/
|
||||
function printTimeInfo(time) {
|
||||
console.log(`Done in ${time} ms!`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write optimizing information in human readable format.
|
||||
* @param {number} inBytes size before optimization.
|
||||
* @param {number} outBytes size after optimization.
|
||||
*/
|
||||
function printProfitInfo(inBytes, outBytes) {
|
||||
var profitPercents = 100 - outBytes * 100 / inBytes;
|
||||
|
||||
console.log(
|
||||
(Math.round((inBytes / 1024) * 1000) / 1000) + ' KiB' +
|
||||
(profitPercents < 0 ? ' + ' : ' - ') +
|
||||
chalk.green(Math.abs((Math.round(profitPercents * 10) / 10)) + '%') + ' = ' +
|
||||
(Math.round((outBytes / 1024) * 1000) / 1000) + ' KiB'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for errors, if it's a dir optimize the dir.
|
||||
* @param {Object} config
|
||||
* @param {string} input
|
||||
* @param {string} output
|
||||
* @param {Error} error
|
||||
* @return {Promise}
|
||||
*/
|
||||
function checkOptimizeFileError(config, input, output, error) {
|
||||
if (error.code == 'EISDIR') {
|
||||
return optimizeFolder(config, input, output);
|
||||
} else if (error.code == 'ENOENT') {
|
||||
return Promise.reject(new Error(`Error: no such file or directory '${error.path}'.`));
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for saving file error. If the output is a dir, then write file there.
|
||||
* @param {string} input
|
||||
* @param {string} output
|
||||
* @param {string} data
|
||||
* @param {Error} error
|
||||
* @return {Promise}
|
||||
*/
|
||||
function checkWriteFileError(input, output, data, error) {
|
||||
if (error.code == 'EISDIR' && input) {
|
||||
return writeFile(PATH.resolve(output, PATH.basename(input)), data, 'utf8');
|
||||
} else {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show list of available plugins with short description.
|
||||
*/
|
||||
function showAvailablePlugins() {
|
||||
console.log('Currently available plugins:');
|
||||
|
||||
// Flatten an array of plugins grouped per type, sort and write output
|
||||
var list = [].concat.apply([], new SVGO().config.plugins)
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map(plugin => ` [ ${chalk.green(plugin.name)} ] ${plugin.description}`)
|
||||
.join('\n');
|
||||
console.log(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write an error and exit.
|
||||
* @param {Error} error
|
||||
* @return {Promise} a promise for running tests
|
||||
*/
|
||||
function printErrorAndExit(error) {
|
||||
console.error(chalk.red(error));
|
||||
process.exit(1);
|
||||
return Promise.reject(error); // for tests
|
||||
}
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
'use strict';
|
||||
|
||||
var FS = require('fs');
|
||||
var PATH = require('path');
|
||||
var yaml = require('js-yaml');
|
||||
|
||||
/**
|
||||
* Read and/or extend/replace default config file,
|
||||
* prepare and optimize plugins array.
|
||||
*
|
||||
* @param {Object} [config] input config
|
||||
* @return {Object} output config
|
||||
*/
|
||||
module.exports = function(config) {
|
||||
|
||||
var defaults;
|
||||
config = typeof config == 'object' && config || {};
|
||||
|
||||
if (config.plugins && !Array.isArray(config.plugins)) {
|
||||
return { error: 'Error: Invalid plugins list. Provided \'plugins\' in config should be an array.' };
|
||||
}
|
||||
|
||||
if (config.full) {
|
||||
defaults = config;
|
||||
|
||||
if (Array.isArray(defaults.plugins)) {
|
||||
defaults.plugins = preparePluginsArray(config, defaults.plugins);
|
||||
}
|
||||
} else {
|
||||
defaults = Object.assign({}, yaml.safeLoad(FS.readFileSync(__dirname + '/../../.svgo.yml', 'utf8')));
|
||||
defaults.plugins = preparePluginsArray(config, defaults.plugins || []);
|
||||
defaults = extendConfig(defaults, config);
|
||||
}
|
||||
|
||||
if ('floatPrecision' in config && Array.isArray(defaults.plugins)) {
|
||||
defaults.plugins.forEach(function(plugin) {
|
||||
if (plugin.params && ('floatPrecision' in plugin.params)) {
|
||||
// Don't touch default plugin params
|
||||
plugin.params = Object.assign({}, plugin.params, { floatPrecision: config.floatPrecision });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if ('datauri' in config) {
|
||||
defaults.datauri = config.datauri;
|
||||
}
|
||||
|
||||
if (Array.isArray(defaults.plugins)) {
|
||||
defaults.plugins = optimizePluginsArray(defaults.plugins);
|
||||
}
|
||||
|
||||
return defaults;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Require() all plugins in array.
|
||||
*
|
||||
* @param {Object} config
|
||||
* @param {Array} plugins input plugins array
|
||||
* @return {Array} input plugins array of arrays
|
||||
*/
|
||||
function preparePluginsArray(config, plugins) {
|
||||
|
||||
var plugin,
|
||||
key;
|
||||
|
||||
return plugins.map(function(item) {
|
||||
|
||||
// {}
|
||||
if (typeof item === 'object') {
|
||||
|
||||
key = Object.keys(item)[0];
|
||||
|
||||
// custom
|
||||
if (typeof item[key] === 'object' && item[key].fn && typeof item[key].fn === 'function') {
|
||||
plugin = setupCustomPlugin(key, item[key]);
|
||||
|
||||
} else {
|
||||
|
||||
plugin = setPluginActiveState(
|
||||
loadPlugin(config, key, item[key].path),
|
||||
item,
|
||||
key
|
||||
);
|
||||
plugin.name = key;
|
||||
}
|
||||
|
||||
// name
|
||||
} else {
|
||||
|
||||
plugin = loadPlugin(config, item);
|
||||
plugin.name = item;
|
||||
if (typeof plugin.params === 'object') {
|
||||
plugin.params = Object.assign({}, plugin.params);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return plugin;
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend plugins with the custom config object.
|
||||
*
|
||||
* @param {Array} plugins input plugins
|
||||
* @param {Object} config config
|
||||
* @return {Array} output plugins
|
||||
*/
|
||||
function extendConfig(defaults, config) {
|
||||
|
||||
var key;
|
||||
|
||||
// plugins
|
||||
if (config.plugins) {
|
||||
|
||||
config.plugins.forEach(function(item) {
|
||||
|
||||
// {}
|
||||
if (typeof item === 'object') {
|
||||
|
||||
key = Object.keys(item)[0];
|
||||
|
||||
if (item[key] == null) {
|
||||
console.error(`Error: '${key}' plugin is misconfigured! Have you padded its content in YML properly?\n`);
|
||||
}
|
||||
|
||||
// custom
|
||||
if (typeof item[key] === 'object' && item[key].fn && typeof item[key].fn === 'function') {
|
||||
defaults.plugins.push(setupCustomPlugin(key, item[key]));
|
||||
|
||||
// plugin defined via path
|
||||
} else if (typeof item[key] === 'object' && item[key].path) {
|
||||
defaults.plugins.push(setPluginActiveState(loadPlugin(config, undefined, item[key].path), item, key));
|
||||
|
||||
} else {
|
||||
defaults.plugins.forEach(function(plugin) {
|
||||
|
||||
if (plugin.name === key) {
|
||||
plugin = setPluginActiveState(plugin, item, key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
defaults.multipass = config.multipass;
|
||||
|
||||
// svg2js
|
||||
if (config.svg2js) {
|
||||
defaults.svg2js = config.svg2js;
|
||||
}
|
||||
|
||||
// js2svg
|
||||
if (config.js2svg) {
|
||||
defaults.js2svg = config.js2svg;
|
||||
}
|
||||
|
||||
return defaults;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup and enable a custom plugin
|
||||
*
|
||||
* @param {String} plugin name
|
||||
* @param {Object} custom plugin
|
||||
* @return {Array} enabled plugin
|
||||
*/
|
||||
function setupCustomPlugin(name, plugin) {
|
||||
plugin.active = true;
|
||||
plugin.params = Object.assign({}, plugin.params || {});
|
||||
plugin.name = name;
|
||||
|
||||
return plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to group sequential elements of plugins array.
|
||||
*
|
||||
* @param {Object} plugins input plugins
|
||||
* @return {Array} output plugins
|
||||
*/
|
||||
function optimizePluginsArray(plugins) {
|
||||
|
||||
var prev;
|
||||
|
||||
return plugins.reduce(function(plugins, item) {
|
||||
if (prev && item.type == prev[0].type) {
|
||||
prev.push(item);
|
||||
} else {
|
||||
plugins.push(prev = [item]);
|
||||
}
|
||||
return plugins;
|
||||
}, []);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets plugin to active or inactive state.
|
||||
*
|
||||
* @param {Object} plugin
|
||||
* @param {Object} item
|
||||
* @param {Object} key
|
||||
* @return {Object} plugin
|
||||
*/
|
||||
function setPluginActiveState(plugin, item, key) {
|
||||
// name: {}
|
||||
if (typeof item[key] === 'object') {
|
||||
plugin.params = Object.assign({}, plugin.params || {}, item[key]);
|
||||
plugin.active = true;
|
||||
|
||||
// name: false
|
||||
} else if (item[key] === false) {
|
||||
plugin.active = false;
|
||||
|
||||
// name: true
|
||||
} else if (item[key] === true) {
|
||||
plugin.active = true;
|
||||
}
|
||||
|
||||
return plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads default plugin using name or custom plugin defined via path in config.
|
||||
*
|
||||
* @param {Object} config
|
||||
* @param {Object} name
|
||||
* @param {Object} path
|
||||
* @return {Object} plugin
|
||||
*/
|
||||
function loadPlugin(config, name, path) {
|
||||
var plugin;
|
||||
|
||||
if (!path) {
|
||||
plugin = require('../../plugins/' + name);
|
||||
} else {
|
||||
plugin = require(PATH.resolve(config.__DIR, path));
|
||||
}
|
||||
|
||||
return Object.assign({}, plugin);
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
'use strict';
|
||||
|
||||
var values = require('object.values');
|
||||
if (!Object.values) {
|
||||
values.shim();
|
||||
}
|
||||
|
||||
|
||||
var CSSClassList = function(node) {
|
||||
this.parentNode = node;
|
||||
this.classNames = new Set();
|
||||
this.classAttr = null;
|
||||
//this.classValue = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Performs a deep clone of this object.
|
||||
*
|
||||
* @param parentNode the parentNode to assign to the cloned result
|
||||
*/
|
||||
CSSClassList.prototype.clone = function(parentNode) {
|
||||
var node = this;
|
||||
var nodeData = {};
|
||||
|
||||
Object.keys(node).forEach(function(key) {
|
||||
if (key !== 'parentNode') {
|
||||
nodeData[key] = node[key];
|
||||
}
|
||||
});
|
||||
|
||||
// Deep-clone node data.
|
||||
nodeData = JSON.parse(JSON.stringify(nodeData));
|
||||
|
||||
var clone = new CSSClassList(parentNode);
|
||||
Object.assign(clone, nodeData);
|
||||
return clone;
|
||||
};
|
||||
|
||||
CSSClassList.prototype.hasClass = function() {
|
||||
this.classAttr = { // empty class attr
|
||||
'name': 'class',
|
||||
'value': null
|
||||
};
|
||||
|
||||
this.addClassHandler();
|
||||
};
|
||||
|
||||
|
||||
// attr.class
|
||||
|
||||
CSSClassList.prototype.addClassHandler = function() {
|
||||
|
||||
Object.defineProperty(this.parentNode.attrs, 'class', {
|
||||
get: this.getClassAttr.bind(this),
|
||||
set: this.setClassAttr.bind(this),
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
});
|
||||
|
||||
this.addClassValueHandler();
|
||||
};
|
||||
|
||||
// attr.class.value
|
||||
|
||||
CSSClassList.prototype.addClassValueHandler = function() {
|
||||
|
||||
Object.defineProperty(this.classAttr, 'value', {
|
||||
get: this.getClassValue.bind(this),
|
||||
set: this.setClassValue.bind(this),
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
});
|
||||
};
|
||||
|
||||
CSSClassList.prototype.getClassAttr = function() {
|
||||
return this.classAttr;
|
||||
};
|
||||
|
||||
CSSClassList.prototype.setClassAttr = function(newClassAttr) {
|
||||
this.setClassValue(newClassAttr.value); // must before applying value handler!
|
||||
|
||||
this.classAttr = newClassAttr;
|
||||
this.addClassValueHandler();
|
||||
};
|
||||
|
||||
CSSClassList.prototype.getClassValue = function() {
|
||||
var arrClassNames = Array.from(this.classNames);
|
||||
return arrClassNames.join(' ');
|
||||
};
|
||||
|
||||
CSSClassList.prototype.setClassValue = function(newValue) {
|
||||
if(typeof newValue === 'undefined') {
|
||||
this.classNames.clear();
|
||||
return;
|
||||
}
|
||||
var arrClassNames = newValue.split(' ');
|
||||
this.classNames = new Set(arrClassNames);
|
||||
};
|
||||
|
||||
|
||||
CSSClassList.prototype.add = function(/* variadic */) {
|
||||
this.hasClass();
|
||||
Object.values(arguments).forEach(this._addSingle.bind(this));
|
||||
};
|
||||
|
||||
CSSClassList.prototype._addSingle = function(className) {
|
||||
this.classNames.add(className);
|
||||
};
|
||||
|
||||
|
||||
CSSClassList.prototype.remove = function(/* variadic */) {
|
||||
this.hasClass();
|
||||
Object.values(arguments).forEach(this._removeSingle.bind(this));
|
||||
};
|
||||
|
||||
CSSClassList.prototype._removeSingle = function(className) {
|
||||
this.classNames.delete(className);
|
||||
};
|
||||
|
||||
|
||||
CSSClassList.prototype.item = function(index) {
|
||||
var arrClassNames = Array.from(this.classNames);
|
||||
return arrClassNames[index];
|
||||
};
|
||||
|
||||
CSSClassList.prototype.toggle = function(className, force) {
|
||||
if(this.contains(className) || force === false) {
|
||||
this.classNames.delete(className);
|
||||
}
|
||||
this.classNames.add(className);
|
||||
};
|
||||
|
||||
CSSClassList.prototype.contains = function(className) {
|
||||
return this.classNames.has(className);
|
||||
};
|
||||
|
||||
|
||||
module.exports = CSSClassList;
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
'use strict';
|
||||
|
||||
var baseCssAdapter = require('css-select-base-adapter');
|
||||
|
||||
/**
|
||||
* DOMUtils API for SVGO AST (used by css-select)
|
||||
*/
|
||||
var svgoCssSelectAdapterMin = {
|
||||
|
||||
// is the node a tag?
|
||||
// isTag: ( node:Node ) => isTag:Boolean
|
||||
isTag: function(node) {
|
||||
return node.isElem();
|
||||
},
|
||||
|
||||
// get the parent of the node
|
||||
// getParent: ( node:Node ) => parentNode:Node
|
||||
// returns null when no parent exists
|
||||
getParent: function(node) {
|
||||
return node.parentNode || null;
|
||||
},
|
||||
|
||||
// get the node's children
|
||||
// getChildren: ( node:Node ) => children:[Node]
|
||||
getChildren: function(node) {
|
||||
return node.content || [];
|
||||
},
|
||||
|
||||
// get the name of the tag
|
||||
// getName: ( elem:ElementNode ) => tagName:String
|
||||
getName: function(elemAst) {
|
||||
return elemAst.elem;
|
||||
},
|
||||
|
||||
// get the text content of the node, and its children if it has any
|
||||
// getText: ( node:Node ) => text:String
|
||||
// returns empty string when there is no text
|
||||
getText: function(node) {
|
||||
return node.content[0].text || node.content[0].cdata || '';
|
||||
},
|
||||
|
||||
// get the attribute value
|
||||
// getAttributeValue: ( elem:ElementNode, name:String ) => value:String
|
||||
// returns null when attribute doesn't exist
|
||||
getAttributeValue: function(elem, name) {
|
||||
return elem.hasAttr(name) ? elem.attr(name).value : null;
|
||||
}
|
||||
};
|
||||
|
||||
// use base adapter for default implementation
|
||||
var svgoCssSelectAdapter = baseCssAdapter(svgoCssSelectAdapterMin);
|
||||
|
||||
module.exports = svgoCssSelectAdapter;
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
'use strict';
|
||||
|
||||
var csstree = require('css-tree'),
|
||||
csstools = require('../css-tools');
|
||||
|
||||
|
||||
var CSSStyleDeclaration = function(node) {
|
||||
this.parentNode = node;
|
||||
|
||||
this.properties = new Map();
|
||||
this.hasSynced = false;
|
||||
|
||||
this.styleAttr = null;
|
||||
this.styleValue = null;
|
||||
|
||||
this.parseError = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Performs a deep clone of this object.
|
||||
*
|
||||
* @param parentNode the parentNode to assign to the cloned result
|
||||
*/
|
||||
CSSStyleDeclaration.prototype.clone = function(parentNode) {
|
||||
var node = this;
|
||||
var nodeData = {};
|
||||
|
||||
Object.keys(node).forEach(function(key) {
|
||||
if (key !== 'parentNode') {
|
||||
nodeData[key] = node[key];
|
||||
}
|
||||
});
|
||||
|
||||
// Deep-clone node data.
|
||||
nodeData = JSON.parse(JSON.stringify(nodeData));
|
||||
|
||||
var clone = new CSSStyleDeclaration(parentNode);
|
||||
Object.assign(clone, nodeData);
|
||||
return clone;
|
||||
};
|
||||
|
||||
CSSStyleDeclaration.prototype.hasStyle = function() {
|
||||
this.addStyleHandler();
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
// attr.style
|
||||
|
||||
CSSStyleDeclaration.prototype.addStyleHandler = function() {
|
||||
|
||||
this.styleAttr = { // empty style attr
|
||||
'name': 'style',
|
||||
'value': null
|
||||
};
|
||||
|
||||
Object.defineProperty(this.parentNode.attrs, 'style', {
|
||||
get: this.getStyleAttr.bind(this),
|
||||
set: this.setStyleAttr.bind(this),
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
});
|
||||
|
||||
this.addStyleValueHandler();
|
||||
};
|
||||
|
||||
// attr.style.value
|
||||
|
||||
CSSStyleDeclaration.prototype.addStyleValueHandler = function() {
|
||||
|
||||
Object.defineProperty(this.styleAttr, 'value', {
|
||||
get: this.getStyleValue.bind(this),
|
||||
set: this.setStyleValue.bind(this),
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
});
|
||||
};
|
||||
|
||||
CSSStyleDeclaration.prototype.getStyleAttr = function() {
|
||||
return this.styleAttr;
|
||||
};
|
||||
|
||||
CSSStyleDeclaration.prototype.setStyleAttr = function(newStyleAttr) {
|
||||
this.setStyleValue(newStyleAttr.value); // must before applying value handler!
|
||||
|
||||
this.styleAttr = newStyleAttr;
|
||||
this.addStyleValueHandler();
|
||||
this.hasSynced = false; // raw css changed
|
||||
};
|
||||
|
||||
CSSStyleDeclaration.prototype.getStyleValue = function() {
|
||||
return this.getCssText();
|
||||
};
|
||||
|
||||
CSSStyleDeclaration.prototype.setStyleValue = function(newValue) {
|
||||
this.properties.clear(); // reset all existing properties
|
||||
this.styleValue = newValue;
|
||||
this.hasSynced = false; // raw css changed
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
CSSStyleDeclaration.prototype._loadCssText = function() {
|
||||
if (this.hasSynced) {
|
||||
return;
|
||||
}
|
||||
this.hasSynced = true; // must be set here to prevent loop in setProperty(...)
|
||||
|
||||
if (!this.styleValue || this.styleValue.length === 0) {
|
||||
return;
|
||||
}
|
||||
var inlineCssStr = this.styleValue;
|
||||
|
||||
var declarations = {};
|
||||
try {
|
||||
declarations = csstree.parse(inlineCssStr, {
|
||||
context: 'declarationList',
|
||||
parseValue: false
|
||||
});
|
||||
} catch (parseError) {
|
||||
this.parseError = parseError;
|
||||
return;
|
||||
}
|
||||
this.parseError = false;
|
||||
|
||||
var self = this;
|
||||
declarations.children.each(function(declaration) {
|
||||
try {
|
||||
var styleDeclaration = csstools.csstreeToStyleDeclaration(declaration);
|
||||
self.setProperty(styleDeclaration.name, styleDeclaration.value, styleDeclaration.priority);
|
||||
} catch(styleError) {
|
||||
if(styleError.message !== 'Unknown node type: undefined') {
|
||||
self.parseError = styleError;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// only reads from properties
|
||||
|
||||
/**
|
||||
* Get the textual representation of the declaration block (equivalent to .cssText attribute).
|
||||
*
|
||||
* @return {String} Textual representation of the declaration block (empty string for no properties)
|
||||
*/
|
||||
CSSStyleDeclaration.prototype.getCssText = function() {
|
||||
var properties = this.getProperties();
|
||||
|
||||
if (this.parseError) {
|
||||
// in case of a parse error, pass through original styles
|
||||
return this.styleValue;
|
||||
}
|
||||
|
||||
var cssText = [];
|
||||
properties.forEach(function(property, propertyName) {
|
||||
var strImportant = property.priority === 'important' ? '!important' : '';
|
||||
cssText.push(propertyName.trim() + ':' + property.value.trim() + strImportant);
|
||||
});
|
||||
return cssText.join(';');
|
||||
};
|
||||
|
||||
CSSStyleDeclaration.prototype._handleParseError = function() {
|
||||
if (this.parseError) {
|
||||
console.warn('Warning: Parse error when parsing inline styles, style properties of this element cannot be used. The raw styles can still be get/set using .attr(\'style\').value. Error details: ' + this.parseError);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
CSSStyleDeclaration.prototype._getProperty = function(propertyName) {
|
||||
if(typeof propertyName === 'undefined') {
|
||||
throw Error('1 argument required, but only 0 present.');
|
||||
}
|
||||
|
||||
var properties = this.getProperties();
|
||||
this._handleParseError();
|
||||
|
||||
var property = properties.get(propertyName.trim());
|
||||
return property;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the optional priority, "important".
|
||||
*
|
||||
* @param {String} propertyName representing the property name to be checked.
|
||||
* @return {String} priority that represents the priority (e.g. "important") if one exists. If none exists, returns the empty string.
|
||||
*/
|
||||
CSSStyleDeclaration.prototype.getPropertyPriority = function(propertyName) {
|
||||
var property = this._getProperty(propertyName);
|
||||
return property ? property.priority : '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the property value given a property name.
|
||||
*
|
||||
* @param {String} propertyName representing the property name to be checked.
|
||||
* @return {String} value containing the value of the property. If not set, returns the empty string.
|
||||
*/
|
||||
CSSStyleDeclaration.prototype.getPropertyValue = function(propertyName) {
|
||||
var property = this._getProperty(propertyName);
|
||||
return property ? property.value : null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a property name.
|
||||
*
|
||||
* @param {Number} index of the node to be fetched. The index is zero-based.
|
||||
* @return {String} propertyName that is the name of the CSS property at the specified index.
|
||||
*/
|
||||
CSSStyleDeclaration.prototype.item = function(index) {
|
||||
if(typeof index === 'undefined') {
|
||||
throw Error('1 argument required, but only 0 present.');
|
||||
}
|
||||
|
||||
var properties = this.getProperties();
|
||||
this._handleParseError();
|
||||
|
||||
return Array.from(properties.keys())[index];
|
||||
};
|
||||
|
||||
/**
|
||||
* Return all properties of the node.
|
||||
*
|
||||
* @return {Map} properties that is a Map with propertyName as key and property (propertyValue + propertyPriority) as value.
|
||||
*/
|
||||
CSSStyleDeclaration.prototype.getProperties = function() {
|
||||
this._loadCssText();
|
||||
return this.properties;
|
||||
};
|
||||
|
||||
|
||||
// writes to properties
|
||||
|
||||
/**
|
||||
* Remove a property from the CSS declaration block.
|
||||
*
|
||||
* @param {String} propertyName representing the property name to be removed.
|
||||
* @return {String} oldValue equal to the value of the CSS property before it was removed.
|
||||
*/
|
||||
CSSStyleDeclaration.prototype.removeProperty = function(propertyName) {
|
||||
if(typeof propertyName === 'undefined') {
|
||||
throw Error('1 argument required, but only 0 present.');
|
||||
}
|
||||
|
||||
this.hasStyle();
|
||||
|
||||
var properties = this.getProperties();
|
||||
this._handleParseError();
|
||||
|
||||
var oldValue = this.getPropertyValue(propertyName);
|
||||
properties.delete(propertyName.trim());
|
||||
return oldValue;
|
||||
};
|
||||
|
||||
/**
|
||||
* Modify an existing CSS property or creates a new CSS property in the declaration block.
|
||||
*
|
||||
* @param {String} propertyName representing the CSS property name to be modified.
|
||||
* @param {String} [value] containing the new property value. If not specified, treated as the empty string. value must not contain "!important" -- that should be set using the priority parameter.
|
||||
* @param {String} [priority] allowing the "important" CSS priority to be set. If not specified, treated as the empty string.
|
||||
* @return {undefined}
|
||||
*/
|
||||
CSSStyleDeclaration.prototype.setProperty = function(propertyName, value, priority) {
|
||||
if(typeof propertyName === 'undefined') {
|
||||
throw Error('propertyName argument required, but only not present.');
|
||||
}
|
||||
|
||||
this.hasStyle();
|
||||
|
||||
var properties = this.getProperties();
|
||||
this._handleParseError();
|
||||
|
||||
var property = {
|
||||
value: value.trim(),
|
||||
priority: priority.trim()
|
||||
};
|
||||
properties.set(propertyName.trim(), property);
|
||||
|
||||
return property;
|
||||
};
|
||||
|
||||
|
||||
module.exports = CSSStyleDeclaration;
|
||||
+348
@@ -0,0 +1,348 @@
|
||||
'use strict';
|
||||
|
||||
var EOL = require('os').EOL,
|
||||
textElem = require('../../plugins/_collections.js').elemsGroups.textContent.concat('title');
|
||||
|
||||
var defaults = {
|
||||
doctypeStart: '<!DOCTYPE',
|
||||
doctypeEnd: '>',
|
||||
procInstStart: '<?',
|
||||
procInstEnd: '?>',
|
||||
tagOpenStart: '<',
|
||||
tagOpenEnd: '>',
|
||||
tagCloseStart: '</',
|
||||
tagCloseEnd: '>',
|
||||
tagShortStart: '<',
|
||||
tagShortEnd: '/>',
|
||||
attrStart: '="',
|
||||
attrEnd: '"',
|
||||
commentStart: '<!--',
|
||||
commentEnd: '-->',
|
||||
cdataStart: '<![CDATA[',
|
||||
cdataEnd: ']]>',
|
||||
textStart: '',
|
||||
textEnd: '',
|
||||
indent: 4,
|
||||
regEntities: /[&'"<>]/g,
|
||||
regValEntities: /[&"<>]/g,
|
||||
encodeEntity: encodeEntity,
|
||||
pretty: false,
|
||||
useShortTags: true
|
||||
};
|
||||
|
||||
var entities = {
|
||||
'&': '&',
|
||||
'\'': ''',
|
||||
'"': '"',
|
||||
'>': '>',
|
||||
'<': '<',
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert SVG-as-JS object to SVG (XML) string.
|
||||
*
|
||||
* @param {Object} data input data
|
||||
* @param {Object} config config
|
||||
*
|
||||
* @return {Object} output data
|
||||
*/
|
||||
module.exports = function(data, config) {
|
||||
|
||||
return new JS2SVG(config).convert(data);
|
||||
|
||||
};
|
||||
|
||||
function JS2SVG(config) {
|
||||
|
||||
if (config) {
|
||||
this.config = Object.assign({}, defaults, config);
|
||||
} else {
|
||||
this.config = Object.assign({}, defaults);
|
||||
}
|
||||
|
||||
var indent = this.config.indent;
|
||||
if (typeof indent == 'number' && !isNaN(indent)) {
|
||||
this.config.indent = (indent < 0) ? '\t' : ' '.repeat(indent);
|
||||
} else if (typeof indent != 'string') {
|
||||
this.config.indent = ' ';
|
||||
}
|
||||
|
||||
if (this.config.pretty) {
|
||||
this.config.doctypeEnd += EOL;
|
||||
this.config.procInstEnd += EOL;
|
||||
this.config.commentEnd += EOL;
|
||||
this.config.cdataEnd += EOL;
|
||||
this.config.tagShortEnd += EOL;
|
||||
this.config.tagOpenEnd += EOL;
|
||||
this.config.tagCloseEnd += EOL;
|
||||
this.config.textEnd += EOL;
|
||||
}
|
||||
|
||||
this.indentLevel = 0;
|
||||
this.textContext = null;
|
||||
|
||||
}
|
||||
|
||||
function encodeEntity(char) {
|
||||
return entities[char];
|
||||
}
|
||||
|
||||
/**
|
||||
* Start conversion.
|
||||
*
|
||||
* @param {Object} data input data
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
JS2SVG.prototype.convert = function(data) {
|
||||
|
||||
var svg = '';
|
||||
|
||||
if (data.content) {
|
||||
|
||||
this.indentLevel++;
|
||||
|
||||
data.content.forEach(function(item) {
|
||||
|
||||
if (item.elem) {
|
||||
svg += this.createElem(item);
|
||||
} else if (item.text) {
|
||||
svg += this.createText(item.text);
|
||||
} else if (item.doctype) {
|
||||
svg += this.createDoctype(item.doctype);
|
||||
} else if (item.processinginstruction) {
|
||||
svg += this.createProcInst(item.processinginstruction);
|
||||
} else if (item.comment) {
|
||||
svg += this.createComment(item.comment);
|
||||
} else if (item.cdata) {
|
||||
svg += this.createCDATA(item.cdata);
|
||||
}
|
||||
|
||||
}, this);
|
||||
|
||||
}
|
||||
|
||||
this.indentLevel--;
|
||||
|
||||
return {
|
||||
data: svg,
|
||||
info: {
|
||||
width: this.width,
|
||||
height: this.height
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Create indent string in accordance with the current node level.
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
JS2SVG.prototype.createIndent = function() {
|
||||
|
||||
var indent = '';
|
||||
|
||||
if (this.config.pretty && !this.textContext) {
|
||||
indent = this.config.indent.repeat(this.indentLevel - 1);
|
||||
}
|
||||
|
||||
return indent;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Create doctype tag.
|
||||
*
|
||||
* @param {String} doctype doctype body string
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
JS2SVG.prototype.createDoctype = function(doctype) {
|
||||
|
||||
return this.config.doctypeStart +
|
||||
doctype +
|
||||
this.config.doctypeEnd;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Create XML Processing Instruction tag.
|
||||
*
|
||||
* @param {Object} instruction instruction object
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
JS2SVG.prototype.createProcInst = function(instruction) {
|
||||
|
||||
return this.config.procInstStart +
|
||||
instruction.name +
|
||||
' ' +
|
||||
instruction.body +
|
||||
this.config.procInstEnd;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Create comment tag.
|
||||
*
|
||||
* @param {String} comment comment body
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
JS2SVG.prototype.createComment = function(comment) {
|
||||
|
||||
return this.config.commentStart +
|
||||
comment +
|
||||
this.config.commentEnd;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Create CDATA section.
|
||||
*
|
||||
* @param {String} cdata CDATA body
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
JS2SVG.prototype.createCDATA = function(cdata) {
|
||||
|
||||
return this.createIndent() +
|
||||
this.config.cdataStart +
|
||||
cdata +
|
||||
this.config.cdataEnd;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Create element tag.
|
||||
*
|
||||
* @param {Object} data element object
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
JS2SVG.prototype.createElem = function(data) {
|
||||
|
||||
// beautiful injection for obtaining SVG information :)
|
||||
if (
|
||||
data.isElem('svg') &&
|
||||
data.hasAttr('width') &&
|
||||
data.hasAttr('height')
|
||||
) {
|
||||
this.width = data.attr('width').value;
|
||||
this.height = data.attr('height').value;
|
||||
}
|
||||
|
||||
// empty element and short tag
|
||||
if (data.isEmpty()) {
|
||||
if (this.config.useShortTags) {
|
||||
return this.createIndent() +
|
||||
this.config.tagShortStart +
|
||||
data.elem +
|
||||
this.createAttrs(data) +
|
||||
this.config.tagShortEnd;
|
||||
} else {
|
||||
return this.createIndent() +
|
||||
this.config.tagShortStart +
|
||||
data.elem +
|
||||
this.createAttrs(data) +
|
||||
this.config.tagOpenEnd +
|
||||
this.config.tagCloseStart +
|
||||
data.elem +
|
||||
this.config.tagCloseEnd;
|
||||
}
|
||||
// non-empty element
|
||||
} else {
|
||||
var tagOpenStart = this.config.tagOpenStart,
|
||||
tagOpenEnd = this.config.tagOpenEnd,
|
||||
tagCloseStart = this.config.tagCloseStart,
|
||||
tagCloseEnd = this.config.tagCloseEnd,
|
||||
openIndent = this.createIndent(),
|
||||
textIndent = '',
|
||||
processedData = '',
|
||||
dataEnd = '';
|
||||
|
||||
if (this.textContext) {
|
||||
tagOpenStart = defaults.tagOpenStart;
|
||||
tagOpenEnd = defaults.tagOpenEnd;
|
||||
tagCloseStart = defaults.tagCloseStart;
|
||||
tagCloseEnd = defaults.tagCloseEnd;
|
||||
openIndent = '';
|
||||
} else if (data.isElem(textElem)) {
|
||||
if (this.config.pretty) {
|
||||
textIndent += openIndent + this.config.indent;
|
||||
}
|
||||
this.textContext = data;
|
||||
}
|
||||
|
||||
processedData += this.convert(data).data;
|
||||
|
||||
if (this.textContext == data) {
|
||||
this.textContext = null;
|
||||
if (this.config.pretty) dataEnd = EOL;
|
||||
}
|
||||
|
||||
return openIndent +
|
||||
tagOpenStart +
|
||||
data.elem +
|
||||
this.createAttrs(data) +
|
||||
tagOpenEnd +
|
||||
textIndent +
|
||||
processedData +
|
||||
dataEnd +
|
||||
this.createIndent() +
|
||||
tagCloseStart +
|
||||
data.elem +
|
||||
tagCloseEnd;
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Create element attributes.
|
||||
*
|
||||
* @param {Object} elem attributes object
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
JS2SVG.prototype.createAttrs = function(elem) {
|
||||
|
||||
var attrs = '';
|
||||
|
||||
elem.eachAttr(function(attr) {
|
||||
|
||||
if (attr.value !== undefined) {
|
||||
attrs += ' ' +
|
||||
attr.name +
|
||||
this.config.attrStart +
|
||||
String(attr.value).replace(this.config.regValEntities, this.config.encodeEntity) +
|
||||
this.config.attrEnd;
|
||||
}
|
||||
else {
|
||||
attrs += ' ' +
|
||||
attr.name;
|
||||
}
|
||||
|
||||
|
||||
}, this);
|
||||
|
||||
return attrs;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Create text node.
|
||||
*
|
||||
* @param {String} text text
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
JS2SVG.prototype.createText = function(text) {
|
||||
|
||||
return this.createIndent() +
|
||||
this.config.textStart +
|
||||
text.replace(this.config.regEntities, this.config.encodeEntity) +
|
||||
(this.textContext ? '' : this.config.textEnd);
|
||||
|
||||
};
|
||||
+372
@@ -0,0 +1,372 @@
|
||||
'use strict';
|
||||
|
||||
var cssSelect = require('css-select');
|
||||
|
||||
var svgoCssSelectAdapter = require('./css-select-adapter');
|
||||
var cssSelectOpts = {
|
||||
xmlMode: true,
|
||||
adapter: svgoCssSelectAdapter
|
||||
};
|
||||
|
||||
var JSAPI = module.exports = function(data, parentNode) {
|
||||
Object.assign(this, data);
|
||||
if (parentNode) {
|
||||
Object.defineProperty(this, 'parentNode', {
|
||||
writable: true,
|
||||
value: parentNode
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Perform a deep clone of this node.
|
||||
*
|
||||
* @return {Object} element
|
||||
*/
|
||||
JSAPI.prototype.clone = function() {
|
||||
var node = this;
|
||||
var nodeData = {};
|
||||
|
||||
Object.keys(node).forEach(function(key) {
|
||||
if (key !== 'class' && key !== 'style' && key !== 'content') {
|
||||
nodeData[key] = node[key];
|
||||
}
|
||||
});
|
||||
|
||||
// Deep-clone node data.
|
||||
nodeData = JSON.parse(JSON.stringify(nodeData));
|
||||
|
||||
// parentNode gets set to a proper object by the parent clone,
|
||||
// but it needs to be true/false now to do the right thing
|
||||
// in the constructor.
|
||||
var clonedNode = new JSAPI(nodeData, !!node.parentNode);
|
||||
|
||||
if (node.class) {
|
||||
clonedNode.class = node.class.clone(clonedNode);
|
||||
}
|
||||
if (node.style) {
|
||||
clonedNode.style = node.style.clone(clonedNode);
|
||||
}
|
||||
if (node.content) {
|
||||
clonedNode.content = node.content.map(function(childNode) {
|
||||
var clonedChild = childNode.clone();
|
||||
clonedChild.parentNode = clonedNode;
|
||||
return clonedChild;
|
||||
});
|
||||
}
|
||||
|
||||
return clonedNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine if item is an element
|
||||
* (any, with a specific name or in a names array).
|
||||
*
|
||||
* @param {String|Array} [param] element name or names arrays
|
||||
* @return {Boolean}
|
||||
*/
|
||||
JSAPI.prototype.isElem = function(param) {
|
||||
|
||||
if (!param) return !!this.elem;
|
||||
|
||||
if (Array.isArray(param)) return !!this.elem && (param.indexOf(this.elem) > -1);
|
||||
|
||||
return !!this.elem && this.elem === param;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Renames an element
|
||||
*
|
||||
* @param {String} name new element name
|
||||
* @return {Object} element
|
||||
*/
|
||||
JSAPI.prototype.renameElem = function(name) {
|
||||
|
||||
if (name && typeof name === 'string')
|
||||
this.elem = this.local = name;
|
||||
|
||||
return this;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine if element is empty.
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
JSAPI.prototype.isEmpty = function() {
|
||||
|
||||
return !this.content || !this.content.length;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Find the closest ancestor of the current element.
|
||||
* @param elemName
|
||||
*
|
||||
* @return {?Object}
|
||||
*/
|
||||
JSAPI.prototype.closestElem = function(elemName) {
|
||||
var elem = this;
|
||||
|
||||
while ((elem = elem.parentNode) && !elem.isElem(elemName));
|
||||
|
||||
return elem;
|
||||
};
|
||||
|
||||
/**
|
||||
* Changes content by removing elements and/or adding new elements.
|
||||
*
|
||||
* @param {Number} start Index at which to start changing the content.
|
||||
* @param {Number} n Number of elements to remove.
|
||||
* @param {Array|Object} [insertion] Elements to add to the content.
|
||||
* @return {Array} Removed elements.
|
||||
*/
|
||||
JSAPI.prototype.spliceContent = function(start, n, insertion) {
|
||||
|
||||
if (arguments.length < 2) return [];
|
||||
|
||||
if (!Array.isArray(insertion))
|
||||
insertion = Array.apply(null, arguments).slice(2);
|
||||
|
||||
insertion.forEach(function(inner) { inner.parentNode = this }, this);
|
||||
|
||||
return this.content.splice.apply(this.content, [start, n].concat(insertion));
|
||||
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine if element has an attribute
|
||||
* (any, or by name or by name + value).
|
||||
*
|
||||
* @param {String} [name] attribute name
|
||||
* @param {String} [val] attribute value (will be toString()'ed)
|
||||
* @return {Boolean}
|
||||
*/
|
||||
JSAPI.prototype.hasAttr = function(name, val) {
|
||||
|
||||
if (!this.attrs || !Object.keys(this.attrs).length) return false;
|
||||
|
||||
if (!arguments.length) return !!this.attrs;
|
||||
|
||||
if (val !== undefined) return !!this.attrs[name] && this.attrs[name].value === val.toString();
|
||||
|
||||
return !!this.attrs[name];
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine if element has an attribute by local name
|
||||
* (any, or by name or by name + value).
|
||||
*
|
||||
* @param {String} [localName] local attribute name
|
||||
* @param {Number|String|RegExp|Function} [val] attribute value (will be toString()'ed or executed, otherwise ignored)
|
||||
* @return {Boolean}
|
||||
*/
|
||||
JSAPI.prototype.hasAttrLocal = function(localName, val) {
|
||||
|
||||
if (!this.attrs || !Object.keys(this.attrs).length) return false;
|
||||
|
||||
if (!arguments.length) return !!this.attrs;
|
||||
|
||||
var callback;
|
||||
|
||||
switch (val != null && val.constructor && val.constructor.name) {
|
||||
case 'Number': // same as String
|
||||
case 'String': callback = stringValueTest; break;
|
||||
case 'RegExp': callback = regexpValueTest; break;
|
||||
case 'Function': callback = funcValueTest; break;
|
||||
default: callback = nameTest;
|
||||
}
|
||||
return this.someAttr(callback);
|
||||
|
||||
function nameTest(attr) {
|
||||
return attr.local === localName;
|
||||
}
|
||||
|
||||
function stringValueTest(attr) {
|
||||
return attr.local === localName && val == attr.value;
|
||||
}
|
||||
|
||||
function regexpValueTest(attr) {
|
||||
return attr.local === localName && val.test(attr.value);
|
||||
}
|
||||
|
||||
function funcValueTest(attr) {
|
||||
return attr.local === localName && val(attr.value);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a specific attribute from an element
|
||||
* (by name or name + value).
|
||||
*
|
||||
* @param {String} name attribute name
|
||||
* @param {String} [val] attribute value (will be toString()'ed)
|
||||
* @return {Object|Undefined}
|
||||
*/
|
||||
JSAPI.prototype.attr = function(name, val) {
|
||||
|
||||
if (!this.hasAttr() || !arguments.length) return undefined;
|
||||
|
||||
if (val !== undefined) return this.hasAttr(name, val) ? this.attrs[name] : undefined;
|
||||
|
||||
return this.attrs[name];
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Get computed attribute value from an element
|
||||
*
|
||||
* @param {String} name attribute name
|
||||
* @return {Object|Undefined}
|
||||
*/
|
||||
JSAPI.prototype.computedAttr = function(name, val) {
|
||||
/* jshint eqnull: true */
|
||||
if (!arguments.length) return;
|
||||
|
||||
for (var elem = this; elem && (!elem.hasAttr(name) || !elem.attr(name).value); elem = elem.parentNode);
|
||||
|
||||
if (val != null) {
|
||||
return elem ? elem.hasAttr(name, val) : false;
|
||||
} else if (elem && elem.hasAttr(name)) {
|
||||
return elem.attrs[name].value;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a specific attribute.
|
||||
*
|
||||
* @param {String|Array} name attribute name
|
||||
* @param {String} [val] attribute value
|
||||
* @return {Boolean}
|
||||
*/
|
||||
JSAPI.prototype.removeAttr = function(name, val, recursive) {
|
||||
|
||||
if (!arguments.length) return false;
|
||||
|
||||
if (Array.isArray(name)) {
|
||||
name.forEach(this.removeAttr, this);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.hasAttr(name)) return false;
|
||||
|
||||
if (!recursive && val && this.attrs[name].value !== val) return false;
|
||||
|
||||
delete this.attrs[name];
|
||||
|
||||
if (!Object.keys(this.attrs).length) delete this.attrs;
|
||||
|
||||
return true;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Add attribute.
|
||||
*
|
||||
* @param {Object} [attr={}] attribute object
|
||||
* @return {Object|Boolean} created attribute or false if no attr was passed in
|
||||
*/
|
||||
JSAPI.prototype.addAttr = function(attr) {
|
||||
attr = attr || {};
|
||||
|
||||
if (attr.name === undefined ||
|
||||
attr.prefix === undefined ||
|
||||
attr.local === undefined
|
||||
) return false;
|
||||
|
||||
this.attrs = this.attrs || {};
|
||||
this.attrs[attr.name] = attr;
|
||||
|
||||
if(attr.name === 'class') { // newly added class attribute
|
||||
this.class.hasClass();
|
||||
}
|
||||
|
||||
if(attr.name === 'style') { // newly added style attribute
|
||||
this.style.hasStyle();
|
||||
}
|
||||
|
||||
return this.attrs[attr.name];
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Iterates over all attributes.
|
||||
*
|
||||
* @param {Function} callback callback
|
||||
* @param {Object} [context] callback context
|
||||
* @return {Boolean} false if there are no any attributes
|
||||
*/
|
||||
JSAPI.prototype.eachAttr = function(callback, context) {
|
||||
|
||||
if (!this.hasAttr()) return false;
|
||||
|
||||
for (var name in this.attrs) {
|
||||
callback.call(context, this.attrs[name]);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests whether some attribute passes the test.
|
||||
*
|
||||
* @param {Function} callback callback
|
||||
* @param {Object} [context] callback context
|
||||
* @return {Boolean} false if there are no any attributes
|
||||
*/
|
||||
JSAPI.prototype.someAttr = function(callback, context) {
|
||||
|
||||
if (!this.hasAttr()) return false;
|
||||
|
||||
for (var name in this.attrs) {
|
||||
if (callback.call(context, this.attrs[name])) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Evaluate a string of CSS selectors against the element and returns matched elements.
|
||||
*
|
||||
* @param {String} selectors CSS selector(s) string
|
||||
* @return {Array} null if no elements matched
|
||||
*/
|
||||
JSAPI.prototype.querySelectorAll = function(selectors) {
|
||||
|
||||
var matchedEls = cssSelect(selectors, this, cssSelectOpts);
|
||||
|
||||
return matchedEls.length > 0 ? matchedEls : null;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Evaluate a string of CSS selectors against the element and returns only the first matched element.
|
||||
*
|
||||
* @param {String} selectors CSS selector(s) string
|
||||
* @return {Array} null if no element matched
|
||||
*/
|
||||
JSAPI.prototype.querySelector = function(selectors) {
|
||||
|
||||
return cssSelect.selectOne(selectors, this, cssSelectOpts);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Test if a selector matches a given element.
|
||||
*
|
||||
* @param {String} selector CSS selector string
|
||||
* @return {Boolean} true if element would be selected by selector string, false if it does not
|
||||
*/
|
||||
JSAPI.prototype.matches = function(selector) {
|
||||
|
||||
return cssSelect.is(this, selector, cssSelectOpts);
|
||||
|
||||
};
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Plugins engine.
|
||||
*
|
||||
* @module plugins
|
||||
*
|
||||
* @param {Object} data input data
|
||||
* @param {Object} info extra information
|
||||
* @param {Object} plugins plugins object from config
|
||||
* @return {Object} output data
|
||||
*/
|
||||
module.exports = function(data, info, plugins) {
|
||||
|
||||
plugins.forEach(function(group) {
|
||||
|
||||
switch(group[0].type) {
|
||||
case 'perItem':
|
||||
data = perItem(data, info, group);
|
||||
break;
|
||||
case 'perItemReverse':
|
||||
data = perItem(data, info, group, true);
|
||||
break;
|
||||
case 'full':
|
||||
data = full(data, info, group);
|
||||
break;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
return data;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Direct or reverse per-item loop.
|
||||
*
|
||||
* @param {Object} data input data
|
||||
* @param {Object} info extra information
|
||||
* @param {Array} plugins plugins list to process
|
||||
* @param {Boolean} [reverse] reverse pass?
|
||||
* @return {Object} output data
|
||||
*/
|
||||
function perItem(data, info, plugins, reverse) {
|
||||
|
||||
function monkeys(items) {
|
||||
|
||||
items.content = items.content.filter(function(item) {
|
||||
|
||||
// reverse pass
|
||||
if (reverse && item.content) {
|
||||
monkeys(item);
|
||||
}
|
||||
|
||||
// main filter
|
||||
var filter = true;
|
||||
|
||||
for (var i = 0; filter && i < plugins.length; i++) {
|
||||
var plugin = plugins[i];
|
||||
|
||||
if (plugin.active && plugin.fn(item, plugin.params, info) === false) {
|
||||
filter = false;
|
||||
}
|
||||
}
|
||||
|
||||
// direct pass
|
||||
if (!reverse && item.content) {
|
||||
monkeys(item);
|
||||
}
|
||||
|
||||
return filter;
|
||||
|
||||
});
|
||||
|
||||
return items;
|
||||
|
||||
}
|
||||
|
||||
return monkeys(data);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* "Full" plugins.
|
||||
*
|
||||
* @param {Object} data input data
|
||||
* @param {Object} info extra information
|
||||
* @param {Array} plugins plugins list to process
|
||||
* @return {Object} output data
|
||||
*/
|
||||
function full(data, info, plugins) {
|
||||
|
||||
plugins.forEach(function(plugin) {
|
||||
if (plugin.active) {
|
||||
data = plugin.fn(data, plugin.params, info);
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
|
||||
}
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
'use strict';
|
||||
|
||||
var SAX = require('sax'),
|
||||
JSAPI = require('./jsAPI.js'),
|
||||
CSSClassList = require('./css-class-list'),
|
||||
CSSStyleDeclaration = require('./css-style-declaration'),
|
||||
entityDeclaration = /<!ENTITY\s+(\S+)\s+(?:'([^\']+)'|"([^\"]+)")\s*>/g;
|
||||
|
||||
var config = {
|
||||
strict: true,
|
||||
trim: false,
|
||||
normalize: true,
|
||||
lowercase: true,
|
||||
xmlns: true,
|
||||
position: true
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert SVG (XML) string to SVG-as-JS object.
|
||||
*
|
||||
* @param {String} data input data
|
||||
* @param {Function} callback
|
||||
*/
|
||||
module.exports = function(data, callback) {
|
||||
|
||||
var sax = SAX.parser(config.strict, config),
|
||||
root = new JSAPI({ elem: '#document', content: [] }),
|
||||
current = root,
|
||||
stack = [root],
|
||||
textContext = null,
|
||||
parsingError = false;
|
||||
|
||||
function pushToContent(content) {
|
||||
|
||||
content = new JSAPI(content, current);
|
||||
|
||||
(current.content = current.content || []).push(content);
|
||||
|
||||
return content;
|
||||
|
||||
}
|
||||
|
||||
sax.ondoctype = function(doctype) {
|
||||
|
||||
pushToContent({
|
||||
doctype: doctype
|
||||
});
|
||||
|
||||
var subsetStart = doctype.indexOf('['),
|
||||
entityMatch;
|
||||
|
||||
if (subsetStart >= 0) {
|
||||
entityDeclaration.lastIndex = subsetStart;
|
||||
|
||||
while ((entityMatch = entityDeclaration.exec(data)) != null) {
|
||||
sax.ENTITIES[entityMatch[1]] = entityMatch[2] || entityMatch[3];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
sax.onprocessinginstruction = function(data) {
|
||||
|
||||
pushToContent({
|
||||
processinginstruction: data
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
sax.oncomment = function(comment) {
|
||||
|
||||
pushToContent({
|
||||
comment: comment.trim()
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
sax.oncdata = function(cdata) {
|
||||
|
||||
pushToContent({
|
||||
cdata: cdata
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
sax.onopentag = function(data) {
|
||||
|
||||
var elem = {
|
||||
elem: data.name,
|
||||
prefix: data.prefix,
|
||||
local: data.local,
|
||||
attrs: {}
|
||||
};
|
||||
|
||||
elem.class = new CSSClassList(elem);
|
||||
elem.style = new CSSStyleDeclaration(elem);
|
||||
|
||||
if (Object.keys(data.attributes).length) {
|
||||
for (var name in data.attributes) {
|
||||
|
||||
if (name === 'class') { // has class attribute
|
||||
elem.class.hasClass();
|
||||
}
|
||||
|
||||
if (name === 'style') { // has style attribute
|
||||
elem.style.hasStyle();
|
||||
}
|
||||
|
||||
elem.attrs[name] = {
|
||||
name: name,
|
||||
value: data.attributes[name].value,
|
||||
prefix: data.attributes[name].prefix,
|
||||
local: data.attributes[name].local
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
elem = pushToContent(elem);
|
||||
current = elem;
|
||||
|
||||
// Save info about <text> tag to prevent trimming of meaningful whitespace
|
||||
if (data.name == 'text' && !data.prefix) {
|
||||
textContext = current;
|
||||
}
|
||||
|
||||
stack.push(elem);
|
||||
|
||||
};
|
||||
|
||||
sax.ontext = function(text) {
|
||||
|
||||
if (/\S/.test(text) || textContext) {
|
||||
|
||||
if (!textContext)
|
||||
text = text.trim();
|
||||
|
||||
pushToContent({
|
||||
text: text
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
sax.onclosetag = function() {
|
||||
|
||||
var last = stack.pop();
|
||||
|
||||
// Trim text inside <text> tag.
|
||||
if (last == textContext) {
|
||||
trim(textContext);
|
||||
textContext = null;
|
||||
}
|
||||
current = stack[stack.length - 1];
|
||||
|
||||
};
|
||||
|
||||
sax.onerror = function(e) {
|
||||
|
||||
e.message = 'Error in parsing SVG: ' + e.message;
|
||||
if (e.message.indexOf('Unexpected end') < 0) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
sax.onend = function() {
|
||||
|
||||
if (!this.error) {
|
||||
callback(root);
|
||||
} else {
|
||||
callback({ error: this.error.message });
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
try {
|
||||
sax.write(data);
|
||||
} catch (e) {
|
||||
callback({ error: e.message });
|
||||
parsingError = true;
|
||||
}
|
||||
if (!parsingError) sax.close();
|
||||
|
||||
function trim(elem) {
|
||||
if (!elem.content) return elem;
|
||||
|
||||
var start = elem.content[0],
|
||||
end = elem.content[elem.content.length - 1];
|
||||
|
||||
while (start && start.content && !start.text) start = start.content[0];
|
||||
if (start && start.text) start.text = start.text.replace(/^\s+/, '');
|
||||
|
||||
while (end && end.content && !end.text) end = end.content[end.content.length - 1];
|
||||
if (end && end.text) end.text = end.text.replace(/\s+$/, '');
|
||||
|
||||
return elem;
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
'use strict';
|
||||
|
||||
var FS = require('fs');
|
||||
|
||||
/**
|
||||
* Encode plain SVG data string into Data URI string.
|
||||
*
|
||||
* @param {String} str input string
|
||||
* @param {String} type Data URI type
|
||||
* @return {String} output string
|
||||
*/
|
||||
exports.encodeSVGDatauri = function(str, type) {
|
||||
var prefix = 'data:image/svg+xml';
|
||||
if (!type || type === 'base64') {
|
||||
// base64
|
||||
prefix += ';base64,';
|
||||
if (Buffer.from) {
|
||||
str = prefix + Buffer.from(str).toString('base64');
|
||||
} else {
|
||||
str = prefix + new Buffer(str).toString('base64');
|
||||
}
|
||||
} else if (type === 'enc') {
|
||||
// URI encoded
|
||||
str = prefix + ',' + encodeURIComponent(str);
|
||||
} else if (type === 'unenc') {
|
||||
// unencoded
|
||||
str = prefix + ',' + str;
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
/**
|
||||
* Decode SVG Data URI string into plain SVG string.
|
||||
*
|
||||
* @param {string} str input string
|
||||
* @return {String} output string
|
||||
*/
|
||||
exports.decodeSVGDatauri = function(str) {
|
||||
var regexp = /data:image\/svg\+xml(;charset=[^;,]*)?(;base64)?,(.*)/;
|
||||
var match = regexp.exec(str);
|
||||
|
||||
// plain string
|
||||
if (!match) return str;
|
||||
|
||||
var data = match[3];
|
||||
|
||||
if (match[2]) {
|
||||
// base64
|
||||
str = new Buffer(data, 'base64').toString('utf8');
|
||||
} else if (data.charAt(0) === '%') {
|
||||
// URI encoded
|
||||
str = decodeURIComponent(data);
|
||||
} else if (data.charAt(0) === '<') {
|
||||
// unencoded
|
||||
str = data;
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
exports.intersectArrays = function(a, b) {
|
||||
return a.filter(function(n) {
|
||||
return b.indexOf(n) > -1;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a row of numbers to an optimized string view.
|
||||
*
|
||||
* @example
|
||||
* [0, -1, .5, .5] → "0-1 .5.5"
|
||||
*
|
||||
* @param {number[]} data
|
||||
* @param {Object} params
|
||||
* @param {string?} command path data instruction
|
||||
* @return {string}
|
||||
*/
|
||||
exports.cleanupOutData = function(data, params, command) {
|
||||
var str = '',
|
||||
delimiter,
|
||||
prev;
|
||||
|
||||
data.forEach(function(item, i) {
|
||||
// space delimiter by default
|
||||
delimiter = ' ';
|
||||
|
||||
// no extra space in front of first number
|
||||
if (i == 0) delimiter = '';
|
||||
|
||||
// no extra space after 'arcto' command flags
|
||||
if (params.noSpaceAfterFlags && (command == 'A' || command == 'a')) {
|
||||
var pos = i % 7;
|
||||
if (pos == 4 || pos == 5) delimiter = '';
|
||||
}
|
||||
|
||||
// remove floating-point numbers leading zeros
|
||||
// 0.5 → .5
|
||||
// -0.5 → -.5
|
||||
if (params.leadingZero) {
|
||||
item = removeLeadingZero(item);
|
||||
}
|
||||
|
||||
// no extra space in front of negative number or
|
||||
// in front of a floating number if a previous number is floating too
|
||||
if (
|
||||
params.negativeExtraSpace &&
|
||||
delimiter != '' &&
|
||||
(item < 0 ||
|
||||
(String(item).charCodeAt(0) == 46 && prev % 1 !== 0)
|
||||
)
|
||||
) {
|
||||
delimiter = '';
|
||||
}
|
||||
// save prev item value
|
||||
prev = item;
|
||||
str += delimiter + item;
|
||||
});
|
||||
return str;
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove floating-point numbers leading zero.
|
||||
*
|
||||
* @example
|
||||
* 0.5 → .5
|
||||
*
|
||||
* @example
|
||||
* -0.5 → -.5
|
||||
*
|
||||
* @param {Float} num input number
|
||||
*
|
||||
* @return {String} output number as string
|
||||
*/
|
||||
var removeLeadingZero = exports.removeLeadingZero = function(num) {
|
||||
var strNum = num.toString();
|
||||
|
||||
if (0 < num && num < 1 && strNum.charCodeAt(0) == 48) {
|
||||
strNum = strNum.slice(1);
|
||||
} else if (-1 < num && num < 0 && strNum.charCodeAt(1) == 48) {
|
||||
strNum = strNum.charAt(0) + strNum.slice(2);
|
||||
}
|
||||
return strNum;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Synchronously check if path is a directory. Tolerant to errors like ENOENT.
|
||||
* @param {string} path
|
||||
*/
|
||||
exports.checkIsDir = function(path) {
|
||||
try {
|
||||
return FS.lstatSync(path).isDirectory();
|
||||
} catch(e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
+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;
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"_from": "css-select@^2.0.0",
|
||||
"_id": "css-select@2.1.0",
|
||||
"_inBundle": false,
|
||||
"_integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==",
|
||||
"_location": "/svgo/css-select",
|
||||
"_phantomChildren": {},
|
||||
"_requested": {
|
||||
"type": "range",
|
||||
"registry": true,
|
||||
"raw": "css-select@^2.0.0",
|
||||
"name": "css-select",
|
||||
"escapedName": "css-select",
|
||||
"rawSpec": "^2.0.0",
|
||||
"saveSpec": null,
|
||||
"fetchSpec": "^2.0.0"
|
||||
},
|
||||
"_requiredBy": [
|
||||
"/svgo"
|
||||
],
|
||||
"_resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz",
|
||||
"_shasum": "6a34653356635934a81baca68d0255432105dbef",
|
||||
"_spec": "css-select@^2.0.0",
|
||||
"_where": "O:\\zero\\zero.Web\\node_modules\\svgo",
|
||||
"author": {
|
||||
"name": "Felix Boehm",
|
||||
"email": "me@feedic.com"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/fb55/css-select/issues"
|
||||
},
|
||||
"bundleDependencies": false,
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0",
|
||||
"css-what": "^3.2.1",
|
||||
"domutils": "^1.7.0",
|
||||
"nth-check": "^1.0.2"
|
||||
},
|
||||
"deprecated": false,
|
||||
"description": "a CSS selector compiler/engine",
|
||||
"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"
|
||||
},
|
||||
"files": [
|
||||
"index.js",
|
||||
"index.d.ts",
|
||||
"lib"
|
||||
],
|
||||
"homepage": "https://github.com/fb55/css-select#readme",
|
||||
"keywords": [
|
||||
"css",
|
||||
"selector",
|
||||
"sizzle"
|
||||
],
|
||||
"license": "BSD-2-Clause",
|
||||
"name": "css-select",
|
||||
"prettier": {
|
||||
"tabWidth": 4
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/fb55/css-select.git"
|
||||
},
|
||||
"scripts": {
|
||||
"coveralls": "npm run lint && npm run lcov && (cat coverage/lcov.info | coveralls || exit 0)",
|
||||
"lcov": "istanbul cover _mocha --report lcovonly -- -R spec",
|
||||
"lint": "eslint index.js lib/*.js test/*.js",
|
||||
"test": "mocha && npm run lint"
|
||||
},
|
||||
"types": "index.d.ts",
|
||||
"version": "2.1.0"
|
||||
}
|
||||
+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"}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
"use strict";
|
||||
function __export(m) {
|
||||
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
|
||||
}
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
__export(require("./parse"));
|
||||
var parse_1 = require("./parse");
|
||||
exports.parse = parse_1.default;
|
||||
var stringify_1 = require("./stringify");
|
||||
exports.stringify = 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;AA2DhB,iBAAS,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,QAAQ,EAAE,EAAE,CAUhE"}
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.default = parse;
|
||||
var reName = /^(?:\\([\da-f]{1,6}\s?|(\s)|.)|[\w\-\u00b0-\uFFFF])+/, reEscape = /\\([\da-f]{1,6}\s?|(\s)|.)/gi,
|
||||
//modified version of https://github.com/jquery/sizzle/blob/master/src/sizzle.js#L87
|
||||
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"]);
|
||||
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 tokens = [], 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 _a = attribSelectors[firstChar], name_1 = _a[0], action = _a[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 data = selector.match(reAttr);
|
||||
if (!data) {
|
||||
throw new Error("Malformed attribute selector: " + selector);
|
||||
}
|
||||
selector = selector.substr(data[0].length);
|
||||
var name_2 = unescapeCSS(data[1]);
|
||||
if (!options ||
|
||||
("lowerCaseAttributeNames" in options
|
||||
? options.lowerCaseAttributeNames
|
||||
: !options.xmlMode)) {
|
||||
name_2 = name_2.toLowerCase();
|
||||
}
|
||||
tokens.push({
|
||||
type: "attribute",
|
||||
name: name_2,
|
||||
action: actionTypes[data[2]],
|
||||
value: unescapeCSS(data[4] || data[5] || ""),
|
||||
ignoreCase: !!data[6]
|
||||
});
|
||||
}
|
||||
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.charAt(0) === "(") {
|
||||
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.charAt(0) !== quot) {
|
||||
throw new Error("Unmatched quotes in :" + name_3);
|
||||
}
|
||||
else {
|
||||
selector = selector.substr(1);
|
||||
}
|
||||
}
|
||||
if (selector.charAt(0) !== ")") {
|
||||
throw new Error("Missing closing parenthesis in :" + name_3 + " (" + selector + ")");
|
||||
}
|
||||
selector = selector.substr(1);
|
||||
}
|
||||
else {
|
||||
var pos = 1, 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 (!options ||
|
||||
("lowerCaseTags" in options
|
||||
? options.lowerCaseTags
|
||||
: !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;AAqBnC,MAAM,CAAC,OAAO,UAAU,SAAS,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,GAAG,MAAM,CAE7D"}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var actionTypes = {
|
||||
equals: "",
|
||||
element: "~",
|
||||
start: "^",
|
||||
end: "$",
|
||||
any: "*",
|
||||
not: "!",
|
||||
hyphen: "|"
|
||||
};
|
||||
var simpleSelectors = {
|
||||
child: " > ",
|
||||
parent: " < ",
|
||||
sibling: " ~ ",
|
||||
adjacent: " + ",
|
||||
descendant: " ",
|
||||
universal: "*"
|
||||
};
|
||||
function stringify(token) {
|
||||
return token.map(stringifySubselector).join(", ");
|
||||
}
|
||||
exports.default = stringify;
|
||||
function stringifySubselector(token) {
|
||||
return token.map(stringifyToken).join("");
|
||||
}
|
||||
function stringifyToken(token) {
|
||||
if (token.type in simpleSelectors)
|
||||
return simpleSelectors[token.type];
|
||||
if (token.type === "tag")
|
||||
return escapeName(token.name);
|
||||
if (token.type === "pseudo-element")
|
||||
return "::" + escapeName(token.name);
|
||||
if (token.type === "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);
|
||||
}
|
||||
var atributeName = escapeName(token.name);
|
||||
var action = actionTypes[token.action];
|
||||
var value = escapeName(token.value);
|
||||
var ignoreCase = token.ignoreCase ? "i" : "";
|
||||
return "[" + atributeName + action + "='" + value + "'" + ignoreCase + "]";
|
||||
}
|
||||
if (token.type === "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) + ")";
|
||||
}
|
||||
throw new Error("Unknown type");
|
||||
}
|
||||
function escapeName(str) {
|
||||
//TODO
|
||||
return str;
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"_from": "css-what@^3.2.1",
|
||||
"_id": "css-what@3.2.1",
|
||||
"_inBundle": false,
|
||||
"_integrity": "sha512-WwOrosiQTvyms+Ti5ZC5vGEK0Vod3FTt1ca+payZqvKuGJF+dq7bG63DstxtN0dpm6FxY27a/zS3Wten+gEtGw==",
|
||||
"_location": "/svgo/css-what",
|
||||
"_phantomChildren": {},
|
||||
"_requested": {
|
||||
"type": "range",
|
||||
"registry": true,
|
||||
"raw": "css-what@^3.2.1",
|
||||
"name": "css-what",
|
||||
"escapedName": "css-what",
|
||||
"rawSpec": "^3.2.1",
|
||||
"saveSpec": null,
|
||||
"fetchSpec": "^3.2.1"
|
||||
},
|
||||
"_requiredBy": [
|
||||
"/svgo/css-select"
|
||||
],
|
||||
"_resolved": "https://registry.npmjs.org/css-what/-/css-what-3.2.1.tgz",
|
||||
"_shasum": "f4a8f12421064621b456755e34a03a2c22df5da1",
|
||||
"_spec": "css-what@^3.2.1",
|
||||
"_where": "O:\\zero\\zero.Web\\node_modules\\svgo\\node_modules\\css-select",
|
||||
"author": {
|
||||
"name": "Felix Böhm",
|
||||
"email": "me@feedic.com",
|
||||
"url": "http://feedic.com"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/fb55/css-what/issues"
|
||||
},
|
||||
"bundleDependencies": false,
|
||||
"dependencies": {},
|
||||
"deprecated": false,
|
||||
"description": "a CSS selector parser",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^24.0.16",
|
||||
"@types/node": "^12.6.2",
|
||||
"@typescript-eslint/eslint-plugin": "^1.12.0",
|
||||
"@typescript-eslint/parser": "^1.12.0",
|
||||
"coveralls": "^3.0.5",
|
||||
"eslint": "^6.0.1",
|
||||
"eslint-config-prettier": "^6.0.0",
|
||||
"jest": "^24.8.0",
|
||||
"prettier": "^1.18.2",
|
||||
"ts-jest": "^24.0.2",
|
||||
"typescript": "^3.5.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
},
|
||||
"files": [
|
||||
"lib/**/*"
|
||||
],
|
||||
"homepage": "https://github.com/fb55/css-what#readme",
|
||||
"jest": {
|
||||
"preset": "ts-jest",
|
||||
"testEnvironment": "node"
|
||||
},
|
||||
"license": "BSD-2-Clause",
|
||||
"main": "lib/index.js",
|
||||
"name": "css-what",
|
||||
"optionalDependencies": {},
|
||||
"prettier": {
|
||||
"tabWidth": 4
|
||||
},
|
||||
"repository": {
|
||||
"url": "git+https://github.com/fb55/css-what.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"coverage": "cat coverage/lcov.info | coveralls",
|
||||
"format": "prettier --write '**/*.{ts,md,json}'",
|
||||
"lint": "eslint --ext=js,ts src",
|
||||
"prepare": "npm run build",
|
||||
"test": "jest --coverage -u && npm run lint"
|
||||
},
|
||||
"types": "lib/index.d.ts",
|
||||
"version": "3.2.1"
|
||||
}
|
||||
+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)
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
sudo: true
|
||||
language: node_js
|
||||
node_js:
|
||||
- 8
|
||||
|
||||
+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.
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
var DomUtils = module.exports;
|
||||
|
||||
[
|
||||
require("./lib/stringify"),
|
||||
require("./lib/traversal"),
|
||||
require("./lib/manipulation"),
|
||||
require("./lib/querying"),
|
||||
require("./lib/legacy"),
|
||||
require("./lib/helpers")
|
||||
].forEach(function(ext){
|
||||
Object.keys(ext).forEach(function(key){
|
||||
DomUtils[key] = ext[key].bind(DomUtils);
|
||||
});
|
||||
});
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
// removeSubsets
|
||||
// Given an array of nodes, remove any member that is contained by another.
|
||||
exports.removeSubsets = function(nodes) {
|
||||
var idx = nodes.length, node, ancestor, replace;
|
||||
|
||||
// Check if each node (or one of its ancestors) is already contained in the
|
||||
// array.
|
||||
while (--idx > -1) {
|
||||
node = ancestor = nodes[idx];
|
||||
|
||||
// Temporarily remove the node under consideration
|
||||
nodes[idx] = null;
|
||||
replace = true;
|
||||
|
||||
while (ancestor) {
|
||||
if (nodes.indexOf(ancestor) > -1) {
|
||||
replace = false;
|
||||
nodes.splice(idx, 1);
|
||||
break;
|
||||
}
|
||||
ancestor = ancestor.parent;
|
||||
}
|
||||
|
||||
// If the node has been found to be unique, re-insert it.
|
||||
if (replace) {
|
||||
nodes[idx] = node;
|
||||
}
|
||||
}
|
||||
|
||||
return nodes;
|
||||
};
|
||||
|
||||
// Source: http://dom.spec.whatwg.org/#dom-node-comparedocumentposition
|
||||
var POSITION = {
|
||||
DISCONNECTED: 1,
|
||||
PRECEDING: 2,
|
||||
FOLLOWING: 4,
|
||||
CONTAINS: 8,
|
||||
CONTAINED_BY: 16
|
||||
};
|
||||
|
||||
// Compare the position of one node against another node in any other document.
|
||||
// The return value is a bitmask with the following values:
|
||||
//
|
||||
// document order:
|
||||
// > There is an ordering, document order, defined on all the nodes in the
|
||||
// > document corresponding to the order in which the first character of the
|
||||
// > XML representation of each node occurs in the XML representation of the
|
||||
// > document after expansion of general entities. Thus, the document element
|
||||
// > node will be the first node. Element nodes occur before their children.
|
||||
// > Thus, document order orders element nodes in order of the occurrence of
|
||||
// > their start-tag in the XML (after expansion of entities). The attribute
|
||||
// > nodes of an element occur after the element and before its children. The
|
||||
// > relative order of attribute nodes is implementation-dependent./
|
||||
// Source:
|
||||
// http://www.w3.org/TR/DOM-Level-3-Core/glossary.html#dt-document-order
|
||||
//
|
||||
// @argument {Node} nodaA The first node to use in the comparison
|
||||
// @argument {Node} nodeB The second node to use in the comparison
|
||||
//
|
||||
// @return {Number} A bitmask describing the input nodes' relative position.
|
||||
// See http://dom.spec.whatwg.org/#dom-node-comparedocumentposition for
|
||||
// a description of these values.
|
||||
var comparePos = exports.compareDocumentPosition = function(nodeA, nodeB) {
|
||||
var aParents = [];
|
||||
var bParents = [];
|
||||
var current, sharedParent, siblings, aSibling, bSibling, idx;
|
||||
|
||||
if (nodeA === nodeB) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
current = nodeA;
|
||||
while (current) {
|
||||
aParents.unshift(current);
|
||||
current = current.parent;
|
||||
}
|
||||
current = nodeB;
|
||||
while (current) {
|
||||
bParents.unshift(current);
|
||||
current = current.parent;
|
||||
}
|
||||
|
||||
idx = 0;
|
||||
while (aParents[idx] === bParents[idx]) {
|
||||
idx++;
|
||||
}
|
||||
|
||||
if (idx === 0) {
|
||||
return POSITION.DISCONNECTED;
|
||||
}
|
||||
|
||||
sharedParent = aParents[idx - 1];
|
||||
siblings = sharedParent.children;
|
||||
aSibling = aParents[idx];
|
||||
bSibling = bParents[idx];
|
||||
|
||||
if (siblings.indexOf(aSibling) > siblings.indexOf(bSibling)) {
|
||||
if (sharedParent === nodeB) {
|
||||
return POSITION.FOLLOWING | POSITION.CONTAINED_BY;
|
||||
}
|
||||
return POSITION.FOLLOWING;
|
||||
} else {
|
||||
if (sharedParent === nodeA) {
|
||||
return POSITION.PRECEDING | POSITION.CONTAINS;
|
||||
}
|
||||
return POSITION.PRECEDING;
|
||||
}
|
||||
};
|
||||
|
||||
// Sort an array of nodes based on their relative position in the document and
|
||||
// remove any duplicate nodes. If the array contains nodes that do not belong
|
||||
// to the same document, sort order is unspecified.
|
||||
//
|
||||
// @argument {Array} nodes Array of DOM nodes
|
||||
//
|
||||
// @returns {Array} collection of unique nodes, sorted in document order
|
||||
exports.uniqueSort = function(nodes) {
|
||||
var idx = nodes.length, node, position;
|
||||
|
||||
nodes = nodes.slice();
|
||||
|
||||
while (--idx > -1) {
|
||||
node = nodes[idx];
|
||||
position = nodes.indexOf(node);
|
||||
if (position > -1 && position < idx) {
|
||||
nodes.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
nodes.sort(function(a, b) {
|
||||
var relative = comparePos(a, b);
|
||||
if (relative & POSITION.PRECEDING) {
|
||||
return -1;
|
||||
} else if (relative & POSITION.FOLLOWING) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
return nodes;
|
||||
};
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
var ElementType = require("domelementtype");
|
||||
var isTag = exports.isTag = ElementType.isTag;
|
||||
|
||||
exports.testElement = function(options, element){
|
||||
for(var key in options){
|
||||
if(!options.hasOwnProperty(key));
|
||||
else if(key === "tag_name"){
|
||||
if(!isTag(element) || !options.tag_name(element.name)){
|
||||
return false;
|
||||
}
|
||||
} else if(key === "tag_type"){
|
||||
if(!options.tag_type(element.type)) return false;
|
||||
} else if(key === "tag_contains"){
|
||||
if(isTag(element) || !options.tag_contains(element.data)){
|
||||
return false;
|
||||
}
|
||||
} else if(!element.attribs || !options[key](element.attribs[key])){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
var Checks = {
|
||||
tag_name: function(name){
|
||||
if(typeof name === "function"){
|
||||
return function(elem){ return isTag(elem) && name(elem.name); };
|
||||
} else if(name === "*"){
|
||||
return isTag;
|
||||
} else {
|
||||
return function(elem){ return isTag(elem) && elem.name === name; };
|
||||
}
|
||||
},
|
||||
tag_type: function(type){
|
||||
if(typeof type === "function"){
|
||||
return function(elem){ return type(elem.type); };
|
||||
} else {
|
||||
return function(elem){ return elem.type === type; };
|
||||
}
|
||||
},
|
||||
tag_contains: function(data){
|
||||
if(typeof data === "function"){
|
||||
return function(elem){ return !isTag(elem) && data(elem.data); };
|
||||
} else {
|
||||
return function(elem){ return !isTag(elem) && elem.data === data; };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function getAttribCheck(attrib, value){
|
||||
if(typeof value === "function"){
|
||||
return function(elem){ return elem.attribs && value(elem.attribs[attrib]); };
|
||||
} else {
|
||||
return function(elem){ return elem.attribs && elem.attribs[attrib] === value; };
|
||||
}
|
||||
}
|
||||
|
||||
function combineFuncs(a, b){
|
||||
return function(elem){
|
||||
return a(elem) || b(elem);
|
||||
};
|
||||
}
|
||||
|
||||
exports.getElements = function(options, element, recurse, limit){
|
||||
var funcs = Object.keys(options).map(function(key){
|
||||
var value = options[key];
|
||||
return key in Checks ? Checks[key](value) : getAttribCheck(key, value);
|
||||
});
|
||||
|
||||
return funcs.length === 0 ? [] : this.filter(
|
||||
funcs.reduce(combineFuncs),
|
||||
element, recurse, limit
|
||||
);
|
||||
};
|
||||
|
||||
exports.getElementById = function(id, element, recurse){
|
||||
if(!Array.isArray(element)) element = [element];
|
||||
return this.findOne(getAttribCheck("id", id), element, recurse !== false);
|
||||
};
|
||||
|
||||
exports.getElementsByTagName = function(name, element, recurse, limit){
|
||||
return this.filter(Checks.tag_name(name), element, recurse, limit);
|
||||
};
|
||||
|
||||
exports.getElementsByTagType = function(type, element, recurse, limit){
|
||||
return this.filter(Checks.tag_type(type), element, recurse, limit);
|
||||
};
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
exports.removeElement = function(elem){
|
||||
if(elem.prev) elem.prev.next = elem.next;
|
||||
if(elem.next) elem.next.prev = elem.prev;
|
||||
|
||||
if(elem.parent){
|
||||
var childs = elem.parent.children;
|
||||
childs.splice(childs.lastIndexOf(elem), 1);
|
||||
}
|
||||
};
|
||||
|
||||
exports.replaceElement = function(elem, replacement){
|
||||
var prev = replacement.prev = elem.prev;
|
||||
if(prev){
|
||||
prev.next = replacement;
|
||||
}
|
||||
|
||||
var next = replacement.next = elem.next;
|
||||
if(next){
|
||||
next.prev = replacement;
|
||||
}
|
||||
|
||||
var parent = replacement.parent = elem.parent;
|
||||
if(parent){
|
||||
var childs = parent.children;
|
||||
childs[childs.lastIndexOf(elem)] = replacement;
|
||||
}
|
||||
};
|
||||
|
||||
exports.appendChild = function(elem, child){
|
||||
child.parent = elem;
|
||||
|
||||
if(elem.children.push(child) !== 1){
|
||||
var sibling = elem.children[elem.children.length - 2];
|
||||
sibling.next = child;
|
||||
child.prev = sibling;
|
||||
child.next = null;
|
||||
}
|
||||
};
|
||||
|
||||
exports.append = function(elem, next){
|
||||
var parent = elem.parent,
|
||||
currNext = elem.next;
|
||||
|
||||
next.next = currNext;
|
||||
next.prev = elem;
|
||||
elem.next = next;
|
||||
next.parent = parent;
|
||||
|
||||
if(currNext){
|
||||
currNext.prev = next;
|
||||
if(parent){
|
||||
var childs = parent.children;
|
||||
childs.splice(childs.lastIndexOf(currNext), 0, next);
|
||||
}
|
||||
} else if(parent){
|
||||
parent.children.push(next);
|
||||
}
|
||||
};
|
||||
|
||||
exports.prepend = function(elem, prev){
|
||||
var parent = elem.parent;
|
||||
if(parent){
|
||||
var childs = parent.children;
|
||||
childs.splice(childs.lastIndexOf(elem), 0, prev);
|
||||
}
|
||||
|
||||
if(elem.prev){
|
||||
elem.prev.next = prev;
|
||||
}
|
||||
|
||||
prev.parent = parent;
|
||||
prev.prev = elem.prev;
|
||||
prev.next = elem;
|
||||
elem.prev = prev;
|
||||
};
|
||||
|
||||
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
var isTag = require("domelementtype").isTag;
|
||||
|
||||
module.exports = {
|
||||
filter: filter,
|
||||
find: find,
|
||||
findOneChild: findOneChild,
|
||||
findOne: findOne,
|
||||
existsOne: existsOne,
|
||||
findAll: findAll
|
||||
};
|
||||
|
||||
function filter(test, element, recurse, limit){
|
||||
if(!Array.isArray(element)) element = [element];
|
||||
|
||||
if(typeof limit !== "number" || !isFinite(limit)){
|
||||
limit = Infinity;
|
||||
}
|
||||
return find(test, element, recurse !== false, limit);
|
||||
}
|
||||
|
||||
function find(test, elems, recurse, limit){
|
||||
var result = [], childs;
|
||||
|
||||
for(var i = 0, j = elems.length; i < j; i++){
|
||||
if(test(elems[i])){
|
||||
result.push(elems[i]);
|
||||
if(--limit <= 0) break;
|
||||
}
|
||||
|
||||
childs = elems[i].children;
|
||||
if(recurse && childs && childs.length > 0){
|
||||
childs = find(test, childs, recurse, limit);
|
||||
result = result.concat(childs);
|
||||
limit -= childs.length;
|
||||
if(limit <= 0) break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function findOneChild(test, elems){
|
||||
for(var i = 0, l = elems.length; i < l; i++){
|
||||
if(test(elems[i])) return elems[i];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function findOne(test, elems){
|
||||
var elem = null;
|
||||
|
||||
for(var i = 0, l = elems.length; i < l && !elem; i++){
|
||||
if(!isTag(elems[i])){
|
||||
continue;
|
||||
} else if(test(elems[i])){
|
||||
elem = elems[i];
|
||||
} else if(elems[i].children.length > 0){
|
||||
elem = findOne(test, elems[i].children);
|
||||
}
|
||||
}
|
||||
|
||||
return elem;
|
||||
}
|
||||
|
||||
function existsOne(test, elems){
|
||||
for(var i = 0, l = elems.length; i < l; i++){
|
||||
if(
|
||||
isTag(elems[i]) && (
|
||||
test(elems[i]) || (
|
||||
elems[i].children.length > 0 &&
|
||||
existsOne(test, elems[i].children)
|
||||
)
|
||||
)
|
||||
){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function findAll(test, rootElems){
|
||||
var result = [];
|
||||
var stack = rootElems.slice();
|
||||
while(stack.length){
|
||||
var elem = stack.shift();
|
||||
if(!isTag(elem)) continue;
|
||||
if (elem.children && elem.children.length > 0) {
|
||||
stack.unshift.apply(stack, elem.children);
|
||||
}
|
||||
if(test(elem)) result.push(elem);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
var ElementType = require("domelementtype"),
|
||||
getOuterHTML = require("dom-serializer"),
|
||||
isTag = ElementType.isTag;
|
||||
|
||||
module.exports = {
|
||||
getInnerHTML: getInnerHTML,
|
||||
getOuterHTML: getOuterHTML,
|
||||
getText: getText
|
||||
};
|
||||
|
||||
function getInnerHTML(elem, opts){
|
||||
return elem.children ? elem.children.map(function(elem){
|
||||
return getOuterHTML(elem, opts);
|
||||
}).join("") : "";
|
||||
}
|
||||
|
||||
function getText(elem){
|
||||
if(Array.isArray(elem)) return elem.map(getText).join("");
|
||||
if(isTag(elem)) return elem.name === "br" ? "\n" : getText(elem.children);
|
||||
if(elem.type === ElementType.CDATA) return getText(elem.children);
|
||||
if(elem.type === ElementType.Text) return elem.data;
|
||||
return "";
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
var getChildren = exports.getChildren = function(elem){
|
||||
return elem.children;
|
||||
};
|
||||
|
||||
var getParent = exports.getParent = function(elem){
|
||||
return elem.parent;
|
||||
};
|
||||
|
||||
exports.getSiblings = function(elem){
|
||||
var parent = getParent(elem);
|
||||
return parent ? getChildren(parent) : [elem];
|
||||
};
|
||||
|
||||
exports.getAttributeValue = function(elem, name){
|
||||
return elem.attribs && elem.attribs[name];
|
||||
};
|
||||
|
||||
exports.hasAttrib = function(elem, name){
|
||||
return !!elem.attribs && hasOwnProperty.call(elem.attribs, name);
|
||||
};
|
||||
|
||||
exports.getName = function(elem){
|
||||
return elem.name;
|
||||
};
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"_from": "domutils@^1.7.0",
|
||||
"_id": "domutils@1.7.0",
|
||||
"_inBundle": false,
|
||||
"_integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==",
|
||||
"_location": "/svgo/domutils",
|
||||
"_phantomChildren": {},
|
||||
"_requested": {
|
||||
"type": "range",
|
||||
"registry": true,
|
||||
"raw": "domutils@^1.7.0",
|
||||
"name": "domutils",
|
||||
"escapedName": "domutils",
|
||||
"rawSpec": "^1.7.0",
|
||||
"saveSpec": null,
|
||||
"fetchSpec": "^1.7.0"
|
||||
},
|
||||
"_requiredBy": [
|
||||
"/svgo/css-select"
|
||||
],
|
||||
"_resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
|
||||
"_shasum": "56ea341e834e06e6748af7a1cb25da67ea9f8c2a",
|
||||
"_spec": "domutils@^1.7.0",
|
||||
"_where": "O:\\zero\\zero.Web\\node_modules\\svgo\\node_modules\\css-select",
|
||||
"author": {
|
||||
"name": "Felix Boehm",
|
||||
"email": "me@feedic.com"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/FB55/domutils/issues"
|
||||
},
|
||||
"bundleDependencies": false,
|
||||
"dependencies": {
|
||||
"dom-serializer": "0",
|
||||
"domelementtype": "1"
|
||||
},
|
||||
"deprecated": false,
|
||||
"description": "utilities for working with htmlparser2's dom",
|
||||
"devDependencies": {
|
||||
"domhandler": "2",
|
||||
"htmlparser2": "~3.9.2",
|
||||
"jshint": "~2.9.4",
|
||||
"mocha": "~3.2.0"
|
||||
},
|
||||
"directories": {
|
||||
"test": "tests"
|
||||
},
|
||||
"homepage": "https://github.com/FB55/domutils#readme",
|
||||
"jshintConfig": {
|
||||
"proto": true,
|
||||
"unused": true,
|
||||
"eqnull": true,
|
||||
"undef": true,
|
||||
"quotmark": "double",
|
||||
"eqeqeq": true,
|
||||
"trailing": true,
|
||||
"node": true,
|
||||
"globals": {
|
||||
"describe": true,
|
||||
"it": true,
|
||||
"beforeEach": true
|
||||
}
|
||||
},
|
||||
"keywords": [
|
||||
"dom",
|
||||
"htmlparser2"
|
||||
],
|
||||
"license": "BSD-2-Clause",
|
||||
"main": "index.js",
|
||||
"name": "domutils",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/FB55/domutils.git"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "mocha test/tests/**.js && jshint index.js test/**/*.js lib/*.js"
|
||||
},
|
||||
"version": "1.7.0"
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Utilities for working with htmlparser2's dom
|
||||
|
||||
[](https://travis-ci.org/fb55/domutils)
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
var makeDom = require("./utils").makeDom;
|
||||
var markup = Array(21).join(
|
||||
"<?xml><tag1 id='asdf'> <script>text</script> <!-- comment --> <tag2> text </tag1>"
|
||||
);
|
||||
|
||||
module.exports = makeDom(markup);
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
var makeDom = require("../utils").makeDom;
|
||||
var helpers = require("../..");
|
||||
var assert = require("assert");
|
||||
|
||||
describe("helpers", function() {
|
||||
describe("removeSubsets", function() {
|
||||
var removeSubsets = helpers.removeSubsets;
|
||||
var dom = makeDom("<div><p><span></span></p><p></p></div>")[0];
|
||||
|
||||
it("removes identical trees", function() {
|
||||
var matches = removeSubsets([dom, dom]);
|
||||
assert.equal(matches.length, 1);
|
||||
});
|
||||
|
||||
it("Removes subsets found first", function() {
|
||||
var matches = removeSubsets([dom, dom.children[0].children[0]]);
|
||||
assert.equal(matches.length, 1);
|
||||
});
|
||||
|
||||
it("Removes subsets found last", function() {
|
||||
var matches = removeSubsets([dom.children[0], dom]);
|
||||
assert.equal(matches.length, 1);
|
||||
});
|
||||
|
||||
it("Does not remove unique trees", function() {
|
||||
var matches = removeSubsets([dom.children[0], dom.children[1]]);
|
||||
assert.equal(matches.length, 2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("compareDocumentPosition", function() {
|
||||
var compareDocumentPosition = helpers.compareDocumentPosition;
|
||||
var markup = "<div><p><span></span></p><a></a></div>";
|
||||
var dom = makeDom(markup)[0];
|
||||
var p = dom.children[0];
|
||||
var span = p.children[0];
|
||||
var a = dom.children[1];
|
||||
|
||||
it("reports when the first node occurs before the second indirectly", function() {
|
||||
assert.equal(compareDocumentPosition(span, a), 2);
|
||||
});
|
||||
|
||||
it("reports when the first node contains the second", function() {
|
||||
assert.equal(compareDocumentPosition(p, span), 10);
|
||||
});
|
||||
|
||||
it("reports when the first node occurs after the second indirectly", function() {
|
||||
assert.equal(compareDocumentPosition(a, span), 4);
|
||||
});
|
||||
|
||||
it("reports when the first node is contained by the second", function() {
|
||||
assert.equal(compareDocumentPosition(span, p), 20);
|
||||
});
|
||||
|
||||
it("reports when the nodes belong to separate documents", function() {
|
||||
var other = makeDom(markup)[0].children[0].children[0];
|
||||
|
||||
assert.equal(compareDocumentPosition(span, other), 1);
|
||||
});
|
||||
|
||||
it("reports when the nodes are identical", function() {
|
||||
assert.equal(compareDocumentPosition(span, span), 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("uniqueSort", function() {
|
||||
var uniqueSort = helpers.uniqueSort;
|
||||
var dom, p, span, a;
|
||||
|
||||
beforeEach(function() {
|
||||
dom = makeDom("<div><p><span></span></p><a></a></div>")[0];
|
||||
p = dom.children[0];
|
||||
span = p.children[0];
|
||||
a = dom.children[1];
|
||||
});
|
||||
|
||||
it("leaves unique elements untouched", function() {
|
||||
assert.deepEqual(uniqueSort([p, a]), [p, a]);
|
||||
});
|
||||
|
||||
it("removes duplicate elements", function() {
|
||||
assert.deepEqual(uniqueSort([p, a, p]), [p, a]);
|
||||
});
|
||||
|
||||
it("sorts nodes in document order", function() {
|
||||
assert.deepEqual(uniqueSort([a, dom, span, p]), [dom, p, span, a]);
|
||||
});
|
||||
});
|
||||
});
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
var DomUtils = require("../..");
|
||||
var fixture = require("../fixture");
|
||||
var assert = require("assert");
|
||||
|
||||
// Set up expected structures
|
||||
var expected = {
|
||||
idAsdf: fixture[1],
|
||||
tag2: [],
|
||||
typeScript: []
|
||||
};
|
||||
for (var idx = 0; idx < 20; ++idx) {
|
||||
expected.tag2.push(fixture[idx*2 + 1].children[5]);
|
||||
expected.typeScript.push(fixture[idx*2 + 1].children[1]);
|
||||
}
|
||||
|
||||
describe("legacy", function() {
|
||||
describe("getElements", function() {
|
||||
var getElements = DomUtils.getElements;
|
||||
it("returns the node with the specified ID", function() {
|
||||
assert.deepEqual(
|
||||
getElements({ id: "asdf" }, fixture, true, 1),
|
||||
[expected.idAsdf]
|
||||
);
|
||||
});
|
||||
it("returns empty array for unknown IDs", function() {
|
||||
assert.deepEqual(getElements({ id: "asdfs" }, fixture, true), []);
|
||||
});
|
||||
it("returns the nodes with the specified tag name", function() {
|
||||
assert.deepEqual(
|
||||
getElements({ tag_name:"tag2" }, fixture, true),
|
||||
expected.tag2
|
||||
);
|
||||
});
|
||||
it("returns empty array for unknown tag names", function() {
|
||||
assert.deepEqual(
|
||||
getElements({ tag_name : "asdfs" }, fixture, true),
|
||||
[]
|
||||
);
|
||||
});
|
||||
it("returns the nodes with the specified tag type", function() {
|
||||
assert.deepEqual(
|
||||
getElements({ tag_type: "script" }, fixture, true),
|
||||
expected.typeScript
|
||||
);
|
||||
});
|
||||
it("returns empty array for unknown tag types", function() {
|
||||
assert.deepEqual(
|
||||
getElements({ tag_type: "video" }, fixture, true),
|
||||
[]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getElementById", function() {
|
||||
var getElementById = DomUtils.getElementById;
|
||||
it("returns the specified node", function() {
|
||||
assert.equal(
|
||||
expected.idAsdf,
|
||||
getElementById("asdf", fixture, true)
|
||||
);
|
||||
});
|
||||
it("returns `null` for unknown IDs", function() {
|
||||
assert.equal(null, getElementById("asdfs", fixture, true));
|
||||
});
|
||||
});
|
||||
|
||||
describe("getElementsByTagName", function() {
|
||||
var getElementsByTagName = DomUtils.getElementsByTagName;
|
||||
it("returns the specified nodes", function() {
|
||||
assert.deepEqual(
|
||||
getElementsByTagName("tag2", fixture, true),
|
||||
expected.tag2
|
||||
);
|
||||
});
|
||||
it("returns empty array for unknown tag names", function() {
|
||||
assert.deepEqual(
|
||||
getElementsByTagName("tag23", fixture, true),
|
||||
[]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getElementsByTagType", function() {
|
||||
var getElementsByTagType = DomUtils.getElementsByTagType;
|
||||
it("returns the specified nodes", function() {
|
||||
assert.deepEqual(
|
||||
getElementsByTagType("script", fixture, true),
|
||||
expected.typeScript
|
||||
);
|
||||
});
|
||||
it("returns empty array for unknown tag types", function() {
|
||||
assert.deepEqual(
|
||||
getElementsByTagType("video", fixture, true),
|
||||
[]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getOuterHTML", function() {
|
||||
var getOuterHTML = DomUtils.getOuterHTML;
|
||||
it("Correctly renders the outer HTML", function() {
|
||||
assert.equal(
|
||||
getOuterHTML(fixture[1]),
|
||||
"<tag1 id=\"asdf\"> <script>text</script> <!-- comment --> <tag2> text </tag2></tag1>"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getInnerHTML", function() {
|
||||
var getInnerHTML = DomUtils.getInnerHTML;
|
||||
it("Correctly renders the inner HTML", function() {
|
||||
assert.equal(
|
||||
getInnerHTML(fixture[1]),
|
||||
" <script>text</script> <!-- comment --> <tag2> text </tag2>"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
var makeDom = require("../utils").makeDom;
|
||||
var traversal = require("../..");
|
||||
var assert = require("assert");
|
||||
|
||||
describe("traversal", function() {
|
||||
describe("hasAttrib", function() {
|
||||
var hasAttrib = traversal.hasAttrib;
|
||||
|
||||
it("doesn't throw on text nodes", function() {
|
||||
var dom = makeDom("textnode");
|
||||
assert.doesNotThrow(function() {
|
||||
hasAttrib(dom[0], "some-attrib");
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
var htmlparser = require("htmlparser2");
|
||||
|
||||
exports.makeDom = function(markup) {
|
||||
var handler = new htmlparser.DomHandler(),
|
||||
parser = new htmlparser.Parser(handler);
|
||||
parser.write(markup);
|
||||
parser.done();
|
||||
return handler.dom;
|
||||
};
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
{
|
||||
"_from": "svgo@^1.0.0",
|
||||
"_id": "svgo@1.3.2",
|
||||
"_inBundle": false,
|
||||
"_integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==",
|
||||
"_location": "/svgo",
|
||||
"_phantomChildren": {
|
||||
"boolbase": "1.0.0",
|
||||
"dom-serializer": "0.2.2",
|
||||
"domelementtype": "1.3.1",
|
||||
"nth-check": "1.0.2"
|
||||
},
|
||||
"_requested": {
|
||||
"type": "range",
|
||||
"registry": true,
|
||||
"raw": "svgo@^1.0.0",
|
||||
"name": "svgo",
|
||||
"escapedName": "svgo",
|
||||
"rawSpec": "^1.0.0",
|
||||
"saveSpec": null,
|
||||
"fetchSpec": "^1.0.0"
|
||||
},
|
||||
"_requiredBy": [
|
||||
"/postcss-svgo"
|
||||
],
|
||||
"_resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz",
|
||||
"_shasum": "b6dc511c063346c9e415b81e43401145b96d4167",
|
||||
"_spec": "svgo@^1.0.0",
|
||||
"_where": "O:\\zero\\zero.Web\\node_modules\\postcss-svgo",
|
||||
"author": {
|
||||
"name": "Kir Belevich",
|
||||
"email": "kir@belevi.ch",
|
||||
"url": "https://github.com/deepsweet"
|
||||
},
|
||||
"bin": {
|
||||
"svgo": "./bin/svgo"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/svg/svgo/issues"
|
||||
},
|
||||
"bundleDependencies": false,
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Sergey Belov",
|
||||
"email": "peimei@ya.ru",
|
||||
"url": "http://github.com/arikon"
|
||||
},
|
||||
{
|
||||
"name": "Lev Solntsev",
|
||||
"email": "lev.sun@ya.ru",
|
||||
"url": "http://github.com/GreLI"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"chalk": "^2.4.1",
|
||||
"coa": "^2.0.2",
|
||||
"css-select": "^2.0.0",
|
||||
"css-select-base-adapter": "^0.1.1",
|
||||
"css-tree": "1.0.0-alpha.37",
|
||||
"csso": "^4.0.2",
|
||||
"js-yaml": "^3.13.1",
|
||||
"mkdirp": "~0.5.1",
|
||||
"object.values": "^1.1.0",
|
||||
"sax": "~1.2.4",
|
||||
"stable": "^0.1.8",
|
||||
"unquote": "~1.1.1",
|
||||
"util.promisify": "~1.0.0"
|
||||
},
|
||||
"deprecated": false,
|
||||
"description": "Nodejs-based tool for optimizing SVG vector graphics files",
|
||||
"devDependencies": {
|
||||
"coveralls": "^3.0.7",
|
||||
"fs-extra": "~8.1.0",
|
||||
"istanbul": "~0.4.5",
|
||||
"jshint": "~2.10.2",
|
||||
"mocha": "~6.2.2",
|
||||
"mocha-istanbul": "~0.3.0",
|
||||
"mock-stdin": "~0.3.1",
|
||||
"should": "~13.2.3"
|
||||
},
|
||||
"directories": {
|
||||
"bin": "./bin",
|
||||
"lib": "./lib",
|
||||
"example": "./examples"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
},
|
||||
"homepage": "https://github.com/svg/svgo",
|
||||
"keywords": [
|
||||
"svgo",
|
||||
"svg",
|
||||
"optimize",
|
||||
"minify"
|
||||
],
|
||||
"license": "MIT",
|
||||
"main": "./lib/svgo.js",
|
||||
"name": "svgo",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/svg/svgo.git"
|
||||
},
|
||||
"scripts": {
|
||||
"jshint": "npm run lint",
|
||||
"lint": "jshint --show-non-errors .",
|
||||
"test": "set NODE_ENV=test && mocha"
|
||||
},
|
||||
"version": "1.3.2"
|
||||
}
|
||||
+2558
File diff suppressed because it is too large
Load Diff
+988
@@ -0,0 +1,988 @@
|
||||
/* global a2c */
|
||||
'use strict';
|
||||
|
||||
var rNumber = String.raw`[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?\s*`,
|
||||
rCommaWsp = String.raw`(?:\s,?\s*|,\s*)`,
|
||||
rNumberCommaWsp = `(${rNumber})` + rCommaWsp,
|
||||
rFlagCommaWsp = `([01])${rCommaWsp}?`,
|
||||
rCoordinatePair = String.raw`(${rNumber})${rCommaWsp}?(${rNumber})`,
|
||||
rArcSeq = (rNumberCommaWsp + '?').repeat(2) + rNumberCommaWsp + rFlagCommaWsp.repeat(2) + rCoordinatePair;
|
||||
|
||||
var regPathInstructions = /([MmLlHhVvCcSsQqTtAaZz])\s*/,
|
||||
regCoordinateSequence = new RegExp(rNumber, 'g'),
|
||||
regArcArgumentSequence = new RegExp(rArcSeq, 'g'),
|
||||
regNumericValues = /[-+]?(\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/,
|
||||
transform2js = require('./_transforms').transform2js,
|
||||
transformsMultiply = require('./_transforms').transformsMultiply,
|
||||
transformArc = require('./_transforms').transformArc,
|
||||
collections = require('./_collections.js'),
|
||||
referencesProps = collections.referencesProps,
|
||||
defaultStrokeWidth = collections.attrsGroupsDefaults.presentation['stroke-width'],
|
||||
cleanupOutData = require('../lib/svgo/tools').cleanupOutData,
|
||||
removeLeadingZero = require('../lib/svgo/tools').removeLeadingZero,
|
||||
prevCtrlPoint;
|
||||
|
||||
/**
|
||||
* Convert path string to JS representation.
|
||||
*
|
||||
* @param {String} pathString input string
|
||||
* @param {Object} params plugin params
|
||||
* @return {Array} output array
|
||||
*/
|
||||
exports.path2js = function(path) {
|
||||
if (path.pathJS) return path.pathJS;
|
||||
|
||||
var paramsLength = { // Number of parameters of every path command
|
||||
H: 1, V: 1, M: 2, L: 2, T: 2, Q: 4, S: 4, C: 6, A: 7,
|
||||
h: 1, v: 1, m: 2, l: 2, t: 2, q: 4, s: 4, c: 6, a: 7
|
||||
},
|
||||
pathData = [], // JS representation of the path data
|
||||
instruction, // current instruction context
|
||||
startMoveto = false;
|
||||
|
||||
// splitting path string into array like ['M', '10 50', 'L', '20 30']
|
||||
path.attr('d').value.split(regPathInstructions).forEach(function(data) {
|
||||
if (!data) return;
|
||||
if (!startMoveto) {
|
||||
if (data == 'M' || data == 'm') {
|
||||
startMoveto = true;
|
||||
} else return;
|
||||
}
|
||||
|
||||
// instruction item
|
||||
if (regPathInstructions.test(data)) {
|
||||
instruction = data;
|
||||
|
||||
// z - instruction w/o data
|
||||
if (instruction == 'Z' || instruction == 'z') {
|
||||
pathData.push({
|
||||
instruction: 'z'
|
||||
});
|
||||
}
|
||||
// data item
|
||||
} else {
|
||||
/* jshint boss: true */
|
||||
if (instruction == 'A' || instruction == 'a') {
|
||||
var newData = [];
|
||||
for (var args; (args = regArcArgumentSequence.exec(data));) {
|
||||
for (var i = 1; i < args.length; i++) {
|
||||
newData.push(args[i]);
|
||||
}
|
||||
}
|
||||
data = newData;
|
||||
} else {
|
||||
data = data.match(regCoordinateSequence);
|
||||
}
|
||||
if (!data) return;
|
||||
|
||||
data = data.map(Number);
|
||||
// Subsequent moveto pairs of coordinates are threated as implicit lineto commands
|
||||
// http://www.w3.org/TR/SVG/paths.html#PathDataMovetoCommands
|
||||
if (instruction == 'M' || instruction == 'm') {
|
||||
pathData.push({
|
||||
instruction: pathData.length == 0 ? 'M' : instruction,
|
||||
data: data.splice(0, 2)
|
||||
});
|
||||
instruction = instruction == 'M' ? 'L' : 'l';
|
||||
}
|
||||
|
||||
for (var pair = paramsLength[instruction]; data.length;) {
|
||||
pathData.push({
|
||||
instruction: instruction,
|
||||
data: data.splice(0, pair)
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// First moveto is actually absolute. Subsequent coordinates were separated above.
|
||||
if (pathData.length && pathData[0].instruction == 'm') {
|
||||
pathData[0].instruction = 'M';
|
||||
}
|
||||
path.pathJS = pathData;
|
||||
|
||||
return pathData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert relative Path data to absolute.
|
||||
*
|
||||
* @param {Array} data input data
|
||||
* @return {Array} output data
|
||||
*/
|
||||
var relative2absolute = exports.relative2absolute = function(data) {
|
||||
var currentPoint = [0, 0],
|
||||
subpathPoint = [0, 0],
|
||||
i;
|
||||
|
||||
return data.map(function(item) {
|
||||
|
||||
var instruction = item.instruction,
|
||||
itemData = item.data && item.data.slice();
|
||||
|
||||
if (instruction == 'M') {
|
||||
|
||||
set(currentPoint, itemData);
|
||||
set(subpathPoint, itemData);
|
||||
|
||||
} else if ('mlcsqt'.indexOf(instruction) > -1) {
|
||||
|
||||
for (i = 0; i < itemData.length; i++) {
|
||||
itemData[i] += currentPoint[i % 2];
|
||||
}
|
||||
set(currentPoint, itemData);
|
||||
|
||||
if (instruction == 'm') {
|
||||
set(subpathPoint, itemData);
|
||||
}
|
||||
|
||||
} else if (instruction == 'a') {
|
||||
|
||||
itemData[5] += currentPoint[0];
|
||||
itemData[6] += currentPoint[1];
|
||||
set(currentPoint, itemData);
|
||||
|
||||
} else if (instruction == 'h') {
|
||||
|
||||
itemData[0] += currentPoint[0];
|
||||
currentPoint[0] = itemData[0];
|
||||
|
||||
} else if (instruction == 'v') {
|
||||
|
||||
itemData[0] += currentPoint[1];
|
||||
currentPoint[1] = itemData[0];
|
||||
|
||||
} else if ('MZLCSQTA'.indexOf(instruction) > -1) {
|
||||
|
||||
set(currentPoint, itemData);
|
||||
|
||||
} else if (instruction == 'H') {
|
||||
|
||||
currentPoint[0] = itemData[0];
|
||||
|
||||
} else if (instruction == 'V') {
|
||||
|
||||
currentPoint[1] = itemData[0];
|
||||
|
||||
} else if (instruction == 'z') {
|
||||
|
||||
set(currentPoint, subpathPoint);
|
||||
|
||||
}
|
||||
|
||||
return instruction == 'z' ?
|
||||
{ instruction: 'z' } :
|
||||
{
|
||||
instruction: instruction.toUpperCase(),
|
||||
data: itemData
|
||||
};
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply transformation(s) to the Path data.
|
||||
*
|
||||
* @param {Object} elem current element
|
||||
* @param {Array} path input path data
|
||||
* @param {Object} params whether to apply transforms to stroked lines and transform precision (used for stroke width)
|
||||
* @return {Array} output path data
|
||||
*/
|
||||
exports.applyTransforms = function(elem, path, params) {
|
||||
// if there are no 'stroke' attr and references to other objects such as
|
||||
// gradiends or clip-path which are also subjects to transform.
|
||||
if (!elem.hasAttr('transform') || !elem.attr('transform').value ||
|
||||
elem.someAttr(function(attr) {
|
||||
return ~referencesProps.indexOf(attr.name) && ~attr.value.indexOf('url(');
|
||||
}))
|
||||
return path;
|
||||
|
||||
var matrix = transformsMultiply(transform2js(elem.attr('transform').value)),
|
||||
stroke = elem.computedAttr('stroke'),
|
||||
id = elem.computedAttr('id'),
|
||||
transformPrecision = params.transformPrecision,
|
||||
newPoint, scale;
|
||||
|
||||
if (stroke && stroke != 'none') {
|
||||
if (!params.applyTransformsStroked ||
|
||||
(matrix.data[0] != matrix.data[3] || matrix.data[1] != -matrix.data[2]) &&
|
||||
(matrix.data[0] != -matrix.data[3] || matrix.data[1] != matrix.data[2]))
|
||||
return path;
|
||||
|
||||
// "stroke-width" should be inside the part with ID, otherwise it can be overrided in <use>
|
||||
if (id) {
|
||||
var idElem = elem,
|
||||
hasStrokeWidth = false;
|
||||
|
||||
do {
|
||||
if (idElem.hasAttr('stroke-width')) hasStrokeWidth = true;
|
||||
} while (!idElem.hasAttr('id', id) && !hasStrokeWidth && (idElem = idElem.parentNode));
|
||||
|
||||
if (!hasStrokeWidth) return path;
|
||||
}
|
||||
|
||||
scale = +Math.sqrt(matrix.data[0] * matrix.data[0] + matrix.data[1] * matrix.data[1]).toFixed(transformPrecision);
|
||||
|
||||
if (scale !== 1) {
|
||||
var strokeWidth = elem.computedAttr('stroke-width') || defaultStrokeWidth;
|
||||
|
||||
if (!elem.hasAttr('vector-effect') || elem.attr('vector-effect').value !== 'non-scaling-stroke') {
|
||||
if (elem.hasAttr('stroke-width')) {
|
||||
elem.attrs['stroke-width'].value = elem.attrs['stroke-width'].value.trim()
|
||||
.replace(regNumericValues, function(num) {
|
||||
return removeLeadingZero(num * scale);
|
||||
});
|
||||
} else {
|
||||
elem.addAttr({
|
||||
name: 'stroke-width',
|
||||
prefix: '',
|
||||
local: 'stroke-width',
|
||||
value: strokeWidth.replace(regNumericValues, function(num) {
|
||||
return removeLeadingZero(num * scale);
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (id) { // Stroke and stroke-width can be redefined with <use>
|
||||
return path;
|
||||
}
|
||||
|
||||
path.forEach(function(pathItem) {
|
||||
|
||||
if (pathItem.data) {
|
||||
|
||||
// h -> l
|
||||
if (pathItem.instruction === 'h') {
|
||||
|
||||
pathItem.instruction = 'l';
|
||||
pathItem.data[1] = 0;
|
||||
|
||||
// v -> l
|
||||
} else if (pathItem.instruction === 'v') {
|
||||
|
||||
pathItem.instruction = 'l';
|
||||
pathItem.data[1] = pathItem.data[0];
|
||||
pathItem.data[0] = 0;
|
||||
|
||||
}
|
||||
|
||||
// if there is a translate() transform
|
||||
if (pathItem.instruction === 'M' &&
|
||||
(matrix.data[4] !== 0 ||
|
||||
matrix.data[5] !== 0)
|
||||
) {
|
||||
|
||||
// then apply it only to the first absoluted M
|
||||
newPoint = transformPoint(matrix.data, pathItem.data[0], pathItem.data[1]);
|
||||
set(pathItem.data, newPoint);
|
||||
set(pathItem.coords, newPoint);
|
||||
|
||||
// clear translate() data from transform matrix
|
||||
matrix.data[4] = 0;
|
||||
matrix.data[5] = 0;
|
||||
|
||||
} else {
|
||||
|
||||
if (pathItem.instruction == 'a') {
|
||||
|
||||
transformArc(pathItem.data, matrix.data);
|
||||
|
||||
// reduce number of digits in rotation angle
|
||||
if (Math.abs(pathItem.data[2]) > 80) {
|
||||
var a = pathItem.data[0],
|
||||
rotation = pathItem.data[2];
|
||||
pathItem.data[0] = pathItem.data[1];
|
||||
pathItem.data[1] = a;
|
||||
pathItem.data[2] = rotation + (rotation > 0 ? -90 : 90);
|
||||
}
|
||||
|
||||
newPoint = transformPoint(matrix.data, pathItem.data[5], pathItem.data[6]);
|
||||
pathItem.data[5] = newPoint[0];
|
||||
pathItem.data[6] = newPoint[1];
|
||||
|
||||
} else {
|
||||
|
||||
for (var i = 0; i < pathItem.data.length; i += 2) {
|
||||
newPoint = transformPoint(matrix.data, pathItem.data[i], pathItem.data[i + 1]);
|
||||
pathItem.data[i] = newPoint[0];
|
||||
pathItem.data[i + 1] = newPoint[1];
|
||||
}
|
||||
}
|
||||
|
||||
pathItem.coords[0] = pathItem.base[0] + pathItem.data[pathItem.data.length - 2];
|
||||
pathItem.coords[1] = pathItem.base[1] + pathItem.data[pathItem.data.length - 1];
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// remove transform attr
|
||||
elem.removeAttr('transform');
|
||||
|
||||
return path;
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply transform 3x3 matrix to x-y point.
|
||||
*
|
||||
* @param {Array} matrix transform 3x3 matrix
|
||||
* @param {Array} point x-y point
|
||||
* @return {Array} point with new coordinates
|
||||
*/
|
||||
function transformPoint(matrix, x, y) {
|
||||
|
||||
return [
|
||||
matrix[0] * x + matrix[2] * y + matrix[4],
|
||||
matrix[1] * x + matrix[3] * y + matrix[5]
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute Cubic Bézie bounding box.
|
||||
*
|
||||
* @see http://processingjs.nihongoresources.com/bezierinfo/
|
||||
*
|
||||
* @param {Float} xa
|
||||
* @param {Float} ya
|
||||
* @param {Float} xb
|
||||
* @param {Float} yb
|
||||
* @param {Float} xc
|
||||
* @param {Float} yc
|
||||
* @param {Float} xd
|
||||
* @param {Float} yd
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
exports.computeCubicBoundingBox = function(xa, ya, xb, yb, xc, yc, xd, yd) {
|
||||
|
||||
var minx = Number.POSITIVE_INFINITY,
|
||||
miny = Number.POSITIVE_INFINITY,
|
||||
maxx = Number.NEGATIVE_INFINITY,
|
||||
maxy = Number.NEGATIVE_INFINITY,
|
||||
ts,
|
||||
t,
|
||||
x,
|
||||
y,
|
||||
i;
|
||||
|
||||
// X
|
||||
if (xa < minx) { minx = xa; }
|
||||
if (xa > maxx) { maxx = xa; }
|
||||
if (xd < minx) { minx= xd; }
|
||||
if (xd > maxx) { maxx = xd; }
|
||||
|
||||
ts = computeCubicFirstDerivativeRoots(xa, xb, xc, xd);
|
||||
|
||||
for (i = 0; i < ts.length; i++) {
|
||||
|
||||
t = ts[i];
|
||||
|
||||
if (t >= 0 && t <= 1) {
|
||||
x = computeCubicBaseValue(t, xa, xb, xc, xd);
|
||||
// y = computeCubicBaseValue(t, ya, yb, yc, yd);
|
||||
|
||||
if (x < minx) { minx = x; }
|
||||
if (x > maxx) { maxx = x; }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Y
|
||||
if (ya < miny) { miny = ya; }
|
||||
if (ya > maxy) { maxy = ya; }
|
||||
if (yd < miny) { miny = yd; }
|
||||
if (yd > maxy) { maxy = yd; }
|
||||
|
||||
ts = computeCubicFirstDerivativeRoots(ya, yb, yc, yd);
|
||||
|
||||
for (i = 0; i < ts.length; i++) {
|
||||
|
||||
t = ts[i];
|
||||
|
||||
if (t >= 0 && t <= 1) {
|
||||
// x = computeCubicBaseValue(t, xa, xb, xc, xd);
|
||||
y = computeCubicBaseValue(t, ya, yb, yc, yd);
|
||||
|
||||
if (y < miny) { miny = y; }
|
||||
if (y > maxy) { maxy = y; }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
minx: minx,
|
||||
miny: miny,
|
||||
maxx: maxx,
|
||||
maxy: maxy
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
// compute the value for the cubic bezier function at time=t
|
||||
function computeCubicBaseValue(t, a, b, c, d) {
|
||||
|
||||
var mt = 1 - t;
|
||||
|
||||
return mt * mt * mt * a + 3 * mt * mt * t * b + 3 * mt * t * t * c + t * t * t * d;
|
||||
|
||||
}
|
||||
|
||||
// compute the value for the first derivative of the cubic bezier function at time=t
|
||||
function computeCubicFirstDerivativeRoots(a, b, c, d) {
|
||||
|
||||
var result = [-1, -1],
|
||||
tl = -a + 2 * b - c,
|
||||
tr = -Math.sqrt(-a * (c - d) + b * b - b * (c + d) + c * c),
|
||||
dn = -a + 3 * b - 3 * c + d;
|
||||
|
||||
if (dn !== 0) {
|
||||
result[0] = (tl + tr) / dn;
|
||||
result[1] = (tl - tr) / dn;
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute Quadratic Bézier bounding box.
|
||||
*
|
||||
* @see http://processingjs.nihongoresources.com/bezierinfo/
|
||||
*
|
||||
* @param {Float} xa
|
||||
* @param {Float} ya
|
||||
* @param {Float} xb
|
||||
* @param {Float} yb
|
||||
* @param {Float} xc
|
||||
* @param {Float} yc
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
exports.computeQuadraticBoundingBox = function(xa, ya, xb, yb, xc, yc) {
|
||||
|
||||
var minx = Number.POSITIVE_INFINITY,
|
||||
miny = Number.POSITIVE_INFINITY,
|
||||
maxx = Number.NEGATIVE_INFINITY,
|
||||
maxy = Number.NEGATIVE_INFINITY,
|
||||
t,
|
||||
x,
|
||||
y;
|
||||
|
||||
// X
|
||||
if (xa < minx) { minx = xa; }
|
||||
if (xa > maxx) { maxx = xa; }
|
||||
if (xc < minx) { minx = xc; }
|
||||
if (xc > maxx) { maxx = xc; }
|
||||
|
||||
t = computeQuadraticFirstDerivativeRoot(xa, xb, xc);
|
||||
|
||||
if (t >= 0 && t <= 1) {
|
||||
x = computeQuadraticBaseValue(t, xa, xb, xc);
|
||||
// y = computeQuadraticBaseValue(t, ya, yb, yc);
|
||||
|
||||
if (x < minx) { minx = x; }
|
||||
if (x > maxx) { maxx = x; }
|
||||
}
|
||||
|
||||
// Y
|
||||
if (ya < miny) { miny = ya; }
|
||||
if (ya > maxy) { maxy = ya; }
|
||||
if (yc < miny) { miny = yc; }
|
||||
if (yc > maxy) { maxy = yc; }
|
||||
|
||||
t = computeQuadraticFirstDerivativeRoot(ya, yb, yc);
|
||||
|
||||
if (t >= 0 && t <=1 ) {
|
||||
// x = computeQuadraticBaseValue(t, xa, xb, xc);
|
||||
y = computeQuadraticBaseValue(t, ya, yb, yc);
|
||||
|
||||
if (y < miny) { miny = y; }
|
||||
if (y > maxy) { maxy = y ; }
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
minx: minx,
|
||||
miny: miny,
|
||||
maxx: maxx,
|
||||
maxy: maxy
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
// compute the value for the quadratic bezier function at time=t
|
||||
function computeQuadraticBaseValue(t, a, b, c) {
|
||||
|
||||
var mt = 1 - t;
|
||||
|
||||
return mt * mt * a + 2 * mt * t * b + t * t * c;
|
||||
|
||||
}
|
||||
|
||||
// compute the value for the first derivative of the quadratic bezier function at time=t
|
||||
function computeQuadraticFirstDerivativeRoot(a, b, c) {
|
||||
|
||||
var t = -1,
|
||||
denominator = a - 2 * b + c;
|
||||
|
||||
if (denominator !== 0) {
|
||||
t = (a - b) / denominator;
|
||||
}
|
||||
|
||||
return t;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert path array to string.
|
||||
*
|
||||
* @param {Array} path input path data
|
||||
* @param {Object} params plugin params
|
||||
* @return {String} output path string
|
||||
*/
|
||||
exports.js2path = function(path, data, params) {
|
||||
|
||||
path.pathJS = data;
|
||||
|
||||
if (params.collapseRepeated) {
|
||||
data = collapseRepeated(data);
|
||||
}
|
||||
|
||||
path.attr('d').value = data.reduce(function(pathString, item) {
|
||||
var strData = '';
|
||||
if (item.data) {
|
||||
strData = cleanupOutData(item.data, params, item.instruction);
|
||||
}
|
||||
return pathString += item.instruction + strData;
|
||||
}, '');
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Collapse repeated instructions data
|
||||
*
|
||||
* @param {Array} path input path data
|
||||
* @return {Array} output path data
|
||||
*/
|
||||
function collapseRepeated(data) {
|
||||
|
||||
var prev,
|
||||
prevIndex;
|
||||
|
||||
// copy an array and modifieds item to keep original data untouched
|
||||
data = data.reduce(function(newPath, item) {
|
||||
if (
|
||||
prev && item.data &&
|
||||
item.instruction == prev.instruction
|
||||
) {
|
||||
// concat previous data with current
|
||||
if (item.instruction != 'M') {
|
||||
prev = newPath[prevIndex] = {
|
||||
instruction: prev.instruction,
|
||||
data: prev.data.concat(item.data),
|
||||
coords: item.coords,
|
||||
base: prev.base
|
||||
};
|
||||
} else {
|
||||
prev.data = item.data;
|
||||
prev.coords = item.coords;
|
||||
}
|
||||
} else {
|
||||
newPath.push(item);
|
||||
prev = item;
|
||||
prevIndex = newPath.length - 1;
|
||||
}
|
||||
|
||||
return newPath;
|
||||
}, []);
|
||||
|
||||
return data;
|
||||
|
||||
}
|
||||
|
||||
function set(dest, source) {
|
||||
dest[0] = source[source.length - 2];
|
||||
dest[1] = source[source.length - 1];
|
||||
return dest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if two paths have an intersection by checking convex hulls
|
||||
* collision using Gilbert-Johnson-Keerthi distance algorithm
|
||||
* http://entropyinteractive.com/2011/04/gjk-algorithm/
|
||||
*
|
||||
* @param {Array} path1 JS path representation
|
||||
* @param {Array} path2 JS path representation
|
||||
* @return {Boolean}
|
||||
*/
|
||||
exports.intersects = function(path1, path2) {
|
||||
if (path1.length < 3 || path2.length < 3) return false; // nothing to fill
|
||||
|
||||
// Collect points of every subpath.
|
||||
var points1 = relative2absolute(path1).reduce(gatherPoints, []),
|
||||
points2 = relative2absolute(path2).reduce(gatherPoints, []);
|
||||
|
||||
// Axis-aligned bounding box check.
|
||||
if (points1.maxX <= points2.minX || points2.maxX <= points1.minX ||
|
||||
points1.maxY <= points2.minY || points2.maxY <= points1.minY ||
|
||||
points1.every(function (set1) {
|
||||
return points2.every(function (set2) {
|
||||
return set1[set1.maxX][0] <= set2[set2.minX][0] ||
|
||||
set2[set2.maxX][0] <= set1[set1.minX][0] ||
|
||||
set1[set1.maxY][1] <= set2[set2.minY][1] ||
|
||||
set2[set2.maxY][1] <= set1[set1.minY][1];
|
||||
});
|
||||
})
|
||||
) return false;
|
||||
|
||||
// Get a convex hull from points of each subpath. Has the most complexity O(n·log n).
|
||||
var hullNest1 = points1.map(convexHull),
|
||||
hullNest2 = points2.map(convexHull);
|
||||
|
||||
// Check intersection of every subpath of the first path with every subpath of the second.
|
||||
return hullNest1.some(function(hull1) {
|
||||
if (hull1.length < 3) return false;
|
||||
|
||||
return hullNest2.some(function(hull2) {
|
||||
if (hull2.length < 3) return false;
|
||||
|
||||
var simplex = [getSupport(hull1, hull2, [1, 0])], // create the initial simplex
|
||||
direction = minus(simplex[0]); // set the direction to point towards the origin
|
||||
|
||||
var iterations = 1e4; // infinite loop protection, 10 000 iterations is more than enough
|
||||
while (true) {
|
||||
if (iterations-- == 0) {
|
||||
console.error('Error: infinite loop while processing mergePaths plugin.');
|
||||
return true; // true is the safe value that means “do nothing with paths”
|
||||
}
|
||||
// add a new point
|
||||
simplex.push(getSupport(hull1, hull2, direction));
|
||||
// see if the new point was on the correct side of the origin
|
||||
if (dot(direction, simplex[simplex.length - 1]) <= 0) return false;
|
||||
// process the simplex
|
||||
if (processSimplex(simplex, direction)) return true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function getSupport(a, b, direction) {
|
||||
return sub(supportPoint(a, direction), supportPoint(b, minus(direction)));
|
||||
}
|
||||
|
||||
// Computes farthest polygon point in particular direction.
|
||||
// Thanks to knowledge of min/max x and y coordinates we can choose a quadrant to search in.
|
||||
// Since we're working on convex hull, the dot product is increasing until we find the farthest point.
|
||||
function supportPoint(polygon, direction) {
|
||||
var index = direction[1] >= 0 ?
|
||||
direction[0] < 0 ? polygon.maxY : polygon.maxX :
|
||||
direction[0] < 0 ? polygon.minX : polygon.minY,
|
||||
max = -Infinity,
|
||||
value;
|
||||
while ((value = dot(polygon[index], direction)) > max) {
|
||||
max = value;
|
||||
index = ++index % polygon.length;
|
||||
}
|
||||
return polygon[(index || polygon.length) - 1];
|
||||
}
|
||||
};
|
||||
|
||||
function processSimplex(simplex, direction) {
|
||||
/* jshint -W004 */
|
||||
|
||||
// we only need to handle to 1-simplex and 2-simplex
|
||||
if (simplex.length == 2) { // 1-simplex
|
||||
var a = simplex[1],
|
||||
b = simplex[0],
|
||||
AO = minus(simplex[1]),
|
||||
AB = sub(b, a);
|
||||
// AO is in the same direction as AB
|
||||
if (dot(AO, AB) > 0) {
|
||||
// get the vector perpendicular to AB facing O
|
||||
set(direction, orth(AB, a));
|
||||
} else {
|
||||
set(direction, AO);
|
||||
// only A remains in the simplex
|
||||
simplex.shift();
|
||||
}
|
||||
} else { // 2-simplex
|
||||
var a = simplex[2], // [a, b, c] = simplex
|
||||
b = simplex[1],
|
||||
c = simplex[0],
|
||||
AB = sub(b, a),
|
||||
AC = sub(c, a),
|
||||
AO = minus(a),
|
||||
ACB = orth(AB, AC), // the vector perpendicular to AB facing away from C
|
||||
ABC = orth(AC, AB); // the vector perpendicular to AC facing away from B
|
||||
|
||||
if (dot(ACB, AO) > 0) {
|
||||
if (dot(AB, AO) > 0) { // region 4
|
||||
set(direction, ACB);
|
||||
simplex.shift(); // simplex = [b, a]
|
||||
} else { // region 5
|
||||
set(direction, AO);
|
||||
simplex.splice(0, 2); // simplex = [a]
|
||||
}
|
||||
} else if (dot(ABC, AO) > 0) {
|
||||
if (dot(AC, AO) > 0) { // region 6
|
||||
set(direction, ABC);
|
||||
simplex.splice(1, 1); // simplex = [c, a]
|
||||
} else { // region 5 (again)
|
||||
set(direction, AO);
|
||||
simplex.splice(0, 2); // simplex = [a]
|
||||
}
|
||||
} else // region 7
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function minus(v) {
|
||||
return [-v[0], -v[1]];
|
||||
}
|
||||
|
||||
function sub(v1, v2) {
|
||||
return [v1[0] - v2[0], v1[1] - v2[1]];
|
||||
}
|
||||
|
||||
function dot(v1, v2) {
|
||||
return v1[0] * v2[0] + v1[1] * v2[1];
|
||||
}
|
||||
|
||||
function orth(v, from) {
|
||||
var o = [-v[1], v[0]];
|
||||
return dot(o, minus(from)) < 0 ? minus(o) : o;
|
||||
}
|
||||
|
||||
function gatherPoints(points, item, index, path) {
|
||||
|
||||
var subPath = points.length && points[points.length - 1],
|
||||
prev = index && path[index - 1],
|
||||
basePoint = subPath.length && subPath[subPath.length - 1],
|
||||
data = item.data,
|
||||
ctrlPoint = basePoint;
|
||||
|
||||
switch (item.instruction) {
|
||||
case 'M':
|
||||
points.push(subPath = []);
|
||||
break;
|
||||
case 'H':
|
||||
addPoint(subPath, [data[0], basePoint[1]]);
|
||||
break;
|
||||
case 'V':
|
||||
addPoint(subPath, [basePoint[0], data[0]]);
|
||||
break;
|
||||
case 'Q':
|
||||
addPoint(subPath, data.slice(0, 2));
|
||||
prevCtrlPoint = [data[2] - data[0], data[3] - data[1]]; // Save control point for shorthand
|
||||
break;
|
||||
case 'T':
|
||||
if (prev.instruction == 'Q' || prev.instruction == 'T') {
|
||||
ctrlPoint = [basePoint[0] + prevCtrlPoint[0], basePoint[1] + prevCtrlPoint[1]];
|
||||
addPoint(subPath, ctrlPoint);
|
||||
prevCtrlPoint = [data[0] - ctrlPoint[0], data[1] - ctrlPoint[1]];
|
||||
}
|
||||
break;
|
||||
case 'C':
|
||||
// Approximate quibic Bezier curve with middle points between control points
|
||||
addPoint(subPath, [.5 * (basePoint[0] + data[0]), .5 * (basePoint[1] + data[1])]);
|
||||
addPoint(subPath, [.5 * (data[0] + data[2]), .5 * (data[1] + data[3])]);
|
||||
addPoint(subPath, [.5 * (data[2] + data[4]), .5 * (data[3] + data[5])]);
|
||||
prevCtrlPoint = [data[4] - data[2], data[5] - data[3]]; // Save control point for shorthand
|
||||
break;
|
||||
case 'S':
|
||||
if (prev.instruction == 'C' || prev.instruction == 'S') {
|
||||
addPoint(subPath, [basePoint[0] + .5 * prevCtrlPoint[0], basePoint[1] + .5 * prevCtrlPoint[1]]);
|
||||
ctrlPoint = [basePoint[0] + prevCtrlPoint[0], basePoint[1] + prevCtrlPoint[1]];
|
||||
}
|
||||
addPoint(subPath, [.5 * (ctrlPoint[0] + data[0]), .5 * (ctrlPoint[1]+ data[1])]);
|
||||
addPoint(subPath, [.5 * (data[0] + data[2]), .5 * (data[1] + data[3])]);
|
||||
prevCtrlPoint = [data[2] - data[0], data[3] - data[1]];
|
||||
break;
|
||||
case 'A':
|
||||
// Convert the arc to bezier curves and use the same approximation
|
||||
var curves = a2c.apply(0, basePoint.concat(data));
|
||||
for (var cData; (cData = curves.splice(0,6).map(toAbsolute)).length;) {
|
||||
addPoint(subPath, [.5 * (basePoint[0] + cData[0]), .5 * (basePoint[1] + cData[1])]);
|
||||
addPoint(subPath, [.5 * (cData[0] + cData[2]), .5 * (cData[1] + cData[3])]);
|
||||
addPoint(subPath, [.5 * (cData[2] + cData[4]), .5 * (cData[3] + cData[5])]);
|
||||
if (curves.length) addPoint(subPath, basePoint = cData.slice(-2));
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Save final command coordinates
|
||||
if (data && data.length >= 2) addPoint(subPath, data.slice(-2));
|
||||
return points;
|
||||
|
||||
function toAbsolute(n, i) { return n + basePoint[i % 2] }
|
||||
|
||||
// Writes data about the extreme points on each axle
|
||||
function addPoint(path, point) {
|
||||
if (!path.length || point[1] > path[path.maxY][1]) {
|
||||
path.maxY = path.length;
|
||||
points.maxY = points.length ? Math.max(point[1], points.maxY) : point[1];
|
||||
}
|
||||
if (!path.length || point[0] > path[path.maxX][0]) {
|
||||
path.maxX = path.length;
|
||||
points.maxX = points.length ? Math.max(point[0], points.maxX) : point[0];
|
||||
}
|
||||
if (!path.length || point[1] < path[path.minY][1]) {
|
||||
path.minY = path.length;
|
||||
points.minY = points.length ? Math.min(point[1], points.minY) : point[1];
|
||||
}
|
||||
if (!path.length || point[0] < path[path.minX][0]) {
|
||||
path.minX = path.length;
|
||||
points.minX = points.length ? Math.min(point[0], points.minX) : point[0];
|
||||
}
|
||||
path.push(point);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forms a convex hull from set of points of every subpath using monotone chain convex hull algorithm.
|
||||
* http://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Convex_hull/Monotone_chain
|
||||
*
|
||||
* @param points An array of [X, Y] coordinates
|
||||
*/
|
||||
function convexHull(points) {
|
||||
/* jshint -W004 */
|
||||
|
||||
points.sort(function(a, b) {
|
||||
return a[0] == b[0] ? a[1] - b[1] : a[0] - b[0];
|
||||
});
|
||||
|
||||
var lower = [],
|
||||
minY = 0,
|
||||
bottom = 0;
|
||||
for (var i = 0; i < points.length; i++) {
|
||||
while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], points[i]) <= 0) {
|
||||
lower.pop();
|
||||
}
|
||||
if (points[i][1] < points[minY][1]) {
|
||||
minY = i;
|
||||
bottom = lower.length;
|
||||
}
|
||||
lower.push(points[i]);
|
||||
}
|
||||
|
||||
var upper = [],
|
||||
maxY = points.length - 1,
|
||||
top = 0;
|
||||
for (var i = points.length; i--;) {
|
||||
while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], points[i]) <= 0) {
|
||||
upper.pop();
|
||||
}
|
||||
if (points[i][1] > points[maxY][1]) {
|
||||
maxY = i;
|
||||
top = upper.length;
|
||||
}
|
||||
upper.push(points[i]);
|
||||
}
|
||||
|
||||
// last points are equal to starting points of the other part
|
||||
upper.pop();
|
||||
lower.pop();
|
||||
|
||||
var hull = lower.concat(upper);
|
||||
|
||||
hull.minX = 0; // by sorting
|
||||
hull.maxX = lower.length;
|
||||
hull.minY = bottom;
|
||||
hull.maxY = (lower.length + top) % hull.length;
|
||||
|
||||
return hull;
|
||||
}
|
||||
|
||||
function cross(o, a, b) {
|
||||
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
|
||||
}
|
||||
|
||||
/* Based on code from Snap.svg (Apache 2 license). http://snapsvg.io/
|
||||
* Thanks to Dmitry Baranovskiy for his great work!
|
||||
*/
|
||||
|
||||
// jshint ignore: start
|
||||
function a2c(x1, y1, rx, ry, angle, large_arc_flag, sweep_flag, x2, y2, recursive) {
|
||||
// for more information of where this Math came from visit:
|
||||
// http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
|
||||
var _120 = Math.PI * 120 / 180,
|
||||
rad = Math.PI / 180 * (+angle || 0),
|
||||
res = [],
|
||||
rotateX = function(x, y, rad) { return x * Math.cos(rad) - y * Math.sin(rad) },
|
||||
rotateY = function(x, y, rad) { return x * Math.sin(rad) + y * Math.cos(rad) };
|
||||
if (!recursive) {
|
||||
x1 = rotateX(x1, y1, -rad);
|
||||
y1 = rotateY(x1, y1, -rad);
|
||||
x2 = rotateX(x2, y2, -rad);
|
||||
y2 = rotateY(x2, y2, -rad);
|
||||
var x = (x1 - x2) / 2,
|
||||
y = (y1 - y2) / 2;
|
||||
var h = (x * x) / (rx * rx) + (y * y) / (ry * ry);
|
||||
if (h > 1) {
|
||||
h = Math.sqrt(h);
|
||||
rx = h * rx;
|
||||
ry = h * ry;
|
||||
}
|
||||
var rx2 = rx * rx,
|
||||
ry2 = ry * ry,
|
||||
k = (large_arc_flag == sweep_flag ? -1 : 1) *
|
||||
Math.sqrt(Math.abs((rx2 * ry2 - rx2 * y * y - ry2 * x * x) / (rx2 * y * y + ry2 * x * x))),
|
||||
cx = k * rx * y / ry + (x1 + x2) / 2,
|
||||
cy = k * -ry * x / rx + (y1 + y2) / 2,
|
||||
f1 = Math.asin(((y1 - cy) / ry).toFixed(9)),
|
||||
f2 = Math.asin(((y2 - cy) / ry).toFixed(9));
|
||||
|
||||
f1 = x1 < cx ? Math.PI - f1 : f1;
|
||||
f2 = x2 < cx ? Math.PI - f2 : f2;
|
||||
f1 < 0 && (f1 = Math.PI * 2 + f1);
|
||||
f2 < 0 && (f2 = Math.PI * 2 + f2);
|
||||
if (sweep_flag && f1 > f2) {
|
||||
f1 = f1 - Math.PI * 2;
|
||||
}
|
||||
if (!sweep_flag && f2 > f1) {
|
||||
f2 = f2 - Math.PI * 2;
|
||||
}
|
||||
} else {
|
||||
f1 = recursive[0];
|
||||
f2 = recursive[1];
|
||||
cx = recursive[2];
|
||||
cy = recursive[3];
|
||||
}
|
||||
var df = f2 - f1;
|
||||
if (Math.abs(df) > _120) {
|
||||
var f2old = f2,
|
||||
x2old = x2,
|
||||
y2old = y2;
|
||||
f2 = f1 + _120 * (sweep_flag && f2 > f1 ? 1 : -1);
|
||||
x2 = cx + rx * Math.cos(f2);
|
||||
y2 = cy + ry * Math.sin(f2);
|
||||
res = a2c(x2, y2, rx, ry, angle, 0, sweep_flag, x2old, y2old, [f2, f2old, cx, cy]);
|
||||
}
|
||||
df = f2 - f1;
|
||||
var c1 = Math.cos(f1),
|
||||
s1 = Math.sin(f1),
|
||||
c2 = Math.cos(f2),
|
||||
s2 = Math.sin(f2),
|
||||
t = Math.tan(df / 4),
|
||||
hx = 4 / 3 * rx * t,
|
||||
hy = 4 / 3 * ry * t,
|
||||
m = [
|
||||
- hx * s1, hy * c1,
|
||||
x2 + hx * s2 - x1, y2 - hy * c2 - y1,
|
||||
x2 - x1, y2 - y1
|
||||
];
|
||||
if (recursive) {
|
||||
return m.concat(res);
|
||||
} else {
|
||||
res = m.concat(res);
|
||||
var newres = [];
|
||||
for (var i = 0, n = res.length; i < n; i++) {
|
||||
newres[i] = i % 2 ? rotateY(res[i - 1], res[i], rad) : rotateX(res[i], res[i + 1], rad);
|
||||
}
|
||||
return newres;
|
||||
}
|
||||
}
|
||||
// jshint ignore: end
|
||||
+310
@@ -0,0 +1,310 @@
|
||||
'use strict';
|
||||
|
||||
var regTransformTypes = /matrix|translate|scale|rotate|skewX|skewY/,
|
||||
regTransformSplit = /\s*(matrix|translate|scale|rotate|skewX|skewY)\s*\(\s*(.+?)\s*\)[\s,]*/,
|
||||
regNumericValues = /[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/g;
|
||||
|
||||
/**
|
||||
* Convert transform string to JS representation.
|
||||
*
|
||||
* @param {String} transformString input string
|
||||
* @param {Object} params plugin params
|
||||
* @return {Array} output array
|
||||
*/
|
||||
exports.transform2js = function(transformString) {
|
||||
|
||||
// JS representation of the transform data
|
||||
var transforms = [],
|
||||
// current transform context
|
||||
current;
|
||||
|
||||
// split value into ['', 'translate', '10 50', '', 'scale', '2', '', 'rotate', '-45', '']
|
||||
transformString.split(regTransformSplit).forEach(function(item) {
|
||||
/*jshint -W084 */
|
||||
var num;
|
||||
|
||||
if (item) {
|
||||
// if item is a translate function
|
||||
if (regTransformTypes.test(item)) {
|
||||
// then collect it and change current context
|
||||
transforms.push(current = { name: item });
|
||||
// else if item is data
|
||||
} else {
|
||||
// then split it into [10, 50] and collect as context.data
|
||||
while (num = regNumericValues.exec(item)) {
|
||||
num = Number(num);
|
||||
if (current.data)
|
||||
current.data.push(num);
|
||||
else
|
||||
current.data = [num];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// return empty array if broken transform (no data)
|
||||
return current && current.data ? transforms : [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Multiply transforms into one.
|
||||
*
|
||||
* @param {Array} input transforms array
|
||||
* @return {Array} output matrix array
|
||||
*/
|
||||
exports.transformsMultiply = function(transforms) {
|
||||
|
||||
// convert transforms objects to the matrices
|
||||
transforms = transforms.map(function(transform) {
|
||||
if (transform.name === 'matrix') {
|
||||
return transform.data;
|
||||
}
|
||||
return transformToMatrix(transform);
|
||||
});
|
||||
|
||||
// multiply all matrices into one
|
||||
transforms = {
|
||||
name: 'matrix',
|
||||
data: transforms.length > 0 ? transforms.reduce(multiplyTransformMatrices) : []
|
||||
};
|
||||
|
||||
return transforms;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Do math like a schoolgirl.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
var mth = exports.mth = {
|
||||
|
||||
rad: function(deg) {
|
||||
return deg * Math.PI / 180;
|
||||
},
|
||||
|
||||
deg: function(rad) {
|
||||
return rad * 180 / Math.PI;
|
||||
},
|
||||
|
||||
cos: function(deg) {
|
||||
return Math.cos(this.rad(deg));
|
||||
},
|
||||
|
||||
acos: function(val, floatPrecision) {
|
||||
return +(this.deg(Math.acos(val)).toFixed(floatPrecision));
|
||||
},
|
||||
|
||||
sin: function(deg) {
|
||||
return Math.sin(this.rad(deg));
|
||||
},
|
||||
|
||||
asin: function(val, floatPrecision) {
|
||||
return +(this.deg(Math.asin(val)).toFixed(floatPrecision));
|
||||
},
|
||||
|
||||
tan: function(deg) {
|
||||
return Math.tan(this.rad(deg));
|
||||
},
|
||||
|
||||
atan: function(val, floatPrecision) {
|
||||
return +(this.deg(Math.atan(val)).toFixed(floatPrecision));
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Decompose matrix into simple transforms. See
|
||||
* http://frederic-wang.fr/decomposition-of-2d-transform-matrices.html
|
||||
*
|
||||
* @param {Object} data matrix transform object
|
||||
* @return {Object|Array} transforms array or original transform object
|
||||
*/
|
||||
exports.matrixToTransform = function(transform, params) {
|
||||
var floatPrecision = params.floatPrecision,
|
||||
data = transform.data,
|
||||
transforms = [],
|
||||
sx = +Math.hypot(data[0], data[1]).toFixed(params.transformPrecision),
|
||||
sy = +((data[0] * data[3] - data[1] * data[2]) / sx).toFixed(params.transformPrecision),
|
||||
colsSum = data[0] * data[2] + data[1] * data[3],
|
||||
rowsSum = data[0] * data[1] + data[2] * data[3],
|
||||
scaleBefore = rowsSum != 0 || sx == sy;
|
||||
|
||||
// [..., ..., ..., ..., tx, ty] → translate(tx, ty)
|
||||
if (data[4] || data[5]) {
|
||||
transforms.push({ name: 'translate', data: data.slice(4, data[5] ? 6 : 5) });
|
||||
}
|
||||
|
||||
// [sx, 0, tan(a)·sy, sy, 0, 0] → skewX(a)·scale(sx, sy)
|
||||
if (!data[1] && data[2]) {
|
||||
transforms.push({ name: 'skewX', data: [mth.atan(data[2] / sy, floatPrecision)] });
|
||||
|
||||
// [sx, sx·tan(a), 0, sy, 0, 0] → skewY(a)·scale(sx, sy)
|
||||
} else if (data[1] && !data[2]) {
|
||||
transforms.push({ name: 'skewY', data: [mth.atan(data[1] / data[0], floatPrecision)] });
|
||||
sx = data[0];
|
||||
sy = data[3];
|
||||
|
||||
// [sx·cos(a), sx·sin(a), sy·-sin(a), sy·cos(a), x, y] → rotate(a[, cx, cy])·(scale or skewX) or
|
||||
// [sx·cos(a), sy·sin(a), sx·-sin(a), sy·cos(a), x, y] → scale(sx, sy)·rotate(a[, cx, cy]) (if !scaleBefore)
|
||||
} else if (!colsSum || (sx == 1 && sy == 1) || !scaleBefore) {
|
||||
if (!scaleBefore) {
|
||||
sx = (data[0] < 0 ? -1 : 1) * Math.hypot(data[0], data[2]);
|
||||
sy = (data[3] < 0 ? -1 : 1) * Math.hypot(data[1], data[3]);
|
||||
transforms.push({ name: 'scale', data: [sx, sy] });
|
||||
}
|
||||
var angle = Math.min(Math.max(-1, data[0] / sx), 1),
|
||||
rotate = [mth.acos(angle, floatPrecision) * ((scaleBefore ? 1 : sy) * data[1] < 0 ? -1 : 1)];
|
||||
|
||||
if (rotate[0]) transforms.push({ name: 'rotate', data: rotate });
|
||||
|
||||
if (rowsSum && colsSum) transforms.push({
|
||||
name: 'skewX',
|
||||
data: [mth.atan(colsSum / (sx * sx), floatPrecision)]
|
||||
});
|
||||
|
||||
// rotate(a, cx, cy) can consume translate() within optional arguments cx, cy (rotation point)
|
||||
if (rotate[0] && (data[4] || data[5])) {
|
||||
transforms.shift();
|
||||
var cos = data[0] / sx,
|
||||
sin = data[1] / (scaleBefore ? sx : sy),
|
||||
x = data[4] * (scaleBefore || sy),
|
||||
y = data[5] * (scaleBefore || sx),
|
||||
denom = (Math.pow(1 - cos, 2) + Math.pow(sin, 2)) * (scaleBefore || sx * sy);
|
||||
rotate.push(((1 - cos) * x - sin * y) / denom);
|
||||
rotate.push(((1 - cos) * y + sin * x) / denom);
|
||||
}
|
||||
|
||||
// Too many transformations, return original matrix if it isn't just a scale/translate
|
||||
} else if (data[1] || data[2]) {
|
||||
return transform;
|
||||
}
|
||||
|
||||
if (scaleBefore && (sx != 1 || sy != 1) || !transforms.length) transforms.push({
|
||||
name: 'scale',
|
||||
data: sx == sy ? [sx] : [sx, sy]
|
||||
});
|
||||
|
||||
return transforms;
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert transform to the matrix data.
|
||||
*
|
||||
* @param {Object} transform transform object
|
||||
* @return {Array} matrix data
|
||||
*/
|
||||
function transformToMatrix(transform) {
|
||||
|
||||
if (transform.name === 'matrix') return transform.data;
|
||||
|
||||
var matrix;
|
||||
|
||||
switch (transform.name) {
|
||||
case 'translate':
|
||||
// [1, 0, 0, 1, tx, ty]
|
||||
matrix = [1, 0, 0, 1, transform.data[0], transform.data[1] || 0];
|
||||
break;
|
||||
case 'scale':
|
||||
// [sx, 0, 0, sy, 0, 0]
|
||||
matrix = [transform.data[0], 0, 0, transform.data[1] || transform.data[0], 0, 0];
|
||||
break;
|
||||
case 'rotate':
|
||||
// [cos(a), sin(a), -sin(a), cos(a), x, y]
|
||||
var cos = mth.cos(transform.data[0]),
|
||||
sin = mth.sin(transform.data[0]),
|
||||
cx = transform.data[1] || 0,
|
||||
cy = transform.data[2] || 0;
|
||||
|
||||
matrix = [cos, sin, -sin, cos, (1 - cos) * cx + sin * cy, (1 - cos) * cy - sin * cx];
|
||||
break;
|
||||
case 'skewX':
|
||||
// [1, 0, tan(a), 1, 0, 0]
|
||||
matrix = [1, 0, mth.tan(transform.data[0]), 1, 0, 0];
|
||||
break;
|
||||
case 'skewY':
|
||||
// [1, tan(a), 0, 1, 0, 0]
|
||||
matrix = [1, mth.tan(transform.data[0]), 0, 1, 0, 0];
|
||||
break;
|
||||
}
|
||||
|
||||
return matrix;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies transformation to an arc. To do so, we represent ellipse as a matrix, multiply it
|
||||
* by the transformation matrix and use a singular value decomposition to represent in a form
|
||||
* rotate(θ)·scale(a b)·rotate(φ). This gives us new ellipse params a, b and θ.
|
||||
* SVD is being done with the formulae provided by Wolffram|Alpha (svd {{m0, m2}, {m1, m3}})
|
||||
*
|
||||
* @param {Array} arc [a, b, rotation in deg]
|
||||
* @param {Array} transform transformation matrix
|
||||
* @return {Array} arc transformed input arc
|
||||
*/
|
||||
exports.transformArc = function(arc, transform) {
|
||||
|
||||
var a = arc[0],
|
||||
b = arc[1],
|
||||
rot = arc[2] * Math.PI / 180,
|
||||
cos = Math.cos(rot),
|
||||
sin = Math.sin(rot),
|
||||
h = Math.pow(arc[5] * cos + arc[6] * sin, 2) / (4 * a * a) +
|
||||
Math.pow(arc[6] * cos - arc[5] * sin, 2) / (4 * b * b);
|
||||
if (h > 1) {
|
||||
h = Math.sqrt(h);
|
||||
a *= h;
|
||||
b *= h;
|
||||
}
|
||||
var ellipse = [a * cos, a * sin, -b * sin, b * cos, 0, 0],
|
||||
m = multiplyTransformMatrices(transform, ellipse),
|
||||
// Decompose the new ellipse matrix
|
||||
lastCol = m[2] * m[2] + m[3] * m[3],
|
||||
squareSum = m[0] * m[0] + m[1] * m[1] + lastCol,
|
||||
root = Math.hypot(m[0] - m[3], m[1] + m[2]) * Math.hypot(m[0] + m[3], m[1] - m[2]);
|
||||
|
||||
if (!root) { // circle
|
||||
arc[0] = arc[1] = Math.sqrt(squareSum / 2);
|
||||
arc[2] = 0;
|
||||
} else {
|
||||
var majorAxisSqr = (squareSum + root) / 2,
|
||||
minorAxisSqr = (squareSum - root) / 2,
|
||||
major = Math.abs(majorAxisSqr - lastCol) > 1e-6,
|
||||
sub = (major ? majorAxisSqr : minorAxisSqr) - lastCol,
|
||||
rowsSum = m[0] * m[2] + m[1] * m[3],
|
||||
term1 = m[0] * sub + m[2] * rowsSum,
|
||||
term2 = m[1] * sub + m[3] * rowsSum;
|
||||
arc[0] = Math.sqrt(majorAxisSqr);
|
||||
arc[1] = Math.sqrt(minorAxisSqr);
|
||||
arc[2] = ((major ? term2 < 0 : term1 > 0) ? -1 : 1) *
|
||||
Math.acos((major ? term1 : term2) / Math.hypot(term1, term2)) * 180 / Math.PI;
|
||||
}
|
||||
|
||||
if ((transform[0] < 0) !== (transform[3] < 0)) {
|
||||
// Flip the sweep flag if coordinates are being flipped horizontally XOR vertically
|
||||
arc[4] = 1 - arc[4];
|
||||
}
|
||||
|
||||
return arc;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Multiply transformation matrices.
|
||||
*
|
||||
* @param {Array} a matrix A data
|
||||
* @param {Array} b matrix B data
|
||||
* @return {Array} result
|
||||
*/
|
||||
function multiplyTransformMatrices(a, b) {
|
||||
|
||||
return [
|
||||
a[0] * b[0] + a[2] * b[1],
|
||||
a[1] * b[0] + a[3] * b[1],
|
||||
a[0] * b[2] + a[2] * b[3],
|
||||
a[1] * b[2] + a[3] * b[3],
|
||||
a[0] * b[4] + a[2] * b[5] + a[4],
|
||||
a[1] * b[4] + a[3] * b[5] + a[5]
|
||||
];
|
||||
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
'use strict';
|
||||
|
||||
exports.type = 'full';
|
||||
|
||||
exports.active = false;
|
||||
|
||||
exports.description = 'adds attributes to an outer <svg> element';
|
||||
|
||||
var ENOCLS = `Error in plugin "addAttributesToSVGElement": absent parameters.
|
||||
It should have a list of "attributes" or one "attribute".
|
||||
Config example:
|
||||
|
||||
plugins:
|
||||
- addAttributesToSVGElement:
|
||||
attribute: "mySvg"
|
||||
|
||||
plugins:
|
||||
- addAttributesToSVGElement:
|
||||
attributes: ["mySvg", "size-big"]
|
||||
|
||||
plugins:
|
||||
- addAttributesToSVGElement:
|
||||
attributes:
|
||||
- focusable: false
|
||||
- data-image: icon`;
|
||||
|
||||
/**
|
||||
* Add attributes to an outer <svg> element. Example config:
|
||||
*
|
||||
* plugins:
|
||||
* - addAttributesToSVGElement:
|
||||
* attribute: 'data-icon'
|
||||
*
|
||||
* plugins:
|
||||
* - addAttributesToSVGElement:
|
||||
* attributes: ['data-icon', 'data-disabled']
|
||||
*
|
||||
* plugins:
|
||||
* - addAttributesToSVGElement:
|
||||
* attributes:
|
||||
* - focusable: false
|
||||
* - data-image: icon
|
||||
*
|
||||
* @author April Arcus
|
||||
*/
|
||||
exports.fn = function(data, params) {
|
||||
if (!params || !(Array.isArray(params.attributes) || params.attribute)) {
|
||||
console.error(ENOCLS);
|
||||
return data;
|
||||
}
|
||||
|
||||
var attributes = params.attributes || [ params.attribute ],
|
||||
svg = data.content[0];
|
||||
|
||||
if (svg.isElem('svg')) {
|
||||
attributes.forEach(function (attribute) {
|
||||
if (typeof attribute === 'string') {
|
||||
if (!svg.hasAttr(attribute)) {
|
||||
svg.addAttr({
|
||||
name: attribute,
|
||||
prefix: '',
|
||||
local: attribute
|
||||
});
|
||||
}
|
||||
} else if (typeof attribute === 'object') {
|
||||
Object.keys(attribute).forEach(function (key) {
|
||||
if (!svg.hasAttr(key)) {
|
||||
svg.addAttr({
|
||||
name: key,
|
||||
value: attribute[key],
|
||||
prefix: '',
|
||||
local: key
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
|
||||
};
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
'use strict';
|
||||
|
||||
exports.type = 'full';
|
||||
|
||||
exports.active = false;
|
||||
|
||||
exports.description = 'adds classnames to an outer <svg> element';
|
||||
|
||||
var ENOCLS = `Error in plugin "addClassesToSVGElement": absent parameters.
|
||||
It should have a list of classes in "classNames" or one "className".
|
||||
Config example:
|
||||
|
||||
plugins:
|
||||
- addClassesToSVGElement:
|
||||
className: "mySvg"
|
||||
|
||||
plugins:
|
||||
- addClassesToSVGElement:
|
||||
classNames: ["mySvg", "size-big"]
|
||||
`;
|
||||
|
||||
/**
|
||||
* Add classnames to an outer <svg> element. Example config:
|
||||
*
|
||||
* plugins:
|
||||
* - addClassesToSVGElement:
|
||||
* className: 'mySvg'
|
||||
*
|
||||
* plugins:
|
||||
* - addClassesToSVGElement:
|
||||
* classNames: ['mySvg', 'size-big']
|
||||
*
|
||||
* @author April Arcus
|
||||
*/
|
||||
exports.fn = function(data, params) {
|
||||
if (!params || !(Array.isArray(params.classNames) && params.classNames.some(String) || params.className)) {
|
||||
console.error(ENOCLS);
|
||||
return data;
|
||||
}
|
||||
|
||||
var classNames = params.classNames || [ params.className ],
|
||||
svg = data.content[0];
|
||||
|
||||
if (svg.isElem('svg')) {
|
||||
svg.class.add.apply(svg.class, classNames);
|
||||
}
|
||||
|
||||
return data;
|
||||
|
||||
};
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
'use strict';
|
||||
|
||||
exports.type = 'perItem';
|
||||
|
||||
exports.active = true;
|
||||
|
||||
exports.description = 'cleanups attributes from newlines, trailing and repeating spaces';
|
||||
|
||||
exports.params = {
|
||||
newlines: true,
|
||||
trim: true,
|
||||
spaces: true
|
||||
};
|
||||
|
||||
var regNewlinesNeedSpace = /(\S)\r?\n(\S)/g,
|
||||
regNewlines = /\r?\n/g,
|
||||
regSpaces = /\s{2,}/g;
|
||||
|
||||
/**
|
||||
* Cleanup attributes values from newlines, trailing and repeating spaces.
|
||||
*
|
||||
* @param {Object} item current iteration item
|
||||
* @param {Object} params plugin params
|
||||
* @return {Boolean} if false, item will be filtered out
|
||||
*
|
||||
* @author Kir Belevich
|
||||
*/
|
||||
exports.fn = function(item, params) {
|
||||
|
||||
if (item.isElem()) {
|
||||
|
||||
item.eachAttr(function(attr) {
|
||||
|
||||
if (params.newlines) {
|
||||
// new line which requires a space instead of themselve
|
||||
attr.value = attr.value.replace(regNewlinesNeedSpace, function(match, p1, p2) {
|
||||
return p1 + ' ' + p2;
|
||||
});
|
||||
|
||||
// simple new line
|
||||
attr.value = attr.value.replace(regNewlines, '');
|
||||
}
|
||||
|
||||
if (params.trim) {
|
||||
attr.value = attr.value.trim();
|
||||
}
|
||||
|
||||
if (params.spaces) {
|
||||
attr.value = attr.value.replace(regSpaces, ' ');
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
'use strict';
|
||||
|
||||
exports.type = 'full';
|
||||
|
||||
exports.active = true;
|
||||
|
||||
exports.description = 'remove or cleanup enable-background attribute when possible';
|
||||
|
||||
/**
|
||||
* Remove or cleanup enable-background attr which coincides with a width/height box.
|
||||
*
|
||||
* @see http://www.w3.org/TR/SVG/filters.html#EnableBackgroundProperty
|
||||
*
|
||||
* @example
|
||||
* <svg width="100" height="50" enable-background="new 0 0 100 50">
|
||||
* ⬇
|
||||
* <svg width="100" height="50">
|
||||
*
|
||||
* @param {Object} item current iteration item
|
||||
* @return {Boolean} if false, item will be filtered out
|
||||
*
|
||||
* @author Kir Belevich
|
||||
*/
|
||||
exports.fn = function(data) {
|
||||
|
||||
var regEnableBackground = /^new\s0\s0\s([\-+]?\d*\.?\d+([eE][\-+]?\d+)?)\s([\-+]?\d*\.?\d+([eE][\-+]?\d+)?)$/,
|
||||
hasFilter = false,
|
||||
elems = ['svg', 'mask', 'pattern'];
|
||||
|
||||
function checkEnableBackground(item) {
|
||||
if (
|
||||
item.isElem(elems) &&
|
||||
item.hasAttr('enable-background') &&
|
||||
item.hasAttr('width') &&
|
||||
item.hasAttr('height')
|
||||
) {
|
||||
|
||||
var match = item.attr('enable-background').value.match(regEnableBackground);
|
||||
|
||||
if (match) {
|
||||
if (
|
||||
item.attr('width').value === match[1] &&
|
||||
item.attr('height').value === match[3]
|
||||
) {
|
||||
if (item.isElem('svg')) {
|
||||
item.removeAttr('enable-background');
|
||||
} else {
|
||||
item.attr('enable-background').value = 'new';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function checkForFilter(item) {
|
||||
if (item.isElem('filter')) {
|
||||
hasFilter = true;
|
||||
}
|
||||
}
|
||||
|
||||
function monkeys(items, fn) {
|
||||
items.content.forEach(function(item) {
|
||||
fn(item);
|
||||
|
||||
if (item.content) {
|
||||
monkeys(item, fn);
|
||||
}
|
||||
});
|
||||
return items;
|
||||
}
|
||||
|
||||
var firstStep = monkeys(data, function(item) {
|
||||
checkEnableBackground(item);
|
||||
if (!hasFilter) {
|
||||
checkForFilter(item);
|
||||
}
|
||||
});
|
||||
|
||||
return hasFilter ? firstStep : monkeys(firstStep, function(item) {
|
||||
//we don't need 'enable-background' if we have no filters
|
||||
item.removeAttr('enable-background');
|
||||
});
|
||||
};
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
'use strict';
|
||||
|
||||
exports.type = 'full';
|
||||
|
||||
exports.active = true;
|
||||
|
||||
exports.description = 'removes unused IDs and minifies used';
|
||||
|
||||
exports.params = {
|
||||
remove: true,
|
||||
minify: true,
|
||||
prefix: '',
|
||||
preserve: [],
|
||||
preservePrefixes: [],
|
||||
force: false
|
||||
};
|
||||
|
||||
var referencesProps = new Set(require('./_collections').referencesProps),
|
||||
regReferencesUrl = /\burl\(("|')?#(.+?)\1\)/,
|
||||
regReferencesHref = /^#(.+?)$/,
|
||||
regReferencesBegin = /(\w+)\./,
|
||||
styleOrScript = ['style', 'script'],
|
||||
generateIDchars = [
|
||||
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
|
||||
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'
|
||||
],
|
||||
maxIDindex = generateIDchars.length - 1;
|
||||
|
||||
/**
|
||||
* Remove unused and minify used IDs
|
||||
* (only if there are no any <style> or <script>).
|
||||
*
|
||||
* @param {Object} item current iteration item
|
||||
* @param {Object} params plugin params
|
||||
*
|
||||
* @author Kir Belevich
|
||||
*/
|
||||
exports.fn = function(data, params) {
|
||||
var currentID,
|
||||
currentIDstring,
|
||||
IDs = new Map(),
|
||||
referencesIDs = new Map(),
|
||||
hasStyleOrScript = false,
|
||||
preserveIDs = new Set(Array.isArray(params.preserve) ? params.preserve : params.preserve ? [params.preserve] : []),
|
||||
preserveIDPrefixes = new Set(Array.isArray(params.preservePrefixes) ? params.preservePrefixes : (params.preservePrefixes ? [params.preservePrefixes] : [])),
|
||||
idValuePrefix = '#',
|
||||
idValuePostfix = '.';
|
||||
|
||||
/**
|
||||
* Bananas!
|
||||
*
|
||||
* @param {Array} items input items
|
||||
* @return {Array} output items
|
||||
*/
|
||||
function monkeys(items) {
|
||||
for (var i = 0; i < items.content.length && !hasStyleOrScript; i++) {
|
||||
var item = items.content[i];
|
||||
|
||||
// quit if <style> or <script> present ('force' param prevents quitting)
|
||||
if (!params.force) {
|
||||
if (item.isElem(styleOrScript)) {
|
||||
hasStyleOrScript = true;
|
||||
continue;
|
||||
}
|
||||
// Don't remove IDs if the whole SVG consists only of defs.
|
||||
if (item.isElem('defs') && item.parentNode.isElem('svg')) {
|
||||
var hasDefsOnly = true;
|
||||
for (var j = i + 1; j < items.content.length; j++) {
|
||||
if (items.content[j].isElem()) {
|
||||
hasDefsOnly = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hasDefsOnly) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// …and don't remove any ID if yes
|
||||
if (item.isElem()) {
|
||||
item.eachAttr(function(attr) {
|
||||
var key, match;
|
||||
|
||||
// save IDs
|
||||
if (attr.name === 'id') {
|
||||
key = attr.value;
|
||||
if (IDs.has(key)) {
|
||||
item.removeAttr('id'); // remove repeated id
|
||||
} else {
|
||||
IDs.set(key, item);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// save references
|
||||
if (referencesProps.has(attr.name) && (match = attr.value.match(regReferencesUrl))) {
|
||||
key = match[2]; // url() reference
|
||||
} else if (
|
||||
attr.local === 'href' && (match = attr.value.match(regReferencesHref)) ||
|
||||
attr.name === 'begin' && (match = attr.value.match(regReferencesBegin))
|
||||
) {
|
||||
key = match[1]; // href reference
|
||||
}
|
||||
if (key) {
|
||||
var ref = referencesIDs.get(key) || [];
|
||||
ref.push(attr);
|
||||
referencesIDs.set(key, ref);
|
||||
}
|
||||
});
|
||||
}
|
||||
// go deeper
|
||||
if (item.content) {
|
||||
monkeys(item);
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
data = monkeys(data);
|
||||
|
||||
if (hasStyleOrScript) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const idPreserved = id => preserveIDs.has(id) || idMatchesPrefix(preserveIDPrefixes, id);
|
||||
|
||||
for (var ref of referencesIDs) {
|
||||
var key = ref[0];
|
||||
|
||||
if (IDs.has(key)) {
|
||||
// replace referenced IDs with the minified ones
|
||||
if (params.minify && !idPreserved(key)) {
|
||||
do {
|
||||
currentIDstring = getIDstring(currentID = generateID(currentID), params);
|
||||
} while (idPreserved(currentIDstring));
|
||||
|
||||
IDs.get(key).attr('id').value = currentIDstring;
|
||||
|
||||
for (var attr of ref[1]) {
|
||||
attr.value = attr.value.includes(idValuePrefix) ?
|
||||
attr.value.replace(idValuePrefix + key, idValuePrefix + currentIDstring) :
|
||||
attr.value.replace(key + idValuePostfix, currentIDstring + idValuePostfix);
|
||||
}
|
||||
}
|
||||
// don't remove referenced IDs
|
||||
IDs.delete(key);
|
||||
}
|
||||
}
|
||||
// remove non-referenced IDs attributes from elements
|
||||
if (params.remove) {
|
||||
for(var keyElem of IDs) {
|
||||
if (!idPreserved(keyElem[0])) {
|
||||
keyElem[1].removeAttr('id');
|
||||
}
|
||||
}
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if an ID starts with any one of a list of strings.
|
||||
*
|
||||
* @param {Array} of prefix strings
|
||||
* @param {String} current ID
|
||||
* @return {Boolean} if currentID starts with one of the strings in prefixArray
|
||||
*/
|
||||
function idMatchesPrefix(prefixArray, currentID) {
|
||||
if (!currentID) return false;
|
||||
|
||||
for (var prefix of prefixArray) if (currentID.startsWith(prefix)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate unique minimal ID.
|
||||
*
|
||||
* @param {Array} [currentID] current ID
|
||||
* @return {Array} generated ID array
|
||||
*/
|
||||
function generateID(currentID) {
|
||||
if (!currentID) return [0];
|
||||
|
||||
currentID[currentID.length - 1]++;
|
||||
|
||||
for(var i = currentID.length - 1; i > 0; i--) {
|
||||
if (currentID[i] > maxIDindex) {
|
||||
currentID[i] = 0;
|
||||
|
||||
if (currentID[i - 1] !== undefined) {
|
||||
currentID[i - 1]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (currentID[0] > maxIDindex) {
|
||||
currentID[0] = 0;
|
||||
currentID.unshift(0);
|
||||
}
|
||||
return currentID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get string from generated ID array.
|
||||
*
|
||||
* @param {Array} arr input ID array
|
||||
* @return {String} output ID string
|
||||
*/
|
||||
function getIDstring(arr, params) {
|
||||
var str = params.prefix;
|
||||
return str + arr.map(i => generateIDchars[i]).join('');
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
'use strict';
|
||||
|
||||
exports.type = 'perItem';
|
||||
|
||||
exports.active = false;
|
||||
|
||||
exports.description = 'rounds list of values to the fixed precision';
|
||||
|
||||
exports.params = {
|
||||
floatPrecision: 3,
|
||||
leadingZero: true,
|
||||
defaultPx: true,
|
||||
convertToPx: true
|
||||
};
|
||||
|
||||
var regNumericValues = /^([\-+]?\d*\.?\d+([eE][\-+]?\d+)?)(px|pt|pc|mm|cm|m|in|ft|em|ex|%)?$/,
|
||||
regSeparator = /\s+,?\s*|,\s*/,
|
||||
removeLeadingZero = require('../lib/svgo/tools').removeLeadingZero,
|
||||
absoluteLengths = { // relative to px
|
||||
cm: 96/2.54,
|
||||
mm: 96/25.4,
|
||||
in: 96,
|
||||
pt: 4/3,
|
||||
pc: 16
|
||||
};
|
||||
|
||||
/**
|
||||
* Round list of values to the fixed precision.
|
||||
*
|
||||
* @example
|
||||
* <svg viewBox="0 0 200.28423 200.28423" enable-background="new 0 0 200.28423 200.28423">
|
||||
* ⬇
|
||||
* <svg viewBox="0 0 200.284 200.284" enable-background="new 0 0 200.284 200.284">
|
||||
*
|
||||
*
|
||||
* <polygon points="208.250977 77.1308594 223.069336 ... "/>
|
||||
* ⬇
|
||||
* <polygon points="208.251 77.131 223.069 ... "/>
|
||||
*
|
||||
*
|
||||
* @param {Object} item current iteration item
|
||||
* @param {Object} params plugin params
|
||||
* @return {Boolean} if false, item will be filtered out
|
||||
*
|
||||
* @author kiyopikko
|
||||
*/
|
||||
exports.fn = function(item, params) {
|
||||
|
||||
|
||||
if ( item.hasAttr('points') ) {
|
||||
roundValues(item.attrs.points);
|
||||
}
|
||||
|
||||
if ( item.hasAttr('enable-background') ) {
|
||||
roundValues(item.attrs['enable-background']);
|
||||
}
|
||||
|
||||
if ( item.hasAttr('viewBox') ) {
|
||||
roundValues(item.attrs.viewBox);
|
||||
}
|
||||
|
||||
if ( item.hasAttr('stroke-dasharray') ) {
|
||||
roundValues(item.attrs['stroke-dasharray']);
|
||||
}
|
||||
|
||||
if ( item.hasAttr('dx') ) {
|
||||
roundValues(item.attrs.dx);
|
||||
}
|
||||
|
||||
if ( item.hasAttr('dy') ) {
|
||||
roundValues(item.attrs.dy);
|
||||
}
|
||||
|
||||
if ( item.hasAttr('x') ) {
|
||||
roundValues(item.attrs.x);
|
||||
}
|
||||
|
||||
if ( item.hasAttr('y') ) {
|
||||
roundValues(item.attrs.y);
|
||||
}
|
||||
|
||||
|
||||
function roundValues($prop){
|
||||
|
||||
var num, units,
|
||||
match,
|
||||
matchNew,
|
||||
lists = $prop.value,
|
||||
listsArr = lists.split(regSeparator),
|
||||
roundedListArr = [],
|
||||
roundedList;
|
||||
|
||||
listsArr.forEach(function(elem){
|
||||
|
||||
match = elem.match(regNumericValues);
|
||||
matchNew = elem.match(/new/);
|
||||
|
||||
// if attribute value matches regNumericValues
|
||||
if (match) {
|
||||
// round it to the fixed precision
|
||||
num = +(+match[1]).toFixed(params.floatPrecision),
|
||||
units = match[3] || '';
|
||||
|
||||
// convert absolute values to pixels
|
||||
if (params.convertToPx && units && (units in absoluteLengths)) {
|
||||
var pxNum = +(absoluteLengths[units] * match[1]).toFixed(params.floatPrecision);
|
||||
|
||||
if (String(pxNum).length < match[0].length)
|
||||
num = pxNum,
|
||||
units = 'px';
|
||||
}
|
||||
|
||||
// and remove leading zero
|
||||
if (params.leadingZero) {
|
||||
num = removeLeadingZero(num);
|
||||
}
|
||||
|
||||
// remove default 'px' units
|
||||
if (params.defaultPx && units === 'px') {
|
||||
units = '';
|
||||
}
|
||||
|
||||
roundedListArr.push(num+units);
|
||||
}
|
||||
// if attribute value is "new"(only enable-background).
|
||||
else if (matchNew) {
|
||||
roundedListArr.push('new');
|
||||
} else if (elem) {
|
||||
roundedListArr.push(elem);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
roundedList = roundedListArr.join(' ');
|
||||
$prop.value = roundedList;
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
'use strict';
|
||||
|
||||
exports.type = 'perItem';
|
||||
|
||||
exports.active = true;
|
||||
|
||||
exports.description = 'rounds numeric values to the fixed precision, removes default ‘px’ units';
|
||||
|
||||
exports.params = {
|
||||
floatPrecision: 3,
|
||||
leadingZero: true,
|
||||
defaultPx: true,
|
||||
convertToPx: true
|
||||
};
|
||||
|
||||
var regNumericValues = /^([\-+]?\d*\.?\d+([eE][\-+]?\d+)?)(px|pt|pc|mm|cm|m|in|ft|em|ex|%)?$/,
|
||||
removeLeadingZero = require('../lib/svgo/tools').removeLeadingZero,
|
||||
absoluteLengths = { // relative to px
|
||||
cm: 96/2.54,
|
||||
mm: 96/25.4,
|
||||
in: 96,
|
||||
pt: 4/3,
|
||||
pc: 16
|
||||
};
|
||||
|
||||
/**
|
||||
* Round numeric values to the fixed precision,
|
||||
* remove default 'px' units.
|
||||
*
|
||||
* @param {Object} item current iteration item
|
||||
* @param {Object} params plugin params
|
||||
* @return {Boolean} if false, item will be filtered out
|
||||
*
|
||||
* @author Kir Belevich
|
||||
*/
|
||||
exports.fn = function(item, params) {
|
||||
|
||||
if (item.isElem()) {
|
||||
|
||||
var floatPrecision = params.floatPrecision;
|
||||
|
||||
if (item.hasAttr('viewBox')) {
|
||||
var nums = item.attr('viewBox').value.split(/\s,?\s*|,\s*/g);
|
||||
item.attr('viewBox').value = nums.map(function(value) {
|
||||
var num = +value;
|
||||
return isNaN(num) ? value : +num.toFixed(floatPrecision);
|
||||
}).join(' ');
|
||||
}
|
||||
|
||||
item.eachAttr(function(attr) {
|
||||
// The `version` attribute is a text string and cannot be rounded
|
||||
if (attr.name === 'version') { return }
|
||||
|
||||
var match = attr.value.match(regNumericValues);
|
||||
|
||||
// if attribute value matches regNumericValues
|
||||
if (match) {
|
||||
// round it to the fixed precision
|
||||
var num = +(+match[1]).toFixed(floatPrecision),
|
||||
units = match[3] || '';
|
||||
|
||||
// convert absolute values to pixels
|
||||
if (params.convertToPx && units && (units in absoluteLengths)) {
|
||||
var pxNum = +(absoluteLengths[units] * match[1]).toFixed(floatPrecision);
|
||||
|
||||
if (String(pxNum).length < match[0].length) {
|
||||
num = pxNum;
|
||||
units = 'px';
|
||||
}
|
||||
}
|
||||
|
||||
// and remove leading zero
|
||||
if (params.leadingZero) {
|
||||
num = removeLeadingZero(num);
|
||||
}
|
||||
|
||||
// remove default 'px' units
|
||||
if (params.defaultPx && units === 'px') {
|
||||
units = '';
|
||||
}
|
||||
|
||||
attr.value = num + units;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
'use strict';
|
||||
|
||||
exports.type = 'perItemReverse';
|
||||
|
||||
exports.active = true;
|
||||
|
||||
exports.description = 'collapses useless groups';
|
||||
|
||||
var collections = require('./_collections'),
|
||||
attrsInheritable = collections.inheritableAttrs,
|
||||
animationElems = collections.elemsGroups.animation;
|
||||
|
||||
function hasAnimatedAttr(item) {
|
||||
/* jshint validthis:true */
|
||||
return item.isElem(animationElems) && item.hasAttr('attributeName', this) ||
|
||||
!item.isEmpty() && item.content.some(hasAnimatedAttr, this);
|
||||
}
|
||||
|
||||
/*
|
||||
* Collapse useless groups.
|
||||
*
|
||||
* @example
|
||||
* <g>
|
||||
* <g attr1="val1">
|
||||
* <path d="..."/>
|
||||
* </g>
|
||||
* </g>
|
||||
* ⬇
|
||||
* <g>
|
||||
* <g>
|
||||
* <path attr1="val1" d="..."/>
|
||||
* </g>
|
||||
* </g>
|
||||
* ⬇
|
||||
* <path attr1="val1" d="..."/>
|
||||
*
|
||||
* @param {Object} item current iteration item
|
||||
* @return {Boolean} if false, item will be filtered out
|
||||
*
|
||||
* @author Kir Belevich
|
||||
*/
|
||||
exports.fn = function(item) {
|
||||
|
||||
// non-empty elements
|
||||
if (item.isElem() && !item.isElem('switch') && !item.isEmpty()) {
|
||||
item.content.forEach(function(g, i) {
|
||||
// non-empty groups
|
||||
if (g.isElem('g') && !g.isEmpty()) {
|
||||
// move group attibutes to the single content element
|
||||
if (g.hasAttr() && g.content.length === 1) {
|
||||
var inner = g.content[0];
|
||||
|
||||
if (inner.isElem() && !inner.hasAttr('id') && !g.hasAttr('filter') &&
|
||||
!(g.hasAttr('class') && inner.hasAttr('class')) && (
|
||||
!g.hasAttr('clip-path') && !g.hasAttr('mask') ||
|
||||
inner.isElem('g') && !g.hasAttr('transform') && !inner.hasAttr('transform')
|
||||
)
|
||||
) {
|
||||
g.eachAttr(function(attr) {
|
||||
if (g.content.some(hasAnimatedAttr, attr.name)) return;
|
||||
|
||||
if (!inner.hasAttr(attr.name)) {
|
||||
inner.addAttr(attr);
|
||||
} else if (attr.name == 'transform') {
|
||||
inner.attr(attr.name).value = attr.value + ' ' + inner.attr(attr.name).value;
|
||||
} else if (inner.hasAttr(attr.name, 'inherit')) {
|
||||
inner.attr(attr.name).value = attr.value;
|
||||
} else if (
|
||||
attrsInheritable.indexOf(attr.name) < 0 &&
|
||||
!inner.hasAttr(attr.name, attr.value)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
g.removeAttr(attr.name);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// collapse groups without attributes
|
||||
if (!g.hasAttr() && !g.content.some(function(item) { return item.isElem(animationElems) })) {
|
||||
item.spliceContent(i, 1, g.content);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
'use strict';
|
||||
|
||||
exports.type = 'perItem';
|
||||
|
||||
exports.active = true;
|
||||
|
||||
exports.description = 'converts colors: rgb() to #rrggbb and #rrggbb to #rgb';
|
||||
|
||||
exports.params = {
|
||||
currentColor: false,
|
||||
names2hex: true,
|
||||
rgb2hex: true,
|
||||
shorthex: true,
|
||||
shortname: true
|
||||
};
|
||||
|
||||
var collections = require('./_collections'),
|
||||
rNumber = '([+-]?(?:\\d*\\.\\d+|\\d+\\.?)%?)',
|
||||
rComma = '\\s*,\\s*',
|
||||
regRGB = new RegExp('^rgb\\(\\s*' + rNumber + rComma + rNumber + rComma + rNumber + '\\s*\\)$'),
|
||||
regHEX = /^\#(([a-fA-F0-9])\2){3}$/,
|
||||
none = /\bnone\b/i;
|
||||
|
||||
/**
|
||||
* Convert different colors formats in element attributes to hex.
|
||||
*
|
||||
* @see http://www.w3.org/TR/SVG/types.html#DataTypeColor
|
||||
* @see http://www.w3.org/TR/SVG/single-page.html#types-ColorKeywords
|
||||
*
|
||||
* @example
|
||||
* Convert color name keyword to long hex:
|
||||
* fuchsia ➡ #ff00ff
|
||||
*
|
||||
* Convert rgb() to long hex:
|
||||
* rgb(255, 0, 255) ➡ #ff00ff
|
||||
* rgb(50%, 100, 100%) ➡ #7f64ff
|
||||
*
|
||||
* Convert long hex to short hex:
|
||||
* #aabbcc ➡ #abc
|
||||
*
|
||||
* Convert hex to short name
|
||||
* #000080 ➡ navy
|
||||
*
|
||||
* @param {Object} item current iteration item
|
||||
* @param {Object} params plugin params
|
||||
* @return {Boolean} if false, item will be filtered out
|
||||
*
|
||||
* @author Kir Belevich
|
||||
*/
|
||||
exports.fn = function(item, params) {
|
||||
|
||||
if (item.elem) {
|
||||
|
||||
item.eachAttr(function(attr) {
|
||||
|
||||
if (collections.colorsProps.indexOf(attr.name) > -1) {
|
||||
|
||||
var val = attr.value,
|
||||
match;
|
||||
|
||||
// Convert colors to currentColor
|
||||
if (params.currentColor) {
|
||||
if (typeof params.currentColor === 'string') {
|
||||
match = val === params.currentColor;
|
||||
} else if (params.currentColor.exec) {
|
||||
match = params.currentColor.exec(val);
|
||||
} else {
|
||||
match = !val.match(none);
|
||||
}
|
||||
if (match) {
|
||||
val = 'currentColor';
|
||||
}
|
||||
}
|
||||
|
||||
// Convert color name keyword to long hex
|
||||
if (params.names2hex && val.toLowerCase() in collections.colorsNames) {
|
||||
val = collections.colorsNames[val.toLowerCase()];
|
||||
}
|
||||
|
||||
// Convert rgb() to long hex
|
||||
if (params.rgb2hex && (match = val.match(regRGB))) {
|
||||
match = match.slice(1, 4).map(function(m) {
|
||||
if (m.indexOf('%') > -1)
|
||||
m = Math.round(parseFloat(m) * 2.55);
|
||||
|
||||
return Math.max(0, Math.min(m, 255));
|
||||
});
|
||||
|
||||
val = rgb2hex(match);
|
||||
}
|
||||
|
||||
// Convert long hex to short hex
|
||||
if (params.shorthex && (match = val.match(regHEX))) {
|
||||
val = '#' + match[0][1] + match[0][3] + match[0][5];
|
||||
}
|
||||
|
||||
// Convert hex to short name
|
||||
if (params.shortname) {
|
||||
var lowerVal = val.toLowerCase();
|
||||
if (lowerVal in collections.colorsShortNames) {
|
||||
val = collections.colorsShortNames[lowerVal];
|
||||
}
|
||||
}
|
||||
|
||||
attr.value = val;
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert [r, g, b] to #rrggbb.
|
||||
*
|
||||
* @see https://gist.github.com/983535
|
||||
*
|
||||
* @example
|
||||
* rgb2hex([255, 255, 255]) // '#ffffff'
|
||||
*
|
||||
* @param {Array} rgb [r, g, b]
|
||||
* @return {String} #rrggbb
|
||||
*
|
||||
* @author Jed Schmidt
|
||||
*/
|
||||
function rgb2hex(rgb) {
|
||||
return '#' + ('00000' + (rgb[0] << 16 | rgb[1] << 8 | rgb[2]).toString(16)).slice(-6).toUpperCase();
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
'use strict';
|
||||
|
||||
exports.type = 'perItem';
|
||||
|
||||
exports.active = true;
|
||||
|
||||
exports.description = 'converts non-eccentric <ellipse>s to <circle>s';
|
||||
|
||||
/**
|
||||
* Converts non-eccentric <ellipse>s to <circle>s.
|
||||
*
|
||||
* @see http://www.w3.org/TR/SVG/shapes.html
|
||||
*
|
||||
* @param {Object} item current iteration item
|
||||
* @return {Boolean} if false, item will be filtered out
|
||||
*
|
||||
* @author Taylor Hunt
|
||||
*/
|
||||
exports.fn = function(item) {
|
||||
if (item.isElem('ellipse')) {
|
||||
var rx = item.attr('rx').value || 0;
|
||||
var ry = item.attr('ry').value || 0;
|
||||
|
||||
if (rx === ry ||
|
||||
rx === 'auto' || ry === 'auto' // SVG2
|
||||
) {
|
||||
var radius = rx !== 'auto' ? rx : ry;
|
||||
item.renameElem('circle');
|
||||
item.removeAttr(['rx', 'ry']);
|
||||
item.addAttr({
|
||||
name: 'r',
|
||||
value: radius,
|
||||
prefix: '',
|
||||
local: 'r',
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
};
|
||||
+971
@@ -0,0 +1,971 @@
|
||||
'use strict';
|
||||
|
||||
exports.type = 'perItem';
|
||||
|
||||
exports.active = true;
|
||||
|
||||
exports.description = 'optimizes path data: writes in shorter form, applies transformations';
|
||||
|
||||
exports.params = {
|
||||
applyTransforms: true,
|
||||
applyTransformsStroked: true,
|
||||
makeArcs: {
|
||||
threshold: 2.5, // coefficient of rounding error
|
||||
tolerance: 0.5 // percentage of radius
|
||||
},
|
||||
straightCurves: true,
|
||||
lineShorthands: true,
|
||||
curveSmoothShorthands: true,
|
||||
floatPrecision: 3,
|
||||
transformPrecision: 5,
|
||||
removeUseless: true,
|
||||
collapseRepeated: true,
|
||||
utilizeAbsolute: true,
|
||||
leadingZero: true,
|
||||
negativeExtraSpace: true,
|
||||
noSpaceAfterFlags: true,
|
||||
forceAbsolutePath: false
|
||||
};
|
||||
|
||||
var pathElems = require('./_collections.js').pathElems,
|
||||
path2js = require('./_path.js').path2js,
|
||||
js2path = require('./_path.js').js2path,
|
||||
applyTransforms = require('./_path.js').applyTransforms,
|
||||
cleanupOutData = require('../lib/svgo/tools').cleanupOutData,
|
||||
roundData,
|
||||
precision,
|
||||
error,
|
||||
arcThreshold,
|
||||
arcTolerance,
|
||||
hasMarkerMid,
|
||||
hasStrokeLinecap;
|
||||
|
||||
/**
|
||||
* Convert absolute Path to relative,
|
||||
* collapse repeated instructions,
|
||||
* detect and convert Lineto shorthands,
|
||||
* remove useless instructions like "l0,0",
|
||||
* trim useless delimiters and leading zeros,
|
||||
* decrease accuracy of floating-point numbers.
|
||||
*
|
||||
* @see http://www.w3.org/TR/SVG/paths.html#PathData
|
||||
*
|
||||
* @param {Object} item current iteration item
|
||||
* @param {Object} params plugin params
|
||||
* @return {Boolean} if false, item will be filtered out
|
||||
*
|
||||
* @author Kir Belevich
|
||||
*/
|
||||
exports.fn = function(item, params) {
|
||||
|
||||
if (item.isElem(pathElems) && item.hasAttr('d')) {
|
||||
|
||||
precision = params.floatPrecision;
|
||||
error = precision !== false ? +Math.pow(.1, precision).toFixed(precision) : 1e-2;
|
||||
roundData = precision > 0 && precision < 20 ? strongRound : round;
|
||||
if (params.makeArcs) {
|
||||
arcThreshold = params.makeArcs.threshold;
|
||||
arcTolerance = params.makeArcs.tolerance;
|
||||
}
|
||||
hasMarkerMid = item.hasAttr('marker-mid');
|
||||
|
||||
var stroke = item.computedAttr('stroke'),
|
||||
strokeLinecap = item.computedAttr('stroke');
|
||||
hasStrokeLinecap = stroke && stroke != 'none' && strokeLinecap && strokeLinecap != 'butt';
|
||||
|
||||
var data = path2js(item);
|
||||
|
||||
// TODO: get rid of functions returns
|
||||
if (data.length) {
|
||||
convertToRelative(data);
|
||||
|
||||
if (params.applyTransforms) {
|
||||
data = applyTransforms(item, data, params);
|
||||
}
|
||||
|
||||
data = filters(data, params);
|
||||
|
||||
if (params.utilizeAbsolute) {
|
||||
data = convertToMixed(data, params);
|
||||
}
|
||||
|
||||
js2path(item, data, params);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert absolute path data coordinates to relative.
|
||||
*
|
||||
* @param {Array} path input path data
|
||||
* @param {Object} params plugin params
|
||||
* @return {Array} output path data
|
||||
*/
|
||||
function convertToRelative(path) {
|
||||
|
||||
var point = [0, 0],
|
||||
subpathPoint = [0, 0],
|
||||
baseItem;
|
||||
|
||||
path.forEach(function(item, index) {
|
||||
|
||||
var instruction = item.instruction,
|
||||
data = item.data;
|
||||
|
||||
// data !== !z
|
||||
if (data) {
|
||||
|
||||
// already relative
|
||||
// recalculate current point
|
||||
if ('mcslqta'.indexOf(instruction) > -1) {
|
||||
|
||||
point[0] += data[data.length - 2];
|
||||
point[1] += data[data.length - 1];
|
||||
|
||||
if (instruction === 'm') {
|
||||
subpathPoint[0] = point[0];
|
||||
subpathPoint[1] = point[1];
|
||||
baseItem = item;
|
||||
}
|
||||
|
||||
} else if (instruction === 'h') {
|
||||
|
||||
point[0] += data[0];
|
||||
|
||||
} else if (instruction === 'v') {
|
||||
|
||||
point[1] += data[0];
|
||||
|
||||
}
|
||||
|
||||
// convert absolute path data coordinates to relative
|
||||
// if "M" was not transformed from "m"
|
||||
// M → m
|
||||
if (instruction === 'M') {
|
||||
|
||||
if (index > 0) instruction = 'm';
|
||||
|
||||
data[0] -= point[0];
|
||||
data[1] -= point[1];
|
||||
|
||||
subpathPoint[0] = point[0] += data[0];
|
||||
subpathPoint[1] = point[1] += data[1];
|
||||
|
||||
baseItem = item;
|
||||
|
||||
}
|
||||
|
||||
// L → l
|
||||
// T → t
|
||||
else if ('LT'.indexOf(instruction) > -1) {
|
||||
|
||||
instruction = instruction.toLowerCase();
|
||||
|
||||
// x y
|
||||
// 0 1
|
||||
data[0] -= point[0];
|
||||
data[1] -= point[1];
|
||||
|
||||
point[0] += data[0];
|
||||
point[1] += data[1];
|
||||
|
||||
// C → c
|
||||
} else if (instruction === 'C') {
|
||||
|
||||
instruction = 'c';
|
||||
|
||||
// x1 y1 x2 y2 x y
|
||||
// 0 1 2 3 4 5
|
||||
data[0] -= point[0];
|
||||
data[1] -= point[1];
|
||||
data[2] -= point[0];
|
||||
data[3] -= point[1];
|
||||
data[4] -= point[0];
|
||||
data[5] -= point[1];
|
||||
|
||||
point[0] += data[4];
|
||||
point[1] += data[5];
|
||||
|
||||
// S → s
|
||||
// Q → q
|
||||
} else if ('SQ'.indexOf(instruction) > -1) {
|
||||
|
||||
instruction = instruction.toLowerCase();
|
||||
|
||||
// x1 y1 x y
|
||||
// 0 1 2 3
|
||||
data[0] -= point[0];
|
||||
data[1] -= point[1];
|
||||
data[2] -= point[0];
|
||||
data[3] -= point[1];
|
||||
|
||||
point[0] += data[2];
|
||||
point[1] += data[3];
|
||||
|
||||
// A → a
|
||||
} else if (instruction === 'A') {
|
||||
|
||||
instruction = 'a';
|
||||
|
||||
// rx ry x-axis-rotation large-arc-flag sweep-flag x y
|
||||
// 0 1 2 3 4 5 6
|
||||
data[5] -= point[0];
|
||||
data[6] -= point[1];
|
||||
|
||||
point[0] += data[5];
|
||||
point[1] += data[6];
|
||||
|
||||
// H → h
|
||||
} else if (instruction === 'H') {
|
||||
|
||||
instruction = 'h';
|
||||
|
||||
data[0] -= point[0];
|
||||
|
||||
point[0] += data[0];
|
||||
|
||||
// V → v
|
||||
} else if (instruction === 'V') {
|
||||
|
||||
instruction = 'v';
|
||||
|
||||
data[0] -= point[1];
|
||||
|
||||
point[1] += data[0];
|
||||
|
||||
}
|
||||
|
||||
item.instruction = instruction;
|
||||
item.data = data;
|
||||
|
||||
// store absolute coordinates for later use
|
||||
item.coords = point.slice(-2);
|
||||
|
||||
}
|
||||
|
||||
// !data === z, reset current point
|
||||
else if (instruction == 'z') {
|
||||
if (baseItem) {
|
||||
item.coords = baseItem.coords;
|
||||
}
|
||||
point[0] = subpathPoint[0];
|
||||
point[1] = subpathPoint[1];
|
||||
}
|
||||
|
||||
item.base = index > 0 ? path[index - 1].coords : [0, 0];
|
||||
|
||||
});
|
||||
|
||||
return path;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Main filters loop.
|
||||
*
|
||||
* @param {Array} path input path data
|
||||
* @param {Object} params plugin params
|
||||
* @return {Array} output path data
|
||||
*/
|
||||
function filters(path, params) {
|
||||
|
||||
var stringify = data2Path.bind(null, params),
|
||||
relSubpoint = [0, 0],
|
||||
pathBase = [0, 0],
|
||||
prev = {};
|
||||
|
||||
path = path.filter(function(item, index, path) {
|
||||
|
||||
var instruction = item.instruction,
|
||||
data = item.data,
|
||||
next = path[index + 1];
|
||||
|
||||
if (data) {
|
||||
|
||||
var sdata = data,
|
||||
circle;
|
||||
|
||||
if (instruction === 's') {
|
||||
sdata = [0, 0].concat(data);
|
||||
|
||||
if ('cs'.indexOf(prev.instruction) > -1) {
|
||||
var pdata = prev.data,
|
||||
n = pdata.length;
|
||||
|
||||
// (-x, -y) of the prev tangent point relative to the current point
|
||||
sdata[0] = pdata[n - 2] - pdata[n - 4];
|
||||
sdata[1] = pdata[n - 1] - pdata[n - 3];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// convert curves to arcs if possible
|
||||
if (
|
||||
params.makeArcs &&
|
||||
(instruction == 'c' || instruction == 's') &&
|
||||
isConvex(sdata) &&
|
||||
(circle = findCircle(sdata))
|
||||
) {
|
||||
var r = roundData([circle.radius])[0],
|
||||
angle = findArcAngle(sdata, circle),
|
||||
sweep = sdata[5] * sdata[0] - sdata[4] * sdata[1] > 0 ? 1 : 0,
|
||||
arc = {
|
||||
instruction: 'a',
|
||||
data: [r, r, 0, 0, sweep, sdata[4], sdata[5]],
|
||||
coords: item.coords.slice(),
|
||||
base: item.base
|
||||
},
|
||||
output = [arc],
|
||||
// relative coordinates to adjust the found circle
|
||||
relCenter = [circle.center[0] - sdata[4], circle.center[1] - sdata[5]],
|
||||
relCircle = { center: relCenter, radius: circle.radius },
|
||||
arcCurves = [item],
|
||||
hasPrev = 0,
|
||||
suffix = '',
|
||||
nextLonghand;
|
||||
|
||||
if (
|
||||
prev.instruction == 'c' && isConvex(prev.data) && isArcPrev(prev.data, circle) ||
|
||||
prev.instruction == 'a' && prev.sdata && isArcPrev(prev.sdata, circle)
|
||||
) {
|
||||
arcCurves.unshift(prev);
|
||||
arc.base = prev.base;
|
||||
arc.data[5] = arc.coords[0] - arc.base[0];
|
||||
arc.data[6] = arc.coords[1] - arc.base[1];
|
||||
var prevData = prev.instruction == 'a' ? prev.sdata : prev.data;
|
||||
var prevAngle = findArcAngle(prevData,
|
||||
{
|
||||
center: [prevData[4] + circle.center[0], prevData[5] + circle.center[1]],
|
||||
radius: circle.radius
|
||||
}
|
||||
);
|
||||
angle += prevAngle;
|
||||
if (angle > Math.PI) arc.data[3] = 1;
|
||||
hasPrev = 1;
|
||||
}
|
||||
|
||||
// check if next curves are fitting the arc
|
||||
for (var j = index; (next = path[++j]) && ~'cs'.indexOf(next.instruction);) {
|
||||
var nextData = next.data;
|
||||
if (next.instruction == 's') {
|
||||
nextLonghand = makeLonghand({instruction: 's', data: next.data.slice() },
|
||||
path[j - 1].data);
|
||||
nextData = nextLonghand.data;
|
||||
nextLonghand.data = nextData.slice(0, 2);
|
||||
suffix = stringify([nextLonghand]);
|
||||
}
|
||||
if (isConvex(nextData) && isArc(nextData, relCircle)) {
|
||||
angle += findArcAngle(nextData, relCircle);
|
||||
if (angle - 2 * Math.PI > 1e-3) break; // more than 360°
|
||||
if (angle > Math.PI) arc.data[3] = 1;
|
||||
arcCurves.push(next);
|
||||
if (2 * Math.PI - angle > 1e-3) { // less than 360°
|
||||
arc.coords = next.coords;
|
||||
arc.data[5] = arc.coords[0] - arc.base[0];
|
||||
arc.data[6] = arc.coords[1] - arc.base[1];
|
||||
} else {
|
||||
// full circle, make a half-circle arc and add a second one
|
||||
arc.data[5] = 2 * (relCircle.center[0] - nextData[4]);
|
||||
arc.data[6] = 2 * (relCircle.center[1] - nextData[5]);
|
||||
arc.coords = [arc.base[0] + arc.data[5], arc.base[1] + arc.data[6]];
|
||||
arc = {
|
||||
instruction: 'a',
|
||||
data: [r, r, 0, 0, sweep,
|
||||
next.coords[0] - arc.coords[0], next.coords[1] - arc.coords[1]],
|
||||
coords: next.coords,
|
||||
base: arc.coords
|
||||
};
|
||||
output.push(arc);
|
||||
j++;
|
||||
break;
|
||||
}
|
||||
relCenter[0] -= nextData[4];
|
||||
relCenter[1] -= nextData[5];
|
||||
} else break;
|
||||
}
|
||||
|
||||
if ((stringify(output) + suffix).length < stringify(arcCurves).length) {
|
||||
if (path[j] && path[j].instruction == 's') {
|
||||
makeLonghand(path[j], path[j - 1].data);
|
||||
}
|
||||
if (hasPrev) {
|
||||
var prevArc = output.shift();
|
||||
roundData(prevArc.data);
|
||||
relSubpoint[0] += prevArc.data[5] - prev.data[prev.data.length - 2];
|
||||
relSubpoint[1] += prevArc.data[6] - prev.data[prev.data.length - 1];
|
||||
prev.instruction = 'a';
|
||||
prev.data = prevArc.data;
|
||||
item.base = prev.coords = prevArc.coords;
|
||||
}
|
||||
arc = output.shift();
|
||||
if (arcCurves.length == 1) {
|
||||
item.sdata = sdata.slice(); // preserve curve data for future checks
|
||||
} else if (arcCurves.length - 1 - hasPrev > 0) {
|
||||
// filter out consumed next items
|
||||
path.splice.apply(path, [index + 1, arcCurves.length - 1 - hasPrev].concat(output));
|
||||
}
|
||||
if (!arc) return false;
|
||||
instruction = 'a';
|
||||
data = arc.data;
|
||||
item.coords = arc.coords;
|
||||
}
|
||||
}
|
||||
|
||||
// Rounding relative coordinates, taking in account accummulating error
|
||||
// to get closer to absolute coordinates. Sum of rounded value remains same:
|
||||
// l .25 3 .25 2 .25 3 .25 2 -> l .3 3 .2 2 .3 3 .2 2
|
||||
if (precision !== false) {
|
||||
if ('mltqsc'.indexOf(instruction) > -1) {
|
||||
for (var i = data.length; i--;) {
|
||||
data[i] += item.base[i % 2] - relSubpoint[i % 2];
|
||||
}
|
||||
} else if (instruction == 'h') {
|
||||
data[0] += item.base[0] - relSubpoint[0];
|
||||
} else if (instruction == 'v') {
|
||||
data[0] += item.base[1] - relSubpoint[1];
|
||||
} else if (instruction == 'a') {
|
||||
data[5] += item.base[0] - relSubpoint[0];
|
||||
data[6] += item.base[1] - relSubpoint[1];
|
||||
}
|
||||
roundData(data);
|
||||
|
||||
if (instruction == 'h') relSubpoint[0] += data[0];
|
||||
else if (instruction == 'v') relSubpoint[1] += data[0];
|
||||
else {
|
||||
relSubpoint[0] += data[data.length - 2];
|
||||
relSubpoint[1] += data[data.length - 1];
|
||||
}
|
||||
roundData(relSubpoint);
|
||||
|
||||
if (instruction.toLowerCase() == 'm') {
|
||||
pathBase[0] = relSubpoint[0];
|
||||
pathBase[1] = relSubpoint[1];
|
||||
}
|
||||
}
|
||||
|
||||
// convert straight curves into lines segments
|
||||
if (params.straightCurves) {
|
||||
|
||||
if (
|
||||
instruction === 'c' &&
|
||||
isCurveStraightLine(data) ||
|
||||
instruction === 's' &&
|
||||
isCurveStraightLine(sdata)
|
||||
) {
|
||||
if (next && next.instruction == 's')
|
||||
makeLonghand(next, data); // fix up next curve
|
||||
instruction = 'l';
|
||||
data = data.slice(-2);
|
||||
}
|
||||
|
||||
else if (
|
||||
instruction === 'q' &&
|
||||
isCurveStraightLine(data)
|
||||
) {
|
||||
if (next && next.instruction == 't')
|
||||
makeLonghand(next, data); // fix up next curve
|
||||
instruction = 'l';
|
||||
data = data.slice(-2);
|
||||
}
|
||||
|
||||
else if (
|
||||
instruction === 't' &&
|
||||
prev.instruction !== 'q' &&
|
||||
prev.instruction !== 't'
|
||||
) {
|
||||
instruction = 'l';
|
||||
data = data.slice(-2);
|
||||
}
|
||||
|
||||
else if (
|
||||
instruction === 'a' &&
|
||||
(data[0] === 0 || data[1] === 0)
|
||||
) {
|
||||
instruction = 'l';
|
||||
data = data.slice(-2);
|
||||
}
|
||||
}
|
||||
|
||||
// horizontal and vertical line shorthands
|
||||
// l 50 0 → h 50
|
||||
// l 0 50 → v 50
|
||||
if (
|
||||
params.lineShorthands &&
|
||||
instruction === 'l'
|
||||
) {
|
||||
if (data[1] === 0) {
|
||||
instruction = 'h';
|
||||
data.pop();
|
||||
} else if (data[0] === 0) {
|
||||
instruction = 'v';
|
||||
data.shift();
|
||||
}
|
||||
}
|
||||
|
||||
// collapse repeated commands
|
||||
// h 20 h 30 -> h 50
|
||||
if (
|
||||
params.collapseRepeated &&
|
||||
!hasMarkerMid &&
|
||||
('mhv'.indexOf(instruction) > -1) &&
|
||||
prev.instruction &&
|
||||
instruction == prev.instruction.toLowerCase() &&
|
||||
(
|
||||
(instruction != 'h' && instruction != 'v') ||
|
||||
(prev.data[0] >= 0) == (item.data[0] >= 0)
|
||||
)) {
|
||||
prev.data[0] += data[0];
|
||||
if (instruction != 'h' && instruction != 'v') {
|
||||
prev.data[1] += data[1];
|
||||
}
|
||||
prev.coords = item.coords;
|
||||
path[index] = prev;
|
||||
return false;
|
||||
}
|
||||
|
||||
// convert curves into smooth shorthands
|
||||
if (params.curveSmoothShorthands && prev.instruction) {
|
||||
|
||||
// curveto
|
||||
if (instruction === 'c') {
|
||||
|
||||
// c + c → c + s
|
||||
if (
|
||||
prev.instruction === 'c' &&
|
||||
data[0] === -(prev.data[2] - prev.data[4]) &&
|
||||
data[1] === -(prev.data[3] - prev.data[5])
|
||||
) {
|
||||
instruction = 's';
|
||||
data = data.slice(2);
|
||||
}
|
||||
|
||||
// s + c → s + s
|
||||
else if (
|
||||
prev.instruction === 's' &&
|
||||
data[0] === -(prev.data[0] - prev.data[2]) &&
|
||||
data[1] === -(prev.data[1] - prev.data[3])
|
||||
) {
|
||||
instruction = 's';
|
||||
data = data.slice(2);
|
||||
}
|
||||
|
||||
// [^cs] + c → [^cs] + s
|
||||
else if (
|
||||
'cs'.indexOf(prev.instruction) === -1 &&
|
||||
data[0] === 0 &&
|
||||
data[1] === 0
|
||||
) {
|
||||
instruction = 's';
|
||||
data = data.slice(2);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// quadratic Bézier curveto
|
||||
else if (instruction === 'q') {
|
||||
|
||||
// q + q → q + t
|
||||
if (
|
||||
prev.instruction === 'q' &&
|
||||
data[0] === (prev.data[2] - prev.data[0]) &&
|
||||
data[1] === (prev.data[3] - prev.data[1])
|
||||
) {
|
||||
instruction = 't';
|
||||
data = data.slice(2);
|
||||
}
|
||||
|
||||
// t + q → t + t
|
||||
else if (
|
||||
prev.instruction === 't' &&
|
||||
data[2] === prev.data[0] &&
|
||||
data[3] === prev.data[1]
|
||||
) {
|
||||
instruction = 't';
|
||||
data = data.slice(2);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// remove useless non-first path segments
|
||||
if (params.removeUseless && !hasStrokeLinecap) {
|
||||
|
||||
// l 0,0 / h 0 / v 0 / q 0,0 0,0 / t 0,0 / c 0,0 0,0 0,0 / s 0,0 0,0
|
||||
if (
|
||||
(
|
||||
'lhvqtcs'.indexOf(instruction) > -1
|
||||
) &&
|
||||
data.every(function(i) { return i === 0; })
|
||||
) {
|
||||
path[index] = prev;
|
||||
return false;
|
||||
}
|
||||
|
||||
// a 25,25 -30 0,1 0,0
|
||||
if (
|
||||
instruction === 'a' &&
|
||||
data[5] === 0 &&
|
||||
data[6] === 0
|
||||
) {
|
||||
path[index] = prev;
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
item.instruction = instruction;
|
||||
item.data = data;
|
||||
|
||||
prev = item;
|
||||
|
||||
} else {
|
||||
|
||||
// z resets coordinates
|
||||
relSubpoint[0] = pathBase[0];
|
||||
relSubpoint[1] = pathBase[1];
|
||||
if (prev.instruction == 'z') return false;
|
||||
prev = item;
|
||||
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
});
|
||||
|
||||
return path;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes data in shortest form using absolute or relative coordinates.
|
||||
*
|
||||
* @param {Array} data input path data
|
||||
* @return {Boolean} output
|
||||
*/
|
||||
function convertToMixed(path, params) {
|
||||
|
||||
var prev = path[0];
|
||||
|
||||
path = path.filter(function(item, index) {
|
||||
|
||||
if (index == 0) return true;
|
||||
if (!item.data) {
|
||||
prev = item;
|
||||
return true;
|
||||
}
|
||||
|
||||
var instruction = item.instruction,
|
||||
data = item.data,
|
||||
adata = data && data.slice(0);
|
||||
|
||||
if ('mltqsc'.indexOf(instruction) > -1) {
|
||||
for (var i = adata.length; i--;) {
|
||||
adata[i] += item.base[i % 2];
|
||||
}
|
||||
} else if (instruction == 'h') {
|
||||
adata[0] += item.base[0];
|
||||
} else if (instruction == 'v') {
|
||||
adata[0] += item.base[1];
|
||||
} else if (instruction == 'a') {
|
||||
adata[5] += item.base[0];
|
||||
adata[6] += item.base[1];
|
||||
}
|
||||
|
||||
roundData(adata);
|
||||
|
||||
var absoluteDataStr = cleanupOutData(adata, params),
|
||||
relativeDataStr = cleanupOutData(data, params);
|
||||
|
||||
// Convert to absolute coordinates if it's shorter or forceAbsolutePath is true.
|
||||
// v-20 -> V0
|
||||
// Don't convert if it fits following previous instruction.
|
||||
// l20 30-10-50 instead of l20 30L20 30
|
||||
if (
|
||||
params.forceAbsolutePath || (
|
||||
absoluteDataStr.length < relativeDataStr.length &&
|
||||
!(
|
||||
params.negativeExtraSpace &&
|
||||
instruction == prev.instruction &&
|
||||
prev.instruction.charCodeAt(0) > 96 &&
|
||||
absoluteDataStr.length == relativeDataStr.length - 1 &&
|
||||
(data[0] < 0 || /^0\./.test(data[0]) && prev.data[prev.data.length - 1] % 1)
|
||||
))
|
||||
) {
|
||||
item.instruction = instruction.toUpperCase();
|
||||
item.data = adata;
|
||||
}
|
||||
|
||||
prev = item;
|
||||
|
||||
return true;
|
||||
|
||||
});
|
||||
|
||||
return path;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if curve is convex. Control points of such a curve must form
|
||||
* a convex quadrilateral with diagonals crosspoint inside of it.
|
||||
*
|
||||
* @param {Array} data input path data
|
||||
* @return {Boolean} output
|
||||
*/
|
||||
function isConvex(data) {
|
||||
|
||||
var center = getIntersection([0, 0, data[2], data[3], data[0], data[1], data[4], data[5]]);
|
||||
|
||||
return center &&
|
||||
(data[2] < center[0] == center[0] < 0) &&
|
||||
(data[3] < center[1] == center[1] < 0) &&
|
||||
(data[4] < center[0] == center[0] < data[0]) &&
|
||||
(data[5] < center[1] == center[1] < data[1]);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes lines equations by two points and returns their intersection point.
|
||||
*
|
||||
* @param {Array} coords 8 numbers for 4 pairs of coordinates (x,y)
|
||||
* @return {Array|undefined} output coordinate of lines' crosspoint
|
||||
*/
|
||||
function getIntersection(coords) {
|
||||
|
||||
// Prev line equation parameters.
|
||||
var a1 = coords[1] - coords[3], // y1 - y2
|
||||
b1 = coords[2] - coords[0], // x2 - x1
|
||||
c1 = coords[0] * coords[3] - coords[2] * coords[1], // x1 * y2 - x2 * y1
|
||||
|
||||
// Next line equation parameters
|
||||
a2 = coords[5] - coords[7], // y1 - y2
|
||||
b2 = coords[6] - coords[4], // x2 - x1
|
||||
c2 = coords[4] * coords[7] - coords[5] * coords[6], // x1 * y2 - x2 * y1
|
||||
denom = (a1 * b2 - a2 * b1);
|
||||
|
||||
if (!denom) return; // parallel lines havn't an intersection
|
||||
|
||||
var cross = [
|
||||
(b1 * c2 - b2 * c1) / denom,
|
||||
(a1 * c2 - a2 * c1) / -denom
|
||||
];
|
||||
if (
|
||||
!isNaN(cross[0]) && !isNaN(cross[1]) &&
|
||||
isFinite(cross[0]) && isFinite(cross[1])
|
||||
) {
|
||||
return cross;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrease accuracy of floating-point numbers
|
||||
* in path data keeping a specified number of decimals.
|
||||
* Smart rounds values like 2.3491 to 2.35 instead of 2.349.
|
||||
* Doesn't apply "smartness" if the number precision fits already.
|
||||
*
|
||||
* @param {Array} data input data array
|
||||
* @return {Array} output data array
|
||||
*/
|
||||
function strongRound(data) {
|
||||
for (var i = data.length; i-- > 0;) {
|
||||
if (data[i].toFixed(precision) != data[i]) {
|
||||
var rounded = +data[i].toFixed(precision - 1);
|
||||
data[i] = +Math.abs(rounded - data[i]).toFixed(precision + 1) >= error ?
|
||||
+data[i].toFixed(precision) :
|
||||
rounded;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple rounding function if precision is 0.
|
||||
*
|
||||
* @param {Array} data input data array
|
||||
* @return {Array} output data array
|
||||
*/
|
||||
function round(data) {
|
||||
for (var i = data.length; i-- > 0;) {
|
||||
data[i] = Math.round(data[i]);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a curve is a straight line by measuring distance
|
||||
* from middle points to the line formed by end points.
|
||||
*
|
||||
* @param {Array} xs array of curve points x-coordinates
|
||||
* @param {Array} ys array of curve points y-coordinates
|
||||
* @return {Boolean}
|
||||
*/
|
||||
|
||||
function isCurveStraightLine(data) {
|
||||
|
||||
// Get line equation a·x + b·y + c = 0 coefficients a, b (c = 0) by start and end points.
|
||||
var i = data.length - 2,
|
||||
a = -data[i + 1], // y1 − y2 (y1 = 0)
|
||||
b = data[i], // x2 − x1 (x1 = 0)
|
||||
d = 1 / (a * a + b * b); // same part for all points
|
||||
|
||||
if (i <= 1 || !isFinite(d)) return false; // curve that ends at start point isn't the case
|
||||
|
||||
// Distance from point (x0, y0) to the line is sqrt((c − a·x0 − b·y0)² / (a² + b²))
|
||||
while ((i -= 2) >= 0) {
|
||||
if (Math.sqrt(Math.pow(a * data[i] + b * data[i + 1], 2) * d) > error)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts next curve from shorthand to full form using the current curve data.
|
||||
*
|
||||
* @param {Object} item curve to convert
|
||||
* @param {Array} data current curve data
|
||||
*/
|
||||
|
||||
function makeLonghand(item, data) {
|
||||
switch (item.instruction) {
|
||||
case 's': item.instruction = 'c'; break;
|
||||
case 't': item.instruction = 'q'; break;
|
||||
}
|
||||
item.data.unshift(data[data.length - 2] - data[data.length - 4], data[data.length - 1] - data[data.length - 3]);
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns distance between two points
|
||||
*
|
||||
* @param {Array} point1 first point coordinates
|
||||
* @param {Array} point2 second point coordinates
|
||||
* @return {Number} distance
|
||||
*/
|
||||
|
||||
function getDistance(point1, point2) {
|
||||
return Math.hypot(point1[0] - point2[0], point1[1] - point2[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns coordinates of the curve point corresponding to the certain t
|
||||
* a·(1 - t)³·p1 + b·(1 - t)²·t·p2 + c·(1 - t)·t²·p3 + d·t³·p4,
|
||||
* where pN are control points and p1 is zero due to relative coordinates.
|
||||
*
|
||||
* @param {Array} curve array of curve points coordinates
|
||||
* @param {Number} t parametric position from 0 to 1
|
||||
* @return {Array} Point coordinates
|
||||
*/
|
||||
|
||||
function getCubicBezierPoint(curve, t) {
|
||||
var sqrT = t * t,
|
||||
cubT = sqrT * t,
|
||||
mt = 1 - t,
|
||||
sqrMt = mt * mt;
|
||||
|
||||
return [
|
||||
3 * sqrMt * t * curve[0] + 3 * mt * sqrT * curve[2] + cubT * curve[4],
|
||||
3 * sqrMt * t * curve[1] + 3 * mt * sqrT * curve[3] + cubT * curve[5]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds circle by 3 points of the curve and checks if the curve fits the found circle.
|
||||
*
|
||||
* @param {Array} curve
|
||||
* @return {Object|undefined} circle
|
||||
*/
|
||||
|
||||
function findCircle(curve) {
|
||||
var midPoint = getCubicBezierPoint(curve, 1/2),
|
||||
m1 = [midPoint[0] / 2, midPoint[1] / 2],
|
||||
m2 = [(midPoint[0] + curve[4]) / 2, (midPoint[1] + curve[5]) / 2],
|
||||
center = getIntersection([
|
||||
m1[0], m1[1],
|
||||
m1[0] + m1[1], m1[1] - m1[0],
|
||||
m2[0], m2[1],
|
||||
m2[0] + (m2[1] - midPoint[1]), m2[1] - (m2[0] - midPoint[0])
|
||||
]),
|
||||
radius = center && getDistance([0, 0], center),
|
||||
tolerance = Math.min(arcThreshold * error, arcTolerance * radius / 100);
|
||||
|
||||
if (center && radius < 1e15 &&
|
||||
[1/4, 3/4].every(function(point) {
|
||||
return Math.abs(getDistance(getCubicBezierPoint(curve, point), center) - radius) <= tolerance;
|
||||
}))
|
||||
return { center: center, radius: radius};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a curve fits the given circle.
|
||||
*
|
||||
* @param {Object} circle
|
||||
* @param {Array} curve
|
||||
* @return {Boolean}
|
||||
*/
|
||||
|
||||
function isArc(curve, circle) {
|
||||
var tolerance = Math.min(arcThreshold * error, arcTolerance * circle.radius / 100);
|
||||
|
||||
return [0, 1/4, 1/2, 3/4, 1].every(function(point) {
|
||||
return Math.abs(getDistance(getCubicBezierPoint(curve, point), circle.center) - circle.radius) <= tolerance;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a previous curve fits the given circle.
|
||||
*
|
||||
* @param {Object} circle
|
||||
* @param {Array} curve
|
||||
* @return {Boolean}
|
||||
*/
|
||||
|
||||
function isArcPrev(curve, circle) {
|
||||
return isArc(curve, {
|
||||
center: [circle.center[0] + curve[4], circle.center[1] + curve[5]],
|
||||
radius: circle.radius
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds angle of a curve fitting the given arc.
|
||||
|
||||
* @param {Array} curve
|
||||
* @param {Object} relCircle
|
||||
* @return {Number} angle
|
||||
*/
|
||||
|
||||
function findArcAngle(curve, relCircle) {
|
||||
var x1 = -relCircle.center[0],
|
||||
y1 = -relCircle.center[1],
|
||||
x2 = curve[4] - relCircle.center[0],
|
||||
y2 = curve[5] - relCircle.center[1];
|
||||
|
||||
return Math.acos(
|
||||
(x1 * x2 + y1 * y2) /
|
||||
Math.sqrt((x1 * x1 + y1 * y1) * (x2 * x2 + y2 * y2))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts given path data to string.
|
||||
*
|
||||
* @param {Object} params
|
||||
* @param {Array} pathData
|
||||
* @return {String}
|
||||
*/
|
||||
|
||||
function data2Path(params, pathData) {
|
||||
return pathData.reduce(function(pathString, item) {
|
||||
var strData = '';
|
||||
if (item.data) {
|
||||
strData = cleanupOutData(roundData(item.data.slice()), params);
|
||||
}
|
||||
return pathString + item.instruction + strData;
|
||||
}, '');
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
'use strict';
|
||||
|
||||
exports.type = 'perItem';
|
||||
|
||||
exports.active = true;
|
||||
|
||||
exports.description = 'converts basic shapes to more compact path form';
|
||||
|
||||
exports.params = {
|
||||
convertArcs: false
|
||||
};
|
||||
|
||||
var none = { value: 0 },
|
||||
regNumber = /[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/g;
|
||||
|
||||
/**
|
||||
* Converts basic shape to more compact path.
|
||||
* It also allows further optimizations like
|
||||
* combining paths with similar attributes.
|
||||
*
|
||||
* @see http://www.w3.org/TR/SVG/shapes.html
|
||||
*
|
||||
* @param {Object} item current iteration item
|
||||
* @param {Object} params plugin params
|
||||
* @return {Boolean} if false, item will be filtered out
|
||||
*
|
||||
* @author Lev Solntsev
|
||||
*/
|
||||
exports.fn = function(item, params) {
|
||||
var convertArcs = params && params.convertArcs;
|
||||
|
||||
if (
|
||||
item.isElem('rect') &&
|
||||
item.hasAttr('width') &&
|
||||
item.hasAttr('height') &&
|
||||
!item.hasAttr('rx') &&
|
||||
!item.hasAttr('ry')
|
||||
) {
|
||||
|
||||
var x = +(item.attr('x') || none).value,
|
||||
y = +(item.attr('y') || none).value,
|
||||
width = +item.attr('width').value,
|
||||
height = +item.attr('height').value;
|
||||
|
||||
// Values like '100%' compute to NaN, thus running after
|
||||
// cleanupNumericValues when 'px' units has already been removed.
|
||||
// TODO: Calculate sizes from % and non-px units if possible.
|
||||
if (isNaN(x - y + width - height)) return;
|
||||
|
||||
var pathData =
|
||||
'M' + x + ' ' + y +
|
||||
'H' + (x + width) +
|
||||
'V' + (y + height) +
|
||||
'H' + x +
|
||||
'z';
|
||||
|
||||
item.addAttr({
|
||||
name: 'd',
|
||||
value: pathData,
|
||||
prefix: '',
|
||||
local: 'd'
|
||||
});
|
||||
|
||||
item.renameElem('path')
|
||||
.removeAttr(['x', 'y', 'width', 'height']);
|
||||
|
||||
} else if (item.isElem('line')) {
|
||||
|
||||
var x1 = +(item.attr('x1') || none).value,
|
||||
y1 = +(item.attr('y1') || none).value,
|
||||
x2 = +(item.attr('x2') || none).value,
|
||||
y2 = +(item.attr('y2') || none).value;
|
||||
if (isNaN(x1 - y1 + x2 - y2)) return;
|
||||
|
||||
item.addAttr({
|
||||
name: 'd',
|
||||
value: 'M' + x1 + ' ' + y1 + 'L' + x2 + ' ' + y2,
|
||||
prefix: '',
|
||||
local: 'd'
|
||||
});
|
||||
|
||||
item.renameElem('path')
|
||||
.removeAttr(['x1', 'y1', 'x2', 'y2']);
|
||||
|
||||
} else if ((
|
||||
item.isElem('polyline') ||
|
||||
item.isElem('polygon')
|
||||
) &&
|
||||
item.hasAttr('points')
|
||||
) {
|
||||
|
||||
var coords = (item.attr('points').value.match(regNumber) || []).map(Number);
|
||||
if (coords.length < 4) return false;
|
||||
|
||||
item.addAttr({
|
||||
name: 'd',
|
||||
value: 'M' + coords.slice(0,2).join(' ') +
|
||||
'L' + coords.slice(2).join(' ') +
|
||||
(item.isElem('polygon') ? 'z' : ''),
|
||||
prefix: '',
|
||||
local: 'd'
|
||||
});
|
||||
|
||||
item.renameElem('path')
|
||||
.removeAttr('points');
|
||||
} else if (item.isElem('circle') && convertArcs) {
|
||||
|
||||
var cx = +(item.attr('cx') || none).value;
|
||||
var cy = +(item.attr('cy') || none).value;
|
||||
var r = +(item.attr('r') || none).value;
|
||||
if (isNaN(cx - cy + r)) {
|
||||
return;
|
||||
}
|
||||
var cPathData =
|
||||
'M' + cx + ' ' + (cy - r) +
|
||||
'A' + r + ' ' + r + ' 0 1 0 ' + cx + ' ' + (cy + r) +
|
||||
'A' + r + ' ' + r + ' 0 1 0 ' + cx + ' ' + (cy - r) +
|
||||
'Z';
|
||||
item.addAttr({
|
||||
name: 'd',
|
||||
value: cPathData,
|
||||
prefix: '',
|
||||
local: 'd',
|
||||
});
|
||||
item.renameElem('path').removeAttr(['cx', 'cy', 'r']);
|
||||
|
||||
} else if (item.isElem('ellipse') && convertArcs) {
|
||||
|
||||
var ecx = +(item.attr('cx') || none).value;
|
||||
var ecy = +(item.attr('cy') || none).value;
|
||||
var rx = +(item.attr('rx') || none).value;
|
||||
var ry = +(item.attr('ry') || none).value;
|
||||
if (isNaN(ecx - ecy + rx - ry)) {
|
||||
return;
|
||||
}
|
||||
var ePathData =
|
||||
'M' + ecx + ' ' + (ecy - ry) +
|
||||
'A' + rx + ' ' + ry + ' 0 1 0 ' + ecx + ' ' + (ecy + ry) +
|
||||
'A' + rx + ' ' + ry + ' 0 1 0 ' + ecx + ' ' + (ecy - ry) +
|
||||
'Z';
|
||||
item.addAttr({
|
||||
name: 'd',
|
||||
value: ePathData,
|
||||
prefix: '',
|
||||
local: 'd',
|
||||
});
|
||||
item.renameElem('path').removeAttr(['cx', 'cy', 'rx', 'ry']);
|
||||
}
|
||||
};
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
/* jshint quotmark: false */
|
||||
'use strict';
|
||||
|
||||
exports.type = 'perItem';
|
||||
|
||||
exports.active = true;
|
||||
|
||||
exports.description = 'converts style to attributes';
|
||||
|
||||
exports.params = {
|
||||
keepImportant: false
|
||||
};
|
||||
|
||||
var stylingProps = require('./_collections').attrsGroups.presentation,
|
||||
rEscape = '\\\\(?:[0-9a-f]{1,6}\\s?|\\r\\n|.)', // Like \" or \2051. Code points consume one space.
|
||||
rAttr = '\\s*(' + g('[^:;\\\\]', rEscape) + '*?)\\s*', // attribute name like ‘fill’
|
||||
rSingleQuotes = "'(?:[^'\\n\\r\\\\]|" + rEscape + ")*?(?:'|$)", // string in single quotes: 'smth'
|
||||
rQuotes = '"(?:[^"\\n\\r\\\\]|' + rEscape + ')*?(?:"|$)', // string in double quotes: "smth"
|
||||
rQuotedString = new RegExp('^' + g(rSingleQuotes, rQuotes) + '$'),
|
||||
|
||||
// Parentheses, E.g.: url(data:image/png;base64,iVBO...).
|
||||
// ':' and ';' inside of it should be threated as is. (Just like in strings.)
|
||||
rParenthesis = '\\(' + g('[^\'"()\\\\]+', rEscape, rSingleQuotes, rQuotes) + '*?' + '\\)',
|
||||
|
||||
// The value. It can have strings and parentheses (see above). Fallbacks to anything in case of unexpected input.
|
||||
rValue = '\\s*(' + g('[^!\'"();\\\\]+?', rEscape, rSingleQuotes, rQuotes, rParenthesis, '[^;]*?') + '*?' + ')',
|
||||
|
||||
// End of declaration. Spaces outside of capturing groups help to do natural trimming.
|
||||
rDeclEnd = '\\s*(?:;\\s*|$)',
|
||||
|
||||
// Important rule
|
||||
rImportant = '(\\s*!important(?![-(\w]))?',
|
||||
|
||||
// Final RegExp to parse CSS declarations.
|
||||
regDeclarationBlock = new RegExp(rAttr + ':' + rValue + rImportant + rDeclEnd, 'ig'),
|
||||
|
||||
// Comments expression. Honors escape sequences and strings.
|
||||
regStripComments = new RegExp(g(rEscape, rSingleQuotes, rQuotes, '/\\*[^]*?\\*/'), 'ig');
|
||||
|
||||
/**
|
||||
* Convert style in attributes. Cleanups comments and illegal declarations (without colon) as a side effect.
|
||||
*
|
||||
* @example
|
||||
* <g style="fill:#000; color: #fff;">
|
||||
* ⬇
|
||||
* <g fill="#000" color="#fff">
|
||||
*
|
||||
* @example
|
||||
* <g style="fill:#000; color: #fff; -webkit-blah: blah">
|
||||
* ⬇
|
||||
* <g fill="#000" color="#fff" style="-webkit-blah: blah">
|
||||
*
|
||||
* @param {Object} item current iteration item
|
||||
* @return {Boolean} if false, item will be filtered out
|
||||
*
|
||||
* @author Kir Belevich
|
||||
*/
|
||||
exports.fn = function(item, params) {
|
||||
/* jshint boss: true */
|
||||
|
||||
if (item.elem && item.hasAttr('style')) {
|
||||
// ['opacity: 1', 'color: #000']
|
||||
var styleValue = item.attr('style').value,
|
||||
styles = [],
|
||||
attrs = {};
|
||||
|
||||
// Strip CSS comments preserving escape sequences and strings.
|
||||
styleValue = styleValue.replace(regStripComments, function(match) {
|
||||
return match[0] == '/' ? '' :
|
||||
match[0] == '\\' && /[-g-z]/i.test(match[1]) ? match[1] : match;
|
||||
});
|
||||
|
||||
regDeclarationBlock.lastIndex = 0;
|
||||
for (var rule; rule = regDeclarationBlock.exec(styleValue);) {
|
||||
if (!params.keepImportant || !rule[3]) {
|
||||
styles.push([rule[1], rule[2]]);
|
||||
}
|
||||
}
|
||||
|
||||
if (styles.length) {
|
||||
|
||||
styles = styles.filter(function(style) {
|
||||
if (style[0]) {
|
||||
var prop = style[0].toLowerCase(),
|
||||
val = style[1];
|
||||
|
||||
if (rQuotedString.test(val)) {
|
||||
val = val.slice(1, -1);
|
||||
}
|
||||
|
||||
if (stylingProps.indexOf(prop) > -1) {
|
||||
|
||||
attrs[prop] = {
|
||||
name: prop,
|
||||
value: val,
|
||||
local: prop,
|
||||
prefix: ''
|
||||
};
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
Object.assign(item.attrs, attrs);
|
||||
|
||||
if (styles.length) {
|
||||
item.attr('style').value = styles
|
||||
.map(function(declaration) { return declaration.join(':') })
|
||||
.join(';');
|
||||
} else {
|
||||
item.removeAttr('style');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
function g() {
|
||||
return '(?:' + Array.prototype.join.call(arguments, '|') + ')';
|
||||
}
|
||||
+363
@@ -0,0 +1,363 @@
|
||||
'use strict';
|
||||
|
||||
exports.type = 'perItem';
|
||||
|
||||
exports.active = true;
|
||||
|
||||
exports.description = 'collapses multiple transformations and optimizes it';
|
||||
|
||||
exports.params = {
|
||||
convertToShorts: true,
|
||||
// degPrecision: 3, // transformPrecision (or matrix precision) - 2 by default
|
||||
floatPrecision: 3,
|
||||
transformPrecision: 5,
|
||||
matrixToTransform: true,
|
||||
shortTranslate: true,
|
||||
shortScale: true,
|
||||
shortRotate: true,
|
||||
removeUseless: true,
|
||||
collapseIntoOne: true,
|
||||
leadingZero: true,
|
||||
negativeExtraSpace: false
|
||||
};
|
||||
|
||||
var cleanupOutData = require('../lib/svgo/tools').cleanupOutData,
|
||||
transform2js = require('./_transforms.js').transform2js,
|
||||
transformsMultiply = require('./_transforms.js').transformsMultiply,
|
||||
matrixToTransform = require('./_transforms.js').matrixToTransform,
|
||||
degRound,
|
||||
floatRound,
|
||||
transformRound;
|
||||
|
||||
/**
|
||||
* Convert matrices to the short aliases,
|
||||
* convert long translate, scale or rotate transform notations to the shorts ones,
|
||||
* convert transforms to the matrices and multiply them all into one,
|
||||
* remove useless transforms.
|
||||
*
|
||||
* @see http://www.w3.org/TR/SVG/coords.html#TransformMatrixDefined
|
||||
*
|
||||
* @param {Object} item current iteration item
|
||||
* @param {Object} params plugin params
|
||||
* @return {Boolean} if false, item will be filtered out
|
||||
*
|
||||
* @author Kir Belevich
|
||||
*/
|
||||
exports.fn = function(item, params) {
|
||||
|
||||
if (item.elem) {
|
||||
|
||||
// transform
|
||||
if (item.hasAttr('transform')) {
|
||||
convertTransform(item, 'transform', params);
|
||||
}
|
||||
|
||||
// gradientTransform
|
||||
if (item.hasAttr('gradientTransform')) {
|
||||
convertTransform(item, 'gradientTransform', params);
|
||||
}
|
||||
|
||||
// patternTransform
|
||||
if (item.hasAttr('patternTransform')) {
|
||||
convertTransform(item, 'patternTransform', params);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Main function.
|
||||
*
|
||||
* @param {Object} item input item
|
||||
* @param {String} attrName attribute name
|
||||
* @param {Object} params plugin params
|
||||
*/
|
||||
function convertTransform(item, attrName, params) {
|
||||
var data = transform2js(item.attr(attrName).value);
|
||||
params = definePrecision(data, params);
|
||||
|
||||
if (params.collapseIntoOne && data.length > 1) {
|
||||
data = [transformsMultiply(data)];
|
||||
}
|
||||
|
||||
if (params.convertToShorts) {
|
||||
data = convertToShorts(data, params);
|
||||
} else {
|
||||
data.forEach(roundTransform);
|
||||
}
|
||||
|
||||
if (params.removeUseless) {
|
||||
data = removeUseless(data);
|
||||
}
|
||||
|
||||
if (data.length) {
|
||||
item.attr(attrName).value = js2transform(data, params);
|
||||
} else {
|
||||
item.removeAttr(attrName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines precision to work with certain parts.
|
||||
* transformPrecision - for scale and four first matrix parameters (needs a better precision due to multiplying),
|
||||
* floatPrecision - for translate including two last matrix and rotate parameters,
|
||||
* degPrecision - for rotate and skew. By default it's equal to (rougly)
|
||||
* transformPrecision - 2 or floatPrecision whichever is lower. Can be set in params.
|
||||
*
|
||||
* @param {Array} transforms input array
|
||||
* @param {Object} params plugin params
|
||||
* @return {Array} output array
|
||||
*/
|
||||
function definePrecision(data, params) {
|
||||
/* jshint validthis: true */
|
||||
var matrixData = data.reduce(getMatrixData, []),
|
||||
significantDigits = params.transformPrecision;
|
||||
|
||||
// Clone params so it don't affect other elements transformations.
|
||||
params = Object.assign({}, params);
|
||||
|
||||
// Limit transform precision with matrix one. Calculating with larger precision doesn't add any value.
|
||||
if (matrixData.length) {
|
||||
params.transformPrecision = Math.min(params.transformPrecision,
|
||||
Math.max.apply(Math, matrixData.map(floatDigits)) || params.transformPrecision);
|
||||
|
||||
significantDigits = Math.max.apply(Math, matrixData.map(function(n) {
|
||||
return String(n).replace(/\D+/g, '').length; // Number of digits in a number. 123.45 → 5
|
||||
}));
|
||||
}
|
||||
// No sense in angle precision more then number of significant digits in matrix.
|
||||
if (!('degPrecision' in params)) {
|
||||
params.degPrecision = Math.max(0, Math.min(params.floatPrecision, significantDigits - 2));
|
||||
}
|
||||
|
||||
floatRound = params.floatPrecision >= 1 && params.floatPrecision < 20 ?
|
||||
smartRound.bind(this, params.floatPrecision) :
|
||||
round;
|
||||
degRound = params.degPrecision >= 1 && params.floatPrecision < 20 ?
|
||||
smartRound.bind(this, params.degPrecision) :
|
||||
round;
|
||||
transformRound = params.transformPrecision >= 1 && params.floatPrecision < 20 ?
|
||||
smartRound.bind(this, params.transformPrecision) :
|
||||
round;
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gathers four first matrix parameters.
|
||||
*
|
||||
* @param {Array} a array of data
|
||||
* @param {Object} transform
|
||||
* @return {Array} output array
|
||||
*/
|
||||
function getMatrixData(a, b) {
|
||||
return b.name == 'matrix' ? a.concat(b.data.slice(0, 4)) : a;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns number of digits after the point. 0.125 → 3
|
||||
*/
|
||||
function floatDigits(n) {
|
||||
return (n = String(n)).slice(n.indexOf('.')).length - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert transforms to the shorthand alternatives.
|
||||
*
|
||||
* @param {Array} transforms input array
|
||||
* @param {Object} params plugin params
|
||||
* @return {Array} output array
|
||||
*/
|
||||
function convertToShorts(transforms, params) {
|
||||
|
||||
for(var i = 0; i < transforms.length; i++) {
|
||||
|
||||
var transform = transforms[i];
|
||||
|
||||
// convert matrix to the short aliases
|
||||
if (
|
||||
params.matrixToTransform &&
|
||||
transform.name === 'matrix'
|
||||
) {
|
||||
var decomposed = matrixToTransform(transform, params);
|
||||
if (decomposed != transform &&
|
||||
js2transform(decomposed, params).length <= js2transform([transform], params).length) {
|
||||
|
||||
transforms.splice.apply(transforms, [i, 1].concat(decomposed));
|
||||
}
|
||||
transform = transforms[i];
|
||||
}
|
||||
|
||||
// fixed-point numbers
|
||||
// 12.754997 → 12.755
|
||||
roundTransform(transform);
|
||||
|
||||
// convert long translate transform notation to the shorts one
|
||||
// translate(10 0) → translate(10)
|
||||
if (
|
||||
params.shortTranslate &&
|
||||
transform.name === 'translate' &&
|
||||
transform.data.length === 2 &&
|
||||
!transform.data[1]
|
||||
) {
|
||||
transform.data.pop();
|
||||
}
|
||||
|
||||
// convert long scale transform notation to the shorts one
|
||||
// scale(2 2) → scale(2)
|
||||
if (
|
||||
params.shortScale &&
|
||||
transform.name === 'scale' &&
|
||||
transform.data.length === 2 &&
|
||||
transform.data[0] === transform.data[1]
|
||||
) {
|
||||
transform.data.pop();
|
||||
}
|
||||
|
||||
// convert long rotate transform notation to the short one
|
||||
// translate(cx cy) rotate(a) translate(-cx -cy) → rotate(a cx cy)
|
||||
if (
|
||||
params.shortRotate &&
|
||||
transforms[i - 2] &&
|
||||
transforms[i - 2].name === 'translate' &&
|
||||
transforms[i - 1].name === 'rotate' &&
|
||||
transforms[i].name === 'translate' &&
|
||||
transforms[i - 2].data[0] === -transforms[i].data[0] &&
|
||||
transforms[i - 2].data[1] === -transforms[i].data[1]
|
||||
) {
|
||||
transforms.splice(i - 2, 3, {
|
||||
name: 'rotate',
|
||||
data: [
|
||||
transforms[i - 1].data[0],
|
||||
transforms[i - 2].data[0],
|
||||
transforms[i - 2].data[1]
|
||||
]
|
||||
});
|
||||
|
||||
// splice compensation
|
||||
i -= 2;
|
||||
|
||||
transform = transforms[i];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return transforms;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove useless transforms.
|
||||
*
|
||||
* @param {Array} transforms input array
|
||||
* @return {Array} output array
|
||||
*/
|
||||
function removeUseless(transforms) {
|
||||
|
||||
return transforms.filter(function(transform) {
|
||||
|
||||
// translate(0), rotate(0[, cx, cy]), skewX(0), skewY(0)
|
||||
if (
|
||||
['translate', 'rotate', 'skewX', 'skewY'].indexOf(transform.name) > -1 &&
|
||||
(transform.data.length == 1 || transform.name == 'rotate') &&
|
||||
!transform.data[0] ||
|
||||
|
||||
// translate(0, 0)
|
||||
transform.name == 'translate' &&
|
||||
!transform.data[0] &&
|
||||
!transform.data[1] ||
|
||||
|
||||
// scale(1)
|
||||
transform.name == 'scale' &&
|
||||
transform.data[0] == 1 &&
|
||||
(transform.data.length < 2 || transform.data[1] == 1) ||
|
||||
|
||||
// matrix(1 0 0 1 0 0)
|
||||
transform.name == 'matrix' &&
|
||||
transform.data[0] == 1 &&
|
||||
transform.data[3] == 1 &&
|
||||
!(transform.data[1] || transform.data[2] || transform.data[4] || transform.data[5])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert transforms JS representation to string.
|
||||
*
|
||||
* @param {Array} transformJS JS representation array
|
||||
* @param {Object} params plugin params
|
||||
* @return {String} output string
|
||||
*/
|
||||
function js2transform(transformJS, params) {
|
||||
|
||||
var transformString = '';
|
||||
|
||||
// collect output value string
|
||||
transformJS.forEach(function(transform) {
|
||||
roundTransform(transform);
|
||||
transformString += (transformString && ' ') + transform.name + '(' + cleanupOutData(transform.data, params) + ')';
|
||||
});
|
||||
|
||||
return transformString;
|
||||
|
||||
}
|
||||
|
||||
function roundTransform(transform) {
|
||||
switch (transform.name) {
|
||||
case 'translate':
|
||||
transform.data = floatRound(transform.data);
|
||||
break;
|
||||
case 'rotate':
|
||||
transform.data = degRound(transform.data.slice(0, 1)).concat(floatRound(transform.data.slice(1)));
|
||||
break;
|
||||
case 'skewX':
|
||||
case 'skewY':
|
||||
transform.data = degRound(transform.data);
|
||||
break;
|
||||
case 'scale':
|
||||
transform.data = transformRound(transform.data);
|
||||
break;
|
||||
case 'matrix':
|
||||
transform.data = transformRound(transform.data.slice(0, 4)).concat(floatRound(transform.data.slice(4)));
|
||||
break;
|
||||
}
|
||||
return transform;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rounds numbers in array.
|
||||
*
|
||||
* @param {Array} data input data array
|
||||
* @return {Array} output data array
|
||||
*/
|
||||
function round(data) {
|
||||
return data.map(Math.round);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrease accuracy of floating-point numbers
|
||||
* in transforms keeping a specified number of decimals.
|
||||
* Smart rounds values like 2.349 to 2.35.
|
||||
*
|
||||
* @param {Number} fixed number of decimals
|
||||
* @param {Array} data input data array
|
||||
* @return {Array} output data array
|
||||
*/
|
||||
function smartRound(precision, data) {
|
||||
for (var i = data.length, tolerance = +Math.pow(.1, precision).toFixed(precision); i--;) {
|
||||
if (data[i].toFixed(precision) != data[i]) {
|
||||
var rounded = +data[i].toFixed(precision - 1);
|
||||
data[i] = +Math.abs(rounded - data[i]).toFixed(precision + 1) >= tolerance ?
|
||||
+data[i].toFixed(precision) :
|
||||
rounded;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
'use strict';
|
||||
|
||||
exports.type = 'full';
|
||||
|
||||
exports.active = true;
|
||||
|
||||
exports.params = {
|
||||
onlyMatchedOnce: true,
|
||||
removeMatchedSelectors: true,
|
||||
useMqs: ['', 'screen'],
|
||||
usePseudos: ['']
|
||||
};
|
||||
|
||||
exports.description = 'inline styles (additional options)';
|
||||
|
||||
|
||||
var csstree = require('css-tree'),
|
||||
cssTools = require('../lib/css-tools');
|
||||
|
||||
/**
|
||||
* Moves + merges styles from style elements to element styles
|
||||
*
|
||||
* Options
|
||||
* onlyMatchedOnce (default: true)
|
||||
* inline only selectors that match once
|
||||
*
|
||||
* removeMatchedSelectors (default: true)
|
||||
* clean up matched selectors,
|
||||
* leave selectors that hadn't matched
|
||||
*
|
||||
* useMqs (default: ['', 'screen'])
|
||||
* what media queries to be used
|
||||
* empty string element for styles outside media queries
|
||||
*
|
||||
* usePseudos (default: [''])
|
||||
* what pseudo-classes/-elements to be used
|
||||
* empty string element for all non-pseudo-classes and/or -elements
|
||||
*
|
||||
* @param {Object} document document element
|
||||
* @param {Object} opts plugin params
|
||||
*
|
||||
* @author strarsis <strarsis@gmail.com>
|
||||
*/
|
||||
exports.fn = function(document, opts) {
|
||||
|
||||
// collect <style/>s
|
||||
var styleEls = document.querySelectorAll('style');
|
||||
|
||||
//no <styles/>s, nothing to do
|
||||
if (styleEls === null) {
|
||||
return document;
|
||||
}
|
||||
|
||||
var styles = [],
|
||||
selectors = [];
|
||||
|
||||
for (var styleEl of styleEls) {
|
||||
if (styleEl.isEmpty() || styleEl.closestElem('foreignObject')) {
|
||||
// skip empty <style/>s or <foreignObject> content.
|
||||
continue;
|
||||
}
|
||||
|
||||
var cssStr = cssTools.getCssStr(styleEl);
|
||||
|
||||
// collect <style/>s and their css ast
|
||||
var cssAst = {};
|
||||
try {
|
||||
cssAst = csstree.parse(cssStr, {
|
||||
parseValue: false,
|
||||
parseCustomProperty: false
|
||||
});
|
||||
} catch (parseError) {
|
||||
// console.warn('Warning: Parse error of styles of <style/> element, skipped. Error details: ' + parseError);
|
||||
continue;
|
||||
}
|
||||
|
||||
styles.push({
|
||||
styleEl: styleEl,
|
||||
cssAst: cssAst
|
||||
});
|
||||
|
||||
selectors = selectors.concat(cssTools.flattenToSelectors(cssAst));
|
||||
}
|
||||
|
||||
|
||||
// filter for mediaqueries to be used or without any mediaquery
|
||||
var selectorsMq = cssTools.filterByMqs(selectors, opts.useMqs);
|
||||
|
||||
|
||||
// filter for pseudo elements to be used
|
||||
var selectorsPseudo = cssTools.filterByPseudos(selectorsMq, opts.usePseudos);
|
||||
|
||||
// remove PseudoClass from its SimpleSelector for proper matching
|
||||
cssTools.cleanPseudos(selectorsPseudo);
|
||||
|
||||
|
||||
// stable sort selectors
|
||||
var sortedSelectors = cssTools.sortSelectors(selectorsPseudo).reverse();
|
||||
|
||||
|
||||
var selector,
|
||||
selectedEl;
|
||||
|
||||
// match selectors
|
||||
for (selector of sortedSelectors) {
|
||||
var selectorStr = csstree.generate(selector.item.data),
|
||||
selectedEls = null;
|
||||
|
||||
try {
|
||||
selectedEls = document.querySelectorAll(selectorStr);
|
||||
} catch (selectError) {
|
||||
if (selectError.constructor === SyntaxError) {
|
||||
// console.warn('Warning: Syntax error when trying to select \n\n' + selectorStr + '\n\n, skipped. Error details: ' + selectError);
|
||||
continue;
|
||||
}
|
||||
throw selectError;
|
||||
}
|
||||
|
||||
if (selectedEls === null) {
|
||||
// nothing selected
|
||||
continue;
|
||||
}
|
||||
|
||||
selector.selectedEls = selectedEls;
|
||||
}
|
||||
|
||||
|
||||
// apply <style/> styles to matched elements
|
||||
for (selector of sortedSelectors) {
|
||||
if(!selector.selectedEls) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (opts.onlyMatchedOnce && selector.selectedEls !== null && selector.selectedEls.length > 1) {
|
||||
// skip selectors that match more than once if option onlyMatchedOnce is enabled
|
||||
continue;
|
||||
}
|
||||
|
||||
// apply <style/> to matched elements
|
||||
for (selectedEl of selector.selectedEls) {
|
||||
if (selector.rule === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// merge declarations
|
||||
csstree.walk(selector.rule, {visit: 'Declaration', enter: function(styleCsstreeDeclaration) {
|
||||
|
||||
// existing inline styles have higher priority
|
||||
// no inline styles, external styles, external styles used
|
||||
// inline styles, external styles same priority as inline styles, inline styles used
|
||||
// inline styles, external styles higher priority than inline styles, external styles used
|
||||
var styleDeclaration = cssTools.csstreeToStyleDeclaration(styleCsstreeDeclaration);
|
||||
if (selectedEl.style.getPropertyValue(styleDeclaration.name) !== null &&
|
||||
selectedEl.style.getPropertyPriority(styleDeclaration.name) >= styleDeclaration.priority) {
|
||||
return;
|
||||
}
|
||||
selectedEl.style.setProperty(styleDeclaration.name, styleDeclaration.value, styleDeclaration.priority);
|
||||
}});
|
||||
}
|
||||
|
||||
if (opts.removeMatchedSelectors && selector.selectedEls !== null && selector.selectedEls.length > 0) {
|
||||
// clean up matching simple selectors if option removeMatchedSelectors is enabled
|
||||
selector.rule.prelude.children.remove(selector.item);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!opts.removeMatchedSelectors) {
|
||||
return document; // no further processing required
|
||||
}
|
||||
|
||||
|
||||
// clean up matched class + ID attribute values
|
||||
for (selector of sortedSelectors) {
|
||||
if(!selector.selectedEls) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (opts.onlyMatchedOnce && selector.selectedEls !== null && selector.selectedEls.length > 1) {
|
||||
// skip selectors that match more than once if option onlyMatchedOnce is enabled
|
||||
continue;
|
||||
}
|
||||
|
||||
for (selectedEl of selector.selectedEls) {
|
||||
// class
|
||||
var firstSubSelector = selector.item.data.children.first();
|
||||
if(firstSubSelector.type === 'ClassSelector') {
|
||||
selectedEl.class.remove(firstSubSelector.name);
|
||||
}
|
||||
// clean up now empty class attributes
|
||||
if(typeof selectedEl.class.item(0) === 'undefined') {
|
||||
selectedEl.removeAttr('class');
|
||||
}
|
||||
|
||||
// ID
|
||||
if(firstSubSelector.type === 'IdSelector') {
|
||||
selectedEl.removeAttr('id', firstSubSelector.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// clean up now empty elements
|
||||
for (var style of styles) {
|
||||
csstree.walk(style.cssAst, {visit: 'Rule', enter: function(node, item, list) {
|
||||
// clean up <style/> atrules without any rulesets left
|
||||
if (node.type === 'Atrule' &&
|
||||
// only Atrules containing rulesets
|
||||
node.block !== null &&
|
||||
node.block.children.isEmpty()) {
|
||||
list.remove(item);
|
||||
return;
|
||||
}
|
||||
|
||||
// clean up <style/> rulesets without any css selectors left
|
||||
if (node.type === 'Rule' &&
|
||||
node.prelude.children.isEmpty()) {
|
||||
list.remove(item);
|
||||
}
|
||||
}});
|
||||
|
||||
|
||||
if (style.cssAst.children.isEmpty()) {
|
||||
// clean up now emtpy <style/>s
|
||||
var styleParentEl = style.styleEl.parentNode;
|
||||
styleParentEl.spliceContent(styleParentEl.content.indexOf(style.styleEl), 1);
|
||||
|
||||
if (styleParentEl.elem === 'defs' &&
|
||||
styleParentEl.content.length === 0) {
|
||||
// also clean up now empty <def/>s
|
||||
var defsParentEl = styleParentEl.parentNode;
|
||||
defsParentEl.spliceContent(defsParentEl.content.indexOf(styleParentEl), 1);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// update existing, left over <style>s
|
||||
cssTools.setCssStr(style.styleEl, csstree.generate(style.cssAst));
|
||||
}
|
||||
|
||||
|
||||
return document;
|
||||
};
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
'use strict';
|
||||
|
||||
exports.type = 'perItem';
|
||||
|
||||
exports.active = true;
|
||||
|
||||
exports.description = 'merges multiple paths in one if possible';
|
||||
|
||||
exports.params = {
|
||||
collapseRepeated: true,
|
||||
force: false,
|
||||
leadingZero: true,
|
||||
negativeExtraSpace: true,
|
||||
noSpaceAfterFlags: true
|
||||
};
|
||||
|
||||
var path2js = require('./_path.js').path2js,
|
||||
js2path = require('./_path.js').js2path,
|
||||
intersects = require('./_path.js').intersects;
|
||||
|
||||
/**
|
||||
* Merge multiple Paths into one.
|
||||
*
|
||||
* @param {Object} item current iteration item
|
||||
* @return {Boolean} if false, item will be filtered out
|
||||
*
|
||||
* @author Kir Belevich, Lev Solntsev
|
||||
*/
|
||||
exports.fn = function(item, params) {
|
||||
|
||||
if (!item.isElem() || item.isEmpty()) return;
|
||||
|
||||
var prevContentItem = null,
|
||||
prevContentItemKeys = null;
|
||||
|
||||
item.content = item.content.filter(function(contentItem) {
|
||||
|
||||
if (prevContentItem &&
|
||||
prevContentItem.isElem('path') &&
|
||||
prevContentItem.isEmpty() &&
|
||||
prevContentItem.hasAttr('d') &&
|
||||
contentItem.isElem('path') &&
|
||||
contentItem.isEmpty() &&
|
||||
contentItem.hasAttr('d')
|
||||
) {
|
||||
|
||||
if (!prevContentItemKeys) {
|
||||
prevContentItemKeys = Object.keys(prevContentItem.attrs);
|
||||
}
|
||||
|
||||
var contentItemAttrs = Object.keys(contentItem.attrs),
|
||||
equalData = prevContentItemKeys.length == contentItemAttrs.length &&
|
||||
contentItemAttrs.every(function(key) {
|
||||
return key == 'd' ||
|
||||
prevContentItem.hasAttr(key) &&
|
||||
prevContentItem.attr(key).value == contentItem.attr(key).value;
|
||||
}),
|
||||
prevPathJS = path2js(prevContentItem),
|
||||
curPathJS = path2js(contentItem);
|
||||
|
||||
if (equalData && (params.force || !intersects(prevPathJS, curPathJS))) {
|
||||
js2path(prevContentItem, prevPathJS.concat(curPathJS), params);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
prevContentItem = contentItem;
|
||||
prevContentItemKeys = null;
|
||||
return true;
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
'use strict';
|
||||
|
||||
exports.type = 'full';
|
||||
|
||||
exports.active = true;
|
||||
|
||||
exports.description = 'minifies styles and removes unused styles based on usage data';
|
||||
|
||||
exports.params = {
|
||||
// ... CSSO options goes here
|
||||
|
||||
// additional
|
||||
usage: {
|
||||
force: false, // force to use usage data even if it unsafe (document contains <script> or on* attributes)
|
||||
ids: true,
|
||||
classes: true,
|
||||
tags: true
|
||||
}
|
||||
};
|
||||
|
||||
var csso = require('csso');
|
||||
|
||||
/**
|
||||
* Minifies styles (<style> element + style attribute) using CSSO
|
||||
*
|
||||
* @author strarsis <strarsis@gmail.com>
|
||||
*/
|
||||
exports.fn = function(ast, options) {
|
||||
options = options || {};
|
||||
|
||||
var minifyOptionsForStylesheet = cloneObject(options);
|
||||
var minifyOptionsForAttribute = cloneObject(options);
|
||||
var elems = findStyleElems(ast);
|
||||
|
||||
minifyOptionsForStylesheet.usage = collectUsageData(ast, options);
|
||||
minifyOptionsForAttribute.usage = null;
|
||||
|
||||
elems.forEach(function(elem) {
|
||||
if (elem.isElem('style')) {
|
||||
// <style> element
|
||||
var styleCss = elem.content[0].text || elem.content[0].cdata || [];
|
||||
var DATA = styleCss.indexOf('>') >= 0 || styleCss.indexOf('<') >= 0 ? 'cdata' : 'text';
|
||||
|
||||
elem.content[0][DATA] = csso.minify(styleCss, minifyOptionsForStylesheet).css;
|
||||
} else {
|
||||
// style attribute
|
||||
var elemStyle = elem.attr('style').value;
|
||||
|
||||
elem.attr('style').value = csso.minifyBlock(elemStyle, minifyOptionsForAttribute).css;
|
||||
}
|
||||
});
|
||||
|
||||
return ast;
|
||||
};
|
||||
|
||||
function cloneObject(obj) {
|
||||
var result = {};
|
||||
|
||||
for (var key in obj) {
|
||||
result[key] = obj[key];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function findStyleElems(ast) {
|
||||
|
||||
function walk(items, styles) {
|
||||
for (var i = 0; i < items.content.length; i++) {
|
||||
var item = items.content[i];
|
||||
|
||||
// go deeper
|
||||
if (item.content) {
|
||||
walk(item, styles);
|
||||
}
|
||||
|
||||
if (item.isElem('style') && !item.isEmpty()) {
|
||||
styles.push(item);
|
||||
} else if (item.isElem() && item.hasAttr('style')) {
|
||||
styles.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return styles;
|
||||
}
|
||||
|
||||
return walk(ast, []);
|
||||
}
|
||||
|
||||
function shouldFilter(options, name) {
|
||||
if ('usage' in options === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (options.usage && name in options.usage === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return Boolean(options.usage && options.usage[name]);
|
||||
}
|
||||
|
||||
function collectUsageData(ast, options) {
|
||||
|
||||
function walk(items, usageData) {
|
||||
for (var i = 0; i < items.content.length; i++) {
|
||||
var item = items.content[i];
|
||||
|
||||
// go deeper
|
||||
if (item.content) {
|
||||
walk(item, usageData);
|
||||
}
|
||||
|
||||
if (item.isElem('script')) {
|
||||
safe = false;
|
||||
}
|
||||
|
||||
if (item.isElem()) {
|
||||
usageData.tags[item.elem] = true;
|
||||
|
||||
if (item.hasAttr('id')) {
|
||||
usageData.ids[item.attr('id').value] = true;
|
||||
}
|
||||
|
||||
if (item.hasAttr('class')) {
|
||||
item.attr('class').value.replace(/^\s+|\s+$/g, '').split(/\s+/).forEach(function(className) {
|
||||
usageData.classes[className] = true;
|
||||
});
|
||||
}
|
||||
|
||||
if (item.attrs && Object.keys(item.attrs).some(function(name) { return /^on/i.test(name); })) {
|
||||
safe = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return usageData;
|
||||
}
|
||||
|
||||
var safe = true;
|
||||
var usageData = {};
|
||||
var hasData = false;
|
||||
var rawData = walk(ast, {
|
||||
ids: Object.create(null),
|
||||
classes: Object.create(null),
|
||||
tags: Object.create(null)
|
||||
});
|
||||
|
||||
if (!safe && options.usage && options.usage.force) {
|
||||
safe = true;
|
||||
}
|
||||
|
||||
for (var key in rawData) {
|
||||
if (shouldFilter(options, key)) {
|
||||
usageData[key] = Object.keys(rawData[key]);
|
||||
hasData = true;
|
||||
}
|
||||
}
|
||||
|
||||
return safe && hasData ? usageData : null;
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
'use strict';
|
||||
|
||||
exports.type = 'perItemReverse';
|
||||
|
||||
exports.active = true;
|
||||
|
||||
exports.description = 'moves elements attributes to the existing group wrapper';
|
||||
|
||||
var inheritableAttrs = require('./_collections').inheritableAttrs,
|
||||
pathElems = require('./_collections.js').pathElems;
|
||||
|
||||
/**
|
||||
* Collapse content's intersected and inheritable
|
||||
* attributes to the existing group wrapper.
|
||||
*
|
||||
* @example
|
||||
* <g attr1="val1">
|
||||
* <g attr2="val2">
|
||||
* text
|
||||
* </g>
|
||||
* <circle attr2="val2" attr3="val3"/>
|
||||
* </g>
|
||||
* ⬇
|
||||
* <g attr1="val1" attr2="val2">
|
||||
* <g>
|
||||
* text
|
||||
* </g>
|
||||
* <circle attr3="val3"/>
|
||||
* </g>
|
||||
*
|
||||
* @param {Object} item current iteration item
|
||||
* @return {Boolean} if false, item will be filtered out
|
||||
*
|
||||
* @author Kir Belevich
|
||||
*/
|
||||
exports.fn = function(item) {
|
||||
|
||||
if (item.isElem('g') && !item.isEmpty() && item.content.length > 1) {
|
||||
|
||||
var intersection = {},
|
||||
hasTransform = false,
|
||||
hasClip = item.hasAttr('clip-path') || item.hasAttr('mask'),
|
||||
intersected = item.content.every(function(inner) {
|
||||
if (inner.isElem() && inner.hasAttr()) {
|
||||
// don't mess with possible styles (hack until CSS parsing is implemented)
|
||||
if (inner.hasAttr('class')) return false;
|
||||
if (!Object.keys(intersection).length) {
|
||||
intersection = inner.attrs;
|
||||
} else {
|
||||
intersection = intersectInheritableAttrs(intersection, inner.attrs);
|
||||
|
||||
if (!intersection) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}),
|
||||
allPath = item.content.every(function(inner) {
|
||||
return inner.isElem(pathElems);
|
||||
});
|
||||
|
||||
if (intersected) {
|
||||
|
||||
item.content.forEach(function(g) {
|
||||
|
||||
for (var name in intersection) {
|
||||
|
||||
if (!allPath && !hasClip || name !== 'transform') {
|
||||
|
||||
g.removeAttr(name);
|
||||
|
||||
if (name === 'transform') {
|
||||
if (!hasTransform) {
|
||||
if (item.hasAttr('transform')) {
|
||||
item.attr('transform').value += ' ' + intersection[name].value;
|
||||
} else {
|
||||
item.addAttr(intersection[name]);
|
||||
}
|
||||
|
||||
hasTransform = true;
|
||||
}
|
||||
} else {
|
||||
item.addAttr(intersection[name]);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Intersect inheritable attributes.
|
||||
*
|
||||
* @param {Object} a first attrs object
|
||||
* @param {Object} b second attrs object
|
||||
*
|
||||
* @return {Object} intersected attrs object
|
||||
*/
|
||||
function intersectInheritableAttrs(a, b) {
|
||||
|
||||
var c = {};
|
||||
|
||||
for (var n in a) {
|
||||
if (
|
||||
b.hasOwnProperty(n) &&
|
||||
inheritableAttrs.indexOf(n) > -1 &&
|
||||
a[n].name === b[n].name &&
|
||||
a[n].value === b[n].value &&
|
||||
a[n].prefix === b[n].prefix &&
|
||||
a[n].local === b[n].local
|
||||
) {
|
||||
c[n] = a[n];
|
||||
}
|
||||
}
|
||||
|
||||
if (!Object.keys(c).length) return false;
|
||||
|
||||
return c;
|
||||
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
'use strict';
|
||||
|
||||
exports.type = 'perItem';
|
||||
|
||||
exports.active = true;
|
||||
|
||||
exports.description = 'moves some group attributes to the content elements';
|
||||
|
||||
var collections = require('./_collections.js'),
|
||||
pathElems = collections.pathElems.concat(['g', 'text']),
|
||||
referencesProps = collections.referencesProps;
|
||||
|
||||
/**
|
||||
* Move group attrs to the content elements.
|
||||
*
|
||||
* @example
|
||||
* <g transform="scale(2)">
|
||||
* <path transform="rotate(45)" d="M0,0 L10,20"/>
|
||||
* <path transform="translate(10, 20)" d="M0,10 L20,30"/>
|
||||
* </g>
|
||||
* ⬇
|
||||
* <g>
|
||||
* <path transform="scale(2) rotate(45)" d="M0,0 L10,20"/>
|
||||
* <path transform="scale(2) translate(10, 20)" d="M0,10 L20,30"/>
|
||||
* </g>
|
||||
*
|
||||
* @param {Object} item current iteration item
|
||||
* @return {Boolean} if false, item will be filtered out
|
||||
*
|
||||
* @author Kir Belevich
|
||||
*/
|
||||
exports.fn = function(item) {
|
||||
|
||||
// move group transform attr to content's pathElems
|
||||
if (
|
||||
item.isElem('g') &&
|
||||
item.hasAttr('transform') &&
|
||||
!item.isEmpty() &&
|
||||
!item.someAttr(function(attr) {
|
||||
return ~referencesProps.indexOf(attr.name) && ~attr.value.indexOf('url(');
|
||||
}) &&
|
||||
item.content.every(function(inner) {
|
||||
return inner.isElem(pathElems) && !inner.hasAttr('id');
|
||||
})
|
||||
) {
|
||||
item.content.forEach(function(inner) {
|
||||
var attr = item.attr('transform');
|
||||
if (inner.hasAttr('transform')) {
|
||||
inner.attr('transform').value = attr.value + ' ' + inner.attr('transform').value;
|
||||
} else {
|
||||
inner.addAttr({
|
||||
'name': attr.name,
|
||||
'local': attr.local,
|
||||
'prefix': attr.prefix,
|
||||
'value': attr.value
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
item.removeAttr('transform');
|
||||
}
|
||||
|
||||
};
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
'use strict';
|
||||
|
||||
exports.type = 'perItem';
|
||||
|
||||
exports.active = false;
|
||||
|
||||
exports.params = {
|
||||
delim: '__',
|
||||
prefixIds: true,
|
||||
prefixClassNames: true,
|
||||
};
|
||||
|
||||
exports.description = 'prefix IDs';
|
||||
|
||||
|
||||
var path = require('path'),
|
||||
csstree = require('css-tree'),
|
||||
unquote = require('unquote'),
|
||||
collections = require('./_collections.js'),
|
||||
referencesProps = collections.referencesProps,
|
||||
rxId = /^#(.*)$/, // regular expression for matching an ID + extracing its name
|
||||
addPrefix = null;
|
||||
|
||||
|
||||
// Escapes a string for being used as ID
|
||||
var escapeIdentifierName = function(str) {
|
||||
return str.replace(/[\. ]/g, '_');
|
||||
};
|
||||
|
||||
// Matches an #ID value, captures the ID name
|
||||
var matchId = function(urlVal) {
|
||||
var idUrlMatches = urlVal.match(rxId);
|
||||
if (idUrlMatches === null) {
|
||||
return false;
|
||||
}
|
||||
return idUrlMatches[1];
|
||||
};
|
||||
|
||||
// Matches an url(...) value, captures the URL
|
||||
var matchUrl = function(val) {
|
||||
var urlMatches = /url\((.*?)\)/gi.exec(val);
|
||||
if (urlMatches === null) {
|
||||
return false;
|
||||
}
|
||||
return urlMatches[1];
|
||||
};
|
||||
|
||||
// Checks if attribute is empty
|
||||
var attrNotEmpty = function(attr) {
|
||||
return (attr && attr.value && attr.value.length > 0);
|
||||
};
|
||||
|
||||
// prefixes an #ID
|
||||
var prefixId = function(val) {
|
||||
var idName = matchId(val);
|
||||
if (!idName) {
|
||||
return false;
|
||||
}
|
||||
return '#' + addPrefix(idName);
|
||||
};
|
||||
|
||||
|
||||
// attr.value helper methods
|
||||
|
||||
// prefixes a class attribute value
|
||||
var addPrefixToClassAttr = function(attr) {
|
||||
if (!attrNotEmpty(attr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
attr.value = attr.value.split(/\s+/).map(addPrefix).join(' ');
|
||||
};
|
||||
|
||||
// prefixes an ID attribute value
|
||||
var addPrefixToIdAttr = function(attr) {
|
||||
if (!attrNotEmpty(attr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
attr.value = addPrefix(attr.value);
|
||||
};
|
||||
|
||||
// prefixes a href attribute value
|
||||
var addPrefixToHrefAttr = function(attr) {
|
||||
if (!attrNotEmpty(attr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var idPrefixed = prefixId(attr.value);
|
||||
if (!idPrefixed) {
|
||||
return;
|
||||
}
|
||||
attr.value = idPrefixed;
|
||||
};
|
||||
|
||||
// prefixes an URL attribute value
|
||||
var addPrefixToUrlAttr = function(attr) {
|
||||
if (!attrNotEmpty(attr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// url(...) in value
|
||||
var urlVal = matchUrl(attr.value);
|
||||
if (!urlVal) {
|
||||
return;
|
||||
}
|
||||
|
||||
var idPrefixed = prefixId(urlVal);
|
||||
if (!idPrefixed) {
|
||||
return;
|
||||
}
|
||||
|
||||
attr.value = 'url(' + idPrefixed + ')';
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Prefixes identifiers
|
||||
*
|
||||
* @param {Object} node node
|
||||
* @param {Object} opts plugin params
|
||||
* @param {Object} extra plugin extra information
|
||||
*
|
||||
* @author strarsis <strarsis@gmail.com>
|
||||
*/
|
||||
exports.fn = function(node, opts, extra) {
|
||||
|
||||
// skip subsequent passes when multipass is used
|
||||
if(extra.multipassCount && extra.multipassCount > 0) {
|
||||
return node;
|
||||
}
|
||||
|
||||
// prefix, from file name or option
|
||||
var prefix = 'prefix';
|
||||
if (opts.prefix) {
|
||||
if (typeof opts.prefix === 'function') {
|
||||
prefix = opts.prefix(node, extra);
|
||||
} else {
|
||||
prefix = opts.prefix;
|
||||
}
|
||||
} else if (opts.prefix === false) {
|
||||
prefix = false;
|
||||
} else if (extra && extra.path && extra.path.length > 0) {
|
||||
var filename = path.basename(extra.path);
|
||||
prefix = filename;
|
||||
}
|
||||
|
||||
|
||||
// prefixes a normal value
|
||||
addPrefix = function(name) {
|
||||
if(prefix === false){
|
||||
return escapeIdentifierName(name);
|
||||
}
|
||||
return escapeIdentifierName(prefix + opts.delim + name);
|
||||
};
|
||||
|
||||
|
||||
// <style/> property values
|
||||
|
||||
if (node.elem === 'style') {
|
||||
if (node.isEmpty()) {
|
||||
// skip empty <style/>s
|
||||
return node;
|
||||
}
|
||||
|
||||
var cssStr = node.content[0].text || node.content[0].cdata || [];
|
||||
|
||||
var cssAst = {};
|
||||
try {
|
||||
cssAst = csstree.parse(cssStr, {
|
||||
parseValue: true,
|
||||
parseCustomProperty: false
|
||||
});
|
||||
} catch (parseError) {
|
||||
console.warn('Warning: Parse error of styles of <style/> element, skipped. Error details: ' + parseError);
|
||||
return node;
|
||||
}
|
||||
|
||||
var idPrefixed = '';
|
||||
csstree.walk(cssAst, function(node) {
|
||||
|
||||
// #ID, .class
|
||||
if (((opts.prefixIds && node.type === 'IdSelector') ||
|
||||
(opts.prefixClassNames && node.type === 'ClassSelector')) &&
|
||||
node.name) {
|
||||
node.name = addPrefix(node.name);
|
||||
return;
|
||||
}
|
||||
|
||||
// url(...) in value
|
||||
if (node.type === 'Url' &&
|
||||
node.value.value && node.value.value.length > 0) {
|
||||
idPrefixed = prefixId(unquote(node.value.value));
|
||||
if (!idPrefixed) {
|
||||
return;
|
||||
}
|
||||
node.value.value = idPrefixed;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// update <style>s
|
||||
node.content[0].text = csstree.generate(cssAst);
|
||||
return node;
|
||||
}
|
||||
|
||||
|
||||
// element attributes
|
||||
|
||||
if (!node.attrs) {
|
||||
return node;
|
||||
}
|
||||
|
||||
|
||||
// Nodes
|
||||
|
||||
if(opts.prefixIds) {
|
||||
// ID
|
||||
addPrefixToIdAttr(node.attrs.id);
|
||||
}
|
||||
|
||||
if(opts.prefixClassNames) {
|
||||
// Class
|
||||
addPrefixToClassAttr(node.attrs.class);
|
||||
}
|
||||
|
||||
|
||||
// References
|
||||
|
||||
// href
|
||||
addPrefixToHrefAttr(node.attrs.href);
|
||||
|
||||
// (xlink:)href (deprecated, must be still supported)
|
||||
addPrefixToHrefAttr(node.attrs['xlink:href']);
|
||||
|
||||
// (referenceable) properties
|
||||
for (var referencesProp of referencesProps) {
|
||||
addPrefixToUrlAttr(node.attrs[referencesProp]);
|
||||
}
|
||||
|
||||
|
||||
return node;
|
||||
};
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
'use strict';
|
||||
|
||||
exports.type = 'perItem';
|
||||
|
||||
exports.active = false;
|
||||
|
||||
exports.description = 'removes attributes of elements that match a css selector';
|
||||
|
||||
|
||||
/**
|
||||
* Removes attributes of elements that match a css selector.
|
||||
*
|
||||
* @param {Object} item current iteration item
|
||||
* @param {Object} params plugin params
|
||||
* @return {Boolean} if false, item will be filtered out
|
||||
*
|
||||
* @example
|
||||
* <caption>A selector removing a single attribute</caption>
|
||||
* plugins:
|
||||
* - removeAttributesBySelector:
|
||||
* selector: "[fill='#00ff00']"
|
||||
* attributes: "fill"
|
||||
*
|
||||
* <rect x="0" y="0" width="100" height="100" fill="#00ff00" stroke="#00ff00"/>
|
||||
* ↓
|
||||
* <rect x="0" y="0" width="100" height="100" stroke="#00ff00"/>
|
||||
*
|
||||
* <caption>A selector removing multiple attributes</caption>
|
||||
* plugins:
|
||||
* - removeAttributesBySelector:
|
||||
* selector: "[fill='#00ff00']"
|
||||
* attributes:
|
||||
* - fill
|
||||
* - stroke
|
||||
*
|
||||
* <rect x="0" y="0" width="100" height="100" fill="#00ff00" stroke="#00ff00"/>
|
||||
* ↓
|
||||
* <rect x="0" y="0" width="100" height="100"/>
|
||||
*
|
||||
* <caption>Multiple selectors removing attributes</caption>
|
||||
* plugins:
|
||||
* - removeAttributesBySelector:
|
||||
* selectors:
|
||||
* - selector: "[fill='#00ff00']"
|
||||
* attributes: "fill"
|
||||
*
|
||||
* - selector: "#remove"
|
||||
* attributes:
|
||||
* - stroke
|
||||
* - id
|
||||
*
|
||||
* <rect x="0" y="0" width="100" height="100" fill="#00ff00" stroke="#00ff00"/>
|
||||
* ↓
|
||||
* <rect x="0" y="0" width="100" height="100"/>
|
||||
*
|
||||
* @see {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors|MDN CSS Selectors}
|
||||
*
|
||||
* @author Bradley Mease
|
||||
*/
|
||||
exports.fn = function(item, params) {
|
||||
|
||||
var selectors = Array.isArray(params.selectors) ? params.selectors : [params];
|
||||
|
||||
selectors.map(function(i) {
|
||||
if (item.matches(i.selector)) {
|
||||
item.removeAttr(i.attributes);
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
'use strict';
|
||||
|
||||
var DEFAULT_SEPARATOR = ':';
|
||||
|
||||
exports.type = 'perItem';
|
||||
|
||||
exports.active = false;
|
||||
|
||||
exports.description = 'removes specified attributes';
|
||||
|
||||
exports.params = {
|
||||
elemSeparator: DEFAULT_SEPARATOR,
|
||||
preserveCurrentColor: false,
|
||||
attrs: []
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove attributes
|
||||
*
|
||||
* @param elemSeparator
|
||||
* format: string
|
||||
*
|
||||
* @param preserveCurrentColor
|
||||
* format: boolean
|
||||
*
|
||||
* @param attrs:
|
||||
*
|
||||
* format: [ element* : attribute* : value* ]
|
||||
*
|
||||
* element : regexp (wrapped into ^...$), single * or omitted > all elements (must be present when value is used)
|
||||
* attribute : regexp (wrapped into ^...$)
|
||||
* value : regexp (wrapped into ^...$), single * or omitted > all values
|
||||
*
|
||||
* examples:
|
||||
*
|
||||
* > basic: remove fill attribute
|
||||
* ---
|
||||
* removeAttrs:
|
||||
* attrs: 'fill'
|
||||
*
|
||||
* > remove fill attribute on path element
|
||||
* ---
|
||||
* attrs: 'path:fill'
|
||||
*
|
||||
* > remove fill attribute on path element where value is none
|
||||
* ---
|
||||
* attrs: 'path:fill:none'
|
||||
*
|
||||
*
|
||||
* > remove all fill and stroke attribute
|
||||
* ---
|
||||
* attrs:
|
||||
* - 'fill'
|
||||
* - 'stroke'
|
||||
*
|
||||
* [is same as]
|
||||
*
|
||||
* attrs: '(fill|stroke)'
|
||||
*
|
||||
* [is same as]
|
||||
*
|
||||
* attrs: '*:(fill|stroke)'
|
||||
*
|
||||
* [is same as]
|
||||
*
|
||||
* attrs: '.*:(fill|stroke)'
|
||||
*
|
||||
* [is same as]
|
||||
*
|
||||
* attrs: '.*:(fill|stroke):.*'
|
||||
*
|
||||
*
|
||||
* > remove all stroke related attributes
|
||||
* ----
|
||||
* attrs: 'stroke.*'
|
||||
*
|
||||
*
|
||||
* @param {Object} item current iteration item
|
||||
* @param {Object} params plugin params
|
||||
* @return {Boolean} if false, item will be filtered out
|
||||
*
|
||||
* @author Benny Schudel
|
||||
*/
|
||||
exports.fn = function(item, params) {
|
||||
// wrap into an array if params is not
|
||||
if (!Array.isArray(params.attrs)) {
|
||||
params.attrs = [params.attrs];
|
||||
}
|
||||
|
||||
if (item.isElem()) {
|
||||
var elemSeparator = typeof params.elemSeparator == 'string' ? params.elemSeparator : DEFAULT_SEPARATOR;
|
||||
var preserveCurrentColor = typeof params.preserveCurrentColor == 'boolean' ? params.preserveCurrentColor : false;
|
||||
|
||||
// prepare patterns
|
||||
var patterns = params.attrs.map(function(pattern) {
|
||||
|
||||
// if no element separators (:), assume it's attribute name, and apply to all elements *regardless of value*
|
||||
if (pattern.indexOf(elemSeparator) === -1) {
|
||||
pattern = ['.*', elemSeparator, pattern, elemSeparator, '.*'].join('');
|
||||
|
||||
// if only 1 separator, assume it's element and attribute name, and apply regardless of attribute value
|
||||
} else if (pattern.split(elemSeparator).length < 3) {
|
||||
pattern = [pattern, elemSeparator, '.*'].join('');
|
||||
}
|
||||
|
||||
// create regexps for element, attribute name, and attribute value
|
||||
return pattern.split(elemSeparator)
|
||||
.map(function(value) {
|
||||
|
||||
// adjust single * to match anything
|
||||
if (value === '*') { value = '.*'; }
|
||||
|
||||
return new RegExp(['^', value, '$'].join(''), 'i');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// loop patterns
|
||||
patterns.forEach(function(pattern) {
|
||||
|
||||
// matches element
|
||||
if (pattern[0].test(item.elem)) {
|
||||
|
||||
// loop attributes
|
||||
item.eachAttr(function(attr) {
|
||||
var name = attr.name;
|
||||
var value = attr.value;
|
||||
var isFillCurrentColor = preserveCurrentColor && name == 'fill' && value == 'currentColor';
|
||||
var isStrokeCurrentColor = preserveCurrentColor && name == 'stroke' && value == 'currentColor';
|
||||
|
||||
if (!(isFillCurrentColor || isStrokeCurrentColor)) {
|
||||
// matches attribute name
|
||||
if (pattern[1].test(name)) {
|
||||
|
||||
// matches attribute value
|
||||
if (pattern[2].test(attr.value)) {
|
||||
item.removeAttr(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
'use strict';
|
||||
|
||||
exports.type = 'perItem';
|
||||
|
||||
exports.active = true;
|
||||
|
||||
exports.description = 'removes comments';
|
||||
|
||||
/**
|
||||
* Remove comments.
|
||||
*
|
||||
* @example
|
||||
* <!-- Generator: Adobe Illustrator 15.0.0, SVG Export
|
||||
* Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
*
|
||||
* @param {Object} item current iteration item
|
||||
* @return {Boolean} if false, item will be filtered out
|
||||
*
|
||||
* @author Kir Belevich
|
||||
*/
|
||||
exports.fn = function(item) {
|
||||
|
||||
if (item.comment && item.comment.charAt(0) !== '!') {
|
||||
return false;
|
||||
}
|
||||
|
||||
};
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
'use strict';
|
||||
|
||||
exports.type = 'perItem';
|
||||
|
||||
exports.active = true;
|
||||
|
||||
exports.params = {
|
||||
removeAny: true
|
||||
};
|
||||
|
||||
exports.description = 'removes <desc>';
|
||||
|
||||
var standardDescs = /^(Created with|Created using)/;
|
||||
|
||||
/**
|
||||
* Removes <desc>.
|
||||
* Removes only standard editors content or empty elements 'cause it can be used for accessibility.
|
||||
* Enable parameter 'removeAny' to remove any description.
|
||||
*
|
||||
* https://developer.mozilla.org/en-US/docs/Web/SVG/Element/desc
|
||||
*
|
||||
* @param {Object} item current iteration item
|
||||
* @return {Boolean} if false, item will be filtered out
|
||||
*
|
||||
* @author Daniel Wabyick
|
||||
*/
|
||||
exports.fn = function(item, params) {
|
||||
|
||||
return !item.isElem('desc') || !(params.removeAny || item.isEmpty() ||
|
||||
standardDescs.test(item.content[0].text));
|
||||
|
||||
};
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
'use strict';
|
||||
|
||||
exports.type = 'perItem';
|
||||
|
||||
exports.active = false;
|
||||
|
||||
exports.description = 'removes width and height in presence of viewBox (opposite to removeViewBox, disable it first)';
|
||||
|
||||
/**
|
||||
* Remove width/height attributes and add the viewBox attribute if it's missing
|
||||
*
|
||||
* @example
|
||||
* <svg width="100" height="50" />
|
||||
* ↓
|
||||
* <svg viewBox="0 0 100 50" />
|
||||
*
|
||||
* @param {Object} item current iteration item
|
||||
* @return {Boolean} if true, with and height will be filtered out
|
||||
*
|
||||
* @author Benny Schudel
|
||||
*/
|
||||
exports.fn = function(item) {
|
||||
|
||||
if (item.isElem('svg')) {
|
||||
if (item.hasAttr('viewBox')) {
|
||||
item.removeAttr('width');
|
||||
item.removeAttr('height');
|
||||
} else if (
|
||||
item.hasAttr('width') &&
|
||||
item.hasAttr('height') &&
|
||||
!isNaN(Number(item.attr('width').value)) &&
|
||||
!isNaN(Number(item.attr('height').value))
|
||||
) {
|
||||
item.addAttr({
|
||||
name: 'viewBox',
|
||||
value:
|
||||
'0 0 ' +
|
||||
Number(item.attr('width').value) +
|
||||
' ' +
|
||||
Number(item.attr('height').value),
|
||||
prefix: '',
|
||||
local: 'viewBox'
|
||||
});
|
||||
item.removeAttr('width');
|
||||
item.removeAttr('height');
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
'use strict';
|
||||
|
||||
exports.type = 'perItem';
|
||||
|
||||
exports.active = true;
|
||||
|
||||
exports.description = 'removes doctype declaration';
|
||||
|
||||
/**
|
||||
* Remove DOCTYPE declaration.
|
||||
*
|
||||
* "Unfortunately the SVG DTDs are a source of so many
|
||||
* issues that the SVG WG has decided not to write one
|
||||
* for the upcoming SVG 1.2 standard. In fact SVG WG
|
||||
* members are even telling people not to use a DOCTYPE
|
||||
* declaration in SVG 1.0 and 1.1 documents"
|
||||
* https://jwatt.org/svg/authoring/#doctype-declaration
|
||||
*
|
||||
* @example
|
||||
* <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
* q"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
*
|
||||
* @example
|
||||
* <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
* "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" [
|
||||
* <!-- an internal subset can be embedded here -->
|
||||
* ]>
|
||||
*
|
||||
* @param {Object} item current iteration item
|
||||
* @return {Boolean} if false, item will be filtered out
|
||||
*
|
||||
* @author Kir Belevich
|
||||
*/
|
||||
exports.fn = function(item) {
|
||||
|
||||
if (item.doctype) {
|
||||
return false;
|
||||
}
|
||||
|
||||
};
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
'use strict';
|
||||
|
||||
exports.type = 'perItem';
|
||||
|
||||
exports.active = true;
|
||||
|
||||
exports.description = 'removes editors namespaces, elements and attributes';
|
||||
|
||||
var editorNamespaces = require('./_collections').editorNamespaces,
|
||||
prefixes = [];
|
||||
|
||||
exports.params = {
|
||||
additionalNamespaces: []
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove editors namespaces, elements and attributes.
|
||||
*
|
||||
* @example
|
||||
* <svg xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd">
|
||||
* <sodipodi:namedview/>
|
||||
* <path sodipodi:nodetypes="cccc"/>
|
||||
*
|
||||
* @param {Object} item current iteration item
|
||||
* @param {Object} params plugin params
|
||||
* @return {Boolean} if false, item will be filtered out
|
||||
*
|
||||
* @author Kir Belevich
|
||||
*/
|
||||
exports.fn = function(item, params) {
|
||||
|
||||
if (Array.isArray(params.additionalNamespaces)) {
|
||||
editorNamespaces = editorNamespaces.concat(params.additionalNamespaces);
|
||||
}
|
||||
|
||||
if (item.elem) {
|
||||
|
||||
if (item.isElem('svg')) {
|
||||
|
||||
item.eachAttr(function(attr) {
|
||||
if (attr.prefix === 'xmlns' && editorNamespaces.indexOf(attr.value) > -1) {
|
||||
prefixes.push(attr.local);
|
||||
|
||||
// <svg xmlns:sodipodi="">
|
||||
item.removeAttr(attr.name);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// <* sodipodi:*="">
|
||||
item.eachAttr(function(attr) {
|
||||
if (prefixes.indexOf(attr.prefix) > -1) {
|
||||
item.removeAttr(attr.name);
|
||||
}
|
||||
});
|
||||
|
||||
// <sodipodi:*>
|
||||
if (prefixes.indexOf(item.prefix) > -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
'use strict';
|
||||
|
||||
exports.type = 'perItem';
|
||||
|
||||
exports.active = false;
|
||||
|
||||
exports.description = 'removes arbitrary elements by ID or className (disabled by default)';
|
||||
|
||||
exports.params = {
|
||||
id: [],
|
||||
class: []
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove arbitrary SVG elements by ID or className.
|
||||
*
|
||||
* @param id
|
||||
* examples:
|
||||
*
|
||||
* > single: remove element with ID of `elementID`
|
||||
* ---
|
||||
* removeElementsByAttr:
|
||||
* id: 'elementID'
|
||||
*
|
||||
* > list: remove multiple elements by ID
|
||||
* ---
|
||||
* removeElementsByAttr:
|
||||
* id:
|
||||
* - 'elementID'
|
||||
* - 'anotherID'
|
||||
*
|
||||
* @param class
|
||||
* examples:
|
||||
*
|
||||
* > single: remove all elements with class of `elementClass`
|
||||
* ---
|
||||
* removeElementsByAttr:
|
||||
* class: 'elementClass'
|
||||
*
|
||||
* > list: remove all elements with class of `elementClass` or `anotherClass`
|
||||
* ---
|
||||
* removeElementsByAttr:
|
||||
* class:
|
||||
* - 'elementClass'
|
||||
* - 'anotherClass'
|
||||
*
|
||||
* @param {Object} item current iteration item
|
||||
* @param {Object} params plugin params
|
||||
* @return {Boolean} if false, item will be filtered out
|
||||
*
|
||||
* @author Eli Dupuis (@elidupuis)
|
||||
*/
|
||||
exports.fn = function(item, params) {
|
||||
var elemId, elemClass;
|
||||
|
||||
// wrap params in an array if not already
|
||||
['id', 'class'].forEach(function(key) {
|
||||
if (!Array.isArray(params[key])) {
|
||||
params[key] = [ params[key] ];
|
||||
}
|
||||
});
|
||||
|
||||
// abort if current item is no an element
|
||||
if (!item.isElem()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// remove element if it's `id` matches configured `id` params
|
||||
elemId = item.attr('id');
|
||||
if (elemId) {
|
||||
return params.id.indexOf(elemId.value) === -1;
|
||||
}
|
||||
|
||||
// remove element if it's `class` contains any of the configured `class` params
|
||||
elemClass = item.attr('class');
|
||||
if (elemClass) {
|
||||
var hasClassRegex = new RegExp(params.class.join('|'));
|
||||
return !hasClassRegex.test(elemClass.value);
|
||||
}
|
||||
};
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
'use strict';
|
||||
|
||||
exports.type = 'perItem';
|
||||
|
||||
exports.active = true;
|
||||
|
||||
exports.description = 'removes empty attributes';
|
||||
|
||||
/**
|
||||
* Remove attributes with empty values.
|
||||
*
|
||||
* @param {Object} item current iteration item
|
||||
* @return {Boolean} if false, item will be filtered out
|
||||
*
|
||||
* @author Kir Belevich
|
||||
*/
|
||||
exports.fn = function(item) {
|
||||
|
||||
if (item.elem) {
|
||||
|
||||
item.eachAttr(function(attr) {
|
||||
if (attr.value === '') {
|
||||
item.removeAttr(attr.name);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
'use strict';
|
||||
|
||||
exports.type = 'perItemReverse';
|
||||
|
||||
exports.active = true;
|
||||
|
||||
exports.description = 'removes empty container elements';
|
||||
|
||||
var container = require('./_collections').elemsGroups.container;
|
||||
|
||||
/**
|
||||
* Remove empty containers.
|
||||
*
|
||||
* @see http://www.w3.org/TR/SVG/intro.html#TermContainerElement
|
||||
*
|
||||
* @example
|
||||
* <defs/>
|
||||
*
|
||||
* @example
|
||||
* <g><marker><a/></marker></g>
|
||||
*
|
||||
* @param {Object} item current iteration item
|
||||
* @return {Boolean} if false, item will be filtered out
|
||||
*
|
||||
* @author Kir Belevich
|
||||
*/
|
||||
exports.fn = function(item) {
|
||||
|
||||
return !(item.isElem(container) && !item.isElem('svg') && item.isEmpty() &&
|
||||
(!item.isElem('pattern') || !item.hasAttrLocal('href')));
|
||||
|
||||
};
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
'use strict';
|
||||
|
||||
exports.type = 'perItem';
|
||||
|
||||
exports.active = true;
|
||||
|
||||
exports.description = 'removes empty <text> elements';
|
||||
|
||||
exports.params = {
|
||||
text: true,
|
||||
tspan: true,
|
||||
tref: true
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove empty Text elements.
|
||||
*
|
||||
* @see http://www.w3.org/TR/SVG/text.html
|
||||
*
|
||||
* @example
|
||||
* Remove empty text element:
|
||||
* <text/>
|
||||
*
|
||||
* Remove empty tspan element:
|
||||
* <tspan/>
|
||||
*
|
||||
* Remove tref with empty xlink:href attribute:
|
||||
* <tref xlink:href=""/>
|
||||
*
|
||||
* @param {Object} item current iteration item
|
||||
* @param {Object} params plugin params
|
||||
* @return {Boolean} if false, item will be filtered out
|
||||
*
|
||||
* @author Kir Belevich
|
||||
*/
|
||||
exports.fn = function(item, params) {
|
||||
|
||||
// Remove empty text element
|
||||
if (
|
||||
params.text &&
|
||||
item.isElem('text') &&
|
||||
item.isEmpty()
|
||||
) return false;
|
||||
|
||||
// Remove empty tspan element
|
||||
if (
|
||||
params.tspan &&
|
||||
item.isElem('tspan') &&
|
||||
item.isEmpty()
|
||||
) return false;
|
||||
|
||||
// Remove tref with empty xlink:href attribute
|
||||
if (
|
||||
params.tref &&
|
||||
item.isElem('tref') &&
|
||||
!item.hasAttrLocal('href')
|
||||
) return false;
|
||||
|
||||
};
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
'use strict';
|
||||
|
||||
exports.type = 'perItem';
|
||||
|
||||
exports.active = true;
|
||||
|
||||
exports.description = 'removes hidden elements (zero sized, with absent attributes)';
|
||||
|
||||
exports.params = {
|
||||
isHidden: true,
|
||||
displayNone: true,
|
||||
opacity0: true,
|
||||
circleR0: true,
|
||||
ellipseRX0: true,
|
||||
ellipseRY0: true,
|
||||
rectWidth0: true,
|
||||
rectHeight0: true,
|
||||
patternWidth0: true,
|
||||
patternHeight0: true,
|
||||
imageWidth0: true,
|
||||
imageHeight0: true,
|
||||
pathEmptyD: true,
|
||||
polylineEmptyPoints: true,
|
||||
polygonEmptyPoints: true
|
||||
};
|
||||
|
||||
var regValidPath = /M\s*(?:[-+]?(?:\d*\.\d+|\d+(?:\.|(?!\.)))([eE][-+]?\d+)?(?!\d)\s*,?\s*){2}\D*\d/i;
|
||||
|
||||
/**
|
||||
* Remove hidden elements with disabled rendering:
|
||||
* - display="none"
|
||||
* - opacity="0"
|
||||
* - circle with zero radius
|
||||
* - ellipse with zero x-axis or y-axis radius
|
||||
* - rectangle with zero width or height
|
||||
* - pattern with zero width or height
|
||||
* - image with zero width or height
|
||||
* - path with empty data
|
||||
* - polyline with empty points
|
||||
* - polygon with empty points
|
||||
*
|
||||
* @param {Object} item current iteration item
|
||||
* @param {Object} params plugin params
|
||||
* @return {Boolean} if false, item will be filtered out
|
||||
*
|
||||
* @author Kir Belevich
|
||||
*/
|
||||
exports.fn = function (item, params) {
|
||||
|
||||
if (item.elem) {
|
||||
// Removes hidden elements
|
||||
// https://www.w3schools.com/cssref/pr_class_visibility.asp
|
||||
if (
|
||||
params.isHidden &&
|
||||
item.hasAttr('visibility', 'hidden')
|
||||
) return false;
|
||||
|
||||
// display="none"
|
||||
//
|
||||
// http://www.w3.org/TR/SVG/painting.html#DisplayProperty
|
||||
// "A value of display: none indicates that the given element
|
||||
// and its children shall not be rendered directly"
|
||||
if (
|
||||
params.displayNone &&
|
||||
item.hasAttr('display', 'none')
|
||||
) return false;
|
||||
|
||||
// opacity="0"
|
||||
//
|
||||
// http://www.w3.org/TR/SVG/masking.html#ObjectAndGroupOpacityProperties
|
||||
if (
|
||||
params.opacity0 &&
|
||||
item.hasAttr('opacity', '0')
|
||||
) return false;
|
||||
|
||||
// Circles with zero radius
|
||||
//
|
||||
// http://www.w3.org/TR/SVG/shapes.html#CircleElementRAttribute
|
||||
// "A value of zero disables rendering of the element"
|
||||
//
|
||||
// <circle r="0">
|
||||
if (
|
||||
params.circleR0 &&
|
||||
item.isElem('circle') &&
|
||||
item.isEmpty() &&
|
||||
item.hasAttr('r', '0')
|
||||
) return false;
|
||||
|
||||
// Ellipse with zero x-axis radius
|
||||
//
|
||||
// http://www.w3.org/TR/SVG/shapes.html#EllipseElementRXAttribute
|
||||
// "A value of zero disables rendering of the element"
|
||||
//
|
||||
// <ellipse rx="0">
|
||||
if (
|
||||
params.ellipseRX0 &&
|
||||
item.isElem('ellipse') &&
|
||||
item.isEmpty() &&
|
||||
item.hasAttr('rx', '0')
|
||||
) return false;
|
||||
|
||||
// Ellipse with zero y-axis radius
|
||||
//
|
||||
// http://www.w3.org/TR/SVG/shapes.html#EllipseElementRYAttribute
|
||||
// "A value of zero disables rendering of the element"
|
||||
//
|
||||
// <ellipse ry="0">
|
||||
if (
|
||||
params.ellipseRY0 &&
|
||||
item.isElem('ellipse') &&
|
||||
item.isEmpty() &&
|
||||
item.hasAttr('ry', '0')
|
||||
) return false;
|
||||
|
||||
// Rectangle with zero width
|
||||
//
|
||||
// http://www.w3.org/TR/SVG/shapes.html#RectElementWidthAttribute
|
||||
// "A value of zero disables rendering of the element"
|
||||
//
|
||||
// <rect width="0">
|
||||
if (
|
||||
params.rectWidth0 &&
|
||||
item.isElem('rect') &&
|
||||
item.isEmpty() &&
|
||||
item.hasAttr('width', '0')
|
||||
) return false;
|
||||
|
||||
// Rectangle with zero height
|
||||
//
|
||||
// http://www.w3.org/TR/SVG/shapes.html#RectElementHeightAttribute
|
||||
// "A value of zero disables rendering of the element"
|
||||
//
|
||||
// <rect height="0">
|
||||
if (
|
||||
params.rectHeight0 &&
|
||||
params.rectWidth0 &&
|
||||
item.isElem('rect') &&
|
||||
item.isEmpty() &&
|
||||
item.hasAttr('height', '0')
|
||||
) return false;
|
||||
|
||||
// Pattern with zero width
|
||||
//
|
||||
// http://www.w3.org/TR/SVG/pservers.html#PatternElementWidthAttribute
|
||||
// "A value of zero disables rendering of the element (i.e., no paint is applied)"
|
||||
//
|
||||
// <pattern width="0">
|
||||
if (
|
||||
params.patternWidth0 &&
|
||||
item.isElem('pattern') &&
|
||||
item.hasAttr('width', '0')
|
||||
) return false;
|
||||
|
||||
// Pattern with zero height
|
||||
//
|
||||
// http://www.w3.org/TR/SVG/pservers.html#PatternElementHeightAttribute
|
||||
// "A value of zero disables rendering of the element (i.e., no paint is applied)"
|
||||
//
|
||||
// <pattern height="0">
|
||||
if (
|
||||
params.patternHeight0 &&
|
||||
item.isElem('pattern') &&
|
||||
item.hasAttr('height', '0')
|
||||
) return false;
|
||||
|
||||
// Image with zero width
|
||||
//
|
||||
// http://www.w3.org/TR/SVG/struct.html#ImageElementWidthAttribute
|
||||
// "A value of zero disables rendering of the element"
|
||||
//
|
||||
// <image width="0">
|
||||
if (
|
||||
params.imageWidth0 &&
|
||||
item.isElem('image') &&
|
||||
item.hasAttr('width', '0')
|
||||
) return false;
|
||||
|
||||
// Image with zero height
|
||||
//
|
||||
// http://www.w3.org/TR/SVG/struct.html#ImageElementHeightAttribute
|
||||
// "A value of zero disables rendering of the element"
|
||||
//
|
||||
// <image height="0">
|
||||
if (
|
||||
params.imageHeight0 &&
|
||||
item.isElem('image') &&
|
||||
item.hasAttr('height', '0')
|
||||
) return false;
|
||||
|
||||
// Path with empty data
|
||||
//
|
||||
// http://www.w3.org/TR/SVG/paths.html#DAttribute
|
||||
//
|
||||
// <path d=""/>
|
||||
if (
|
||||
params.pathEmptyD &&
|
||||
item.isElem('path') &&
|
||||
(!item.hasAttr('d') || !regValidPath.test(item.attr('d').value))
|
||||
) return false;
|
||||
|
||||
// Polyline with empty points
|
||||
//
|
||||
// http://www.w3.org/TR/SVG/shapes.html#PolylineElementPointsAttribute
|
||||
//
|
||||
// <polyline points="">
|
||||
if (
|
||||
params.polylineEmptyPoints &&
|
||||
item.isElem('polyline') &&
|
||||
!item.hasAttr('points')
|
||||
) return false;
|
||||
|
||||
// Polygon with empty points
|
||||
//
|
||||
// http://www.w3.org/TR/SVG/shapes.html#PolygonElementPointsAttribute
|
||||
//
|
||||
// <polygon points="">
|
||||
if (
|
||||
params.polygonEmptyPoints &&
|
||||
item.isElem('polygon') &&
|
||||
!item.hasAttr('points')
|
||||
) return false;
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
'use strict';
|
||||
|
||||
exports.type = 'perItem';
|
||||
|
||||
exports.active = true;
|
||||
|
||||
exports.description = 'removes <metadata>';
|
||||
|
||||
/**
|
||||
* Remove <metadata>.
|
||||
*
|
||||
* http://www.w3.org/TR/SVG/metadata.html
|
||||
*
|
||||
* @param {Object} item current iteration item
|
||||
* @return {Boolean} if false, item will be filtered out
|
||||
*
|
||||
* @author Kir Belevich
|
||||
*/
|
||||
exports.fn = function(item) {
|
||||
|
||||
return !item.isElem('metadata');
|
||||
|
||||
};
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
'use strict';
|
||||
|
||||
exports.type = 'perItem';
|
||||
|
||||
exports.active = true;
|
||||
|
||||
exports.description = 'removes non-inheritable group’s presentational attributes';
|
||||
|
||||
var inheritableAttrs = require('./_collections').inheritableAttrs,
|
||||
attrsGroups = require('./_collections').attrsGroups,
|
||||
applyGroups = require('./_collections').presentationNonInheritableGroupAttrs;
|
||||
|
||||
/**
|
||||
* Remove non-inheritable group's "presentation" attributes.
|
||||
*
|
||||
* @param {Object} item current iteration item
|
||||
* @return {Boolean} if false, item will be filtered out
|
||||
*
|
||||
* @author Kir Belevich
|
||||
*/
|
||||
exports.fn = function(item) {
|
||||
|
||||
if (item.isElem('g')) {
|
||||
|
||||
item.eachAttr(function(attr) {
|
||||
if (
|
||||
~attrsGroups.presentation.indexOf(attr.name) &&
|
||||
!~inheritableAttrs.indexOf(attr.name) &&
|
||||
!~applyGroups.indexOf(attr.name)
|
||||
) {
|
||||
item.removeAttr(attr.name);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
'use strict';
|
||||
|
||||
exports.type = 'perItem';
|
||||
|
||||
exports.active = false;
|
||||
|
||||
exports.description = 'removes elements that are drawn outside of the viewbox (disabled by default)';
|
||||
|
||||
var SVGO = require('../lib/svgo.js'),
|
||||
_path = require('./_path.js'),
|
||||
intersects = _path.intersects,
|
||||
path2js = _path.path2js,
|
||||
viewBox,
|
||||
viewBoxJS;
|
||||
|
||||
/**
|
||||
* Remove elements that are drawn outside of the viewbox.
|
||||
*
|
||||
* @param {Object} item current iteration item
|
||||
* @return {Boolean} if false, item will be filtered out
|
||||
*
|
||||
* @author JoshyPHP
|
||||
*/
|
||||
exports.fn = function(item) {
|
||||
|
||||
if (item.isElem('path') && item.hasAttr('d') && typeof viewBox !== 'undefined')
|
||||
{
|
||||
// Consider that any item with a transform attribute or a M instruction
|
||||
// within the viewBox is visible
|
||||
if (hasTransform(item) || pathMovesWithinViewBox(item.attr('d').value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var pathJS = path2js(item);
|
||||
if (pathJS.length === 2)
|
||||
{
|
||||
// Use a closed clone of the path if it's too short for intersects()
|
||||
pathJS = JSON.parse(JSON.stringify(pathJS));
|
||||
pathJS.push({ instruction: 'z' });
|
||||
}
|
||||
|
||||
return intersects(viewBoxJS, pathJS);
|
||||
}
|
||||
if (item.isElem('svg'))
|
||||
{
|
||||
parseViewBox(item);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Test whether given item or any of its ancestors has a transform attribute.
|
||||
*
|
||||
* @param {String} path
|
||||
* @return {Boolean}
|
||||
*/
|
||||
function hasTransform(item)
|
||||
{
|
||||
return item.hasAttr('transform') || (item.parentNode && hasTransform(item.parentNode));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the viewBox coordinates and compute the JS representation of its path.
|
||||
*
|
||||
* @param {Object} svg svg element item
|
||||
*/
|
||||
function parseViewBox(svg)
|
||||
{
|
||||
var viewBoxData = '';
|
||||
if (svg.hasAttr('viewBox'))
|
||||
{
|
||||
// Remove commas and plus signs, normalize and trim whitespace
|
||||
viewBoxData = svg.attr('viewBox').value;
|
||||
}
|
||||
else if (svg.hasAttr('height') && svg.hasAttr('width'))
|
||||
{
|
||||
viewBoxData = '0 0 ' + svg.attr('width').value + ' ' + svg.attr('height').value;
|
||||
}
|
||||
|
||||
// Remove commas and plus signs, normalize and trim whitespace
|
||||
viewBoxData = viewBoxData.replace(/[,+]|px/g, ' ').replace(/\s+/g, ' ').replace(/^\s*|\s*$/g, '');
|
||||
|
||||
// Ensure that the dimensions are 4 values separated by space
|
||||
var m = /^(-?\d*\.?\d+) (-?\d*\.?\d+) (\d*\.?\d+) (\d*\.?\d+)$/.exec(viewBoxData);
|
||||
if (!m)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the viewBox boundaries
|
||||
viewBox = {
|
||||
left: parseFloat(m[1]),
|
||||
top: parseFloat(m[2]),
|
||||
right: parseFloat(m[1]) + parseFloat(m[3]),
|
||||
bottom: parseFloat(m[2]) + parseFloat(m[4])
|
||||
};
|
||||
|
||||
var path = new SVGO().createContentItem({
|
||||
elem: 'path',
|
||||
prefix: '',
|
||||
local: 'path'
|
||||
});
|
||||
path.addAttr({
|
||||
name: 'd',
|
||||
prefix: '',
|
||||
local: 'd',
|
||||
value: 'M' + m[1] + ' ' + m[2] + 'h' + m[3] + 'v' + m[4] + 'H' + m[1] + 'z'
|
||||
});
|
||||
|
||||
viewBoxJS = path2js(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether given path has a M instruction with coordinates within the viewBox.
|
||||
*
|
||||
* @param {String} path
|
||||
* @return {Boolean}
|
||||
*/
|
||||
function pathMovesWithinViewBox(path)
|
||||
{
|
||||
var regexp = /M\s*(-?\d*\.?\d+)(?!\d)\s*(-?\d*\.?\d+)/g, m;
|
||||
while (null !== (m = regexp.exec(path)))
|
||||
{
|
||||
if (m[1] >= viewBox.left && m[1] <= viewBox.right && m[2] >= viewBox.top && m[2] <= viewBox.bottom)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
exports.type = 'perItem';
|
||||
|
||||
exports.active = false;
|
||||
|
||||
exports.description = 'removes raster images (disabled by default)';
|
||||
|
||||
/**
|
||||
* Remove raster images references in <image>.
|
||||
*
|
||||
* @see https://bugs.webkit.org/show_bug.cgi?id=63548
|
||||
*
|
||||
* @param {Object} item current iteration item
|
||||
* @return {Boolean} if false, item will be filtered out
|
||||
*
|
||||
* @author Kir Belevich
|
||||
*/
|
||||
exports.fn = function(item) {
|
||||
|
||||
if (
|
||||
item.isElem('image') &&
|
||||
item.hasAttrLocal('href', /(\.|image\/)(jpg|png|gif)/)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
};
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
'use strict';
|
||||
|
||||
exports.type = 'perItem';
|
||||
|
||||
exports.active = false;
|
||||
|
||||
exports.description = 'removes <script> elements (disabled by default)';
|
||||
|
||||
/**
|
||||
* Remove <script>.
|
||||
*
|
||||
* https://www.w3.org/TR/SVG/script.html
|
||||
*
|
||||
* @param {Object} item current iteration item
|
||||
* @return {Boolean} if false, item will be filtered out
|
||||
*
|
||||
* @author Patrick Klingemann
|
||||
*/
|
||||
exports.fn = function(item) {
|
||||
|
||||
return !item.isElem('script');
|
||||
|
||||
};
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
'use strict';
|
||||
|
||||
exports.type = 'perItem';
|
||||
|
||||
exports.active = false;
|
||||
|
||||
exports.description = 'removes <style> element (disabled by default)';
|
||||
|
||||
/**
|
||||
* Remove <style>.
|
||||
*
|
||||
* http://www.w3.org/TR/SVG/styling.html#StyleElement
|
||||
*
|
||||
* @param {Object} item current iteration item
|
||||
* @return {Boolean} if false, item will be filtered out
|
||||
*
|
||||
* @author Betsy Dupuis
|
||||
*/
|
||||
exports.fn = function(item) {
|
||||
|
||||
return !item.isElem('style');
|
||||
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user