├── .npmignore ├── demo ├── file.css ├── one.css ├── two.css ├── real-file.css └── test.html ├── .gitignore ├── tsconfig.json ├── vite.config.js ├── src ├── regex.ts ├── polyfill.ts └── index.ts ├── package.json ├── rollup.config.js ├── README.md ├── test.js └── LICENSE /.npmignore: -------------------------------------------------------------------------------- 1 | demo/ 2 | test.js 3 | -------------------------------------------------------------------------------- /demo/file.css: -------------------------------------------------------------------------------- 1 | @import './real-file.css'; 2 | -------------------------------------------------------------------------------- /demo/one.css: -------------------------------------------------------------------------------- 1 | .one { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /demo/two.css: -------------------------------------------------------------------------------- 1 | .two { 2 | color: blue; 3 | } 4 | -------------------------------------------------------------------------------- /demo/real-file.css: -------------------------------------------------------------------------------- 1 | p { 2 | font-weight: bold; 3 | color: aqua; 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | lib/ 4 | dist/ 5 | scoped.min.js 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "NodeNext", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "moduleResolution": "NodeNext" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import dts from "vite-plugin-dts"; 2 | 3 | /** @type {import("vite").UserConfig} */ 4 | export default { 5 | build: { 6 | lib: { 7 | entry: "src/polyfill.ts", 8 | name: "style_scoped", 9 | formats: ["umd"], 10 | fileName: () => "scoped.min.js", 11 | }, 12 | }, 13 | plugins: [dts()], 14 | }; 15 | -------------------------------------------------------------------------------- /src/regex.ts: -------------------------------------------------------------------------------- 1 | // This monstrosity matches any valid `[foo="bar"]` block, with either quote style. Parenthesis 2 | // have no special meaning within an attribute selector, and the complex regexp below mostly 3 | // exists to allow \" or \' in string parts (e.g. `[foo="b\"ar"]`). 4 | export const attrRe = /^\[.*?(?:(["'])(?:.|\\\1)*\1.*)*\]/; 5 | export const walkSelectorRe = /([([,]|:scope\b)/; // "interesting" setups 6 | export const scopeRe = /^:scope\b/; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "style-scoped", 3 | "version": "0.2.2", 4 | "description": "style[scoped] polyfill", 5 | "main": "dist/lib.js", 6 | "module": "dist/lib.esm.js", 7 | "files": [ 8 | "dist", 9 | "scoped.min.js" 10 | ], 11 | "author": "Sam Thorogood ", 12 | "license": "Apache-2", 13 | "scripts": { 14 | "test": "npm run build && headless-test --cors test.js", 15 | "build": "vite build", 16 | "prepare": "npm run build && cp dist/scoped.min.js scoped.min.js" 17 | }, 18 | "devDependencies": { 19 | "headless-test": "^1.0.3", 20 | "typescript": "^4.9.4", 21 | "vite": "^4.0.4", 22 | "vite-plugin-dts": "^1.7.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/polyfill.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Polyfill for ` 66 |

I should be red (green on >=768px)

67 |

68 | What about me? 69 |

70 |

71 | And aqua 72 |

73 | add 74 | 75 | 87 | 88 | 89 | 90 | 91 | 92 |
93 |

The following should not be colored (not scoped)

94 |
One red
95 |
Two blue
96 |

97 | Paragraph should look normal 98 |

99 |
100 | 101 |

102 | 
103 | 
124 | 
125 | 
126 | 
127 | 
128 | 


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
  1 | # `
 69 |   Bonjour monde!
 70 | 
 71 | 
¡Hola Mundo!
72 | ``` 73 | 74 | ![](https://i.imgur.com/B2uJw5P.png) 75 | 76 | You can also use the `:scope` pseudo-class to select the parent element of the 77 | ` 87 | Go to example.org 88 | 89 | ``` 90 | 91 | ⚠️ Rules which use `:scope` inside another selector (e.g. `:is(div:scope)`) are 92 | not currently supported and will be cleared. If this is _actually something you 93 | need_, I will eat my hat. 🎩 94 | 95 | ## How it works 96 | 97 | TODO: Explain how it works 98 | 99 | ## Notes 100 | 101 | - The polyfill doesn't operate on all CSS rules. `@keyframes`, `@font`, etc. are 102 | ignored. 103 | - If you depend on cross-domain CSS via `@import`, this is loaded dynamically 104 | with an XHR. It may take a little while to arrive. (📚 [#2] & [#3]) 105 | 106 | ## Development 107 | 108 | ![Codespaces](https://img.shields.io/static/v1?style=for-the-badge&message=Codespaces&color=181717&logo=GitHub&logoColor=FFFFFF&label=) 109 | ![Devcontainers](https://img.shields.io/static/v1?style=for-the-badge&message=Devcontainers&color=2496ED&logo=Docker&logoColor=FFFFFF&label=) 110 | 111 | This project uses a [devcontainer] to provide a consistent development 112 | environment for contributors. You can use it with [GitHub Codespaces] online, or 113 | [VS Code] locally. 114 | 115 | There are a few scripts you can run: 116 | 117 | - `npm pack`: Build the project using Vite 118 | - `npm test`: Run the tests using Vitest 119 | 120 | 🚀 These tasks are also available as [VS Code Tasks]. 121 | 122 | 123 | [`@scope` at-rule]: https://drafts.csswg.org/css-cascade-6/#scope-atrule 124 | [unpkg.com]: https://unpkg.com/ 125 | [#2]: https://github.com/samthor/scoped/issues/2 126 | [#3]: https://github.com/samthor/scoped/issues/3 127 | [devcontainer]: https://code.visualstudio.com/docs/remote/containers 128 | [github codespaces]: https://github.com/features/codespaces 129 | [vs code]: https://code.visualstudio.com/ 130 | [vs code tasks]: https://code.visualstudio.com/docs/editor/tasks 131 | [try it on codesandbox]: https://codesandbox.io/s/5ocrf8 132 | [view it on archive.org]: https://web.archive.org/web/20160505103205/https://html.spec.whatwg.org/multipage/semantics.html#the-style-element 133 | 134 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | import './dist/polyfill.js'; 18 | 19 | suite('scoped', function() { 20 | 21 | function element(name, textContent) { 22 | var el = document.createElement(name); 23 | el.textContent = textContent || ''; 24 | return el; 25 | } 26 | 27 | function scoped(css) { 28 | var s = element('style', css); 29 | s.scoped = true; 30 | return s; 31 | } 32 | 33 | /** 34 | * Since our logic runs inside MutationObserver, we need a microtask to observe most things. 35 | */ 36 | function task(fn, done) { 37 | Promise.resolve(true).then(() => { 38 | fn(); 39 | done && done(); 40 | }).catch((err) => done(err)); 41 | } 42 | 43 | var holder; 44 | 45 | setup(function() { 46 | holder = document.createElement('div'); 47 | document.body.appendChild(holder); 48 | }); 49 | teardown(function() { 50 | holder.textContent = ''; 51 | document.body.removeChild(holder); 52 | holder = null; 53 | }); 54 | 55 | suite('rewrite', function() { 56 | const rewrite = (description, source, expected) => { 57 | test(description, function(done) { 58 | var s = scoped(source + ' {}'); 59 | holder.appendChild(s); 60 | 61 | task(() => { 62 | const rules = Array.from(s.sheet.rules); 63 | const actual = rules.map((rule) => rule.selectorText).join('\n'); 64 | assert.equal(actual, expected); 65 | }, done); 66 | }); 67 | }; 68 | 69 | // TODO: These tests rely on order, as the polyfill uses an incrementing counting for prefixes. 70 | 71 | rewrite('rewrite test', 'h1', '[__scoped_1] h1'); 72 | rewrite('many selectors', 'h1, h2', '[__scoped_2] h1, [__scoped_2] h2'); 73 | rewrite(':scope rewrite', '.foo, h1:scope, h2:scope:not(.bar)', '[__scoped_3] .foo, h1[__scoped_3], h2[__scoped_3]:not(.bar)'); 74 | rewrite('unsupported inner :scope', '.foo:-webkit-any(h1:scope, h4, h3:scope):not([foo])', ':not(*)'); 75 | rewrite('tricky without :scope', '.foo:-webkit-any(h1, h4, h3):not([foo])', '[__scoped_5] .foo:-webkit-any(h1,h4,h3):not([foo])'); 76 | rewrite('duplicate :scope rewrite', 'h2:scope:scope', 'h2[__scoped_6][__scoped_6]'); 77 | }); 78 | 79 | suite('apply', function() { 80 | 81 | test(':scope', function(done) { 82 | var s = scoped(':scope { background: red; }'); 83 | holder.appendChild(s); 84 | 85 | task(() => { 86 | var computed = window.getComputedStyle(holder); 87 | assert.equal(computed.backgroundColor, 'rgb(255, 0, 0)', 'scope is changed'); 88 | 89 | computed = window.getComputedStyle(document.documentElement); 90 | assert.notEqual(computed.backgroundColor, 'rgb(255, 0, 0)', ':root remains unchanged'); 91 | }, done); 92 | }); 93 | 94 | test('invalid :scope', function(done) { 95 | var s = scoped('div :scope { background: red; }'); 96 | holder.appendChild(s); 97 | 98 | task(() => { 99 | var computed = window.getComputedStyle(document.documentElement); 100 | assert.notEqual(computed.backgroundColor, 'rgb(255, 0, 0)', ':root remains unchanged'); 101 | }, done); 102 | }); 103 | 104 | test('microtask run', function(done) { 105 | var s = scoped('h1 { color: red; }'); 106 | 107 | var h1 = element('h1', 'first'); 108 | holder.appendChild(h1); 109 | 110 | var computed = window.getComputedStyle(h1); 111 | assert.notEqual(computed.color, 'rgb(255, 0, 0)', 'style should be normal before insertion'); 112 | 113 | task(() => { 114 | holder.appendChild(s); 115 | assert.equal(computed.color, 'rgb(255, 0, 0)', 'changed after insertion'); 116 | }, done); 117 | }); 118 | 119 | test('doesn\'t effect others', function(done) { 120 | var branch = element('div'); 121 | holder.appendChild(branch); 122 | 123 | var unchanged = element('h1', 'unchanged'); 124 | holder.appendChild(unchanged); 125 | 126 | var targeted = element('h1', 'targeted'); 127 | branch.appendChild(targeted); 128 | 129 | // only elements in the scoped branch should change 130 | branch.appendChild(scoped('h2, h1 { color: red; }')); 131 | 132 | task(() => { 133 | var computed = window.getComputedStyle(unchanged); 134 | assert.notEqual(computed.color, 'rgb(255, 0, 0)', 'untargeted element should not be modified'); 135 | 136 | var computed = window.getComputedStyle(targeted); 137 | assert.equal(computed.color, 'rgb(255, 0, 0)'); 138 | }, done); 139 | }); 140 | 141 | test('import', async function() { 142 | 143 | const u = new URL(window.location); 144 | if (u.hostname === '127.0.0.1') { 145 | u.hostname = 'localhost'; 146 | } else { 147 | u.hostname = '127.0.0.1'; 148 | } 149 | 150 | holder.append(scoped(` 151 | @import url("${u.origin}/demo/one.css"); 152 | @import url("${u.origin}/demo/two.css"); 153 | `)); 154 | // TODO(samthor): This is gross, but we need to allow the fetch to happen. 155 | await new Promise((r) => window.setTimeout(r, 100)); 156 | 157 | const elOne = Object.assign(document.createElement('div'), {className: 'one'}); 158 | const elTwo = Object.assign(document.createElement('div'), {className: 'two'}); 159 | 160 | holder.append(elOne, elTwo); 161 | 162 | const computedOne = window.getComputedStyle(elOne); 163 | assert.equal(computedOne.color, 'rgb(255, 0, 0)'); 164 | 165 | const computedTwo = window.getComputedStyle(elTwo); 166 | assert.equal(computedTwo.color, 'rgb(0, 0, 255)'); 167 | 168 | try { 169 | document.body.append(elOne); 170 | 171 | const computedOne = window.getComputedStyle(elOne); 172 | assert.equal(computedOne.color, 'rgb(0, 0, 0)'); 173 | } finally { 174 | elOne.remove(); 175 | } 176 | }); 177 | 178 | }); 179 | 180 | }); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { attrRe, walkSelectorRe, scopeRe } from './regex'; 2 | 3 | const s = document.createElement('style'); 4 | // are we in old IE/Firefox mode, where .selectorText can't be changed inline? 5 | s.textContent = '.x{color:red;}'; 6 | document.head.appendChild(s); 7 | s.sheet.cssRules[0].selectorText = '.change'; 8 | const writeMode = s.sheet.cssRules[0].selectorText === '.change'; 9 | document.head.removeChild(s); 10 | 11 | const scopedCSSOptions = { 12 | 'applyToClass': false, 13 | 'prefix': '__scoped_', 14 | }; 15 | 16 | Object.defineProperty(HTMLStyleElement.prototype, 'scoped', { 17 | enumerable: true, 18 | get() { 19 | return this.hasAttribute('scoped'); 20 | }, 21 | set(v) { 22 | if (v) { 23 | this.setAttribute('scoped', this.getAttribute('scoped') || ''); 24 | } else { 25 | this.removeAttribute('scoped'); 26 | } 27 | }, 28 | }); 29 | 30 | /** 31 | * @type {!Map} 32 | */ 33 | const styleNodes = new Map(); 34 | 35 | 36 | function hashCode(s) { 37 | let hash = 5381; 38 | let j = s.length; 39 | while (j) { 40 | hash = (hash * 33) ^ s.charCodeAt(--j); 41 | } 42 | return hash; 43 | } 44 | 45 | /** 46 | * Consumes a single selector from candidate selector text, which may contain many. 47 | * 48 | * @param {string} raw selector text 49 | * @return {?{selector: string, rest: string}} 50 | */ 51 | function consumeSelector(raw, prefix) { 52 | let i = raw.search(walkSelectorRe); 53 | if (i === -1) { 54 | // found literally nothing interesting, success 55 | return { 56 | selector: `${prefix} ${raw}`, 57 | rest: '', 58 | }; 59 | } else if (raw[i] === ',') { 60 | // found comma without anything interesting, yield rest 61 | return { 62 | selector: `${prefix} ${raw.substr(0, i)}`, 63 | rest: raw.substr(i + 1), 64 | } 65 | } 66 | 67 | let leftmost = true; // whether we're past a descendant or similar selector 68 | let scope = false; // whether :scope has been found + replaced 69 | i = raw.search(/\S/); // place i after initial whitespace only 70 | 71 | let depth = 0; 72 | outer: 73 | for (; i < raw.length; ++i) { 74 | const char = raw[i]; 75 | switch (char) { 76 | case '[': 77 | const match = attrRe.exec(raw.substr(i)); 78 | i += (match ? match[0].length : 1) - 1; // we add 1 every loop 79 | continue; 80 | 81 | case '(': 82 | ++depth; 83 | continue; 84 | 85 | case ':': 86 | if (!leftmost) { 87 | continue; // doesn't matter if :scope is here, it'll always be ignored 88 | } else if (!scopeRe.test(raw.substr(i))) { 89 | continue; // not ':scope', ignore 90 | } else if (depth) { 91 | return null; 92 | } 93 | 94 | // Replace ':scope' with our prefix. This can happen many times; ':scope:scope' is valid. 95 | // It will never apply to a descendant selector (e.g., ".foo :scope") as this is ignored 96 | // by browsers anyway (invalid). 97 | raw = raw.substring(0, i) + prefix + raw.substr(i + 6); 98 | i += prefix.length; 99 | scope = true; 100 | --i; // we'd skip over next character otherwise 101 | continue; // run loop again 102 | 103 | case ')': 104 | if (depth) { 105 | --depth; 106 | } 107 | continue; 108 | } 109 | if (depth) { 110 | continue; 111 | } 112 | 113 | switch (char) { 114 | case ',': 115 | break outer; 116 | 117 | case ' ': 118 | case '>': 119 | case '~': 120 | case '+': 121 | if (!leftmost) { 122 | continue; 123 | } 124 | leftmost = false; 125 | } 126 | } 127 | 128 | const selector = (scope ? '' : `${prefix} `) + raw.substr(0, i); 129 | return {selector, rest: raw.substr(i + 1)}; 130 | } 131 | 132 | function updateSelectorText(selectorText, prefix) { 133 | const found = []; 134 | 135 | while (selectorText) { 136 | const consumed = consumeSelector(selectorText, prefix); 137 | if (consumed === null) { 138 | return ':not(*)'; 139 | } 140 | found.push(consumed.selector); 141 | selectorText = consumed.rest; 142 | } 143 | 144 | return found.join(', '); 145 | } 146 | 147 | 148 | /** 149 | * Upgrades a specific CSSRule. 150 | * 151 | * @param {!CSSRule} rule 152 | * @param {string} prefix to apply 153 | * @param {!CSSMediaRule|!CSSStyleSheet} group 154 | * @param {number} index in group 155 | */ 156 | function upgradeRule(rule, prefix, group, index) { 157 | if (rule instanceof CSSMediaRule) { 158 | // upgrade children 159 | const l = rule.cssRules.length; 160 | for (let j = 0; j < l; ++j) { 161 | upgradeRule(rule.cssRules[j], prefix, rule, j); 162 | } 163 | return; 164 | } 165 | 166 | if (!(rule instanceof CSSStyleRule)) { 167 | return; // unknown rule type, ignore 168 | } 169 | 170 | const update = updateSelectorText(rule.selectorText, prefix); 171 | 172 | if (writeMode) { 173 | // anything but old IE/Firefox 174 | rule.selectorText = update; 175 | } else { 176 | // old browsers which don't allow modification of selectorText 177 | const cssText = rule.style.cssText; // save before we delete 178 | group.deleteRule(index); 179 | group.insertRule(`${update} {${cssText}}`, index); 180 | } 181 | } 182 | 183 | 184 | /** 185 | * @param {!CSSRule} rule 186 | * @return {Node} owner of rule 187 | */ 188 | function ownerNode(rule) { 189 | let sheet = rule.parentStyleSheet; 190 | while (sheet) { 191 | if (sheet.ownerNode) { 192 | return sheet.ownerNode; 193 | } 194 | sheet = sheet.parentStyleSheet; 195 | } 196 | return null; 197 | } 198 | 199 | 200 | /** 201 | * Replaces a live rule, returning the new `CSSRule` that it was replaced with. 202 | * 203 | * @param {!CSSRule} rule 204 | * @param {string} update to replace with 205 | * @return {!CSSRule} 206 | */ 207 | function replaceRule(rule, update) { 208 | const parent = rule.parentStyleSheet; 209 | let i; 210 | for (i = 0; i < parent.rules.length; ++i) { 211 | if (parent.rules[i] === rule) { 212 | break; 213 | } 214 | } 215 | parent.removeRule(i); 216 | parent.insertRule(update, i); 217 | 218 | return parent.rules[i]; 219 | } 220 | 221 | 222 | /** 223 | * @param {!CSSStyleSheet} sheet 224 | * @return {?{code: number}} the DOMException found while accessing this CSS 225 | */ 226 | function sheetRulesError(sheet) { 227 | // FIXME: This monstrosity just convinces Closure that `sheet.cssRules` has side-effects. 228 | let rules = null; 229 | try { 230 | rules = sheet.cssRules; 231 | } catch (e) { 232 | if (e instanceof DOMException) { 233 | return e; 234 | } 235 | throw e; 236 | } 237 | if (rules) { 238 | return null; 239 | } 240 | 241 | // Safari no longer throws an error here, just pretend we can't read the data. 242 | return {code: DOMException.SECURITY_ERR}; 243 | } 244 | 245 | // TODO: upgradeSheet could return a Promise or then-like 246 | 247 | /** 248 | * @param {!CSSStyleSheet} sheet already loaded CSSStyleSheet 249 | * @param {string} prefix to apply 250 | * @return {boolean} if applied immediately 251 | */ 252 | const upgradeSheet = (function() { 253 | 254 | /** @type {!WeakMap} */ 255 | const upgradedSheets = new WeakMap(); 256 | 257 | /** @type {!Map} */ 258 | const pendingImportRule = new Map(); 259 | 260 | /** @type {!Map} */ 261 | const pendingInvalidSheet = new Map(); 262 | 263 | /** 264 | * Callback inside rAF to monitor for @import-style loading or for parsing CSS script tags. 265 | * This is ugly, but only happens on styles that are moved or inserted dynamically (static 266 | * styles all fire at once). 267 | */ 268 | const requestCheck = (function() { 269 | let rAF = 0; 270 | 271 | function check() { 272 | let again = false; 273 | rAF = 0; 274 | 275 | pendingImportRule.forEach((prefix, importRule) => { 276 | if (importRule.styleSheet) { 277 | internalUpgrade(importRule.styleSheet, prefix); 278 | } else if (ownerNode(importRule)) { 279 | again = true; 280 | return; // still valid, do nothing 281 | } 282 | pendingImportRule.delete(importRule); 283 | }); 284 | 285 | pendingInvalidSheet.forEach((prefix, sheet) => { 286 | if (sheetRulesError(sheet)) { 287 | again = true; 288 | return; 289 | } 290 | internalUpgrade(sheet, prefix); 291 | pendingInvalidSheet.delete(sheet); 292 | }); 293 | 294 | // check again next frame 295 | if (again) { 296 | rAF = window.requestAnimationFrame(check); 297 | } 298 | } 299 | 300 | return function() { 301 | rAF = rAF || window.requestAnimationFrame(check); 302 | }; 303 | }()); 304 | 305 | /** 306 | * @param {!CSSStyleSheet} sheet 307 | * @param {string} prefix 308 | */ 309 | function internalUpgrade(sheet, prefix) { 310 | if (upgradedSheets.get(sheet) === prefix) { 311 | return; // already done 312 | } 313 | 314 | const e = sheetRulesError(sheet); 315 | if (e) { 316 | switch (e.code) { 317 | case DOMException.SECURITY_ERR: 318 | // Occurs if we try to examine a cross-domain CSS file. Fetch it ourselves and update 319 | // the CSS once it is available on a 'local' URL. 320 | const x = new XMLHttpRequest(); 321 | x.responseType = 'blob'; 322 | x.open('GET', sheet.href); 323 | 324 | // This must also be replaced with a temporary @import, as @import must all appear 325 | // first. Use a base64 URL that doesn't actually contain anything. 326 | const rule = replaceRule( 327 | /** @type {!CSSRule} */ (sheet.ownerRule), 328 | `@import url('data:text/css;base64,')` 329 | ); 330 | 331 | x.onload = () => { 332 | const url = URL.createObjectURL(/** @type {!Blob} */ (x.response)); 333 | const update = /** @type {!CSSImportRule} */ (replaceRule(rule, `@import '${url}'`)); 334 | pendingImportRule.set(update, prefix); 335 | requestCheck(); 336 | 337 | // We can revoke this URL immediately as it's seemingly read synchronously. 338 | URL.revokeObjectURL(url); 339 | }; 340 | // nb. no onerror handling 341 | 342 | x.send(); 343 | return; 344 | 345 | case DOMException.INVALID_ACCESS_ERR: 346 | // This occurs in Firefox if the CSS is yet to be parsed (for dynamic cases), see: 347 | // https://bugzilla.mozilla.org/show_bug.cgi?id=761236 348 | pendingInvalidSheet.set(sheet, prefix); 349 | requestCheck(); 350 | return; 351 | 352 | default: 353 | throw e; 354 | } 355 | } 356 | 357 | // Hooray, the sheet is ready to go! 358 | upgradedSheets.set(sheet, prefix); 359 | 360 | const l = sheet.cssRules.length; 361 | for (let i = 0; i < l; ++i) { 362 | const rule = sheet.cssRules[i]; 363 | 364 | if (!(rule instanceof CSSImportRule)) { 365 | upgradeRule(rule, prefix, sheet, i); 366 | continue; 367 | } 368 | 369 | if (rule.styleSheet) { 370 | // TODO: recursion is bad 371 | internalUpgrade(rule.styleSheet, prefix); 372 | continue; 373 | } 374 | 375 | // otherwise, add to pending queue 376 | pendingImportRule.set(rule, prefix); 377 | requestCheck(); 378 | } 379 | 380 | return true; 381 | } 382 | 383 | return internalUpgrade; 384 | }()); 385 | 386 | 387 | /** 388 | * @param {!HTMLStyleElement} node to reset 389 | */ 390 | function resetCSS(node) { 391 | const css = node.textContent; 392 | node.textContent = ''; 393 | node.textContent = css; 394 | } 395 | 396 | 397 | function applyToAttr(node, attrName, apply) { 398 | // default version is to apply to attributes 399 | if (apply) { 400 | node.setAttribute(attrName, ''); 401 | } else { 402 | node.removeAttribute(attrName); 403 | } 404 | } 405 | 406 | function applyToClass(node, attrName, apply) { 407 | if (apply) { 408 | node.classList.add(attrName); 409 | } else { 410 | node.classList.remove(attrName); 411 | } 412 | } 413 | 414 | 415 | let applyMode = applyToAttr; 416 | let uniqueId = 0; 417 | 418 | 419 | function upgrade(node) { 420 | const effectiveParent = node['scoped'] && document.body.contains(node) ? node.parentNode : null; 421 | 422 | const state = styleNodes.get(node); 423 | if (state) { 424 | if (!effectiveParent) { 425 | // disappearing, clear state and ask browser to reset CSS 426 | styleNodes.delete(node); 427 | resetCSS(node); 428 | } else if (node.sheet) { 429 | // otherwise, upgrade the sheet (succeeds if already done) 430 | // nb. node.sheet is null if being removed 431 | upgradeSheet(node.sheet, state.prefix); 432 | } 433 | 434 | if (state.parent !== effectiveParent) { 435 | state.parent && applyMode(state.parent, state.attrName, false); 436 | effectiveParent && applyMode(effectiveParent, state.attrName, true); 437 | state.parent = effectiveParent; 438 | } 439 | 440 | return false; // already upgraded 441 | } 442 | 443 | if (!effectiveParent) { 444 | return; // not scoped CSS, never seen before, ignore 445 | } 446 | 447 | // TODO: use hash for deduping 448 | // const hash = hashCode(node.textContent); 449 | 450 | // newly found style node, setup attr 451 | const attrName = `${scopedCSSOptions['prefix']}${++uniqueId}` 452 | const prefix = applyMode === applyToAttr ? `[${attrName}]` : `.${attrName}`; 453 | styleNodes.set(node, {attrName, prefix, parent: node.parentNode}); 454 | 455 | upgradeSheet(node.sheet, prefix); 456 | applyMode(effectiveParent, attrName, true); 457 | } 458 | 459 | // this mess basically calls resolve() with any