├── LICENSE ├── README.md ├── demo ├── README.md ├── assets │ ├── content.html │ ├── no-streams-solution.js │ ├── script.js │ └── styles.css ├── index.html └── navigate.html ├── package-lock.json ├── package.json ├── polyfill ├── index.js ├── streaming-include.js ├── test │ ├── assets │ │ ├── blank.txt │ │ ├── img.png │ │ ├── script.js │ │ ├── small-content-1.html │ │ ├── small-content-2.html │ │ └── style.css │ ├── index.html │ └── index.js └── tsconfig.json └── research.md /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Streaming HTML parser and include 2 | 3 | This repo is exploring the idea of a [built-in module](https://github.com/tc39/ecma262/issues/395) that makes it easier to stream HTML content into a page. 4 | 5 | ## The problem 6 | 7 | Single page apps tend to follow this pattern on navigation: 8 | 9 | 1. Fetch all the content. 10 | 1. Display all the content. 11 | 12 | The lack of streaming in this solution can result in an experience [much slower than a server-rendered navigation](https://www.youtube.com/watch?v=4zG0AZRZD6Q). 13 | 14 | This repo aims to explore solutions that allow SPAs to stream content into the document. 15 | 16 | ## Current solutions & feature requests 17 | 18 | Most current solutions buffer content, but there are some hacky ways around it. These are covered in the [research](research.md). 19 | 20 | ## Low-level solution 21 | 22 | The aim is to create an API that generates elements from an HTML stream. Because the input can result in multiple elements being output, a transform stream feels like a good choice: 23 | 24 | ```js 25 | const response = await fetch(url); 26 | const domStream = response.body 27 | .pipeThrough(new TextDecoderStream()) 28 | .pipeThrough(new HTMLParserStream()); 29 | ``` 30 | 31 | The stream yields `ParserChunk`s: 32 | 33 | ```js 34 | for await (const { node, parent, nextSibling } of domStream) { 35 | (parent || document.body).insertBefore(node, nextSibling); 36 | } 37 | ``` 38 | 39 | * `node` - The newly created node. 40 | * `parent` - The node it should be inserted into. Null for top-level elements. 41 | * `nextSibling` - The node it should be inserted before. Null for elements that should be inserted as the last item of their parent. 42 | 43 | **Note:** The stream yields every node including descendants, not just top-level nodes. This means: 44 | 45 | * Once `HTMLParserStream` yields a node, it isn't going to add anything to it automatically. 46 | * Developers can modify nodes before they're adopted. This means they can change image urls before they're requested, or filter script nodes before they're executed. 47 | 48 | ## Mid-level solution 49 | 50 | ```js 51 | const writable = new DOMWritable(targetElement); 52 | ``` 53 | 54 | * `targetElement` - The element to insert nodes into. 55 | 56 | This writable takes the output of `HTMLParserStream` and appends them into the `targetElement`. 57 | 58 | ```js 59 | const bodyWritable = new DOMWritable(targetElement); 60 | const response = await fetch(url); 61 | const domStream = response.body 62 | .pipeThrough(new TextDecoderStream()) 63 | .pipeThrough(new HTMLParserStream()) 64 | .pipeTo(bodyWritable); 65 | ``` 66 | 67 | ## High-level solution 68 | 69 | A custom element. 70 | 71 | ```html 72 | 73 | ``` 74 | 75 | It fetches the `src`, clears the content of the element, pipes the response through `TextDecoderStream`, `HTMLParserStream`, then adds resulting elements (as with `DOMWritable`) to itself. 76 | 77 | Attributes: 78 | 79 | * `src` - URL of the content. When `src` changes, any current stream is cancelled, and the above process starts again. 80 | * `crossorigin` - "anonymous" by default. Can be set to "use-credentials". 81 | 82 | Properties: 83 | 84 | * `src` and `crossOrigin` reflect their attributes. 85 | * `parsed` - A promise that resolves once the response has been fully read and elements created. Rejects if the fetch errors. 86 | 87 | If `src` is set (even to the same value), or `crossorigin` is set to a new value, any current stream is cancelled, and the loading process starts again. 88 | 89 | Methods: 90 | 91 | * `abort()` - Abort any current fetch. 92 | 93 | # Additional ideas 94 | 95 | There could also be a `DOMParserBlocker`: 96 | 97 | ```js 98 | const throttledDOMStream = domStream.passThrough(new DOMParserBlocker()); 99 | ``` 100 | 101 | This will apply the parser-blocking rules of scripts & stylesheets. Namely: 102 | 103 | * If a script-blocking stylesheet is passed through, it will hold-back any ` 8 | 9 | 10 | 11 | 12 |

Load comments using:

13 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "streaming-include", 3 | "requires": true, 4 | "lockfileVersion": 1, 5 | "dependencies": { 6 | "ansi-colors": { 7 | "version": "3.2.3", 8 | "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", 9 | "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", 10 | "dev": true 11 | }, 12 | "ansi-regex": { 13 | "version": "3.0.0", 14 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", 15 | "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", 16 | "dev": true 17 | }, 18 | "ansi-styles": { 19 | "version": "3.2.1", 20 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 21 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 22 | "dev": true, 23 | "requires": { 24 | "color-convert": "^1.9.0" 25 | } 26 | }, 27 | "argparse": { 28 | "version": "1.0.10", 29 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", 30 | "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", 31 | "dev": true, 32 | "requires": { 33 | "sprintf-js": "~1.0.2" 34 | } 35 | }, 36 | "assertion-error": { 37 | "version": "1.1.0", 38 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", 39 | "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", 40 | "dev": true 41 | }, 42 | "balanced-match": { 43 | "version": "1.0.0", 44 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 45 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 46 | "dev": true 47 | }, 48 | "brace-expansion": { 49 | "version": "1.1.11", 50 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 51 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 52 | "dev": true, 53 | "requires": { 54 | "balanced-match": "^1.0.0", 55 | "concat-map": "0.0.1" 56 | } 57 | }, 58 | "browser-stdout": { 59 | "version": "1.3.1", 60 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", 61 | "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", 62 | "dev": true 63 | }, 64 | "camelcase": { 65 | "version": "5.3.1", 66 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", 67 | "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", 68 | "dev": true 69 | }, 70 | "chai": { 71 | "version": "4.2.0", 72 | "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", 73 | "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", 74 | "dev": true, 75 | "requires": { 76 | "assertion-error": "^1.1.0", 77 | "check-error": "^1.0.2", 78 | "deep-eql": "^3.0.1", 79 | "get-func-name": "^2.0.0", 80 | "pathval": "^1.1.0", 81 | "type-detect": "^4.0.5" 82 | } 83 | }, 84 | "chalk": { 85 | "version": "2.4.2", 86 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 87 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 88 | "dev": true, 89 | "requires": { 90 | "ansi-styles": "^3.2.1", 91 | "escape-string-regexp": "^1.0.5", 92 | "supports-color": "^5.3.0" 93 | }, 94 | "dependencies": { 95 | "supports-color": { 96 | "version": "5.5.0", 97 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 98 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 99 | "dev": true, 100 | "requires": { 101 | "has-flag": "^3.0.0" 102 | } 103 | } 104 | } 105 | }, 106 | "check-error": { 107 | "version": "1.0.2", 108 | "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", 109 | "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", 110 | "dev": true 111 | }, 112 | "cliui": { 113 | "version": "4.1.0", 114 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", 115 | "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", 116 | "dev": true, 117 | "requires": { 118 | "string-width": "^2.1.1", 119 | "strip-ansi": "^4.0.0", 120 | "wrap-ansi": "^2.0.0" 121 | } 122 | }, 123 | "code-point-at": { 124 | "version": "1.1.0", 125 | "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", 126 | "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", 127 | "dev": true 128 | }, 129 | "color-convert": { 130 | "version": "1.9.3", 131 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 132 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 133 | "dev": true, 134 | "requires": { 135 | "color-name": "1.1.3" 136 | } 137 | }, 138 | "color-name": { 139 | "version": "1.1.3", 140 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 141 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", 142 | "dev": true 143 | }, 144 | "concat-map": { 145 | "version": "0.0.1", 146 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 147 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 148 | "dev": true 149 | }, 150 | "cross-spawn": { 151 | "version": "6.0.5", 152 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", 153 | "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", 154 | "dev": true, 155 | "requires": { 156 | "nice-try": "^1.0.4", 157 | "path-key": "^2.0.1", 158 | "semver": "^5.5.0", 159 | "shebang-command": "^1.2.0", 160 | "which": "^1.2.9" 161 | } 162 | }, 163 | "debug": { 164 | "version": "3.2.6", 165 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", 166 | "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", 167 | "dev": true, 168 | "requires": { 169 | "ms": "^2.1.1" 170 | } 171 | }, 172 | "decamelize": { 173 | "version": "1.2.0", 174 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", 175 | "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", 176 | "dev": true 177 | }, 178 | "deep-eql": { 179 | "version": "3.0.1", 180 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", 181 | "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", 182 | "dev": true, 183 | "requires": { 184 | "type-detect": "^4.0.0" 185 | } 186 | }, 187 | "define-properties": { 188 | "version": "1.1.3", 189 | "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", 190 | "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", 191 | "dev": true, 192 | "requires": { 193 | "object-keys": "^1.0.12" 194 | } 195 | }, 196 | "diff": { 197 | "version": "3.5.0", 198 | "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", 199 | "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", 200 | "dev": true 201 | }, 202 | "emoji-regex": { 203 | "version": "7.0.3", 204 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", 205 | "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", 206 | "dev": true 207 | }, 208 | "end-of-stream": { 209 | "version": "1.4.1", 210 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", 211 | "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", 212 | "dev": true, 213 | "requires": { 214 | "once": "^1.4.0" 215 | } 216 | }, 217 | "es-abstract": { 218 | "version": "1.13.0", 219 | "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", 220 | "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", 221 | "dev": true, 222 | "requires": { 223 | "es-to-primitive": "^1.2.0", 224 | "function-bind": "^1.1.1", 225 | "has": "^1.0.3", 226 | "is-callable": "^1.1.4", 227 | "is-regex": "^1.0.4", 228 | "object-keys": "^1.0.12" 229 | } 230 | }, 231 | "es-to-primitive": { 232 | "version": "1.2.0", 233 | "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", 234 | "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", 235 | "dev": true, 236 | "requires": { 237 | "is-callable": "^1.1.4", 238 | "is-date-object": "^1.0.1", 239 | "is-symbol": "^1.0.2" 240 | } 241 | }, 242 | "escape-string-regexp": { 243 | "version": "1.0.5", 244 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 245 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 246 | "dev": true 247 | }, 248 | "esprima": { 249 | "version": "4.0.1", 250 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 251 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", 252 | "dev": true 253 | }, 254 | "execa": { 255 | "version": "1.0.0", 256 | "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", 257 | "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", 258 | "dev": true, 259 | "requires": { 260 | "cross-spawn": "^6.0.0", 261 | "get-stream": "^4.0.0", 262 | "is-stream": "^1.1.0", 263 | "npm-run-path": "^2.0.0", 264 | "p-finally": "^1.0.0", 265 | "signal-exit": "^3.0.0", 266 | "strip-eof": "^1.0.0" 267 | } 268 | }, 269 | "find-up": { 270 | "version": "3.0.0", 271 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", 272 | "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", 273 | "dev": true, 274 | "requires": { 275 | "locate-path": "^3.0.0" 276 | } 277 | }, 278 | "flat": { 279 | "version": "4.1.0", 280 | "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", 281 | "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", 282 | "dev": true, 283 | "requires": { 284 | "is-buffer": "~2.0.3" 285 | } 286 | }, 287 | "fs.realpath": { 288 | "version": "1.0.0", 289 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 290 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 291 | "dev": true 292 | }, 293 | "function-bind": { 294 | "version": "1.1.1", 295 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 296 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", 297 | "dev": true 298 | }, 299 | "get-caller-file": { 300 | "version": "2.0.5", 301 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 302 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 303 | "dev": true 304 | }, 305 | "get-func-name": { 306 | "version": "2.0.0", 307 | "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", 308 | "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", 309 | "dev": true 310 | }, 311 | "get-stream": { 312 | "version": "4.1.0", 313 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", 314 | "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", 315 | "dev": true, 316 | "requires": { 317 | "pump": "^3.0.0" 318 | } 319 | }, 320 | "glob": { 321 | "version": "7.1.3", 322 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", 323 | "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", 324 | "dev": true, 325 | "requires": { 326 | "fs.realpath": "^1.0.0", 327 | "inflight": "^1.0.4", 328 | "inherits": "2", 329 | "minimatch": "^3.0.4", 330 | "once": "^1.3.0", 331 | "path-is-absolute": "^1.0.0" 332 | } 333 | }, 334 | "growl": { 335 | "version": "1.10.5", 336 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", 337 | "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", 338 | "dev": true 339 | }, 340 | "has": { 341 | "version": "1.0.3", 342 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 343 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 344 | "dev": true, 345 | "requires": { 346 | "function-bind": "^1.1.1" 347 | } 348 | }, 349 | "has-flag": { 350 | "version": "3.0.0", 351 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 352 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 353 | "dev": true 354 | }, 355 | "has-symbols": { 356 | "version": "1.0.0", 357 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", 358 | "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", 359 | "dev": true 360 | }, 361 | "he": { 362 | "version": "1.2.0", 363 | "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", 364 | "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", 365 | "dev": true 366 | }, 367 | "inflight": { 368 | "version": "1.0.6", 369 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 370 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 371 | "dev": true, 372 | "requires": { 373 | "once": "^1.3.0", 374 | "wrappy": "1" 375 | } 376 | }, 377 | "inherits": { 378 | "version": "2.0.3", 379 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 380 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 381 | "dev": true 382 | }, 383 | "invert-kv": { 384 | "version": "2.0.0", 385 | "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", 386 | "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", 387 | "dev": true 388 | }, 389 | "is-buffer": { 390 | "version": "2.0.3", 391 | "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", 392 | "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==", 393 | "dev": true 394 | }, 395 | "is-callable": { 396 | "version": "1.1.4", 397 | "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", 398 | "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", 399 | "dev": true 400 | }, 401 | "is-date-object": { 402 | "version": "1.0.1", 403 | "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", 404 | "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", 405 | "dev": true 406 | }, 407 | "is-fullwidth-code-point": { 408 | "version": "2.0.0", 409 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", 410 | "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", 411 | "dev": true 412 | }, 413 | "is-regex": { 414 | "version": "1.0.4", 415 | "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", 416 | "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", 417 | "dev": true, 418 | "requires": { 419 | "has": "^1.0.1" 420 | } 421 | }, 422 | "is-stream": { 423 | "version": "1.1.0", 424 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", 425 | "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", 426 | "dev": true 427 | }, 428 | "is-symbol": { 429 | "version": "1.0.2", 430 | "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", 431 | "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", 432 | "dev": true, 433 | "requires": { 434 | "has-symbols": "^1.0.0" 435 | } 436 | }, 437 | "isexe": { 438 | "version": "2.0.0", 439 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 440 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", 441 | "dev": true 442 | }, 443 | "js-yaml": { 444 | "version": "3.13.1", 445 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", 446 | "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", 447 | "dev": true, 448 | "requires": { 449 | "argparse": "^1.0.7", 450 | "esprima": "^4.0.0" 451 | } 452 | }, 453 | "lcid": { 454 | "version": "2.0.0", 455 | "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", 456 | "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", 457 | "dev": true, 458 | "requires": { 459 | "invert-kv": "^2.0.0" 460 | } 461 | }, 462 | "locate-path": { 463 | "version": "3.0.0", 464 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", 465 | "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", 466 | "dev": true, 467 | "requires": { 468 | "p-locate": "^3.0.0", 469 | "path-exists": "^3.0.0" 470 | } 471 | }, 472 | "lodash": { 473 | "version": "4.17.11", 474 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", 475 | "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", 476 | "dev": true 477 | }, 478 | "log-symbols": { 479 | "version": "2.2.0", 480 | "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", 481 | "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", 482 | "dev": true, 483 | "requires": { 484 | "chalk": "^2.0.1" 485 | } 486 | }, 487 | "map-age-cleaner": { 488 | "version": "0.1.3", 489 | "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", 490 | "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", 491 | "dev": true, 492 | "requires": { 493 | "p-defer": "^1.0.0" 494 | } 495 | }, 496 | "mem": { 497 | "version": "4.3.0", 498 | "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", 499 | "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", 500 | "dev": true, 501 | "requires": { 502 | "map-age-cleaner": "^0.1.1", 503 | "mimic-fn": "^2.0.0", 504 | "p-is-promise": "^2.0.0" 505 | } 506 | }, 507 | "mimic-fn": { 508 | "version": "2.1.0", 509 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", 510 | "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", 511 | "dev": true 512 | }, 513 | "minimatch": { 514 | "version": "3.0.4", 515 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 516 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 517 | "dev": true, 518 | "requires": { 519 | "brace-expansion": "^1.1.7" 520 | } 521 | }, 522 | "minimist": { 523 | "version": "0.0.8", 524 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 525 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 526 | "dev": true 527 | }, 528 | "mkdirp": { 529 | "version": "0.5.1", 530 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 531 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 532 | "dev": true, 533 | "requires": { 534 | "minimist": "0.0.8" 535 | } 536 | }, 537 | "mocha": { 538 | "version": "6.1.4", 539 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.1.4.tgz", 540 | "integrity": "sha512-PN8CIy4RXsIoxoFJzS4QNnCH4psUCPWc4/rPrst/ecSJJbLBkubMiyGCP2Kj/9YnWbotFqAoeXyXMucj7gwCFg==", 541 | "dev": true, 542 | "requires": { 543 | "ansi-colors": "3.2.3", 544 | "browser-stdout": "1.3.1", 545 | "debug": "3.2.6", 546 | "diff": "3.5.0", 547 | "escape-string-regexp": "1.0.5", 548 | "find-up": "3.0.0", 549 | "glob": "7.1.3", 550 | "growl": "1.10.5", 551 | "he": "1.2.0", 552 | "js-yaml": "3.13.1", 553 | "log-symbols": "2.2.0", 554 | "minimatch": "3.0.4", 555 | "mkdirp": "0.5.1", 556 | "ms": "2.1.1", 557 | "node-environment-flags": "1.0.5", 558 | "object.assign": "4.1.0", 559 | "strip-json-comments": "2.0.1", 560 | "supports-color": "6.0.0", 561 | "which": "1.3.1", 562 | "wide-align": "1.1.3", 563 | "yargs": "13.2.2", 564 | "yargs-parser": "13.0.0", 565 | "yargs-unparser": "1.5.0" 566 | } 567 | }, 568 | "ms": { 569 | "version": "2.1.1", 570 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 571 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", 572 | "dev": true 573 | }, 574 | "nice-try": { 575 | "version": "1.0.5", 576 | "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", 577 | "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", 578 | "dev": true 579 | }, 580 | "node-environment-flags": { 581 | "version": "1.0.5", 582 | "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz", 583 | "integrity": "sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==", 584 | "dev": true, 585 | "requires": { 586 | "object.getownpropertydescriptors": "^2.0.3", 587 | "semver": "^5.7.0" 588 | } 589 | }, 590 | "npm-run-path": { 591 | "version": "2.0.2", 592 | "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", 593 | "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", 594 | "dev": true, 595 | "requires": { 596 | "path-key": "^2.0.0" 597 | } 598 | }, 599 | "number-is-nan": { 600 | "version": "1.0.1", 601 | "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", 602 | "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", 603 | "dev": true 604 | }, 605 | "object-keys": { 606 | "version": "1.1.1", 607 | "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", 608 | "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", 609 | "dev": true 610 | }, 611 | "object.assign": { 612 | "version": "4.1.0", 613 | "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", 614 | "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", 615 | "dev": true, 616 | "requires": { 617 | "define-properties": "^1.1.2", 618 | "function-bind": "^1.1.1", 619 | "has-symbols": "^1.0.0", 620 | "object-keys": "^1.0.11" 621 | } 622 | }, 623 | "object.getownpropertydescriptors": { 624 | "version": "2.0.3", 625 | "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", 626 | "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", 627 | "dev": true, 628 | "requires": { 629 | "define-properties": "^1.1.2", 630 | "es-abstract": "^1.5.1" 631 | } 632 | }, 633 | "once": { 634 | "version": "1.4.0", 635 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 636 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 637 | "dev": true, 638 | "requires": { 639 | "wrappy": "1" 640 | } 641 | }, 642 | "os-locale": { 643 | "version": "3.1.0", 644 | "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", 645 | "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", 646 | "dev": true, 647 | "requires": { 648 | "execa": "^1.0.0", 649 | "lcid": "^2.0.0", 650 | "mem": "^4.0.0" 651 | } 652 | }, 653 | "p-defer": { 654 | "version": "1.0.0", 655 | "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", 656 | "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", 657 | "dev": true 658 | }, 659 | "p-finally": { 660 | "version": "1.0.0", 661 | "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", 662 | "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", 663 | "dev": true 664 | }, 665 | "p-is-promise": { 666 | "version": "2.1.0", 667 | "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", 668 | "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", 669 | "dev": true 670 | }, 671 | "p-limit": { 672 | "version": "2.2.0", 673 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", 674 | "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", 675 | "dev": true, 676 | "requires": { 677 | "p-try": "^2.0.0" 678 | } 679 | }, 680 | "p-locate": { 681 | "version": "3.0.0", 682 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", 683 | "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", 684 | "dev": true, 685 | "requires": { 686 | "p-limit": "^2.0.0" 687 | } 688 | }, 689 | "p-try": { 690 | "version": "2.2.0", 691 | "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", 692 | "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", 693 | "dev": true 694 | }, 695 | "path-exists": { 696 | "version": "3.0.0", 697 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", 698 | "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", 699 | "dev": true 700 | }, 701 | "path-is-absolute": { 702 | "version": "1.0.1", 703 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 704 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 705 | "dev": true 706 | }, 707 | "path-key": { 708 | "version": "2.0.1", 709 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", 710 | "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", 711 | "dev": true 712 | }, 713 | "pathval": { 714 | "version": "1.1.0", 715 | "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", 716 | "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", 717 | "dev": true 718 | }, 719 | "pump": { 720 | "version": "3.0.0", 721 | "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", 722 | "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", 723 | "dev": true, 724 | "requires": { 725 | "end-of-stream": "^1.1.0", 726 | "once": "^1.3.1" 727 | } 728 | }, 729 | "require-directory": { 730 | "version": "2.1.1", 731 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 732 | "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", 733 | "dev": true 734 | }, 735 | "require-main-filename": { 736 | "version": "2.0.0", 737 | "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", 738 | "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", 739 | "dev": true 740 | }, 741 | "semver": { 742 | "version": "5.7.0", 743 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", 744 | "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", 745 | "dev": true 746 | }, 747 | "set-blocking": { 748 | "version": "2.0.0", 749 | "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", 750 | "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", 751 | "dev": true 752 | }, 753 | "shebang-command": { 754 | "version": "1.2.0", 755 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", 756 | "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", 757 | "dev": true, 758 | "requires": { 759 | "shebang-regex": "^1.0.0" 760 | } 761 | }, 762 | "shebang-regex": { 763 | "version": "1.0.0", 764 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", 765 | "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", 766 | "dev": true 767 | }, 768 | "signal-exit": { 769 | "version": "3.0.2", 770 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", 771 | "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", 772 | "dev": true 773 | }, 774 | "sprintf-js": { 775 | "version": "1.0.3", 776 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 777 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", 778 | "dev": true 779 | }, 780 | "string-width": { 781 | "version": "2.1.1", 782 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", 783 | "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", 784 | "dev": true, 785 | "requires": { 786 | "is-fullwidth-code-point": "^2.0.0", 787 | "strip-ansi": "^4.0.0" 788 | } 789 | }, 790 | "strip-ansi": { 791 | "version": "4.0.0", 792 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", 793 | "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", 794 | "dev": true, 795 | "requires": { 796 | "ansi-regex": "^3.0.0" 797 | } 798 | }, 799 | "strip-eof": { 800 | "version": "1.0.0", 801 | "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", 802 | "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", 803 | "dev": true 804 | }, 805 | "strip-json-comments": { 806 | "version": "2.0.1", 807 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", 808 | "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", 809 | "dev": true 810 | }, 811 | "supports-color": { 812 | "version": "6.0.0", 813 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", 814 | "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", 815 | "dev": true, 816 | "requires": { 817 | "has-flag": "^3.0.0" 818 | } 819 | }, 820 | "type-detect": { 821 | "version": "4.0.8", 822 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", 823 | "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", 824 | "dev": true 825 | }, 826 | "which": { 827 | "version": "1.3.1", 828 | "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", 829 | "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", 830 | "dev": true, 831 | "requires": { 832 | "isexe": "^2.0.0" 833 | } 834 | }, 835 | "which-module": { 836 | "version": "2.0.0", 837 | "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", 838 | "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", 839 | "dev": true 840 | }, 841 | "wide-align": { 842 | "version": "1.1.3", 843 | "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", 844 | "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", 845 | "dev": true, 846 | "requires": { 847 | "string-width": "^1.0.2 || 2" 848 | } 849 | }, 850 | "wrap-ansi": { 851 | "version": "2.1.0", 852 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", 853 | "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", 854 | "dev": true, 855 | "requires": { 856 | "string-width": "^1.0.1", 857 | "strip-ansi": "^3.0.1" 858 | }, 859 | "dependencies": { 860 | "ansi-regex": { 861 | "version": "2.1.1", 862 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", 863 | "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", 864 | "dev": true 865 | }, 866 | "is-fullwidth-code-point": { 867 | "version": "1.0.0", 868 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", 869 | "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", 870 | "dev": true, 871 | "requires": { 872 | "number-is-nan": "^1.0.0" 873 | } 874 | }, 875 | "string-width": { 876 | "version": "1.0.2", 877 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", 878 | "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", 879 | "dev": true, 880 | "requires": { 881 | "code-point-at": "^1.0.0", 882 | "is-fullwidth-code-point": "^1.0.0", 883 | "strip-ansi": "^3.0.0" 884 | } 885 | }, 886 | "strip-ansi": { 887 | "version": "3.0.1", 888 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", 889 | "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", 890 | "dev": true, 891 | "requires": { 892 | "ansi-regex": "^2.0.0" 893 | } 894 | } 895 | } 896 | }, 897 | "wrappy": { 898 | "version": "1.0.2", 899 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 900 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 901 | "dev": true 902 | }, 903 | "y18n": { 904 | "version": "4.0.0", 905 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", 906 | "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", 907 | "dev": true 908 | }, 909 | "yargs": { 910 | "version": "13.2.2", 911 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.2.tgz", 912 | "integrity": "sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA==", 913 | "dev": true, 914 | "requires": { 915 | "cliui": "^4.0.0", 916 | "find-up": "^3.0.0", 917 | "get-caller-file": "^2.0.1", 918 | "os-locale": "^3.1.0", 919 | "require-directory": "^2.1.1", 920 | "require-main-filename": "^2.0.0", 921 | "set-blocking": "^2.0.0", 922 | "string-width": "^3.0.0", 923 | "which-module": "^2.0.0", 924 | "y18n": "^4.0.0", 925 | "yargs-parser": "^13.0.0" 926 | }, 927 | "dependencies": { 928 | "ansi-regex": { 929 | "version": "4.1.0", 930 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", 931 | "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", 932 | "dev": true 933 | }, 934 | "string-width": { 935 | "version": "3.1.0", 936 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", 937 | "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", 938 | "dev": true, 939 | "requires": { 940 | "emoji-regex": "^7.0.1", 941 | "is-fullwidth-code-point": "^2.0.0", 942 | "strip-ansi": "^5.1.0" 943 | } 944 | }, 945 | "strip-ansi": { 946 | "version": "5.2.0", 947 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", 948 | "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", 949 | "dev": true, 950 | "requires": { 951 | "ansi-regex": "^4.1.0" 952 | } 953 | } 954 | } 955 | }, 956 | "yargs-parser": { 957 | "version": "13.0.0", 958 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.0.0.tgz", 959 | "integrity": "sha512-w2LXjoL8oRdRQN+hOyppuXs+V/fVAYtpcrRxZuF7Kt/Oc+Jr2uAcVntaUTNT6w5ihoWfFDpNY8CPx1QskxZ/pw==", 960 | "dev": true, 961 | "requires": { 962 | "camelcase": "^5.0.0", 963 | "decamelize": "^1.2.0" 964 | } 965 | }, 966 | "yargs-unparser": { 967 | "version": "1.5.0", 968 | "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.5.0.tgz", 969 | "integrity": "sha512-HK25qidFTCVuj/D1VfNiEndpLIeJN78aqgR23nL3y4N0U/91cOAzqfHlF8n2BvoNDcZmJKin3ddNSvOxSr8flw==", 970 | "dev": true, 971 | "requires": { 972 | "flat": "^4.1.0", 973 | "lodash": "^4.17.11", 974 | "yargs": "^12.0.5" 975 | }, 976 | "dependencies": { 977 | "get-caller-file": { 978 | "version": "1.0.3", 979 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", 980 | "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", 981 | "dev": true 982 | }, 983 | "require-main-filename": { 984 | "version": "1.0.1", 985 | "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", 986 | "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", 987 | "dev": true 988 | }, 989 | "yargs": { 990 | "version": "12.0.5", 991 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", 992 | "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", 993 | "dev": true, 994 | "requires": { 995 | "cliui": "^4.0.0", 996 | "decamelize": "^1.2.0", 997 | "find-up": "^3.0.0", 998 | "get-caller-file": "^1.0.1", 999 | "os-locale": "^3.0.0", 1000 | "require-directory": "^2.1.1", 1001 | "require-main-filename": "^1.0.1", 1002 | "set-blocking": "^2.0.0", 1003 | "string-width": "^2.0.0", 1004 | "which-module": "^2.0.0", 1005 | "y18n": "^3.2.1 || ^4.0.0", 1006 | "yargs-parser": "^11.1.1" 1007 | } 1008 | }, 1009 | "yargs-parser": { 1010 | "version": "11.1.1", 1011 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", 1012 | "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", 1013 | "dev": true, 1014 | "requires": { 1015 | "camelcase": "^5.0.0", 1016 | "decamelize": "^1.2.0" 1017 | } 1018 | } 1019 | } 1020 | } 1021 | } 1022 | } 1023 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "streaming-include", 3 | "private": true, 4 | "devDependencies": { 5 | "chai": "^4.2.0", 6 | "mocha": "^6.1.4" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /polyfill/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @template {{}} T 3 | * @typedef {WeakMap} CloneMap 4 | */ 5 | 6 | class ParserChunk { 7 | /** 8 | * @param node {Node} 9 | * @param parent {Node | null} 10 | * @param nextSibling {Node | null} 11 | */ 12 | constructor(node, parent, nextSibling) { 13 | this.node = node; 14 | this.parent = parent; 15 | this.nextSibling = nextSibling; 16 | } 17 | } 18 | 19 | /** 20 | * @param {Node} node 21 | * @returns {Node} 22 | */ 23 | function cloneNode(node) { 24 | if (node.nodeName !== 'SCRIPT') return node.cloneNode(); 25 | // Take a manual path for scripts, to avoid copying the "already started" flag 26 | // https://html.spec.whatwg.org/multipage/scripting.html#script-processing-model 27 | const originalScript = /** @type {HTMLScriptElement} */(/** @type {unknown} */(node)); 28 | const script = document.createElementNS( 29 | /** @type {string} */ (originalScript.namespaceURI), originalScript.localName, 30 | ); 31 | //const attributes = Array.from(originalScript.attributes); 32 | for (const attribute of originalScript.attributes) { 33 | script.attributes.setNamedItemNS(/** @type {Attr} */(attribute.cloneNode())); 34 | } 35 | 36 | return script; 37 | } 38 | 39 | /** 40 | * @extends {TransformStream} 41 | */ 42 | export class HTMLParserStream extends TransformStream { 43 | /** @type {CloneMap} */ 44 | _cloneMap = new WeakMap(); 45 | /** @type {Parameters | undefined} */ 46 | _bufferedEntry; 47 | _doc = document.implementation.createHTMLDocument(); 48 | _root = this._doc.body; 49 | /** @type {Node[]} */ 50 | _roots = [this._root]; 51 | _cloneStartPoint = document.createElement('template').content; 52 | /** @type {TransformStreamDefaultController} */ 53 | _controller; 54 | 55 | _observer = new MutationObserver((entries) => { 56 | /** @type {Set} */ 57 | const removedNodes = new Set(); 58 | 59 | for (const entry of entries) { 60 | for (const node of entry.removedNodes) { 61 | // Nodes are removed during parse errors, but will reappear later. They may be inserted 62 | // into a node that isn't currently in the document, so it won't reappear in addedNodes, 63 | // so we need to cater for that. 64 | removedNodes.add(node); 65 | } 66 | for (const node of entry.addedNodes) { 67 | removedNodes.delete(node); 68 | this._handleAddedNode(node, entry.target, entry.nextSibling); 69 | } 70 | } 71 | 72 | while (removedNodes.size) { 73 | for (const node of removedNodes) { 74 | // I don't think there's a case where removed nodes simply disappear, but just in case: 75 | if (!this._roots.some(root => root.contains(node))) { 76 | removedNodes.delete(node); 77 | continue; 78 | } 79 | 80 | // If we haven't added the parent or next sibling yet, leave it until a later iteration. 81 | if ( 82 | removedNodes.has(/** @type {Node} */(node.parentNode)) || 83 | (node.nextSibling && removedNodes.has(node.nextSibling)) 84 | ) continue; 85 | 86 | this._handleAddedNode(node, node.parentNode, node.nextSibling); 87 | removedNodes.delete(node); 88 | } 89 | } 90 | }); 91 | 92 | /** 93 | * @this {HTMLParserStream} 94 | * @param {Node} node 95 | * @param {Node | null} parent 96 | * @param {Node | null} nextSibling 97 | */ 98 | _flushNode = function flushNode(node, parent, nextSibling) { 99 | let isNewTemplate = false; 100 | 101 | if (!this._cloneMap.has(node)) { 102 | const clone = cloneNode(node); 103 | this._cloneStartPoint.append(clone); 104 | this._cloneMap.set(node, clone); 105 | 106 | if (clone instanceof HTMLTemplateElement) { 107 | isNewTemplate = true; 108 | this._cloneMap.set(/** @type {HTMLTemplateElement} */ (node).content, clone.content); 109 | } 110 | } 111 | 112 | this._controller.enqueue( 113 | new ParserChunk( 114 | /** @type {Node} */ (this._cloneMap.get(node)), 115 | !parent || parent === this._root ? null : /** @type {Node} */ (this._cloneMap.get(parent)), 116 | !nextSibling ? null : /** @type {Node} */ (this._cloneMap.get(nextSibling)) 117 | ) 118 | ); 119 | 120 | if (isNewTemplate) this._handleAddedTemplate(/** @type {HTMLTemplateElement} */ (node)); 121 | } 122 | 123 | /** 124 | * @this {HTMLParserStream} 125 | * @param {Node} node 126 | * @param {Node | null} parent 127 | * @param {Node | null} nextSibling 128 | */ 129 | _handleAddedNode = function handleAddedNode(node, parent, nextSibling) { 130 | // Text nodes are buffered until the next node comes along. This means we know the text is 131 | // complete by the time we yield it, and we don't need to add more text to it. 132 | if (this._bufferedEntry) { 133 | this._flushNode(...this._bufferedEntry); 134 | this._bufferedEntry = undefined; 135 | } 136 | if (node.nodeType === 3) { 137 | // @ts-ignore 138 | this._bufferedEntry = [node, parent, nextSibling]; 139 | return; 140 | } 141 | this._flushNode(node, parent, nextSibling); 142 | } 143 | 144 | /** 145 | * @this {HTMLParserStream} 146 | * @param {HTMLTemplateElement} template 147 | */ 148 | _handleAddedTemplate = function handleAddedTemplate(template) { 149 | const nodeIttr = this._doc.createNodeIterator(template.content); 150 | let node; 151 | 152 | while (node = nodeIttr.nextNode()) { 153 | this._handleAddedNode(node, node.parentNode, null); 154 | } 155 | 156 | this._roots.push(template.content); 157 | this._observer.observe(template.content, { subtree: true, childList: true }); 158 | } 159 | 160 | 161 | constructor() { 162 | super({ 163 | start: (c) => { controller = c; }, 164 | transform: (chunk) => { this._doc.write(chunk); }, 165 | flush: () => { 166 | if (this._bufferedEntry) this._flushNode(...this._bufferedEntry); 167 | this._doc.close(); 168 | } 169 | }); 170 | 171 | /** @type {TransformStreamDefaultController} */ 172 | var controller; 173 | // @ts-ignore 174 | this._controller = controller; 175 | 176 | this._doc.write(''); 177 | this._observer.observe(this._root, { subtree: true, childList: true }); 178 | } 179 | } 180 | 181 | /** 182 | * @extends {WritableStream} 183 | */ 184 | export class DOMWritable extends WritableStream { 185 | /** 186 | * @param {Element} target 187 | */ 188 | constructor(target) { 189 | super({ 190 | write({ node, nextSibling, parent }) { 191 | (parent || target).insertBefore(node, nextSibling); 192 | } 193 | }); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /polyfill/streaming-include.js: -------------------------------------------------------------------------------- 1 | import { HTMLParserStream, DOMWritable } from './index.js'; 2 | 3 | const observedAttributes = Object.freeze(['src', 'crossorigin']); 4 | 5 | /** 6 | * @param {string | null} attrVal 7 | * @returns {'use-credentials' | 'anonymous'} 8 | */ 9 | function crossOriginAttrToProp(attrVal) { 10 | // https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#reflect 11 | if (attrVal && attrVal.toLowerCase() === 'use-credentials') return 'use-credentials'; 12 | return 'anonymous'; 13 | } 14 | 15 | export default class HTMLStreamingIncludeElement extends HTMLElement { 16 | static get observedAttributes() { return observedAttributes; } 17 | /** @type {AbortController | undefined} */ 18 | _abortController; 19 | /** @type {Promise} */ 20 | _parsed = Promise.resolve(); 21 | /** @type {(() => void)} */ 22 | // @ts-ignore - This value is set in the promise constructor 23 | _connectedResolve; 24 | /** @type {Promise} */ 25 | _connected = new Promise(resolve => { this._connectedResolve = resolve; }) 26 | 27 | /** 28 | * @this {HTMLStreamingIncludeElement} 29 | * @returns {void} 30 | */ 31 | _initLoad = function startLoad() { 32 | if (this._abortController) this._abortController.abort(); 33 | this.innerHTML = ''; 34 | 35 | if (!this.hasAttribute('src')) { 36 | this._parsed = Promise.resolve(); 37 | return; 38 | } 39 | 40 | // Catch invalid URLs: 41 | try { 42 | new URL(this.src); 43 | } catch (err) { 44 | this._parsed = Promise.reject(new TypeError()); 45 | return; 46 | } 47 | 48 | const { signal } = this._abortController = new AbortController(); 49 | const abortReject = new Promise((_, reject) => { 50 | signal.addEventListener('abort', () => reject(new DOMException('', 'AbortError'))); 51 | }); 52 | 53 | this._parsed = Promise.race([ 54 | abortReject, 55 | (async () => { 56 | // This microtask not only waits for the element to be connected, but if it's already 57 | // connected it allows for multiple attribute/property changes to be rolled into one fetch. 58 | await this._connected; 59 | if (signal.aborted) return; 60 | 61 | const includeCredentials = this.crossOrigin === 'use-credentials'; 62 | const response = await fetch(this.src, { 63 | signal, 64 | credentials: includeCredentials ? 'include' : 'same-origin' 65 | }); 66 | 67 | const body = /** @type {ReadableStream} */(response.body); 68 | 69 | await body 70 | // @ts-ignore - Type checker doesn't know about TextDecoderStream. 71 | .pipeThrough(new TextDecoderStream()) 72 | .pipeThrough(new HTMLParserStream()) 73 | // @ts-ignore - Type checker doesn't know about the signal option. 74 | .pipeTo(new DOMWritable(this), { signal }); 75 | })(), 76 | ]); 77 | }; 78 | 79 | /** @type {string} */ 80 | get src() { 81 | // https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#reflect 82 | const attr = this.getAttribute('src'); 83 | if (attr === null) return ''; 84 | try { 85 | const url = new URL(attr, location.href); 86 | return url.href; 87 | } catch (err) { 88 | return attr; 89 | } 90 | } 91 | 92 | set src(newVal) { 93 | this.setAttribute('src', newVal); 94 | } 95 | 96 | /** @type {'anonymous' | 'use-credentials'} */ 97 | get crossOrigin() { 98 | return crossOriginAttrToProp(this.getAttribute('crossorigin')); 99 | } 100 | 101 | set crossOrigin(newVal) { 102 | if (newVal === null) { 103 | this.removeAttribute('crossorigin'); 104 | return; 105 | } 106 | this.setAttribute('crossorigin', newVal); 107 | } 108 | 109 | /** 110 | * A promise that resolves once the response has been fully read and elements created. Rejects if 111 | * the fetch errors. 112 | * 113 | * @returns {Promise} 114 | */ 115 | get parsed() { 116 | return this._parsed; 117 | } 118 | 119 | /** 120 | * Abort the current fetch (if any) 121 | */ 122 | abort() { 123 | if (this._abortController) this._abortController.abort(); 124 | } 125 | 126 | connectedCallback() { 127 | this._connectedResolve(); 128 | } 129 | 130 | disconnectedCallback() { 131 | this._connected = new Promise(resolve => { this._connectedResolve = resolve; }) 132 | } 133 | 134 | /** 135 | * @param {'src' | 'crossorigin'} name 136 | * @param {string | null} oldValue 137 | * @param {string | null} newValue 138 | */ 139 | async attributeChangedCallback(name, oldValue, newValue) { 140 | if (name === 'src') { 141 | // Like , any change to src triggers a load, even if the value is the same. 142 | this._initLoad(); 143 | return; 144 | } 145 | 146 | if (name === 'crossorigin') { 147 | // Like , crossorigin must change computed value to trigger a load. 148 | if (crossOriginAttrToProp(oldValue) !== crossOriginAttrToProp(newValue)) { 149 | this._initLoad(); 150 | } 151 | return; 152 | } 153 | } 154 | } 155 | 156 | customElements.define('streaming-include', HTMLStreamingIncludeElement); 157 | -------------------------------------------------------------------------------- /polyfill/test/assets/blank.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakearchibald/streaming-include/98a5692f6dd3fc4a681450e65c4b44decd5fedb0/polyfill/test/assets/blank.txt -------------------------------------------------------------------------------- /polyfill/test/assets/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakearchibald/streaming-include/98a5692f6dd3fc4a681450e65c4b44decd5fedb0/polyfill/test/assets/img.png -------------------------------------------------------------------------------- /polyfill/test/assets/script.js: -------------------------------------------------------------------------------- 1 | { 2 | const scriptURL = new URL(document.currentScript.src); 3 | self[scriptURL.searchParams.get('prop')] = true; 4 | } 5 | -------------------------------------------------------------------------------- /polyfill/test/assets/small-content-1.html: -------------------------------------------------------------------------------- 1 | hello world 2 | -------------------------------------------------------------------------------- /polyfill/test/assets/small-content-2.html: -------------------------------------------------------------------------------- 1 | foo bar 2 | -------------------------------------------------------------------------------- /polyfill/test/assets/style.css: -------------------------------------------------------------------------------- 1 | .test-style { background-color: rgb(0, 128, 0); } 2 | -------------------------------------------------------------------------------- /polyfill/test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | -------------------------------------------------------------------------------- /polyfill/test/index.js: -------------------------------------------------------------------------------- 1 | import '../../node_modules/mocha/mocha.js'; 2 | import '../../node_modules/chai/chai.js'; 3 | 4 | import { HTMLParserStream, DOMWritable } from '../index.js' 5 | import HTMLStreamingIncludeElement from '../streaming-include.js'; 6 | 7 | mocha.setup('tdd'); 8 | 9 | const { assert } = chai; 10 | 11 | let globalUniqueCounter = 0; 12 | 13 | function getUniqueName() { 14 | return 'unique' + globalUniqueCounter++; 15 | } 16 | 17 | function createElementWritable(el) { 18 | const transform = new HTMLParserStream(); 19 | transform.readable.pipeTo(new DOMWritable(el)); 20 | return transform.writable; 21 | } 22 | 23 | function monitorStream(callback) { 24 | return new TransformStream({ 25 | transform(chunk, controller) { 26 | callback(chunk); 27 | controller.enqueue(chunk); 28 | } 29 | }); 30 | } 31 | 32 | function wait(ms = 0) { 33 | return new Promise(r => setTimeout(r, ms)); 34 | } 35 | 36 | /** 37 | * @param {() => Promise} callback 38 | */ 39 | async function assertNoNetworkActivity(callback) { 40 | performance.clearResourceTimings(); 41 | await callback(); 42 | assert.strictEqual( 43 | performance.getEntriesByType('resource').length, 44 | 0, 45 | 'No network activity' 46 | ); 47 | } 48 | 49 | async function assertURLRequested(url, callback) { 50 | url = new URL(url, location).href; 51 | performance.clearResourceTimings(); 52 | await callback(); 53 | const entries = /** @type {PerformanceResourceTiming[]} */(performance.getEntriesByType('resource')); 54 | assert.isTrue(entries.some(entry => entry.name === url)); 55 | } 56 | 57 | const smallContent1 = document.createElement('div'); 58 | smallContent1.innerHTML = 'hello world\n'; 59 | const smallContent2 = document.createElement('div'); 60 | smallContent2.innerHTML = 'foo bar\n'; 61 | 62 | /** 63 | * @param {HTMLStreamingIncludeElement} el 64 | */ 65 | function assertElContentEqual(el1, el2) { 66 | assert.strictEqual(el1.childNodes.length, el2.childNodes.length); 67 | 68 | for (const [i, node] of [...el1.childNodes].entries()) { 69 | assert.isTrue(node.isEqualNode(el2.childNodes[i])); 70 | } 71 | } 72 | 73 | function parseHTMLTest(html) { 74 | let name = html; 75 | if (name.length > 80) name = name.slice(0, 80) + '…'; 76 | 77 | test(name, async () => { 78 | const targetInnerHTML = document.createElement('div'); 79 | const targetOneChunkStream = document.createElement('div'); 80 | const targetMultiChunkStream = document.createElement('div'); 81 | 82 | targetInnerHTML.innerHTML = html; 83 | 84 | { 85 | const writer = createElementWritable(targetOneChunkStream).getWriter(); 86 | writer.write(html); 87 | await writer.close(); 88 | } 89 | 90 | { 91 | const writer = createElementWritable(targetMultiChunkStream).getWriter(); 92 | for (const char of html) { 93 | writer.write(char); 94 | } 95 | await writer.close(); 96 | } 97 | 98 | assert.isTrue(targetInnerHTML.isEqualNode(targetOneChunkStream), 'targetOneChunkStream'); 99 | // Comparing innerHTML as it handles world'); 120 | parseHTMLTest('hello world'); 121 | parseHTMLTest('hello world'); 122 | parseHTMLTest('hello

everyone in the world'); 123 | }); 124 | 125 | suite('Special elements', () => { 126 | setup(() => { 127 | for (const el of document.querySelectorAll('.test-stylesheet')) el.remove(); 128 | }); 129 | 130 | test('Images do not load until connected', async () => { 131 | const transform = new HTMLParserStream(); 132 | const reader = transform.readable.getReader(); 133 | const writer = transform.writable.getWriter(); 134 | writer.write(''); 135 | writer.close(); 136 | 137 | while (true) { 138 | const { done, value } = await reader.read(); 139 | if (done) throw Error('Unexpected done'); 140 | 141 | const img = /** @type {HTMLImageElement} */ (value.node); 142 | assert.instanceOf(img, HTMLImageElement); 143 | assert.isTrue(img.complete); 144 | assert.strictEqual(img.naturalHeight, 0); 145 | return; 146 | } 147 | }); 148 | 149 | for (const charByChar of [false, true]) { 150 | test('Inline script' + (charByChar ? ' char by char' : ''), async () => { 151 | const writable = createElementWritable(document.body); 152 | const writer = writable.getWriter(); 153 | const varName = getUniqueName(); 154 | const content = ``; 155 | if (charByChar) { 156 | for (const char of content) writer.write(char); 157 | } else { 158 | writer.write(content); 159 | } 160 | await writer.close(); 161 | assert.isTrue(self[varName]); 162 | }); 163 | } 164 | 165 | test('Inline script partial', async () => { 166 | const writable = createElementWritable(document.body); 167 | const writer = writable.getWriter(); 168 | const varName = getUniqueName(); 169 | await writer.write(``); 173 | await writer.close(); 174 | assert.isTrue(self[varName]); 175 | }); 176 | 177 | for (const charByChar of [false, true]) { 178 | test('Script attributes' + (charByChar ? ' char by char' : ''), async () => { 179 | const transform = new HTMLParserStream(); 180 | const writer = transform.writable.getWriter(); 181 | const className = getUniqueName(); 182 | const dataValue = getUniqueName(); 183 | const content = ``; 184 | if (charByChar) { 185 | for (const char of content) writer.write(char); 186 | } else { 187 | writer.write(content); 188 | } 189 | writer.close(); 190 | 191 | const { value } = await transform.readable.getReader().read(); 192 | const script = value.node; 193 | assert.instanceOf(script, HTMLScriptElement); 194 | assert.strictEqual(script.getAttribute('class'), className, 'class attribute'); 195 | assert.strictEqual(script.getAttribute('data-val'), dataValue, 'data-val attribute'); 196 | }); 197 | } 198 | 199 | for (const charByChar of [false, true]) { 200 | test('External script' + (charByChar ? ' char by char' : ''), async () => { 201 | const varName = getUniqueName(); 202 | 203 | const loadPromise = new Promise((resolve, reject) => { 204 | const transform = new HTMLParserStream(); 205 | 206 | transform.readable.pipeThrough(monitorStream(chunk => { 207 | chunk.node.addEventListener('load', () => resolve()); 208 | chunk.node.addEventListener('error', () => reject(Error('Script load error'))); 209 | })).pipeTo(new DOMWritable(document.body)); 210 | 211 | const writer = transform.writable.getWriter(); 212 | const content = ``; 213 | if (charByChar) { 214 | for (const char of content) writer.write(char); 215 | } else { 216 | writer.write(content); 217 | } 218 | }); 219 | 220 | await loadPromise; 221 | assert.isTrue(self[varName]); 222 | }); 223 | } 224 | 225 | for (const charByChar of [false, true]) { 226 | test('Inline style' + (charByChar ? ' char by char' : ''), async () => { 227 | const writable = createElementWritable(document.body); 228 | const writer = writable.getWriter(); 229 | const className = getUniqueName(); 230 | const content = ``; 231 | if (charByChar) { 232 | for (const char of content) writer.write(char); 233 | } else { 234 | writer.write(content); 235 | } 236 | await writer.close(); 237 | const div = document.createElement('div'); 238 | div.classList.add(className); 239 | document.body.append(div); 240 | assert.strictEqual(getComputedStyle(div).backgroundColor, 'rgb(0, 128, 0)'); 241 | div.remove(); 242 | }); 243 | } 244 | 245 | test('Inline style partial', async () => { 246 | const writable = createElementWritable(document.body); 247 | const writer = writable.getWriter(); 248 | const className = getUniqueName(); 249 | await writer.write(``); 256 | await writer.close(); 257 | assert.strictEqual(getComputedStyle(div).backgroundColor, 'rgb(0, 128, 0)'); 258 | div.remove(); 259 | }); 260 | 261 | for (const charByChar of [false, true]) { 262 | test('External style' + (charByChar ? ' char by char' : ''), async () => { 263 | const loadPromise = new Promise((resolve, reject) => { 264 | const transform = new HTMLParserStream(); 265 | 266 | transform.readable.pipeThrough(monitorStream(chunk => { 267 | chunk.node.addEventListener('load', () => resolve()); 268 | chunk.node.addEventListener('error', () => reject(Error('Style load error'))); 269 | })).pipeTo(new DOMWritable(document.body)); 270 | 271 | const writer = transform.writable.getWriter(); 272 | const content = ``; 273 | 274 | if (charByChar) { 275 | for (const char of content) writer.write(char); 276 | } else { 277 | writer.write(content); 278 | } 279 | }); 280 | 281 | await loadPromise; 282 | const div = document.createElement('div'); 283 | div.classList.add('test-style'); 284 | document.body.append(div); 285 | assert.strictEqual(getComputedStyle(div).backgroundColor, 'rgb(0, 128, 0)'); 286 | div.remove(); 287 | }); 288 | } 289 | }); 290 | 291 | // These are from https://github.com/html5lib/html5lib-tests/blob/master/tree-construction/tricky01.dat 292 | const trickyTests = [ 293 | '

Bold Not bold

Also not bold.', 294 | `Italic and Red

Italic and Red Just italic.

Italic only.
Plain 295 |

I should not be red. Red. Italic and red.

296 |

Italic and red. Red. I should not be red.

297 | Bold Bold and italic Only Italic Plain`, 298 | `
299 |
Boo 300 |
Goo? 301 |
`, 302 | `