├── .github └── workflows │ └── jstest.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── jsdoc.common.js ├── jsdoc.config.js ├── karma.common.js ├── karma.conf.js ├── package-lock.json ├── package.json ├── plugins └── element.js ├── src ├── dom.ts ├── dom_test.ts ├── hintable.ts ├── human.ts ├── human_test.ts ├── jsonOrThrow.ts ├── object.ts ├── object_test.ts ├── query.ts ├── query_test.ts └── stateReflector.ts ├── tsconfig.json ├── tslint.json └── webpack.config.js /.github/workflows/jstest.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: jstest 5 | 6 | on: 7 | push: 8 | branches: [master] 9 | pull_request: 10 | branches: [master] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [10.x, 12.x] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | 23 | - name: Cache node modules 24 | uses: actions/cache@v1 25 | with: 26 | path: ~/.npm 27 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 28 | restore-keys: | 29 | ${{ runner.os }}-node- 30 | 31 | - name: Use Node.js ${{ matrix.node-version }} 32 | uses: actions/setup-node@v1 33 | with: 34 | node-version: ${{ matrix.node-version }} 35 | 36 | - run: npm run build:ci 37 | env: 38 | CI: true 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | modules 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of 9 | experience, education, socio-economic status, nationality, personal appearance, 10 | race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or reject 41 | comments, commits, code, wiki edits, issues, and other contributions that are 42 | not aligned to this Code of Conduct, or to ban temporarily or permanently any 43 | contributor for other behaviors that they deem inappropriate, threatening, 44 | offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | This Code of Conduct also applies outside the project spaces when the Project 56 | Steward has a reasonable belief that an individual's behavior may have a 57 | negative impact on the project or its community. 58 | 59 | ## Conflict Resolution 60 | 61 | We do not believe that all conflict is bad; healthy debate and disagreement 62 | often yield positive results. However, it is never okay to be disrespectful or 63 | to engage in behavior that violates the project’s code of conduct. 64 | 65 | If you see someone violating the code of conduct, you are encouraged to address 66 | the behavior directly with those involved. Many issues can be resolved quickly 67 | and easily, and this gives people more control over the outcome of their 68 | dispute. If you are unable to resolve the matter for any reason, or if the 69 | behavior is threatening or harassing, report it. We are dedicated to providing 70 | an environment where participants feel welcome and safe. 71 | 72 | Reports should be directed to [Joe Gregorio](mailto:jcgregorio@google.com), the 73 | Project Steward(s) for *pulito*. It is the Project Steward’s duty to 74 | receive and address reported violations of the code of conduct. They will then 75 | work with a committee consisting of representatives from the Open Source 76 | Programs Office and the Google Open Source Strategy team. If for any reason you 77 | are uncomfortable reaching out the Project Steward, please email 78 | opensource@google.com. 79 | 80 | We will investigate every complaint, but you may not receive a direct response. 81 | We will use our discretion in determining when and how to follow up on reported 82 | incidents, which may range from not taking action to permanent expulsion from 83 | the project and project-sponsored spaces. We will notify the accused of the 84 | report and provide them an opportunity to discuss it before any action is taken. 85 | The identity of the reporter will be omitted from the details of the report 86 | supplied to the accused. In potentially harmful situations, such as ongoing 87 | harassment or threats to anyone's safety, we may take action without notice. 88 | 89 | ## Attribution 90 | 91 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, 92 | available at 93 | https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 94 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: package-lock.json 2 | tsc 3 | npx webpack --mode=development 4 | 5 | production: package-lock.json 6 | npx webpack --mode=production 7 | 8 | serve: package-lock.json 9 | npx webpack-dev-server --content-base ./dist --watch-poll 10 | 11 | demos: package-lock.json 12 | npx webpack --mode=development 13 | 14 | lint: 15 | npm run lint 16 | 17 | test: 18 | npm test 19 | 20 | continuous: package-lock.json 21 | # Setup for continuous testing when ssh'd into a machine. 22 | # To debug tests, set up port forwarding via ssh with "-L 9876:localhost:9876". 23 | # Start Xvfb on port :99 so Chrome can start. 24 | -Xvfb :99 & 25 | # Continuously monitor the source files and rebuild the test files as needed. 26 | npx webpack --watch & 27 | sleep 2 28 | # Continuously run the tests each time they are modified. 29 | DISPLAY=:99 npx karma start --no-single-run & 30 | 31 | continuous_desktop: package-lock.json 32 | # Setup for continuous testing when running on the desktop. 33 | # Continuously monitor the source files and rebuild the test files as needed. 34 | npx webpack --watch & 35 | sleep 2 36 | # Continuously run the tests each time they are modified. 37 | npx karma start & 38 | 39 | clean: 40 | rm -rf node_modules 41 | rm -f package-lock.json 42 | npm install 43 | 44 | docs: 45 | npx jsdoc -c jsdoc.config.js 46 | xdg-open out/index.html 47 | 48 | package-lock.json: package.json 49 | npm install 50 | 51 | login: 52 | npm login --registry https://wombat-dressing-room.appspot.com/common-sk/_ns 53 | 54 | publish: 55 | npm publish --registry https://wombat-dressing-room.appspot.com/common-sk/_ns 56 | 57 | update-major: 58 | npm version major 59 | echo "Don't forget to publish." 60 | 61 | update-minor: 62 | npm version minor 63 | echo "Don't forget to publish." 64 | 65 | update-patch: 66 | npm version patch 67 | echo "Don't forget to publish." 68 | 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **THIS REPOSITORY IS ARCHIVED**. Code moved to the Skia Infrastructure repository 2 | [here](https://skia.googlesource.com/buildbot/+/28404b632bd4830fe00185f5db86cfad4cc63abb/infra-sk/modules/) 3 | (see [CL](https://skia-review.googlesource.com/c/buildbot/+/653997)). 4 | 5 | ## common-sk 6 | 7 | This is the common set of JS libraries used by Skia Infrastructure. 8 | 9 | See [A la carte Web Development](https://bitworking.org/news/2018/03/a-la-carte-web-development) 10 | for more background on pulito and how it fits into "A la carte" web 11 | development. 12 | 13 | ![jstest](https://github.com/google/common-sk/workflows/jstest/badge.svg) 14 | 15 | ## Installation 16 | 17 | If loaded via npm then importing will just work, i.e.: 18 | 19 | $ npm i common-sk 20 | 21 | Then from within your code: 22 | 23 | import { $$ } from 'common-sk/modules/dom' 24 | 25 | ## Documentation 26 | 27 | [jsdoc.skia.org](https://jsdoc.skia.org/) contains the 28 | [jsdoc](http://usejsdoc.org/) generated documentation for all of the code. 29 | 30 | ## Disclaimer 31 | 32 | This is not an officially supported Google product. 33 | -------------------------------------------------------------------------------- /jsdoc.common.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // This is a common configuration for running jsdoc 16 | // over modules, including custom elements. To use 17 | // this, just create a jsdoc.config.js file in your 18 | // application directory and populate it with: 19 | // 20 | // module.exports = require('../common/jsdoc.common.js'); 21 | // 22 | // Add jsdoc via npm: 23 | // 24 | // $ npm add jsdoc 25 | // 26 | // Then add this to your Makefile: 27 | // 28 | // docs: 29 | // npx jsdoc -c jsdoc.config.js 30 | // 31 | // This config loads the element plugin which adds support 32 | // for @evt and @attr tags in documentation. 33 | // 34 | // It also presumes the modules exists under the './modules' directory. 35 | // 36 | // Docs will appear in the ./out directory, which should be added 37 | // to .gitignore. 38 | const path = require('path'); 39 | 40 | module.exports = { 41 | plugins: [path.resolve(__dirname, './plugins/element')], 42 | source: { 43 | include: ['./modules'], 44 | includePattern: '.+\\.js$', 45 | }, 46 | opts: { 47 | recurse: true, 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /jsdoc.config.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module.exports = require('./jsdoc.common.js'); 16 | -------------------------------------------------------------------------------- /karma.common.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // This is a common configuration for running karma 16 | // tests in your modules, including custom elements. 17 | // To use this, just create a karma.conf.js file in your 18 | // application directory and populate it with: 19 | // 20 | // module.exports = require('../common/karma.common.js')(__dirname); 21 | // 22 | // Add all the testing packages found in common/packages.json via yarn: 23 | // 24 | // $ yarn add chai karma karma-chai karma-chrome-launcher \ 25 | // karma-firefox-launcher karma-mocha karma-sinon karma-webpack mocha sinon 26 | // 27 | // Then add this to your Makefile: 28 | // 29 | // test: build 30 | // # Run the generated tests just once under Xvfb. 31 | // xvfb-run --auto-servernum --server-args "-screen 0 1280x1024x24" npx karma start --single-run 32 | // 33 | // You can always run: 34 | // 35 | // $ npx karma start 36 | // 37 | // if you need to debug tests using your desktop browser. 38 | const path = require('path'); 39 | 40 | module.exports = function (dirname) { 41 | return function (config) { 42 | let webpackConfig = require(path.resolve(dirname, 'webpack.config.js')); 43 | // Webpack 3+ configs can be either objects or functions that produce the 44 | // config object. Karma currently doesn't handle the latter, so do it 45 | // ourselves here. 46 | if (typeof webpackConfig === 'function') { 47 | webpackConfig = webpackConfig({}, { mode: 'development' }); 48 | } 49 | webpackConfig.entry = null; 50 | webpackConfig.mode = 'development'; 51 | 52 | // Work-around for karma-webpack issues: 53 | // https://github.com/webpack-contrib/karma-webpack/issues/322#issuecomment-417862717 54 | webpackConfig.output = { 55 | filename: '[name]', 56 | }; 57 | 58 | config.set({ 59 | 60 | // base path, that will be used to resolve files and exclude 61 | basePath: '', 62 | 63 | 64 | // frameworks to use 65 | frameworks: ['mocha', 'chai', 'sinon'], 66 | 67 | plugins: [ 68 | 'karma-chrome-launcher', 69 | 'karma-firefox-launcher', 70 | 'karma-webpack', 71 | 'karma-sinon', 72 | 'karma-mocha', 73 | 'karma-chai', 74 | ], 75 | 76 | // list of files / patterns to load in the browser 77 | files: [ 78 | 'node_modules/@webcomponents/custom-elements/custom-elements.min.js', 79 | 'modules/*_test.js', 80 | 'modules/**/*_test.js', 81 | ], 82 | 83 | preprocessors: { 84 | // add webpack as preprocessor 85 | 'modules/*_test.js': ['webpack'], 86 | 'modules/**/*_test.js': ['webpack'], 87 | }, 88 | 89 | // list of files to exclude 90 | exclude: [ 91 | ], 92 | 93 | 94 | // test results reporter to use 95 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' 96 | reporters: ['dots'], 97 | 98 | 99 | // Get the port from KARMA_PORT if it is set. 100 | port: parseInt(process.env.KARMA_PORT || '9876'), 101 | 102 | 103 | // enable / disable colors in the output (reporters and logs) 104 | colors: false, 105 | 106 | 107 | // level of logging 108 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || 109 | // config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 110 | logLevel: config.LOG_INFO, 111 | 112 | 113 | // enable / disable watching file and executing tests whenever any file changes 114 | autoWatch: true, 115 | 116 | 117 | // Start these browsers. 118 | browsers: ['Chrome'], 119 | 120 | 121 | customLaunchers: { 122 | ChromeHeadlessCustom: { 123 | base: 'ChromeHeadless', 124 | flags: ['--no-sandbox', '--disable-gpu'], 125 | }, 126 | }, 127 | 128 | // If browser does not capture in given timeout [ms], kill it 129 | captureTimeout: 60000, 130 | 131 | 132 | // Continuous Integration mode 133 | // if true, it capture browsers, run tests and exit 134 | // 135 | // This can be over-ridden by command-line flag when running Karma. I.e.: 136 | // 137 | // ./node_modules/karma/bin/karma --no-single-run start 138 | // 139 | singleRun: true, 140 | 141 | webpack: webpackConfig, 142 | }); 143 | }; 144 | }; 145 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module.exports = require('./karma.common.js')(__dirname); 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "common-sk", 3 | "version": "3.4.2", 4 | "description": "Common Skia JS modules.", 5 | "homepage": "https://github.com/google/common-sk", 6 | "repository": "https://github.com/google/common-sk.git", 7 | "scripts": { 8 | "clean": "rimraf ./dist", 9 | "test": "npx karma start --single-run --browsers=ChromeHeadlessCustom", 10 | "lint": "npx tslint -c tslint.json 'src/**/*.ts'", 11 | "build:ci": "npm ci && npm run lint && npm run clean && npx tsc && npm run test" 12 | }, 13 | "publishConfig": { 14 | "registry": "https://wombat-dressing-room.appspot.com/" 15 | }, 16 | "files": [ 17 | "modules/", 18 | "*.js", 19 | "src/*.ts", 20 | "plugins" 21 | ], 22 | "main": "index.js", 23 | "license": "Apache-2.0", 24 | "devDependencies": { 25 | "@types/chai": "^4.1.0", 26 | "@types/mocha": "^7.0.1", 27 | "chai": "^4.2.0", 28 | "glob": "^7.1.3", 29 | "jsdoc": "^3.5.5", 30 | "karma": "^6.3.16", 31 | "karma-chai": "^0.1.0", 32 | "karma-chrome-launcher": "^2.2.0", 33 | "karma-firefox-launcher": "^1.1.0", 34 | "karma-mocha": "^1.3.0", 35 | "karma-sinon": "^1.0.5", 36 | "karma-webpack": "3.0.5", 37 | "mocha": "^5.2.0", 38 | "pulito": "^4.1.1", 39 | "rimraf": "^3.0.2", 40 | "sinon": "^7.2.2", 41 | "source-map-loader": "^0.2.4", 42 | "ts-loader": "^6.2.1", 43 | "tslint": "^6.1.0", 44 | "typescript": "^3.8.3", 45 | "webpack": "^4.28.2", 46 | "webpack-cli": "^3.1.2" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /plugins/element.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 16 | 17 | // This is a jsdoc plugin that adds support for both @evt and @attr 18 | // tags on a class, where @evt documents an event the custome element may 19 | // generate, and @attr documents an attribute the custom element recognizes. 20 | // 21 | // The tags are defined here, and we use the onTagged callback to 22 | // store the name and description associated with each tag. 23 | exports.defineTags = function(dictionary) { 24 | dictionary.defineTag('evt', { 25 | canHaveName: true, 26 | onTagged: function(doclet, tag) { 27 | if (!doclet.webevents) { 28 | doclet.webevents= []; 29 | } 30 | 31 | doclet.webevents.push({ 32 | name: tag.value.name, 33 | description: tag.value.description || '', 34 | }); 35 | } 36 | }); 37 | 38 | dictionary.defineTag('attr', { 39 | canHaveName: true, 40 | onTagged: function(doclet, tag) { 41 | if (!doclet.attrs) { 42 | doclet.attrs= []; 43 | } 44 | 45 | doclet.attrs.push({ 46 | name: tag.value.name, 47 | description: tag.value.description || '', 48 | }); 49 | } 50 | }); 51 | } 52 | 53 | const rows = (rowData) => rowData.map((e) => ` 54 | 55 | ${e.name} 56 | ${e.description} 57 | 58 | `); 59 | 60 | const section = (title, rowData) => `
${title}
61 | 62 | 63 | 64 | 65 | 66 | ${rows(rowData).join('')} 67 |
NameDescription
68 | `; 69 | 70 | // The handlers look for doclets with data we stored from our custom 71 | // tags and emit HTML for the data we found. 72 | exports.handlers = { 73 | newDoclet: function(e) { 74 | if (e.doclet.webevents) { 75 | e.doclet.description = ` 76 | ${e.doclet.description || ''} 77 | ${section('Events', e.doclet.webevents)} 78 | `; 79 | } 80 | if (e.doclet.attrs) { 81 | e.doclet.description = ` 82 | ${e.doclet.description || ''} 83 | ${section('Attributes', e.doclet.attrs)} 84 | `; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/dom.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /** @module common-sk/modules/dom */ 16 | /** 17 | * A Promise that resolves when DOMContentLoaded has fired. 18 | */ 19 | export const DomReady = new Promise((resolve) => { 20 | if (document.readyState !== 'loading') { 21 | // If readyState is already past loading then 22 | // DOMContentLoaded has already fired, so just resolve. 23 | resolve(); 24 | } else { 25 | document.addEventListener('DOMContentLoaded', resolve); 26 | } 27 | }); 28 | 29 | /** @function $ 30 | * 31 | * @description Returns a real JS array of DOM elements that match the CSS selector. 32 | * 33 | * @param query CSS selector string. 34 | * @param ele The Element to start the search from. 35 | * @returns Array of DOM Elements that match the CSS selector. 36 | * 37 | */ 38 | export function $( 39 | query: string, 40 | ele: Element | Document = document 41 | ): E[] { 42 | return Array.from(ele.querySelectorAll(query)); 43 | } 44 | 45 | /** @function $$ 46 | * 47 | * @description Returns the first DOM element that matches the CSS query selector. 48 | * 49 | * @param query CSS selector string. 50 | * @param ele The Element to start the search from. 51 | * @returns The first Element in DOM order that matches the CSS selector. 52 | */ 53 | export function $$( 54 | query: string, 55 | ele: Element | Document = document 56 | ): E | null { 57 | return ele.querySelector(query); 58 | } 59 | 60 | /** 61 | * Find the first parent of 'ele' with the given 'nodeName'. 62 | * 63 | * @param ele - The element to start searching a. 64 | * @param nodeName - The node name we are looking for. 65 | * @returns Either 'ele' or the first parent of 'ele' that has the nodeName of 'nodeName'. Returns null if none are found. 66 | * 67 | * @example 68 | * 69 | * findParent(ele, 'DIV') 70 | * 71 | */ 72 | export function findParent( 73 | ele: HTMLElement | null, 74 | nodeName: string 75 | ): HTMLElement | null { 76 | while (ele !== null) { 77 | if (ele.nodeName === nodeName) { 78 | return ele; 79 | } 80 | ele = ele.parentElement; 81 | } 82 | return null; 83 | } 84 | 85 | /** 86 | * Find the first parent of 'ele' with the given 'nodeName'. Just like findParent, but TypeScript typesafe. 87 | * 88 | * @param ele - The element to start searching a. 89 | * @param nodeName - The lower-case node name we are looking for, e.g. 'div'. 90 | * @returns Either 'ele' or the first parent of 'ele' that has the nodeName of 'nodeName'. Returns null if none are found. 91 | * 92 | * @example 93 | * 94 | * findParentSafe(ele, 'div') 95 | * 96 | */ 97 | export function findParentSafe( 98 | ele: HTMLElement | null, 99 | nodeName: K 100 | ): HTMLElementTagNameMap[K] | null { 101 | while (ele !== null) { 102 | if (ele.nodeName.toLowerCase() === nodeName) { 103 | return ele as HTMLElementTagNameMap[K]; 104 | } 105 | ele = ele.parentElement; 106 | } 107 | return null; 108 | } 109 | -------------------------------------------------------------------------------- /src/dom_test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { $, $$, findParent, findParentSafe } from './dom'; 16 | 17 | const container = document.createElement('div'); 18 | document.body.appendChild(container); 19 | 20 | beforeEach(() => { 21 | container.innerHTML = ` 22 |
23 | 24 | 25 | 26 | `; 27 | }); 28 | 29 | afterEach(() => { 30 | container.innerHTML = ''; 31 | }); 32 | 33 | const assert = chai.assert; 34 | 35 | describe('$ aka querySelectorAll', () => { 36 | // checks that each "array-like" thing has 37 | // the same things in the same indices. 38 | function assertEquals(arr: T[], qsa: NodeListOf) { 39 | assert.isOk(arr); 40 | assert.equal(arr.length, qsa.length); 41 | for (let i = 0; i < arr.length; i++) { 42 | assert.equal(arr[i], qsa[i]); 43 | } 44 | } 45 | 46 | it('should mimic querySelectorAll', () => { 47 | assertEquals($('.alpha', container), container.querySelectorAll('.alpha')); 48 | assertEquals( 49 | $('#epsilon', container), 50 | container.querySelectorAll('#epsilon') 51 | ); 52 | assertEquals($('span', container), container.querySelectorAll('span')); 53 | }); 54 | 55 | it('should default to document', () => { 56 | assertEquals($('.alpha'), document.querySelectorAll('.alpha')); 57 | assertEquals($('#epsilon'), document.querySelectorAll('#epsilon')); 58 | assertEquals($('span'), document.querySelectorAll('span')); 59 | }); 60 | 61 | it('should return a real array', () => { 62 | const arr = $('.alpha'); 63 | assert.isTrue(Array.isArray(arr)); 64 | }); 65 | 66 | it('returns empty array if not found', () => { 67 | const arr = $('#not-found'); 68 | assert.deepEqual([], arr); 69 | }); 70 | }); 71 | 72 | describe('$$ aka querySelector', () => { 73 | it('should mimic querySelector', () => { 74 | assert.equal($$('.alpha', container), container.querySelector('.alpha')); 75 | assert.equal( 76 | $$('#epsilon', container), 77 | container.querySelector('#epsilon') 78 | ); 79 | assert.equal($$('span', container), container.querySelector('span')); 80 | }); 81 | 82 | it('should default to document', () => { 83 | assert.equal($$('.alpha'), document.querySelector('.alpha')); 84 | assert.equal($$('#epsilon'), document.querySelector('#epsilon')); 85 | assert.equal($$('span'), document.querySelector('span')); 86 | }); 87 | 88 | it('returns a single item', () => { 89 | const ele = $$('.alpha'); 90 | assert.isFalse(Array.isArray(ele)); 91 | assert.isNotNull(ele); 92 | if (ele !== null) { 93 | assert.equal('delta', ele.id); 94 | } 95 | }); 96 | }); 97 | 98 | describe('findParent', () => { 99 | it('identifies the correct parent element', () => { 100 | // Add an HTML tree to the document. 101 | const div = document.createElement('div'); 102 | div.innerHTML = ` 103 |
104 |

