├── .github ├── dependabot.yml └── workflows │ ├── auto-merge-dependabot.yml │ ├── auto-releaser.yml │ ├── linter.yml │ ├── npmpublish.yml │ ├── tests.yml │ └── update-prefixes.yml ├── .gitignore ├── .jsbeautifyrc ├── .npmignore ├── .prettierignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── bin └── analyze-css.js ├── create-gh-release.sh ├── data └── prefixes.js ├── eslint.config.js ├── examples ├── _reset.scss ├── base.scss ├── elecena.css ├── example.js ├── propertyResets.css ├── sass.sass └── ti.mobile.css ├── lib ├── collection.js ├── css-analyzer.d.ts ├── css-analyzer.js ├── index.d.ts ├── index.js ├── preprocessors.js ├── preprocessors │ └── sass.js ├── runner.js └── types.d.ts ├── package-lock.json ├── package.json ├── rules ├── base64.js ├── bodySelectors.js ├── childSelectors.js ├── colors.js ├── comments.js ├── complex.js ├── duplicated.js ├── emptyRules.js ├── expressions.js ├── ieFixes.js ├── import.js ├── important.js ├── length.js ├── mediaQueries.js ├── minified.js ├── multiClassesSelectors.js ├── parsingErrors.js ├── prefixes.js ├── prefixes.json ├── propertyResets.js ├── qualified.js ├── specificity.js └── stats.js ├── scripts └── types.js └── test ├── colors.test.js ├── errors.test.js ├── fixes └── 280-url-with-semicolons.test.js ├── opts.test.js ├── rules.test.js ├── rules ├── base64.js ├── bodySelectors.js ├── childSelectors.js ├── colors.js ├── comments.js ├── complex.js ├── duplicated.js ├── emptyRules.js ├── expressions.js ├── ieFixes.js ├── import.js ├── important.js ├── length.js ├── mediaQueries.js ├── minified.js ├── multiClassesSelectors.js ├── parsingErrors.js ├── prefixes.js ├── propertyResets.js ├── qualified.js ├── specificity.js └── stats.js └── sass.test.js /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Basic set up 2 | # https://help.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates#package-ecosystem 3 | 4 | version: 2 5 | updates: 6 | 7 | - package-ecosystem: "npm" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: daily 16 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge-dependabot.yml: -------------------------------------------------------------------------------- 1 | # Based on https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/automating-dependabot-with-github-actions#enable-auto-merge-on-a-pull-request 2 | name: Dependabot auto-merge 3 | on: pull_request_target 4 | 5 | permissions: 6 | pull-requests: write 7 | contents: write 8 | 9 | jobs: 10 | dependabot: 11 | runs-on: ubuntu-latest 12 | if: ${{ github.actor == 'dependabot[bot]' }} 13 | steps: 14 | - name: Dependabot metadata 15 | id: metadata 16 | uses: dependabot/fetch-metadata@v2.4.0 17 | with: 18 | github-token: "${{ secrets.GITHUB_TOKEN }}" 19 | - name: Enable auto-merge for Dependabot PRs 20 | # Automatically merge semver-patch and semver-minor PRs 21 | if: "${{ steps.metadata.outputs.update-type == 22 | 'version-update:semver-minor' || 23 | steps.metadata.outputs.update-type == 24 | 'version-update:semver-patch' }}" 25 | run: gh pr merge --auto --merge "$PR_URL" 26 | env: 27 | PR_URL: ${{github.event.pull_request.html_url}} 28 | GITHUB_TOKEN: ${{secrets.PAT_TOKEN}} 29 | -------------------------------------------------------------------------------- /.github/workflows/auto-releaser.yml: -------------------------------------------------------------------------------- 1 | name: Auto-release specific changes 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | # Allows you to run this workflow manually from the Actions tab 8 | workflow_dispatch: 9 | 10 | jobs: 11 | auto-release: 12 | runs-on: ubuntu-latest 13 | timeout-minutes: 2 14 | 15 | env: 16 | GIT_USER_NAME: "macbre" 17 | GIT_USER_EMAIL: "" 18 | 19 | steps: 20 | # https://github.com/actions/checkout 21 | - name: Cloning ${{ env.GIT_BRANCH }} branch 22 | uses: actions/checkout@v4 23 | with: 24 | ref: master 25 | 26 | - uses: actions/setup-node@v4 27 | with: 28 | node-version: 'latest' 29 | cache: 'npm' 30 | - run: npm ci 31 | 32 | # https://github.com/actions/checkout#push-a-commit-using-the-built-in-token 33 | - name: Set up git and gh CLI tool 34 | run: | 35 | echo ${{ secrets.GITHUB_TOKEN }} | gh auth login --with-token 36 | gh auth status 37 | 38 | git config user.name ${GIT_USER_NAME} 39 | git config user.email ${GIT_USER_EMAIL} 40 | 41 | - name: Show the most recent changes 42 | run: | 43 | git status 44 | git log -n 1 45 | 46 | # https://docs.github.com/en/actions/learn-github-actions/contexts#github-context 47 | - name: Take the latest commit message 48 | id: commit-message 49 | env: 50 | GITHUB_CONTEXT: ${{ toJSON(github) }} 51 | run: | 52 | # -r output raw strings, not JSON texts; 53 | export COMMIT_MESSAGE=$(echo "$GITHUB_CONTEXT" | jq -r .event.head_commit.message | head -n1) 54 | 55 | set -x 56 | echo "::set-output name=commit-message::${COMMIT_MESSAGE}" 57 | echo "::notice::Got the commit message: ${COMMIT_MESSAGE}" 58 | 59 | - name: Automatically create a patch release 60 | # e.g. Updating rules/prefixes.json (#416) 61 | if: contains( steps.commit-message.outputs.commit-message , 'Updating rules/prefixes.json' ) || contains( steps.commit-message.outputs.commit-message , 'prefixes-update' ) 62 | run: | 63 | ./create-gh-release.sh 64 | 65 | echo "::notice::A new release has been created - $(npm ls --json | jq .version | sed 's/"//g')" 66 | -------------------------------------------------------------------------------- /.github/workflows/linter.yml: -------------------------------------------------------------------------------- 1 | name: Linter and code style 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | 8 | jobs: 9 | lint_and_style: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version: latest 17 | 18 | - name: Install dependencies 19 | run: npm ci 20 | 21 | - name: Run prettier 22 | run: npx prettier --check . 23 | 24 | - name: Run check-dts 25 | run: npm run check-dts 26 | 27 | - name: Run eslint 28 | run: npm run lint 29 | -------------------------------------------------------------------------------- /.github/workflows/npmpublish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Publish to npm and GitHub Packages 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | # try to publish a new version every 24 hours 11 | schedule: 12 | # * is a special character in YAML so you have to quote this string 13 | - cron: '0 9 * * *' 14 | 15 | # Allows you to run this workflow manually from the Actions tab 16 | workflow_dispatch: 17 | 18 | jobs: 19 | build: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: actions/setup-node@v4 24 | with: 25 | node-version: 'latest' 26 | cache: 'npm' 27 | - run: npm ci 28 | - run: npm test 29 | 30 | publish-npm: 31 | needs: build 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v4 35 | 36 | # 37 | # https://github.com/actions/setup-node/blob/main/docs/advanced-usage.md#publish-to-npmjs-and-gpr-with-npm 38 | # 39 | - uses: actions/setup-node@v4 40 | with: 41 | node-version: 16 42 | registry-url: 'https://npm.pkg.github.com' 43 | 44 | # GitHub Packages only supports scoped npm packages. 45 | # Scoped packages have names with the format of @owner/name 46 | # 47 | # https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-npm-registry#publishing-a-package 48 | - name: Publish to GitHub Packages 49 | run: | 50 | sed -i 's/"analyze-css"/"@macbre\/analyze-css"/g' package.json; git diff 51 | 52 | npm publish --access public || true 53 | 54 | git checkout -- . 55 | env: 56 | NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 57 | 58 | 59 | - uses: actions/setup-node@v4 60 | with: 61 | node-version: 16 62 | registry-url: 'https://registry.npmjs.org' 63 | 64 | - name: Publish to npm 65 | run: | 66 | npm publish --access public 67 | env: 68 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 69 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Tests 5 | 6 | on: 7 | push: 8 | branches: [ master, prefixes-update ] 9 | pull_request: 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 2 15 | 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | node-version: 20 | - 'lts/*' 21 | - 'latest' 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | 26 | - name: Use Node.js ${{ matrix.node-version }} 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: ${{ matrix.node-version }} 30 | cache: npm 31 | 32 | - name: Install dependencies 33 | run: npm ci 34 | 35 | - name: Run tests (without sass support) 36 | run: | 37 | npm test 38 | 39 | - name: Install sass package 40 | run: | 41 | # https://github.com/macbre/analyze-css/blob/devel/package-lock.json 42 | # e.g. sass@1.42.1 43 | export SASS_WITH_VERSION=$(grep 'registry.npmjs.org/sass/' package-lock.json | egrep -o 'sass-[0-9\.]+' | head -n 1 | sed 's/-/@/' | sed 's/\.$//g') 44 | 45 | echo "Installing ${SASS_WITH_VERSION} ..." 46 | 47 | set -x 48 | time npm install ${SASS_WITH_VERSION} 49 | 50 | - name: Run tests (with sass support) 51 | run: | 52 | npm test 53 | 54 | # https://github.com/marketplace/actions/coveralls-github-action 55 | # upload coverage report for just one of Node.js version matrix runs 56 | - name: Upload coverage report to Coveralls 57 | if: matrix.node-version == 'latest' 58 | uses: coverallsapp/github-action@v2.3.6 59 | continue-on-error: true 60 | with: 61 | github-token: ${{ github.token }} 62 | 63 | - name: Run bin/analyze-css.js for CSS file 64 | run: ./bin/analyze-css.js --file examples/ti.mobile.css -p | jq .metrics | grep '"' 65 | 66 | - name: Run bin/analyze-css.js for SCSS file 67 | run: ./bin/analyze-css.js --file examples/base.scss -p | jq .offenders | grep '"' 68 | 69 | - name: Run bin/analyze-css.js for stdin-provided CSS 70 | run: cat examples/ti.mobile.css | ./bin/analyze-css.js - | jq .metrics | grep '"' 71 | 72 | - name: Run bin/analyze-css.js for external file over HTTP 73 | run: ./bin/analyze-css.js --url http://s3.macbre.net/analyze-css/propertyResets.css -p | jq .metrics | grep '"' 74 | 75 | - name: Run bin/analyze-css.js for external file over HTTPS (--ignore-ssl-errors) 76 | run: ./bin/analyze-css.js --url https://s3.macbre.net/analyze-css/propertyResets.css --ignore-ssl-errors | jq .metrics | grep '"' 77 | -------------------------------------------------------------------------------- /.github/workflows/update-prefixes.yml: -------------------------------------------------------------------------------- 1 | name: CSS prefixes 2 | 3 | # Controls when the action will run. 4 | on: 5 | schedule: 6 | # * is a special character in YAML so you have to quote this string 7 | # run once a week, on Thursdays at 5:00 8 | - cron: '0 5 * * 4' 9 | 10 | # Allows you to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | jobs: 14 | update: 15 | runs-on: ubuntu-latest 16 | 17 | env: 18 | GIT_BRANCH: "master" 19 | GIT_USER_NAME: "macbre" 20 | GIT_USER_EMAIL: "" 21 | 22 | # Steps represent a sequence of tasks that will be executed as part of the job 23 | steps: 24 | # https://github.com/actions/checkout 25 | - name: Cloning ${{ env.GIT_BRANCH }} branch 26 | uses: actions/checkout@v4 27 | with: 28 | ref: ${{ env.GIT_BRANCH }} 29 | 30 | - name: Install Node.js 31 | uses: actions/setup-node@v4 32 | with: 33 | node-version: latest 34 | 35 | - name: Update prefixes 36 | run: | 37 | npm ci 38 | npm run prefixes 39 | 40 | # https://github.com/actions/checkout#push-a-commit-using-the-built-in-token 41 | - name: Set up git and gh CLI tool 42 | run: | 43 | echo ${{ secrets.PAT_TOKEN }} | gh auth login --with-token 44 | gh auth status 45 | 46 | git config user.name ${GIT_USER_NAME} 47 | git config user.email ${GIT_USER_EMAIL} 48 | 49 | - name: Show a diff 50 | run: | 51 | git checkout -- package* 52 | git diff 53 | 54 | - name: Commit if needed to a new branch (and create a PR) 55 | continue-on-error: true 56 | run: | 57 | set -x 58 | 59 | export PR_BRANCH="prefixes-update" 60 | git checkout -b ${PR_BRANCH} 61 | git add rules/prefixes.json && git checkout -- . 62 | git commit -m "Updating rules/prefixes.json with autoprefixer $(npm list --json | jq -r .dependencies.autoprefixer.version) and browserslist $(npm list --json | jq -r .dependencies.browserslist.version)" \ 63 | && git push origin ${PR_BRANCH} --force \ 64 | && npm version patch \ 65 | && git push origin ${PR_BRANCH} --force \ 66 | && git push --tags \ 67 | && gh pr create \ 68 | --assignee macbre \ 69 | --label dependencies \ 70 | --label "css-prefixes-update" \ 71 | --base ${{ env.GIT_BRANCH }} \ 72 | --title "Updating prefixes rules" \ 73 | --body "rules/prefixes.json bumped" 74 | 75 | # e.g. https://github.com/macbre/analyze-css/pull/443 76 | export PR_URL=$(gh pr view prefixes-update --json url | jq -r .url) 77 | echo "::notice::Pull request created: <${PR_URL}>" 78 | 79 | # now mark the PR as auto-merged 80 | # automated patch release will happen on merges to master 81 | gh pr merge --auto --merge "$PR_URL" 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | *.swp 10 | 11 | pids 12 | logs 13 | results 14 | coverage/ 15 | 16 | npm-debug.* 17 | node_modules 18 | .nyc_output/ 19 | -------------------------------------------------------------------------------- /.jsbeautifyrc: -------------------------------------------------------------------------------- 1 | { 2 | "indent_size": 0, 3 | "indent_with_tabs": true, 4 | "preserve_newlines": true, 5 | "end_with_newline": true, 6 | "max_preserve_newlines": 10, 7 | "jslint_happy": false, 8 | "brace_style": "collapse", 9 | "keep_array_indentation": true, 10 | "keep_function_indentation": false, 11 | "space_before_conditional": true, 12 | "eval_code": false, 13 | "unescape_strings": false, 14 | "wrap_line_length": 0 15 | } 16 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | data/ 3 | examples/ 4 | scripts/ 5 | test/ 6 | .github/ 7 | 8 | *.md 9 | .* 10 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | examples/ 2 | test/ 3 | coverage/ 4 | *.md 5 | *.yml 6 | *.json 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at maciej.brencz@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Maciej Brencz 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 21 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | analyze-css 2 | =========== 3 | 4 | [![NPM version](https://badge.fury.io/js/analyze-css.png)](http://badge.fury.io/js/analyze-css) 5 | [![Known Vulnerabilities](https://snyk.io/test/github/macbre/analyze-css/badge.svg)](https://snyk.io/test/github/macbre/analyze-css) 6 | [![Coverage Status](https://coveralls.io/repos/github/macbre/analyze-css/badge.svg?branch=devel)](https://coveralls.io/github/macbre/analyze-css?branch=devel) 7 | [![CodeFactor](https://www.codefactor.io/repository/github/macbre/analyze-css/badge)](https://www.codefactor.io/repository/github/macbre/analyze-css) 8 | 9 | [![Download stats](https://nodei.co/npm/analyze-css.png?downloads=true&downloadRank=true)](https://nodei.co/npm/analyze-css/) 10 | 11 | CSS selectors complexity and performance analyzer. analyze-css is built as a set of rules bound to events fired by CSS parser. Each rule can generate metrics and add "offenders" with more detailed information (see Usage section for an example). 12 | 13 | ## Install 14 | 15 | analyze-css comes as a "binary" for command-line and as CommonJS module. Run the following to install them globally: 16 | 17 | ``` 18 | npm install --global analyze-css 19 | ``` 20 | 21 | or to install from GitHub's repository: 22 | 23 | ``` 24 | npm install --global @macbre/analyze-css 25 | ``` 26 | 27 | ## Usage 28 | 29 | ### Command line tool 30 | 31 | You can use analyze-css "binary" to analyze local CSS files or remote CSS assets: 32 | 33 | ```sh 34 | $ analyze-css --file examples/elecena.css 35 | 36 | $ analyze-css --url http://s3.macbre.net/analyze-css/propertyResets.css 37 | $ analyze-css --url https://s3.macbre.net/analyze-css/propertyResets.css --ignore-ssl-errors 38 | ``` 39 | 40 | You can provide CSS via stdin as well (notice the dash: ``-``): 41 | 42 | ```sh 43 | $ echo ".foo {margin: 0 \!important}" | analyze-css - 44 | ``` 45 | 46 | This will emit JSON formatted results on ``stdout``. Use ``--pretty`` (or ``-p`` shortcut) option to make the output readable for human beings. 47 | 48 | Basic HTTP authentication can be provided through the options `--auth-user` and `--auth-pass`. 49 | 50 | HTTP proxy (e.g. `http://localhost:8080`) can be provided via: 51 | 52 | * `--proxy` or `-x` option 53 | * `HTTP_PROXY` env variable 54 | 55 | ### CommonJS module 56 | 57 | ``` 58 | npm i --save analyze-css 59 | ``` 60 | 61 | ```js 62 | const analyze = require('analyze-css'); 63 | 64 | (async() => { 65 | const results = await analyze('.foo {margin: 0 !important}'); 66 | console.log(results); // example? see below 67 | })(); 68 | ``` 69 | 70 | ```js 71 | // options can be provided 72 | const opts = { 73 | 'noOffenders': true 74 | }; 75 | 76 | (async() => { 77 | const results = await analyze('.foo {margin: 0 !important}', opts); 78 | console.log(results); // example? see below 79 | })(); 80 | ``` 81 | 82 | ### [grunt task](https://www.npmjs.org/package/grunt-contrib-analyze-css) 83 | 84 | > Created by @DeuxHuitHuit 85 | 86 | ```sh 87 | $ npm i grunt-contrib-analyze-css 88 | ``` 89 | 90 | It uses configurable threshold and compares the analyze-css result with it. 91 | 92 | ### Results 93 | 94 | ```json 95 | { 96 | "generator": "analyze-css v0.10.2", 97 | "metrics": { 98 | "base64Length": 11332, 99 | "redundantBodySelectors": 0, 100 | "redundantChildNodesSelectors": 1, 101 | "colors": 106, 102 | "comments": 1, 103 | "commentsLength": 68, 104 | "complexSelectors": 37, 105 | "duplicatedSelectors": 7, 106 | "duplicatedProperties": 24, 107 | "emptyRules": 0, 108 | "expressions": 0, 109 | "oldIEFixes": 51, 110 | "imports": 0, 111 | "importants": 3, 112 | "mediaQueries": 0, 113 | "notMinified": 0, 114 | "multiClassesSelectors": 74, 115 | "parsingErrors": 0, 116 | "oldPropertyPrefixes": 79, 117 | "propertyResets": 0, 118 | "qualifiedSelectors": 28, 119 | "specificityIdAvg": 0.04, 120 | "specificityIdTotal": 25, 121 | "specificityClassAvg": 1.27, 122 | "specificityClassTotal": 904, 123 | "specificityTagAvg": 0.79, 124 | "specificityTagTotal": 562, 125 | "selectors": 712, 126 | "selectorLengthAvg": 1.5722460658082975, 127 | "selectorsByAttribute": 92, 128 | "selectorsByClass": 600, 129 | "selectorsById": 25, 130 | "selectorsByPseudo": 167, 131 | "selectorsByTag": 533, 132 | "length": 55173, 133 | "rules": 433, 134 | "declarations": 1288 135 | }, 136 | "offenders": { 137 | "importants": [ 138 | ".foo {margin: 0 !important}" 139 | ] 140 | } 141 | } 142 | ``` 143 | 144 | ## Metrics 145 | 146 | * **base64Length**: total length of base64-encoded data in CSS source (will warn about base64-encoded data bigger than 4 kB) 147 | * **redundantBodySelectors**: number of redundant body selectors (e.g. ``body .foo``, ``section body h2``, but not ``body > h1``) 148 | * **redundantChildNodesSelectors**: number of redundant child nodes selectors (e.g. ``ul li``, ``table tr``) 149 | * **colors**: number of unique colors used in CSS 150 | * **comments**: number of comments in CSS source 151 | * **commentsLength**: length of comments content in CSS source 152 | * **complexSelectors**: number of complex selectors (consisting of more than three expressions, e.g. ``header ul li .foo``) 153 | * **duplicatedSelectors**: number of CSS selectors defined more than once in CSS source 154 | * **duplicatedProperties**: number of CSS property definitions duplicated within a selector 155 | * **emptyRules**: number of rules with no properties (e.g. ``.foo { }``) 156 | * **expressions**: number of rules with CSS expressions (e.g. ``expression( document.body.clientWidth > 600 ? "600px" : "auto" )``) 157 | * **oldIEFixes**: number of fixes for old versions of Internet Explorer (e.g. ``* html .foo {}`` and ``.foo { *zoom: 1 }``, [read](http://blogs.msdn.com/b/ie/archive/2005/09/02/460115.aspx) [more](http://www.impressivewebs.com/ie7-ie8-css-hacks/)) 158 | * **imports** number of ``@import`` rules 159 | * **importants**: number of properties with value forced by ``!important`` 160 | * **mediaQueries**: number of media queries (e.g. ``@media screen and (min-width: 1370px)``) 161 | * **notMinified**: set to 1 if the provided CSS is not minified 162 | * **multiClassesSelectors**: reports selectors with multiple classes (e.g. ``span.foo.bar``) 163 | * **parsingErrors**: number of CSS parsing errors 164 | * **oldPropertyPrefixes**: number of properties with no longer needed vendor prefix, powered by data provided by [autoprefixer](https://github.com/ai/autoprefixer) (e.g. ``--moz-border-radius``) 165 | * **propertyResets**: number of [accidental property resets](http://css-tricks.com/accidental-css-resets/) 166 | * **qualifiedSelectors**: number of [qualified selectors](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Writing_efficient_CSS) (e.g. ``header#nav``, ``.foo#bar``, ``h1.title``) 167 | * **specificityIdAvg**: average [specificity](http://css-tricks.com/specifics-on-css-specificity/) for ID 168 | * **specificityIdTotal**: total [specificity](http://css-tricks.com/specifics-on-css-specificity/) for ID 169 | * **specificityClassAvg**: average [specificity](http://css-tricks.com/specifics-on-css-specificity/) for class, pseudo-class or attribute 170 | * **specificityClassTotal**: total [specificity](http://css-tricks.com/specifics-on-css-specificity/) for class, pseudo-class or attribute 171 | * **specificityTagAvg**: average [specificity](http://css-tricks.com/specifics-on-css-specificity/) for element 172 | * **specificityTagTotal**: total [specificity](http://css-tricks.com/specifics-on-css-specificity/) for element 173 | * **selectors**: number of selectors (e.g. ``.foo, .bar { color: red }`` is counted as two selectors - ``.foo`` and ``.bar``) 174 | * **selectorLengthAvg**: average length of selector (e.g. for ``.foo .bar, #test div > span { color: red }`` will be set as 2.5) 175 | * **selectorsByAttribute**: number of selectors by attribute (e.g. ``.foo[value=bar]``) 176 | * **selectorsByClass**: number of selectors by class 177 | * **selectorsById**: number of selectors by ID 178 | * **selectorsByPseudo**: number of pseudo-selectors (e,g. ``:hover``) 179 | * **selectorsByTag**: number of selectors by tag name 180 | * **length**: length of CSS source (in bytes) 181 | * **rules**: number of rules (e.g. ``.foo, .bar { color: red }`` is counted as one rule) 182 | * **declarations**: number of declarations (e.g. ``.foo, .bar { color: red }`` is counted as one declaration - ``color: red``) 183 | 184 | ## Read more 185 | 186 | * [Optimize browser rendering](https://developers.google.com/speed/docs/best-practices/rendering) (by Google) 187 | * [Profiling CSS for fun and profit. Optimization notes.](http://perfectionkills.com/profiling-css-for-fun-and-profit-optimization-notes/) 188 | * [CSS specificity](http://css-tricks.com/specifics-on-css-specificity/) 189 | * [CSS Selector Performance has changed! (For the better)](http://calendar.perfplanet.com/2011/css-selector-performance-has-changed-for-the-better/) (by Nicole Sullivan) 190 | * [GitHub's CSS Performance](https://speakerdeck.com/jonrohan/githubs-css-performance) (by Joh Rohan) 191 | 192 | ## Dev hints 193 | 194 | Running tests and linting the code: 195 | 196 | ```sh 197 | $ npm test && npm run-script lint 198 | ``` 199 | 200 | Turning on debug mode (i.e. verbose logging to stderr via [debug module](https://npmjs.org/package/debug)): 201 | 202 | ```sh 203 | $ DEBUG=analyze-css* analyze-css ... 204 | ``` 205 | 206 | ## Stargazers over time 207 | 208 | [![Stargazers over time](https://starchart.cc/macbre/analyze-css.svg)](https://starchart.cc/macbre/analyze-css) 209 | -------------------------------------------------------------------------------- /bin/analyze-css.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * analyze-css entry point 5 | * 6 | * @see https://github.com/macbre/analyze-css 7 | */ 8 | "use strict"; 9 | 10 | const { program } = require("commander"); 11 | 12 | var analyzer = require("./../lib/index"), 13 | debug = require("debug")("analyze-css:bin"), 14 | runner = require("./../lib/runner"), 15 | runnerOpts = {}; 16 | 17 | // parse options 18 | program 19 | .version(analyzer.version) 20 | .usage("--url [options]") 21 | // https://www.npmjs.com/package/commander#common-option-types-boolean-and-value 22 | .option("--url ", "Set URL of CSS to analyze") 23 | .option("--file ", "Set local CSS file to analyze") 24 | .option( 25 | "--ignore-ssl-errors", 26 | "Ignores SSL errors, such as expired or self-signed certificate errors", 27 | ) 28 | .option("-p, --pretty", "Causes JSON with the results to be pretty-printed") 29 | .option( 30 | "-N, --no-offenders", 31 | "Show only the metrics without the offenders part", 32 | ) 33 | .option( 34 | "--auth-user ", 35 | "Sets the user name used for HTTP authentication", 36 | ) 37 | .option( 38 | "--auth-pass ", 39 | "Sets the password used for HTTP authentication", 40 | ) 41 | .option("-x, --proxy ", "Sets the HTTP proxy") 42 | // allowExcessArguments is now set to false by default 43 | // we need to let commander know that '-' argument is used to read input from stdin 44 | // https://github.com/tj/commander.js/issues/2149 45 | .argument("[input]", "Pass - as an argument to read CSS from stdin"); 46 | 47 | // parse it 48 | program.parse(process.argv); 49 | const options = program.opts(); 50 | 51 | debug("analyze-css v%s", analyzer.version); 52 | debug("argv %j", process.argv); 53 | debug("opts %j", options); 54 | 55 | // support stdin (issue #28) 56 | if (process.argv.indexOf("-") > -1) { 57 | runnerOpts.stdin = true; 58 | } 59 | // --url 60 | else if (options.url) { 61 | runnerOpts.url = options.url; 62 | } 63 | // --file 64 | else if (options.file) { 65 | runnerOpts.file = options.file; 66 | } 67 | // either --url or --file or - (stdin) needs to be provided 68 | else { 69 | console.log(program.helpInformation()); 70 | process.exit(analyzer.EXIT_NEED_OPTIONS); 71 | } 72 | 73 | runnerOpts.ignoreSslErrors = options.ignoreSslErrors === true; 74 | runnerOpts.noOffenders = program.offenders === false; 75 | runnerOpts.authUser = program["auth-user"]; 76 | runnerOpts.authPass = program["auth-pass"]; 77 | runnerOpts.proxy = program.proxy; 78 | 79 | debug("runner opts: %j", runnerOpts); 80 | 81 | // run the analyzer 82 | runner(runnerOpts, function (err, res) { 83 | var output, exitCode; 84 | 85 | // emit an error and die 86 | if (err) { 87 | exitCode = err.code || 1; 88 | debug("Exiting with exit code #%d", exitCode); 89 | 90 | console.error(err.toString()); 91 | process.exitCode = exitCode; 92 | return; 93 | } 94 | 95 | // make offenders flat (and append position if possible - issue #25) 96 | if (typeof res.offenders !== "undefined") { 97 | Object.keys(res.offenders).forEach(function (metricName) { 98 | res.offenders[metricName] = res.offenders[metricName].map( 99 | function (offender) { 100 | var position = offender.position && offender.position.start; 101 | return ( 102 | offender.message + 103 | (position ? " @ " + position.line + ":" + position.column : "") 104 | ); 105 | }, 106 | ); 107 | }); 108 | } 109 | 110 | // format the results 111 | if (options.pretty === true) { 112 | output = JSON.stringify(res, null, " "); 113 | } else { 114 | output = JSON.stringify(res); 115 | } 116 | 117 | process.stdout.write(output); 118 | }); 119 | -------------------------------------------------------------------------------- /create-gh-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # @see https://cli.github.com/manual/gh_release_create 3 | 4 | export VERSION=v$(npm ls --json | jq .version | sed 's/"//g') 5 | export NOTES=$(git log | grep 'Updating rules/prefixes.json' | head -n 1 | sed "s/ //g") 6 | 7 | echo "::notice::Creating a GitHub release for tag ${VERSION} (with notes '${NOTES}') ..." 8 | 9 | set -x 10 | gh release create $VERSION --title "${VERSION}: updating prefixes data" --notes "${NOTES}" && echo "::notice::Done" || echo "::error::Failed" 11 | -------------------------------------------------------------------------------- /data/prefixes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generates data for prefixes rule using data from autoprefixer 3 | * 4 | * @see https://github.com/ai/autoprefixer 5 | */ 6 | var autoprefixer = require("autoprefixer"), 7 | browserslist = require("browserslist"), 8 | debug = require("debug")("analyze-css:prefixes"), 9 | fs = require("fs"), 10 | prefixes = autoprefixer.data.prefixes, 11 | // data 12 | browsersByPrefix = {}, 13 | namesByVendor = {}, 14 | data; 15 | 16 | // prepare data 17 | data = { 18 | generated: 19 | "Using autoprefixer v" + 20 | require("../node_modules/autoprefixer/package.json").version, 21 | // supported browsers, i.e. will keep venoder prefixes that they require 22 | browsers: browserslist().sort(), 23 | // list of prefixes: prefix / hash (keep: true / false, msg: reason, list of browsers) 24 | prefixes: {}, 25 | }; 26 | 27 | debug("Generator: %s", data.generated); 28 | debug("Supported browsers: %s", data.browsers.join(", ")); 29 | 30 | // prepare vendors data 31 | // [prefix] => [supported browsers] 32 | Object.keys(autoprefixer.data.browsers).forEach(function (vendor) { 33 | var vendorData = autoprefixer.data.browsers[vendor], 34 | prefix = vendorData.prefix; 35 | 36 | if (typeof browsersByPrefix[prefix] === "undefined") { 37 | browsersByPrefix[prefix] = { 38 | names: [], 39 | browsers: [], 40 | }; 41 | } 42 | 43 | // push all browsers matching vendor 44 | data.browsers.forEach(function (browser) { 45 | // e.g. browser = 'ff 26' 46 | if (browser.split(" ")[0] === vendor) { 47 | browsersByPrefix[prefix].names.push(vendor); 48 | browsersByPrefix[prefix].browsers.push(browser); 49 | } 50 | }); 51 | 52 | // "and_uc" : "UC Browser for Android" 53 | namesByVendor[vendor] = vendorData.browser; 54 | }); 55 | 56 | debug("Browsers by prefix: %j", browsersByPrefix); 57 | debug("Names by vendor: %j", namesByVendor); 58 | 59 | function getLatestVersions(browsers, oldest) { 60 | var latest = {}, 61 | ret = []; 62 | 63 | oldest = !!oldest; 64 | 65 | browsers.forEach(function (browser) { 66 | var parts = browser.split(" "), 67 | vendor = parts[0], 68 | version = parseFloat(parts[1]); 69 | 70 | if (oldest) { 71 | // the oldest one 72 | latest[vendor] = Math.min(version, latest[vendor] || 1000); 73 | } else { 74 | // the latest version 75 | latest[vendor] = Math.max(version, latest[vendor] || 0); 76 | } 77 | }); 78 | 79 | Object.keys(latest).forEach(function (vendor) { 80 | ret.push((namesByVendor[vendor] || vendor) + " " + latest[vendor]); 81 | }); 82 | 83 | return ret; 84 | } 85 | 86 | // iterate through prefixes 87 | Object.keys(prefixes).forEach(function (property) { 88 | var propertyData = prefixes[property]; 89 | 90 | if (propertyData.selector || propertyData.props) { 91 | return; 92 | } 93 | 94 | Object.keys(browsersByPrefix).forEach(function (prefix) { 95 | // support browsers that should be checked against given prefix 96 | var prefixBrowsers = browsersByPrefix[prefix].browsers, 97 | vendorNames = browsersByPrefix[prefix].names, 98 | browsers, 99 | keep, 100 | msg; 101 | 102 | // check which supported browsers require this prefix 103 | browsers = prefixBrowsers.filter(function (browser) { 104 | return propertyData.browsers.indexOf(browser) > -1; 105 | }); 106 | 107 | // given prefix never existed 108 | if (propertyData.mistakes && propertyData.mistakes.indexOf(prefix) > -1) { 109 | msg = prefix + property + " is a mistake"; 110 | } 111 | // prefix no longer needed 112 | else if (browsers.length === 0) { 113 | // generate the list of old browsers requiring given prefix 114 | browsers = propertyData.browsers.filter(function (browser) { 115 | return vendorNames.indexOf(browser.split(" ")[0]) > -1; 116 | }); 117 | 118 | if (browsers.length > 0) { 119 | msg = 120 | "was required by " + 121 | getLatestVersions(browsers).join(", ") + 122 | " and earlier"; 123 | } else { 124 | // special handling for -ms- prefixes 125 | // @see http://msdn.microsoft.com/en-us/library/ie/ms530752(v=vs.85).aspx 126 | msg = "prefix is no longer supported"; 127 | } 128 | } 129 | // prefix still required by... 130 | else { 131 | keep = true; 132 | msg = 133 | "required by " + 134 | getLatestVersions(browsers, true).join(", ") + 135 | " and later"; 136 | } 137 | 138 | prefix = "-" + prefix + "-"; // "mozborder-radius" -> "-moz-border-radius" 139 | 140 | debug("%j", browsers); 141 | debug("%s: keep? %j (%s)", prefix + property, !!keep, msg); 142 | 143 | data.prefixes[prefix + property] = { 144 | keep: !!keep, 145 | msg: msg, 146 | }; 147 | }); 148 | }); 149 | 150 | // store in JSON file 151 | debug("Writing to a file..."); 152 | fs.writeFileSync( 153 | __dirname + "/../rules/prefixes.json", 154 | JSON.stringify(data, null, " "), 155 | ); 156 | 157 | debug("Done"); 158 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | const globals = require("globals"); 2 | const js = require("@eslint/js"); 3 | 4 | module.exports = [ 5 | js.configs.recommended, 6 | { 7 | languageOptions: { 8 | ecmaVersion: 2020, 9 | sourceType: "script", 10 | globals: { 11 | // https://eslint.org/docs/latest/use/configure/language-options#predefined-global-variables 12 | ...globals.browser, 13 | ...globals.node, 14 | }, 15 | }, 16 | rules: { 17 | semi: "error", 18 | "prefer-const": "error", 19 | "no-async-promise-executor": "off", 20 | "no-empty": ["error", { allowEmptyCatch: true }], 21 | "no-prototype-builtins": "off", 22 | "node/no-extraneous-require": "off", 23 | "node/shebang": "off", 24 | // https://eslint.org/docs/latest/rules/no-unused-vars 25 | "no-unused-vars": [ 26 | "error", 27 | { 28 | vars: "all", 29 | args: "after-used", 30 | caughtErrors: "none", // ignore catch block variables 31 | ignoreRestSiblings: false, 32 | reportUsedIgnorePattern: false, 33 | }, 34 | ], 35 | }, 36 | }, 37 | ]; 38 | -------------------------------------------------------------------------------- /examples/_reset.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | ul, 4 | ol { 5 | margin: 0; 6 | padding: 0; 7 | } 8 | -------------------------------------------------------------------------------- /examples/base.scss: -------------------------------------------------------------------------------- 1 | /* base.scss */ 2 | @import 'reset'; 3 | 4 | $font-stack: Helvetica, sans-serif; 5 | $primary-color: #333; 6 | 7 | body { 8 | color: $primary-color; 9 | font: 100% $font-stack; 10 | } 11 | -------------------------------------------------------------------------------- /examples/elecena.css: -------------------------------------------------------------------------------- 1 | article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}a:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}a:hover,a:active{outline:0}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{max-width:100%;vertical-align:middle;border:0;-ms-interpolation-mode:bicubic}#map_canvas img{max-width:none}button,input,select,textarea{margin:0;font-size:100%;vertical-align:middle}button,input{*overflow:visible;line-height:normal}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}button,input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button}input[type="search"]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none}textarea{overflow:auto;vertical-align:top}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:28px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box}body{margin:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;line-height:18px;color:#333;background-color:#fff}a{color:#08c;text-decoration:none}a:hover{color:#005580;text-decoration:underline}.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;content:""}.row:after{clear:both}[class*="span"]{float:left;margin-left:20px}.container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px}.span12{width:940px}.span11{width:860px}.span10{width:780px}.span9{width:700px}.span8{width:620px}.span7{width:540px}.span6{width:460px}.span5{width:380px}.span4{width:300px}.span3{width:220px}.span2{width:140px}.span1{width:60px}.offset12{margin-left:980px}.offset11{margin-left:900px}.offset10{margin-left:820px}.offset9{margin-left:740px}.offset8{margin-left:660px}.offset7{margin-left:580px}.offset6{margin-left:500px}.offset5{margin-left:420px}.offset4{margin-left:340px}.offset3{margin-left:260px}.offset2{margin-left:180px}.offset1{margin-left:100px}.container{margin-right:auto;margin-left:auto;*zoom:1}.container:before,.container:after{display:table;content:""}.container:after{clear:both}.container-fluid{padding-right:20px;padding-left:20px;*zoom:1}.container-fluid:before,.container-fluid:after{display:table;content:""}.container-fluid:after{clear:both}p{margin:0 0 9px}p small{font-size:11px;color:#999}.lead{margin-bottom:18px;font-size:20px;font-weight:200;line-height:27px}h1,h2,h3,h4,h5,h6{margin:0;font-family:inherit;font-weight:bold;color:inherit;text-rendering:optimizelegibility}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-weight:normal;color:#999}h1{font-size:30px;line-height:36px}h1 small{font-size:18px}h2{font-size:24px;line-height:36px}h2 small{font-size:18px}h3{font-size:18px;line-height:27px}h3 small{font-size:14px}h4,h5,h6{line-height:18px}h4{font-size:14px}h4 small{font-size:12px}h5{font-size:12px}h6{font-size:11px;color:#999;text-transform:uppercase}.page-header{padding-bottom:17px;margin:18px 0;border-bottom:1px solid #eee}.page-header h1{line-height:1}ul,ol{padding:0;margin:0 0 9px 25px}ul ul,ul ol,ol ol,ol ul{margin-bottom:0}ul{list-style:disc}ol{list-style:decimal}li{line-height:18px}ul.unstyled,ol.unstyled{margin-left:0;list-style:none}dl{margin-bottom:18px}dt,dd{line-height:18px}dt{font-weight:bold;line-height:17px}dd{margin-left:9px}.dl-horizontal dt{float:left;width:120px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:130px}hr{margin:18px 0;border:0;border-top:1px solid #eee;border-bottom:1px solid #fff}strong{font-weight:bold}em{font-style:italic}.muted{color:#999}abbr[title]{cursor:help;border-bottom:1px dotted #999}abbr.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:0 0 0 15px;margin:0 0 18px;border-left:5px solid #eee}blockquote p{margin-bottom:0;font-size:16px;font-weight:300;line-height:22.5px}blockquote small{display:block;line-height:18px;color:#999}blockquote small:before{content:'\2014 \00A0'}blockquote.pull-right{float:right;padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0}blockquote.pull-right p,blockquote.pull-right small{text-align:right}q:before,q:after,blockquote:before,blockquote:after{content:""}address{display:block;margin-bottom:18px;font-style:normal;line-height:18px}small{font-size:100%}cite{font-style:normal}code,pre{padding:0 3px 2px;font-family:Menlo,Monaco,Consolas,"Courier New",monospace;font-size:12px;color:#333;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}code{padding:2px 4px;color:#d14;background-color:#f7f7f9;border:1px solid #e1e1e8}pre{display:block;padding:8.5px;margin:0 0 9px;font-size:12.025px;line-height:18px;word-break:break-all;word-wrap:break-word;white-space:pre;white-space:pre-wrap;background-color:#f5f5f5;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}pre.prettyprint{margin-bottom:18px}pre code{padding:0;color:inherit;background-color:transparent;border:0}.pre-scrollable{max-height:340px;overflow-y:scroll}form{margin:0 0 18px}fieldset{padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:27px;font-size:19.5px;line-height:36px;color:#333;border:0;border-bottom:1px solid #e5e5e5}legend small{font-size:13.5px;color:#999}label,input,button,select,textarea{font-size:13px;font-weight:normal;line-height:18px}input,button,select,textarea{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif}label{display:block;margin-bottom:5px}select,textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{display:inline-block;height:18px;padding:4px;margin-bottom:9px;font-size:13px;line-height:18px;color:#555}input,textarea{width:210px}textarea{height:auto}textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{background-color:#fff;border:1px solid #ccc;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border linear 0.2s,box-shadow linear 0.2s;-moz-transition:border linear 0.2s,box-shadow linear 0.2s;-ms-transition:border linear 0.2s,box-shadow linear 0.2s;-o-transition:border linear 0.2s,box-shadow linear 0.2s;transition:border linear 0.2s,box-shadow linear 0.2s}textarea:focus,input[type="text"]:focus,input[type="password"]:focus,input[type="datetime"]:focus,input[type="datetime-local"]:focus,input[type="date"]:focus,input[type="month"]:focus,input[type="time"]:focus,input[type="week"]:focus,input[type="number"]:focus,input[type="email"]:focus,input[type="url"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="color"]:focus,.uneditable-input:focus{border-color:rgba(82,168,236,0.8);outline:0;outline:thin dotted \9; -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6)}input[type="radio"],input[type="checkbox"]{margin:3px 0;*margin-top:0; line-height:normal;cursor:pointer}input[type="submit"],input[type="reset"],input[type="button"],input[type="radio"],input[type="checkbox"]{width:auto}.uneditable-textarea{width:auto;height:auto}select,input[type="file"]{height:28px; *margin-top:4px; line-height:28px}select{width:220px;border:1px solid #bbb}select[multiple],select[size]{height:auto}select:focus,input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.radio,.checkbox{min-height:18px;padding-left:18px}.radio input[type="radio"],.checkbox input[type="checkbox"]{float:left;margin-left:-18px}.table{width:100%;margin-bottom:18px}.table th,.table td{padding:8px;line-height:18px;text-align:left;vertical-align:top;border-top:1px solid #ddd}.table th{font-weight:bold}.table thead th{vertical-align:bottom}.table caption + thead tr:first-child th,.table caption + thead tr:first-child td,.table colgroup + thead tr:first-child th,.table colgroup + thead tr:first-child td,.table thead:first-child tr:first-child th,.table thead:first-child tr:first-child td{border-top:0}.table tbody + tbody{border-top:2px solid #ddd}.table-condensed th,.table-condensed td{padding:4px 5px}.table-bordered{border:1px solid #ddd;border-collapse:separate;*border-collapse:collapsed;border-left:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.table-bordered th,.table-bordered td{border-left:1px solid #ddd}.table-bordered caption + thead tr:first-child th,.table-bordered caption + tbody tr:first-child th,.table-bordered caption + tbody tr:first-child td,.table-bordered colgroup + thead tr:first-child th,.table-bordered colgroup + tbody tr:first-child th,.table-bordered colgroup + tbody tr:first-child td,.table-bordered thead:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child td{border-top:0}.table-bordered thead:first-child tr:first-child th:first-child,.table-bordered tbody:first-child tr:first-child td:first-child{-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topleft:4px}.table-bordered thead:first-child tr:first-child th:last-child,.table-bordered tbody:first-child tr:first-child td:last-child{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-topright:4px}.table-bordered thead:last-child tr:last-child th:first-child,.table-bordered tbody:last-child tr:last-child td:first-child{-webkit-border-radius:0 0 0 4px;-moz-border-radius:0 0 0 4px;border-radius:0 0 0 4px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px}.table-bordered thead:last-child tr:last-child th:last-child,.table-bordered tbody:last-child tr:last-child td:last-child{-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px}.table-striped tbody tr:nth-child(odd) td,.table-striped tbody tr:nth-child(odd) th{background-color:#f9f9f9}.table tbody tr:hover td,.table tbody tr:hover th{background-color:#f5f5f5} .btn{display:inline-block;*display:inline;padding:4px 10px 4px;margin-bottom:0;*margin-left:.3em;font-size:13px;line-height:18px;*line-height:20px;color:#333;text-align:center;text-shadow:0 1px 1px rgba(255,255,255,0.75);vertical-align:middle;cursor:pointer;background-color:#f5f5f5;*background-color:#e6e6e6;background-image:-ms-linear-gradient(top,#ffffff,#e6e6e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ffffff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#ffffff,#e6e6e6);background-image:-o-linear-gradient(top,#ffffff,#e6e6e6);background-image:linear-gradient(top,#ffffff,#e6e6e6);background-image:-moz-linear-gradient(top,#ffffff,#e6e6e6);background-repeat:repeat-x;border:1px solid #ccc;*border:0;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-bottom-color:#b3b3b3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ffffff',endColorstr='#e6e6e6',GradientType=0);filter:progid:dximagetransform.microsoft.gradient(enabled=false);*zoom:1;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05)}.btn:hover,.btn:active,.btn.active,.btn.disabled,.btn[disabled]{background-color:#e6e6e6;*background-color:#d9d9d9}.btn:active,.btn.active{background-color:#ccc \9}.btn:first-child{*margin-left:0}.btn:hover{color:#333;text-decoration:none;background-color:#e6e6e6;*background-color:#d9d9d9; background-position:0 -15px;-webkit-transition:background-position 0.1s linear;-moz-transition:background-position 0.1s linear;-ms-transition:background-position 0.1s linear;-o-transition:background-position 0.1s linear;transition:background-position 0.1s linear}.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.active,.btn:active{background-color:#e6e6e6;background-color:#d9d9d9 \9;background-image:none;outline:0;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.btn.disabled,.btn[disabled]{cursor:default;background-color:#e6e6e6;background-image:none;opacity:0.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-large{padding:9px 14px;font-size:15px;line-height:normal;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.btn-large [class^="icon-"]{margin-top:1px}.btn-small{padding:5px 9px;font-size:11px;line-height:16px}.btn-small [class^="icon-"]{margin-top:-1px}.btn-mini{padding:2px 6px;font-size:11px;line-height:14px}.btn-primary,.btn-primary:hover,.btn-warning,.btn-warning:hover,.btn-danger,.btn-danger:hover,.btn-success,.btn-success:hover,.btn-info,.btn-info:hover,.btn-inverse,.btn-inverse:hover{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.btn-primary.active,.btn-warning.active,.btn-danger.active,.btn-success.active,.btn-info.active,.btn-inverse.active{color:rgba(255,255,255,0.75)}.btn{border-color:#ccc;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25)}.btn-primary{background-color:#0074cc;*background-color:#05c;background-image:-ms-linear-gradient(top,#0088cc,#0055cc);background-image:-webkit-gradient(linear,0 0,0 100%,from(#0088cc),to(#0055cc));background-image:-webkit-linear-gradient(top,#0088cc,#0055cc);background-image:-o-linear-gradient(top,#0088cc,#0055cc);background-image:-moz-linear-gradient(top,#0088cc,#0055cc);background-image:linear-gradient(top,#0088cc,#0055cc);background-repeat:repeat-x;border-color:#05c #0055cc #003580;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#0088cc',endColorstr='#0055cc',GradientType=0);filter:progid:dximagetransform.microsoft.gradient(enabled=false)}.btn-primary:hover,.btn-primary:active,.btn-primary.active,.btn-primary.disabled,.btn-primary[disabled]{background-color:#05c;*background-color:#004ab3}.btn-primary:active,.btn-primary.active{background-color:#004099 \9}.btn-warning{background-color:#faa732;*background-color:#f89406;background-image:-ms-linear-gradient(top,#fbb450,#f89406);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fbb450),to(#f89406));background-image:-webkit-linear-gradient(top,#fbb450,#f89406);background-image:-o-linear-gradient(top,#fbb450,#f89406);background-image:-moz-linear-gradient(top,#fbb450,#f89406);background-image:linear-gradient(top,#fbb450,#f89406);background-repeat:repeat-x;border-color:#f89406 #f89406 #ad6704;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#fbb450',endColorstr='#f89406',GradientType=0);filter:progid:dximagetransform.microsoft.gradient(enabled=false)}.btn-warning:hover,.btn-warning:active,.btn-warning.active,.btn-warning.disabled,.btn-warning[disabled]{background-color:#f89406;*background-color:#df8505}.btn-warning:active,.btn-warning.active{background-color:#c67605 \9}.btn-danger{background-color:#da4f49;*background-color:#bd362f;background-image:-ms-linear-gradient(top,#ee5f5b,#bd362f);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ee5f5b),to(#bd362f));background-image:-webkit-linear-gradient(top,#ee5f5b,#bd362f);background-image:-o-linear-gradient(top,#ee5f5b,#bd362f);background-image:-moz-linear-gradient(top,#ee5f5b,#bd362f);background-image:linear-gradient(top,#ee5f5b,#bd362f);background-repeat:repeat-x;border-color:#bd362f #bd362f #802420;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ee5f5b',endColorstr='#bd362f',GradientType=0);filter:progid:dximagetransform.microsoft.gradient(enabled=false)}.btn-danger:hover,.btn-danger:active,.btn-danger.active,.btn-danger.disabled,.btn-danger[disabled]{background-color:#bd362f;*background-color:#a9302a}.btn-danger:active,.btn-danger.active{background-color:#942a25 \9}.btn-success{background-color:#5bb75b;*background-color:#51a351;background-image:-ms-linear-gradient(top,#62c462,#51a351);background-image:-webkit-gradient(linear,0 0,0 100%,from(#62c462),to(#51a351));background-image:-webkit-linear-gradient(top,#62c462,#51a351);background-image:-o-linear-gradient(top,#62c462,#51a351);background-image:-moz-linear-gradient(top,#62c462,#51a351);background-image:linear-gradient(top,#62c462,#51a351);background-repeat:repeat-x;border-color:#51a351 #51a351 #387038;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#62c462',endColorstr='#51a351',GradientType=0);filter:progid:dximagetransform.microsoft.gradient(enabled=false)}.btn-success:hover,.btn-success:active,.btn-success.active,.btn-success.disabled,.btn-success[disabled]{background-color:#51a351;*background-color:#499249}.btn-success:active,.btn-success.active{background-color:#408140 \9}.btn-info{background-color:#49afcd;*background-color:#2f96b4;background-image:-ms-linear-gradient(top,#5bc0de,#2f96b4);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5bc0de),to(#2f96b4));background-image:-webkit-linear-gradient(top,#5bc0de,#2f96b4);background-image:-o-linear-gradient(top,#5bc0de,#2f96b4);background-image:-moz-linear-gradient(top,#5bc0de,#2f96b4);background-image:linear-gradient(top,#5bc0de,#2f96b4);background-repeat:repeat-x;border-color:#2f96b4 #2f96b4 #1f6377;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#5bc0de',endColorstr='#2f96b4',GradientType=0);filter:progid:dximagetransform.microsoft.gradient(enabled=false)}.btn-info:hover,.btn-info:active,.btn-info.active,.btn-info.disabled,.btn-info[disabled]{background-color:#2f96b4;*background-color:#2a85a0}.btn-info:active,.btn-info.active{background-color:#24748c \9}.btn-inverse{background-color:#414141;*background-color:#222;background-image:-ms-linear-gradient(top,#555555,#222222);background-image:-webkit-gradient(linear,0 0,0 100%,from(#555555),to(#222222));background-image:-webkit-linear-gradient(top,#555555,#222222);background-image:-o-linear-gradient(top,#555555,#222222);background-image:-moz-linear-gradient(top,#555555,#222222);background-image:linear-gradient(top,#555555,#222222);background-repeat:repeat-x;border-color:#222 #222222 #000;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#555555',endColorstr='#222222',GradientType=0);filter:progid:dximagetransform.microsoft.gradient(enabled=false)}.btn-inverse:hover,.btn-inverse:active,.btn-inverse.active,.btn-inverse.disabled,.btn-inverse[disabled]{background-color:#222;*background-color:#151515}.btn-inverse:active,.btn-inverse.active{background-color:#080808 \9}button.btn,input[type="submit"].btn{*padding-top:2px;*padding-bottom:2px}button.btn::-moz-focus-inner,input[type="submit"].btn::-moz-focus-inner{padding:0;border:0}button.btn.btn-large,input[type="submit"].btn.btn-large{*padding-top:7px;*padding-bottom:7px}button.btn.btn-small,input[type="submit"].btn.btn-small{*padding-top:3px;*padding-bottom:3px}button.btn.btn-mini,input[type="submit"].btn.btn-mini{*padding-top:1px;*padding-bottom:1px}.btn-group{position:relative;*margin-left:.3em;*zoom:1}.btn-group:before,.btn-group:after{display:table;content:""}.btn-group:after{clear:both}.btn-group:first-child{*margin-left:0}.btn-group + .btn-group{margin-left:5px}.btn-toolbar{margin-top:9px;margin-bottom:9px}.btn-toolbar .btn-group{display:inline-block;*display:inline; *zoom:1}.btn-group > .btn{position:relative;float:left;margin-left:-1px;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group > .btn:first-child{margin-left:0;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-bottomleft:4px;-moz-border-radius-topleft:4px}.btn-group > .btn:last-child,.btn-group > .dropdown-toggle{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-bottomright:4px}.btn-group > .btn.large:first-child{margin-left:0;-webkit-border-bottom-left-radius:6px;border-bottom-left-radius:6px;-webkit-border-top-left-radius:6px;border-top-left-radius:6px;-moz-border-radius-bottomleft:6px;-moz-border-radius-topleft:6px}.btn-group > .btn.large:last-child,.btn-group > .large.dropdown-toggle{-webkit-border-top-right-radius:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;border-bottom-right-radius:6px;-moz-border-radius-topright:6px;-moz-border-radius-bottomright:6px}.btn-group > .btn:hover,.btn-group > .btn:focus,.btn-group > .btn:active,.btn-group > .btn.active{z-index:2}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group > .dropdown-toggle{*padding-top:4px;padding-right:8px;*padding-bottom:4px;padding-left:8px;-webkit-box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05)}.btn-group > .btn-mini.dropdown-toggle{padding-right:5px;padding-left:5px}.btn-group > .btn-small.dropdown-toggle{*padding-top:4px;*padding-bottom:4px}.btn-group > .btn-large.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{background-image:none;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.btn-group.open .btn.dropdown-toggle{background-color:#e6e6e6}.btn-group.open .btn-primary.dropdown-toggle{background-color:#05c}.btn-group.open .btn-warning.dropdown-toggle{background-color:#f89406}.btn-group.open .btn-danger.dropdown-toggle{background-color:#bd362f}.btn-group.open .btn-success.dropdown-toggle{background-color:#51a351}.btn-group.open .btn-info.dropdown-toggle{background-color:#2f96b4}.btn-group.open .btn-inverse.dropdown-toggle{background-color:#222}.btn .caret{margin-top:7px;margin-left:0}.btn:hover .caret,.open.btn-group .caret{opacity:1;filter:alpha(opacity=100)}.btn-mini .caret{margin-top:5px}.btn-small .caret{margin-top:6px}.btn-large .caret{margin-top:6px;border-top-width:5px;border-right-width:5px;border-left-width:5px}.dropup .btn-large .caret{border-top:0;border-bottom:5px solid #000}.btn-primary .caret,.btn-warning .caret,.btn-danger .caret,.btn-info .caret,.btn-success .caret,.btn-inverse .caret{border-top-color:#fff;border-bottom-color:#fff;opacity:0.75;filter:alpha(opacity=75)}.pagination{height:36px;margin:18px 0}.pagination ul{display:inline-block;*display:inline;margin-bottom:0;margin-left:0;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;*zoom:1;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.pagination li{display:inline}.pagination a{float:left;padding:0 14px;line-height:34px;text-decoration:none;border:1px solid #ddd;border-left-width:0}.pagination a:hover,.pagination .active a{background-color:#f5f5f5}.pagination .active a{color:#999;cursor:default}.pagination .disabled span,.pagination .disabled a,.pagination .disabled a:hover{color:#999;cursor:default;background-color:transparent}.pagination li:first-child a{border-left-width:1px;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px}.pagination li:last-child a{-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0}.pagination-centered{text-align:center}.pagination-right{text-align:right} .popover{position:absolute;top:0;left:0;z-index:1010;display:none;padding:5px}.popover.top{margin-top:-5px}.popover.right{margin-left:5px}.popover.bottom{margin-top:5px}.popover.left{margin-left:-5px}.popover.top .arrow{bottom:0;left:50%;margin-left:-5px;border-top:5px solid #000;border-right:5px solid transparent;border-left:5px solid transparent}.popover.right .arrow{top:50%;left:0;margin-top:-5px;border-top:5px solid transparent;border-right:5px solid #000;border-bottom:5px solid transparent}.popover.bottom .arrow{top:0;left:50%;margin-left:-5px;border-right:5px solid transparent;border-bottom:5px solid #000;border-left:5px solid transparent}.popover.left .arrow{top:50%;right:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-left:5px solid #000}.popover .arrow{position:absolute;width:0;height:0}.popover-inner{width:280px;padding:3px;overflow:hidden;background:#000;background:rgba(0,0,0,0.8);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 3px 7px rgba(0,0,0,0.3);-moz-box-shadow:0 3px 7px rgba(0,0,0,0.3);box-shadow:0 3px 7px rgba(0,0,0,0.3)}.popover-title{padding:9px 15px;line-height:1;background-color:#f5f5f5;border-bottom:1px solid #eee;-webkit-border-radius:3px 3px 0 0;-moz-border-radius:3px 3px 0 0;border-radius:3px 3px 0 0}.popover-content{padding:14px;background-color:#fff;-webkit-border-radius:0 0 3px 3px;-moz-border-radius:0 0 3px 3px;border-radius:0 0 3px 3px;-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box}.popover-content p,.popover-content ul,.popover-content ol{margin-bottom:0}.label,.badge{font-size:10.998px;font-weight:bold;line-height:14px;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);white-space:nowrap;vertical-align:baseline;background-color:#999}.label{padding:1px 4px 2px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.badge{padding:1px 9px 2px;-webkit-border-radius:9px;-moz-border-radius:9px;border-radius:9px}a.label:hover,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.label-important,.badge-important{background-color:#b94a48}.label-important[href],.badge-important[href]{background-color:#953b39}.label-warning,.badge-warning{background-color:#f89406}.label-warning[href],.badge-warning[href]{background-color:#c67605}.label-success,.badge-success{background-color:#468847}.label-success[href],.badge-success[href]{background-color:#356635}.label-info,.badge-info{background-color:#3a87ad}.label-info[href],.badge-info[href]{background-color:#2d6987}.label-inverse,.badge-inverse{background-color:#333}.label-inverse[href],.badge-inverse[href]{background-color:#1a1a1a}.ui-widget{font-family:Trebuchet MS,Tahoma,Verdana,Arial,sans-serif;font-size:1.1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Trebuchet MS,Tahoma,Verdana,Arial,sans-serif;font-size:1em}.ui-widget-content{border:1px solid #ddd;background:#eee;color:#333}.ui-widget-content a{color:#333}.ui-widget-header{border:1px solid #e78f08;background:#f6a828;color:#fff;font-weight:bold}.ui-widget-header a{color:#fff}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default{border:1px solid #ccc;background:#f6f6f6;font-weight:bold;color:#1c94c4}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited{color:#1c94c4;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus{border:1px solid #fbcb09;background:#fdf5ce;font-weight:bold;color:#c77405}.ui-state-hover a,.ui-state-hover a:hover{color:#c77405;text-decoration:none}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active{border:1px solid #fbd850;background:#fff;font-weight:bold;color:#eb8f00}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#eb8f00;text-decoration:none}.ui-widget :active{outline:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #fed22f;background:#ffe45c;color:#363636}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#363636}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #cd0a0a;background:#b81900;color:#fff}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#fff}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#fff}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-widget-overlay{background:#666;opacity:.50;filter:Alpha(Opacity=50)}.ui-widget-shadow{margin:-5px 0 0 -5px;padding:5px;background:#000;opacity:.20;filter:Alpha(Opacity=20);-moz-border-radius:5px;-khtml-border-radius:5px;-webkit-border-radius:5px;border-radius:5px}.ui-autocomplete{background-color:rgba(255,255,255,0.85);border:none;border-top:4px solid #8E9FAF;box-shadow:0 3px 7px rgba(0,0,0,0.3);min-width:300px}.ui-autocomplete .arrow{border-right:5px solid transparent;border-bottom:5px solid #8F99A3;border-left:5px solid transparent;height:0;left:50%;margin-left:-5px;position:absolute;top:-9px;width:0}.ui-menu-item{font-size:13px}.ui-menu-item span{float:right;font-size:10px;line-height:2em;margin-left:10px}.ui-autocomplete-category{background-image:linear-gradient(#8F99A3,#697784);background-image:-webkit-linear-gradient(#8F99A3,#697784);border-bottom:4px solid #8E9FAF;clear:both;color:#FFF;font-size:14px;letter-spacing:-1px;padding:6px}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus{background-color:#697784;color:#fff}header .ui-autocomplete{left:-75px !important}body{background-color:#fff;color:#18354F;font-family:Calibri,"Gill Sans","Gill Sans MT","Myriad Pro",Myriad,"DejaVu Sans Condensed","Liberation Sans","Nimbus Sans L",Tahoma,Geneva,"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px}h1,h2,h3{font-weight:normal}article{position:relative}header{background-color:#E0E8EF;background-image:-moz-linear-gradient(#E0E8EF 70%,#D0D8DF);background-image:-o-linear-gradient(#E0E8EF 70%,#D0D8DF);background-image:-webkit-linear-gradient(#E0E8EF 70%,#D0D8DF);border-bottom:solid 1px #8E9FAF;box-shadow:0 0 5px -1px #697784;height:150px}header > div{background:transparent url(/r518/skins/elecena/img/header.jpg) no-repeat -3px 0;position:relative}header h2 a{background-image:url(/r518/skins/elecena/img/logo.png);display:block;height:111px;opacity:0.8;text-indent:-999px;width:121px;-moz-transition:opacity .5s;-o-transition:opacity .5s;-webkit-transition:opacity .5s;transition:opacity .5s}header h2 a:hover{opacity:1}header nav{background-color:#8F99A3;background-image:-moz-linear-gradient(#8F99A3,#697784);background-image:-o-linear-gradient(#8F99A3,#697784);background-image:-webkit-linear-gradient(#8F99A3,#697784);border-bottom:solid 4px #8E9FAF;box-shadow:0 0 20px -2px #697784;padding:4px;position:absolute;right:0;top:0;z-index:2}header nav > ul{float:left;list-style:none;margin:0}header nav > ul > li{border-left:solid 1px #8E9FAF;float:left;padding:0 5px}header nav > ul > li:first-child{border-left:none}header nav > ul > li > a{color:#fff;display:block;font-size:14px;letter-spacing:-1px;padding:5px}header nav > ul > li > a:hover{color:#fff}header nav .sub{display:none;position:absolute;top:35px}header nav .arrow{border-color:#fff}header .search{float:left;margin:0;margin-left:10px;width:200px}.search{background-color:#fff;box-shadow:inset 0 0 3px #697784;padding-right:30px;position:relative}.search input[type="text"]{background-color:transparent;border:none; color:#697784;font-size:14px;letter-spacing:-1px;margin:0;padding:5px;width:100%}.search input[type="submit"]{background:transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAMAAAAMCGV4AAAAB3RJTUUH2gweECoNB7tLoQAAAAlwSFlzAAALEwAACxMBAJqcGAAAAARnQU1BAACxjwv8YQUAAABgUExURf7+/vf5+pqkrmt6hqqzu5KdqGRwfnF+jICLlqawu/Lz9fT2+IORncLJz36Ll7nCyXeCjqKuuaKpsXmFksbM0naDko2Yo4WToXyLmu7w8qu3woycqpmmsM7T2M/T14qUnnx+iXoAAAB7SURBVHjaVY7ZDsMgDAQNhjocKQFCoOmR///LiuKq6b6sZrSSDQCvjehY4RtVajS1JMZniw5A7G0erO+ul3jUwW0fHZodrJj9bey9+PTG+zXLLkwJfCDSklKmrB2L2SNeg6RFWzauf+AJJwu/GPoXQhPiBGeBeIGzkKjez1EExgadrwQAAAAASUVORK5CYII=) no-repeat 50% 50%;border:none;cursor:pointer;height:28px;margin-top:-14px;padding:0;position:absolute;right:0;top:50%;width:28px}#main{background-color:#fff;border-top:solid 3px #697784;position:relative;top:-40px}article{padding:15px 0;margin-right:15px}article h1{font-size:30px;letter-spacing:-3px;margin:.2em 0 .5em}article h2{font-size:19px;letter-spacing:-1px;margin:.75em 0 .5em 0}article h3{font-size:15px;letter-spacing:-1px;margin:.75em 0 .5em 0}article a{color:#2A5D89;border-bottom:dotted 1px #697784}article a:hover{border-bottom-style:solid;text-decoration:none}article > ul,article .description ul{list-style:square;padding-left:1.5em}article > ul li,article .description ul li{line-height:1.75em}article p{line-height:1.75em;margin:.5em 0}article code{color:#18354F}article mark{background-color:#E0E8EF;border-bottom:solid 1px #8E9FAF;padding:2px}h3.bar{background-color:#E0E8EF;border-left:4px solid #8E9FAF;clear:both;font-size:13px;letter-spacing:-.75px;line-height:20px;margin:1em 0 .5em 0}h3.bar > span{background-color:#697784;color:#fff;display:inline-block;padding:4px 8px}h3.bar > span > a{border-bottom-color:#fff;color:#fff}.framed{border:solid 1px #8E9FAF;outline:3px solid #E0E8EF}#sidebar{font-size:12px;margin:0}#sidebar section{margin:15px 5px;position:relative}#sidebar h3{font-size:14px;letter-spacing:-1px;line-height:16px}#sidebar ul{list-style:none;margin:0;padding:5px 0 5px 10px}#sidebar li{line-height:1.5em}#sidebar a{color:#37556F}#sidebar .rss{position:absolute;right:0;top:0}#sidebar > .google-plus{margin:0}#sidebar .greeting{background-color:#E0E8EF;background-image:url(/r518/skins/elecena/img/product-bg.png);border:solid 1px #8E9FAF;box-shadow:0 0 20px -5px #697784 inset;margin:3px;margin-bottom:10px;outline:3px solid #E0E8EF;padding:8px}.greeting > p{margin:9px 0}.greeting > .links{text-align:right}.greeting > .links > a{font-weight:bold;margin:0 5px}footer{background-color:#697784;background-image:-moz-linear-gradient(#596774,#697784 7%);background-image:-o-linear-gradient(#596774,#697784 7%);background-image:-webkit-linear-gradient(#596774,#697784 7%);border-top:solid 4px #8E9FAF;color:#fff;font-size:11px;margin-top:40px;padding:40px 0}footer .row{padding:20px 0 40px 0}footer h3{font-size:14px;letter-spacing:-1px}footer ul{list-style:none;margin:0;padding:5px 0 5px 10px}footer li{line-height:1.5em}footer a,footer a:hover{color:#fff}footer li > a{display:inline-block;line-height:1.2em;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}footer p{color:#C3D2DF;line-height:1.75em;margin:5px 0}#version{float:right} .attr{background-repeat:no-repeat;background-position:left 50%;padding-left:20px}.attr.rohs{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAASCAIAAADdWck9AAAACXBIWXMAAAsTAAALEwEAmpwYAAABo0lEQVR4nJWRu04bQRSG57az3hsrLDvOUpgCAU3MGyA5yE9AQ0kTpUiTJsoLUOUF8hDpoyhCSgMvEIUCJTHYuSisZWTDzu6sZ2ZnaBLFwUMkvu7MOZ/+czTQGAPuA7rX9C3BXOWDaT4E4H+ZcH6lSot08pqX8VLYqYePMK4tCmS+KMVwzN6ffv8yq6RLtteSvU67R4lnT1AV/3rxcsKPs1xeFfx8PDsbeZsPu092XkVe3XJDKdJLdiwkiDxiDGxEjhCmP/pw+OmNmbvqr8DlgIuMlaVUEiF4XRiMFIL46PRQqZlF0JoZrVxKRFUhSDVQa4n5ealENZgWY4vgOq3IC/J8VggVuk7OHcZJ5PHrgk1YahE82qakGYeu1hAhyQr9Yyw3V3wAJMHEIlDSWA66FGsEoZBaVcSnOHAB0FHsNy0ChLgR7Qb0QeAiDAGBMXVAP4UbSXc5bFkEAEDkb602D2I/0UBttMhwpLJi9WnvOYLY8nF/MBk/+fzrBdbhu4/t/cfPVurr//ZtCDm9+PaWZf3F1mLCb4oyxUC7teTW+53CXdwAdfDre2QsAhAAAAAASUVORK5CYII=);color:#228B22}.attr.sample{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAN7SURBVHjaYvz//z8DNnBzq582w79/Lf///ef9/+9fqWbg9vPY1AEEECO6Abe2+4sBNeUBNWULKwcK/P3+huHdg71fgGIz///916cTsfcZsnqAAIIbcHtHACuQnQDUWMYr7aYirhnCwPTvL8OX52cYWNmZGV7dOcDw4cnZh0D5HiCeaxB/5DtIH0AAgQ24tSPAjeHf/xp2QQNbcY1gBg5BVQaG/78Y/ry/zXD/QCuDrJY1AxOXPMPPr68ZXt45y/D59fWT///+bTVKPbkZIIAYb273nw/0a7yISgQjh5Auw4/Pbxg+vr7O8PfXF4bfX58zsDP+ZJCQU2dg+PGegZlHjoGJg5fh0/NbYIO+vX+4HCCAWID+ilFzmcL4G6jxy/MDDP9+fWJg//aa4dun5wyfHj9lMIqYzMDKyc3A8PUBw/+vdxgYP91iEJbSY+AXYmA4t+1eKEAAsQD98/U/AyM/Gwc7A6+QHMP319cYuAQFGAQEORk4Gb4BbX7EwPCXDUg/ZWD4/pjh24uTDF8eHGPgUAhmAFr+FSCAQAYwMPwDhQYbAyu/KjDAeBl+vDrD8O/LKwZWZqDwt7sMDEz/GX692MPw+dMHhj+cAgwMbOwM/3//YQDpBQggkBcYIPHwFxhwPxkYWLgZOMQMGf6wcTJw/HjL8O/FboaPvz8z/OHgYfgLNPzft18MDH/+A70KNODvXwaAAGICEWDwH0j/+w3E38AGsfBIM/BKqDF8Y2Vg+MXOwfDn5x+Gvz9+Mfz99oPh97efQKW/QV5gAAggJqAzOJlYgaqYOYEafwANALriH5D+C4xmDimQZUDz/oJt/vPlJ8PvLz+AXtUG+QrkBQ6AAAK64N+he4cmMHx/DwwsdimgQVxAzUBn/ge6Bhg4/3//A9v8B2gAM5scA796KMO/v5wMd84cBxlwEiCAWP79/ev9+fGR+BsPDpUJKbuoyOh5MTDzAuP92x1gyH9l+PvzN8O/P9wMvAquDH++/2B4fPEMw+e3F4Ap8n8vMP3MBQggeFI+N9scmAf+5QNdlCWlHyAgZeDFwPD7BzAtAG0C+uP5tasMbx8dA+aJf7OAuM+29t5TkD6AAMLITKenGGoDDakCKopUtk9m/P7qAcOz6xeBtj9ZC7S11abmDkquBAggRlzZ+XiPthvQkAqgM9mAdKd11e3N2NQBBBgAH97FT2/aOUAAAAAASUVORK5CYII=);color:#C38423}.attr.promo{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAALxSURBVHjaYvz//z8DIdBzpP3rn79/JCvsaz+hywEEEBMhzd2HW78KcAlw/fv372P9rio+dHmAAMJrQOehlq8C3AJcMqKyDMK8wgwgQ0o3F6IYAhBAOA3oONj8VZBbkEtWVI7h868PDHJAQ8T4RRn+///3MWdNOtwQgADCakD7gaavgjyCXCBNn35+ZPjx5wfDwSuHGZQklRkkhSQZ/v3//zFpaRzYEIAAwjCgbX8DVLMcw+VHVxjuPLsL1PCPQUNZleEj0CVqMqoMEoISDL///H4BUg8QQIzIsdCyr/4b0K+cCmIKDOrCGgzSnHIM66+sZbj74RaDoqQiAy8rL8O5u+cZbj25/WdpwkpWkB6AAAK7YMqpftXmvXW/hHiEgJrlGa4AbQZpnn9sHoOFnCUDMxMLAw8rD8PZO+cZ7jy9C+Qzs0TMDWEB6QUIIKYpp/pq3396fwuomVVRXIHh069PDL///wLb7KHjwbDk1GIGLRlNhnO3zzPcfnqbIcc9h0FeXJ7h99/fDSADAAKIsWF39X9QFCmJKzJ8+v2J4cff7wwgbz199ZyBhZGFQROo+eydcwyvvr5iSLRMZPjz9y/DL2CgTt01jeHbj29CAAHEwsLEsvDj14/xz9+/YBAXFGM4e/Mcg6aSOoOChDzYz2dun2X4xfQLqDmB4e3ndww7723/+frZW/av37+V/Pn3RxEggBiBAScJTKbP/vz5wyAtIsUgJSzF8BEYdXxsfAxnbp1lePn1JYOvjh/D/Td3GfZfPrgLqPYGEE/cnrf7HsgLAAEEjoXq7WWz1cTVUy4/vcwgD4w+WWD8n7l1huEW0M9qMmoMNx7fvPfnz+9UoMbnW3J2XEeOdoAAAhtQurlAnZeT96qVsjXzxnMbGPi4+BiuP7r5n42FFWTjoTVpG9pwpViAAAIbkL8+mxWYMFYBbfHj4uBmevjy4R82VjZLoOZ3a9M23sOXXwACCJ6QUpcnGgINmQ2MnnpgItnKQCQACDAAnzJCxfbuVEgAAAAASUVORK5CYII=); color:#50960A}.attr.foreign{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAABPlBMVEX///9Lnkaay5KQyIxNokiSx4xHm0Ohz5tZqlNGm0JRpU1stmeMxYdosl2z16yRx4ml0J2z2K2w1qur1KWOxIWi0J2VyZCYyZCv1amWyY5rsl93um9qsl6ezpmczZZ1um5jr1mSx4qx16yZy5FYqlJUpk9Spk1jsFpPokpfrlaczJWv1qpaq1RUpk6u1qh3um6ezpqq06WdzpiVyo+Jw4Fvt2hvtGNVp1Bwt2eOxYix16qXy5Gfz5uazJaMxoiy1qxNoUhyuGuVypCWypFYq1VZqlSs1KeGwoGZzZZwuGxRo0x5u3OWy5BfsFml0p9GmkJhsFuMxYWOx4tPpUxksF2Qx4uazZaWy5OFw4KQyY5/wHs9lDpaq1WKxYabzZeZzJaEw4FYrFY9kjlWq1SRyI2RyY5ZrlY/ljxut2g/lj3Fi45aAAAAAXRSTlMAQObYZgAAAMBJREFUeF5Nj00LAVEUhu+5M8a5UySfZSyYMTIbrCyUsvExSmJt5+NnWfoDVmRjb2el+AXWpCjOuN146r7nPb2nty6T1DmPsH+SjWhBOu2r2Yd1ZdbtpfJcrBNPVFJpJM8Z8US00UHMMEk+bDtF5pYmZVqAngdwduFy/6YQSBWIk3d4qw5EPGINm0JdEK19CwB2cjHbOqno9nwaZPsUDTY+rIcgO7ZCjExDCKpSHUZoDAFLpmDT2XwhHZdD11eadB9gDxqpNHryDwAAAABJRU5ErkJggg==);color:#50960A}.attr.sale{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAKZSURBVDjLjY/Na1R3GIWf+zV3JolJ40iMptZMGrGtVLoQpK66qagLV0VwI3XThS4EQQQ31Y34F7gQFOqqdCMtdGGCRXAhQmskgVSNmYAOmkySmczHvTP33t/vfbsUkyl44OwOD+dxVJXNSWfHJ1X0oSqjKuKpkKjoPMK1/m8rv38wVtUt7cx81sre3VWN51Tbj1Ub05qt3NXmo92vNm99ekStDPifHILuWzB1yOq44TbEyOebtz0BYkGSCk62AdrFdpYgcbFmq+7/PFBs8x+ylftI0kbiBkHxMGLgIx8IbjhGMPglmiwjQR+igjWyFZDM7B1U0XkVGVXBU6uJow6m9S+mNod229i4RWHiYG8FsXLFH7k0Fuw8CdoFG4VZtEj84hqFHUfQ/DJeWAc12IxeAL3sjxwH0wTbBNvGL4yQRet47jzaaWGjFoEzgs16KFgDSISaNmiKJKuQdjBGyA1NovkqNqyxOrtB5S/D4u1ArKcV4ObRKXPDFyPYaAG78RRJV9DkDd7gBDZVktpzNI5Ye9Ygqo1x6MzPhKUDTmd2as/8o+nrT84WJlybKU5QxCuU8Pu/wB/4BtRiMiUc3kdu+y7e/F1l8rtT5Bcf4vxymr7yPcb3Fp24Zn70rREc1yWLF9DuOzRdIRw7gUnvkUVr2HoVUxfyoyU4cfG9+9VdSJvAtxm/ddZmTuW3fYUEw6DjxOtlvHA7tm83+Z0H8IZeEj/7k/4/zpF0lomBVtNDC07Hu/BD4VM3N3jMzQ/g+A5ZWqO1+pJWZeFB4/Xz+vqLpzt8vy+qvqqGbuCSeRGNdaW87OEPuVNO+ddiSQw/iZXvreVrMcyJ1Wmx3Dp4vr4EsHR7uFSby9/ZKK8dISKnBdKg6D0e2J87+x98zpgrhVsXPQAAAABJRU5ErkJggg==);color:#968016}.price-change.price-change-up{color:#C30}.price-change.price-change-down{color:#228B22}.rss{background:#fff url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAB3RJTUUH2wECDQ4x5MEZkAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAARnQU1BAACxjwv8YQUAAABgUExURXiGlHKAjXF+i/7+/mx4hWd0gGVyfpOcpOvt8Gh2g+bo6rO8w217iZykrdvf44WSn97k6cfM0qmxuO7x9MzS2LrDzIGNl9Ta36WttYOOmYiXprC3vqOzwo+gsJSltZ+or7L2E3UAAAChSURBVHjaPc5bEoMgDAXQhGAFjcUnrVR0/7vsxVrzkTBn4AZSsx7DJHcRio0ezQ0p55Qi6/iHnPd9z6D4u0Qibt4ggai5ANVvp7gCTN2AuSTIWsCoKoGGFKL5XGvVvkXasJK6E4hMkTqSGRE6LRHiZ3mVL55bpsCsLIJhZ7J1Lw4xfpAWMJJ/+EZaVd/JU9UGstWjFuetx+MKvQThaJiv/gUpagiFCxbpCgAAAABJRU5ErkJggg==) no-repeat 0 0;display:block;height:16px;text-indent:-9999px;width:16px}.ad{ clear:both}.loading{background-image:url(data:image/gif;base64,R0lGODlhEAAQAPYAAP///xg1T/r6+5yos5OgrO/x8s3T2Nvf46eyuxg1T5ilsMvR10VccWJ1h+3v8a64wZ6qtPP09Y2bqDFLYrnByay2v6OuufT29/j5+bC5wtLY3FVqfSI+V26AkeHk5+rs7sLJ0D5Wa0hfdFdsf9fc4cnQ1k5keF5yhFBmeVltgNTZ3s7U2ml8jbrDy+vu8H2NnLO9xb7GzcDIz21/j2t9jnCCkpGfq+Lm6YqYpfz8/Kq1vi1HXz9XbWV5ihs4UWB0htDW20phdWd6iy9JYPHy9Nne4qWwuuTn6ujr7VFne7G7xDhRZzROZbW+xn+PnVtvgUdecneImNbb38XM0zpTaYiXpFNpfGR3iCE8VR87VLfAyObp7Fxxg/b3+LzFzHSFlXaHlt/j5qizvShDWzZPZgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH+GkNyZWF0ZWQgd2l0aCBhamF4bG9hZC5pbmZvACH5BAAIAAAAIf8LTkVUU0NBUEUyLjADAQAAACwAAAAAEAAQAAAHaIAAgoMgIiYlg4kACxIaACEJCSiKggYMCRselwkpghGJBJEcFgsjJyoAGBmfggcNEx0flBiKDhQFlIoCCA+5lAORFb4AJIihCRbDxQAFChAXw9HSqb60iREZ1omqrIPdJCTe0SWI09GBACH5BAAIAAEALAAAAAAQABAAAAdrgACCgwc0NTeDiYozCQkvOTo9GTmDKy8aFy+NOBA7CTswgywJDTIuEjYFIY0JNYMtKTEFiRU8Pjwygy4ws4owPyCKwsMAJSTEgiQlgsbIAMrO0dKDGMTViREZ14kYGRGK38nHguHEJcvTyIEAIfkEAAgAAgAsAAAAABAAEAAAB2iAAIKDAggPg4iJAAMJCRUAJRIqiRGCBI0WQEEJJkWDERkYAAUKEBc4Po1GiKKJHkJDNEeKig4URLS0ICImJZAkuQAhjSi/wQyNKcGDCyMnk8u5rYrTgqDVghgZlYjcACTA1sslvtHRgQAh+QQACAADACwAAAAAEAAQAAAHZ4AAgoOEhYaCJSWHgxGDJCQARAtOUoQRGRiFD0kJUYWZhUhKT1OLhR8wBaaFBzQ1NwAlkIszCQkvsbOHL7Y4q4IuEjaqq0ZQD5+GEEsJTDCMmIUhtgk1lo6QFUwJVDKLiYJNUd6/hoEAIfkEAAgABAAsAAAAABAAEAAAB2iAAIKDhIWGgiUlh4MRgyQkjIURGRiGGBmNhJWHm4uen4ICCA+IkIsDCQkVACWmhwSpFqAABQoQF6ALTkWFnYMrVlhWvIKTlSAiJiVVPqlGhJkhqShHV1lCW4cMqSkAR1ofiwsjJyqGgQAh+QQACAAFACwAAAAAEAAQAAAHZ4AAgoOEhYaCJSWHgxGDJCSMhREZGIYYGY2ElYebi56fhyWQniSKAKKfpaCLFlAPhl0gXYNGEwkhGYREUywag1wJwSkHNDU3D0kJYIMZQwk8MjPBLx9eXwuETVEyAC/BOKsuEjYFhoEAIfkEAAgABgAsAAAAABAAEAAAB2eAAIKDhIWGgiUlh4MRgyQkjIURGRiGGBmNhJWHm4ueICImip6CIQkJKJ4kigynKaqKCyMnKqSEK05StgAGQRxPYZaENqccFgIID4KXmQBhXFkzDgOnFYLNgltaSAAEpxa7BQoQF4aBACH5BAAIAAcALAAAAAAQABAAAAdogACCg4SFggJiPUqCJSWGgkZjCUwZACQkgxGEXAmdT4UYGZqCGWQ+IjKGGIUwPzGPhAc0NTewhDOdL7Ykji+dOLuOLhI2BbaFETICx4MlQitdqoUsCQ2vhKGjglNfU0SWmILaj43M5oEAOwAAAAAAAAAAAA==);background-repeat:no-repeat}input.loading{background-position:95% 50%}.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{position:absolute !important;clip:rect(1px 1px 1px 1px);clip:rect(1px,1px,1px,1px)}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table}.ui-helper-clearfix:after{clear:both}.ui-helper-clearfix{zoom:1}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-state-disabled{cursor:default !important} .ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat} .ui-widget-overlay{position:absolute;top:0;left:0;width:100%;height:100%}.ui-autocomplete{position:absolute;cursor:default}* html .ui-autocomplete{width:1px} .ui-menu{list-style:none;padding:2px;margin:0;display:block}.ui-menu .ui-menu{margin-top:-3px}.ui-menu .ui-menu-item{margin:0;padding:0;zoom:1;float:left;clear:left;width:100%}.ui-menu .ui-menu-item a{border:none;text-decoration:none;display:block;padding:.2em .4em;line-height:1.5;zoom:1}.ui-menu .ui-menu-item a.ui-state-hover,.ui-menu .ui-menu-item a.ui-state-active{border:none;font-weight:normal}.search-wrapper{background-color:#697784;border-bottom:4px solid #8E9FAF;padding:4px}#search-nav{border-bottom:1px solid #E0E8EF;font-size:11px;margin:10px 0 0;padding:0}#search-nav ul{list-style:none;margin:0}#search-nav li{float:left}#search-nav li a{border:none;color:#697784;display:block;font-size:1.3em;letter-spacing:-1px;margin-right:5px;padding:5px 12px}#search-nav li a.selected{border-bottom:2px solid #8E9FAF;color:#18354F}#search-nav li a:hover{color:#18354F}#search-nav li a.selected,#search-nav li a:hover{text-decoration:none}#search-nav span{opacity:0.8}.search-results > nav{border-bottom:3px solid #E0E8EF;font-size:11px;line-height:24px;padding:8px 4px}.search-result{padding:10px 0}.search-result + .search-result{border-top:1px dotted #8E9FAF}.search-results .separator{background-color:#8E9FAF;color:#fff;letter-spacing:-1px;padding:5px;text-align:center} .search-result figure{background-color:#E0E8EF;background-image:url(/r518/skins/elecena/img/product-bg-small.png);border:1px solid #8E9FAF;box-shadow:0 0 20px -10px #697784 inset;float:left;line-height:100px;height:100px;margin:5px 15px 5px 0;outline:solid 3px #E0E8EF;overflow:hidden;position:relative;text-align:center;width:100px}.search-result figure img{background-color:#fff}.search-result figure > span{bottom:0;font-weight:normal;position:absolute;right:0}.search-result figure > .price{background:transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAAAvCAYAAAAVfW/7AAAAB3RJTUUH2wQPEgMlZkUAQAAAAAlwSFlzAABOIAAATiABFn2Z3gAAAARnQU1BAACxjwv8YQUAAAOxSURBVHja7Vu7bhNBFJ31Og+RyDF5kIQgCKIApEhQ0FEQoUg0FHQ0lHwCJQUFH0CJREOLaNOkgYaGBlHQgCIRUAIEk8SJHdv7muHe2WdMst4A3ll775HuzJhEq+Gce8/MeDYaI6SO5ZVXI9BNQsx7PcYMhqZ6cv0CIHmaheSeOWJ8GmIOQj/qOSRIDIDkc6wtgyOf5yHKEKeYS3QsbMdhjsNl4Nj/LMc2jDln05MnWVH1fzpNRKyiPYOR6LLX42ckudTpeZbtkcxdUl2SRUgy9kA0jpPAMKzeFwRInmBhBiOZZXbQn8vev89CDMU9SwjMZNvLXC4JtWw7yGwLfmbbnHHuZvb/hmFlVBDPKnxiD8vgaHbHQgAgQzUrYhk+yb51uNntEq0Shmmlt4Z4duET6pM80zaegpjo9CzOhYDs1cwge11i0ULc7AXSLVtaCfyuCm7/CgPF4r8J0rZ9a8/o6OKHRI/EPQuIFUCqFvovD0h2CeeBTwvROyQfF4kEAeKfQYf2FrWOWE8GysAqbAcI1R1JNBJqy91EmMGhhRBcdBTE8/M1HIMdcCBTAJlIsiQUMzjcYXCGNsLlzkOtH/cqkizqV7H5uV1lm792Cqon3O9IIsglbBoNQ/oQobtIXCEt0wA9SJFuI1GF4K7GMGnhTQNJBLncMkxGfpUOYgWBHdYF6IZQkD7e+mcKnSpkCRvTshgpkg6OFASq4y50T+HoIXZrDY3kSAeHHgxBjCvQvQUxBlfXNrT9Zkv1PHODPyrE+37qBcTQ5/UfrE5ipIrDLOsJxEU4lbPqXl31/HKHA1+FQHVch+4+7qo2Niuq55ZLtFfIY2y+ftvsqXuEfkJQIVAdN6BbRJvaqzdUzyu3iFbIQ2y+V7b6+gIo65CCeHceS7X9JsMgqINfIfewqWzv0IlcMXxB7qBNbVVr9BWiYhTBrsagv1ZvNBnvwrtGhOMBK2QRB7u1faqODAAFkTeCdVzMaf1QDhRkAQdNw6AKyQBQEPk6ZrNlqp4LgXkvvxmmSXaVEQQndZIjGyjC+UOXalCFZAJFTdMcqYfqmRAk0LLqgwMDVCEZAQqyXihoC7qud+WvggjHAwqyioPh4UFWo3sQ5cALqvc4KI2eUD0XAnMr5A0OSqMjbF3QPbpqFG7fuvkR+i/j5ZKAtUT1fHIP/2D4Ui8UHoyPlVhlu6p6TrmG/5LDc2zmZqaYfyahUBOBRy2vvHoN3eK7D5/Ybo1ekFOF6Ityj7A5f3ZWeZbkOX4DkHLBt/YiYBYAAAAASUVORK5CYII=) no-repeat 0 0;color:#fff;font-size:18px;letter-spacing:-1px;line-height:1.75em;opacity:0.9;padding:10px 5px 2px 15px; transition:opacity 0.75s,bottom 0.75s;-webkit-transition:opacity 0.75s,bottom 0.75s}.search-result figure .multiply{font-size:10px;padding-left:5px}.search-result figure.image:hover > .price{opacity:0.25;bottom:-40px}.search-result .btn{float:right;font-size:10px}.search-result h3{font-size:14px;letter-spacing:-1px;line-height:20px;margin:0}.search-result h3 > a{color:#18354F}.search-result h3 > a:hover{text-decoration:none}.search-result blockquote{font-size:12px;line-height:18px;margin:0;padding:4px 0}.search-result .details{color:#697784;float:left}.search-result .details ul,.search-result .details dl{margin:0;max-width:550px;padding:2px 0}.search-result .details li,.search-result .details dt,.search-result .details dd{float:left;font-size:10px;font-weight:normal;line-height:18px}.search-result .details dd{margin:0 10px 0 2px}.search-result .details dd:last-child{margin-right:0}.search-result .details li{font-size:11px;margin:0 10px 0 2px}.search-result .details a{color:#697784}.mainpage{margin:0;padding:0}#slideshow{border-bottom:2px solid #8E9FAF;height:400px;position:relative;width:940px}#slideshow > form{box-shadow:0 0 20px -2px #697784;left:50%;margin-left:-300px;margin-top:-50px;padding:0;position:absolute;top:50%;width:600px;z-index:1}#slideshow > form input[type="text"]{font-size:30px;height:40px;line-height:40px}#slideshow > .promo,#slideshow > .slide{opacity:0;transition:opacity 1s;-webkit-transition:opacity 1s}#slideshow > .active{opacity:1}#slideshow > .slide{left:0;position:absolute;top:0}#slideshow h1{background-color:rgba(105,119,132,0.75);border-bottom:2px solid #8E9FAF;color:#fff;font-size:1.5em;left:0;letter-spacing:-1px;margin:0;padding:10px 15px;position:absolute;top:0;z-index:1}#slideshow > .promo{background-color:rgba(105,119,132,0.75);bottom:0;color:#fff;font-size:1.2em;letter-spacing:-1px;padding:10px 15px;position:absolute;right:0;text-align:right}#slideshow > .promo big{display:block;font-size:2em;font-weight:bold;letter-spacing:-3px;line-height:1.2em}#features{margin:15px 0}#features > div{min-height:200px;margin-bottom:20px}#features > div:first-child{margin-left:0}#features .btn{letter-spacing:-1px} 2 | 3 | /* Cached as static-css-r518-9b0f5ab4632defb55d67a1d672aa31bd120f4414 */ -------------------------------------------------------------------------------- /examples/example.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const analyze = require('../'); 3 | 4 | (async() => { 5 | const results = await analyze('.foo {margin: 0 !important}'); 6 | 7 | console.log(results.generator); 8 | console.dir(results.metrics); 9 | console.dir(results.metrics.length); // please note that this metric is well typed 10 | 11 | console.dir(results.offenders); 12 | console.dir(results.offenders.importants); // and this one is too 13 | })(); 14 | -------------------------------------------------------------------------------- /examples/propertyResets.css: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://css-tricks.com/accidental-css-resets/ 3 | */ 4 | .flex > span { 5 | /* Will be reset to `auto` (or `main-size` if supported) */ 6 | flex-basis: 150px; 7 | 8 | /* Will be reset to `1` */ 9 | flex-grow: 0; 10 | 11 | /* Will be reset to `1` */ 12 | flex-shrink: 0; 13 | 14 | /* OVERRIDE */ 15 | flex: auto; 16 | } 17 | 18 | .module { 19 | margin-right: 20px; 20 | margin: 10px; 21 | /* margin-right will be 10px now */ 22 | 23 | padding-top: 30px; 24 | padding: 10px; 25 | /* padding-top will be 10px now */ 26 | 27 | border-left: 1px; 28 | border: 0; 29 | /* border-left will be removed */ 30 | 31 | border-right: 3px; 32 | } 33 | -------------------------------------------------------------------------------- /examples/sass.sass: -------------------------------------------------------------------------------- 1 | #main 2 | color: blue 3 | font-size: 0.3em 4 | 5 | a 6 | font: 7 | weight: bold 8 | family: serif 9 | &:hover 10 | background-color: #eee 11 | -------------------------------------------------------------------------------- /examples/ti.mobile.css: -------------------------------------------------------------------------------- 1 | body { font-size: 14px; } 2 | .ui-body-d .ui-link { color: #ea272a; font-weight: normal; } 3 | .ui-header { border: 0; height: 43px; background-image: -webkit-gradient(linear, left top, left bottom, from(#ff0000), to(#cd0000)); background-image: -webkit-linear-gradient(top, #ff0000, #cd0000); background-image: -moz-linear-gradient(top, #ff0000, #cd0000); background-image: -ms-linear-gradient(top, #ff0000, #cd0000); background-image: -o-linear-gradient(top, #ff0000, #cd0000); background-image: linear-gradient(top, #ff0000, #cd0000); -moz-box-shadow: 0 1px 2px rgba(0,0,0,1); -webkit-box-shadow: 0 1px 2px rgba(0,0,0,1); box-shadow: 0 1px 2px #000; } 4 | .ui-header .ui-btn-inner { border: 0; padding: 8px 10px; } 5 | .ui-header .ui-btn-left { top: 0; left: 0; } 6 | .ui-header .ui-btn-right { top: 2px; right: 0; } 7 | .ui-header .ui-btn-up-a, .ui-header .ui-btn-hover-a { border: 0; background: none; background-image: none; box-shadow: none; } 8 | .ui-footer { background: #000; border: 0; padding: 0 0 10px 0; } 9 | .ui-footer .ui-listview { margin: 0 0 20px 0; } 10 | .ui-footer li.ui-btn { display: block; } 11 | .ui-footer a.ui-btn-up-a, 12 | .ui-footer a.ui-btn-hover-a, 13 | .ui-footer span.ui-btn-inner { border: 0; font-weight: normal; background-color: #000; box-shadow: none; border-radius: 0; } 14 | div.search { margin: 10px 5px; } 15 | .ui-input-search { background-color: #fff; border: 2px solid #ee0000; border-radius: 0; } 16 | .ui-input-search .ui-input-text { color: #333; text-shadow: none; font-size: 14px; } 17 | .ui-input-search .ui-input-clear { border: 0; background: none; box-shadow: none; -webkit-box-shadow: none; } 18 | .ui-radio .ui-btn-inner { padding: 0.6em 24px; } 19 | .ui-icon { background-color: transparent; } 20 | .ui-icon-searchfield:after { background-color: transparent; border-radius: 0; opacity: 1; } 21 | .ui-icon-shadow { box-shadow: none; } 22 | .ui-input-clear .ui-btn-inner {} 23 | .ui-input-clear .ui-icon { background-color: #999; } 24 | .ui-content { padding: 0 10px 30px 10px; } 25 | .ui-content .ui-listview { margin: -1px -10px 0 -10px; } 26 | .ui-li .ui-btn-inner a.ui-link-inherit { padding: .7em 20px .7em 10px; } 27 | .ui-li-divider, .ui-li-static { padding: .5em 10px; } 28 | .ui-header .ui-btn-icon-right .ui-icon, .ui-footer .ui-btn-icon-right .ui-icon, .ui-bar .ui-btn-icon-right .ui-icon { right: 10px; } 29 | .ui-li-desc { margin: 0; } 30 | .ui-content h1 { font-size: 24px; } 31 | .ui-content h1 p { margin-top: 0; } 32 | .crossRef, .ui-content h1 span.crossRef { color: #990099; } 33 | .ui-content h2 { font-size: 14px; color: #000; margin-bottom: 5px; } 34 | .ui-content p, ul.cnt, .ui-content table { font-size: 13px; line-height: 14px; font-weight: normal; } 35 | .ui-content p.date { position: absolute; top: .7em; right: 30px; color: #ee0000; } 36 | ul.cnt, ul.cnt ul { list-style: none; padding: 0; } 37 | ul.cnt li li { background: url(../images/ul-dash.gif) no-repeat 2px 7px; padding-left: 10px; } 38 | .ui-listview a.ui-link-inherit.pdf { background: url(../images/pdf.png) no-repeat 10px center; padding-left: 27px; } 39 | .ui-content table { border-collapse: collapse; margin: 10px -10px; width: 100%; } 40 | .ui-content .ui-listview table { margin-bottom: 0; } 41 | .ui-content table td { padding: 1px 1px 1px 10px; width: 50%; } 42 | .ui-content table .alt { background-color: #f0f0f0; } 43 | .diagram .ui-li-thumb { position: relative; left: 0; max-height: 200px; max-width: 200px; } 44 | .ui-li-has-thumb.diagram .ui-btn-inner a.ui-link-inherit { min-height: 150px; } 45 | .ui-content p.diagram img { max-width: 300px; } 46 | .showMore .ui-li { text-align: center; } 47 | div.option { margin-top: 10px; height: 30px; } 48 | div.option button { float: right; } 49 | div.option p { padding-top: 10px; } 50 | div.email { background-color: #f0f0f0; margin: 10px -10px 0 -10px; padding: 1px 10px; } 51 | div.email input { height: 20px; width: 200px; } 52 | button { background: #ff0000; background-image: -webkit-linear-gradient(top, #ff0000, #cd0000); background-image: -moz-linear-gradient(top, #ff0000, #cd0000); background-image: -ms-linear-gradient(top, #ff0000, #cd0000); background-image: -o-linear-gradient(top, #ff0000, #cd0000); border: 1px solid #730000; -moz-border-radius: 5px; -webkit-border-radius: 5px; border-radius: 5px; padding: 5px 20px; color: #fff; font-weight: bold; font-size: 14px; } 53 | .ui-collapsible-set { margin: 0; } 54 | .ui-collapsible { margin: -1px -10px 0 -10px; } 55 | .ui-collapsible-set .ui-collapsible { margin: -1px 0; } 56 | .ui-corner-top, .ui-corner-bottom { border-radius: 0; } 57 | .ui-collapsible-content { margin: 0 10px; padding: 0; } 58 | .ui-li-static.ui-li { padding: 0; } 59 | .ui-li-static .ui-li-count { right: 48px; } 60 | .ui-collapsible-heading { font-size: 14px; margin: 0; } 61 | .ui-collapsible-heading a { border-width: 1px 0; } 62 | .ui-collapsible-heading .ui-btn-icon-left .ui-btn-inner { padding-left: .7em; } 63 | .ui-btn-icon-left .ui-icon-plus, 64 | .ui-btn-icon-left .ui-icon-minus { left: auto; right: 10px; } 65 | .ui-collapsible-content .ui-btn-up-d, 66 | .ui-collapsible-content .ui-btn-hover-d { font-weight: normal; } 67 | .ui-collapsible-content .ui-li .ui-btn-inner a.ui-link-inherit, 68 | .ui-collapsible-content .ui-collapsible-heading a .ui-btn-inner { padding-left: 25px } 69 | .ui-collapsible-content .ui-collapsible-content .ui-li .ui-btn-inner a.ui-link-inherit { padding-left: 40px } 70 | .ui-content p img, .ui-collapsible-content p img { max-width: 100%; } 71 | .ui-controlgroup .ui-radio label { font-size: 14px; } 72 | .promo { margin: -10px 0 20px 0; height: 50px; } 73 | -------------------------------------------------------------------------------- /lib/collection.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Push items and count them 3 | */ 4 | "use strict"; 5 | 6 | /** 7 | * @class 8 | */ 9 | function Collection() { 10 | this.items = {}; 11 | } 12 | 13 | Collection.prototype = { 14 | /** 15 | * Pushes a given item to the collection and counts each occurrence 16 | * 17 | * @param {string} item 18 | * @return {void} 19 | */ 20 | push: function (item) { 21 | if (typeof this.items[item] === "undefined") { 22 | this.items[item] = { 23 | cnt: 1, 24 | }; 25 | } else { 26 | this.items[item].cnt++; 27 | } 28 | }, 29 | 30 | /** 31 | * Sorts collected items in desending order by their occurrences 32 | * 33 | * @return {Collection} 34 | */ 35 | sort: function () { 36 | var newItems = {}, 37 | sortedKeys; 38 | 39 | // sort in descending order (by cnt) 40 | sortedKeys = Object.keys(this.items).sort( 41 | function (a, b) { 42 | return this.items[b].cnt - this.items[a].cnt; 43 | }.bind(this), 44 | ); 45 | 46 | // build new items dictionary 47 | sortedKeys.forEach(function (key) { 48 | newItems[key] = this.items[key]; 49 | }, this); 50 | 51 | this.items = newItems; 52 | return this; 53 | }, 54 | 55 | /** 56 | * Runs provided callback for each item in the collection. 57 | * 58 | * Item and the count is provided to the callback. 59 | * 60 | * @param {forEachCallback} callback 61 | * 62 | */ 63 | forEach: function (callback) { 64 | Object.keys(this.items).forEach(function (key) { 65 | callback(key, this.items[key].cnt); 66 | }, this); 67 | }, 68 | }; 69 | 70 | /** 71 | * @callback forEachCallback 72 | * @param {string} item 73 | * @param {number} count 74 | */ 75 | 76 | module.exports = Collection; 77 | -------------------------------------------------------------------------------- /lib/css-analyzer.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * analyzer instance passed to the rules code 3 | * 4 | * See https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html 5 | */ 6 | 7 | import { StyleRules, Stylesheet, Position } from "css"; 8 | import { Selector } from "css-what"; 9 | import { 10 | EventsNames, 11 | MetricsNames, 12 | Metrics, 13 | Offenders, 14 | CSSRule, 15 | } from "./types"; 16 | 17 | declare class CSSAnalyzer { 18 | public setMetric( 19 | name: MetricsNames, 20 | value: number | undefined /** = 0 */, 21 | ): void; 22 | public incrMetric( 23 | name: MetricsNames, 24 | incr: number | undefined /** = 1 */, 25 | ): void; 26 | public addOffender( 27 | metricName: MetricsNames, 28 | msg: string, 29 | position: Position | undefined, 30 | ): void; 31 | public setCurrentPosition(position: Position): void; 32 | 33 | // types based on the event 34 | public on(ev: EventsNames, fn: any): void; 35 | 36 | public on(ev: "comment", fn: (comment: string) => void): void; 37 | public on(ev: "css", fn: (css: string) => void): void; 38 | public on( 39 | ev: "declaration", 40 | fn: (rule: CSSRule, property: string, value: string) => void, 41 | ): void; 42 | public on(ev: "error", fn: (err: Error) => void): void; 43 | public on( 44 | ev: "expression", 45 | fn: (selector: string, expression: Selector) => void, 46 | ): void; 47 | public on(ev: "font-face", fn: (rule: CSSRule) => void): void; 48 | public on(ev: "import", fn: (url: string) => void): void; 49 | public on( 50 | ev: "media", 51 | fn: (query: string, rules: Array) => void, 52 | ): void; 53 | public on( 54 | ev: "mediaEnd", 55 | fn: (query: string, rules: Array) => void, 56 | ): void; 57 | public on(ev: "report", fn: () => void): void; 58 | public on(ev: "rule", fn: (rule: CSSRule) => void): void; 59 | public on( 60 | ev: "selector", 61 | fn: (rule: CSSRule, selector: string, expressions: Selector[]) => void, 62 | ): void; 63 | public on(ev: "stylesheet", fn: (stylesheet: StyleRules) => void): void; 64 | 65 | public analyze(css: string): boolean | Error; 66 | public parseRules(rules: Array): void; 67 | 68 | public metrics: Metrics; 69 | public offenders: Offenders; 70 | 71 | public tree: Stylesheet; 72 | } 73 | 74 | export = CSSAnalyzer; 75 | -------------------------------------------------------------------------------- /lib/css-analyzer.js: -------------------------------------------------------------------------------- 1 | const analyze = require("."), 2 | cssParser = require("@adobe/css-tools").parse, 3 | debug = require("debug")("analyze-css:analyzer"), 4 | fs = require("fs"), 5 | CSSwhat = require("css-what"); 6 | 7 | function error(msg, code) { 8 | var err = new Error(msg); 9 | err.code = code; 10 | 11 | return err; 12 | } 13 | 14 | class CSSAnalyzer { 15 | constructor(options) { 16 | this.options = options; 17 | 18 | this.metrics = {}; 19 | this.offenders = {}; 20 | 21 | // init events emitter 22 | this.emitter = new (require("events").EventEmitter)(); 23 | this.emitter.setMaxListeners(200); 24 | } 25 | 26 | // emit given event 27 | emit(/* eventName, arg1, arg2, ... */) { 28 | //debug('Event %s emitted', arguments[0]); 29 | this.emitter.emit.apply(this.emitter, arguments); 30 | } 31 | 32 | // bind to a given event 33 | on(ev, fn) { 34 | this.emitter.on(ev, fn); 35 | } 36 | 37 | setMetric(name, value) { 38 | value = value || 0; 39 | 40 | //debug('setMetric(%s) = %d', name, value); 41 | this.metrics[name] = value; 42 | } 43 | 44 | // increements given metric by given number (default is one) 45 | incrMetric(name, incr /* =1 */) { 46 | var currVal = this.metrics[name] || 0; 47 | incr = incr || 1; 48 | 49 | //debug('incrMetric(%s) += %d', name, incr); 50 | this.setMetric(name, currVal + incr); 51 | } 52 | 53 | addOffender(metricName, msg, position /* = undefined */) { 54 | if (typeof this.offenders[metricName] === "undefined") { 55 | this.offenders[metricName] = []; 56 | } 57 | 58 | this.offenders[metricName].push({ 59 | message: msg, 60 | position: position || this.currentPosition, 61 | }); 62 | } 63 | 64 | setCurrentPosition(position) { 65 | this.currentPosition = position; 66 | } 67 | 68 | initRules() { 69 | var debug = require("debug")("analyze-css:rules"), 70 | re = /\.js$/, 71 | rules = []; 72 | 73 | // load all rules 74 | rules = fs 75 | .readdirSync(fs.realpathSync(__dirname + "/../rules/")) 76 | // filter out all non *.js files 77 | .filter(function (file) { 78 | return re.test(file); 79 | }) 80 | // remove file extensions to get just names 81 | .map(function (file) { 82 | return file.replace(re, ""); 83 | }); 84 | 85 | debug("Rules to be loaded: %s", rules.join(", ")); 86 | 87 | rules.forEach(function (name) { 88 | var rule = require("./../rules/" + name); 89 | rule(this); 90 | 91 | debug('"%s" loaded: %s', name, rule.description); 92 | }, this); 93 | } 94 | 95 | fixCss(css) { 96 | // properly handle ; in @import URLs 97 | // see https://github.com/macbre/analyze-css/pull/322 98 | // see https://github.com/reworkcss/css/issues/137 99 | return css.replace(/@import url([^)]+["'])/, (match) => { 100 | return match.replace(/;/g, "%3B"); 101 | }); 102 | } 103 | 104 | parseCss(css) { 105 | var debug = require("debug")("analyze-css:parser"); 106 | debug("Going to parse %s kB of CSS", (css.length / 1024).toFixed(2)); 107 | 108 | if (css.trim() === "") { 109 | return error("Empty CSS was provided", analyze.EXIT_EMPTY_CSS); 110 | } 111 | 112 | css = this.fixCss(css); 113 | 114 | this.tree = cssParser(css, { 115 | // errors are listed in the parsingErrors property instead of being thrown (#84) 116 | silent: true, 117 | }); 118 | 119 | debug("CSS parsed"); 120 | return true; 121 | } 122 | 123 | /** 124 | * 125 | * @param { import("./types").CSSRule[] } rules 126 | */ 127 | parseRules(rules) { 128 | const debug = require("debug")("analyze-css:parseRules"); 129 | 130 | rules.forEach(function (rule) { 131 | debug("rule: %j", rule); 132 | 133 | // store the default current position 134 | // 135 | // it will be used when this.addOffender is called from within the rule 136 | // it can be overridden by providing a "custom" position via a call to this.setCurrentPosition 137 | this.setCurrentPosition(rule.position); 138 | 139 | switch (rule.type) { 140 | // { 141 | // "type":"media" 142 | // "media":"screen and (min-width: 1370px)", 143 | // "rules":[{"type":"rule","selectors":["#foo"],"declarations":[]}] 144 | // } 145 | case "media": 146 | this.emit("media", rule.media, rule.rules); 147 | 148 | // now run recursively to parse rules within the media query 149 | /* istanbul ignore else */ 150 | if (rule.rules) { 151 | this.parseRules(rule.rules); 152 | } 153 | 154 | this.emit("mediaEnd", rule.media, rule.rules); 155 | break; 156 | 157 | // { 158 | // "type":"rule", 159 | // "selectors":[".ui-header .ui-btn-up-a",".ui-header .ui-btn-hover-a"], 160 | // "declarations":[{"type":"declaration","property":"border","value":"0"},{"type":"declaration","property":"background","value":"none"}] 161 | // } 162 | case "rule": 163 | this.emit("rule", rule); 164 | 165 | // analyze each selector and declaration 166 | rule.selectors.forEach(function (selector) { 167 | // https://github.com/fb55/css-what#example 168 | // "#features > div:first-child" will become: 169 | // {"type":"attribute","name":"id","action":"equals","value":"features","namespace":null,"ignoreCase":false} 170 | // {"type":"child"} 171 | // {"type":"tag","name":"div","namespace":null} 172 | // {"type":"pseudo","name":"first-child","data":null} 173 | debug("selector: %s", selector); 174 | 175 | let parsedSelector; 176 | 177 | try { 178 | parsedSelector = CSSwhat.parse(selector); 179 | } catch (err) { 180 | /** 181 | * > require("css-what").parse('foo { color= red; }'); 182 | Uncaught Error: Unmatched selector: { color= red; } 183 | at Object.parse (node_modules/css-what/lib/parse.js:139:15) 184 | */ 185 | 186 | debug("selector parsing failed: %s", err.message); 187 | this.emit("error", err); 188 | return; 189 | } 190 | 191 | // convert object with keys to array with numeric index 192 | const expressions = []; 193 | 194 | for (let i = 0, len = parsedSelector[0].length; i < len; i++) { 195 | expressions.push(parsedSelector[0][i]); 196 | } 197 | 198 | debug("selector expressions: %j", expressions); 199 | this.emit("selector", rule, selector, expressions); 200 | 201 | expressions.forEach(function (expression) { 202 | this.emit("expression", selector, expression); 203 | }, this); 204 | }, this); 205 | 206 | rule.declarations.forEach(function (declaration) { 207 | this.setCurrentPosition(declaration.position); 208 | 209 | switch (declaration.type) { 210 | case "declaration": 211 | this.emit( 212 | "declaration", 213 | rule, 214 | declaration.property, 215 | declaration.value, 216 | ); 217 | break; 218 | 219 | case "comment": 220 | this.emit("comment", declaration.comment); 221 | break; 222 | } 223 | }, this); 224 | break; 225 | 226 | // {"type":"comment","comment":" Cached as static-css-r518-9b0f5ab4632defb55d67a1d672aa31bd120f4414 "} 227 | case "comment": 228 | this.emit("comment", rule.comment); 229 | break; 230 | 231 | // {"type":"font-face","declarations":[{"type":"declaration","property":"font-family","value":"myFont"... 232 | case "font-face": 233 | this.emit("font-face", rule); 234 | break; 235 | 236 | // {"type":"import","import":"url('/css/styles.css')"} 237 | case "import": 238 | // replace encoded semicolon back into ; 239 | // https://github.com/macbre/analyze-css/pull/322 240 | this.emit("import", rule.import.replace(/%3B/g, ";")); 241 | break; 242 | } 243 | }, this); 244 | } 245 | 246 | run() { 247 | const stylesheet = this.tree && this.tree.stylesheet, 248 | rules = stylesheet && stylesheet.rules; 249 | 250 | this.emit("stylesheet", stylesheet); 251 | 252 | this.parseRules(rules); 253 | } 254 | 255 | analyze(css) { 256 | var res, 257 | then = Date.now(); 258 | 259 | this.metrics = {}; 260 | this.offenders = {}; 261 | 262 | // load and init all rules 263 | this.initRules(); 264 | 265 | // parse CSS 266 | res = this.parseCss(css); 267 | 268 | if (res !== true) { 269 | debug("parseCss() returned an error: " + res); 270 | return res; 271 | } 272 | 273 | this.emit("css", css); 274 | 275 | // now go through parsed CSS tree and emit events for rules 276 | this.run(); 277 | 278 | this.emit("report"); 279 | 280 | debug("Completed in %d ms", Date.now() - then); 281 | return true; 282 | } 283 | } 284 | 285 | module.exports = CSSAnalyzer; 286 | -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file brings type hinting for index.js file. 3 | * 4 | * While this package is not written in TypeScript, this file will allow your IDE to provide you with auto-completion. 5 | */ 6 | 7 | import { Metrics, Offenders } from "./types"; 8 | 9 | /** 10 | * Encapsulates results from analyze() 11 | */ 12 | declare interface Results { 13 | generator: string; 14 | metrics: Metrics; 15 | offenders: Offenders; 16 | } 17 | 18 | /** 19 | * The main entry-point to analyze a given css 20 | */ 21 | declare function analyze(css: string, options?: object): Promise; 22 | 23 | export = analyze; 24 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * analyze-css CommonJS module 3 | */ 4 | "use strict"; 5 | 6 | const debug = require("debug")("analyze-css"), 7 | path = require("path"), 8 | preprocessors = new (require("./preprocessors"))(), 9 | VERSION = require("./../package").version; 10 | 11 | function error(msg, code) { 12 | var err = new Error(msg); 13 | err.code = code; 14 | 15 | return err; 16 | } 17 | 18 | // Promise-based public endpoint 19 | function analyze(css, options) { 20 | // options can be omitted 21 | options = options || {}; 22 | 23 | debug("opts: %j", options); 24 | 25 | return new Promise((resolve, reject) => { 26 | if (typeof css !== "string") { 27 | reject( 28 | error( 29 | "css parameter passed is not a string!", 30 | analyze.EXIT_CSS_PASSED_IS_NOT_STRING, 31 | ), 32 | ); 33 | return; 34 | } 35 | 36 | // preprocess the CSS (issue #3) 37 | if (typeof options.preprocessor === "string") { 38 | debug('Using "%s" preprocessor', options.preprocessor); 39 | 40 | var preprocessor = preprocessors.get(options.preprocessor); 41 | 42 | try { 43 | css = preprocessor.process(css, options); 44 | } catch (ex) { 45 | throw new Error("Preprocessing failed: " + ex); 46 | } 47 | 48 | debug("Preprocessing completed"); 49 | } 50 | 51 | const CSSAnalyzer = require("./css-analyzer"); 52 | const instance = new CSSAnalyzer(options); 53 | const res = instance.analyze(css); 54 | 55 | // error handling 56 | if (res instanceof Error) { 57 | debug("Rejecting a promise with an error: " + res); 58 | reject(res); 59 | return; 60 | } 61 | 62 | // return the results 63 | const result = { 64 | generator: "analyze-css v" + VERSION, 65 | metrics: instance.metrics, 66 | }; 67 | 68 | // disable offenders output if requested (issue #64) 69 | if (options.noOffenders !== true) { 70 | result.offenders = instance.offenders; 71 | } 72 | 73 | debug("Promise resolved"); 74 | resolve(result); 75 | }); 76 | } 77 | 78 | analyze.version = VERSION; 79 | 80 | // @see https://github.com/macbre/phantomas/issues/664 81 | analyze.path = path.normalize(__dirname + "/.."); 82 | analyze.pathBin = analyze.path + "/bin/analyze-css.js"; 83 | 84 | // exit codes 85 | analyze.EXIT_NEED_OPTIONS = 2; 86 | analyze.EXIT_PARSING_FAILED = 251; 87 | analyze.EXIT_EMPTY_CSS = 252; 88 | analyze.EXIT_CSS_PASSED_IS_NOT_STRING = 253; 89 | analyze.EXIT_URL_LOADING_FAILED = 254; 90 | analyze.EXIT_FILE_LOADING_FAILED = 255; 91 | 92 | module.exports = analyze; 93 | -------------------------------------------------------------------------------- /lib/preprocessors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A wrapper for preprocessors 3 | */ 4 | "use strict"; 5 | 6 | var debug = require("debug")("analyze-css:preprocessors"), 7 | glob = require("glob"); 8 | 9 | var preprocessors = function () {}; 10 | 11 | preprocessors.prototype = { 12 | get: function (name) { 13 | return require(__dirname + "/preprocessors/" + name + ".js"); 14 | }, 15 | 16 | // return instances of all available preprocessors 17 | getAll: function () { 18 | var files, 19 | res = []; 20 | 21 | files = glob.sync(__dirname + "/preprocessors/*.js"); 22 | debug("Initializing..."); 23 | 24 | /* istanbul ignore next */ 25 | if (Array.isArray(files)) { 26 | files.forEach(function (file) { 27 | res.push(require(file)); 28 | }); 29 | } 30 | 31 | return res; 32 | }, 33 | 34 | // get name of matching preprocessor 35 | findMatchingByFileName: function (fileName) { 36 | var matching = false; 37 | 38 | this.getAll().forEach(function (preprocessor) { 39 | if (preprocessor.matchesFileName(fileName)) { 40 | matching = preprocessor.name; 41 | 42 | debug('%s matches "%s" preprocessor', fileName, matching); 43 | return false; 44 | } 45 | }); 46 | 47 | return matching; 48 | }, 49 | }; 50 | 51 | module.exports = preprocessors; 52 | -------------------------------------------------------------------------------- /lib/preprocessors/sass.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SASS preprocessor 3 | * 4 | * @see https://github.com/sass/dart-sass#javascript-api 5 | */ 6 | "use strict"; 7 | 8 | var debug = require("debug")("analyze-css:preprocessors:sass"); 9 | 10 | module.exports = { 11 | name: "sass", 12 | matchesFileName: function (fileName) { 13 | return /\.(scss|sass)$/.test(fileName); 14 | }, 15 | process: function (css, options) { 16 | var path = require("path"), 17 | sass, 18 | out; 19 | 20 | // check the presense of the optional "sass" module (#318) 21 | try { 22 | sass = require("sass"); 23 | debug("Using: %s", sass.info.replace(/[\n\t]/g, " ")); 24 | } catch (e) /* istanbul ignore next */ { 25 | throw new Error("Can't process SASS/SCSS, please run 'npm install sass'"); 26 | } 27 | 28 | var includeDir = options.file ? path.dirname(options.file) : undefined; 29 | debug('Using "%s" include path', includeDir); 30 | 31 | try { 32 | // 1: try to parse using SCSS syntax (i.e. with brackets) 33 | debug("Parsing using SCSS syntax"); 34 | 35 | out = sass.renderSync({ 36 | data: css, 37 | indentedSyntax: false, 38 | includePaths: [includeDir], 39 | }); 40 | } catch (e) { 41 | // 2: try to parse using SASS syntax (i.e. with indends) - issue #79 42 | debug("Exception: %s", e.toString().trim()); 43 | debug("Parsing using SASS syntax as a fallback"); 44 | 45 | out = sass.renderSync({ 46 | data: css, 47 | indentedSyntax: true, 48 | includePaths: [includeDir], 49 | }); 50 | } 51 | 52 | return out.css.toString(); 53 | }, 54 | }; 55 | -------------------------------------------------------------------------------- /lib/runner.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Fetches remote asset / local CSS file and returns analyzer results 3 | * 4 | * Used internally by analyze-css "binary" to communicate with CommonJS module 5 | */ 6 | "use strict"; 7 | 8 | var cli = require("cli"), 9 | debug = require("debug")("analyze-css:runner"), 10 | fs = require("fs"), 11 | resolve = require("path").resolve, 12 | analyzer = require("./index"), 13 | preprocessors = new (require("./preprocessors"))(); 14 | 15 | /** 16 | * Return user agent to be used by analyze-css when making HTTP requests (issue #75) 17 | */ 18 | function getUserAgent() { 19 | var format = require("util").format, 20 | version = require("../package").version; 21 | 22 | return format( 23 | "analyze-css/%s (%s %s, %s %s)", 24 | version, 25 | process.release.name, 26 | process.version, 27 | process.platform, 28 | process.arch, 29 | ); 30 | } 31 | 32 | /** 33 | * Simplified implementation of "request" npm module 34 | * 35 | * @see https://www.npmjs.com/package/node-fetch 36 | */ 37 | async function request(requestOptions, callback) { 38 | const debug = require("debug")("analyze-css:http"); 39 | 40 | const fetch = (await import("node-fetch")).default; 41 | 42 | debug("GET %s", requestOptions.url); 43 | debug("Options: %j", requestOptions); 44 | 45 | fetch(requestOptions.url, requestOptions) 46 | .then(function (resp) { 47 | debug("HTTP %d %s", resp.status, resp.statusText); 48 | debug("Headers: %j", resp.headers._headers); 49 | 50 | if (!resp.ok) { 51 | var err = new Error( 52 | "HTTP request failed: " + 53 | (err 54 | ? err.toString() 55 | : "received HTTP " + resp.status + " " + resp.statusText), 56 | ); 57 | callback(err); 58 | } else { 59 | return resp.text(); // a promise 60 | } 61 | }) 62 | .then(function (body) { 63 | debug("Received %d bytes of CSS", body.length); 64 | callback(null, body); 65 | }) 66 | .catch(function (err) { 67 | debug(err); 68 | callback(err); 69 | }); 70 | } 71 | 72 | /** 73 | * Module's main function 74 | */ 75 | function runner(options, callback) { 76 | // call CommonJS module 77 | var analyzerOpts = { 78 | noOffenders: options.noOffenders, 79 | preprocessor: false, 80 | }; 81 | 82 | function analyze(css) { 83 | analyzer(css, analyzerOpts) 84 | .then((res) => callback(null, res)) 85 | .catch((err) => callback(err, null)); 86 | } 87 | 88 | if (options.url) { 89 | debug("Fetching remote CSS file: %s", options.url); 90 | 91 | // @see https://www.npmjs.com/package/node-fetch#options 92 | var agentOptions = {}, 93 | requestOptions = { 94 | url: options.url, 95 | headers: { 96 | "User-Agent": getUserAgent(), 97 | }, 98 | }; 99 | 100 | // handle options 101 | 102 | // @see https://github.com/bitinn/node-fetch/issues/15 103 | // @see https://nodejs.org/api/https.html#https_https_request_options_callback 104 | if (options.ignoreSslErrors) { 105 | agentOptions.rejectUnauthorized = false; 106 | } 107 | 108 | // @see https://gist.github.com/cojohn/1772154 109 | if (options.authUser && options.authPass) { 110 | requestOptions.headers.Authorization = 111 | "Basic " + 112 | Buffer.from(options.authUser + ":" + options.authPass, "utf8").toString( 113 | "base64", 114 | ); 115 | } 116 | 117 | // @see https://nodejs.org/api/http.html#http_class_http_agent 118 | var client = require(/^https:/.test(options.url) ? "https" : "http"); 119 | requestOptions.agent = new client.Agent(agentOptions); 120 | 121 | // @see http://stackoverflow.com/a/5810547 122 | options.proxy = options.proxy || process.env.HTTP_PROXY; 123 | 124 | if (options.proxy) { 125 | debug("Using HTTP proxy: %s", options.proxy); 126 | 127 | requestOptions.agent = new (require("http-proxy-agent"))(options.proxy); 128 | } 129 | 130 | request(requestOptions, function (err, css) { 131 | if (err) { 132 | err.code = analyzer.EXIT_URL_LOADING_FAILED; 133 | 134 | debug(err); 135 | callback(err); 136 | } else { 137 | analyze(css); 138 | } 139 | }); 140 | } else if (options.file) { 141 | // resolve to the full path 142 | options.file = resolve(process.cwd(), options.file); 143 | debug("Loading local CSS file: %s", options.file); 144 | 145 | fs.readFile( 146 | options.file, 147 | { 148 | encoding: "utf-8", 149 | }, 150 | function (err, css) { 151 | if (err) { 152 | err = new Error("Loading CSS file failed: " + err.toString()); 153 | err.code = analyzer.EXIT_FILE_LOADING_FAILED; 154 | 155 | debug(err); 156 | callback(err); 157 | } else { 158 | // find the matching preprocessor and use it 159 | if (analyzerOpts.preprocessor === false) { 160 | analyzerOpts.preprocessor = preprocessors.findMatchingByFileName( 161 | options.file, 162 | ); 163 | } 164 | 165 | // pass the name of the file being analyzed 166 | analyzerOpts.file = options.file; 167 | 168 | analyze(css); 169 | } 170 | }, 171 | ); 172 | } else if (options.stdin) { 173 | debug("Reading from stdin"); 174 | cli.withStdin(analyze); 175 | } 176 | } 177 | 178 | module.exports = runner; 179 | -------------------------------------------------------------------------------- /lib/types.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file defines names of metrics and events. 3 | */ 4 | 5 | /** 6 | * Names of metrics. This list is generated by ./scripts/types.js file. 7 | */ 8 | export type MetricsNames = 9 | | "base64Length" 10 | | "colors" 11 | | "comments" 12 | | "commentsLength" 13 | | "complexSelectors" 14 | | "declarations" 15 | | "duplicatedProperties" 16 | | "duplicatedSelectors" 17 | | "emptyRules" 18 | | "expressions" 19 | | "importants" 20 | | "imports" 21 | | "length" 22 | | "mediaQueries" 23 | | "multiClassesSelectors" 24 | | "notMinified" 25 | | "oldIEFixes" 26 | | "oldPropertyPrefixes" 27 | | "parsingErrors" 28 | | "propertyResets" 29 | | "qualifiedSelectors" 30 | | "redundantBodySelectors" 31 | | "redundantChildNodesSelectors" 32 | | "rules" 33 | | "selectorLengthAvg" 34 | | "selectors" 35 | | "selectorsByAttribute" 36 | | "selectorsByClass" 37 | | "selectorsById" 38 | | "selectorsByPseudo" 39 | | "selectorsByTag" 40 | | "specificityClassAvg" 41 | | "specificityClassTotal" 42 | | "specificityIdAvg" 43 | | "specificityIdTotal" 44 | | "specificityTagAvg" 45 | | "specificityTagTotal"; 46 | 47 | /** 48 | * Names of events. This list is generated by ./scripts/types.js file. 49 | */ 50 | export type EventsNames = 51 | | "comment" /* (comment) */ 52 | | "css" /* (css) */ 53 | | "declaration" /* (rule, property, value) */ 54 | | "error" /* (err) */ 55 | | "expression" /* (selector, expression) */ 56 | | "font-face" /* (rule) */ 57 | | "import" /* (url) */ 58 | | "media" /* (query) */ 59 | | "mediaEnd" /* (query) */ 60 | | "report" /* () */ 61 | | "rule" /* (rule) */ 62 | | "selector" /* (rule, selector, expressions) */; 63 | 64 | /** 65 | * Encapsulates a set of metrics 66 | */ 67 | export type Metrics = { [metric in MetricsNames]: number }; 68 | 69 | /** 70 | * Encapsulates a set of offenders 71 | */ 72 | import { Position } from "css"; 73 | 74 | export interface Offender { 75 | message: string; 76 | position: Position; 77 | } 78 | export type Offenders = { [metric in MetricsNames]: Array }; 79 | 80 | /** 81 | * A CSS rule taken from "css" package 82 | */ 83 | import { Rule, AtRule, Declaration } from "css"; 84 | 85 | declare interface CssDeclaration extends Declaration { 86 | comment?: string | undefined; 87 | } 88 | 89 | // Array | undefined; 90 | export interface CSSRule extends Rule { 91 | comment?: string | undefined; 92 | 93 | /** The part following @import. */ 94 | import?: string | undefined; 95 | 96 | /** Array of nodes with the types declaration and comment. */ 97 | declarations?: Array | undefined; 98 | 99 | /** The part following @media. */ 100 | media?: string | undefined; 101 | /** Array of nodes with the types rule, comment and any of the at-rule types. */ 102 | rules?: Array | undefined; 103 | } 104 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "analyze-css", 3 | "version": "2.4.7", 4 | "author": "Maciej Brencz (https://github.com/macbre)", 5 | "description": "CSS selectors complexity and performance analyzer", 6 | "main": "./lib/index.js", 7 | "types": "./lib/index.d.ts", 8 | "repository": { 9 | "type": "git", 10 | "url": "git://github.com/macbre/analyze-css.git" 11 | }, 12 | "keywords": [ 13 | "analysis", 14 | "complexity", 15 | "css", 16 | "stylesheet", 17 | "webperf" 18 | ], 19 | "license": "BSD-2-Clause", 20 | "engines": { 21 | "node": ">18" 22 | }, 23 | "dependencies": { 24 | "@adobe/css-tools": "^4.4.2", 25 | "cli": "^1.0.1", 26 | "commander": "^14.0.0", 27 | "css-shorthand-properties": "^1.1.1", 28 | "css-what": "^6.0.1", 29 | "debug": "^4.1.1", 30 | "fast-stats": "0.0.7", 31 | "glob": "^11.0.0", 32 | "http-proxy-agent": "^7.0.0", 33 | "node-fetch": "^3.0.0", 34 | "onecolor": "^4.0.0", 35 | "specificity": "^1.0.0" 36 | }, 37 | "devDependencies": { 38 | "@eslint/js": "^9.1.1", 39 | "@types/css": "0.0.38", 40 | "autoprefixer": "^10.2.4", 41 | "browserslist": "^4.11.1", 42 | "check-dts": "^0.9.0", 43 | "eslint": "^9.1.0", 44 | "eslint-config-prettier": "10.1.5", 45 | "eslint-plugin-node": "^11.1.0", 46 | "globals": "^16.0.0", 47 | "jest": "^29.0.0", 48 | "postcss": "^8.3.6", 49 | "prettier": "3.5.3" 50 | }, 51 | "optionalDependencies": { 52 | "sass": "^1.34.1" 53 | }, 54 | "bin": "./bin/analyze-css.js", 55 | "preferGlobal": true, 56 | "scripts": { 57 | "test": "jest test/ --coverage --detectOpenHandles", 58 | "lint": "eslint .", 59 | "lint:fix": "eslint . --fix", 60 | "prettier": "npx prettier --write .", 61 | "prefixes": "npx browserslist@latest --update-db; DEBUG=* node data/prefixes.js", 62 | "bump-version-patch": "npm version patch && git add -A . && git push origin master && git push --tags && ./create-gh-release.sh", 63 | "check-dts": "check-dts lib/*.d.ts" 64 | }, 65 | "jshintConfig": { 66 | "esversion": 6, 67 | "node": true, 68 | "strict": true, 69 | "validthis": true 70 | }, 71 | "jest": { 72 | "verbose": true, 73 | "coveragePathIgnorePatterns": [ 74 | "test/" 75 | ], 76 | "coverageThreshold": { 77 | "global": { 78 | "statements": 100, 79 | "branches": 100, 80 | "functions": 100, 81 | "lines": 100 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /rules/base64.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var format = require("util").format, 4 | MAX_LENGTH = 4 * 1024; 5 | 6 | /** 7 | * @param { import("../lib/css-analyzer") } analyzer 8 | */ 9 | function rule(analyzer) { 10 | var re = /data:[^/]+\/([^;]+)(?:;charset=[^;]+)?;base64,([^)]+)/; 11 | analyzer.setMetric("base64Length"); 12 | 13 | analyzer.on("declaration", function (rule, property, value) { 14 | var base64, buf, matches; 15 | 16 | if (re.test(value)) { 17 | // parse data URI 18 | matches = value.match(re); 19 | base64 = matches[2]; 20 | buf = Buffer.from(base64, "base64"); 21 | 22 | analyzer.incrMetric("base64Length", base64.length); 23 | 24 | if (base64.length > MAX_LENGTH) { 25 | analyzer.addOffender( 26 | "base64Length", 27 | format( 28 | "%s { %s: ... } // base64: %s kB, raw: %s kB", 29 | rule.selectors.join(", "), 30 | property, 31 | (base64.length / 1024).toFixed(2), 32 | (buf.length / 1024).toFixed(2), 33 | ), 34 | ); 35 | } 36 | } 37 | }); 38 | } 39 | 40 | rule.description = "Reports on base64-encoded images"; 41 | module.exports = rule; 42 | -------------------------------------------------------------------------------- /rules/bodySelectors.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * @typedef { import("css-what").AttributeSelector[] } AttributeSelectors 4 | */ 5 | 6 | /** 7 | * @param { AttributeSelectors } expressions 8 | * @returns { number } 9 | */ 10 | function getBodyIndex(expressions) { 11 | let idx = 0; 12 | 13 | // body.foo h1 -> 0 14 | // .foo body -> 1 15 | // html.css body -> 1 16 | 17 | for (let i = 0; i < expressions.length; i++) { 18 | switch (expressions[i].type) { 19 | case "tag": 20 | if (expressions[i].name === "body") { 21 | return idx; 22 | } 23 | break; 24 | 25 | case "child": 26 | case "descendant": 27 | idx++; 28 | } 29 | } 30 | 31 | return -1; 32 | } 33 | 34 | /** 35 | * @param { AttributeSelectors } expressions 36 | * @returns {boolean} 37 | */ 38 | function firstSelectorHasClass(expressions) { 39 | // remove any non-class selectors 40 | return expressions[0].type === "tag" 41 | ? // h1.foo 42 | expressions[1].type === "attribute" && expressions[1].name === "class" 43 | : // .foo 44 | expressions[0].type === "attribute" && expressions[0].name === "class"; 45 | } 46 | 47 | /** 48 | * @param { AttributeSelectors } expressions 49 | * @returns {number} 50 | */ 51 | function getDescendantCombinatorIndex(expressions) { 52 | // body > .foo 53 | // {"type":"child"} 54 | return expressions 55 | .filter((item) => { 56 | return !["tag", "attribute", "pseudo"].includes(item.type); 57 | }) 58 | .map((item) => { 59 | return item.type; 60 | }) 61 | .indexOf("child"); 62 | } 63 | 64 | /** 65 | * @param { import("../lib/css-analyzer") } analyzer 66 | */ 67 | function rule(analyzer) { 68 | const debug = require("debug")("analyze-css:bodySelectors"); 69 | 70 | analyzer.setMetric("redundantBodySelectors"); 71 | 72 | analyzer.on("selector", function (_, selector, expressions) { 73 | const noExpressions = expressions.length; 74 | 75 | // check more complex selectors only 76 | if (noExpressions < 2) { 77 | return; 78 | } 79 | 80 | const firstTag = expressions[0].type === "tag" && expressions[0].name; 81 | 82 | const firstHasClass = firstSelectorHasClass(expressions); 83 | 84 | const isDescendantCombinator = 85 | getDescendantCombinatorIndex(expressions) === 0; 86 | 87 | // there only a single descendant / child selector 88 | // e.g. "body > foo" or "html h1" 89 | const isShortExpression = 90 | expressions.filter((item) => { 91 | return ["child", "descendant"].includes(item.type); 92 | }).length === 1; 93 | 94 | let isRedundant = true; // always expect the worst ;) 95 | 96 | // first, let's find the body tag selector in the expression 97 | const bodyIndex = getBodyIndex(expressions); 98 | 99 | debug("selector: %s %j", selector, { 100 | firstTag, 101 | firstHasClass, 102 | isDescendantCombinator, 103 | isShortExpression, 104 | bodyIndex, 105 | }); 106 | 107 | // body selector not found - skip the rules that follow 108 | if (bodyIndex < 0) { 109 | return; 110 | } 111 | 112 | // matches "html > body" 113 | // {"type":"tag","name":"html","namespace":null} 114 | // {"type":"child"} 115 | // {"type":"tag","name":"body","namespace":null} 116 | // 117 | // matches "html.modal-popup-mode body" (issue #44) 118 | // {"type":"tag","name":"html","namespace":null} 119 | // {"type":"attribute","name":"class","action":"element","value":"modal-popup-mode","namespace":null,"ignoreCase":false} 120 | // {"type":"descendant"} 121 | // {"type":"tag","name":"body","namespace":null} 122 | if ( 123 | firstTag === "html" && 124 | bodyIndex === 1 && 125 | (isDescendantCombinator || isShortExpression) 126 | ) { 127 | isRedundant = false; 128 | } 129 | // matches "body > .bar" (issue #82) 130 | else if (bodyIndex === 0 && isDescendantCombinator) { 131 | isRedundant = false; 132 | } 133 | // matches "body.foo ul li a" 134 | else if (bodyIndex === 0 && firstHasClass) { 135 | isRedundant = false; 136 | } 137 | // matches ".has-modal > body" (issue #49) 138 | else if (firstHasClass && bodyIndex === 1 && isDescendantCombinator) { 139 | isRedundant = false; 140 | } 141 | 142 | // report he redundant body selector 143 | if (isRedundant) { 144 | debug("selector %s - is redundant", selector); 145 | 146 | analyzer.incrMetric("redundantBodySelectors"); 147 | analyzer.addOffender("redundantBodySelectors", selector); 148 | } 149 | }); 150 | } 151 | 152 | rule.description = "Reports redundant body selectors"; 153 | module.exports = rule; 154 | -------------------------------------------------------------------------------- /rules/childSelectors.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * @param { import("css-what").Selector[] } expressions 5 | * @returns { number } 6 | */ 7 | function getExpressionsLength(expressions) { 8 | // body -> 1 9 | // ul li -> 2 10 | // ol:lang(or) li -> 2 11 | // .class + foo a -> 3 12 | return ( 13 | expressions.filter((item) => { 14 | return ["child", "descendant", "adjacent"].includes(item.type); 15 | }).length + 1 16 | ); 17 | } 18 | 19 | /** 20 | * Report redundant child selectors, e.g.: 21 | * 22 | * ul li 23 | * ul > li 24 | * table > tr 25 | * tr td 26 | * 27 | * @param { import("../lib/css-analyzer") } analyzer 28 | */ 29 | function rule(analyzer) { 30 | // definition of redundant child nodes selectors (see #51 for the initial idea): 31 | // ul li 32 | // ol li 33 | // table tr 34 | // table th 35 | const redundantChildSelectors = { 36 | ul: ["li"], 37 | ol: ["li"], 38 | select: ["option"], 39 | table: ["tr", "th"], // e.g. table can not be followed by any of tr / th 40 | tr: ["td", "th"], 41 | }; 42 | 43 | analyzer.setMetric("redundantChildNodesSelectors"); 44 | 45 | analyzer.on("selector", (_, selector, expressions) => { 46 | // there only a single descendant / child selector 47 | // e.g. "body > foo" or "html h1" 48 | // 49 | // check more complex selectors only 50 | if (getExpressionsLength(expressions) < 3) { 51 | return; 52 | } 53 | 54 | Object.keys(redundantChildSelectors).forEach((tagName) => { 55 | // find the tagName in our selector 56 | const tagInSelectorIndex = expressions 57 | .map((expr) => expr.type == "tag" && expr.name) 58 | .indexOf(tagName); 59 | 60 | // tag not found in the selector 61 | if (tagInSelectorIndex < 0) { 62 | return; 63 | } 64 | 65 | // converts "ul#foo > li.test" selector into [{tag: 'ul'}, {combinator:'child'}, {tag: 'li'}] list 66 | const selectorNodeNames = expressions 67 | .filter((expr) => 68 | [ 69 | "tag", 70 | "descendant" /* */, 71 | "child" /* > */, 72 | "adjacent" /* + */, 73 | ].includes(expr.type), 74 | ) 75 | .map((expr) => 76 | expr.name ? { tag: expr.name } : { combinator: expr.type }, 77 | ); 78 | 79 | // console.log(selector, expressions, selectorNodeNames); 80 | 81 | const tagIndex = selectorNodeNames 82 | .map((item) => item.tag) 83 | .indexOf(tagName); 84 | 85 | const nextTagInSelector = selectorNodeNames[tagIndex + 2]?.tag; 86 | const nextCombinator = selectorNodeNames[tagIndex + 1]?.combinator; 87 | const previousCombinator = selectorNodeNames[tagIndex - 1]?.combinator; 88 | 89 | // our tag is not followed by the tag listed in redundantChildSelectors 90 | const followedByRedundantTag = 91 | redundantChildSelectors[tagName].includes(nextTagInSelector); 92 | if (!followedByRedundantTag) { 93 | return; 94 | } 95 | 96 | // ignore cases like "article > ul li" 97 | if (previousCombinator === "child") { 98 | return; 99 | } 100 | 101 | // console.log( 102 | // tagName, {selector, expressions}, selectorNodeNames, 103 | // {tagIndex, prreviousTagInSelector, previousCombinator, nextTagInSelector, nextCombinator, followedByRedundantTag} 104 | // ); 105 | 106 | // only the following combinator can match: 107 | // ul li 108 | // ul > li 109 | if ( 110 | followedByRedundantTag && 111 | ["descendant", "child"].includes(nextCombinator) 112 | ) { 113 | analyzer.incrMetric("redundantChildNodesSelectors"); 114 | analyzer.addOffender("redundantChildNodesSelectors", selector); 115 | } 116 | }); 117 | }); 118 | } 119 | 120 | rule.description = "Reports redundant child nodes selectors"; 121 | module.exports = rule; 122 | -------------------------------------------------------------------------------- /rules/colors.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var collection = require("../lib/collection"), 4 | debug = require("debug")("analyze-css:colors"), 5 | format = require("util").format, 6 | onecolor = require("onecolor"); 7 | 8 | /** 9 | * Extract CSS colors from given CSS property value 10 | */ 11 | var regex = /(((rgba?|hsl)\([^)]+\))|#(\w{3,6}))/g; 12 | 13 | function extractColors(value) { 14 | var matches = value.match(regex); 15 | return matches || false; 16 | } 17 | 18 | /** 19 | * @param { import("../lib/css-analyzer") } analyzer 20 | */ 21 | function rule(analyzer) { 22 | // store unique colors with the counter 23 | var colors = new collection(); 24 | 25 | analyzer.setMetric("colors"); 26 | 27 | analyzer.on("declaration", function (rule, property, value) { 28 | var extractedColors = extractColors(value); 29 | 30 | if (extractedColors === false) { 31 | return; 32 | } 33 | 34 | debug("%s: %s -> %j", property, value, extractedColors); 35 | 36 | extractedColors 37 | .map(function (item) { 38 | var color = onecolor(item); 39 | 40 | // handle parsing errors 41 | if (color === false) { 42 | return false; 43 | } 44 | 45 | // return either rgba(0,0,0,0.25) or #000000 46 | return color.alpha() < 1.0 ? color.cssa() : color.hex(); 47 | }) 48 | .forEach(function (color) { 49 | if (color !== false) { 50 | colors.push(color); 51 | } 52 | }); 53 | }); 54 | 55 | analyzer.on("report", function () { 56 | analyzer.setCurrentPosition(undefined); 57 | 58 | colors.sort().forEach(function (color, cnt) { 59 | analyzer.incrMetric("colors"); 60 | analyzer.addOffender("colors", format("%s (%d times)", color, cnt)); 61 | }); 62 | }); 63 | } 64 | 65 | rule.description = "Reports number of unique colors used in CSS"; 66 | module.exports = rule; 67 | 68 | // expose for unit testing 69 | module.exports.extractColors = extractColors; 70 | -------------------------------------------------------------------------------- /rules/comments.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const format = require("util").format, 4 | MAX_LENGTH = 256; 5 | 6 | /** 7 | * @param { import("../lib/css-analyzer") } analyzer 8 | */ 9 | function rule(analyzer) { 10 | analyzer.setMetric("comments"); 11 | analyzer.setMetric("commentsLength"); 12 | 13 | analyzer.on("comment", function (comment) { 14 | analyzer.incrMetric("comments"); 15 | analyzer.incrMetric("commentsLength", comment.length); 16 | 17 | // report too long comments 18 | if (comment.length > MAX_LENGTH) { 19 | analyzer.addOffender( 20 | "comments", 21 | format( 22 | '"%s" is too long (%d characters)', 23 | comment.substr(0, 100), 24 | comment.length, 25 | ), 26 | ); 27 | } 28 | }); 29 | } 30 | 31 | rule.description = "Reports too long CSS comments"; 32 | module.exports = rule; 33 | -------------------------------------------------------------------------------- /rules/complex.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var COMPLEX_SELECTOR_THRESHOLD = 3; 4 | 5 | /** 6 | * @param { import("../lib/css-analyzer") } analyzer 7 | */ 8 | function rule(analyzer) { 9 | analyzer.setMetric("complexSelectors"); 10 | 11 | // #foo .bar ul li a 12 | analyzer.on("selector", function (rule, selector, expressions) { 13 | const filteredExpr = expressions.filter((item) => { 14 | return ![ 15 | "adjacent", 16 | "parent", 17 | "child", 18 | "descendant", 19 | "sibling", 20 | "column-combinator", 21 | ].includes(item.type); 22 | }); 23 | if (filteredExpr.length > COMPLEX_SELECTOR_THRESHOLD) { 24 | analyzer.incrMetric("complexSelectors"); 25 | analyzer.addOffender("complexSelectors", selector); 26 | } 27 | }); 28 | } 29 | 30 | rule.description = "Reports too complex CSS selectors"; 31 | module.exports = rule; 32 | -------------------------------------------------------------------------------- /rules/duplicated.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Collection = require("../lib/collection"), 4 | debug = require("debug")("analyze-css:duplicated"), 5 | format = require("util").format; 6 | 7 | /** 8 | * @param { import("../lib/css-analyzer") } analyzer 9 | */ 10 | function rule(analyzer) { 11 | var selectors = new Collection(), 12 | mediaQueryStack = [], 13 | browserPrefixRegEx = /^-(moz|o|webkit|ms)-/; 14 | 15 | analyzer.setMetric("duplicatedSelectors"); 16 | analyzer.setMetric("duplicatedProperties"); 17 | 18 | // handle nested media queries 19 | analyzer.on("media", function (query) { 20 | mediaQueryStack.push(query); 21 | debug("push: %j", mediaQueryStack); 22 | }); 23 | 24 | analyzer.on("mediaEnd", function (query) { 25 | mediaQueryStack.pop(query); 26 | debug("pop: %j", mediaQueryStack); 27 | }); 28 | 29 | // register each rule's selectors 30 | analyzer.on("rule", function (rule) { 31 | selectors.push( 32 | // @media foo 33 | (mediaQueryStack.length > 0 34 | ? "@media " + mediaQueryStack.join(" @media ") + " " 35 | : "") + 36 | // #foo 37 | rule.selectors.join(", "), 38 | ); 39 | }); 40 | 41 | // find duplicated properties (issue #60) 42 | analyzer.on("rule", function (rule) { 43 | var propertiesHash = {}; 44 | 45 | /* istanbul ignore else */ 46 | if (rule.declarations) { 47 | rule.declarations.forEach(function (declaration) { 48 | var propertyName; 49 | 50 | if (declaration.type === "declaration") { 51 | propertyName = declaration.property; 52 | 53 | // skip properties that require browser prefixes 54 | // background-image:-moz-linear-gradient(...) 55 | // background-image:-webkit-gradient(...) 56 | if (browserPrefixRegEx.test(declaration.value) === true) { 57 | return; 58 | } 59 | 60 | // property was already used in the current selector - report it 61 | if (propertiesHash[propertyName] === true) { 62 | // report the position of the offending property 63 | analyzer.setCurrentPosition(declaration.position); 64 | 65 | analyzer.incrMetric("duplicatedProperties"); 66 | analyzer.addOffender( 67 | "duplicatedProperties", 68 | format( 69 | "%s {%s: %s}", 70 | rule.selectors.join(", "), 71 | declaration.property, 72 | declaration.value, 73 | ), 74 | ); 75 | } else { 76 | // mark given property as defined in the context of the current selector 77 | propertiesHash[propertyName] = true; 78 | } 79 | } 80 | }); 81 | } 82 | }); 83 | 84 | // special handling for @font-face (#52) 85 | // include URL when detecting duplicates 86 | analyzer.on("font-face", function (rule) { 87 | rule.declarations.forEach(function (declaration) { 88 | if (declaration.property === "src") { 89 | selectors.push("@font-face src: " + declaration.value); 90 | 91 | debug( 92 | "special handling for @font-face, provided src: %s", 93 | declaration.value, 94 | ); 95 | return false; 96 | } 97 | }); 98 | }); 99 | 100 | analyzer.on("report", function () { 101 | analyzer.setCurrentPosition(undefined); 102 | 103 | selectors.sort().forEach((selector, cnt) => { 104 | if (cnt > 1) { 105 | analyzer.incrMetric("duplicatedSelectors"); 106 | analyzer.addOffender( 107 | "duplicatedSelectors", 108 | format("%s (%d times)", selector, cnt), 109 | ); 110 | } 111 | }); 112 | }); 113 | } 114 | 115 | rule.description = "Reports duplicated CSS selectors and properties"; 116 | module.exports = rule; 117 | -------------------------------------------------------------------------------- /rules/emptyRules.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * @param { import("../lib/css-analyzer") } analyzer 5 | */ 6 | function rule(analyzer) { 7 | analyzer.setMetric("emptyRules"); 8 | 9 | analyzer.on("rule", function (rule) { 10 | var properties = rule.declarations.filter(function (item) { 11 | return item.type === "declaration"; 12 | }); 13 | 14 | if (properties.length === 0) { 15 | analyzer.incrMetric("emptyRules"); 16 | analyzer.addOffender("emptyRules", rule.selectors.join(", ")); 17 | } 18 | }); 19 | } 20 | 21 | rule.description = "Total number of empty CSS rules"; 22 | module.exports = rule; 23 | -------------------------------------------------------------------------------- /rules/expressions.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var format = require("util").format; 4 | 5 | /** 6 | * @param { import("../lib/css-analyzer") } analyzer 7 | */ 8 | function rule(analyzer) { 9 | var re = /^expression/i; 10 | 11 | analyzer.setMetric("expressions"); 12 | 13 | analyzer.on("declaration", function (rule, property, value) { 14 | if (re.test(value)) { 15 | analyzer.incrMetric("expressions"); 16 | analyzer.addOffender( 17 | "expressions", 18 | format("%s {%s: %s}", rule.selectors.join(", "), property, value), 19 | ); 20 | } 21 | }); 22 | } 23 | 24 | rule.description = "Reports CSS expressions"; 25 | module.exports = rule; 26 | -------------------------------------------------------------------------------- /rules/ieFixes.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var format = require("util").format; 4 | 5 | /** 6 | * Rules below match ugly fixes for IE9 and below 7 | * 8 | * @see http://browserhacks.com/ 9 | * @param { import("../lib/css-analyzer") } analyzer 10 | */ 11 | function rule(analyzer) { 12 | var re = { 13 | property: /^(\*|-ms-filter)/, 14 | selector: /^(\* html|html\s?>\s?body) /, 15 | value: /progid:DXImageTransform\.Microsoft|!ie$/, 16 | }; 17 | 18 | analyzer.setMetric("oldIEFixes"); 19 | 20 | // * html // below IE7 fix 21 | // html>body // IE6 excluded fix 22 | // @see http://blogs.msdn.com/b/ie/archive/2005/09/02/460115.aspx 23 | analyzer.on("selector", function (rule, selector) { 24 | if (re.selector.test(selector)) { 25 | analyzer.incrMetric("oldIEFixes"); 26 | analyzer.addOffender("oldIEFixes", selector); 27 | } 28 | }); 29 | 30 | // *foo: bar // IE7 and below fix 31 | // -ms-filter // IE9 and below specific property 32 | // !ie // IE 7 and below equivalent of !important 33 | // @see http://www.impressivewebs.com/ie7-ie8-css-hacks/ 34 | analyzer.on("declaration", function (rule, property, value) { 35 | if (re.property.test(property) || re.value.test(value)) { 36 | analyzer.incrMetric("oldIEFixes"); 37 | analyzer.addOffender( 38 | "oldIEFixes", 39 | format("%s {%s: %s}", rule.selectors.join(", "), property, value), 40 | ); 41 | } 42 | }); 43 | } 44 | 45 | rule.description = 46 | "Reports fixes for old versions of Internet Explorer (IE9 and below)"; 47 | module.exports = rule; 48 | -------------------------------------------------------------------------------- /rules/import.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * @param { import("../lib/css-analyzer") } analyzer 5 | */ 6 | function rule(analyzer) { 7 | analyzer.setMetric("imports"); 8 | 9 | analyzer.on("import", function (url) { 10 | analyzer.incrMetric("imports"); 11 | analyzer.addOffender("imports", url); 12 | }); 13 | } 14 | 15 | rule.description = "Number of @import rules"; 16 | module.exports = rule; 17 | -------------------------------------------------------------------------------- /rules/important.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const format = require("util").format; 4 | 5 | /** 6 | * @param { import("../lib/css-analyzer") } analyzer 7 | */ 8 | function rule(analyzer) { 9 | analyzer.setMetric("importants"); 10 | 11 | analyzer.on("declaration", function (rule, property, value) { 12 | if (value.indexOf("!important") > -1) { 13 | analyzer.incrMetric("importants"); 14 | analyzer.addOffender( 15 | "importants", 16 | format("%s {%s: %s}", rule.selectors.join(", "), property, value), 17 | ); 18 | } 19 | }); 20 | } 21 | 22 | rule.description = "Number of properties with value forced by !important"; 23 | module.exports = rule; 24 | -------------------------------------------------------------------------------- /rules/length.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * @param { import("../lib/css-analyzer") } analyzer 5 | */ 6 | function rule(analyzer) { 7 | analyzer.on("css", function (css) { 8 | analyzer.setMetric("length", css.length); 9 | }); 10 | } 11 | 12 | rule.description = "Length of CSS file"; 13 | module.exports = rule; 14 | -------------------------------------------------------------------------------- /rules/mediaQueries.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var format = require("util").format; 4 | 5 | /** 6 | * @param { import("../lib/css-analyzer") } analyzer 7 | */ 8 | function rule(analyzer) { 9 | analyzer.setMetric("mediaQueries"); 10 | 11 | analyzer.on("media", function (query, rules) { 12 | analyzer.incrMetric("mediaQueries"); 13 | analyzer.addOffender( 14 | "mediaQueries", 15 | format("@media %s (%d rules)", query, rules.length), 16 | ); 17 | }); 18 | } 19 | 20 | rule.description = "Reports media queries"; 21 | module.exports = rule; 22 | -------------------------------------------------------------------------------- /rules/minified.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Detect not minified CSS 5 | * 6 | * @param { import("../lib/css-analyzer") } analyzer 7 | */ 8 | function rule(analyzer) { 9 | analyzer.setMetric("notMinified"); 10 | 11 | /** 12 | * A simple CSS minification detector 13 | * 14 | * @param {string} css 15 | * @return {boolean} 16 | */ 17 | function isMinified(css) { 18 | // analyze the first 1024 characters 19 | css = css.trim().substring(0, 1024); 20 | 21 | // there should be no newline in minified file 22 | return /\n/.test(css) === false; 23 | } 24 | 25 | analyzer.on("css", (css) => { 26 | analyzer.setMetric("notMinified", isMinified(css) ? 0 : 1); 27 | }); 28 | } 29 | 30 | rule.description = "Reports not minified CSS "; 31 | module.exports = rule; 32 | -------------------------------------------------------------------------------- /rules/multiClassesSelectors.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * @param { import("../lib/css-analyzer") } analyzer 5 | */ 6 | function rule(analyzer) { 7 | analyzer.setMetric("multiClassesSelectors"); 8 | 9 | analyzer.on("selector", (_, selector, expressions) => { 10 | const expressionsWithClass = expressions.filter( 11 | (expr) => expr.name === "class", 12 | ); 13 | 14 | // console.log(selector, expressions, {expressionsWithClass}); 15 | 16 | if (expressionsWithClass.length > 1) { 17 | analyzer.incrMetric("multiClassesSelectors"); 18 | analyzer.addOffender( 19 | "multiClassesSelectors", 20 | "." + expressionsWithClass.map((expr) => expr.value).join("."), 21 | ); 22 | } 23 | }); 24 | } 25 | 26 | rule.description = "Reports selectors with multiple classes"; 27 | module.exports = rule; 28 | -------------------------------------------------------------------------------- /rules/parsingErrors.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * @param { import("../lib/css-analyzer") } analyzer 5 | */ 6 | function rule(analyzer) { 7 | analyzer.setMetric("parsingErrors"); 8 | 9 | analyzer.on("error", (err) => { 10 | analyzer.incrMetric("parsingErrors"); 11 | analyzer.addOffender("parsingErrors", err.message); 12 | }); 13 | } 14 | 15 | rule.description = "CSS parsing errors"; 16 | module.exports = rule; 17 | -------------------------------------------------------------------------------- /rules/prefixes.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var debug = require("debug")("analyze-css:prefixes"), 4 | format = require("util").format; 5 | 6 | /** 7 | * @param { import("../lib/css-analyzer") } analyzer 8 | */ 9 | function rule(analyzer) { 10 | var data = require(__dirname + "/prefixes.json"), 11 | prefixes = data.prefixes; 12 | 13 | debug("Using data generated on %s", data.generated); 14 | analyzer.setMetric("oldPropertyPrefixes"); 15 | 16 | analyzer.on("declaration", function (rule, property, value) { 17 | var prefixData = prefixes[property]; 18 | 19 | // prefix needs to be kept 20 | if (prefixData && !prefixData.keep) { 21 | analyzer.incrMetric("oldPropertyPrefixes"); 22 | analyzer.addOffender( 23 | "oldPropertyPrefixes", 24 | format( 25 | "%s { %s: %s } // %s", 26 | rule.selectors.join(", "), 27 | property, 28 | value, 29 | prefixData.msg, 30 | ), 31 | ); 32 | } 33 | }); 34 | } 35 | 36 | rule.description = "Reports outdated vendor prefixes"; 37 | module.exports = rule; 38 | -------------------------------------------------------------------------------- /rules/propertyResets.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var format = require("util").format, 4 | shorthandProperties = require("css-shorthand-properties"); 5 | 6 | /** 7 | * Detect accidental property resets 8 | * 9 | * @see http://css-tricks.com/accidental-css-resets/ 10 | * @param { import("../lib/css-analyzer") } analyzer 11 | */ 12 | function rule(analyzer) { 13 | var debug = require("debug"); 14 | 15 | analyzer.setMetric("propertyResets"); 16 | 17 | analyzer.on("selector", function (rule, selector) { 18 | var declarations = rule.declarations, 19 | properties; 20 | 21 | // prepare the list of properties used in this selector 22 | properties = declarations 23 | .map(function (declaration) { 24 | return declaration.type === "declaration" 25 | ? declaration.property 26 | : false; 27 | }) 28 | .filter(function (item) { 29 | return item !== false; 30 | }); 31 | 32 | debug("%s: %j", selector, properties); 33 | 34 | // iterate through all properties, expand shorthand properties and 35 | // check if there's no expanded version of it earlier in the array 36 | properties.forEach(function (property, idx) { 37 | var expanded; 38 | 39 | // skip if the current property is not the shorthand version 40 | if ( 41 | typeof shorthandProperties.shorthandProperties[property] === "undefined" 42 | ) { 43 | return; 44 | } 45 | 46 | // property = 'margin' 47 | // expanded = [ 'margin-top', 'margin-right', 'margin-bottom', 'margin-left' ] 48 | expanded = shorthandProperties.expand(property); 49 | debug("%s: %s", property, expanded.join(", ")); 50 | 51 | expanded.forEach(function (expandedProperty) { 52 | var propertyPos = properties.indexOf(expandedProperty); 53 | 54 | if (propertyPos > -1 && propertyPos < idx) { 55 | analyzer.incrMetric("propertyResets"); 56 | analyzer.addOffender( 57 | "propertyResets", 58 | format( 59 | '%s: "%s" resets "%s" property set earlier', 60 | selector, 61 | property, 62 | expandedProperty, 63 | ), 64 | ); 65 | } 66 | }); 67 | }); 68 | }); 69 | } 70 | 71 | rule.description = "Reports accidental property resets"; 72 | module.exports = rule; 73 | -------------------------------------------------------------------------------- /rules/qualified.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * @param { import("../lib/css-analyzer") } analyzer 5 | */ 6 | function rule(analyzer) { 7 | analyzer.setMetric("qualifiedSelectors"); 8 | 9 | // @see https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Writing_efficient_CSS 10 | analyzer.on("selector", (_, selector, expressions) => { 11 | var hasId = expressions.some((expr) => expr.name === "id"), 12 | hasTag = expressions.some((expr) => expr.type === "tag"), 13 | hasClass = expressions.some((expr) => expr.name === "class"); 14 | 15 | // console.log(selector, expressions, {hasId, hasTag, hasClass}); 16 | 17 | if ( 18 | // tag#id 19 | (hasId && hasTag) || 20 | // .class#id 21 | (hasId && hasClass) || 22 | // tag.class 23 | (hasClass && hasTag) 24 | ) { 25 | analyzer.incrMetric("qualifiedSelectors"); 26 | analyzer.addOffender("qualifiedSelectors", selector); 27 | } 28 | }); 29 | } 30 | 31 | rule.description = "Reports qualified selectors"; 32 | module.exports = rule; 33 | -------------------------------------------------------------------------------- /rules/specificity.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var debug = require("debug")("analyze-css:specificity"), 4 | specificity = require("specificity"), 5 | stats = require("fast-stats").Stats; 6 | 7 | /** 8 | * @param { import("../lib/css-analyzer") } analyzer 9 | */ 10 | function rule(analyzer) { 11 | var types = ["Id", "Class", "Tag"], 12 | values = []; 13 | 14 | // prepare metrics and stacks for values 15 | types.forEach(function (type) { 16 | analyzer.setMetric("specificity" + type + "Avg"); 17 | analyzer.setMetric("specificity" + type + "Total"); 18 | 19 | values.push(new stats()); 20 | }); 21 | 22 | analyzer.on("selector", function (rule, selector) { 23 | let selectorSpecificity; 24 | try { 25 | selectorSpecificity = specificity.calculate(selector); 26 | } catch (ex) { 27 | return; 28 | } 29 | 30 | /* istanbul ignore if */ 31 | if (!selectorSpecificity) { 32 | debug("not counted for %s!", selector); 33 | return; 34 | } 35 | 36 | // parse the results 37 | const parts = [ 38 | selectorSpecificity["A"], 39 | selectorSpecificity["B"], 40 | selectorSpecificity["C"], 41 | ]; 42 | 43 | debug("%s: %s", selector, parts.join(",")); 44 | 45 | // add each piece to a separate stack 46 | parts.forEach(function (val, idx) { 47 | values[idx].push(val); 48 | }); 49 | }); 50 | 51 | analyzer.on("report", function () { 52 | debug("Gathering stats..."); 53 | 54 | types.forEach(function (type, idx) { 55 | analyzer.setMetric( 56 | "specificity" + type + "Avg", 57 | parseFloat(values[idx].amean().toFixed(2)), 58 | ); 59 | analyzer.setMetric("specificity" + type + "Total", values[idx].Σ()); 60 | }); 61 | 62 | debug("Done"); 63 | }); 64 | } 65 | 66 | // @see http://www.w3.org/TR/css3-selectors/#specificity 67 | // @see http://css-tricks.com/specifics-on-css-specificity/ 68 | rule.description = "Reports rules specificity"; 69 | module.exports = rule; 70 | -------------------------------------------------------------------------------- /rules/stats.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * @param { import("../lib/css-analyzer") } analyzer 5 | */ 6 | function rule(analyzer) { 7 | let selectors = 0, 8 | selectorsLength = 0; 9 | 10 | analyzer.setMetric("selectors"); 11 | analyzer.setMetric("selectorLengthAvg"); 12 | 13 | analyzer.setMetric("selectorsByAttribute"); 14 | analyzer.setMetric("selectorsByClass"); 15 | analyzer.setMetric("selectorsById"); 16 | analyzer.setMetric("selectorsByPseudo"); 17 | analyzer.setMetric("selectorsByTag"); 18 | 19 | analyzer.on("rule", () => { 20 | analyzer.incrMetric("rules"); 21 | }); 22 | 23 | analyzer.on("selector", (_, __, expressions) => { 24 | selectors += 1; 25 | selectorsLength += 26 | expressions.filter((item) => { 27 | return ["child", "descendant"].includes(item.type); 28 | }).length + 1; 29 | }); 30 | 31 | analyzer.on("declaration", () => { 32 | analyzer.incrMetric("declarations"); 33 | }); 34 | 35 | analyzer.on("expression", (selector, expression) => { 36 | // console.log(selector, expression); 37 | 38 | // a[href] 39 | if (["exists"].includes(expression.action)) { 40 | analyzer.incrMetric("selectorsByAttribute"); 41 | } 42 | 43 | // .bar 44 | if (expression.name === "class") { 45 | analyzer.incrMetric("selectorsByClass"); 46 | } 47 | 48 | // #foo 49 | if (expression.name === "id") { 50 | analyzer.incrMetric("selectorsById"); 51 | } 52 | 53 | // a:hover 54 | if (expression.type === "pseudo") { 55 | analyzer.incrMetric("selectorsByPseudo"); 56 | } 57 | 58 | // header 59 | if (expression.type === "tag") { 60 | analyzer.incrMetric("selectorsByTag"); 61 | } 62 | }); 63 | 64 | analyzer.on("report", () => { 65 | analyzer.setMetric("selectors", selectors); 66 | analyzer.setMetric("selectorLengthAvg", selectorsLength / selectors); 67 | }); 68 | } 69 | 70 | rule.description = "Emit CSS stats"; 71 | module.exports = rule; 72 | -------------------------------------------------------------------------------- /scripts/types.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const CSSAnalyzer = require("../lib/css-analyzer"); 3 | 4 | (async () => { 5 | const css = ".foo {margin: 0 !important}"; 6 | 7 | const instance = new CSSAnalyzer({}); 8 | instance.analyze(css); 9 | 10 | console.log("// Typing for metrics and events names."); 11 | 12 | // list of all available metrics 13 | console.log( 14 | "export type MetricsNames = " + 15 | Object.keys(instance.metrics) 16 | .sort() 17 | .map((metric) => { 18 | return `"${metric}"`; 19 | }) 20 | .join(" |\n\t") + 21 | ";", 22 | ); 23 | 24 | // list of all available events 25 | // https://nodejs.org/api/events.html#events_class_eventemitter 26 | const eventsEmitter = instance.emitter; 27 | const events = eventsEmitter.eventNames().sort(); 28 | 29 | console.log( 30 | "export type EventsNames = " + 31 | events 32 | .map((event) => { 33 | return `"${event}"`; 34 | }) 35 | .join(" |\n\t") + 36 | ";", 37 | ); 38 | 39 | console.log("// on() overloaded methods via event-specific callbacks"); 40 | console.log( 41 | events 42 | .map((event) => { 43 | const listeners = eventsEmitter.listeners(event); 44 | const sourceCode = listeners[0].toString(); 45 | const signature = sourceCode 46 | .split("\n")[0] 47 | .toString() 48 | .replace(/function\s?| {/g, "") 49 | .replace(/, /g, ": any, ") 50 | .replace(/([a-z])\)/, "$1: any)"); 51 | 52 | return `public on(ev: "${event}", fn: ${signature} => void): void;`; 53 | }) 54 | .join("\n"), 55 | ); 56 | })(); 57 | -------------------------------------------------------------------------------- /test/colors.test.js: -------------------------------------------------------------------------------- 1 | const { describe, it } = require("@jest/globals"); 2 | 3 | var extractColors = require('../rules/colors').extractColors, 4 | assert = require('assert'); 5 | 6 | describe('Colors', () => { 7 | it('should be properly extracted from CSS properties', () => { 8 | var testCases = [ 9 | [ 10 | '-moz-linear-gradient(top, rgba(240, 231, 223, 0) 50%, #f0e7df 100%)', 11 | ['rgba(240, 231, 223, 0)', '#f0e7df'] 12 | ], 13 | [ 14 | '#c6c3c0 #c6c3c0 transparent transparent', 15 | ['#c6c3c0', '#c6c3c0'] 16 | ], 17 | [ 18 | '1px solid #5dc9f4', 19 | ['#5dc9f4'] 20 | ], 21 | [ 22 | '#dfd', 23 | ['#dfd'] 24 | ], 25 | [ 26 | 'rgb(0,0,0)', 27 | ['rgb(0,0,0)'] 28 | ], 29 | [ 30 | '-webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(50%, #f8f4f0), color-stop(100%, #f0e7df))', 31 | ['#f8f4f0', '#f0e7df'] 32 | ], 33 | [ 34 | 'none', 35 | false 36 | ], 37 | ]; 38 | 39 | testCases.forEach(function(testCase) { 40 | var colors = extractColors(testCase[0]); 41 | assert.deepStrictEqual(colors, testCase[1]); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /test/errors.test.js: -------------------------------------------------------------------------------- 1 | const { describe, it } = require("@jest/globals"); 2 | 3 | const analyzer = require('../'); 4 | const assert = require('assert'); 5 | 6 | const tests = [ 7 | { 8 | name: 'Empty CSS', 9 | css: '', 10 | check: /Empty CSS was provided/, 11 | code: analyzer.EXIT_EMPTY_CSS 12 | }, 13 | { 14 | name: 'CSS with whitespaces only', 15 | css: ' ', 16 | check: /Empty CSS was provided/, 17 | code: analyzer.EXIT_EMPTY_CSS 18 | }, 19 | { 20 | name: 'Non-string value', 21 | css: false, 22 | check: /css parameter passed is not a string/, 23 | code: analyzer.EXIT_CSS_PASSED_IS_NOT_STRING 24 | }, 25 | ]; 26 | 27 | describe('Errors handling', () => { 28 | tests.forEach(test => { 29 | describe(test.name || '"' + test.css + '" CSS snippet', () => { 30 | it('should raise an error with correct error code', async () => { 31 | try { 32 | await analyzer(test.css); 33 | assert.fail("analyzer() is expected to fail"); 34 | } 35 | catch(err) { 36 | assert.strictEqual(err instanceof Error, true, 'Error should be thrown'); 37 | 38 | if (!test.check.test(err.toString())) { 39 | console.error('Got instead: ', err); 40 | assert.fail(`${test.name} case raised: ${err.message} (expected ${test.check})`); 41 | } 42 | 43 | assert.strictEqual(err.code, test.code); 44 | }; 45 | }); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /test/fixes/280-url-with-semicolons.test.js: -------------------------------------------------------------------------------- 1 | const { describe, it } = require("@jest/globals"); 2 | const assert = require('assert'); 3 | 4 | describe('@import rule with url containing semicolon', () => { 5 | it('is properly parsed', () => { 6 | const analyzer = require('../..'), 7 | css = ` 8 | @import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;800&display=swap'); 9 | 10 | body { 11 | 12 | } 13 | `.trim(); 14 | 15 | new analyzer(css, (err, res) => { 16 | assert.ok(err === null, err); 17 | assert.strictEqual(res.metrics.imports, 1, '@import is detected'); 18 | assert.strictEqual(res.metrics.selectors, 1, 'body {} is detected'); 19 | assert.deepStrictEqual(res.offenders.imports[0].message, "url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;800&display=swap')"); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/opts.test.js: -------------------------------------------------------------------------------- 1 | const { describe, it } = require("@jest/globals"); 2 | 3 | var analyzer = require('../'), 4 | assert = require('assert'), 5 | css = '.foo { color: white }'; 6 | 7 | describe('CommonJS module API', () => { 8 | describe('noOffenders option', () => { 9 | it('should be respected', async () => { 10 | var opts = { 11 | 'noOffenders': true 12 | }; 13 | 14 | const res = await analyzer(css, opts); 15 | assert.strictEqual(typeof res.offenders, 'undefined', 'Results should no contain offenders'); 16 | }); 17 | 18 | it('should be void if not provided', async () => { 19 | const res = await analyzer(css); 20 | assert.strictEqual(typeof res.offenders, 'object', 'Results should contain offenders'); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/rules.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * You can test a specific rule only by providing TEST_RULE env variable. 3 | * 4 | * e.g. $ TEST_RULE=stats npx jest test/rules.test.js 5 | */ 6 | const { describe, it } = require("@jest/globals"); 7 | 8 | const analyzer = require('../'), 9 | assert = require('assert'), 10 | glob = require('glob'); 11 | 12 | function testCase(test, testId, testName) { 13 | 14 | it(`case #${testId + 1}`, async () => { 15 | const res = await analyzer(test.css); 16 | 17 | const metricsExpected = test.metrics || {}, 18 | offendersExpected = test.offenders || {}, 19 | metricsActual = res && res.metrics, 20 | offendersActual = res && res.offenders; 21 | 22 | function it(name, fn) { 23 | // console.debug(name + "\n"); 24 | fn(); 25 | } 26 | 27 | Object.keys(metricsExpected).forEach(function(metric) { 28 | it( 29 | 'should emit "' + metric + '" metric with a valid value - #' + (testId + 1), 30 | () => { 31 | assert.strictEqual(metricsActual[metric], metricsExpected[metric], `${testName}: testing ${metric} metric against: ${test.css}`); 32 | } 33 | ); 34 | }); 35 | 36 | Object.keys(offendersExpected).forEach(function(metric) { 37 | it( 38 | 'should emit offender for "' + metric + '" metric with a valid value - #' + (testId + 1), 39 | () => { 40 | assert.deepStrictEqual(offendersActual[metric].map(item => item.message), offendersExpected[metric], `${testName}: testing ${metric} offender against: ${test.css}`); 41 | } 42 | ); 43 | }); 44 | }); 45 | } 46 | 47 | /** 48 | * Read all files in rules/ subdirectory and perform tests defined there 49 | */ 50 | describe('Rules', () => { 51 | const files = glob.sync(__dirname + "/rules/*.js"), 52 | nameRe = /([^/]+)\.js$/, 53 | selectedRule = process.env.TEST_RULE; 54 | 55 | files.forEach(function(file) { 56 | var name = file.match(nameRe)[1], 57 | testDef = require(file).tests || []; 58 | 59 | if (selectedRule && selectedRule !== name) { 60 | return; 61 | } 62 | 63 | describe(name, () => { 64 | testDef.forEach((testItem, testId) => { 65 | testCase(testItem, testId, name); 66 | }); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /test/rules/base64.js: -------------------------------------------------------------------------------- 1 | exports.tests = [ 2 | { 3 | css: '.foo { background-image: url(data:image/gif;base64,R0lGODlhAQABAIABAAAAAP///yH5BAEAAAEALAAAAAABAAEAQAICTAEAOw%3D%3D) }', // blank 1x1 gif 4 | metrics: { 5 | base64Length: 64 6 | }, 7 | }, 8 | { 9 | css: '.foo { background-image: url(data:image/gif;base64,' + 'FFFFFF'.repeat(1024) + ') }', // to big base64-encoded asset 10 | metrics: { 11 | base64Length: 6144 12 | }, 13 | offenders: { 14 | base64Length: [ 15 | ".foo { background-image: ... } // base64: 6.00 kB, raw: 4.50 kB" 16 | ] 17 | } 18 | } 19 | ]; 20 | -------------------------------------------------------------------------------- /test/rules/bodySelectors.js: -------------------------------------------------------------------------------- 1 | exports.tests = [ 2 | { 3 | css: '.foo body > h2 {}', 4 | metrics: { 5 | redundantBodySelectors: 1 6 | } 7 | }, 8 | { 9 | css: 'body ul li a {}', 10 | metrics: { 11 | redundantBodySelectors: 1 12 | } 13 | }, 14 | { 15 | css: 'body#foo ul li a {}', 16 | metrics: { 17 | redundantBodySelectors: 1 18 | } 19 | }, 20 | { 21 | css: 'body > h1 {}', 22 | metrics: { 23 | redundantBodySelectors: 0 24 | } 25 | }, 26 | { 27 | css: 'body > h1 .foo {}', 28 | metrics: { 29 | redundantBodySelectors: 0 30 | } 31 | }, 32 | { 33 | css: 'html > body #foo .bar {}', 34 | metrics: { 35 | redundantBodySelectors: 0 36 | } 37 | }, 38 | { 39 | css: 'body {}', 40 | metrics: { 41 | redundantBodySelectors: 0 42 | } 43 | }, 44 | { 45 | css: 'body.mainpage {}', 46 | metrics: { 47 | redundantBodySelectors: 0 48 | } 49 | }, 50 | { 51 | css: 'body.foo ul li a {}', 52 | metrics: { 53 | redundantBodySelectors: 0 54 | } 55 | }, 56 | { 57 | css: 'html.modal-popup-mode body {}', 58 | metrics: { 59 | redundantBodySelectors: 0 60 | } 61 | }, 62 | { 63 | css: '.has-modal > body {}', 64 | metrics: { 65 | redundantBodySelectors: 0 66 | } 67 | }, 68 | { 69 | css: '.has-modal > body p {}', 70 | metrics: { 71 | redundantBodySelectors: 0 72 | } 73 | } 74 | 75 | ]; 76 | -------------------------------------------------------------------------------- /test/rules/childSelectors.js: -------------------------------------------------------------------------------- 1 | exports.tests = [ 2 | { 3 | css: '.foo body > h2 {}', 4 | metrics: { 5 | redundantChildNodesSelectors: 0 6 | } 7 | }, 8 | // ul li 9 | { 10 | css: 'ul li a {}', 11 | metrics: { 12 | redundantChildNodesSelectors: 1 13 | } 14 | }, 15 | { 16 | css: 'ul > li a {}', 17 | metrics: { 18 | redundantChildNodesSelectors: 1 19 | } 20 | }, 21 | { 22 | css: 'ul + li a {}', 23 | metrics: { 24 | redundantChildNodesSelectors: 0 25 | } 26 | }, 27 | { 28 | css: 'ul a {}', 29 | metrics: { 30 | redundantChildNodesSelectors: 0 31 | } 32 | }, 33 | { 34 | css: '.box ul {}', 35 | metrics: { 36 | redundantChildNodesSelectors: 0 37 | } 38 | }, 39 | { 40 | css: 'li a {}', 41 | metrics: { 42 | redundantChildNodesSelectors: 0 43 | } 44 | }, 45 | { 46 | css: 'article > ul li {}', 47 | metrics: { 48 | redundantChildNodesSelectors: 0 49 | } 50 | }, 51 | // table tr 52 | { 53 | css: '.foo table.test tr.row {}', 54 | metrics: { 55 | redundantChildNodesSelectors: 1 56 | } 57 | }, 58 | { 59 | css: '.foo table th {}', 60 | metrics: { 61 | redundantChildNodesSelectors: 1 62 | } 63 | }, 64 | { 65 | css: '.foo table td {}', 66 | metrics: { 67 | redundantChildNodesSelectors: 0 68 | } 69 | }, 70 | { 71 | css: 'table[class*="infobox"] tr {}', 72 | metrics: { 73 | redundantChildNodesSelectors: 0 74 | } 75 | }, 76 | { 77 | css: 'table[class*="infobox"] tr p {}', 78 | metrics: { 79 | redundantChildNodesSelectors: 1 80 | } 81 | }, 82 | { 83 | css: 'ol:lang(or) li {}', 84 | metrics: { 85 | redundantChildNodesSelectors: 0 86 | } 87 | }, 88 | // select option 89 | { 90 | css: '.form select option {}', 91 | metrics: { 92 | redundantChildNodesSelectors: 1 93 | } 94 | }, 95 | // tr + td 96 | { 97 | css: '.foo tr td {}', 98 | metrics: { 99 | redundantChildNodesSelectors: 1 100 | } 101 | }, 102 | { 103 | css: '.foo tr th {}', 104 | metrics: { 105 | redundantChildNodesSelectors: 1 106 | } 107 | }, 108 | // table + tr & tr + td 109 | { 110 | css: 'table.recommended tr td {}', 111 | metrics: { 112 | redundantChildNodesSelectors: 2 113 | } 114 | }, 115 | { 116 | css: 'table.tableborder tr.first td {}', 117 | metrics: { 118 | redundantChildNodesSelectors: 2 119 | } 120 | }, 121 | { 122 | css: 'table tr th.first {}', 123 | metrics: { 124 | redundantChildNodesSelectors: 2 125 | } 126 | }, 127 | ]; 128 | -------------------------------------------------------------------------------- /test/rules/colors.js: -------------------------------------------------------------------------------- 1 | exports.tests = [ 2 | { 3 | css: '.foo { background-image: url(data:image/gif;base64,R0lGODlhAQABAIABAAAAAP///yH5BAEAAAEALAAAAAABAAEAQAICTAEAOw%3D%3D) }', // blank 1x1 gif 4 | metrics: { 5 | colors: 0 6 | } 7 | }, 8 | { 9 | css: '.foo { border-color: #c6c3c0 #c6c3c0 transparent transparent; }', 10 | metrics: { 11 | colors: 1 12 | } 13 | }, 14 | { 15 | css: '.foo { border-color: #FF8C00; /* foo */}', 16 | metrics: { 17 | colors: 1 18 | } 19 | }, 20 | { 21 | css: '.foo {background-image: -moz-linear-gradient(top, rgba(240, 231, 223, 0) 50%, #f0e7df 100%);background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(50%, #f8f4f0), color-stop(100%, #f0e7df));}', 22 | metrics: { 23 | colors: 3 24 | } 25 | }, 26 | // invalid colors 27 | { 28 | css: '.foo { border-color: #xyz; color: #00; background: #0000 }', 29 | metrics: { 30 | colors: 1 31 | } 32 | }, 33 | // different colors notations should be "casted" to either hex or rgba 34 | { 35 | css: '.foo { border-color: #000 #000000 rgb(0,0,0) rgba(0,0,0,1) }', 36 | metrics: { 37 | colors: 1 38 | } 39 | }, 40 | ]; 41 | -------------------------------------------------------------------------------- /test/rules/comments.js: -------------------------------------------------------------------------------- 1 | exports.tests = [ 2 | { 3 | css: '.foo { color: red } /* test */', 4 | metrics: { 5 | comments: 1, 6 | commentsLength: 6 7 | } 8 | }, 9 | { 10 | css: 'body { padding-bottom: 6em; min-width: 40em; /* for the tabs, mostly */ }', 11 | metrics: { 12 | comments: 1, 13 | commentsLength: 22 14 | } 15 | }, 16 | { 17 | css: 'body { padding-bottom: 6em; min-width: 40em }' + 18 | '/* really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really long comment */', 19 | metrics: { 20 | comments: 1, 21 | commentsLength: 273 22 | }, 23 | offenders: { 24 | comments: [ 25 | "\" really really really really really really really really really really really really really really r\" is too long (273 characters)" 26 | ] 27 | } 28 | } 29 | ]; 30 | -------------------------------------------------------------------------------- /test/rules/complex.js: -------------------------------------------------------------------------------- 1 | exports.tests = [ 2 | { 3 | css: 'header ul li { color: red }', 4 | metrics: { 5 | complexSelectors: 0 6 | } 7 | }, 8 | { 9 | css: 'header > ul li { color: red }', 10 | metrics: { 11 | complexSelectors: 0 12 | } 13 | }, 14 | { 15 | css: 'header ~ ul li { color: red }', 16 | metrics: { 17 | complexSelectors: 0 18 | } 19 | }, 20 | { 21 | css: 'header + ul li { color: red }', 22 | metrics: { 23 | complexSelectors: 0 24 | } 25 | }, 26 | { 27 | css: 'header || ul li { color: red }', 28 | metrics: { 29 | complexSelectors: 0 30 | } 31 | }, 32 | { 33 | css: 'header & ul li { color: red }', 34 | metrics: { 35 | complexSelectors: 0 36 | } 37 | }, 38 | { 39 | css: 'header ul li .foo { color: red }', 40 | metrics: { 41 | complexSelectors: 1 42 | } 43 | }, 44 | { 45 | css: '#foo .bar ul li a { color: red }', 46 | metrics: { 47 | complexSelectors: 1 48 | } 49 | }, 50 | { 51 | css: '.someclass:not([ng-show="gcdmShowSystemNotAvailableMessage()"]){display:none}', 52 | metrics: { 53 | complexSelectors: 0 54 | } 55 | } 56 | ]; 57 | -------------------------------------------------------------------------------- /test/rules/duplicated.js: -------------------------------------------------------------------------------- 1 | exports.tests = [ 2 | { 3 | css: '.foo { } .bar { }', 4 | metrics: { 5 | duplicatedSelectors: 0 6 | } 7 | }, 8 | { 9 | css: '.foo, #bar { } .foo { }', 10 | metrics: { 11 | duplicatedSelectors: 0 12 | } 13 | }, 14 | // media queries 15 | { 16 | css: '.foo { } @media screen { .foo { } }', 17 | metrics: { 18 | duplicatedSelectors: 0 19 | } 20 | }, 21 | { 22 | css: '@media print { .foo { } } @media screen { .foo { } } .foo {}', 23 | metrics: { 24 | duplicatedSelectors: 0 25 | } 26 | }, 27 | { 28 | css: '.foo { } @media screen { .foo { } } .foo { color: red }', 29 | metrics: { 30 | duplicatedSelectors: 1 31 | } 32 | }, 33 | { 34 | css: '#foo { } @media screen { .foo { } } @media screen { .foo { color: red } }', 35 | metrics: { 36 | duplicatedSelectors: 1 37 | } 38 | }, 39 | // duplicated selectors 40 | { 41 | css: '.foo { } .foo { }', 42 | metrics: { 43 | duplicatedSelectors: 1 44 | } 45 | }, 46 | { 47 | css: '.foo { } .bar .foo { } .foo { }', 48 | metrics: { 49 | duplicatedSelectors: 1 50 | } 51 | }, 52 | { 53 | css: '.foo { } .foo { } .foo { }', 54 | metrics: { 55 | duplicatedSelectors: 1 56 | } 57 | }, 58 | { 59 | css: '.foo { } .bar { } .foo { } .bar { } #foo { } .bar { }', 60 | metrics: { 61 | duplicatedSelectors: 2 62 | } 63 | }, 64 | // @font-face (see #52) 65 | { 66 | css: '@font-face { font-family: myFont; src: url(sansation.woff);} @font-face { font-family: myFont; src: url(sansation_bold.woff); font-weight: bold;} ', 67 | metrics: { 68 | duplicatedSelectors: 0 69 | } 70 | }, 71 | { 72 | css: '@font-face { font-family: myFont; src: url(sansation_foo.woff);} @font-face { font-family: myFontFoo; src: url(sansation_foo.woff); font-weight: bold;} ', 73 | metrics: { 74 | duplicatedSelectors: 1 75 | } 76 | }, 77 | // duplicated properties 78 | { 79 | css: '#foo { background: none; background-color: red;}', 80 | metrics: { 81 | duplicatedProperties: 0 82 | } 83 | }, 84 | { 85 | css: '#foo { background: none; background-color: red; background: transparent}', 86 | metrics: { 87 | duplicatedProperties: 1 88 | } 89 | }, 90 | { 91 | css: '#foo { color: #000; background: none; background-color: red; color: red; color: blue; background: none}', 92 | metrics: { 93 | duplicatedProperties: 3 // color x3, background x2 94 | } 95 | }, 96 | { 97 | css: 'button{background-color:#006cb0; background-image:-moz-linear-gradient(top,#008be3 35%,#006cb0 65%);background-image:-webkit-gradient(linear,0% 0%,0% 100%,color-stop(35%,#008be3),color-stop(65%,#006cb0));background-image:-o-linear-gradient(top,#008be3 35%,#006cb0 65%);background-image:-ms-linear-gradient(top,#008be3 35%,#006cb0 65%); border:1px solid #006cb0;border-radius:4px;}', 98 | metrics: { 99 | duplicatedProperties: 0 // browser prefixes should not be included 100 | } 101 | }, 102 | { 103 | css: 'button{background-color:#006cb0; background:-moz-linear-gradient(top,#008be3 35%,#006cb0 65%);background:-webkit-gradient(linear,0% 0%,0% 100%,color-stop(35%,#008be3),color-stop(65%,#006cb0));background:-o-linear-gradient(top,#008be3 35%,#006cb0 65%);background:-ms-linear-gradient(top,#008be3 35%,#006cb0 65%); border:1px solid #006cb0;border-radius:4px;}', 104 | metrics: { 105 | duplicatedProperties: 0 // browser prefixes should not be included 106 | } 107 | }, 108 | ]; 109 | -------------------------------------------------------------------------------- /test/rules/emptyRules.js: -------------------------------------------------------------------------------- 1 | exports.tests = [ 2 | { 3 | css: '.foo { } .bar { color: red } ', 4 | metrics: { 5 | emptyRules: 1 6 | } 7 | }, 8 | { 9 | css: '.foo { /* a comment */ }', 10 | metrics: { 11 | emptyRules: 1 12 | } 13 | }, 14 | 15 | { 16 | css: '.foo { color:red /* a comment */ }', 17 | metrics: { 18 | emptyRules: 0 19 | } 20 | } 21 | ]; 22 | -------------------------------------------------------------------------------- /test/rules/expressions.js: -------------------------------------------------------------------------------- 1 | exports.tests = [ 2 | { 3 | css: '.expression { color: red; }', 4 | metrics: { 5 | expressions: 0 6 | } 7 | }, 8 | { 9 | css: 'p { width: expression( document.body.clientWidth > 600 ? "600px" : "auto" ); }', 10 | metrics: { 11 | expressions: 1 12 | } 13 | }, 14 | { 15 | css: 'body { background-color: expression( (new Date()).getHours()%2 ? "#B8D4FF" : "#F08A00" ); }', 16 | metrics: { 17 | expressions: 1 18 | } 19 | } 20 | ]; 21 | -------------------------------------------------------------------------------- /test/rules/ieFixes.js: -------------------------------------------------------------------------------- 1 | exports.tests = [ 2 | { 3 | css: '* html .foo { color: red } .bar { color: blue }', 4 | metrics: { 5 | oldIEFixes: 1 6 | } 7 | }, 8 | { 9 | css: '.foo { *color: red; border: blue }', 10 | metrics: { 11 | oldIEFixes: 1 12 | } 13 | }, 14 | { 15 | css: 'html>body #tres { color: red }', 16 | metrics: { 17 | oldIEFixes: 1 18 | } 19 | }, 20 | { 21 | css: 'html > body #tres { color: red }', 22 | metrics: { 23 | oldIEFixes: 1 24 | } 25 | }, 26 | { 27 | css: '.foo { filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=70)" }', 28 | metrics: { 29 | oldIEFixes: 1 30 | } 31 | }, 32 | { 33 | css: '.foo { border: blue !ie }', 34 | metrics: { 35 | oldIEFixes: 1 36 | } 37 | } 38 | ]; 39 | -------------------------------------------------------------------------------- /test/rules/import.js: -------------------------------------------------------------------------------- 1 | exports.tests = [ 2 | { 3 | css: '.import { color: red !important }', 4 | metrics: { 5 | imports: 0 6 | } 7 | }, 8 | { 9 | css: '@import url(\'/css/styles.css\');\n.import { color: red !important }', 10 | metrics: { 11 | imports: 1 12 | } 13 | }, 14 | { 15 | css: '@import url(/css/styles.css);\n.import { color: red !important }', 16 | metrics: { 17 | imports: 1 18 | } 19 | } 20 | ]; 21 | -------------------------------------------------------------------------------- /test/rules/important.js: -------------------------------------------------------------------------------- 1 | exports.tests = [ 2 | { 3 | css: '.important { color: red !important }', 4 | metrics: { 5 | importants: 1 6 | } 7 | } 8 | ]; 9 | -------------------------------------------------------------------------------- /test/rules/length.js: -------------------------------------------------------------------------------- 1 | exports.tests = [ 2 | { 3 | css: '.important { color: red !important }', 4 | metrics: { 5 | length: 36 6 | } 7 | } 8 | ]; 9 | -------------------------------------------------------------------------------- /test/rules/mediaQueries.js: -------------------------------------------------------------------------------- 1 | exports.tests = [ 2 | { 3 | css: '#foo {}', 4 | metrics: { 5 | mediaQueries: 0 6 | } 7 | }, 8 | { 9 | css: '@media screen { * {} }', 10 | metrics: { 11 | mediaQueries: 1 12 | } 13 | }, 14 | { 15 | css: '@media screen { * {} } @media print { * {} }', 16 | metrics: { 17 | mediaQueries: 2 18 | } 19 | }, 20 | { 21 | css: '@media screen { @media screen { * {} } }', 22 | metrics: { 23 | mediaQueries: 2 24 | } 25 | } 26 | ]; 27 | -------------------------------------------------------------------------------- /test/rules/minified.js: -------------------------------------------------------------------------------- 1 | exports.tests = [ 2 | { 3 | css: 'code,pre{font-family:Consolas,Menlo,"Liberation Mono",Courier,monospace;font-size:12px;line-height:1.4}html.firefox.windows code span,html.firefox.windows pre span{display:inline-block;line-height:1.25}code,tt{background:#f5f5f5;border:1px solid #ccc;border-radius:3px;padding:1px 3px}pre>code{border-radius:3px;display:block;line-height:18px;margin-left:10px;overflow-y:hidden;padding:6px 10px}textarea{box-sizing:border-box;border:1px solid #ccc;border-radius:5px;padding:7px;line-height:1.29230769;overflow:auto;font-family:Arial,FreeSans,Helvetica,sans-serif;font-size:13px;resize:vertical}img{border:none}img:-moz-broken{-moz-force-broken-image-icon:1}hr{margin:20px 0;border:0;border-bottom:1px solid #ccc}.icon{background-position:center center;background-repeat:no-repeat;display:inline-block;height:16px;text-align:left;overflow:hidden;text-indent:-9999px;width:16px}.dropdown-arrow{border-left:4px solid transparent;border-right:4px solid transparent;border-top:4px solid #fff;content:\'\';display:inline-block;height:0;opacity:.3;vertical-align:top;width:0}', 4 | metrics: { 5 | notMinified: 0 6 | } 7 | }, 8 | { 9 | css: 'code, pre {\n\tfont-family:Consolas,Menlo,"Liberation Mono",Courier,monospace;\n\tfont-size:12px;\n\tline-height:1.4\n}', 10 | metrics: { 11 | notMinified: 1 12 | } 13 | } 14 | ]; 15 | -------------------------------------------------------------------------------- /test/rules/multiClassesSelectors.js: -------------------------------------------------------------------------------- 1 | exports.tests = [ 2 | { 3 | css: '.test, .foo, #bar, header {}', 4 | metrics: { 5 | multiClassesSelectors: 0 6 | } 7 | }, 8 | { 9 | css: 'header#nav.test {}', 10 | metrics: { 11 | multiClassesSelectors: 0 12 | } 13 | }, 14 | { 15 | css: '.foo.test#bar {}', 16 | metrics: { 17 | multiClassesSelectors: 1 18 | }, 19 | offenders: { 20 | multiClassesSelectors: [ 21 | '.foo.test' 22 | ] 23 | } 24 | }, 25 | { 26 | css: 'h1.title.big, a {}', 27 | metrics: { 28 | multiClassesSelectors: 1 29 | }, 30 | offenders: { 31 | multiClassesSelectors: [ 32 | '.title.big' 33 | ] 34 | } 35 | } 36 | ]; 37 | -------------------------------------------------------------------------------- /test/rules/parsingErrors.js: -------------------------------------------------------------------------------- 1 | exports.tests = [ 2 | { 3 | css: 'foo { color= red; } bar { color: blue; } baz {}} boo { display: none}', 4 | metrics: { 5 | parsingErrors: 1 6 | }, 7 | offenders: { 8 | parsingErrors: [ 9 | 'Empty sub-selector' 10 | ] 11 | } 12 | }, 13 | { 14 | css: '.foo { /* a comment */ }', 15 | metrics: { 16 | parsingErrors: 0 17 | } 18 | }, 19 | ]; 20 | -------------------------------------------------------------------------------- /test/rules/prefixes.js: -------------------------------------------------------------------------------- 1 | exports.tests = [ 2 | { 3 | css: '.foo { -moz-border-radius: 2px }', 4 | metrics: { 5 | oldPropertyPrefixes: 1 6 | } 7 | }, 8 | { 9 | css: '.foo { -webkit-backdrop-filter: blur(5px) }', 10 | metrics: { 11 | oldPropertyPrefixes: 0 12 | } 13 | } 14 | ]; 15 | -------------------------------------------------------------------------------- /test/rules/propertyResets.js: -------------------------------------------------------------------------------- 1 | exports.tests = [ 2 | // @see https://css-tricks.com/accidental-css-resets/ 3 | { 4 | css: '.foo { margin: 0; margin-top: 3px }', 5 | metrics: { 6 | propertyResets: 0 7 | } 8 | }, 9 | { 10 | css: '.foo { margin-top: 3px; margin: 0 }', 11 | metrics: { 12 | propertyResets: 1 13 | } 14 | }, 15 | { 16 | css: '.foo { color: red; margin-top: 3px; padding: 0; margin: 0 }', 17 | metrics: { 18 | propertyResets: 1 19 | } 20 | }, 21 | { 22 | css: '.foo { color: red; font-family: Sans-Serif; margin-top: 3px; padding: 0; margin: 0; font: 16px Serif }', 23 | metrics: { 24 | propertyResets: 2 25 | } 26 | }, 27 | // @see https://developer.mozilla.org/en-US/docs/Web/CSS/Shorthand_properties 28 | { 29 | css: '.foo { background-color: red; background: url(images/bg.gif) no-repeat top right }', 30 | metrics: { 31 | propertyResets: 1 32 | } 33 | }, 34 | ]; 35 | -------------------------------------------------------------------------------- /test/rules/qualified.js: -------------------------------------------------------------------------------- 1 | exports.tests = [ 2 | { 3 | css: '.foo, #bar, header {}', 4 | metrics: { 5 | qualifiedSelectors: 0 6 | } 7 | }, 8 | { 9 | css: 'header#nav {}', 10 | metrics: { 11 | qualifiedSelectors: 1 12 | } 13 | }, 14 | { 15 | css: '.foo#bar {}', 16 | metrics: { 17 | qualifiedSelectors: 1 18 | } 19 | }, 20 | { 21 | css: 'h1.title, a {}', 22 | metrics: { 23 | qualifiedSelectors: 1 24 | } 25 | } 26 | ]; 27 | -------------------------------------------------------------------------------- /test/rules/specificity.js: -------------------------------------------------------------------------------- 1 | exports.tests = [ 2 | { 3 | css: '#foo {}', 4 | metrics: { 5 | specificityIdAvg: 1, 6 | specificityIdTotal: 1 7 | } 8 | }, 9 | { 10 | css: '.foo {}', 11 | metrics: { 12 | specificityClassAvg: 1, 13 | specificityClassTotal: 1 14 | } 15 | }, 16 | { 17 | css: 'a {}', 18 | metrics: { 19 | specificityTagAvg: 1, 20 | specificityTagTotal: 1 21 | } 22 | }, 23 | { 24 | css: '.search-result figure.image:hover > .price {}', 25 | metrics: { 26 | specificityIdAvg: 0, 27 | specificityIdTotal: 0, 28 | specificityClassAvg: 4, 29 | specificityClassTotal: 4, 30 | specificityTagAvg: 1, 31 | specificityTagTotal: 1 32 | } 33 | }, 34 | { 35 | css: '.search-result figure.image:hover > .price {} #slideshow > form input[type="text"] {}', 36 | metrics: { 37 | specificityIdAvg: 0.5, 38 | specificityIdTotal: 1, 39 | specificityClassAvg: 2.5, 40 | specificityClassTotal: 5, 41 | specificityTagAvg: 1.5, 42 | specificityTagTotal: 3 43 | } 44 | } 45 | ]; 46 | -------------------------------------------------------------------------------- /test/rules/stats.js: -------------------------------------------------------------------------------- 1 | exports.tests = [ 2 | { 3 | css: '.foo, .bar { color: red; border: blue }', 4 | metrics: { 5 | rules: 1, 6 | selectors: 2, 7 | selectorLengthAvg: 1, 8 | declarations: 2 9 | } 10 | }, 11 | { 12 | css: '#foo {}', 13 | metrics: { 14 | selectorsById: 1 15 | } 16 | }, 17 | { 18 | css: 'a {}', 19 | metrics: { 20 | selectorsByTag: 1 21 | } 22 | }, 23 | { 24 | css: 'ul li, a.foo.bar {}', 25 | metrics: { 26 | selectorsByClass: 2, 27 | selectorsByTag: 3, 28 | selectorLengthAvg: 1.5 29 | } 30 | }, 31 | { 32 | css: '* {}', 33 | metrics: { 34 | selectorsByTag: 0 35 | } 36 | }, 37 | { 38 | css: '[href] {}', 39 | metrics: { 40 | selectorsByAttribute: 1, 41 | selectorLengthAvg: 1 42 | } 43 | }, 44 | { 45 | css: '.bar {}', 46 | metrics: { 47 | selectorsByClass: 1, 48 | } 49 | }, 50 | { 51 | css: 'a:hover {}', 52 | metrics: { 53 | selectorsByPseudo: 1, 54 | } 55 | }, 56 | { 57 | css: '#foo, a.bar, li, a[href], a:hover {}', 58 | metrics: { 59 | selectorsByAttribute: 1, 60 | selectorsByClass: 1, 61 | selectorsById: 1, 62 | selectorsByPseudo: 1, 63 | selectorsByTag: 4 64 | } 65 | }, 66 | { 67 | css: 'div .foo, div .bar, div .foo .bar, div#foo .bar span.test {}', 68 | metrics: { 69 | selectors: 4, 70 | selectorLengthAvg: 2.5 71 | } 72 | } 73 | ]; 74 | -------------------------------------------------------------------------------- /test/sass.test.js: -------------------------------------------------------------------------------- 1 | const { describe, it } = require("@jest/globals"); 2 | 3 | var analyzer = require('../'), 4 | fs = require('fs'), 5 | isSassInstalled = true, 6 | assert = require('assert'), 7 | scss = `.foo { 8 | &.nav { 9 | color: blue 10 | } 11 | };`.trim(), 12 | sass = 'nav\n\tul\n\t\tcolor: white\n', 13 | nodeSassInfo; 14 | 15 | try { 16 | nodeSassInfo = require('sass').info; 17 | console.log(`Using ${nodeSassInfo.replace(/[\n\t]/g, " ")}`); 18 | } catch (e) { 19 | isSassInstalled = false; 20 | } 21 | 22 | describe('SASS preprocessor [' + (isSassInstalled ? 'sass installed' : 'sass missing') + ']', () => { 23 | it('should be chosen for SCSS files', () => { 24 | var preprocessors = new(require('../lib/preprocessors.js'))(); 25 | 26 | assert.strictEqual(preprocessors.findMatchingByFileName('test/foo.scss'), 'sass'); 27 | assert.strictEqual(preprocessors.findMatchingByFileName('test/foo.sass'), 'sass'); 28 | assert.strictEqual(preprocessors.findMatchingByFileName('test/foo.css'), false); 29 | }); 30 | 31 | it('should report parsing error (if not selected)', async () => { 32 | const res = await analyzer(scss); 33 | 34 | assert.strictEqual(res.metrics.parsingErrors, 1); 35 | assert.strictEqual(res.offenders.parsingErrors[0].message, 'Empty sub-selector'); 36 | }); 37 | 38 | if (isSassInstalled === false) { 39 | return; 40 | } 41 | 42 | it('should generate CSS from SCSS correctly', async () => { 43 | try { 44 | await analyzer(scss, { 45 | preprocessor: 'sass' 46 | }); 47 | } catch (e) { 48 | assert.ok(e instanceof Error); 49 | assert.strictEqual(e.message, "Preprocessing failed: Error: TypeError: null: type 'JSNull' is not a subtype of type 'String'"); 50 | } 51 | }); 52 | 53 | it('should generate CSS from SASS correctly', async () => { 54 | try { 55 | await analyzer(sass, { 56 | preprocessor: 'sass' 57 | }); 58 | } catch (e) { 59 | assert.ok(e instanceof Error); 60 | assert.strictEqual(e.message, "Preprocessing failed: Error: TypeError: null: type 'JSNull' is not a subtype of type 'String'"); 61 | } 62 | }); 63 | 64 | it('should parse SCSS file correctly', async () => { 65 | const file = __dirname + '/../examples/base.scss', 66 | source = fs.readFileSync(file).toString(); 67 | 68 | await analyzer(source, { 69 | file: file, 70 | preprocessor: 'sass' 71 | }); 72 | }); 73 | 74 | it('should report parsing error when provided an incorrect syntax', async () => { 75 | try { 76 | await analyzer("bar {foo--}", { 77 | preprocessor: 'sass' 78 | }); 79 | } 80 | catch (err) { 81 | assert.ok(err instanceof Error); 82 | assert.ok(err.message.indexOf("Preprocessing failed:") === 0); 83 | } 84 | }); 85 | 86 | }); 87 | --------------------------------------------------------------------------------