@appsoftwareltd/etherpk-extension-api 0.9.5 → 0.10.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 CHANGED
@@ -1,41 +1,104 @@
1
1
  # EtherPK Extension API
2
2
 
3
- The types an [EtherPK](https://etherpk.com) extension is written against, and the function that
4
- checks an extension's manifest the way the EtherPK Client does. MIT licensed.
3
+ This package holds the types an [EtherPK](https://etherpk.com) extension is written against, and
4
+ `readExtensionPackage`, which checks an extension's manifest the way the EtherPK Client does. It
5
+ holds no code that the Client runs. MIT licensed.
5
6
 
6
- An extension is an npm package. Its package.json carries an `etherpk` manifest, and its main
7
- module exports `activate(context)`, which the Client calls as each graph opens:
7
+ ```bash
8
+ npm install --save-dev @appsoftwareltd/etherpk-extension-api
9
+ ```
8
10
 
9
- ```ts
10
- import type { ExtensionContext } from '@appsoftwareltd/etherpk-extension-api'
11
+ It is a development dependency, because the Client supplies everything at run time.
11
12
 
12
- export function activate(context: ExtensionContext): void {
13
- context.commands.register('hello.greet', () => context.notify('Hello from an extension.'))
14
- }
15
- ```
13
+ ## What an extension is
14
+
15
+ An extension is an npm package. Its `package.json` has an `etherpk` manifest, which says what the
16
+ extension is and what it adds, and its main module exports `activate(context)`. The Client calls
17
+ `activate` each time a graph opens, with a context for that graph. When the graph closes, the Client
18
+ removes everything the extension registered through the context, and calls the function `activate`
19
+ returned, if it returned one, for anything else the extension holds.
16
20
 
17
21
  ```json
18
22
  {
19
23
  "name": "etherpk-extension-hello",
20
24
  "version": "1.0.0",
25
+ "type": "module",
26
+ "files": ["dist"],
21
27
  "etherpk": {
22
28
  "id": "hello",
23
29
  "displayName": "Hello",
24
- "publisher": "You",
25
- "minClientVersion": "0.8.54",
30
+ "publisher": "you",
31
+ "minClientVersion": "0.10.0",
26
32
  "main": "./dist/main.js"
27
33
  }
28
34
  }
29
35
  ```
30
36
 
31
- Everything here is Proposed (`@beta` in the type declarations) and may change in any release until
32
- extensions from other publishers can be installed. From then on, a Stable declaration (`@public`)
33
- never breaks. `etc/` holds the API reports CI checks every change against.
37
+ ```ts
38
+ import type { ExtensionContext } from '@appsoftwareltd/etherpk-extension-api'
39
+
40
+ export function activate(context: ExtensionContext): void {
41
+ context.commands.register('hello.greet', () => context.notify('Hello from an extension.'))
42
+ context.commandMenu.register({ id: 'hello.greet', title: 'Say hello', command: 'hello.greet' })
43
+ }
44
+ ```
45
+
46
+ ## Stable and Proposed parts
47
+
48
+ Each declaration in this package, and each member of one, is either Stable or Proposed. Its
49
+ documentation says which, with `@public` for Stable and `@beta` for Proposed.
50
+
51
+ - **A Stable part never breaks.** A change can add to it. Anything a change replaces is marked
52
+ deprecated and kept working.
53
+ - **A Proposed part may change in any release.** Only the extensions that come with EtherPK may use
54
+ it. In an extension installed from the Extension Directory, a call to a Proposed member throws an
55
+ error that names it.
56
+
57
+ An extension listed in the Extension Directory uses only the Stable part.
58
+ [`etc/etherpk-extension-api.public.api.md`](etc/etherpk-extension-api.public.api.md) lists the
59
+ Stable part, and [`etc/etherpk-extension-api.beta.api.md`](etc/etherpk-extension-api.beta.api.md)
60
+ lists everything. A Proposed part becomes Stable when an extension from another publisher needs it.
61
+ To ask for one, open an issue on
62
+ [the EtherPK Client repository](https://github.com/appsoftwareltd/etherpk-client/issues).
63
+
64
+ ## The manifest
34
65
 
35
- ## Settings
66
+ | Field | What it is |
67
+ | --- | --- |
68
+ | `id` | The extension's id: lowercase letters, digits and hyphens, starting with a letter. The ids of everything it registers start with it. An extension in the Extension Directory can't use `map`, `math`, `mermaid` or `etherpk-cipher`, the words of blocks the Client draws itself. |
69
+ | `displayName` | The name Settings shows, and the name a dialog it opens gives. |
70
+ | `publisher` | Who publishes the extension. |
71
+ | `minClientVersion` | The oldest Client version the extension runs on, as `x.y.z`. An extension in the Extension Directory must give one. |
72
+ | `main` | The main module, an ES module. |
73
+ | `styles` | The extension's stylesheets (see *Styles* below). |
74
+ | `isolation` | `"shadow"`, the default, or `"page"` (see *Styles* below). |
75
+ | `icons` | Icons by name, each the SVG elements of a 16 by 16 picture. A Command Menu row or a View refers to an icon by its name. |
76
+ | `views` | The View kinds the extension supplies (see *Views* below). |
77
+ | `settings` | What the person can set (see *Settings* below). |
36
78
 
37
- A value the person sets, such as a key for a service, is declared in the manifest, and the Client
38
- draws it under the extension's entry in Settings and Extensions → Extensions:
79
+ `readExtensionPackage(packageJson)` checks a manifest as the Client does, and lists every problem
80
+ it finds.
81
+
82
+ ## What an extension can do
83
+
84
+ These are the Stable parts of the context.
85
+
86
+ ### Commands and the `/` menu
87
+
88
+ `context.commands.register(id, handler)` adds a Command, whose id starts with the extension's id
89
+ (`hello.greet`). `context.commandMenu.register(item)` adds a row to the `/` menu that runs a Command.
90
+ A row can have a `detail` line, an `icon`, `keywords` that find it, a `group` and an `order`. Its
91
+ `when(menu)` says whether it is offered at the caret, from what `menu` reports: `bodyWritable`,
92
+ `inTable` and `protectedDocument`.
93
+
94
+ ### Notices
95
+
96
+ `context.notify(text)` shows a short notice, as the Client's own notices do.
97
+
98
+ ### Settings
99
+
100
+ A value the person sets, such as a key for a service, is declared in the manifest. The Client draws
101
+ it under the extension's entry in Settings, in the Extensions area.
39
102
 
40
103
  ```json
41
104
  "settings": [
@@ -50,25 +113,197 @@ draws it under the extension's entry in Settings and Extensions → Extensions:
50
113
  ]
51
114
  ```
52
115
 
53
- `type` is `text`, `secret` or `boolean`. A secret stays masked until the person shows it. A
54
- `boolean` is a switch, which may name its starting position with `"default": true`. The person can
55
- set them while the extension is switched off. The extension reads the value with
56
- `context.settings.get('api-key')`, which is undefined until it is set, and hears every change with
57
- `context.settings.subscribe`. A switch always reads `'true'` or `'false'`, its default until the
58
- person changes it. Settings belong to the person rather than a graph. Where their
59
- account syncs a graph, they follow the person to their other devices, encrypted so only those
60
- devices can read them.
116
+ `type` is `text`, `secret` or `boolean`. A secret stays hidden until the person shows it. A
117
+ `boolean` is a switch, and `"default": true` starts it on. The person can change settings while the
118
+ extension is switched off.
119
+
120
+ The extension reads a setting with `context.settings.get('api-key')`, which is undefined until the
121
+ person sets it, and hears every change with `context.settings.subscribe`. A switch always reads
122
+ `'true'` or `'false'`. Settings belong to the person, not to a graph. Once their account syncs a
123
+ graph, the settings follow them to their other devices, encrypted so that only those devices can read
124
+ them.
125
+
126
+ ### Storage
127
+
128
+ `context.storage` keeps the extension's own values on the device, as JSON, with `get`, `set` and
129
+ `remove`. It is for what the extension works out or caches, not for what the person sets.
130
+
131
+ ### Fence widgets
132
+
133
+ A fence widget draws over a fenced block in a document, in place of its text. The extension
134
+ registers one for an info word, which is either its id or `<id>.<name>`:
135
+
136
+ - **Its id** (```` ```hello ````) makes an ordinary fence. Without the extension it shows as a code
137
+ block, so the document reads well anywhere.
138
+ - **`<id>.<name>`** (```` ```hello.board ````) makes an Extension Fence, which holds the extension's
139
+ data. EtherPK never searches its lines and never publishes them as code. The caret passes over it,
140
+ and where the extension is not running, a note says so.
141
+
142
+ ```ts
143
+ context.editor.registerFence('hello', {
144
+ height: () => 40,
145
+ mount(host, initial) {
146
+ const line = document.createElement('p')
147
+ const show = (fence: { body: readonly string[] }) => {
148
+ line.textContent = `${fence.body.length} lines`
149
+ }
150
+ show(initial)
151
+ host.append(line)
152
+ return { update: show, destroy: () => line.remove() }
153
+ },
154
+ })
155
+ ```
156
+
157
+ `height` must answer before the widget is drawn, so the lines below never move as it loads. The
158
+ context a widget is given has the fence's lines (`body`), whether the document can be written
159
+ (`writable`), and the theme. It can also:
160
+
161
+ - Change the lines (`edit`), as one change that Mod+Z undoes.
162
+ - Show the fence as text (`editAsText`), or delete it (`remove`).
163
+ - Open the fence in a tab of its own, or over the whole window.
164
+
165
+ The third argument to `registerFence` adds options:
166
+
167
+ - `title`, what the fence is called in its tab.
168
+ - `fromPaste`, which turns text pasted on a line of its own into the fence. A line it answers with
169
+ that would end the fence is changed so it can't.
170
+ - `publish`, which says how a published site shows the fence, as a picture or a link card. An SVG
171
+ picture is cleaned of scripts, event handlers and links that run code before the site gets it.
172
+ - `identify`, which finds a fence again by an id in its body, so it can open in a tab.
173
+ - `keepsHistory`, which leaves Mod+Z to the widget while it has the keyboard.
174
+
175
+ ### A block at the caret
176
+
177
+ `context.editor.insertBlock(text, { focus })` puts lines at the caret, as a code block or a table
178
+ goes in. It returns false where nothing went in, such as a document that cannot be written, a
179
+ Protected Document, or a caret in the frontmatter, in a fence or in a table.
180
+
181
+ ### Dialogs
182
+
183
+ `context.dialogs.prompt(options)` asks for one line of text in a dialog the Client draws. Its
184
+ `validate` function can refuse an answer by returning a message, and the dialog stays open with the
185
+ text kept. It resolves to the text, trimmed, or undefined when the person cancels. The dialog names
186
+ the extension, and shows every string it is given as text.
187
+
188
+ ### Frames
189
+
190
+ `context.embeds.frame({ src, title })` builds a frame showing a page from a host the Client allows,
191
+ and throws for any other host. `context.embeds.frameHosts` lists the hosts: the video players of
192
+ YouTube (`www.youtube-nocookie.com`) and Vimeo (`player.vimeo.com`).
193
+
194
+ ### Views
195
+
196
+ A View is the content of a tab. The manifest declares each View kind, and the extension supplies it
197
+ with `context.views.register(kind, view)` as the graph opens.
198
+
199
+ ```json
200
+ "views": [
201
+ {
202
+ "kind": "hello.board",
203
+ "title": "Board: {target}",
204
+ "region": "main",
205
+ "address": { "segment": "board", "target": "concept" }
206
+ }
207
+ ]
208
+ ```
209
+
210
+ `kind` starts with the extension's id. `{target}` in the title stands for the View's target, here a
211
+ concept. `region` is where the kind opens: `main`, `left-sidebar` or `right-sidebar`. A kind with
212
+ one View per graph gives a fixed `target` instead.
213
+
214
+ An `address` gives the View a URL. For an extension from the Extension Directory, the URL is
215
+ `/g/<graph>/x/<extension id>/<segment>`, followed by the concept when `target` is `concept`. A link
216
+ to it opens the View. On a device without the extension, it opens a page that names the extension
217
+ and offers to install it, if the directory lists it.
218
+
219
+ `context.layout.openView({ kind, target })` opens a View. `context.layout.isDesktop()` says whether
220
+ the desktop layout is showing, which it is at 1024 pixels wide and more.
221
+
222
+ ## Styles
223
+
224
+ The manifest's `styles` are the extension's stylesheets. By default, each widget and View the
225
+ extension draws is mounted in a shadow root with them, so the extension's styles and the Client's
226
+ stay apart.
227
+
228
+ Some libraries can't run in a shadow root. An extension that uses one can set
229
+ `"isolation": "page"` to mount in the page instead. Then every rule in its stylesheets must style
230
+ only elements with a class that starts with the extension's id, or elements inside one. The Client
231
+ refuses any other stylesheet. The colours of the Client's theme are CSS variables (`--gk-*`) that
232
+ reach into a shadow root as well, so an extension can follow the light and dark themes either way.
233
+ An extension's stylesheet can't register one of them with `@property`.
234
+
235
+ Either way, the shadow root and the check keep styles apart, and nothing more. They don't limit
236
+ what the extension's code can do in the page.
61
237
 
62
238
  ## Writing to the page
63
239
 
64
- The Client enforces [Trusted Types](https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API),
65
- and extension code runs under it:
240
+ The Client enforces
241
+ [Trusted Types](https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API), and extension
242
+ code runs under them.
66
243
 
67
- - **An extension cannot create a Trusted Types policy.** The Client's Content Security Policy names
68
- the policies it allows, and each belongs to the Client or a library it ships.
69
244
  - **Write no HTML.** Build elements with the DOM (`createElement`, `createElementNS`,
70
245
  `textContent`). A string written to an HTML sink such as `innerHTML` is sanitized by the Client's
71
246
  default policy, which may remove what you wrote.
72
- - **Draw icons with `context.icons.svg`.** It returns `IconMarkup`, which Svelte's `{@html}` takes
73
- as it is. The icons your manifest adds are sanitized as SVG before they are drawn, so they can
74
- hold only SVG.
247
+ - **An extension can't create a Trusted Types policy.** The Client's Content Security Policy names
248
+ the policies it allows, and each one belongs to the Client or to a library it ships.
249
+ - **Icons come from the manifest.** The Client sanitizes the icons a manifest declares as SVG, so
250
+ they can hold only SVG.
251
+
252
+ ## Proposed parts
253
+
254
+ The Proposed parts are for the extensions that come with EtherPK, and may change in any release.
255
+ They include:
256
+
257
+ - Context Menu rows (`contextMenu`), key chords (`keybindings`) and Graph Sidebar buttons
258
+ (`graphSidebar`).
259
+ - Events (`events`), questions to the graph's index (`index`) and the rules for naming a concept
260
+ (`concepts`).
261
+ - The theme (`theme`), the Client's icons (`icons`) and a View that lives in a Sidebar (`resident`).
262
+ - Running another extension's Commands (`commands.execute`) and opening documents
263
+ (`layout.openDocument`).
264
+
265
+ `contributions`, which reaches the Client's own registries, stays Proposed for good, since its
266
+ shape is the Client's internals.
267
+
268
+ ### Graph Sidebar buttons
269
+
270
+ A button in the Graph Sidebar, below Search and today's date, runs a Command. These rules keep the
271
+ buttons readable together:
272
+
273
+ - **`title`** is short. Two buttons sit side by side in the sidebar at its default width of 304
274
+ pixels, and "Graph Kanban", with its icon, is about the widest that fits.
275
+ - **`description`** says what the button does, such as "Open a map of every place and route in this
276
+ graph". The Client shows it as the button's help text. Don't write a keyboard shortcut in it: the
277
+ Client adds the key chords bound to the button's Command itself.
278
+ - **`icon`** is clear on its own. On a phone the buttons show their icons only.
279
+ - **`when`** hides the button where it does not apply. A button for the desktop alone passes
280
+ `when: () => context.layout.isDesktop()`. A `when` that throws hides the button too.
281
+ - **`order`** places the button among the others. The Graph Map's is 20 and the Graph Kanban's is
282
+ 30.
283
+
284
+ ## Trying an extension
285
+
286
+ To try an extension, build it, and start a development copy of the EtherPK Client with the
287
+ extension's folder named in `ETHERPK_DEV_EXTENSIONS`. Separate several folders with commas. The
288
+ Client loads each one as it loads the extensions it comes with, and one with the same id replaces
289
+ it.
290
+
291
+ ```bash
292
+ ETHERPK_DEV_EXTENSIONS=/path/to/my-extension pnpm dev
293
+ ```
294
+
295
+ [The EtherPK Client repository](https://github.com/appsoftwareltd/etherpk-client) says how to run
296
+ the Client.
297
+
298
+ ## Listing an extension
299
+
300
+ The [Extension Directory](https://github.com/appsoftwareltd/etherpk-extension-directory) lists the
301
+ extensions people can install from inside EtherPK. Each release is a pull request there. It is
302
+ rebuilt from its tagged source and compared with its npm package, and a person reviews it before it
303
+ is listed. Installed extensions update by themselves to the newest listed release the person's
304
+ EtherPK can run. The directory's contributing guide says what a release needs, including the size
305
+ limits.
306
+
307
+ ## Licence
308
+
309
+ MIT.
package/dist/context.d.ts CHANGED
@@ -4,8 +4,12 @@
4
4
  * Everything an extension adds through it sits under the extension's id and is removed when the
5
5
  * graph closes or the extension is switched off, so an extension never has to undo its own
6
6
  * registrations. It holds no document store, no keys and no sync: an extension reads documents
7
- * only through the index's answers, and changes them only by running the Client's Commands.
7
+ * only through the index's answers and its own fences, and changes them only through its fences,
8
+ * the blocks it puts at the caret and the Client's Commands.
8
9
  */
10
+ import type { ExtensionDialogs } from './dialogs';
11
+ import type { ExtensionEditor } from './editor';
12
+ import type { ExtensionEmbeds } from './embeds';
9
13
  import type { ExtensionEvents } from './events';
10
14
  import type { ExtensionIndex } from './graph-index';
11
15
  import type { CommandMenuItem, ContextMenuItem, GraphSidebarButton } from './menus';
@@ -13,26 +17,34 @@ import type { ViewContribution, ViewRef } from './views';
13
17
  /**
14
18
  * A Command's handler. What it returns, or resolves to, is what `execute` resolves to.
15
19
  *
16
- * @beta
20
+ * @public
17
21
  */
18
22
  export type CommandHandler = (arg?: unknown) => unknown;
19
23
  /**
20
24
  * Running and adding Commands.
21
25
  *
22
- * @beta
26
+ * @public
23
27
  */
24
28
  export interface ExtensionCommands {
25
29
  /** Add a Command, its id under the extension's id. Throws if the id is taken or outside it. */
26
30
  register(id: string, handler: CommandHandler): () => void;
27
- /** Run any Command, the Client's or an extension's. Rejects if no Command has the id. */
31
+ /**
32
+ * Run any Command, the Client's or an extension's. Rejects if no Command has the id.
33
+ *
34
+ * @beta
35
+ */
28
36
  execute(id: string, arg?: unknown): Promise<unknown>;
29
- /** Whether any Command has the id. */
37
+ /**
38
+ * Whether any Command has the id.
39
+ *
40
+ * @beta
41
+ */
30
42
  has(id: string): boolean;
31
43
  }
32
44
  /**
33
45
  * Supplying the Views the manifest declares.
34
46
  *
35
- * @beta
47
+ * @public
36
48
  */
37
49
  export interface ExtensionViews {
38
50
  /**
@@ -53,14 +65,14 @@ export interface ExtensionContextMenu {
53
65
  /**
54
66
  * Adding Command Menu rows.
55
67
  *
56
- * @beta
68
+ * @public
57
69
  */
58
70
  export interface ExtensionCommandMenu {
59
71
  /** Add a row, its id under the extension's id. */
60
72
  register(item: CommandMenuItem): () => void;
61
73
  }
62
74
  /**
63
- * Adding buttons to the Graph Sidebar, under Today's journal.
75
+ * Adding buttons to the Graph Sidebar's group of action buttons, after Search and today's date.
64
76
  *
65
77
  * @beta
66
78
  */
@@ -95,7 +107,7 @@ export interface ExtensionKeybindings {
95
107
  /**
96
108
  * Opening Views and documents in the open graph's Layout.
97
109
  *
98
- * @beta
110
+ * @public
99
111
  */
100
112
  export interface ExtensionLayout {
101
113
  /**
@@ -107,19 +119,29 @@ export interface ExtensionLayout {
107
119
  inPaneOf?: string;
108
120
  beside?: string;
109
121
  }): void;
110
- /** Bring a Sidebar resident to the front of its expanded Sidebar. */
122
+ /**
123
+ * Bring a Sidebar resident to the front of its expanded Sidebar.
124
+ *
125
+ * @beta
126
+ */
111
127
  reveal(kind: string): void;
112
128
  /**
113
129
  * Open the document of a concept, under the name it resolves to, as following a wikilink
114
130
  * does. `line` scrolls to and selects that line, `inPaneOf` and `beside` place it as for
115
131
  * {@link ExtensionLayout.openView}.
132
+ *
133
+ * @beta
116
134
  */
117
135
  openDocument(concept: string, options?: {
118
136
  line?: number;
119
137
  inPaneOf?: string;
120
138
  beside?: string;
121
139
  }): void;
122
- /** The document the user is working in, or null. */
140
+ /**
141
+ * The document the user is working in, or null.
142
+ *
143
+ * @beta
144
+ */
123
145
  activeDocument(): string | null;
124
146
  /** True while the desktop presenter is showing, 1024 pixels wide and more. */
125
147
  isDesktop(): boolean;
@@ -139,7 +161,7 @@ export interface ExtensionConcepts {
139
161
  * The extension's own storage on this device, under `etherpk-<extension id>:` in the browser's
140
162
  * localStorage. Values are stored as JSON.
141
163
  *
142
- * @beta
164
+ * @public
143
165
  */
144
166
  export interface ExtensionStorage {
145
167
  /** The value stored under `key`, or undefined when there is none or it cannot be read. */
@@ -155,7 +177,7 @@ export interface ExtensionStorage {
155
177
  * keeps them with the person: on this device, and in their account where it syncs a graph, so they
156
178
  * follow the person to their other devices. The extension only reads them.
157
179
  *
158
- * @beta
180
+ * @public
159
181
  */
160
182
  export interface ExtensionSettings {
161
183
  /**
@@ -232,54 +254,112 @@ export interface ExtensionContributions {
232
254
  /**
233
255
  * What an extension is started with, once for each graph that opens.
234
256
  *
235
- * @beta
257
+ * @public
236
258
  */
237
259
  export interface ExtensionContext {
238
- /** The extension's id, from its manifest. */
260
+ /**
261
+ * The extension's id, from its manifest.
262
+ *
263
+ * @beta
264
+ */
239
265
  readonly extensionId: string;
240
- /** The open graph. */
266
+ /**
267
+ * The open graph.
268
+ *
269
+ * @beta
270
+ */
241
271
  readonly graphId: string;
242
- /** The Client's version, `x.y.z`. */
272
+ /**
273
+ * The Client's version, `x.y.z`.
274
+ *
275
+ * @beta
276
+ */
243
277
  readonly clientVersion: string;
244
- /** True in a development build of the Client, where tests may reach in. */
278
+ /**
279
+ * True in a development build of the Client, where tests may reach in.
280
+ *
281
+ * @beta
282
+ */
245
283
  readonly dev: boolean;
246
284
  /** Supplying the Views the manifest declares. */
247
285
  readonly views: ExtensionViews;
248
286
  /** Running and adding Commands. */
249
287
  readonly commands: ExtensionCommands;
250
- /** Adding Context Menu rows. */
288
+ /**
289
+ * Adding Context Menu rows.
290
+ *
291
+ * @beta
292
+ */
251
293
  readonly contextMenu: ExtensionContextMenu;
252
294
  /** Adding Command Menu rows. */
253
295
  readonly commandMenu: ExtensionCommandMenu;
254
- /** Adding buttons to the Graph Sidebar. */
296
+ /**
297
+ * Adding buttons to the Graph Sidebar.
298
+ *
299
+ * @beta
300
+ */
255
301
  readonly graphSidebar: ExtensionGraphSidebar;
256
- /** Adding key chords. */
302
+ /**
303
+ * Adding key chords.
304
+ *
305
+ * @beta
306
+ */
257
307
  readonly keybindings: ExtensionKeybindings;
258
- /** Every Contribution Point by kind, for the kinds the parts above do not cover. */
308
+ /**
309
+ * Every Contribution Point by kind, for the kinds the parts above do not cover.
310
+ *
311
+ * @beta
312
+ */
259
313
  readonly contributions: ExtensionContributions;
260
- /** Listening to Events. */
314
+ /**
315
+ * Listening to Events.
316
+ *
317
+ * @beta
318
+ */
261
319
  readonly events: ExtensionEvents;
262
- /** Questions to the open graph's index. */
320
+ /**
321
+ * Questions to the open graph's index.
322
+ *
323
+ * @beta
324
+ */
263
325
  readonly index: ExtensionIndex;
264
326
  /** Opening Views and documents. */
265
327
  readonly layout: ExtensionLayout;
266
- /** The rules for naming a concept. */
328
+ /**
329
+ * The rules for naming a concept.
330
+ *
331
+ * @beta
332
+ */
267
333
  readonly concepts: ExtensionConcepts;
268
334
  /** The extension's own storage on this device. */
269
335
  readonly storage: ExtensionStorage;
270
336
  /** The person's answers to the settings the manifest declares. */
271
337
  readonly settings: ExtensionSettings;
272
- /** The Client's light or dark theme. */
338
+ /**
339
+ * The Client's light or dark theme.
340
+ *
341
+ * @beta
342
+ */
273
343
  readonly theme: ExtensionTheme;
274
- /** The Client's icons and the extension's own. */
344
+ /**
345
+ * The Client's icons and the extension's own.
346
+ *
347
+ * @beta
348
+ */
275
349
  readonly icons: ExtensionIcons;
350
+ /** Widgets over the extension's own fences, and blocks put at the caret. */
351
+ readonly editor: ExtensionEditor;
352
+ /** Dialogs the Client draws. */
353
+ readonly dialogs: ExtensionDialogs;
354
+ /** Frames showing content from the hosts the Client allows. */
355
+ readonly embeds: ExtensionEmbeds;
276
356
  /** Say something in passing, as the Client's own notices do: "Coordinates copied." */
277
357
  notify(text: string): void;
278
358
  }
279
359
  /**
280
360
  * What an extension's main module exports.
281
361
  *
282
- * @beta
362
+ * @public
283
363
  */
284
364
  export interface ExtensionModule {
285
365
  /**
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Dialogs the Client draws for an extension, so every extension's questions look and behave like the
3
+ * Client's own: the keyboard stays inside while one is open, Enter answers, Esc cancels, and the
4
+ * keyboard returns where it was when one closes. Each dialog names the extension that asks, and
5
+ * shows every string it is given as plain text, never as markup.
6
+ */
7
+ /**
8
+ * A question with one line of text for an answer.
9
+ *
10
+ * @public
11
+ */
12
+ export interface PromptOptions {
13
+ /** The dialog's heading. */
14
+ title: string;
15
+ /** The text field's label. */
16
+ label: string;
17
+ /** Example text shown in the empty field. */
18
+ placeholder?: string;
19
+ /** The field's text when the dialog opens. */
20
+ value?: string;
21
+ /** The button that answers. "OK" when left out. */
22
+ submitLabel?: string;
23
+ /**
24
+ * Checks the answer when it is given: a message saying what is wrong, shown at the field, or
25
+ * undefined when it is fine. The dialog stays open, with the text kept, until it is fine.
26
+ */
27
+ validate?(value: string): string | undefined;
28
+ }
29
+ /**
30
+ * Dialogs.
31
+ *
32
+ * @public
33
+ */
34
+ export interface ExtensionDialogs {
35
+ /** Ask for one line of text. Resolves to the text, trimmed, or undefined when it was cancelled. */
36
+ prompt(options: PromptOptions): Promise<string | undefined>;
37
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Dialogs the Client draws for an extension, so every extension's questions look and behave like the
3
+ * Client's own: the keyboard stays inside while one is open, Enter answers, Esc cancels, and the
4
+ * keyboard returns where it was when one closes. Each dialog names the extension that asks, and
5
+ * shows every string it is given as plain text, never as markup.
6
+ */
7
+ export {};
@@ -0,0 +1,236 @@
1
+ /**
2
+ * What an extension may do in the document editor: draw a widget over its own kind of fenced block,
3
+ * and put a block at the caret.
4
+ *
5
+ * A widget never sees the editor. It is told the fence's body as lines of text and asks for changes
6
+ * to those lines, which the Client writes as ordinary editor changes: undone with the editor's undo,
7
+ * synced and merged like typing, and refused where the document cannot be written.
8
+ */
9
+ /**
10
+ * One change to a fence's body, by line number from 0. A replacement or a removal names the text the
11
+ * widget last read on that line, and applies only while the line still says it, so a widget acting on
12
+ * an old reading never lands on someone else's edit.
13
+ *
14
+ * @public
15
+ */
16
+ export type FenceEdit = {
17
+ kind: 'insert';
18
+ line: number;
19
+ text: string;
20
+ } | {
21
+ kind: 'replace';
22
+ line: number;
23
+ expect: string;
24
+ text: string;
25
+ } | {
26
+ kind: 'remove';
27
+ line: number;
28
+ expect: string;
29
+ };
30
+ /**
31
+ * What a widget is told about its fence.
32
+ *
33
+ * @public
34
+ */
35
+ export interface FenceInfo {
36
+ /** The fence's info word, the one the widget was registered for. */
37
+ readonly info: string;
38
+ /** The fence's body: its lines between the fences, less their indentation. */
39
+ readonly body: readonly string[];
40
+ /** The document the fence is in, by its concept, or null outside a workspace. */
41
+ readonly document: string | null;
42
+ /**
43
+ * How many fences with this info word come before this one in the document. It changes when one
44
+ * is added or removed above, so it keys only what lasts a moment.
45
+ */
46
+ readonly ordinal: number;
47
+ }
48
+ /**
49
+ * Where a widget is drawn: in the document, in a tab of its own, or filling the EtherPK window.
50
+ *
51
+ * @public
52
+ */
53
+ export type FencePresentation = 'inline' | 'tab' | 'window';
54
+ /**
55
+ * What a mounted widget can see and ask for.
56
+ *
57
+ * @public
58
+ */
59
+ export interface FenceContext extends FenceInfo {
60
+ /** Whether the document accepts the widget's changes now: not read-only, not locked. */
61
+ readonly writable: boolean;
62
+ /** Whether the dark theme shows. */
63
+ readonly dark: boolean;
64
+ /**
65
+ * Whether anyone can see the widget: false while its tab is behind another, its Pane is collapsed
66
+ * or the browser tab is hidden. A widget holding something costly lets it go while this is false.
67
+ */
68
+ readonly onScreen: boolean;
69
+ /** Where the widget is drawn. */
70
+ readonly presentation: FencePresentation;
71
+ /**
72
+ * Change the body, as one undoable change: one edit, or several applied together. False when it
73
+ * was refused (the document cannot be written, the fence is gone, or a line no longer says what an
74
+ * edit expects), and then nothing changed. The widget hears the current body in an `update`.
75
+ */
76
+ edit(change: FenceEdit | readonly FenceEdit[]): boolean;
77
+ /** Show the fence's text in the editor, with the caret on its first body line. */
78
+ editAsText(): void;
79
+ /** Delete the fence, as one undoable change. False when it was refused. */
80
+ remove(): boolean;
81
+ /** The widget's height changed: the editor asks `height` again. */
82
+ resized(): void;
83
+ /**
84
+ * True once, when the widget is to take the keyboard: for the widget of a fence `insertBlock` put
85
+ * in with `focus`, and for a widget drawn in a tab or over the window as it opens. False otherwise.
86
+ */
87
+ takeFocus(): boolean;
88
+ /**
89
+ * Open the fence in a tab of its own, which draws the same widget with `presentation` `tab`. Only
90
+ * for a fence the registration can `identify`.
91
+ */
92
+ openInTab(): void;
93
+ /**
94
+ * Draw the widget again over the whole EtherPK window, with `presentation` `window`, and a Return
95
+ * button the Client draws. Only for a fence the registration can `identify`.
96
+ */
97
+ openWindow(): void;
98
+ /** Close the window this widget fills. Does nothing for a widget drawn elsewhere. */
99
+ closeWindow(): void;
100
+ }
101
+ /**
102
+ * A widget the extension has drawn, as the Client drives it.
103
+ *
104
+ * @public
105
+ */
106
+ export interface MountedFence {
107
+ /** The body, the theme, whether it can be written or whether it is on screen changed. */
108
+ update(context: FenceContext): void;
109
+ /** The widget leaves: scrolled far away, its fence deleted, its tab or the editor closed. */
110
+ destroy(): void;
111
+ }
112
+ /**
113
+ * What an extension registers for one info word.
114
+ *
115
+ * @public
116
+ */
117
+ export interface FenceWidget {
118
+ /**
119
+ * The widget's height in CSS pixels, before and after it mounts. The editor reserves it so the
120
+ * lines below never move while the widget loads. It is asked again after `resized`. Not asked for
121
+ * a widget in a tab or a window, which fills what it is given.
122
+ */
123
+ height(info: FenceInfo): number;
124
+ /** Draw the widget into `host`, which the Client owns and places. */
125
+ mount(host: HTMLElement, context: FenceContext): MountedFence;
126
+ }
127
+ /**
128
+ * A fence as the publisher hands it to the extension: already public content.
129
+ *
130
+ * @public
131
+ */
132
+ export interface PublishFenceRequest {
133
+ /** The fence's info word. */
134
+ readonly info: string;
135
+ /** The fence's body lines. */
136
+ readonly body: readonly string[];
137
+ /** The document being published, by its concept. */
138
+ readonly document: string;
139
+ }
140
+ /**
141
+ * How a published site shows a fence: a picture the site keeps, or a link with an optional picture
142
+ * from the web. The publisher writes the HTML, so an extension never writes markup into a site.
143
+ *
144
+ * @public
145
+ */
146
+ export type PublishedFence = {
147
+ kind: 'picture';
148
+ /** The picture's type. */
149
+ type: 'image/svg+xml' | 'image/png' | 'image/webp';
150
+ /** The picture's bytes, written into the site as a file of its own. */
151
+ bytes: Uint8Array;
152
+ width: number;
153
+ height: number;
154
+ /** What the picture shows, for anyone who cannot see it. */
155
+ alt: string;
156
+ } | {
157
+ kind: 'link';
158
+ /** Where the link goes: an `https` address. */
159
+ href: string;
160
+ /** The link's text. */
161
+ text: string;
162
+ /** A picture from the web shown in the link: an `https` address. */
163
+ image?: {
164
+ src: string;
165
+ alt: string;
166
+ width?: number;
167
+ height?: number;
168
+ };
169
+ };
170
+ /**
171
+ * Options for a fence registration.
172
+ *
173
+ * @public
174
+ */
175
+ export interface FenceOptions {
176
+ /**
177
+ * A short name for what the fence holds, such as "Drawing": a tab's title and the full window's
178
+ * heading start with it.
179
+ */
180
+ title?: string;
181
+ /**
182
+ * What text pasted as plain text on an empty line or bullet becomes: the body lines of a fence of
183
+ * this kind, or null to leave the paste as it is. A link to a video, say, gives the link as the
184
+ * one line. The text is the clipboard's own, untrimmed. The Client asks each registration in
185
+ * turn, and the first that answers takes the paste. Mod+Z straight after it gives back the
186
+ * text that was pasted.
187
+ *
188
+ * The lines are written inside the fence as they are, except that a line which would end the
189
+ * fence, or start another, is changed so it cannot. A line break inside a line splits it in two.
190
+ */
191
+ fromPaste?(text: string): readonly string[] | null;
192
+ /**
193
+ * How a published site shows the fence, or null to leave it out. Called only for public content,
194
+ * in a publish run in the browser. Where it is not called (the Headless Client, or the extension
195
+ * off), an Extension Fence is left out and an ordinary fence is published as code.
196
+ */
197
+ publish?(fence: PublishFenceRequest): PublishedFence | null | Promise<PublishedFence | null>;
198
+ /**
199
+ * The fence's own id, read from its body, or null. A fence with an id can be opened in a tab or a
200
+ * window, where the Client finds it again by the id after every change.
201
+ */
202
+ identify?(body: readonly string[]): string | null;
203
+ /**
204
+ * True when the widget keeps its own undo: Mod+Z and Mod+Y pressed inside it are its own, rather
205
+ * than the editor's.
206
+ */
207
+ keepsHistory?: boolean;
208
+ }
209
+ /**
210
+ * The document editor, as far as an extension may reach it.
211
+ *
212
+ * @public
213
+ */
214
+ export interface ExtensionEditor {
215
+ /**
216
+ * Draw `widget` over every complete fence whose info word is `info`. The word is the extension's
217
+ * id (an ordinary fence, whose text stays text) or `<id>.<name>` (an Extension Fence, whose body
218
+ * is the extension's data and never searched or shown as text). Throws for any other word, or one
219
+ * that is taken.
220
+ *
221
+ * An extension listed in the Extension Directory may not have an id that is one of the words the
222
+ * Client draws blocks for itself: `map`, `math`, `mermaid` and `etherpk-cipher`.
223
+ */
224
+ registerFence(info: string, widget: FenceWidget, options?: FenceOptions): () => void;
225
+ /**
226
+ * Put a block at the caret of the editor the person is working in, as a code block or a table
227
+ * goes in: an empty bullet takes it as its content, an empty line is replaced, and after any other
228
+ * line it goes on new lines beneath at the line's content column. `text` is the block's lines
229
+ * without indentation. A block that is a fence leaves the caret outside it, and with `focus` its
230
+ * widget is told to `takeFocus`. False where nothing was put in: no editor, a body that cannot be
231
+ * written, or a caret in the frontmatter, a fence or a table.
232
+ */
233
+ insertBlock(text: string, options?: {
234
+ focus?: boolean;
235
+ }): boolean;
236
+ }
package/dist/editor.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * What an extension may do in the document editor: draw a widget over its own kind of fenced block,
3
+ * and put a block at the caret.
4
+ *
5
+ * A widget never sees the editor. It is told the fence's body as lines of text and asks for changes
6
+ * to those lines, which the Client writes as ordinary editor changes: undone with the editor's undo,
7
+ * synced and merged like typing, and refused where the document cannot be written.
8
+ */
9
+ export {};
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Content from another site shown inside the Client (ADR 0137). The Client allows frames from a short
3
+ * list of hosts only, and builds every frame itself, with the referrer policy, sandbox and
4
+ * permissions those hosts need, so an extension never writes an `<iframe>`.
5
+ */
6
+ /**
7
+ * A frame to build.
8
+ *
9
+ * @public
10
+ */
11
+ export interface FrameOptions {
12
+ /**
13
+ * The address to show: an `https` address on a host the Client allows, which today are
14
+ * `https://www.youtube-nocookie.com` and `https://player.vimeo.com`.
15
+ */
16
+ src: string;
17
+ /** What the frame shows, read out to anyone using a screen reader. Plain text, never markup. */
18
+ title: string;
19
+ }
20
+ /**
21
+ * Frames.
22
+ *
23
+ * @public
24
+ */
25
+ export interface ExtensionEmbeds {
26
+ /** The hosts a frame may show, as origins such as `https://player.vimeo.com`. */
27
+ readonly frameHosts: readonly string[];
28
+ /**
29
+ * A frame the Client built for `src`, to put in the extension's own element. It loads once it is
30
+ * in the page. Throws for an address on a host the Client does not allow.
31
+ */
32
+ frame(options: FrameOptions): HTMLIFrameElement;
33
+ }
package/dist/embeds.js ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Content from another site shown inside the Client (ADR 0137). The Client allows frames from a short
3
+ * list of hosts only, and builds every frame itself, with the referrer policy, sandbox and
4
+ * permissions those hosts need, so an extension never writes an `<iframe>`.
5
+ */
6
+ export {};
package/dist/index.d.ts CHANGED
@@ -7,15 +7,20 @@
7
7
  * manifest and of the contract a View follows, and {@link readExtensionPackage}, which checks a
8
8
  * manifest the way the Client does. It holds none of the Client's implementation.
9
9
  *
10
- * Every declaration is Proposed (`@beta`) until extensions from other publishers can be installed:
11
- * until then it may change in any release. Stable declarations (`@public`) will never break.
10
+ * A declaration, or a member of one, is Stable (`@public`) or Proposed (`@beta`). A Stable one never
11
+ * breaks: it is deprecated and kept. A Proposed one may change in any release, and only the
12
+ * extensions that come with EtherPK may use it. An extension from the Extension Directory uses the
13
+ * Stable part alone, which `etc/etherpk-extension-api.public.api.md` lists.
12
14
  *
13
15
  * @packageDocumentation
14
16
  */
15
17
  export type { CommandHandler, ExtensionCommandMenu, ExtensionCommands, ExtensionConcepts, ExtensionContext, ExtensionContextMenu, ExtensionContributions, ExtensionGraphSidebar, ExtensionIcons, ExtensionKeybindings, ExtensionLayout, ExtensionModule, ExtensionSettings, ExtensionStorage, ExtensionTheme, ExtensionViews, IconMarkup, Keybinding, } from './context';
18
+ export type { ExtensionDialogs, PromptOptions } from './dialogs';
19
+ export type { ExtensionEditor, FenceContext, FenceEdit, FenceInfo, FenceOptions, FencePresentation, FenceWidget, MountedFence, PublishedFence, PublishFenceRequest, } from './editor';
20
+ export type { ExtensionEmbeds, FrameOptions } from './embeds';
16
21
  export type { ExtensionEventName, ExtensionEventPayloads, ExtensionEvents } from './events';
17
22
  export type { ConceptCandidate, ConceptKind, ExtensionIndex, IndexUpdate, LinkGraph, LinkGraphConcept, LinkGraphLink } from './graph-index';
18
- export type { ExtensionManifest, ExtensionPackage, ReadExtensionPackageResult, SettingDeclaration, ViewDeclaration, ViewRegion, } from './manifest';
23
+ export type { ExtensionIsolation, ExtensionManifest, ExtensionPackage, ReadExtensionPackageResult, SettingDeclaration, ViewDeclaration, ViewRegion, } from './manifest';
19
24
  export { readExtensionPackage } from './manifest';
20
25
  export type { CommandMenuContext, CommandMenuItem, ContextMenuItem, DocumentMenuTarget, ExtensionMenuTarget, GraphSidebarButton, MenuTarget, TabMenuTarget, WikilinkMenuTarget, } from './menus';
21
26
  export type { MountedView, ViewContribution, ViewMountProps, ViewRef, ViewVisibility } from './views';
package/dist/index.js CHANGED
@@ -7,8 +7,10 @@
7
7
  * manifest and of the contract a View follows, and {@link readExtensionPackage}, which checks a
8
8
  * manifest the way the Client does. It holds none of the Client's implementation.
9
9
  *
10
- * Every declaration is Proposed (`@beta`) until extensions from other publishers can be installed:
11
- * until then it may change in any release. Stable declarations (`@public`) will never break.
10
+ * A declaration, or a member of one, is Stable (`@public`) or Proposed (`@beta`). A Stable one never
11
+ * breaks: it is deprecated and kept. A Proposed one may change in any release, and only the
12
+ * extensions that come with EtherPK may use it. An extension from the Extension Directory uses the
13
+ * Stable part alone, which `etc/etherpk-extension-api.public.api.md` lists.
12
14
  *
13
15
  * @packageDocumentation
14
16
  */
@@ -6,14 +6,14 @@
6
6
  /**
7
7
  * Where a View kind opens when nothing says otherwise.
8
8
  *
9
- * @beta
9
+ * @public
10
10
  */
11
11
  export type ViewRegion = 'main' | 'left-sidebar' | 'right-sidebar';
12
12
  /**
13
13
  * One View kind an extension adds, declared so the Client can show its tabs, titles and loading
14
14
  * state before any of the extension's code has run.
15
15
  *
16
- * @beta
16
+ * @public
17
17
  */
18
18
  export interface ViewDeclaration {
19
19
  /** The kind, `<extension id>.<name>`: the key every saved Layout stores it under. */
@@ -35,15 +35,18 @@ export interface ViewDeclaration {
35
35
  /**
36
36
  * The View lives in a Sidebar from the moment a graph opens, beside the Client's own residents,
37
37
  * and the Sidebar's toggle brings it back if it was closed. `desktopOnly` keeps it off a phone.
38
+ *
39
+ * @beta
38
40
  */
39
41
  resident?: {
40
42
  side: 'left' | 'right';
41
43
  desktopOnly?: boolean;
42
44
  };
43
45
  /**
44
- * A short address of the View's own: `/g/<graph>/<segment>`, followed by the target when
45
- * `target` is `concept`. Only a Built-in Extension may declare one. A View kind whose address
46
- * takes a concept follows that concept through a rename.
46
+ * A short address of the View's own, followed by the target when `target` is `concept`: for an
47
+ * extension from the Extension Directory `/g/<graph>/x/<extension id>/<segment>`, and for one that
48
+ * comes with EtherPK `/g/<graph>/<segment>`. A View kind whose address takes a concept follows
49
+ * that concept through a rename.
47
50
  */
48
51
  address?: {
49
52
  segment: string;
@@ -57,7 +60,7 @@ export interface ViewDeclaration {
57
60
  * a graph, so it follows them to their other devices. The extension reads it through
58
61
  * `context.settings`.
59
62
  *
60
- * @beta
63
+ * @public
61
64
  */
62
65
  export interface SettingDeclaration {
63
66
  /** Unique within the extension: lowercase letters, digits and hyphens, such as `mapbox-token`. */
@@ -81,10 +84,20 @@ export interface SettingDeclaration {
81
84
  url: string;
82
85
  };
83
86
  }
87
+ /**
88
+ * Where a loaded extension's Views and fence widgets mount. `shadow`, the default, puts each in a
89
+ * shadow root holding the extension's stylesheets, so neither side's CSS reaches the other. `page`
90
+ * mounts them in the page and has the document adopt the stylesheets, for a library that cannot work
91
+ * in a shadow root. The Client then checks every rule is scoped under a class whose name starts with
92
+ * the extension's id, and refuses to start the extension otherwise.
93
+ *
94
+ * @public
95
+ */
96
+ export type ExtensionIsolation = 'shadow' | 'page';
84
97
  /**
85
98
  * The `etherpk` field of an extension's package.json.
86
99
  *
87
- * @beta
100
+ * @public
88
101
  */
89
102
  export interface ExtensionManifest {
90
103
  /**
@@ -101,10 +114,13 @@ export interface ExtensionManifest {
101
114
  /** The module that exports `activate`, relative to the package's root. */
102
115
  main: string;
103
116
  /**
104
- * Stylesheets adopted into the shadow root of each of the extension's Views, relative to the
105
- * package's root. Only a loaded extension has them: a compiled-in one shares the Client's.
117
+ * Stylesheets adopted into the shadow root of each of the extension's Views and fence widgets, or
118
+ * by the document with `isolation` `page`, relative to the package's root. Only a loaded extension
119
+ * has them: a compiled-in one shares the Client's.
106
120
  */
107
121
  styles?: string[];
122
+ /** Where a loaded extension's Views and fence widgets mount. `shadow` when left out. */
123
+ isolation?: ExtensionIsolation;
108
124
  /** Icons by name: the inner markup of a 16 by 16 SVG drawn with `currentColor` strokes. */
109
125
  icons?: Record<string, string>;
110
126
  /** The View kinds the extension adds. */
package/dist/manifest.js CHANGED
@@ -6,6 +6,8 @@
6
6
  const NAME = /^[a-z][a-z0-9-]*$/;
7
7
  const VERSION = /^\d+\.\d+\.\d+$/;
8
8
  const REGIONS = ['main', 'left-sidebar', 'right-sidebar'];
9
+ /** The longest an icon's markup may be: a 16 by 16 picture needs a few hundred characters. */
10
+ const ICON_MARKUP_LIMIT = 4096;
9
11
  /**
10
12
  * Read an extension's package.json, already parsed, and check its manifest.
11
13
  *
@@ -44,6 +46,9 @@ export function readExtensionPackage(packageJson) {
44
46
  if (field.styles !== undefined && (!Array.isArray(field.styles) || !field.styles.every((path) => typeof path === 'string' && relativePath(path)))) {
45
47
  errors.push('"etherpk.styles" must be a list of paths inside the package.');
46
48
  }
49
+ if (field.isolation !== undefined && field.isolation !== 'shadow' && field.isolation !== 'page') {
50
+ errors.push('"etherpk.isolation" must be "shadow" or "page".');
51
+ }
47
52
  if (field.icons !== undefined) {
48
53
  if (!isRecord(field.icons))
49
54
  errors.push('"etherpk.icons" must map names to SVG markup.');
@@ -53,6 +58,8 @@ export function readExtensionPackage(packageJson) {
53
58
  errors.push(`Icon "${iconName}" must be lowercase letters, digits and hyphens.`);
54
59
  if (typeof markup !== 'string' || markup.trim() === '')
55
60
  errors.push(`Icon "${iconName}" has no markup.`);
61
+ else if (markup.length > ICON_MARKUP_LIMIT)
62
+ errors.push(`Icon "${iconName}" is longer than ${ICON_MARKUP_LIMIT} characters.`);
56
63
  }
57
64
  }
58
65
  const views = field.views === undefined ? [] : field.views;
@@ -164,9 +171,12 @@ function checkView(view, index, id, kinds, segments, errors) {
164
171
  errors.push(`${where}.address.target may only be "concept".`);
165
172
  }
166
173
  }
167
- /** A path that stays inside the package: relative, and never climbing out of it. */
174
+ /**
175
+ * A path that stays inside the package: relative, never climbing out of it, and never an address or
176
+ * a drive (`https:`, `data:`, `C:`), which a URL would read as somewhere else.
177
+ */
168
178
  function relativePath(path) {
169
- return path !== '' && !path.startsWith('/') && !path.includes('\\') && !path.split('/').includes('..');
179
+ return path !== '' && !path.startsWith('/') && !path.includes('\\') && !/^[a-z][a-z0-9+.-]*:/i.test(path) && !path.split('/').includes('..');
170
180
  }
171
181
  function isRecord(value) {
172
182
  return typeof value === 'object' && value !== null && !Array.isArray(value);
package/dist/menus.d.ts CHANGED
@@ -87,12 +87,16 @@ export interface ContextMenuItem {
87
87
  /**
88
88
  * What a Command Menu row's `when` may read about the editor it was opened in.
89
89
  *
90
- * @beta
90
+ * @public
91
91
  */
92
92
  export interface CommandMenuContext {
93
93
  /** The caret is in a table. */
94
94
  inTable: boolean;
95
- /** A table may be inserted at the caret. */
95
+ /**
96
+ * A table may be inserted at the caret.
97
+ *
98
+ * @beta
99
+ */
96
100
  tableInsertable: boolean;
97
101
  /** The document's body may be written: false in a locked Protected Document. */
98
102
  bodyWritable: boolean;
@@ -106,13 +110,15 @@ export interface CommandMenuContext {
106
110
  * caret line's links, the links above it in the outline, the document's own concept, then the
107
111
  * scopes in its name. Names are as written, an alias as the alias. Worked out only when a row
108
112
  * reads it. Absent reads as none.
113
+ *
114
+ * @beta
109
115
  */
110
116
  readonly conceptsAtCaret?: readonly string[];
111
117
  }
112
118
  /**
113
119
  * One row of the Command Menu, the menu `/` opens in the editor.
114
120
  *
115
- * @beta
121
+ * @public
116
122
  */
117
123
  export interface CommandMenuItem {
118
124
  /** Under the extension's id. */
@@ -136,24 +142,41 @@ export interface CommandMenuItem {
136
142
  when?: (context: CommandMenuContext) => boolean;
137
143
  /** The Command the row runs. */
138
144
  command: string;
139
- /** What the Command is given. */
145
+ /**
146
+ * What the Command is given.
147
+ *
148
+ * @beta
149
+ */
140
150
  args?: unknown;
141
151
  }
142
152
  /**
143
- * A button in the Graph Sidebar, under Today's journal: a way in to what the extension shows for
144
- * the whole graph, such as the Graph Map View. Pressing it runs `command`.
153
+ * A button in the Graph Sidebar's group of action buttons, after Search and today's date: a way in
154
+ * to what the extension shows for the whole graph, such as the Graph Map. Pressing it runs
155
+ * `command`.
145
156
  *
146
157
  * @beta
147
158
  */
148
159
  export interface GraphSidebarButton {
149
160
  /** Under the extension's id. */
150
161
  id: string;
151
- /** The button's text. */
162
+ /** The button's text: short, because the Sidebar fits two buttons side by side. */
152
163
  title: string;
164
+ /**
165
+ * What pressing the button does, shown as its help text: "Open a map of every place and route
166
+ * in this graph". The Client adds the key chords bound to `command`, so it names none. Without
167
+ * one, the help text is the title.
168
+ */
169
+ description?: string;
153
170
  /** An icon drawn before the title: the extension's own or one of the Client's. */
154
171
  icon?: string;
155
172
  /** Lower lists earlier. A button without one lists after every button that has one. */
156
173
  order?: number;
174
+ /**
175
+ * Whether the button shows here, asked each time the Sidebar draws, which it does afresh when
176
+ * the layout changes between desktop and phone. Absent means everywhere. A `when` that throws
177
+ * hides the button.
178
+ */
179
+ when?: () => boolean;
157
180
  /** The Command the button runs. */
158
181
  command: string;
159
182
  /** What the Command is given. */
package/dist/views.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * Which View: its kind and what it is about. A View with no subject has an empty target.
7
7
  *
8
- * @beta
8
+ * @public
9
9
  */
10
10
  export interface ViewRef {
11
11
  /** The View kind, `<extension id>.<name>` for an extension's. */
@@ -18,7 +18,7 @@ export interface ViewRef {
18
18
  * The desktop keeps every tab mounted, so a View that does work while shown (drawing, reading the
19
19
  * index) stops it while this is false.
20
20
  *
21
- * @beta
21
+ * @public
22
22
  */
23
23
  export interface ViewVisibility {
24
24
  /** True while the View can be seen. */
@@ -29,7 +29,7 @@ export interface ViewVisibility {
29
29
  /**
30
30
  * What a View is mounted with.
31
31
  *
32
- * @beta
32
+ * @public
33
33
  */
34
34
  export interface ViewMountProps {
35
35
  /** The View being mounted. */
@@ -45,7 +45,7 @@ export interface ViewMountProps {
45
45
  /**
46
46
  * A View the extension has drawn, as the Client drives it.
47
47
  *
48
- * @beta
48
+ * @public
49
49
  */
50
50
  export interface MountedView {
51
51
  /** The same View with new props: a rename re-keyed its target, or it moved to another Pane. */
@@ -56,7 +56,7 @@ export interface MountedView {
56
56
  /**
57
57
  * What an extension registers for a View kind its manifest declares.
58
58
  *
59
- * @beta
59
+ * @public
60
60
  */
61
61
  export interface ViewContribution {
62
62
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appsoftwareltd/etherpk-extension-api",
3
- "version": "0.9.5",
3
+ "version": "0.10.0",
4
4
  "license": "MIT",
5
5
  "description": "The API an EtherPK extension is written against: the context it is started with, its manifest, and the contract its Views follow.",
6
6
  "type": "module",