├── .babelrc ├── .eslintrc.js ├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── .gitignore ├── .markdownlint.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── karma.conf.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── get-css.js ├── get-urls.js └── index.js └── tests ├── .eslintrc.js ├── fixtures ├── 404.html ├── a │ ├── b │ │ └── import.css │ ├── c │ │ └── import.css │ └── import.css ├── style-empty.css ├── style1.css ├── style2.css ├── style2.out.css ├── style3.css └── style3.out.css ├── get-css.test.js └── helpers ├── create-test-elms.js └── inject-test-component.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["node_modules/**"], 3 | "presets": [ 4 | ["@babel/preset-env", { 5 | "targets": "ie >= 9" 6 | }] 7 | ], 8 | "plugins": [ 9 | "transform-custom-element-classes" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'env': { 3 | 'browser' : true, 4 | 'commonjs': true, 5 | 'es6' : true, 6 | 'node' : true 7 | }, 8 | 'extends': 'eslint:recommended', 9 | 'ignorePatterns': [ 10 | 'dist' 11 | ], 12 | 'plugins': [], 13 | 'parserOptions': { 14 | 'sourceType' : 'module' 15 | }, 16 | 'rules': { 17 | 'array-bracket-spacing' : ['error', 'never'], 18 | 'array-callback-return' : ['error'], 19 | 'block-scoped-var' : ['error'], 20 | 'block-spacing' : ['error', 'always'], 21 | 'curly' : ['error'], 22 | 'dot-notation' : ['error'], 23 | 'eqeqeq' : ['error'], 24 | 'indent' : ['error', 4], 25 | 'no-console' : ['warn'], 26 | 'no-floating-decimal' : ['error'], 27 | 'no-implicit-coercion' : ['error'], 28 | 'no-implicit-globals' : ['error'], 29 | 'no-loop-func' : ['error'], 30 | 'no-return-assign' : ['error'], 31 | 'no-template-curly-in-string': ['error'], 32 | 'no-unneeded-ternary' : ['error'], 33 | 'no-unused-vars' : ['error', { 'args': 'none' }], 34 | 'no-useless-computed-key' : ['error'], 35 | 'no-useless-return' : ['error'], 36 | 'no-var' : ['error'], 37 | 'prefer-const' : ['error'], 38 | 'quotes' : ['error', 'single'], 39 | 'semi' : ['error', 'always'] 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: jhildenbiddle 2 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: 'Build & Test' 2 | 3 | on: [push, pull_request_target] 4 | 5 | jobs: 6 | build: 7 | name: Build (${{ matrix.os }}) 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | matrix: 11 | os: [macos-latest, ubuntu-latest, windows-latest] 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - name: Install 17 | run: npm ci --ignore-scripts 18 | 19 | - name: Lint 20 | run: npm run lint 21 | 22 | - name: Build 23 | run: npm run build 24 | 25 | test: 26 | name: Test 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v4 31 | 32 | - name: Install 33 | run: npm ci --ignore-scripts 34 | 35 | - name: Build 36 | run: npm run build 37 | 38 | - name: Setup BrowserStack environment 39 | uses: browserstack/github-actions/setup-env@master 40 | with: 41 | username: ${{ secrets.BROWSERSTACK_USERNAME }} 42 | access-key: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} 43 | 44 | - name: Start BrowserStack tunnel 45 | uses: browserstack/github-actions/setup-local@master 46 | with: 47 | local-testing: start 48 | local-identifier: random 49 | 50 | - name: Run tests on BrowserStack 51 | run: npm run test-remote 52 | 53 | - name: Stop BrowserStack tunnel 54 | uses: browserstack/github-actions/setup-local@master 55 | with: 56 | local-testing: stop 57 | 58 | - name: Report code coverage 59 | uses: codacy/codacy-coverage-reporter-action@v1 60 | with: 61 | project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} 62 | coverage-reports: coverage/lcov.info 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Folders 2 | coverage 3 | dist 4 | node_modules 5 | 6 | # Files 7 | *.log 8 | 9 | # OS 10 | ._* 11 | .cache 12 | .DS_Store 13 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "MD001": false, 4 | "MD004": { "style": "consistent" }, 5 | "MD013": false, 6 | "MD033": false, 7 | "MD036": false 8 | } 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 2.1.1 4 | 5 | *2024-02-06* 6 | 7 | - Fix GitHub workflow badge 8 | 9 | ## 2.1.0 10 | 11 | *2022-04-21* 12 | 13 | - Add support for `` elements using data URIs 14 | 15 | ## 2.0.2 16 | 17 | *2021-03-16* 18 | 19 | - Fix empty `` stylesheet triggering `onError()` callback 20 | 21 | ## 2.0.1 22 | 23 | *2021-01-30* 24 | 25 | - Update version number in CDN URLs 26 | 27 | ## 2.0.0 28 | 29 | *2021-01-30* 30 | 31 | - Remove nodes skipped during `onSuccess()` from `onComplete()` arguments 32 | - Support XHR status < 400 for `file://` URLs 33 | - Remove distributable files from version control 34 | 35 | ## 1.9.1 36 | 37 | *2020-11-20* 38 | 39 | - Fix detection of SVG ` 90 | 91 | ``` 92 | 93 | CSS: 94 | 95 | ```css 96 | /* style1.css */ 97 | p { color: red; } 98 | ``` 99 | 100 | ```css 101 | /* style2.css */ 102 | p { color: green; } 103 | ``` 104 | 105 | JavaScript (see [Options](#options) for details) 106 | 107 | ```javascript 108 | getCssData({ 109 | onComplete: function(cssText, cssArray, nodeArray) { 110 | console.log(cssText); // 1 111 | console.log(cssArray); // 2 112 | console.log(nodeArray); // 3 113 | } 114 | }); 115 | 116 | // 1 => 'p { color: red; } p { color: green; } p { color: blue; }' 117 | // 2 => ['p { color: red; }', 'p { color: green; } p { color: blue; }'] 118 | // 3 => [, ] 119 | ``` 120 | 121 | ## Options 122 | 123 | 124 | - [rootElement](#optionsrootelement) 125 | - [include](#optionsinclude) 126 | - [exclude](#optionsexclude) 127 | - [filter](#optionsfilter) 128 | - [skipDisabled](#optionsskipdisabled) 129 | - [useCSSOM](#optionsusecssom) 130 | - [onBeforeSend](#optionsonbeforesend) 131 | - [onSuccess](#optionsonsuccess) 132 | - [onError](#optionsonerror) 133 | - [onComplete](#optionsoncomplete) 134 | 135 | **Example** 136 | 137 | ```javascript 138 | // Default values shown 139 | getCssData({ 140 | rootElement : document, 141 | include : 'link[rel=stylesheet],style', 142 | exclude : '', 143 | filter : '', 144 | skipDisabled: true, 145 | useCSSOM : false, 146 | onBeforeSend: function(xhr, node, url) { 147 | // ... 148 | }, 149 | onSuccess: function(cssText, node, url) { 150 | // ... 151 | }, 152 | onError: function(xhr, node, url) { 153 | // ... 154 | }, 155 | onComplete: function(cssText, cssArray, nodeArray) { 156 | // ... 157 | } 158 | }); 159 | ``` 160 | 161 | ### options.rootElement 162 | 163 | - Type: `object` 164 | - Default: `document` 165 | 166 | Root element to traverse for `` and ``); 56 | 57 | getCss({ 58 | include: '[data-test]', 59 | onComplete(cssText, cssArray, nodeArray) { 60 | expect(cssText).to.equal(styleCss); 61 | done(); 62 | } 63 | }); 64 | }); 65 | 66 | 67 | // Tests: `); 74 | 75 | getCss({ 76 | include: '[data-test]', 77 | onComplete(cssText, cssArray, nodeArray) { 78 | expect(cssText).to.equal(styleCss); 79 | done(); 80 | } 81 | }); 82 | }); 83 | 84 | it('returns CSS from single SVG 90 | 91 | 92 | `); 93 | 94 | getCss({ 95 | include: 'svg style', 96 | onComplete(cssText, cssArray, nodeArray) { 97 | expect(cssText).to.equal(styleCss); 98 | done(); 99 | } 100 | }); 101 | }); 102 | 103 | it('returns CSS from multiple `.repeat(2)); 106 | const expected = styleCss.repeat(styleElms.length); 107 | 108 | getCss({ 109 | include: '[data-test]', 110 | onComplete(cssText, cssArray, nodeArray) { 111 | expect(cssText).to.equal(expected); 112 | done(); 113 | } 114 | }); 115 | }); 116 | 117 | it('returns CSS from multiple `.repeat(2)); 120 | const expected = fixtures['style2.out.css'].repeat(styleElms.length); 121 | 122 | getCss({ 123 | include: '[data-test]', 124 | onComplete(cssText, cssArray, nodeArray) { 125 | expect(cssText).to.equal(expected); 126 | done(); 127 | } 128 | }); 129 | }); 130 | 131 | it('returns CSS from multiple `.repeat(2)); 134 | const expected = fixtures['style3.out.css'].repeat(styleElms.length); 135 | 136 | getCss({ 137 | include: '[data-test]', 138 | onComplete(cssText, cssArray, nodeArray) { 139 | expect(cssText).to.equal(expected); 140 | done(); 141 | } 142 | }); 143 | }); 144 | 145 | it('returns CSS from multiple `.repeat(2)); 150 | 151 | getCss({ 152 | include: '[data-test]', 153 | onComplete(cssText, cssArray, nodeArray) { 154 | cssText = cssText.replace(/\n/g, ''); 155 | 156 | expect(cssText).to.equal(expected); 157 | done(); 158 | } 159 | }); 160 | }); 161 | }); 162 | 163 | 164 | // Tests: CSS 165 | // ------------------------------------------------------------------------- 166 | describe(' nodes', function() { 167 | it('returns CSS from single node', function(done) { 168 | const linkUrl = '/base/tests/fixtures/style1.css'; 169 | const expected = fixtures['style1.css']; 170 | 171 | createTestElms(``); 172 | 173 | getCss({ 174 | include: '[data-test]', 175 | onComplete(cssText, cssArray, nodeArray) { 176 | expect(cssText).to.equal(expected); 177 | done(); 178 | } 179 | }); 180 | }); 181 | 182 | it('returns CSS from single node with data URI scheme', function(done) { 183 | const encodedCSS = encodeURIComponent(fixtures['style1.css']); 184 | const URIScheme = `data:text/css;charset=UTF-8,${encodedCSS}`; 185 | const expected = fixtures['style1.css']; 186 | 187 | createTestElms(``); 188 | 189 | getCss({ 190 | include: '[data-test]', 191 | onComplete(cssText, cssArray, nodeArray) { 192 | expect(cssText).to.equal(expected); 193 | done(); 194 | } 195 | }); 196 | }); 197 | 198 | it('returns CSS from single node via CORS', function(done) { 199 | const linkProtocol = 'https:'; 200 | const linkUrl = `${linkProtocol}//cdn.jsdelivr.net/npm/get-css-data@1.0.0/tests/fixtures/style1.css`; 201 | 202 | createTestElms(``); 203 | 204 | // IE9 does not support CORS requests with different protocol 205 | if (isIElte9 & location.protocol !== linkProtocol) { 206 | getCss({ 207 | include: '[data-test]', 208 | onError(xhr, node, url) { 209 | done(); 210 | } 211 | }); 212 | } 213 | else { 214 | axios.get(linkUrl) 215 | .then(response => response.data) 216 | .then(expected => { 217 | getCss({ 218 | include: '[data-test]', 219 | onComplete(cssText, cssArray, nodeArray) { 220 | expect(cssText).to.equal(expected); 221 | done(); 222 | } 223 | }); 224 | }); 225 | } 226 | }); 227 | 228 | it('returns CSS from multiple nodes', function(done) { 229 | const linkUrl = '/base/tests/fixtures/style1.css'; 230 | const linkElms = createTestElms(``.repeat(2)); 231 | const expected = fixtures['style1.css'].repeat(linkElms.length); 232 | 233 | getCss({ 234 | include: '[data-test]', 235 | onComplete(cssText, cssArray, nodeArray) { 236 | expect(cssText).to.equal(expected); 237 | done(); 238 | } 239 | }); 240 | }); 241 | 242 | it('returns CSS from multiple nodes with data URI scheme', function(done) { 243 | const encodedCSS = encodeURIComponent(fixtures['style1.css']); 244 | const URIScheme = `data:text/css;charset=UTF-8,${encodedCSS}`; 245 | const linkElms = createTestElms(``.repeat(2)); 246 | const expected = fixtures['style1.css'].repeat(linkElms.length); 247 | 248 | getCss({ 249 | include: '[data-test]', 250 | onComplete(cssText, cssArray, nodeArray) { 251 | expect(cssText).to.equal(expected); 252 | done(); 253 | } 254 | }); 255 | }); 256 | 257 | it('returns empty string from single node w/ empty stylesheet', function(done) { 258 | const linkUrl = '/base/tests/fixtures/style-empty.css'; 259 | const expected = fixtures['style-empty.css']; 260 | 261 | createTestElms(``); 262 | 263 | getCss({ 264 | include: '[data-test]', 265 | onError(xhr, node, url) { 266 | console.log('Error', node, url); 267 | }, 268 | onComplete(cssText, cssArray, nodeArray) { 269 | expect(cssText).to.equal(expected); 270 | done(); 271 | } 272 | }); 273 | }); 274 | 275 | it('returns CSS from multiple nodes with flat @import', function(done) { 276 | const linkUrl = '/base/tests/fixtures/style2.css'; 277 | const linkElms = createTestElms(``.repeat(2)); 278 | const expected = fixtures['style2.out.css'].repeat(linkElms.length); 279 | 280 | getCss({ 281 | include: '[data-test]', 282 | onComplete(cssText, cssArray, nodeArray) { 283 | expect(cssText).to.equal(expected); 284 | done(); 285 | } 286 | }); 287 | }); 288 | 289 | it('returns CSS from multiple nodes with nested @import', function(done) { 290 | const linkUrl = '/base/tests/fixtures/style3.css'; 291 | const linkElms = createTestElms(``.repeat(2)); 292 | const expected = fixtures['style3.out.css'].repeat(linkElms.length); 293 | 294 | getCss({ 295 | include: '[data-test]', 296 | onComplete(cssText, cssArray, nodeArray) { 297 | expect(cssText).to.equal(expected); 298 | done(); 299 | } 300 | }); 301 | }); 302 | }); 303 | 304 | 305 | // Tests: & `); 353 | 354 | getCss({ 355 | include: '[data-test]', 356 | exclude: 'link', 357 | onComplete(cssText, cssArray, nodeArray) { 358 | expect(cssText).to.equal(styleCss); 359 | done(); 360 | } 361 | }); 362 | }); 363 | 364 | it('options.filter', function(done) { 365 | const linkUrl = '/base/tests/fixtures/style1.css'; 366 | const styleCss = '.keepme { color: red; }'; 367 | 368 | createTestElms(``); 369 | createTestElms(``); 370 | 371 | getCss({ 372 | include: '[data-test]', 373 | filter : /keepme/, 374 | onComplete(cssText, cssArray, nodeArray) { 375 | expect(cssText).to.equal(styleCss); 376 | done(); 377 | } 378 | }); 379 | }); 380 | 381 | it('options.skipDisabled', function(done) { 382 | const linkUrl = '/base/tests/fixtures/style1.css'; 383 | const styleCss = fixtures['style1.css']; 384 | 385 | const testElms = createTestElms([ 386 | ``, 387 | `` 388 | ]); 389 | 390 | for (const sheet of document.styleSheets) { 391 | sheet.disabled = true; 392 | } 393 | 394 | function step1() { 395 | getCss({ 396 | include : '[data-test]', 397 | skipDisabled: true, 398 | onComplete(cssText, cssArray, nodeArray) { 399 | expect(nodeArray.length, '1:nodeArray').to.equal(0); 400 | expect(cssArray.length, '1:cssArray').to.equal(0); 401 | expect(cssText, '1:cssText').to.equal(''); 402 | 403 | // Enable `.repeat(2)); 536 | 537 | let onSuccessCount = 0; 538 | 539 | getCss({ 540 | include: '[data-test]', 541 | onSuccess(cssText, node, url) { 542 | onSuccessCount++; 543 | 544 | return '!'; 545 | }, 546 | onComplete(cssText, cssArray, nodeArray) { 547 | expect(cssText, 'return value').to.equal('!'.repeat(styleElms.length)); 548 | expect(onSuccessCount, 'onSuccess count').to.equal(styleElms.length); 549 | done(); 550 | } 551 | }); 552 | }); 553 | 554 | it('triggers onSuccess callback for each node', function(done) { 555 | const linkUrl = '/base/tests/fixtures/style1.css'; 556 | const linkElms = createTestElms(``.repeat(2)); 557 | 558 | let onSuccessCount = 0; 559 | 560 | getCss({ 561 | include: '[data-test]', 562 | onSuccess(cssText, node, url) { 563 | onSuccessCount++; 564 | 565 | return '!'; 566 | }, 567 | onComplete(cssText, cssArray, nodeArray) { 568 | expect(cssText, 'return value').to.equal('!'.repeat(linkElms.length)); 569 | expect(onSuccessCount, 'onSuccess count').to.equal(linkElms.length); 570 | done(); 571 | } 572 | }); 573 | }); 574 | 575 | it('filters CSS and nodes based on onSuccess() return value', function(done) { 576 | const testElms = createTestElms([ 577 | '', 578 | '', 579 | '', 580 | '' 581 | ]); 582 | 583 | getCss({ 584 | include: '[data-test]', 585 | onSuccess(cssText, node, url) { 586 | const returnVals = [false, null, 0, '']; 587 | const nodeIndex = testElms.indexOf(node); 588 | 589 | if (nodeIndex > -1) { 590 | return returnVals[nodeIndex]; 591 | } 592 | }, 593 | onComplete(cssText, cssArray, nodeArray) { 594 | expect(cssText).to.equal(''); 595 | expect(cssArray.length).to.equal(0); 596 | expect(nodeArray.length).to.equal(0); 597 | done(); 598 | } 599 | }); 600 | }); 601 | 602 | it('modifies CSS text based on onSuccess() value', function(done) { 603 | const linkUrl = '/base/tests/fixtures/style1.css'; 604 | const styleCss = fixtures['style1.css']; 605 | const modifiedCss = '.modified { color: red; }'; 606 | 607 | createTestElms(``); 608 | createTestElms(``); 609 | 610 | getCss({ 611 | include: '[data-test]', 612 | onSuccess(cssText, node, url) { 613 | return modifiedCss; 614 | }, 615 | onComplete(cssText, cssArray, nodeArray) { 616 | expect(cssText).to.equal(modifiedCss.repeat(2)); 617 | done(); 618 | } 619 | }); 620 | }); 621 | 622 | it('triggers onError callback for each @import 404 error', function(done) { 623 | const styleCss = '@import "fail.css";'; 624 | const styleElms = createTestElms(``.repeat(3)); 625 | 626 | let onErrorCount = 0; 627 | 628 | getCss({ 629 | include: '[data-test]', 630 | onError(xhr, node, url) { 631 | onErrorCount++; 632 | }, 633 | onComplete(cssText, cssArray, nodeArray) { 634 | expect(onErrorCount).to.equal(styleElms.length); 635 | done(); 636 | } 637 | }); 638 | }); 639 | 640 | it('triggers onError callback for each 404 error', function(done) { 641 | const linkUrl = 'fail.css'; 642 | const linkElms = createTestElms(``.repeat(2)); 643 | 644 | let onErrorCount = 0; 645 | 646 | getCss({ 647 | include: '[data-test]', 648 | onError(xhr, node, url) { 649 | onErrorCount++; 650 | }, 651 | onComplete(cssText, cssArray, nodeArray) { 652 | expect(onErrorCount).to.equal(linkElms.length); 653 | done(); 654 | } 655 | }); 656 | }); 657 | 658 | it('triggers onError callback for each invalid XMLHttpRequest.responseText', function(done) { 659 | const linkUrl = '/base/tests/fixtures/404.html'; 660 | const linkElms = createTestElms(``.repeat(2)); 661 | 662 | let onErrorCount = 0; 663 | 664 | getCss({ 665 | include: '[data-test]', 666 | onError(xhr, node, url) { 667 | onErrorCount++; 668 | }, 669 | onComplete(cssText, cssArray, nodeArray) { 670 | expect(onErrorCount).to.equal(linkElms.length); 671 | done(); 672 | } 673 | }); 674 | }); 675 | 676 | it('triggers onComplete callback with no matching 19 | 20 |

${this.getAttribute('data-text')}

21 | `; 22 | } 23 | } 24 | 25 | window.customElements.define('test-component', TestComponent); 26 | 27 | // createElms({ tag: 'style', text: ':root { --test-component-background: green; }', appendTo: 'head' }); 28 | // createElms([ 29 | // { tag: 'test-component', attr: { 'data-text': 'Custom element' }}, 30 | // { tag: 'p', text: 'Standard element' } 31 | // ], { appendTo: 'body' }); 32 | }; 33 | --------------------------------------------------------------------------------