├── .commitlintrc.json ├── .github ├── CODEOWNERS ├── SECURITY.md ├── dependabot.yml ├── mergify.yml └── workflows │ ├── build.yml │ ├── commitlint.yml │ ├── lint.yml │ └── release-please.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .lintstagedrc.json ├── .nvmrc ├── .prettierrc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── eslint.config.mjs ├── examples └── index.html ├── index.d.ts ├── index.js ├── package-lock.json ├── package.json ├── rollup.config.mjs └── test ├── __snapshots__ └── index.test.js.snap ├── data.js ├── index.test.js └── index.test.mjs /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"], 3 | "rules": { 4 | "body-max-line-length": [1, "always", 100] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @remarkablemark 2 | 3 | /package-lock.json 4 | /package.json 5 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Only the current version is supported. Please make sure to update to the latest release. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). 10 | Tidelift will coordinate the fix and disclosure. 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: 'npm' 9 | directory: '/' 10 | schedule: 11 | interval: 'daily' 12 | groups: 13 | commitlint: 14 | patterns: 15 | - '@commitlint/*' 16 | eslint: 17 | patterns: 18 | - '@eslint/*' 19 | prettier: 20 | patterns: 21 | - prettier 22 | - eslint-plugin-prettier 23 | typescript-eslint: 24 | patterns: 25 | - '@typescript-eslint/*' 26 | 27 | - package-ecosystem: 'github-actions' 28 | directory: '/' 29 | schedule: 30 | interval: 'daily' 31 | -------------------------------------------------------------------------------- /.github/mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: automatic merge for Dependabot pull requests 3 | conditions: 4 | - author=dependabot[bot] 5 | - check-success=build 6 | - check-success=commitlint 7 | - check-success=lint 8 | - 'title~=^build\(deps-dev\): bump ' 9 | actions: 10 | merge: 11 | method: squash 12 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | 8 | steps: 9 | - name: Checkout repository 10 | uses: actions/checkout@v4 11 | 12 | - name: Use Node.js 13 | uses: actions/setup-node@v4 14 | with: 15 | cache: npm 16 | node-version-file: .nvmrc 17 | 18 | - name: Install dependencies 19 | run: npm ci --prefer-offline 20 | 21 | - name: Run unit tests 22 | run: npm run test:ci 23 | 24 | - name: Codecov 25 | uses: codecov/codecov-action@v5 26 | env: 27 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 28 | 29 | - name: Run module tests 30 | run: npm run test:esm 31 | 32 | - name: Build artifacts 33 | run: npm run build 34 | -------------------------------------------------------------------------------- /.github/workflows/commitlint.yml: -------------------------------------------------------------------------------- 1 | name: commitlint 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | commitlint: 6 | runs-on: ubuntu-latest 7 | 8 | steps: 9 | - name: Checkout repository 10 | uses: actions/checkout@v4 11 | with: 12 | fetch-depth: 0 13 | 14 | - name: Use Node.js 15 | uses: actions/setup-node@v4 16 | with: 17 | cache: npm 18 | node-version-file: .nvmrc 19 | 20 | - name: Install dependencies 21 | run: npm ci --prefer-offline 22 | 23 | - name: Lint commit message 24 | run: npx commitlint --from=HEAD~1 25 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | lint: 6 | runs-on: ubuntu-latest 7 | 8 | steps: 9 | - name: Checkout repository 10 | uses: actions/checkout@v4 11 | 12 | - name: Use Node.js 13 | uses: actions/setup-node@v4 14 | with: 15 | cache: npm 16 | node-version-file: .nvmrc 17 | 18 | - name: Install dependencies 19 | run: npm ci --prefer-offline 20 | 21 | - name: Run ESLint 22 | run: npm run lint 23 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | name: release-please 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | release-please: 9 | runs-on: ubuntu-latest 10 | outputs: 11 | release_created: ${{ steps.release.outputs.release_created }} 12 | 13 | steps: 14 | - name: Release Please 15 | uses: googleapis/release-please-action@v4 16 | id: release 17 | with: 18 | release-type: node 19 | 20 | publish: 21 | runs-on: ubuntu-latest 22 | permissions: 23 | contents: read 24 | id-token: write 25 | needs: release-please 26 | if: ${{ needs.release-please.outputs.release_created }} 27 | 28 | steps: 29 | - name: Checkout repository 30 | uses: actions/checkout@v4 31 | 32 | - name: Use Node.js 33 | uses: actions/setup-node@v4 34 | with: 35 | node-version-file: .nvmrc 36 | registry-url: https://registry.npmjs.org 37 | 38 | - name: Install dependencies 39 | run: npm ci --prefer-offline 40 | 41 | - name: Publish 42 | run: npm publish --provenance --access public 43 | env: 44 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | *.pid.lock 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # nyc test coverage 19 | .nyc_output 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directories 31 | node_modules 32 | jspm_packages 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional eslint cache 38 | .eslintcache 39 | 40 | # Optional REPL history 41 | .node_repl_history 42 | 43 | # Build files 44 | build 45 | dist 46 | 47 | # Vim swap files 48 | *.swp 49 | 50 | # Mac OS 51 | .DS_Store 52 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx commitlint --edit $1 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm run test:ci 2 | npx lint-staged 3 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.js": "npm run lint:fix", 3 | "*.{css,js,json,md,yml}": "prettier --write" 4 | } 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "none" 4 | } 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [0.2.4](https://github.com/remarkablemark/inline-style-parser/compare/v0.2.3...v0.2.4) (2024-08-29) 6 | 7 | 8 | ### Continuous Integration 9 | 10 | * **github:** publish package to npm registry with provenance ([0b0e756](https://github.com/remarkablemark/inline-style-parser/commit/0b0e756324306750a01bfacfbb53d10a30b76749)) 11 | 12 | ## [0.2.3](https://github.com/remarkablemark/inline-style-parser/compare/v0.2.2...v0.2.3) (2024-03-26) 13 | 14 | 15 | ### Documentation 16 | 17 | * **license:** add MIT license ([eca2bed](https://github.com/remarkablemark/inline-style-parser/commit/eca2bed480f89fb6a2e1566b1ec504a8c2b0d9ff)), closes [#124](https://github.com/remarkablemark/inline-style-parser/issues/124) 18 | 19 | ## [0.2.2](https://github.com/remarkablemark/inline-style-parser/compare/v0.2.1...v0.2.2) (2023-10-15) 20 | 21 | 22 | ### Bug Fixes 23 | 24 | * **index:** export types `Declaration` and `Comment` ([5500e24](https://github.com/remarkablemark/inline-style-parser/commit/5500e24ce617c86d661e6e256d44a57f5b5aadb0)) 25 | 26 | ## [0.2.1](https://github.com/remarkablemark/inline-style-parser/compare/v0.2.0...v0.2.1) (2023-10-15) 27 | 28 | 29 | ### Bug Fixes 30 | 31 | * **package:** add `index.d.ts` to "files" in package.json ([bac1b29](https://github.com/remarkablemark/inline-style-parser/commit/bac1b29ccb36818204936f7983bf4844094560fc)) 32 | 33 | ## [0.2.0](https://github.com/remarkablemark/inline-style-parser/compare/v0.1.2...v0.2.0) (2023-10-15) 34 | 35 | 36 | ### Features 37 | 38 | * **index:** create type declaration file `index.d.ts` ([fc0a669](https://github.com/remarkablemark/inline-style-parser/commit/fc0a669d5c03fddd1687f1d6ff6802da1b0e8852)) 39 | 40 | ## [0.1.2](https://github.com/remarkablemark/inline-style-parser/compare/v0.1.1...v0.1.2) (2023-10-14) 41 | 42 | 43 | ### Build System 44 | 45 | * **rollup:** upgrade rollup, replace plugins, and generate sourcemap ([090cfad](https://github.com/remarkablemark/inline-style-parser/commit/090cfad21d89b9da6bbf215301793a8efb182523)) 46 | 47 | ### [0.1.1](https://github.com/remarkablemark/inline-style-parser/compare/v0.1.0...v0.1.1) (2019-06-20) 48 | 49 | 50 | ### Build System 51 | 52 | * **package:** fix whitelisting of `/dist` in "files" field ([2c13b2f](https://github.com/remarkablemark/inline-style-parser/commit/2c13b2f)) 53 | 54 | 55 | 56 | ## 0.1.0 (2019-06-19) 57 | 58 | 59 | ### Bug Fixes 60 | 61 | * **index:** do not throw an error if a comment precedes the colon ([7f962ee](https://github.com/remarkablemark/inline-style-parser/commit/7f962ee)) 62 | 63 | 64 | ### Build System 65 | 66 | * **package:** add `build` and `clean` scripts ([d27a653](https://github.com/remarkablemark/inline-style-parser/commit/d27a653)) 67 | * **package:** add script `prepublishOnly` and "files" field ([5fad9ff](https://github.com/remarkablemark/inline-style-parser/commit/5fad9ff)) 68 | * **package:** save `css@2.2.4` to devDependencies ([93ad729](https://github.com/remarkablemark/inline-style-parser/commit/93ad729)) 69 | * **package:** save devDependencies for `rollup` and its plugins ([872b1fa](https://github.com/remarkablemark/inline-style-parser/commit/872b1fa)) 70 | * **package:** set `NODE_ENV=development` in script `build:unmin` ([5a7877b](https://github.com/remarkablemark/inline-style-parser/commit/5a7877b)) 71 | * **package:** update `build:min` to generate sourcemap (external) ([c81d66a](https://github.com/remarkablemark/inline-style-parser/commit/c81d66a)) 72 | * **rollup:** add `rollup.config.js` ([ac60124](https://github.com/remarkablemark/inline-style-parser/commit/ac60124)) 73 | 74 | 75 | ### Features 76 | 77 | * clone project from `npm-package-template` ([5976c6f](https://github.com/remarkablemark/inline-style-parser/commit/5976c6f)) 78 | * **index:** copy `parse` module from `css` package ([3bf4bee](https://github.com/remarkablemark/inline-style-parser/commit/3bf4bee)) 79 | * **index:** parse only declarations and remove all unused code ([a04d918](https://github.com/remarkablemark/inline-style-parser/commit/a04d918)) 80 | * **index:** throw error if first argument is not a string ([346ae28](https://github.com/remarkablemark/inline-style-parser/commit/346ae28)) 81 | 82 | 83 | ### Tests 84 | 85 | * add snapshot for the parsed output of a single declaration ([c2c774c](https://github.com/remarkablemark/inline-style-parser/commit/c2c774c)) 86 | * **data:** add more cases for 'content' and 'background-image' ([204c574](https://github.com/remarkablemark/inline-style-parser/commit/204c574)) 87 | * **index:** add more misc and one-off test cases ([a08f521](https://github.com/remarkablemark/inline-style-parser/commit/a08f521)) 88 | * **index:** check that a comment before colon is parsed correctly ([bf9518c](https://github.com/remarkablemark/inline-style-parser/commit/bf9518c)) 89 | * **index:** check that the error message matches ([9169525](https://github.com/remarkablemark/inline-style-parser/commit/9169525)) 90 | * add snapshots for the parsed output of multiple declarations ([8708031](https://github.com/remarkablemark/inline-style-parser/commit/8708031)) 91 | * **index:** disable placeholder test suite ([20bf8af](https://github.com/remarkablemark/inline-style-parser/commit/20bf8af)) 92 | * add cases and compare parser output with `css.parse` output ([361974b](https://github.com/remarkablemark/inline-style-parser/commit/361974b)) 93 | * **index:** refactor tests and use `expect` and `it.each` ([dbf2ef0](https://github.com/remarkablemark/inline-style-parser/commit/dbf2ef0)) 94 | * organize tests with describe blocks and tidy test names ([5c5fcd4](https://github.com/remarkablemark/inline-style-parser/commit/5c5fcd4)) 95 | * replace `mocha` and `nyc` with `jest` ([100311d](https://github.com/remarkablemark/inline-style-parser/commit/100311d)) 96 | * **index:** test "End of comment missing" error and silent option ([9f0c832](https://github.com/remarkablemark/inline-style-parser/commit/9f0c832)) 97 | * **index:** verify that expected errors are thrown ([d0ed3bd](https://github.com/remarkablemark/inline-style-parser/commit/d0ed3bd)) 98 | * **package:** collect coverage from `index.js` only ([bc503b7](https://github.com/remarkablemark/inline-style-parser/commit/bc503b7)) 99 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2012 TJ Holowaychuk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # inline-style-parser 2 | 3 | [![NPM](https://nodei.co/npm/inline-style-parser.png)](https://nodei.co/npm/inline-style-parser/) 4 | 5 | [![NPM version](https://badgen.net/npm/v/inline-style-parser)](https://www.npmjs.com/package/inline-style-parser) 6 | [![Bundlephobia minified + gzip](https://badgen.net/bundlephobia/minzip/inline-style-parser)](https://bundlephobia.com/package/inline-style-parser) 7 | [![build](https://github.com/remarkablemark/inline-style-parser/actions/workflows/build.yml/badge.svg)](https://github.com/remarkablemark/inline-style-parser/actions/workflows/build.yml) 8 | [![codecov](https://codecov.io/gh/remarkablemark/inline-style-parser/branch/master/graph/badge.svg?token=B8EEK5709W)](https://codecov.io/gh/remarkablemark/inline-style-parser) 9 | [![NPM downloads](https://badgen.net/npm/dm/inline-style-parser)](https://www.npmjs.com/package/inline-style-parser) 10 | 11 | Inline style parser copied from [`css/lib/parse/index.js`](https://github.com/reworkcss/css/blob/v2.2.4/lib/parse/index.js): 12 | 13 | ``` 14 | InlineStyleParser(string) 15 | ``` 16 | 17 | Example: 18 | 19 | ```js 20 | const parse = require('inline-style-parser'); 21 | 22 | parse('color: #BADA55;'); 23 | ``` 24 | 25 | Output: 26 | 27 | ```js 28 | [ { type: 'declaration', 29 | property: 'color', 30 | value: '#BADA55', 31 | position: Position { start: [Object], end: [Object], source: undefined } } ] 32 | ``` 33 | 34 | [JSFiddle](https://jsfiddle.net/remarkablemark/hcxbpwq8/) | [Replit](https://replit.com/@remarkablemark/inline-style-parser) | [Examples](https://github.com/remarkablemark/inline-style-parser/tree/master/examples) 35 | 36 | ## Installation 37 | 38 | [NPM](https://www.npmjs.com/package/inline-style-parser): 39 | 40 | ```sh 41 | npm install inline-style-parser --save 42 | ``` 43 | 44 | [Yarn](https://yarnpkg.com/package/inline-style-parser): 45 | 46 | ```sh 47 | yarn add inline-style-parser 48 | ``` 49 | 50 | [CDN](https://unpkg.com/inline-style-parser/): 51 | 52 | ```html 53 | 54 | 57 | ``` 58 | 59 | ## Usage 60 | 61 | Import with ES Modules: 62 | 63 | ```js 64 | import parse from 'inline-style-parser'; 65 | ``` 66 | 67 | Or require with CommonJS: 68 | 69 | ```js 70 | const parse = require('inline-style-parser'); 71 | ``` 72 | 73 | Parse single declaration: 74 | 75 | ```js 76 | parse('left: 0'); 77 | ``` 78 | 79 | Output: 80 | 81 | ```js 82 | [ 83 | { 84 | type: 'declaration', 85 | property: 'left', 86 | value: '0', 87 | position: { 88 | start: { line: 1, column: 1 }, 89 | end: { line: 1, column: 8 }, 90 | source: undefined 91 | } 92 | } 93 | ] 94 | ``` 95 | 96 | Parse multiple declarations: 97 | 98 | ```js 99 | parse('left: 0; right: 100px;'); 100 | ``` 101 | 102 | Output: 103 | 104 | ```js 105 | [ 106 | { 107 | type: 'declaration', 108 | property: 'left', 109 | value: '0', 110 | position: { 111 | start: { line: 1, column: 1 }, 112 | end: { line: 1, column: 8 }, 113 | source: undefined 114 | } 115 | }, 116 | { 117 | type: 'declaration', 118 | property: 'right', 119 | value: '100px', 120 | position: { 121 | start: { line: 1, column: 10 }, 122 | end: { line: 1, column: 22 }, 123 | source: undefined 124 | } 125 | } 126 | ] 127 | ``` 128 | 129 | Parse declaration with missing value: 130 | 131 | ```js 132 | parse('top:'); 133 | ``` 134 | 135 | Output: 136 | 137 | ```js 138 | [ 139 | { 140 | type: 'declaration', 141 | property: 'top', 142 | value: '', 143 | position: { 144 | start: { line: 1, column: 1 }, 145 | end: { line: 1, column: 5 }, 146 | source: undefined 147 | } 148 | } 149 | ] 150 | ``` 151 | 152 | Parse unknown declaration: 153 | 154 | ```js 155 | parse('answer: 42;'); 156 | ``` 157 | 158 | Output: 159 | 160 | ```js 161 | [ 162 | { 163 | type: 'declaration', 164 | property: 'answer', 165 | value: '42', 166 | position: { 167 | start: { line: 1, column: 1 }, 168 | end: { line: 1, column: 11 }, 169 | source: undefined 170 | } 171 | } 172 | ] 173 | ``` 174 | 175 | Invalid declarations: 176 | 177 | ```js 178 | parse(''); // [] 179 | parse(); // throws TypeError 180 | parse(1); // throws TypeError 181 | parse('width'); // throws Error 182 | parse('/*'); // throws Error 183 | ``` 184 | 185 | ## Testing 186 | 187 | Run tests: 188 | 189 | ```sh 190 | npm test 191 | ``` 192 | 193 | Run tests in watch mode: 194 | 195 | ```sh 196 | npm run test:watch 197 | ``` 198 | 199 | Run tests with coverage: 200 | 201 | ```sh 202 | npm run test:coverage 203 | ``` 204 | 205 | Run tests in CI mode: 206 | 207 | ```sh 208 | npm run test:ci 209 | ``` 210 | 211 | Lint files: 212 | 213 | ```sh 214 | npm run lint 215 | ``` 216 | 217 | Fix lint errors: 218 | 219 | ```sh 220 | npm run lint:fix 221 | ``` 222 | 223 | ## Release 224 | 225 | Release and publish are automated by [Release Please](https://github.com/googleapis/release-please). 226 | 227 | ## License 228 | 229 | [MIT](https://github.com/remarkablemark/inline-style-parser/blob/master/LICENSE). See the [license](https://github.com/reworkcss/css/blob/v2.2.4/LICENSE) from the original project. 230 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | 4 | import { includeIgnoreFile } from '@eslint/compat'; 5 | import { FlatCompat } from '@eslint/eslintrc'; 6 | import js from '@eslint/js'; 7 | import prettier from 'eslint-plugin-prettier'; 8 | import simpleImportSort from 'eslint-plugin-simple-import-sort'; 9 | import globals from 'globals'; 10 | 11 | const __filename = fileURLToPath(import.meta.url); 12 | const __dirname = path.dirname(__filename); 13 | const gitignorePath = path.resolve(__dirname, '.gitignore'); 14 | 15 | const compat = new FlatCompat({ 16 | baseDirectory: __dirname, 17 | recommendedConfig: js.configs.recommended, 18 | allConfig: js.configs.all 19 | }); 20 | 21 | export default [ 22 | includeIgnoreFile(gitignorePath), 23 | 24 | ...compat.extends('eslint:recommended'), 25 | 26 | { 27 | plugins: { 28 | prettier, 29 | 'simple-import-sort': simpleImportSort 30 | }, 31 | 32 | languageOptions: { 33 | globals: { 34 | ...globals.browser, 35 | ...globals.commonjs, 36 | ...globals.jest, 37 | ...globals.node 38 | } 39 | }, 40 | 41 | rules: { 42 | 'no-console': 'error', 43 | 'no-debugger': 'error', 44 | 'prettier/prettier': 'error', 45 | 'simple-import-sort/exports': 'error', 46 | 'simple-import-sort/imports': 'error' 47 | } 48 | } 49 | ]; 50 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 3 |
4 | 5 | 6 | 21 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | interface Position { 2 | start: { 3 | line: number; 4 | column: number; 5 | }; 6 | end: { 7 | line: number; 8 | column: number; 9 | }; 10 | source?: string; 11 | } 12 | 13 | export interface Declaration { 14 | type: 'declaration'; 15 | property: string; 16 | value: string; 17 | position: Position; 18 | } 19 | 20 | export interface Comment { 21 | type: 'comment'; 22 | comment: string; 23 | position: Position; 24 | } 25 | 26 | interface Options { 27 | source?: string; 28 | silent?: boolean; 29 | } 30 | 31 | export default function InlineStyleParser( 32 | style: string, 33 | options?: Options 34 | ): (Declaration | Comment)[]; 35 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // http://www.w3.org/TR/CSS21/grammar.html 2 | // https://github.com/visionmedia/css-parse/pull/49#issuecomment-30088027 3 | var COMMENT_REGEX = /\/\*[^*]*\*+([^/*][^*]*\*+)*\//g; 4 | 5 | var NEWLINE_REGEX = /\n/g; 6 | var WHITESPACE_REGEX = /^\s*/; 7 | 8 | // declaration 9 | var PROPERTY_REGEX = /^(\*?[-#/*\\\w]+(\[[0-9a-z_-]+\])?)\s*/; 10 | var COLON_REGEX = /^:\s*/; 11 | var VALUE_REGEX = /^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^)]*?\)|[^};])+)/; 12 | var SEMICOLON_REGEX = /^[;\s]*/; 13 | 14 | // https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/Trim#Polyfill 15 | var TRIM_REGEX = /^\s+|\s+$/g; 16 | 17 | // strings 18 | var NEWLINE = '\n'; 19 | var FORWARD_SLASH = '/'; 20 | var ASTERISK = '*'; 21 | var EMPTY_STRING = ''; 22 | 23 | // types 24 | var TYPE_COMMENT = 'comment'; 25 | var TYPE_DECLARATION = 'declaration'; 26 | 27 | /** 28 | * @param {String} style 29 | * @param {Object} [options] 30 | * @return {Object[]} 31 | * @throws {TypeError} 32 | * @throws {Error} 33 | */ 34 | module.exports = function (style, options) { 35 | if (typeof style !== 'string') { 36 | throw new TypeError('First argument must be a string'); 37 | } 38 | 39 | if (!style) return []; 40 | 41 | options = options || {}; 42 | 43 | /** 44 | * Positional. 45 | */ 46 | var lineno = 1; 47 | var column = 1; 48 | 49 | /** 50 | * Update lineno and column based on `str`. 51 | * 52 | * @param {String} str 53 | */ 54 | function updatePosition(str) { 55 | var lines = str.match(NEWLINE_REGEX); 56 | if (lines) lineno += lines.length; 57 | var i = str.lastIndexOf(NEWLINE); 58 | column = ~i ? str.length - i : column + str.length; 59 | } 60 | 61 | /** 62 | * Mark position and patch `node.position`. 63 | * 64 | * @return {Function} 65 | */ 66 | function position() { 67 | var start = { line: lineno, column: column }; 68 | return function (node) { 69 | node.position = new Position(start); 70 | whitespace(); 71 | return node; 72 | }; 73 | } 74 | 75 | /** 76 | * Store position information for a node. 77 | * 78 | * @constructor 79 | * @property {Object} start 80 | * @property {Object} end 81 | * @property {undefined|String} source 82 | */ 83 | function Position(start) { 84 | this.start = start; 85 | this.end = { line: lineno, column: column }; 86 | this.source = options.source; 87 | } 88 | 89 | /** 90 | * Non-enumerable source string. 91 | */ 92 | Position.prototype.content = style; 93 | 94 | var errorsList = []; 95 | 96 | /** 97 | * Error `msg`. 98 | * 99 | * @param {String} msg 100 | * @throws {Error} 101 | */ 102 | function error(msg) { 103 | var err = new Error( 104 | options.source + ':' + lineno + ':' + column + ': ' + msg 105 | ); 106 | err.reason = msg; 107 | err.filename = options.source; 108 | err.line = lineno; 109 | err.column = column; 110 | err.source = style; 111 | 112 | if (options.silent) { 113 | errorsList.push(err); 114 | } else { 115 | throw err; 116 | } 117 | } 118 | 119 | /** 120 | * Match `re` and return captures. 121 | * 122 | * @param {RegExp} re 123 | * @return {undefined|Array} 124 | */ 125 | function match(re) { 126 | var m = re.exec(style); 127 | if (!m) return; 128 | var str = m[0]; 129 | updatePosition(str); 130 | style = style.slice(str.length); 131 | return m; 132 | } 133 | 134 | /** 135 | * Parse whitespace. 136 | */ 137 | function whitespace() { 138 | match(WHITESPACE_REGEX); 139 | } 140 | 141 | /** 142 | * Parse comments. 143 | * 144 | * @param {Object[]} [rules] 145 | * @return {Object[]} 146 | */ 147 | function comments(rules) { 148 | var c; 149 | rules = rules || []; 150 | while ((c = comment())) { 151 | if (c !== false) { 152 | rules.push(c); 153 | } 154 | } 155 | return rules; 156 | } 157 | 158 | /** 159 | * Parse comment. 160 | * 161 | * @return {Object} 162 | * @throws {Error} 163 | */ 164 | function comment() { 165 | var pos = position(); 166 | if (FORWARD_SLASH != style.charAt(0) || ASTERISK != style.charAt(1)) return; 167 | 168 | var i = 2; 169 | while ( 170 | EMPTY_STRING != style.charAt(i) && 171 | (ASTERISK != style.charAt(i) || FORWARD_SLASH != style.charAt(i + 1)) 172 | ) { 173 | ++i; 174 | } 175 | i += 2; 176 | 177 | if (EMPTY_STRING === style.charAt(i - 1)) { 178 | return error('End of comment missing'); 179 | } 180 | 181 | var str = style.slice(2, i - 2); 182 | column += 2; 183 | updatePosition(str); 184 | style = style.slice(i); 185 | column += 2; 186 | 187 | return pos({ 188 | type: TYPE_COMMENT, 189 | comment: str 190 | }); 191 | } 192 | 193 | /** 194 | * Parse declaration. 195 | * 196 | * @return {Object} 197 | * @throws {Error} 198 | */ 199 | function declaration() { 200 | var pos = position(); 201 | 202 | // prop 203 | var prop = match(PROPERTY_REGEX); 204 | if (!prop) return; 205 | comment(); 206 | 207 | // : 208 | if (!match(COLON_REGEX)) return error("property missing ':'"); 209 | 210 | // val 211 | var val = match(VALUE_REGEX); 212 | 213 | var ret = pos({ 214 | type: TYPE_DECLARATION, 215 | property: trim(prop[0].replace(COMMENT_REGEX, EMPTY_STRING)), 216 | value: val 217 | ? trim(val[0].replace(COMMENT_REGEX, EMPTY_STRING)) 218 | : EMPTY_STRING 219 | }); 220 | 221 | // ; 222 | match(SEMICOLON_REGEX); 223 | 224 | return ret; 225 | } 226 | 227 | /** 228 | * Parse declarations. 229 | * 230 | * @return {Object[]} 231 | */ 232 | function declarations() { 233 | var decls = []; 234 | 235 | comments(decls); 236 | 237 | // declarations 238 | var decl; 239 | while ((decl = declaration())) { 240 | if (decl !== false) { 241 | decls.push(decl); 242 | comments(decls); 243 | } 244 | } 245 | 246 | return decls; 247 | } 248 | 249 | whitespace(); 250 | return declarations(); 251 | }; 252 | 253 | /** 254 | * Trim `str`. 255 | * 256 | * @param {String} str 257 | * @return {String} 258 | */ 259 | function trim(str) { 260 | return str ? str.replace(TRIM_REGEX, EMPTY_STRING) : EMPTY_STRING; 261 | } 262 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inline-style-parser", 3 | "version": "0.2.4", 4 | "description": "An inline style parser.", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "rollup --config --failAfterWarnings", 8 | "clean": "rm -rf dist", 9 | "lint": "eslint .", 10 | "lint:fix": "npm run lint -- --fix", 11 | "prepare": "husky", 12 | "prepublishOnly": "npm run lint && npm test && npm run build", 13 | "test": "jest", 14 | "test:ci": "CI=true jest --ci --colors --coverage --collectCoverageFrom=index.js", 15 | "test:esm": "node --test test/index.test.mjs", 16 | "test:watch": "jest --watch" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/remarkablemark/inline-style-parser.git" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/remarkablemark/inline-style-parser/issues" 24 | }, 25 | "keywords": [ 26 | "inline-style-parser", 27 | "inline-style", 28 | "style", 29 | "parser", 30 | "css" 31 | ], 32 | "devDependencies": { 33 | "@commitlint/cli": "19.8.1", 34 | "@commitlint/config-conventional": "19.8.1", 35 | "@eslint/compat": "1.2.9", 36 | "@eslint/eslintrc": "3.3.1", 37 | "@eslint/js": "9.28.0", 38 | "@rollup/plugin-commonjs": "28.0.3", 39 | "@rollup/plugin-terser": "0.4.4", 40 | "css": "3.0.0", 41 | "eslint": "9.28.0", 42 | "eslint-plugin-prettier": "5.4.1", 43 | "eslint-plugin-simple-import-sort": "12.1.1", 44 | "globals": "16.2.0", 45 | "husky": "9.1.7", 46 | "jest": "29.7.0", 47 | "lint-staged": "16.1.0", 48 | "prettier": "3.5.3", 49 | "rollup": "4.42.0" 50 | }, 51 | "files": [ 52 | "/dist", 53 | "/index.d.ts" 54 | ], 55 | "license": "MIT" 56 | } 57 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import commonjs from '@rollup/plugin-commonjs'; 2 | import terser from '@rollup/plugin-terser'; 3 | 4 | /** 5 | * Build rollup config for development or production. 6 | */ 7 | const getConfig = (minify = false) => ({ 8 | input: 'index.js', 9 | output: { 10 | file: `dist/inline-style-parser${minify ? '.min' : ''}.js`, 11 | format: 'umd', 12 | name: 'InlineStyleParser', 13 | sourcemap: true 14 | }, 15 | plugins: [commonjs(), minify && terser()] 16 | }); 17 | 18 | const configs = [getConfig(), getConfig(true)]; 19 | 20 | export default configs; 21 | -------------------------------------------------------------------------------- /test/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`inline-style-parser parses multiple declarations 1`] = ` 4 | [ 5 | { 6 | "position": Position { 7 | "end": { 8 | "column": 90, 9 | "line": 2, 10 | }, 11 | "source": undefined, 12 | "start": { 13 | "column": 7, 14 | "line": 2, 15 | }, 16 | }, 17 | "property": "background", 18 | "type": "declaration", 19 | "value": "-webkit-gradient(linear, left top, left bottom, from(white), to(black))", 20 | }, 21 | { 22 | "position": Position { 23 | "end": { 24 | "column": 24, 25 | "line": 3, 26 | }, 27 | "source": undefined, 28 | "start": { 29 | "column": 7, 30 | "line": 3, 31 | }, 32 | }, 33 | "property": "content", 34 | "type": "declaration", 35 | "value": "" "", 36 | }, 37 | { 38 | "comment": " comment ", 39 | "position": Position { 40 | "end": { 41 | "column": 39, 42 | "line": 3, 43 | }, 44 | "source": undefined, 45 | "start": { 46 | "column": 26, 47 | "line": 3, 48 | }, 49 | }, 50 | "type": "comment", 51 | }, 52 | { 53 | "position": Position { 54 | "end": { 55 | "column": 14, 56 | "line": 4, 57 | }, 58 | "source": undefined, 59 | "start": { 60 | "column": 7, 61 | "line": 4, 62 | }, 63 | }, 64 | "property": "foo", 65 | "type": "declaration", 66 | "value": "bar", 67 | }, 68 | { 69 | "position": Position { 70 | "end": { 71 | "column": 5, 72 | "line": 5, 73 | }, 74 | "source": undefined, 75 | "start": { 76 | "column": 15, 77 | "line": 4, 78 | }, 79 | }, 80 | "property": "-o-transition", 81 | "type": "declaration", 82 | "value": "all .5s", 83 | }, 84 | ] 85 | `; 86 | 87 | exports[`inline-style-parser parses single declaration 1`] = ` 88 | [ 89 | { 90 | "position": Position { 91 | "end": { 92 | "column": 26, 93 | "line": 1, 94 | }, 95 | "source": undefined, 96 | "start": { 97 | "column": 1, 98 | "line": 1, 99 | }, 100 | }, 101 | "property": "background-color", 102 | "type": "declaration", 103 | "value": "#C0FFEE", 104 | }, 105 | ] 106 | `; 107 | 108 | exports[`inline-style-parser parses when comment precedes colon 1`] = ` 109 | [ 110 | { 111 | "position": Position { 112 | "end": { 113 | "column": 40, 114 | "line": 1, 115 | }, 116 | "source": undefined, 117 | "start": { 118 | "column": 1, 119 | "line": 1, 120 | }, 121 | }, 122 | "property": "text-align", 123 | "type": "declaration", 124 | "value": "center", 125 | }, 126 | ] 127 | `; 128 | 129 | exports[`options source parses declaration 1`] = ` 130 | [ 131 | { 132 | "position": Position { 133 | "end": { 134 | "column": 4, 135 | "line": 1, 136 | }, 137 | "source": "file.css", 138 | "start": { 139 | "column": 1, 140 | "line": 1, 141 | }, 142 | }, 143 | "property": "a", 144 | "type": "declaration", 145 | "value": "b", 146 | }, 147 | ] 148 | `; 149 | -------------------------------------------------------------------------------- /test/data.js: -------------------------------------------------------------------------------- 1 | const cases = [ 2 | // general 3 | 'display: inline-block;', 4 | 'color:red;', 5 | 'margin: 0 auto;', 6 | 'border: 5px solid #BADA55;', 7 | 'font-size: .75em; position:absolute;width: 33.3%; z-index:1337;', 8 | 'font-family: "Goudy Bookletter 1911", Gill Sans Extrabold, sans-serif;', 9 | 10 | // multiple of same property 11 | 'color:rgba(0,0,0,1);color:white;', 12 | 13 | // missing semicolon 14 | 'line-height: 42', 15 | 'font-style:italic; text-transform:uppercase', 16 | 17 | // extra whitespace 18 | ' padding-bottom : 1px', 19 | 'padding: 12px 0 ', 20 | ` 21 | -moz-border-radius: 10px 5px; 22 | -webkit-border-top-left-radius: 10px; 23 | -webkit-border-top-right-radius: 5px; 24 | -webkit-border-bottom-right-radius: 10px; 25 | -webkit-border-bottom-left-radius: 5px; 26 | border-radius: 10px 5px; 27 | `, 28 | 29 | // text and url 30 | 'content: "Lorem ipsum";', 31 | 'content: "foo: bar;";', 32 | 'background-image: url(http://example.com/img.png)', 33 | 'background: #123456 url("https://foo.bar/image.png?v=2")', 34 | 35 | // vendor prefixes 36 | 'background: -webkit-linear-gradient(90deg, black, #111)', 37 | '-webkit-transition: all 4s ease; -moz-transition: all 4s ease; -ms-transition: all 4s ease; -o-transition: all 4s ease; transition: all 4s ease;', 38 | 39 | // comment 40 | '/* comment */', 41 | 'top: 0; /* comment */ bottom: 42rem;', 42 | ` right: 0; /* comment */ 43 | /* comment */ left: 42rem; `, 44 | 45 | // custom 46 | 'foo: bar;', 47 | 'foo:bar; baz:qux', 48 | 49 | // misc 50 | '', 51 | 'overflow:' 52 | ]; 53 | 54 | const snapshots = [ 55 | // `css.parse` throws an error when a comment precedes the colon (bug) 56 | [ 57 | 'parses when comment precedes colon', 58 | 'text-align/**/ /*:*/ : /*:*//**/ center' 59 | ], 60 | 61 | ['parses single declaration', 'background-color: #C0FFEE;'], 62 | 63 | [ 64 | 'parses multiple declarations', 65 | ` 66 | background: -webkit-gradient(linear, left top, left bottom, from(white), to(black)); 67 | content : " " ; /* comment */ 68 | foo:bar;-o-transition:all .5s 69 | ` 70 | ] 71 | ]; 72 | 73 | const errors = [ 74 | ...[ 75 | undefined, 76 | null, 77 | true, 78 | false, 79 | 0, 80 | 1, 81 | {}, 82 | ['Array'], 83 | new Date(), 84 | () => Function 85 | ].map((value) => [value, 'First argument must be a string']), 86 | ['overflow', /property missing ':'$/], 87 | ['/*', /End of comment missing$/] 88 | ]; 89 | 90 | module.exports = { 91 | cases, 92 | errors, 93 | snapshots 94 | }; 95 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | const cssParse = require('css/lib/parse'); 2 | const inlineStyleParser = require('..'); 3 | const { cases, errors, snapshots } = require('./data'); 4 | 5 | /** 6 | * @param {string} inlineStyle 7 | * @return {Array} 8 | */ 9 | function getDeclarations(inlineStyle) { 10 | const { 11 | stylesheet: { rules } 12 | } = cssParse(`a{${inlineStyle}}`); 13 | return rules[0].declarations; 14 | } 15 | 16 | /** 17 | * @param {Object[]} declarations 18 | * @return {Object[]} 19 | */ 20 | function removePosition(declarations) { 21 | return declarations.map(({ type, property, value }) => ({ 22 | type, 23 | property, 24 | value 25 | })); 26 | } 27 | 28 | // compare output with `css.parse` declarations 29 | describe('when compared to `css.parse`', () => { 30 | it.each(cases)('parses "%s"', (style) => { 31 | expect(removePosition(inlineStyleParser(style))).toEqual( 32 | removePosition(getDeclarations(style)) 33 | ); 34 | }); 35 | }); 36 | 37 | // match snapshots 38 | describe('inline-style-parser', () => { 39 | it.each(snapshots)('%s', (_name, style) => { 40 | expect(inlineStyleParser(style)).toMatchSnapshot(); 41 | }); 42 | }); 43 | 44 | // errors 45 | describe('error', () => { 46 | it.each(errors)('throws when argument is `%s`', (value, message) => { 47 | expect(() => inlineStyleParser(value)).toThrow(message); 48 | }); 49 | }); 50 | 51 | describe('options', () => { 52 | describe.each` 53 | style | options | expected 54 | ${':'} | ${undefined} | ${[]} 55 | ${'a'} | ${{ silent: true }} | ${[]} 56 | ${''} | ${{ source: 'a' }} | ${[]} 57 | `( 58 | 'when style="$style" and options=`$options`', 59 | ({ style, options, expected }) => { 60 | it(`returns expected output \`${JSON.stringify(expected)}\``, () => { 61 | expect(inlineStyleParser(style, options)).toEqual(expected); 62 | }); 63 | } 64 | ); 65 | 66 | describe('source', () => { 67 | it('parses declaration', () => { 68 | expect( 69 | inlineStyleParser('a:b', { source: 'file.css' }) 70 | ).toMatchSnapshot(); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /test/index.test.mjs: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'node:test'; 2 | 3 | import assert from 'assert'; 4 | 5 | import parse from '../index.js'; 6 | 7 | describe('index', () => { 8 | it('exports default function', () => { 9 | assert.strictEqual(typeof parse, 'function'); 10 | }); 11 | 12 | it('parses style', () => { 13 | assert.deepEqual(parse('a:b'), [ 14 | { 15 | type: 'declaration', 16 | property: 'a', 17 | value: 'b', 18 | position: { 19 | start: { 20 | line: 1, 21 | column: 1 22 | }, 23 | end: { 24 | line: 1, 25 | column: 4 26 | }, 27 | source: undefined 28 | } 29 | } 30 | ]); 31 | }); 32 | }); 33 | --------------------------------------------------------------------------------