105 | span 106 | span 107 |

108 | 109 |

para

110 |
111 |
112 |

para

113 |
114 |
115 |
116 |

para

117 |
118 | 119 | 120 |

para

121 |
122 |
`; 123 | assert.equal(findParent($$('#a', div), 'DIV'), $$('#a', div), 'Top level'); 124 | assert.equal(findParent($$('#a', div), 'SPAN'), null); 125 | assert.equal(findParent($$('#aa', div), 'DIV'), $$('#a', div)); 126 | assert.equal(findParent($$('#aaa', div), 'DIV'), $$('#a', div)); 127 | assert.equal(findParent($$('#aaa', div), 'P'), $$('#aa', div)); 128 | assert.equal(findParent($$('#aab', div), 'SPAN'), $$('#aab', div)); 129 | assert.equal(findParent($$('#ab', div), 'P'), null); 130 | assert.equal(findParent($$('#aba', div), 'SPAN'), $$('#ab', div)); 131 | assert.equal(findParent($$('#ac', div), 'DIV'), $$('#ac', div)); 132 | assert.equal(findParent($$('#aca', div), 'DIV'), $$('#ac', div)); 133 | assert.equal(findParent($$('#ba', div), 'DIV'), $$('#b', div)); 134 | assert.equal(findParent($$('#caa', div), 'DIV'), div); 135 | assert.equal(findParent($$('#ca', div), 'SPAN'), $$('#ca', div)); 136 | assert.equal(findParent($$('#caa', div), 'SPAN'), $$('#ca', div)); 137 | }); 138 | }); 139 | 140 | describe('findParentSafe', () => { 141 | it('identifies the correct parent element', () => { 142 | // Add an HTML tree to the document. 143 | const div = document.createElement('div'); 144 | div.innerHTML = ` 145 |
146 |

