@arraypress/extension-kit 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -15
- package/package.json +1 -1
- package/src/build/vendor.js +1 -1
- package/src/dom/i18n.d.ts +11 -3
- package/src/dom/i18n.js +17 -4
- package/src/dom/setup.d.ts +4 -3
- package/src/dom/setup.js +3 -2
- package/src/index.d.ts +3 -2
- package/src/index.js +5 -2
- package/src/sites/sites.d.ts +33 -1
- package/src/sites/sites.js +42 -1
- package/src/testing/browser.js +1 -1
- package/src/testing/e2e.js +1 -1
- package/src/testing/index.d.ts +0 -1
- package/src/testing/index.js +0 -1
- package/src/{testing → time}/time.d.ts +7 -6
- package/src/{testing → time}/time.js +7 -6
- package/src/ui/site-list.d.ts +23 -6
- package/src/ui/site-list.js +46 -6
package/README.md
CHANGED
|
@@ -159,13 +159,25 @@ be public before the store listing goes live.
|
|
|
159
159
|
## Page start-up
|
|
160
160
|
|
|
161
161
|
```js
|
|
162
|
-
const t = setUpPage(chrome, { title: 'optionsTitle'
|
|
162
|
+
const t = setUpPage(chrome, { title: 'optionsTitle' });
|
|
163
163
|
```
|
|
164
164
|
|
|
165
165
|
Reports the page's colour scheme for the toolbar glyph (`watchColorScheme`; a failure is only
|
|
166
166
|
logged), fills its `data-i18n` text (`localize`), sets its title from a message, and returns the
|
|
167
167
|
page's `t`.
|
|
168
168
|
|
|
169
|
+
The extension's name lives in one message, `extShortName`. A message that names the extension
|
|
170
|
+
takes it as a placeholder whose content is `$1`, and every lookup without substitutions (`t(key)`,
|
|
171
|
+
`data-i18n`, `data-i18n-*`, the title) fills it in:
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
"optionsAbout": { "message": "About $NAME$", "placeholders": { "name": { "content": "$1" } } }
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
```html
|
|
178
|
+
<h2 class="section-title" data-i18n="optionsAbout"></h2> <!-- About Upright -->
|
|
179
|
+
```
|
|
180
|
+
|
|
169
181
|
## Sites
|
|
170
182
|
|
|
171
183
|
```js
|
|
@@ -175,11 +187,22 @@ onSite(siteHost(location.href), neverSites); // on a listed site or a subdomain
|
|
|
175
187
|
hostPatterns('github.com'); // ['*://github.com/*', '*://*.github.com/*'] (excludeMatches)
|
|
176
188
|
originPattern('https://bit.ly/x'); // 'https://bit.ly/*' (permissions.request)
|
|
177
189
|
siteLabel('https://example.com'); // 'example.com', as a list shows it
|
|
190
|
+
withSite(['a.com', 'b.com'], 'a.com'); // ['b.com', 'a.com'], what a list keeps after Add (also withoutSite, withSiteReplaced)
|
|
178
191
|
```
|
|
179
192
|
|
|
180
193
|
A site is http or https with a dot in its host (or `localhost`); text without a scheme is https;
|
|
181
194
|
`localhost:8080` is a host and port. Pure, so a content script can load `sites/sites.js` alone.
|
|
182
195
|
|
|
196
|
+
## Time limits
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
const groups = await within(chrome.tabGroups.query({}), 2000, []); // a Chrome call that may never answer
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`within(promise, ms, fallback)`: the value in time, or the fallback (late or failed); never
|
|
203
|
+
rejects; its timer cleared once the work settles (the work itself isn't stopped). In the runtime
|
|
204
|
+
half, so a service worker or page can use it; the test helpers use it too.
|
|
205
|
+
|
|
183
206
|
## Page components
|
|
184
207
|
|
|
185
208
|
Chrome's own components, made in the page by script (no markup to copy), styled by `chrome-ui.css`.
|
|
@@ -247,25 +270,50 @@ restarts the wait: 10 seconds, as chrome://bookmarks's (`TOAST_MS`), or `duratio
|
|
|
247
270
|
### Site list
|
|
248
271
|
|
|
249
272
|
```js
|
|
250
|
-
//
|
|
251
|
-
// <
|
|
252
|
-
// <
|
|
273
|
+
// Memory Saver's way (chrome://settings/performance): an Add button and the dialog, ⋮ Edit/Remove.
|
|
274
|
+
// <div class="row"><span class="label secondary">…</span><button id="add-site" type="button">Add</button></div>
|
|
275
|
+
// <p class="row text" id="no-sites"><span class="label">No sites added</span></p> <ul class="list" id="sites"></ul>
|
|
253
276
|
const sites = createSiteList({
|
|
254
|
-
list: byId('sites'), empty: byId('no-sites'),
|
|
255
|
-
parse:
|
|
256
|
-
add:
|
|
257
|
-
remove: (
|
|
258
|
-
|
|
277
|
+
list: byId('sites'), empty: byId('no-sites'), addButton: byId('add-site'),
|
|
278
|
+
parse: siteHost,
|
|
279
|
+
add: (site) => save({ sites: withSite(settings.sites, site) }),
|
|
280
|
+
remove: (site) => save({ sites: withoutSite(settings.sites, site) }),
|
|
281
|
+
replace: (site, edited) => save({ sites: withSiteReplaced(settings.sites, site, edited) }),
|
|
282
|
+
t, // the page's messages, under the standard keys below
|
|
259
283
|
});
|
|
260
284
|
sites.render(settings.sites);
|
|
261
285
|
|
|
262
|
-
//
|
|
263
|
-
|
|
286
|
+
// A short list with an add row (chrome://settings): `form` instead of `addButton`.
|
|
287
|
+
// <form class="row add" id="add-site"><input type="text" …><button type="submit">Add</button></form>
|
|
264
288
|
```
|
|
265
289
|
|
|
266
|
-
`
|
|
267
|
-
|
|
268
|
-
|
|
290
|
+
`withSite` adds a site at the end, once (added again, it moves there); `withoutSite` takes it out;
|
|
291
|
+
`withSiteReplaced` edits it in place, merging it with a duplicate. A list that needs site access
|
|
292
|
+
asks for it in `add`/`replace` (the dialog's click still counts as the person's) and gives it back
|
|
293
|
+
in `remove`. `add` resolving to false keeps the add row's text (access refused); `invalid(text)`
|
|
294
|
+
hears what isn't a site in the add row; the dialog keeps its Add off with “Enter a site, like
|
|
295
|
+
example.com” under the field.
|
|
296
|
+
|
|
297
|
+
The words: Chrome's English (`SITE_LIST_WORDS`) without `t`; with it, the page's messages under
|
|
298
|
+
these keys (`SITE_LIST_MESSAGES`), so every extension's `_locales` names them alike. `words`
|
|
299
|
+
overrides only those that differ.
|
|
300
|
+
|
|
301
|
+
| Word | Key | English |
|
|
302
|
+
|---|---|---|
|
|
303
|
+
| `add` | `optionsAdd` | Add |
|
|
304
|
+
| `addSite` | `optionsAddSite` | Add site |
|
|
305
|
+
| `editSite` | `optionsEditSite` | Edit site |
|
|
306
|
+
| `edit` | `optionsEdit` | Edit |
|
|
307
|
+
| `remove` | `optionsRemove` | Remove |
|
|
308
|
+
| `save` | `optionsSave` | Save |
|
|
309
|
+
| `cancel` | `optionsCancel` | Cancel |
|
|
310
|
+
| `site` | `optionsSiteLabel` | Site |
|
|
311
|
+
| `invalid` | `optionsSiteInvalid` | Enter a site, like example.com |
|
|
312
|
+
| `moreActions` | `optionsMoreActions` | More actions for `$SITE$` (content `$1`; the ⋮ button) |
|
|
313
|
+
| `removeLabel` | `optionsRemoveLabel` | Remove `$SITE$` (content `$1`; the add row's Remove button) |
|
|
314
|
+
|
|
315
|
+
A list with an Add button needs every key but `optionsRemoveLabel`; one with an add row needs
|
|
316
|
+
`optionsRemove` and `optionsRemoveLabel`. A locale test can check them against `SITE_LIST_MESSAGES`.
|
|
269
317
|
|
|
270
318
|
## Build helpers (`@arraypress/extension-kit/build`)
|
|
271
319
|
|
|
@@ -300,7 +348,6 @@ except those listed, each with a reason:
|
|
|
300
348
|
| `launchExtension(chromium, dir, options)` | Load an unpacked extension into a fresh profile; `{context, worker, extensionId, url(page), profile, close()}`. |
|
|
301
349
|
| `extensionProfile(chromium, dir, options)` | A profile to start the browser on more than once (a restart, a first start with a changed manifest): `{dir, launch(), remove()}`. |
|
|
302
350
|
| `checkRunner({timeout})` | Named checks that report and go on: `check(name, fn, {timeout})`, `fail(name)` for an error found outside a check, `finish()`. A check that runs past its limit (`CHECK_MS`, 60 s) fails, and the run goes on. |
|
|
303
|
-
| `within(promise, ms, fallback)` | The value in time, or the fallback (late or failed); never rejects; its timer cleared. |
|
|
304
351
|
| `eachLimited(items, limit, work)` | Work through a list at most `limit` at once, `work(item, slot, index)`. |
|
|
305
352
|
| `realisticBrowser(options)`, `chromeUserAgent(browser)` | Launch options and a user agent for a browser that doesn't say it's automated, for live checks. |
|
|
306
353
|
| `closeOnSignal(close)` | On Ctrl-C or a kill, close the run's browsers, then exit. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arraypress/extension-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Shared pieces for Chrome extensions: Chrome's own page styles, a Chrome Sync settings store, the light/dark toolbar glyph, About links, and build and test helpers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/build/vendor.js
CHANGED
|
@@ -28,7 +28,7 @@ const SOURCE = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
|
28
28
|
const KIT = '@arraypress/extension-kit';
|
|
29
29
|
|
|
30
30
|
/** What a vendored copy of the kit holds, relative to `src/`: everything an extension page or worker may load. */
|
|
31
|
-
export const RUNTIME_ENTRIES = Object.freeze(['index.js', 'index.d.ts', 'about', 'commands', 'dom', 'settings', 'sites', 'styles', 'toolbar', 'ui']);
|
|
31
|
+
export const RUNTIME_ENTRIES = Object.freeze(['index.js', 'index.d.ts', 'about', 'commands', 'dom', 'settings', 'sites', 'styles', 'time', 'toolbar', 'ui']);
|
|
32
32
|
|
|
33
33
|
/** The kit's stylesheet, an absolute path in the installed package. */
|
|
34
34
|
export const STYLESHEET = path.join(SOURCE, 'styles', 'chrome-ui.css');
|
package/src/dom/i18n.d.ts
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
* `data-i18n-<attribute>="key"` (sets that attribute, e.g.
|
|
5
5
|
* `data-i18n-placeholder`, `data-i18n-aria-label`).
|
|
6
6
|
*
|
|
7
|
+
* The extension's name lives in one message, `extShortName`; a message that
|
|
8
|
+
* names the extension takes it as a placeholder whose content is `$1`
|
|
9
|
+
* (`"About $NAME$"`), and every lookup made without substitutions fills it
|
|
10
|
+
* in, in markup and script alike.
|
|
11
|
+
*
|
|
7
12
|
* @module dom/i18n
|
|
8
13
|
*/
|
|
9
14
|
export type I18nApi = {
|
|
@@ -19,18 +24,21 @@ export type I18nApi = {
|
|
|
19
24
|
*/
|
|
20
25
|
/**
|
|
21
26
|
* A message lookup that never comes back blank: a missing key shows as
|
|
22
|
-
* itself, so a typo is visible on screen instead of an empty label.
|
|
27
|
+
* itself, so a typo is visible on screen instead of an empty label. Without
|
|
28
|
+
* substitutions, `$1` is the extension's name (`extShortName`).
|
|
23
29
|
*
|
|
24
30
|
* @param {Pick<I18nApi, 'getMessage'>} i18n - `chrome.i18n`.
|
|
25
31
|
* @returns {(key: string, substitutions?: string | string[]) => string} Looks a key up.
|
|
26
32
|
* @example
|
|
27
33
|
* const t = translator(chrome.i18n);
|
|
28
34
|
* t('optionsVersion', ['1.0.0']); // 'Version 1.0.0'
|
|
35
|
+
* t('optionsAbout'); // 'About Upright', from "About $NAME$"
|
|
29
36
|
*/
|
|
30
37
|
export declare function translator(i18n: Pick<I18nApi, 'getMessage'>): (key: string, substitutions?: string | string[]) => string;
|
|
31
38
|
/**
|
|
32
|
-
* Fill every `data-i18n*` attribute under `root
|
|
33
|
-
* set its `lang` to
|
|
39
|
+
* Fill every `data-i18n*` attribute under `root` (a message that names the
|
|
40
|
+
* extension gets its name), and, for a whole document, set its `lang` to
|
|
41
|
+
* Chrome's UI language.
|
|
34
42
|
*
|
|
35
43
|
* @param {ParentNode} root - A document or an element.
|
|
36
44
|
* @param {I18nApi} i18n - `chrome.i18n`.
|
package/src/dom/i18n.js
CHANGED
|
@@ -4,9 +4,17 @@
|
|
|
4
4
|
* `data-i18n-<attribute>="key"` (sets that attribute, e.g.
|
|
5
5
|
* `data-i18n-placeholder`, `data-i18n-aria-label`).
|
|
6
6
|
*
|
|
7
|
+
* The extension's name lives in one message, `extShortName`; a message that
|
|
8
|
+
* names the extension takes it as a placeholder whose content is `$1`
|
|
9
|
+
* (`"About $NAME$"`), and every lookup made without substitutions fills it
|
|
10
|
+
* in, in markup and script alike.
|
|
11
|
+
*
|
|
7
12
|
* @module dom/i18n
|
|
8
13
|
*/
|
|
9
14
|
|
|
15
|
+
/** The message that holds the extension's name. */
|
|
16
|
+
const NAME_MESSAGE = 'extShortName';
|
|
17
|
+
|
|
10
18
|
/**
|
|
11
19
|
* `chrome.i18n`, as far as pages need it.
|
|
12
20
|
*
|
|
@@ -17,21 +25,26 @@
|
|
|
17
25
|
|
|
18
26
|
/**
|
|
19
27
|
* A message lookup that never comes back blank: a missing key shows as
|
|
20
|
-
* itself, so a typo is visible on screen instead of an empty label.
|
|
28
|
+
* itself, so a typo is visible on screen instead of an empty label. Without
|
|
29
|
+
* substitutions, `$1` is the extension's name (`extShortName`).
|
|
21
30
|
*
|
|
22
31
|
* @param {Pick<I18nApi, 'getMessage'>} i18n - `chrome.i18n`.
|
|
23
32
|
* @returns {(key: string, substitutions?: string | string[]) => string} Looks a key up.
|
|
24
33
|
* @example
|
|
25
34
|
* const t = translator(chrome.i18n);
|
|
26
35
|
* t('optionsVersion', ['1.0.0']); // 'Version 1.0.0'
|
|
36
|
+
* t('optionsAbout'); // 'About Upright', from "About $NAME$"
|
|
27
37
|
*/
|
|
28
38
|
export function translator(i18n) {
|
|
29
|
-
|
|
39
|
+
const name = i18n.getMessage(NAME_MESSAGE);
|
|
40
|
+
const named = name ? [name] : undefined;
|
|
41
|
+
return (key, substitutions = named) => i18n.getMessage(key, substitutions) || key;
|
|
30
42
|
}
|
|
31
43
|
|
|
32
44
|
/**
|
|
33
|
-
* Fill every `data-i18n*` attribute under `root
|
|
34
|
-
* set its `lang` to
|
|
45
|
+
* Fill every `data-i18n*` attribute under `root` (a message that names the
|
|
46
|
+
* extension gets its name), and, for a whole document, set its `lang` to
|
|
47
|
+
* Chrome's UI language.
|
|
35
48
|
*
|
|
36
49
|
* @param {ParentNode} root - A document or an element.
|
|
37
50
|
* @param {I18nApi} i18n - `chrome.i18n`.
|
package/src/dom/setup.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export type PageSetup = {
|
|
|
13
13
|
*/
|
|
14
14
|
title?: string;
|
|
15
15
|
/**
|
|
16
|
-
* The title's placeholders
|
|
16
|
+
* The title's placeholders. Default: the extension's name (`translator`).
|
|
17
17
|
*/
|
|
18
18
|
substitutions?: string | string[];
|
|
19
19
|
/**
|
|
@@ -32,7 +32,7 @@ export type PageSetup = {
|
|
|
32
32
|
*
|
|
33
33
|
* @typedef {object} PageSetup
|
|
34
34
|
* @property {string} [title] The title's message (“optionsTitle”). Default: the title is left as it is.
|
|
35
|
-
* @property {string | string[]} [substitutions] The title's placeholders
|
|
35
|
+
* @property {string | string[]} [substitutions] The title's placeholders. Default: the extension's name (`translator`).
|
|
36
36
|
* @property {Document} [doc] The page. Default `document`.
|
|
37
37
|
* @property {SchemeMedia} [media] The scheme to report. Default the page's `matchMedia(DARK_QUERY)`.
|
|
38
38
|
*/
|
|
@@ -45,7 +45,8 @@ export type PageSetup = {
|
|
|
45
45
|
* @returns {(key: string, substitutions?: string | string[]) => string} The page's message lookup
|
|
46
46
|
* (`translator(chrome.i18n)`).
|
|
47
47
|
* @example
|
|
48
|
-
* const t = setUpPage(chrome, { title: 'optionsTitle'
|
|
48
|
+
* const t = setUpPage(chrome, { title: 'optionsTitle' }); // "$NAME$ settings": 'Upright settings'
|
|
49
|
+
* t('optionsGetHelp'); // "Get help with $NAME$": 'Get help with Upright'
|
|
49
50
|
* // The popup, whose title Chrome doesn't show:
|
|
50
51
|
* const t = setUpPage(chrome);
|
|
51
52
|
*/
|
package/src/dom/setup.js
CHANGED
|
@@ -17,7 +17,7 @@ import { DARK_QUERY, watchColorScheme } from '../toolbar/scheme.js';
|
|
|
17
17
|
*
|
|
18
18
|
* @typedef {object} PageSetup
|
|
19
19
|
* @property {string} [title] The title's message (“optionsTitle”). Default: the title is left as it is.
|
|
20
|
-
* @property {string | string[]} [substitutions] The title's placeholders
|
|
20
|
+
* @property {string | string[]} [substitutions] The title's placeholders. Default: the extension's name (`translator`).
|
|
21
21
|
* @property {Document} [doc] The page. Default `document`.
|
|
22
22
|
* @property {SchemeMedia} [media] The scheme to report. Default the page's `matchMedia(DARK_QUERY)`.
|
|
23
23
|
*/
|
|
@@ -31,7 +31,8 @@ import { DARK_QUERY, watchColorScheme } from '../toolbar/scheme.js';
|
|
|
31
31
|
* @returns {(key: string, substitutions?: string | string[]) => string} The page's message lookup
|
|
32
32
|
* (`translator(chrome.i18n)`).
|
|
33
33
|
* @example
|
|
34
|
-
* const t = setUpPage(chrome, { title: 'optionsTitle'
|
|
34
|
+
* const t = setUpPage(chrome, { title: 'optionsTitle' }); // "$NAME$ settings": 'Upright settings'
|
|
35
|
+
* t('optionsGetHelp'); // "Get help with $NAME$": 'Get help with Upright'
|
|
35
36
|
* // The popup, whose title Chrome doesn't show:
|
|
36
37
|
* const t = setUpPage(chrome);
|
|
37
38
|
*/
|
package/src/index.d.ts
CHANGED
|
@@ -20,12 +20,13 @@ export { setUpPage } from './dom/setup.js';
|
|
|
20
20
|
export { createSettingsSaver } from './settings/saver.js';
|
|
21
21
|
export { MAX_SYNC_ITEM_BYTES } from './settings/storage.js';
|
|
22
22
|
export { createSettingsStore } from './settings/store.js';
|
|
23
|
-
export { MAX_HOST_LENGTH, hostPatterns, onSite, originPattern, siteHost, siteLabel, siteOrigin } from './sites/sites.js';
|
|
23
|
+
export { MAX_HOST_LENGTH, hostPatterns, onSite, originPattern, siteHost, siteLabel, siteOrigin, withSite, withSiteReplaced, withoutSite, } from './sites/sites.js';
|
|
24
|
+
export { within } from './time/time.js';
|
|
24
25
|
export { POLL_MS, watchSchemeOffscreen } from './toolbar/offscreen.js';
|
|
25
26
|
export { DARK_QUERY, TOOLBAR_DARK_KEY, TOOLBAR_SCHEME, reportColorScheme, schemeSender, watchColorScheme } from './toolbar/scheme.js';
|
|
26
27
|
export { OFFSCREEN_PATH, TOOLBAR_ICON_DIR, createToolbarTheme, startToolbarTheme, toolbarIcon, watchFromOffscreen, } from './toolbar/theme.js';
|
|
27
28
|
export { MENU_EDGE, createMenu, isInside, menuPosition, moreActionsButton } from './ui/menu.js';
|
|
28
|
-
export { createSiteList } from './ui/site-list.js';
|
|
29
|
+
export { SITE_LIST_MESSAGES, SITE_LIST_WORDS, createSiteList } from './ui/site-list.js';
|
|
29
30
|
export { askText, checkText } from './ui/text-dialog.js';
|
|
30
31
|
export { TOAST_MS, createToast } from './ui/toast.js';
|
|
31
32
|
export type ActionMenu = import('./ui/menu.js').ActionMenu;
|
package/src/index.js
CHANGED
|
@@ -21,14 +21,17 @@ export { setUpPage } from './dom/setup.js';
|
|
|
21
21
|
export { createSettingsSaver } from './settings/saver.js';
|
|
22
22
|
export { MAX_SYNC_ITEM_BYTES } from './settings/storage.js';
|
|
23
23
|
export { createSettingsStore } from './settings/store.js';
|
|
24
|
-
export {
|
|
24
|
+
export {
|
|
25
|
+
MAX_HOST_LENGTH, hostPatterns, onSite, originPattern, siteHost, siteLabel, siteOrigin, withSite, withSiteReplaced, withoutSite,
|
|
26
|
+
} from './sites/sites.js';
|
|
27
|
+
export { within } from './time/time.js';
|
|
25
28
|
export { POLL_MS, watchSchemeOffscreen } from './toolbar/offscreen.js';
|
|
26
29
|
export { DARK_QUERY, TOOLBAR_DARK_KEY, TOOLBAR_SCHEME, reportColorScheme, schemeSender, watchColorScheme } from './toolbar/scheme.js';
|
|
27
30
|
export {
|
|
28
31
|
OFFSCREEN_PATH, TOOLBAR_ICON_DIR, createToolbarTheme, startToolbarTheme, toolbarIcon, watchFromOffscreen,
|
|
29
32
|
} from './toolbar/theme.js';
|
|
30
33
|
export { MENU_EDGE, createMenu, isInside, menuPosition, moreActionsButton } from './ui/menu.js';
|
|
31
|
-
export { createSiteList } from './ui/site-list.js';
|
|
34
|
+
export { SITE_LIST_MESSAGES, SITE_LIST_WORDS, createSiteList } from './ui/site-list.js';
|
|
32
35
|
export { askText, checkText } from './ui/text-dialog.js';
|
|
33
36
|
export { TOAST_MS, createToast } from './ui/toast.js';
|
|
34
37
|
|
package/src/sites/sites.d.ts
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* "https://www.Example.com/shop") and as pages report them (`location.href`):
|
|
4
4
|
* the origin an extension asks Chrome for access to, the host a per-site
|
|
5
5
|
* on/off list keeps, whether a page is on a listed site (subdomains
|
|
6
|
-
* included),
|
|
6
|
+
* included), the match patterns for both, and a list with a site added,
|
|
7
|
+
* taken out or edited. Pure.
|
|
7
8
|
*
|
|
8
9
|
* A site is a web host: http or https, with a dot in its name (or
|
|
9
10
|
* `localhost`). Text without a scheme is read as https.
|
|
@@ -80,3 +81,34 @@ export declare function originPattern(url: string): string;
|
|
|
80
81
|
* siteLabel('http://example.com'); // 'http://example.com'
|
|
81
82
|
*/
|
|
82
83
|
export declare function siteLabel(site: string): string;
|
|
84
|
+
/**
|
|
85
|
+
* A site list with `site` added: at the end, once (a site added again moves
|
|
86
|
+
* there).
|
|
87
|
+
*
|
|
88
|
+
* @param {readonly string[]} sites - The list.
|
|
89
|
+
* @param {string} site - The site to add.
|
|
90
|
+
* @returns {string[]} A new list.
|
|
91
|
+
* @example
|
|
92
|
+
* withSite(['a.com', 'b.com'], 'a.com'); // ['b.com', 'a.com']
|
|
93
|
+
*/
|
|
94
|
+
export declare function withSite(sites: readonly string[], site: string): string[];
|
|
95
|
+
/**
|
|
96
|
+
* A site list without `site`.
|
|
97
|
+
*
|
|
98
|
+
* @param {readonly string[]} sites - The list.
|
|
99
|
+
* @param {string} site - The site to take out.
|
|
100
|
+
* @returns {string[]} A new list.
|
|
101
|
+
*/
|
|
102
|
+
export declare function withoutSite(sites: readonly string[], site: string): string[];
|
|
103
|
+
/**
|
|
104
|
+
* A site list with `site` edited to `edited`, in its place, once (an edit
|
|
105
|
+
* to a site already listed merges the two).
|
|
106
|
+
*
|
|
107
|
+
* @param {readonly string[]} sites - The list.
|
|
108
|
+
* @param {string} site - The site as it was.
|
|
109
|
+
* @param {string} edited - What it was edited to.
|
|
110
|
+
* @returns {string[]} A new list.
|
|
111
|
+
* @example
|
|
112
|
+
* withSiteReplaced(['a.com', 'b.com'], 'a.com', 'b.com'); // ['b.com']
|
|
113
|
+
*/
|
|
114
|
+
export declare function withSiteReplaced(sites: readonly string[], site: string, edited: string): string[];
|
package/src/sites/sites.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* "https://www.Example.com/shop") and as pages report them (`location.href`):
|
|
4
4
|
* the origin an extension asks Chrome for access to, the host a per-site
|
|
5
5
|
* on/off list keeps, whether a page is on a listed site (subdomains
|
|
6
|
-
* included),
|
|
6
|
+
* included), the match patterns for both, and a list with a site added,
|
|
7
|
+
* taken out or edited. Pure.
|
|
7
8
|
*
|
|
8
9
|
* A site is a web host: http or https, with a dot in its name (or
|
|
9
10
|
* `localhost`). Text without a scheme is read as https.
|
|
@@ -126,3 +127,43 @@ export function originPattern(url) {
|
|
|
126
127
|
export function siteLabel(site) {
|
|
127
128
|
return site.replace(/^https:\/\//, '');
|
|
128
129
|
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* A site list with `site` added: at the end, once (a site added again moves
|
|
133
|
+
* there).
|
|
134
|
+
*
|
|
135
|
+
* @param {readonly string[]} sites - The list.
|
|
136
|
+
* @param {string} site - The site to add.
|
|
137
|
+
* @returns {string[]} A new list.
|
|
138
|
+
* @example
|
|
139
|
+
* withSite(['a.com', 'b.com'], 'a.com'); // ['b.com', 'a.com']
|
|
140
|
+
*/
|
|
141
|
+
export function withSite(sites, site) {
|
|
142
|
+
return [...withoutSite(sites, site), site];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* A site list without `site`.
|
|
147
|
+
*
|
|
148
|
+
* @param {readonly string[]} sites - The list.
|
|
149
|
+
* @param {string} site - The site to take out.
|
|
150
|
+
* @returns {string[]} A new list.
|
|
151
|
+
*/
|
|
152
|
+
export function withoutSite(sites, site) {
|
|
153
|
+
return sites.filter((s) => s !== site);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* A site list with `site` edited to `edited`, in its place, once (an edit
|
|
158
|
+
* to a site already listed merges the two).
|
|
159
|
+
*
|
|
160
|
+
* @param {readonly string[]} sites - The list.
|
|
161
|
+
* @param {string} site - The site as it was.
|
|
162
|
+
* @param {string} edited - What it was edited to.
|
|
163
|
+
* @returns {string[]} A new list.
|
|
164
|
+
* @example
|
|
165
|
+
* withSiteReplaced(['a.com', 'b.com'], 'a.com', 'b.com'); // ['b.com']
|
|
166
|
+
*/
|
|
167
|
+
export function withSiteReplaced(sites, site, edited) {
|
|
168
|
+
return [...new Set(sites.map((s) => (s === site ? edited : s)))];
|
|
169
|
+
}
|
package/src/testing/browser.js
CHANGED
package/src/testing/e2e.js
CHANGED
|
@@ -12,7 +12,7 @@ import assert from 'node:assert/strict';
|
|
|
12
12
|
import { mkdtemp, rm } from 'node:fs/promises';
|
|
13
13
|
import { tmpdir } from 'node:os';
|
|
14
14
|
import path from 'node:path';
|
|
15
|
-
import { deadline } from '
|
|
15
|
+
import { deadline } from '../time/time.js';
|
|
16
16
|
|
|
17
17
|
/** @import { BrowserContext, BrowserType, CDPSession, Page, Worker } from 'playwright' */
|
|
18
18
|
|
package/src/testing/index.d.ts
CHANGED
|
@@ -10,7 +10,6 @@ export { TEST_BUILDS_ENV, testBuildDir } from '../build/test-build.js';
|
|
|
10
10
|
export { chromeUserAgent, closeOnSignal, realisticBrowser } from './browser.js';
|
|
11
11
|
export { CHECK_MS, checkRunner, checkToolbarTheme, extensionProfile, launchExtension, offscreenEmulator, recordIconCalls, until, } from './e2e.js';
|
|
12
12
|
export { eachLimited } from './pool.js';
|
|
13
|
-
export { within } from './time.js';
|
|
14
13
|
export { serve } from './server.js';
|
|
15
14
|
export { SYNC_WRITES_PER_MINUTE, fakeChromeStorage } from './fake-storage.js';
|
|
16
15
|
export { filesUnder, manifestReferences, missingReferences, referencedPath, zipEntries } from './package.js';
|
package/src/testing/index.js
CHANGED
|
@@ -13,7 +13,6 @@ export {
|
|
|
13
13
|
CHECK_MS, checkRunner, checkToolbarTheme, extensionProfile, launchExtension, offscreenEmulator, recordIconCalls, until,
|
|
14
14
|
} from './e2e.js';
|
|
15
15
|
export { eachLimited } from './pool.js';
|
|
16
|
-
export { within } from './time.js';
|
|
17
16
|
export { serve } from './server.js';
|
|
18
17
|
export { SYNC_WRITES_PER_MINUTE, fakeChromeStorage } from './fake-storage.js';
|
|
19
18
|
export { filesUnder, manifestReferences, missingReferences, referencedPath, zipEntries } from './package.js';
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Time limits for
|
|
3
|
-
*
|
|
2
|
+
* Time limits for work that may never finish: a Chrome call a service worker
|
|
3
|
+
* shouldn't wait on forever, or, in tests that drive a real browser, a page
|
|
4
|
+
* that stops responding. Runs anywhere (worker, page, Node).
|
|
4
5
|
*
|
|
5
|
-
* @module
|
|
6
|
+
* @module time/time
|
|
6
7
|
*/
|
|
7
8
|
/**
|
|
8
9
|
* Settle `promise` within `ms`, or settle the way `late` says. The timer is
|
|
@@ -19,9 +20,9 @@
|
|
|
19
20
|
export declare function deadline<T>(promise: Promise<T>, ms: number, late: (resolve: (value: T) => void, reject: (error: Error) => void) => void): Promise<T>;
|
|
20
21
|
/**
|
|
21
22
|
* A promise's value within a time limit, or `fallback` when it's late or
|
|
22
|
-
* fails: for a step
|
|
23
|
-
* that stopped responding, a close that never returns).
|
|
24
|
-
* isn't stopped; the timer is cleared once it settles.
|
|
23
|
+
* fails: for a step to give up on and go past (a Chrome call that never
|
|
24
|
+
* answers, a renderer that stopped responding, a close that never returns).
|
|
25
|
+
* The work itself isn't stopped; the timer is cleared once it settles.
|
|
25
26
|
*
|
|
26
27
|
* @template T
|
|
27
28
|
* @param {Promise<T>} promise - The work.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Time limits for
|
|
3
|
-
*
|
|
2
|
+
* Time limits for work that may never finish: a Chrome call a service worker
|
|
3
|
+
* shouldn't wait on forever, or, in tests that drive a real browser, a page
|
|
4
|
+
* that stops responding. Runs anywhere (worker, page, Node).
|
|
4
5
|
*
|
|
5
|
-
* @module
|
|
6
|
+
* @module time/time
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -28,9 +29,9 @@ export function deadline(promise, ms, late) {
|
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* A promise's value within a time limit, or `fallback` when it's late or
|
|
31
|
-
* fails: for a step
|
|
32
|
-
* that stopped responding, a close that never returns).
|
|
33
|
-
* isn't stopped; the timer is cleared once it settles.
|
|
32
|
+
* fails: for a step to give up on and go past (a Chrome call that never
|
|
33
|
+
* answers, a renderer that stopped responding, a close that never returns).
|
|
34
|
+
* The work itself isn't stopped; the timer is cleared once it settles.
|
|
34
35
|
*
|
|
35
36
|
* @template T
|
|
36
37
|
* @param {Promise<T>} promise - The work.
|
package/src/ui/site-list.d.ts
CHANGED
|
@@ -90,6 +90,14 @@ export type SiteListWords = {
|
|
|
90
90
|
*/
|
|
91
91
|
/** @type {Readonly<SiteListWords>} */
|
|
92
92
|
export declare const SITE_LIST_WORDS: Readonly<SiteListWords>;
|
|
93
|
+
/**
|
|
94
|
+
* The message key each word is read from when a page passes its `t`, so
|
|
95
|
+
* every extension's `_locales` names them alike. `removeLabel` and
|
|
96
|
+
* `moreActions` take the site as `$1` (`"More actions for $SITE$"`).
|
|
97
|
+
*
|
|
98
|
+
* @type {Readonly<Record<keyof SiteListWords, string>>}
|
|
99
|
+
*/
|
|
100
|
+
export declare const SITE_LIST_MESSAGES: Readonly<Record<keyof SiteListWords, string>>;
|
|
93
101
|
export type SiteListOptions = {
|
|
94
102
|
/**
|
|
95
103
|
* The `<ul class="list">` to fill.
|
|
@@ -140,7 +148,13 @@ export type SiteListOptions = {
|
|
|
140
148
|
*/
|
|
141
149
|
maxLength?: number;
|
|
142
150
|
/**
|
|
143
|
-
*
|
|
151
|
+
* The page's message lookup
|
|
152
|
+
* (`setUpPage`'s): the words are then its messages under {@link SITE_LIST_MESSAGES}. Without it,
|
|
153
|
+
* Chrome's English ({@link SITE_LIST_WORDS}).
|
|
154
|
+
*/
|
|
155
|
+
t?: (key: string, substitutions?: string | string[]) => string;
|
|
156
|
+
/**
|
|
157
|
+
* Words that differ from those, by name.
|
|
144
158
|
*/
|
|
145
159
|
words?: Partial<SiteListWords>;
|
|
146
160
|
};
|
|
@@ -170,7 +184,10 @@ export type SiteList = {
|
|
|
170
184
|
* @property {(text: string) => void} [invalid] The add row: called with what was typed when it isn't a site.
|
|
171
185
|
* @property {(site: string) => string} [label] How a site reads. Default {@link siteLabel}.
|
|
172
186
|
* @property {number} [maxLength] The dialog field's `maxlength`. Default {@link MAX_HOST_LENGTH}.
|
|
173
|
-
* @property {
|
|
187
|
+
* @property {(key: string, substitutions?: string | string[]) => string} [t] The page's message lookup
|
|
188
|
+
* (`setUpPage`'s): the words are then its messages under {@link SITE_LIST_MESSAGES}. Without it,
|
|
189
|
+
* Chrome's English ({@link SITE_LIST_WORDS}).
|
|
190
|
+
* @property {Partial<SiteListWords>} [words] Words that differ from those, by name.
|
|
174
191
|
*/
|
|
175
192
|
/**
|
|
176
193
|
* A site list.
|
|
@@ -190,10 +207,10 @@ export type SiteList = {
|
|
|
190
207
|
* const never = createSiteList({
|
|
191
208
|
* list: byId('never-sites'), form: byId('add-never', HTMLFormElement), empty: byId('no-sites'),
|
|
192
209
|
* parse: siteHost,
|
|
193
|
-
* add: (host) => save({ neverSites:
|
|
194
|
-
* remove: (host) => save({ neverSites: settings.neverSites
|
|
195
|
-
*
|
|
210
|
+
* add: (host) => save({ neverSites: withSite(settings.neverSites, host) }),
|
|
211
|
+
* remove: (host) => save({ neverSites: withoutSite(settings.neverSites, host) }),
|
|
212
|
+
* t, // its words from optionsAdd, optionsRemove, …
|
|
196
213
|
* });
|
|
197
214
|
* never.render(settings.neverSites);
|
|
198
215
|
*/
|
|
199
|
-
export declare function createSiteList({ list, empty, form, addButton, parse, add, remove, replace, invalid, label, maxLength, words, }: SiteListOptions): SiteList;
|
|
216
|
+
export declare function createSiteList({ list, empty, form, addButton, parse, add, remove, replace, invalid, label, maxLength, t, words, }: SiteListOptions): SiteList;
|
package/src/ui/site-list.js
CHANGED
|
@@ -63,6 +63,43 @@ export const SITE_LIST_WORDS = Object.freeze({
|
|
|
63
63
|
moreActions: (/** @type {string} */ label) => `More actions for “${label}”`,
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* The message key each word is read from when a page passes its `t`, so
|
|
68
|
+
* every extension's `_locales` names them alike. `removeLabel` and
|
|
69
|
+
* `moreActions` take the site as `$1` (`"More actions for $SITE$"`).
|
|
70
|
+
*
|
|
71
|
+
* @type {Readonly<Record<keyof SiteListWords, string>>}
|
|
72
|
+
*/
|
|
73
|
+
export const SITE_LIST_MESSAGES = Object.freeze({
|
|
74
|
+
add: 'optionsAdd',
|
|
75
|
+
addSite: 'optionsAddSite',
|
|
76
|
+
editSite: 'optionsEditSite',
|
|
77
|
+
edit: 'optionsEdit',
|
|
78
|
+
remove: 'optionsRemove',
|
|
79
|
+
save: 'optionsSave',
|
|
80
|
+
cancel: 'optionsCancel',
|
|
81
|
+
site: 'optionsSiteLabel',
|
|
82
|
+
invalid: 'optionsSiteInvalid',
|
|
83
|
+
removeLabel: 'optionsRemoveLabel',
|
|
84
|
+
moreActions: 'optionsMoreActions',
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The list's words from a page's messages, under {@link SITE_LIST_MESSAGES}.
|
|
89
|
+
*
|
|
90
|
+
* @param {(key: string, substitutions?: string | string[]) => string} t - The page's message lookup.
|
|
91
|
+
* @returns {SiteListWords} The words.
|
|
92
|
+
*/
|
|
93
|
+
function translatedWords(t) {
|
|
94
|
+
const m = SITE_LIST_MESSAGES;
|
|
95
|
+
return {
|
|
96
|
+
add: t(m.add), addSite: t(m.addSite), editSite: t(m.editSite), edit: t(m.edit), remove: t(m.remove),
|
|
97
|
+
save: t(m.save), cancel: t(m.cancel), site: t(m.site), invalid: t(m.invalid),
|
|
98
|
+
removeLabel: (label) => t(m.removeLabel, [label]),
|
|
99
|
+
moreActions: (label) => t(m.moreActions, [label]),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
66
103
|
/**
|
|
67
104
|
* What {@link createSiteList} takes: the list, one way to add to it, and
|
|
68
105
|
* what adding, removing and editing do.
|
|
@@ -83,7 +120,10 @@ export const SITE_LIST_WORDS = Object.freeze({
|
|
|
83
120
|
* @property {(text: string) => void} [invalid] The add row: called with what was typed when it isn't a site.
|
|
84
121
|
* @property {(site: string) => string} [label] How a site reads. Default {@link siteLabel}.
|
|
85
122
|
* @property {number} [maxLength] The dialog field's `maxlength`. Default {@link MAX_HOST_LENGTH}.
|
|
86
|
-
* @property {
|
|
123
|
+
* @property {(key: string, substitutions?: string | string[]) => string} [t] The page's message lookup
|
|
124
|
+
* (`setUpPage`'s): the words are then its messages under {@link SITE_LIST_MESSAGES}. Without it,
|
|
125
|
+
* Chrome's English ({@link SITE_LIST_WORDS}).
|
|
126
|
+
* @property {Partial<SiteListWords>} [words] Words that differ from those, by name.
|
|
87
127
|
*/
|
|
88
128
|
|
|
89
129
|
/**
|
|
@@ -105,17 +145,17 @@ export const SITE_LIST_WORDS = Object.freeze({
|
|
|
105
145
|
* const never = createSiteList({
|
|
106
146
|
* list: byId('never-sites'), form: byId('add-never', HTMLFormElement), empty: byId('no-sites'),
|
|
107
147
|
* parse: siteHost,
|
|
108
|
-
* add: (host) => save({ neverSites:
|
|
109
|
-
* remove: (host) => save({ neverSites: settings.neverSites
|
|
110
|
-
*
|
|
148
|
+
* add: (host) => save({ neverSites: withSite(settings.neverSites, host) }),
|
|
149
|
+
* remove: (host) => save({ neverSites: withoutSite(settings.neverSites, host) }),
|
|
150
|
+
* t, // its words from optionsAdd, optionsRemove, …
|
|
111
151
|
* });
|
|
112
152
|
* never.render(settings.neverSites);
|
|
113
153
|
*/
|
|
114
154
|
export function createSiteList({
|
|
115
|
-
list, empty, form, addButton, parse, add, remove, replace, invalid, label = siteLabel, maxLength = MAX_HOST_LENGTH, words,
|
|
155
|
+
list, empty, form, addButton, parse, add, remove, replace, invalid, label = siteLabel, maxLength = MAX_HOST_LENGTH, t, words,
|
|
116
156
|
}) {
|
|
117
157
|
const doc = list.ownerDocument;
|
|
118
|
-
const w = { ...SITE_LIST_WORDS, ...words };
|
|
158
|
+
const w = { ...(t ? translatedWords(t) : SITE_LIST_WORDS), ...words };
|
|
119
159
|
const menu = addButton ? createMenu({ doc }) : null;
|
|
120
160
|
|
|
121
161
|
/**
|