├── .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
Name | 64 |Description | 65 | 66 | ${rows(rowData).join('')} 67 |
---|
105 | span 106 | span 107 |
108 | 109 |para
110 | 111 |para
113 |para
117 |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 |147 | span 148 | span 149 |
150 | 151 |para
152 | 153 |para
155 |para
159 |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 deepCopy26 | * { 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 | --------------------------------------------------------------------------------