147 | span 148 | span 149 |

150 | 151 |

para

152 |
153 |
154 |

para

155 |
156 |
157 |
158 |

para

159 |
160 | 161 | 162 |

para

163 |
164 |
`; 165 | assert.equal( 166 | findParentSafe($$('#a', div), 'div'), 167 | $$('#a', div), 168 | 'Top level' 169 | ); 170 | assert.equal(findParentSafe($$('#a', div), 'span'), null); 171 | assert.equal(findParentSafe($$('#aa', div), 'div'), $$('#a', div)); 172 | assert.equal(findParentSafe($$('#aaa', div), 'div'), $$('#a', div)); 173 | assert.equal(findParentSafe($$('#aaa', div), 'p'), $$('#aa', div)); 174 | assert.equal(findParentSafe($$('#aab', div), 'span'), $$('#aab', div)); 175 | assert.equal(findParentSafe($$('#ab', div), 'p'), null); 176 | assert.equal(findParentSafe($$('#aba', div), 'span'), $$('#ab', div)); 177 | assert.equal(findParentSafe($$('#ac', div), 'div'), $$('#ac', div)); 178 | assert.equal(findParentSafe($$('#aca', div), 'div'), $$('#ac', div)); 179 | assert.equal(findParentSafe($$('#ba', div), 'div'), $$('#b', div)); 180 | assert.equal(findParentSafe($$('#caa', div), 'div'), div); 181 | assert.equal(findParentSafe($$('#ca', div), 'span'), $$('#ca', div)); 182 | assert.equal(findParentSafe($$('#caa', div), 'span'), $$('#ca', div)); 183 | }); 184 | }); 185 | -------------------------------------------------------------------------------- /src/hintable.ts: -------------------------------------------------------------------------------- 1 | // Hintable is the set of types that we can de/serialize with hints. 2 | // 3 | // When we deserialize objects from a typeless format, such as a query string, 4 | // we can use a hint object to figure out how to deserialize the value. 5 | // 6 | // For example "a=1" could be deserialized as {a:'1'} or {a:1}, but if we 7 | // provide a hint object, e.g. {a:100}, the deserializer can look at the type of 8 | // the value in the hint and use that to guide the deserialization to correctly 9 | // choose {a:1}. 10 | export type Hintable = number | boolean | string | any[] | HintableObject; 11 | 12 | // HintableObject is any object with strings for keys and only contains Hintable 13 | // values. 14 | export type HintableObject = { [key: string]: Hintable }; 15 | -------------------------------------------------------------------------------- /src/human.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /** @module common-sk/modules/human 16 | * @description Utitities for working with human friendly I/O. 17 | */ 18 | 19 | interface Delta { 20 | readonly units: string; 21 | readonly delta: number; 22 | } 23 | 24 | const TIME_DELTAS: Delta[] = [ 25 | { units: 'w', delta: 7 * 24 * 60 * 60 }, 26 | { units: 'd', delta: 24 * 60 * 60 }, 27 | { units: 'h', delta: 60 * 60 }, 28 | { units: 'm', delta: 60 }, 29 | { units: 's', delta: 1 }, 30 | ]; 31 | 32 | /** @constant {number} */ 33 | export const KB = 1024; 34 | /** @constant {number} */ 35 | export const MB = KB * 1024; 36 | /** @constant {number} */ 37 | export const GB = MB * 1024; 38 | /** @constant {number} */ 39 | export const TB = GB * 1024; 40 | /** @constant {number} */ 41 | export const PB = TB * 1024; 42 | 43 | const BYTES_DELTAS: Delta[] = [ 44 | { units: ' PB', delta: PB }, 45 | { units: ' TB', delta: TB }, 46 | { units: ' GB', delta: GB }, 47 | { units: ' MB', delta: MB }, 48 | { units: ' KB', delta: KB }, 49 | { units: ' B', delta: 1 }, 50 | ]; 51 | 52 | /** Left pad a number with 0s. 53 | * 54 | * @param num - The number to pad. 55 | * @param size - The number of digits to pad out to. 56 | */ 57 | export function pad(num: number, size: number): string { 58 | let str = num + ''; 59 | while (str.length < size) { 60 | str = '0' + str; 61 | } 62 | return str; 63 | } 64 | 65 | /** 66 | * Returns a human-readable format of the given duration in seconds. 67 | * For example, 'strDuration(123)' would return "2m 3s". 68 | * Negative seconds is treated the same as positive seconds. 69 | * 70 | * @param seconds - The duration. 71 | */ 72 | export function strDuration(seconds: number): string { 73 | if (seconds < 0) { 74 | seconds = -seconds; 75 | } 76 | if (seconds === 0) { 77 | return ' 0s'; 78 | } 79 | let rv = ''; 80 | for (const td of TIME_DELTAS) { 81 | if (td.delta <= seconds) { 82 | let s = Math.floor(seconds / td.delta) + td.units; 83 | while (s.length < 4) { 84 | s = ' ' + s; 85 | } 86 | rv += s; 87 | seconds = seconds % td.delta; 88 | } 89 | } 90 | return rv; 91 | } 92 | 93 | /** 94 | * Returns the difference between the current time and 's' as a string in a 95 | * human friendly format. If 's' is a number it is assumed to contain the time 96 | * in milliseconds otherwise it is assumed to contain a time string parsable 97 | * by Date.parse(). 98 | * 99 | * For example, a difference of 123 seconds between 's' and the current time 100 | * would return "2m". 101 | * 102 | * @param milliseconds - The time in milliseconds or a time string. 103 | * @param now - The time to diff against, if not supplied then the diff 104 | * is done against Date.now(). 105 | */ 106 | export function diffDate(s: number | string, now?: number): string { 107 | if (now === undefined) { 108 | now = Date.now(); 109 | } 110 | const ms = typeof s === 'number' ? s : Date.parse(s); 111 | let diff = (ms - now) / 1000; 112 | if (diff < 0) { 113 | diff = -1.0 * diff; 114 | } 115 | return humanize(diff, TIME_DELTAS); 116 | } 117 | 118 | /** 119 | * Formats the amount of bytes in a human friendly format. 120 | * unit may be supplied to indicate b is not in bytes, but in something 121 | * like kilobytes (KB) or megabytes (MB) 122 | * 123 | * @example 124 | * // returns "1 KB" 125 | * bytes(1234) 126 | * @example 127 | * // returns "5 GB" 128 | * bytes(5321, MB) 129 | * 130 | * @param b - The number of bytes in units 'unit'. 131 | * @param unit - The number of bytes per unit. 132 | */ 133 | export function bytes(b: number, unit: number = 1): string { 134 | return humanize(b * unit, BYTES_DELTAS); 135 | } 136 | 137 | /** localeTime formats the provided Date object in locale time and appends the timezone to the end. 138 | * 139 | * @param date The date to format. 140 | */ 141 | export function localeTime(date: Date): string { 142 | // caching timezone could be buggy, especially if times from a wide range 143 | // of dates are used. The main concern would be crossing over Daylight 144 | // Savings time and having some times be erroneously in EST instead of 145 | // EDT, for example 146 | const str = date.toString(); 147 | const timezone = str.substring(str.indexOf('(')); 148 | return date.toLocaleString() + ' ' + timezone; 149 | } 150 | 151 | function humanize(n: number, deltas: Delta[]) { 152 | for (let i = 0; i < deltas.length - 1; i++) { 153 | // If n would round to '60s', return '1m' instead. 154 | const nextDeltaRounded = 155 | Math.round(n / deltas[i + 1].delta) * deltas[i + 1].delta; 156 | if (nextDeltaRounded / deltas[i].delta >= 1) { 157 | return Math.round(n / deltas[i].delta) + deltas[i].units; 158 | } 159 | } 160 | const index = deltas.length - 1; 161 | return Math.round(n / deltas[index].delta) + deltas[index].units; 162 | } 163 | -------------------------------------------------------------------------------- /src/human_test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import * as human from './human'; 16 | 17 | const assert = chai.assert; 18 | 19 | describe('The human functions', () => { 20 | function testPad() { 21 | const testCases: [number, number, string][] = [ 22 | [0, 0, '0'], 23 | [1, 1, '1'], 24 | [10, 1, '10'], 25 | [10, 2, '10'], 26 | [10, 3, '010'], 27 | [31558150, 8, '31558150'], 28 | [31558150, 9, '031558150'], 29 | ]; 30 | for (const testCase of testCases) { 31 | assert.equal(human.pad(testCase[0], testCase[1]), testCase[2]); 32 | } 33 | } 34 | 35 | it('should return padded integers from pad', testPad); 36 | 37 | function testStrDuration() { 38 | const testCases: [number, string][] = [ 39 | [0, ' 0s'], 40 | [1, ' 1s'], 41 | [-1, ' 1s'], 42 | [2, ' 2s'], 43 | [10, ' 10s'], 44 | [-30, ' 30s'], 45 | [59, ' 59s'], 46 | [60, ' 1m'], 47 | [-61, ' 1m 1s'], 48 | [123, ' 2m 3s'], 49 | [3599, ' 59m 59s'], 50 | [3600, ' 1h'], 51 | [3601, ' 1h 1s'], 52 | [3659, ' 1h 59s'], 53 | [3660, ' 1h 1m'], 54 | [3661, ' 1h 1m 1s'], 55 | [86399, ' 23h 59m 59s'], 56 | [86400, ' 1d'], 57 | [86401, ' 1d 1s'], 58 | [604799, ' 6d 23h 59m 59s'], 59 | [604800, ' 1w'], 60 | [31558150, ' 52w 1d 6h 9m 10s'], 61 | ]; 62 | for (const testCase of testCases) { 63 | assert.equal(human.strDuration(testCase[0]), testCase[1]); 64 | } 65 | } 66 | 67 | it('should return human-readable duration from strDuration', testStrDuration); 68 | 69 | function testDiffDate() { 70 | const now = 1584972056 * 1000; // 03/23/2020 @ 2:00pm (UTC) 71 | const testCases: [number, string][] = [ 72 | [0, '0s'], // 0s 73 | [1, '0s'], // 0.001s 74 | [499, '0s'], // 0.499s 75 | [500, '1s'], // 0.5s 76 | [-1000, '1s'], // 1s 77 | [1000, '1s'], // 1s 78 | [2000, '2s'], // 2s 79 | [9800, '10s'], // 9.8s 80 | [-10000, '10s'], // 10s 81 | [-30000, '30s'], // 30s 82 | [59000, '59s'], // 59s 83 | [59499, '59s'], // 59.499s 84 | [59500, '1m'], // 59.5s 85 | [60000, '1m'], // 1m 00s 86 | [-61000, '1m'], // 1m 01s 87 | [123000, '2m'], // 2m 03s 88 | [3569000, '59m'], // 59m 29s 89 | [3570000, '1h'], // 59m 30s 90 | [3600000, '1h'], // 1h 00m 00s 91 | [-3601000, '1h'], // 1h 00m 01s 92 | [3659000, '1h'], // 1h 00m 59s 93 | [-3660000, '1h'], // 1h 01m 00s 94 | [5398000, '1h'], // 1h 29m 58s 95 | [5400000, '2h'], // 1h 30m 00s 96 | [-84599000, '23h'], // 23h 29m 59s 97 | [-84600000, '1d'], // 23h 30m 00s 98 | [-86399000, '1d'], // 23h 59m 59s 99 | [86400000, '1d'], // 1d 00h 00m 00s 100 | [-561599000, '6d'], // 6d 11h 59m 59s 101 | [-561600000, '1w'], // 6d 12h 00m 00s 102 | [604800000, '1w'], // 1w 0d 00h 00m 00s 103 | [31558150000, '52w'], // 52w 1d 06h 09m 10s 104 | ]; 105 | for (const testCase of testCases) { 106 | const diffMs = testCase[0]; 107 | const expected = testCase[1]; 108 | const ms = now + diffMs; 109 | // Test the form of diffDate that takes a number. 110 | assert.equal( 111 | human.diffDate(ms, now), 112 | expected, 113 | 'Input is ' + ms + ', now is ' + now 114 | ); 115 | // Test the form of diffDate that takes a date string. 116 | assert.equal( 117 | human.diffDate(new Date(ms).toISOString(), now), 118 | expected, 119 | 'Input is ' + 120 | new Date(ms).toISOString() + 121 | ', now is ' + 122 | new Date(now).toISOString() 123 | ); 124 | } 125 | } 126 | 127 | it('should return human-readable duration from diffDate', testDiffDate); 128 | 129 | function testBytes() { 130 | const testBytesTestCases: [number, string][] = [ 131 | [0, '0 B'], // 0 B 132 | [1, '1 B'], // 1 B 133 | [499, '499 B'], // 499 B 134 | [500, '500 B'], // 500 B 135 | [1000, '1000 B'], // 1000 B 136 | [1234, '1 KB'], // 1 KB 210 B 137 | [2000, '2 KB'], // 1 KB 976 B 138 | [9727, '9 KB'], // 9 KB 511 B 139 | [9728, '10 KB'], // 9 KB 512 B 140 | [30000, '29 KB'], // 29 KB 304 B 141 | [1024000, '1000 KB'], // 1000 KB 000 B 142 | [1048500, '1 MB'], // 1023 KB 948 B 143 | [1048576, '1 MB'], // 1 MB 000 KB 000 B 144 | [1048577, '1 MB'], // 1 MB 000 KB 001 B 145 | [300000000, '286 MB'], // 286 MB 104 KB 768 B 146 | [1072693248, '1023 MB'], // 1023 MB 000 KB 000 B 147 | [1073741300, '1 GB'], // 1023 MB1023 KB 999 B 148 | [1073741824, '1 GB'], // 1 GB 000 MB 000 KB 000 B 149 | [1073741825, '1 GB'], // 1 GB 000 MB 000 KB 001 B 150 | ]; 151 | for (const tb of testBytesTestCases) { 152 | const b = tb[0]; 153 | const expected = tb[1]; 154 | assert.equal( 155 | human.bytes(b), 156 | expected, 157 | 'Input is ' + b + ', Unit is bytes' 158 | ); 159 | } 160 | const testMB: [number, string][] = [ 161 | [0, '0 B'], // 0 MB 162 | [1, '1 MB'], // 1 MB 163 | [499, '499 MB'], // 499 MB 164 | [500, '500 MB'], // 500 MB 165 | [1000, '1000 MB'], // 1000 MB 166 | [1234, '1 GB'], // 1 GB 210 MB 167 | [2000, '2 GB'], // 1 GB 976 MB 168 | [9727, '9 GB'], // 9 GB 511 MB 169 | [9728, '10 GB'], // 9 GB 512 MB 170 | [30000, '29 GB'], // 29 GB 304 MB 171 | [1024000, '1000 GB'], // 1000 GB 000 MB 172 | [1048500, '1 TB'], // 1023 GB 948 MB 173 | [1048576, '1 TB'], // 1 TB 000 GB 000 MB 174 | [1048577, '1 TB'], // 1 TB 000 GB 001 MB 175 | [300000000, '286 TB'], // 286 TB 104 GB 768 MB 176 | [1072693248, '1023 TB'], // 1023 TB 000 GB 000 MB 177 | [1073741300, '1 PB'], // 1023 TB1023 GB 999 MB 178 | [1073741824, '1 PB'], // 1 PB 000 TB 000 GB 000 MB 179 | [1073741825, '1 PB'], // 1 PB 000 TB 000 GB 001 MB 180 | ]; 181 | for (const tm of testMB) { 182 | const b = tm[0]; 183 | const expected = tm[1]; 184 | assert.equal( 185 | human.bytes(b, human.MB), 186 | expected, 187 | 'Input is ' + b + ', Unit is Megabytes' 188 | ); 189 | } 190 | } 191 | 192 | it('should return human-readable bytes from bytes', testBytes); 193 | }); 194 | -------------------------------------------------------------------------------- /src/jsonOrThrow.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /** @module common-sk/modules/jsonOrThrow */ 16 | 17 | // ** The type of Error thrown by jsonOrThrow. */ 18 | export class JsonOrThrowError extends Error { 19 | message: string; 20 | resp: Response; 21 | status: number; 22 | 23 | constructor(resp: Response) { 24 | super(); 25 | this.message = `Bad network response: ${resp.statusText}`; 26 | this.resp = resp; 27 | this.status = resp.status; 28 | } 29 | } 30 | 31 | /** Helper function when making fetch() requests. 32 | * 33 | * Checks if the response is ok and converts it to JSON, otherwise it throws. 34 | * 35 | * @example 36 | * 37 | * fetch('/_/list').then(jsonOrThrow).then((json) => { 38 | * // Do something with the parsed json here. 39 | * }).catch((r) => { 40 | * if (r.status === 403) { 41 | * // Handle HTTP response 403 - not authorized here. 42 | * } else { 43 | * console.err(r.message); 44 | * } 45 | * }); 46 | * 47 | * @throws A JsonOrThrowErrr. See the [Response docs]{@link https://developer.mozilla.org/en-US/docs/Web/API/Response } 48 | * for more detail on reading resp (e.g. resp.text()). 49 | */ 50 | export function jsonOrThrow(resp: Response) { 51 | if (resp.ok) { 52 | return resp.json(); 53 | } 54 | throw new JsonOrThrowError(resp); 55 | } 56 | -------------------------------------------------------------------------------- /src/object.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /** @module common-sk/modules/object 16 | * @description Utility functions for dealing with Objects. 17 | */ 18 | import { fromObject } from './query'; 19 | import { Hintable, HintableObject } from './hintable'; 20 | 21 | /** @method deepCopy 22 | * @param object - The object to make a copy of. 23 | */ 24 | export function deepCopy(o: T): T { 25 | return JSON.parse(JSON.stringify(o)); 26 | } 27 | 28 | /** Returns true if a and b are equal, covers Boolean, Number, String and Arrays and Objects. 29 | * 30 | * @param a The Hintable type object to compare. 31 | * @param b The Hintable type object to compare. 32 | */ 33 | export function equals(a: Hintable, b: Hintable): boolean { 34 | if (typeof a !== typeof b) { 35 | return false; 36 | } 37 | const ta = typeof a; 38 | if (ta === 'string' || ta === 'boolean' || ta === 'number') { 39 | return a === b; 40 | } 41 | if (ta === 'object') { 42 | if (Array.isArray(a)) { 43 | return JSON.stringify(a) === JSON.stringify(b); 44 | } 45 | return fromObject(a as HintableObject) === fromObject(b as HintableObject); 46 | } 47 | return false; 48 | } 49 | 50 | /** Returns an object with only values that are in o that are different from d. 51 | * 52 | * Only works shallowly, i.e. only diffs on the attributes of 53 | * o and d, and only for the types that equals() supports. 54 | * 55 | * @example 56 | * // Returns {a:2} 57 | * getDelta({a:2, b:"foo"}, {a:1, b:"foo", c:3.14}) 58 | * 59 | * @param {Object} o 60 | * @param {Object} d 61 | * @returns {Object} 62 | * 63 | */ 64 | export function getDelta(o: HintableObject, d: HintableObject): HintableObject { 65 | const ret: HintableObject = {}; 66 | Object.keys(o).forEach((key) => { 67 | if (!equals(o[key], d[key])) { 68 | ret[key] = o[key]; 69 | } 70 | }); 71 | return ret; 72 | } 73 | 74 | /** Returns a copy of object o with values from delta if they exist. 75 | * 76 | * @param {Object} delta - A delta object as returned from 'getDelta'. 77 | * @param {Object} o 78 | * @returns {Object} 79 | * 80 | */ 81 | export function applyDelta( 82 | delta: HintableObject, 83 | o: HintableObject 84 | ): HintableObject { 85 | const ret: HintableObject = {}; 86 | Object.keys(o).forEach((key) => { 87 | if (delta.hasOwnProperty(key)) { 88 | ret[key] = deepCopy(delta[key]); 89 | } else { 90 | ret[key] = deepCopy(o[key]); 91 | } 92 | }); 93 | return ret; 94 | } 95 | -------------------------------------------------------------------------------- /src/object_test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import * as object from './object'; 16 | import { HintableObject } from './hintable'; 17 | 18 | const assert = chai.assert; 19 | 20 | describe('object functions', () => { 21 | function testGetDelta() { 22 | const test = ( 23 | o: HintableObject, 24 | d: HintableObject, 25 | expected: HintableObject 26 | ) => { 27 | assert.deepEqual(object.getDelta(o, d), expected); 28 | }; 29 | test({}, {}, {}); 30 | test({ a: 'foo' }, { a: 'foo' }, {}); 31 | const first = {}; // Ensure getDelta does not modify its arguments. 32 | test(first, { a: 'foo' }, {}); 33 | assert.deepEqual(first, {}); 34 | const second = {}; // Ensure getDelta does not modify its arguments. 35 | test({ a: 'foo' }, second, { a: 'foo' }); 36 | assert.deepEqual(second, {}); 37 | test({ a: 'foo' }, { a: 'bar' }, { a: 'foo' }); 38 | test({ a: 'foo', b: 'bar' }, { a: true, c: 'bar' }, { a: 'foo', b: 'bar' }); 39 | } 40 | 41 | function testApplyDelta() { 42 | const test = ( 43 | delta: HintableObject, 44 | o: HintableObject, 45 | expected: HintableObject 46 | ) => { 47 | assert.deepEqual(object.applyDelta(delta, o), expected); 48 | }; 49 | test({}, {}, {}); 50 | test({}, { a: 'foo' }, { a: 'foo' }); 51 | const first = { a: 'bar' }; // Ensure applyDelta does not modify its arguments. 52 | test(first, { a: 'foo' }, { a: 'bar' }); 53 | assert.deepEqual(first, { a: 'bar' }); 54 | const second = { a: 'bar' }; // Ensure applyDelta does not modify its arguments. 55 | test({ a: 'foo' }, second, { a: 'foo' }); 56 | assert.deepEqual(second, { a: 'bar' }); 57 | test({ a: 'foo' }, { a: 'bar', b: 'baz' }, { a: 'foo', b: 'baz' }); 58 | test({ a: 'foo', b: 'baz' }, { a: 'bar' }, { a: 'foo' }); 59 | test({ a: 'foo', b: 'bar' }, { a: true, c: 'bar' }, { a: 'foo', c: 'bar' }); 60 | } 61 | 62 | function testEquals() { 63 | assert.isTrue(object.equals(1, 1)); 64 | assert.isTrue(object.equals([1, 2], [1, 2])); 65 | assert.isTrue(object.equals([], [])); 66 | assert.isFalse(object.equals([1], [])); 67 | } 68 | 69 | it('should be able get differences and apply differences', () => { 70 | testGetDelta(); 71 | testApplyDelta(); 72 | testEquals(); 73 | }); 74 | 75 | it('should be able to make deep copies of objects', () => { 76 | const a = { 77 | a: 1, 78 | b: 'two', 79 | c: ['three'], 80 | d: { four: '' }, 81 | }; 82 | assert.deepEqual(a, object.deepCopy(a)); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /src/query.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /** @module common-sk/modules/query 16 | * @descripiton Utilities for working with URL query strings. 17 | */ 18 | 19 | import { HintableObject } from './hintable'; 20 | 21 | /** ParamSet mirrors //infra/go/paramtools ParamSet. */ 22 | export type ParamSet = { [key: string]: string[] }; 23 | 24 | /** fromParamSet encodes an object of the form: 25 | *
 26 |  * {
 27 |  *   a:["2", "4"],
 28 |  *   b:["3"]
 29 |  * }
 30 |  * 
31 | * 32 | * to a query string like: 33 | * 34 | *
 35 |  * "a=2&a=4&b=3"
 36 |  * 
37 | * 38 | * This function handles URI encoding of both keys and values. 39 | * 40 | * @param {Object} o The object to encode. 41 | * @returns {string} 42 | */ 43 | export function fromParamSet(o: ParamSet): string { 44 | if (!o) { 45 | return ''; 46 | } 47 | const ret: string[] = []; 48 | const keys = Object.keys(o).sort(); 49 | keys.forEach((key) => { 50 | o[key].forEach((value) => { 51 | ret.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`); 52 | }); 53 | }); 54 | return ret.join('&'); 55 | } 56 | 57 | /** toParamSet parses a query string into an object with 58 | * arrays of values for the values. I.e. 59 | * 60 | *
 61 |  *   "a=2&b=3&a=4"
 62 |  * 
63 | * 64 | * decodes to 65 | * 66 | *
 67 |  *   {
 68 |  *     a:["2", "4"],
 69 |  *     b:["3"],
 70 |  *   }
 71 |  * 
72 | * 73 | * This function handles URI decoding of both keys and values. 74 | * 75 | * @param {string} s The query string to decode. 76 | * @returns {ParamSet} 77 | */ 78 | export function toParamSet(s: string): ParamSet { 79 | s = s || ''; 80 | const ret: ParamSet = {}; 81 | const vars = s.split('&'); 82 | for (const v of vars) { 83 | const pair = v.split('=', 2); 84 | if (pair.length === 2) { 85 | const key = decodeURIComponent(pair[0]); 86 | const value = decodeURIComponent(pair[1]); 87 | if (ret.hasOwnProperty(key)) { 88 | ret[key].push(value); 89 | } else { 90 | ret[key] = [value]; 91 | } 92 | } 93 | } 94 | return ret; 95 | } 96 | 97 | /** fromObject takes an object and encodes it into a query string. 98 | * 99 | * The reverse of this function is toObject. 100 | * 101 | * @param o - The object to encode. 102 | */ 103 | export function fromObject(o: HintableObject): string { 104 | const ret: string[] = []; 105 | Object.keys(o) 106 | .sort() 107 | .forEach((key) => { 108 | const value = o[key]; 109 | if (Array.isArray(value)) { 110 | value.forEach((v: string) => { 111 | ret.push(`${encodeURIComponent(key)}=${encodeURIComponent(v)}`); 112 | }); 113 | } else if (typeof value === 'object') { 114 | ret.push( 115 | `${encodeURIComponent(key)}=${encodeURIComponent(fromObject(value))}` 116 | ); 117 | } else { 118 | ret.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`); 119 | } 120 | }); 121 | return ret.join('&'); 122 | } 123 | 124 | /** toObject decodes a query string into an object. 125 | * 126 | * Uses the 'target' as a source for hinting on the types of the values. 127 | * For example: 128 | * 129 | *
130 |  *   "a=2&b=true"
131 |  * 
132 | * 133 | * decodes to: 134 | * 135 | *
136 |  *   {
137 |  *     a: 2,
138 |  *     b: true,
139 |  *   }
140 |  * 
141 | * 142 | * When given a target of: 143 | * 144 | *
145 |  *   {
146 |  *     a: 1.0,
147 |  *     b: false,
148 |  *   }
149 |  * 
150 | * 151 | * Note that a target of {} would decode 152 | * the same query string into: 153 | * 154 | *
155 |  *   {
156 |  *     a: "2",
157 |  *     b: "true",
158 |  *   }
159 |  * 
160 | * 161 | * Only Number, String, Boolean, Object, and Array of String hints are supported. 162 | * 163 | * @param s - The query string. 164 | * @param target - The object that contains the type hints. 165 | */ 166 | export function toObject(s: string, target: HintableObject): HintableObject { 167 | target = target || {}; 168 | const ret: { [key: string]: any } = {}; 169 | const vars = s.split('&'); 170 | for (const v of vars) { 171 | const pair = v.split('=', 2); 172 | if (pair.length === 2) { 173 | const key = decodeURIComponent(pair[0]); 174 | const value = decodeURIComponent(pair[1]); 175 | if (target.hasOwnProperty(key)) { 176 | const targetValue = target[key]; 177 | switch (typeof targetValue) { 178 | case 'boolean': 179 | ret[key] = value === 'true'; 180 | break; 181 | case 'number': 182 | ret[key] = Number(value); 183 | break; 184 | case 'object': // Arrays report as 'object' to typeof. 185 | if (Array.isArray(targetValue)) { 186 | const r = ret[key] || []; 187 | r.push(value); 188 | ret[key] = r; 189 | } else { 190 | ret[key] = toObject(value, targetValue); 191 | } 192 | break; 193 | case 'string': 194 | ret[key] = value; 195 | break; 196 | default: 197 | ret[key] = value; 198 | } 199 | } else { 200 | ret[key] = value; 201 | } 202 | } 203 | } 204 | return ret; 205 | } 206 | 207 | /** splitAmp returns the given query string as a newline 208 | * separated list of key value pairs. If sepator is not 209 | * provided newline will be used. 210 | * 211 | * @param [queryStr=''] A query string. 212 | * @param [separator='\n'] The separator to use when joining. 213 | */ 214 | export function splitAmp(queryStr = '', separator = '\n') { 215 | return queryStr.split('&').join(separator); 216 | } 217 | -------------------------------------------------------------------------------- /src/query_test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import * as query from './query'; 16 | 17 | const assert = chai.assert; 18 | 19 | describe('Test query encoding and decoding functions.', () => { 20 | function testEncode() { 21 | assert.equal(query.fromObject({}), ''); 22 | assert.equal(query.fromObject({ a: 2 }), 'a=2'); 23 | assert.equal(query.fromObject({ a: '2' }), 'a=2'); 24 | assert.equal(query.fromObject({ a: '2 3' }), 'a=2%203'); 25 | assert.equal(query.fromObject({ 'a b': '2 3' }), 'a%20b=2%203'); 26 | assert.equal(query.fromObject({ a: [2, 3] }), 'a=2&a=3'); 27 | assert.equal(query.fromObject({ a: ['2', '3'] }), 'a=2&a=3'); 28 | assert.equal(query.fromObject({ a: [] }), ''); 29 | assert.equal(query.fromObject({ a: { b: '3' } }), 'a=b%3D3'); 30 | assert.equal(query.fromObject({ a: { b: '3' }, b: '3' }), 'a=b%3D3&b=3'); 31 | assert.equal(query.fromObject({ a: {}, b: '3' }), 'a=&b=3'); 32 | assert.equal( 33 | query.fromObject({ a: { b: { c: 'foo bar' } } }), 34 | 'a=b%3Dc%253Dfoo%252520bar' 35 | ); 36 | assert.isTrue( 37 | ['a=2&b=3', 'b=3&a=2'].indexOf(query.fromObject({ a: 2, b: 3 })) !== -1 38 | ); 39 | } 40 | 41 | function testDecodeToObject() { 42 | assert.deepEqual(query.toObject('', {}), {}); 43 | assert.deepEqual(query.toObject('a=2', {}), { a: '2' }); 44 | assert.deepEqual(query.toObject('a=2', { a: 'foo' }), { a: '2' }); 45 | assert.deepEqual(query.toObject('a=2', { a: 1.0 }), { a: 2 }); 46 | assert.deepEqual(query.toObject('a=true', { a: false }), { a: true }); 47 | assert.deepEqual(query.toObject('a=true', { a: 'bar' }), { a: 'true' }); 48 | assert.deepEqual(query.toObject('a=false', { a: false }), { a: false }); 49 | assert.deepEqual(query.toObject('a=baz', { a: 2.0 }), { a: NaN }); 50 | assert.deepEqual(query.toObject('a=true&a=false', { a: [] }), { 51 | a: ['true', 'false'] 52 | }); 53 | assert.deepEqual(query.toObject('a=true%20false', { a: [] }), { 54 | a: ['true false'] 55 | }); 56 | assert.deepEqual( 57 | query.toObject('b=1&a=true%20false&b=2.2', { a: [], b: [] }), 58 | { a: ['true false'], b: ['1', '2.2'] } 59 | ); 60 | assert.deepEqual( 61 | query.toObject('a=b%3Dc%253Dfoo%252520bar', { a: { b: { c: '' } } }), 62 | { a: { b: { c: 'foo bar' } } } 63 | ); 64 | 65 | assert.deepEqual(query.toObject('a=2&b=true', { a: 1.0, b: false }), { 66 | a: 2, 67 | b: true 68 | }); 69 | } 70 | 71 | function testRoundTrip() { 72 | const start: any = { 73 | a: 2.0, 74 | b: true, 75 | c: 'foo bar baz', 76 | e: ['foo bar', '2'], 77 | d: ['foo'], 78 | f: { a: 2.0, b: 'foo bar', c: ['a', 'b'] } 79 | }; 80 | const hint: any = { 81 | a: 0, 82 | b: false, 83 | c: 'string', 84 | d: [], 85 | e: [], 86 | f: { a: 1.0, b: 'string', c: [] } 87 | }; 88 | assert.deepEqual(query.toObject(query.fromObject(start), hint), start); 89 | } 90 | 91 | function testDecodeToParamSet() { 92 | assert.deepEqual(query.toParamSet(''), {}); 93 | assert.deepEqual(query.toParamSet('a=2'), { a: ['2'] }); 94 | assert.deepEqual(query.toParamSet('a=2&a=3'), { a: ['2', '3'] }); 95 | assert.deepEqual(query.toParamSet('a=2&a=3&b=foo'), { 96 | a: ['2', '3'], 97 | b: ['foo'] 98 | }); 99 | assert.deepEqual(query.toParamSet('a=2%20'), { a: ['2 '] }); 100 | } 101 | 102 | function testEncodeFromParamSet() { 103 | assert.deepEqual(query.fromParamSet({}), ''); 104 | assert.deepEqual(query.fromParamSet({ a: ['2'] }), 'a=2'); 105 | assert.deepEqual(query.fromParamSet({ a: ['2', '3'] }), 'a=2&a=3'); 106 | assert.deepEqual( 107 | query.fromParamSet({ a: ['2', '3'], b: ['foo'] }), 108 | 'a=2&a=3&b=foo' 109 | ); 110 | assert.deepEqual(query.fromParamSet({ a: ['2 '] }), 'a=2%20'); 111 | } 112 | 113 | it('should be able to encode and decode objects.', () => { 114 | testEncode(); 115 | testDecodeToObject(); 116 | testRoundTrip(); 117 | testDecodeToParamSet(); 118 | testEncodeFromParamSet(); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /src/stateReflector.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /** @module common-sk/modules/stateReflector */ 16 | import * as query from './query'; 17 | import * as object from './object'; 18 | import { DomReady } from './dom'; 19 | import { HintableObject } from './hintable'; 20 | 21 | /** Track the state of an object and reflect it to and from the URL. 22 | * 23 | * @example 24 | * 25 | * // If an element has a private variable _state: 26 | * this._state = {"foo": "bar", "count": 7} 27 | * 28 | * // then in the connectedCallback() call: 29 | * this._stateHasChanged = stateReflector( 30 | * () => this._state, 31 | * (state) => { 32 | * this._state = state; 33 | * this._render(); 34 | * } 35 | * ); 36 | * 37 | * // And then any time the app changes the value of _state: 38 | * this._stateHasChanged(); 39 | * 40 | * @param getState - Function that returns an object representing the state 41 | * we want reflected to the URL. 42 | * 43 | * @param setState(o) - Function to call when the URL has changed and the state 44 | * object needs to be updated. The object 'o' doesn't need to be copied 45 | * as it is a fresh object. 46 | * 47 | * @returns A function to call when state has changed and needs to be reflected 48 | * to the URL. 49 | */ 50 | export function stateReflector( 51 | getState: () => HintableObject, 52 | setState: (o: HintableObject) => void 53 | ): () => void { 54 | // The default state of the stateHolder. Used to calculate diffs to state. 55 | const defaultState = object.deepCopy(getState()); 56 | 57 | // Have we done an initial read from the the existing query params. 58 | let loaded = false; 59 | 60 | // stateFromURL should be called when the URL has changed, it updates 61 | // the state via setState() and triggers the callback. 62 | const stateFromURL = () => { 63 | loaded = true; 64 | const delta = query.toObject(window.location.search.slice(1), defaultState); 65 | setState(object.applyDelta(delta, defaultState)); 66 | }; 67 | 68 | // When we are loaded we should update the state from the URL. 69 | DomReady.then(stateFromURL); 70 | 71 | // Every popstate event should also update the state. 72 | window.addEventListener('popstate', stateFromURL); 73 | 74 | // Return a function to call when the state has changed to force reflection into the URL. 75 | return () => { 76 | // Don't overwrite the query params until we have done the initial load from them. 77 | if (!loaded) { 78 | return; 79 | } 80 | const q = query.fromObject(object.getDelta(getState(), defaultState)); 81 | history.pushState( 82 | null, 83 | '', 84 | window.location.origin + window.location.pathname + '?' + q 85 | ); 86 | }; 87 | } 88 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": false, 4 | "declaration": true, 5 | "declarationMap": true, 6 | "module": "ES6", 7 | "noImplicitAny": true, 8 | "outDir": "./modules/", 9 | "sourceMap": true, 10 | "inlineSources": true, 11 | "strict": true, 12 | "target": "es6", 13 | "types": ["chai", "mocha"] 14 | }, 15 | "include": ["./src/*.ts"] 16 | } 17 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended"], 4 | "jsRules": {}, 5 | "rules": {}, 6 | "rulesDirectory": [] 7 | } 8 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const { glob } = require('glob'); 16 | const commonBuilder = require('pulito'); 17 | 18 | module.exports = (env, argv) => { 19 | const config = commonBuilder(env, argv, __dirname); 20 | 21 | config.entry.tests = glob.sync('./modules/**/*_test.js'); 22 | 23 | // Enable sourcemaps for debugging webpack's output. 24 | config.devtool = 'source-map'; 25 | 26 | return config; 27 | }; 28 | --------------------------------------------------------------------------------