@dolphy-app/extension-sdk 0.2.0 → 0.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 +25 -8
- package/dist/answer-element-BOQcuxYh.js +114 -0
- package/dist/answer-view-D6wnyThb.d.ts +28 -0
- package/dist/index.d.ts +107 -27
- package/dist/index.js +48 -117
- package/dist/runtime.d.ts +17 -0
- package/dist/runtime.js +24 -0
- package/dist/testing.d.ts +330 -11
- package/dist/testing.js +722 -11
- package/docs/debugging.md +210 -0
- package/docs/no-build.md +99 -0
- package/docs/quick-start.md +172 -0
- package/docs/recipe-command-panel.md +211 -0
- package/docs/recipe-event-storage.md +266 -0
- package/docs/recipe-exercise-type.md +379 -0
- package/docs/recipe-import-export.md +241 -0
- package/docs/recipe-settings.md +161 -0
- package/docs/recipe-theme.md +116 -0
- package/docs/recipe-ui-kit.md +172 -0
- package/docs/recipe-when-dependencies.md +158 -0
- package/package.json +10 -4
package/README.md
CHANGED
|
@@ -1,24 +1,41 @@
|
|
|
1
1
|
# @dolphy-app/extension-sdk
|
|
2
2
|
|
|
3
|
-
SDK
|
|
3
|
+
SDK for extension authors: defineExtension, defineAnswerView, test helpers
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The package version equals the version of the Dolphy app release it was published from (0.4.0).
|
|
6
6
|
|
|
7
|
-
`@dolphy-app/extension-sdk` —
|
|
8
|
-
`defineExerciseType`),
|
|
9
|
-
|
|
7
|
+
`@dolphy-app/extension-sdk` — extension code (`defineExtension`,
|
|
8
|
+
`defineExerciseType`), answer views (`defineAnswerView`), panels,
|
|
9
|
+
markdown renderers and test helpers (`@dolphy-app/extension-sdk/testing`).
|
|
10
|
+
The package has no side effects: an extension `src/index.ts` can be
|
|
11
|
+
imported in plain Node. Ids declared in `extension.json` become types
|
|
12
|
+
through `.dolphy/ids.d.ts`, which `dolphy-ext types` generates.
|
|
10
13
|
|
|
11
14
|
```ts
|
|
12
15
|
import { defineExtension } from '@dolphy-app/extension-sdk';
|
|
13
16
|
import { loadExerciseType } from '@dolphy-app/extension-sdk/testing';
|
|
14
17
|
```
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
The package ships a guide in `docs/` (`node_modules/@dolphy-app/extension-sdk/docs/`
|
|
20
|
+
after the install): a
|
|
21
|
+
[quick start](https://github.com/dolphy-app/dolphy/blob/main/packages/extension-sdk/docs/quick-start.md), recipes for
|
|
22
|
+
[an exercise type](https://github.com/dolphy-app/dolphy/blob/main/packages/extension-sdk/docs/recipe-exercise-type.md),
|
|
23
|
+
[a theme](https://github.com/dolphy-app/dolphy/blob/main/packages/extension-sdk/docs/recipe-theme.md),
|
|
24
|
+
[a command and a panel](https://github.com/dolphy-app/dolphy/blob/main/packages/extension-sdk/docs/recipe-command-panel.md),
|
|
25
|
+
[events and storage](https://github.com/dolphy-app/dolphy/blob/main/packages/extension-sdk/docs/recipe-event-storage.md),
|
|
26
|
+
[settings](https://github.com/dolphy-app/dolphy/blob/main/packages/extension-sdk/docs/recipe-settings.md),
|
|
27
|
+
[an importer and an exporter](https://github.com/dolphy-app/dolphy/blob/main/packages/extension-sdk/docs/recipe-import-export.md),
|
|
28
|
+
[visibility conditions and dependencies](https://github.com/dolphy-app/dolphy/blob/main/packages/extension-sdk/docs/recipe-when-dependencies.md) and
|
|
29
|
+
[a panel on the UI kit](https://github.com/dolphy-app/dolphy/blob/main/packages/extension-sdk/docs/recipe-ui-kit.md), a path
|
|
30
|
+
[without a build](https://github.com/dolphy-app/dolphy/blob/main/packages/extension-sdk/docs/no-build.md) and notes on
|
|
31
|
+
[debugging](https://github.com/dolphy-app/dolphy/blob/main/packages/extension-sdk/docs/debugging.md).
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
17
34
|
|
|
18
35
|
```sh
|
|
19
36
|
npm install @dolphy-app/extension-sdk
|
|
20
37
|
```
|
|
21
38
|
|
|
22
|
-
##
|
|
39
|
+
## Documentation
|
|
23
40
|
|
|
24
|
-
[
|
|
41
|
+
[Dolphy extensions](https://github.com/dolphy-app/dolphy/blob/main/docs/design/extensions.md).
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { ANSWER_EVENT, ELEMENT_NAME_PATTERN } from "@dolphy-app/extension-api";
|
|
2
|
+
|
|
3
|
+
//#region packages/extension-sdk/src/answer-element.ts
|
|
4
|
+
const logFailure = (tag, message, error) => {
|
|
5
|
+
console.error({
|
|
6
|
+
error,
|
|
7
|
+
tag
|
|
8
|
+
}, message);
|
|
9
|
+
};
|
|
10
|
+
const createAnswerElementClass = (tag, answerView) => class AnswerElement extends HTMLElement {
|
|
11
|
+
#root = this.attachShadow({ mode: "open" });
|
|
12
|
+
#props = {
|
|
13
|
+
view: void 0,
|
|
14
|
+
value: void 0,
|
|
15
|
+
disabled: false,
|
|
16
|
+
verdict: null
|
|
17
|
+
};
|
|
18
|
+
#instance = null;
|
|
19
|
+
#isFlushScheduled = false;
|
|
20
|
+
get view() {
|
|
21
|
+
return this.#props.view;
|
|
22
|
+
}
|
|
23
|
+
set view(view) {
|
|
24
|
+
this.#change({ view });
|
|
25
|
+
}
|
|
26
|
+
get value() {
|
|
27
|
+
return this.#props.value;
|
|
28
|
+
}
|
|
29
|
+
set value(value) {
|
|
30
|
+
this.#change({ value });
|
|
31
|
+
}
|
|
32
|
+
get disabled() {
|
|
33
|
+
return this.#props.disabled;
|
|
34
|
+
}
|
|
35
|
+
set disabled(disabled) {
|
|
36
|
+
this.#change({ disabled });
|
|
37
|
+
}
|
|
38
|
+
get verdict() {
|
|
39
|
+
return this.#props.verdict;
|
|
40
|
+
}
|
|
41
|
+
set verdict(verdict) {
|
|
42
|
+
this.#change({ verdict });
|
|
43
|
+
}
|
|
44
|
+
connectedCallback() {
|
|
45
|
+
if (this.#instance !== null) return;
|
|
46
|
+
const readLabel = () => this.getAttribute("aria-label");
|
|
47
|
+
const api = {
|
|
48
|
+
root: this.#root,
|
|
49
|
+
get label() {
|
|
50
|
+
return readLabel();
|
|
51
|
+
},
|
|
52
|
+
setAnswer: (value, complete) => {
|
|
53
|
+
const detail = {
|
|
54
|
+
value,
|
|
55
|
+
complete
|
|
56
|
+
};
|
|
57
|
+
this.#emit(ANSWER_EVENT.change, detail);
|
|
58
|
+
},
|
|
59
|
+
submit: () => this.#emit(ANSWER_EVENT.submit, void 0)
|
|
60
|
+
};
|
|
61
|
+
try {
|
|
62
|
+
this.#instance = answerView.mount(api, Object.freeze({ ...this.#props }));
|
|
63
|
+
} catch (error) {
|
|
64
|
+
logFailure(tag, "answer element failed to mount", error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
disconnectedCallback() {
|
|
68
|
+
const instance = this.#instance;
|
|
69
|
+
this.#instance = null;
|
|
70
|
+
if (instance === null) return;
|
|
71
|
+
try {
|
|
72
|
+
instance.destroy?.();
|
|
73
|
+
} catch (error) {
|
|
74
|
+
logFailure(tag, "answer element failed to destroy", error);
|
|
75
|
+
}
|
|
76
|
+
this.#root.replaceChildren();
|
|
77
|
+
}
|
|
78
|
+
#change(patch) {
|
|
79
|
+
this.#props = {
|
|
80
|
+
...this.#props,
|
|
81
|
+
...patch
|
|
82
|
+
};
|
|
83
|
+
if (this.#instance === null || this.#isFlushScheduled) return;
|
|
84
|
+
this.#isFlushScheduled = true;
|
|
85
|
+
queueMicrotask(() => this.#flush());
|
|
86
|
+
}
|
|
87
|
+
#flush() {
|
|
88
|
+
this.#isFlushScheduled = false;
|
|
89
|
+
const instance = this.#instance;
|
|
90
|
+
if (instance === null) return;
|
|
91
|
+
try {
|
|
92
|
+
instance.update(Object.freeze({ ...this.#props }));
|
|
93
|
+
} catch (error) {
|
|
94
|
+
logFailure(tag, "answer element failed to update", error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
#emit(name, detail) {
|
|
98
|
+
this.dispatchEvent(new CustomEvent(name, {
|
|
99
|
+
detail,
|
|
100
|
+
bubbles: true,
|
|
101
|
+
composed: true
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
/** Defines the kind's custom element; calling again with the same tag changes nothing. */
|
|
106
|
+
const registerAnswerView = (tag, view) => {
|
|
107
|
+
if (!ELEMENT_NAME_PATTERN.test(tag)) throw new TypeError(`invalid custom element name '${tag}'`);
|
|
108
|
+
if (typeof view?.mount !== "function") throw new TypeError(`answer view for '${tag}' has no mount()`);
|
|
109
|
+
if (customElements.get(tag) !== void 0) return;
|
|
110
|
+
customElements.define(tag, createAnswerElementClass(tag, view));
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
//#endregion
|
|
114
|
+
export { registerAnswerView as n, createAnswerElementClass as t };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { AnswerElementProps } from "@dolphy-app/extension-api";
|
|
2
|
+
//#region packages/extension-sdk/src/answer-view.d.ts
|
|
3
|
+
interface AnswerViewApi {
|
|
4
|
+
readonly root: ShadowRoot;
|
|
5
|
+
/** `aria-label` of the host element set by the app; `null` if none. */
|
|
6
|
+
readonly label: string | null;
|
|
7
|
+
/** Reports the current answer to the app: the `dolphy-answer-change` event. */
|
|
8
|
+
setAnswer(value: unknown, complete: boolean): void;
|
|
9
|
+
/** Asks the app to submit the answer: the `dolphy-answer-submit` event. */
|
|
10
|
+
submit(): void;
|
|
11
|
+
}
|
|
12
|
+
interface AnswerViewInstance {
|
|
13
|
+
/** Called when `view`/`value`/`disabled`/`verdict` change. */
|
|
14
|
+
update(props: AnswerElementProps): void;
|
|
15
|
+
destroy?(): void;
|
|
16
|
+
}
|
|
17
|
+
type MountAnswerView = (api: AnswerViewApi, props: AnswerElementProps) => AnswerViewInstance;
|
|
18
|
+
/** Description of an answer input view: side-effect-free data; the build registers the element. */
|
|
19
|
+
interface AnswerView {
|
|
20
|
+
readonly mount: MountAnswerView;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Entry `views[<exercise kind id>]` in `src/index.ts`. Registers nothing:
|
|
24
|
+
* the custom element with the manifest tag is defined by the build's browser file.
|
|
25
|
+
*/
|
|
26
|
+
declare const defineAnswerView: (mount: MountAnswerView) => AnswerView;
|
|
27
|
+
//#endregion
|
|
28
|
+
export { defineAnswerView as a, MountAnswerView as i, AnswerViewApi as n, AnswerViewInstance as r, AnswerView as t };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,36 +1,116 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as defineAnswerView, i as MountAnswerView, n as AnswerViewApi, r as AnswerViewInstance, t as AnswerView } from "./answer-view-D6wnyThb.js";
|
|
2
|
+
import { CommandHandler, ExerciseTypeHandler, ExporterHandler, ExtensionContext as ExtensionContext$1, ExtensionIdSet, ExtensionModule, GradePolicyHandler, ImporterHandler, JsonValue, LearningEventHandler, MarkdownRenderContext, MarkdownRendererModule, NotifyEffect, OpenPanelEffect, PanelContext as PanelContext$1, PanelModule, ScheduleHandler, WidgetContext as WidgetContext$1, WidgetModule } from "@dolphy-app/extension-api";
|
|
2
3
|
export * from "@dolphy-app/extension-api";
|
|
4
|
+
//#region packages/extension-sdk/src/ids.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* The ids declared in `extension.json`. Empty here: `dolphy-ext types` (and
|
|
7
|
+
* every `dolphy-ext build`) writes `.dolphy/ids.d.ts`, which augments this
|
|
8
|
+
* interface with one key per kind of id:
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* declare module '@dolphy-app/extension-sdk' {
|
|
12
|
+
* interface ExtensionIds {
|
|
13
|
+
* exerciseTypes: 'acme.echo';
|
|
14
|
+
* commands: 'acme.a' | 'acme.b';
|
|
15
|
+
* settings: { 'acme.goal': number };
|
|
16
|
+
* // …
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* While the interface is empty (no generated file) every id is a plain
|
|
22
|
+
* `string` and `defineExtension` does not require any record.
|
|
23
|
+
*/
|
|
24
|
+
interface ExtensionIds {}
|
|
25
|
+
type Declared<K extends keyof ExtensionIdSet> = K extends keyof ExtensionIds ? ExtensionIds[K] extends ExtensionIdSet[K] ? ExtensionIds[K] : ExtensionIdSet[K] : ExtensionIdSet[K];
|
|
26
|
+
/** `ExtensionIds` with the plain-string fallback for the kinds it does not declare. */
|
|
27
|
+
type ResolvedIds = { [K in keyof ExtensionIdSet]: Declared<K>; };
|
|
28
|
+
/** Whether the generated declarations are part of the program. */
|
|
29
|
+
type HasGeneratedIds = [keyof ExtensionIds] extends [never] ? false : true;
|
|
30
|
+
/**
|
|
31
|
+
* `ExtensionContext` of this extension: `settings`, `events`, `commands` and
|
|
32
|
+
* the `register…` methods accept the declared ids only.
|
|
33
|
+
*/
|
|
34
|
+
type ExtensionContext = ExtensionContext$1<ResolvedIds>;
|
|
35
|
+
/** Context of a panel module; `call` accepts the declared command ids only. */
|
|
36
|
+
type PanelContext = PanelContext$1<ResolvedIds['commands']>;
|
|
37
|
+
/** Context of a widget module; `call` accepts the declared command ids only. */
|
|
38
|
+
type WidgetContext = WidgetContext$1<ResolvedIds['commands']>;
|
|
39
|
+
/**
|
|
40
|
+
* A record that holds exactly the declared ids: a missing and an extra key are
|
|
41
|
+
* both compile errors. With no generated declarations any keys are accepted;
|
|
42
|
+
* with none declared of this kind no key is accepted.
|
|
43
|
+
*/
|
|
44
|
+
type Exact<Id extends string, Value> = [HasGeneratedIds] extends [false] ? {
|
|
45
|
+
readonly [key: string]: Value;
|
|
46
|
+
} : [Id] extends [never] ? {
|
|
47
|
+
readonly [key: string]: never;
|
|
48
|
+
} : { readonly [K in Id]: Value; };
|
|
49
|
+
/** `export const views = { … } satisfies ExtensionViews`: one `defineAnswerView` per declared exercise type. */
|
|
50
|
+
type ExtensionViews = Exact<ResolvedIds['exerciseTypes'], AnswerView>;
|
|
51
|
+
/** `export const panels = { … } satisfies ExtensionPanels`: one `defineExtensionPanel` per declared panel. */
|
|
52
|
+
type ExtensionPanels = Exact<ResolvedIds['panels'], PanelModule<HTMLElement, ResolvedIds['commands']>>;
|
|
53
|
+
/** `export const widgets = { … } satisfies ExtensionWidgets`: one `defineExtensionWidget` per declared widget. */
|
|
54
|
+
type ExtensionWidgets = Exact<ResolvedIds['widgets'], WidgetModule<HTMLElement, ResolvedIds['commands']>>;
|
|
55
|
+
/** `export const markdown = { … } satisfies ExtensionMarkdown`: one `defineMarkdownRenderer` per declared language. */
|
|
56
|
+
type ExtensionMarkdown = Exact<ResolvedIds['markdownLanguages'], MarkdownRendererModule<HTMLElement>>;
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region packages/extension-sdk/src/commands.d.ts
|
|
59
|
+
/** A command result: the app shows a notification (1–500 characters, as is, no markup). */
|
|
60
|
+
export declare const notify: (text: string) => NotifyEffect;
|
|
61
|
+
/** A command result: the app opens a panel of this extension (a declared panel id); `props` reach the panel as `ctx.props`. */
|
|
62
|
+
export declare const openPanel: (panelId: ResolvedIds["panels"], props?: JsonValue) => OpenPanelEffect;
|
|
63
|
+
//#endregion
|
|
3
64
|
//#region packages/extension-sdk/src/define-extension.d.ts
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
65
|
+
declare const inActivateBrand: unique symbol;
|
|
66
|
+
/** Type of `inActivate`. */
|
|
67
|
+
interface InActivate {
|
|
68
|
+
readonly [inActivateBrand]: true;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* A record value in `defineExtension` that says "this id is registered in
|
|
72
|
+
* `activate`" (`ctx.commands.register`, `ctx.events.on`,
|
|
73
|
+
* `ctx.importers.register`, `ctx.exporters.register`, `ctx.schedule.on`,
|
|
74
|
+
* `ctx.registerExerciseType`, `ctx.registerGradePolicy`) rather than by a
|
|
75
|
+
* handler in the record. Needed because the records must name every declared
|
|
76
|
+
* id: a handler that needs `ctx` is written in `activate`, and its id gets this
|
|
77
|
+
* marker in the record.
|
|
78
|
+
*/
|
|
79
|
+
export declare const inActivate: InActivate;
|
|
80
|
+
/** Without generated declarations a record may name any subset: an index signature already does, a record keyed by the event names needs `Partial`. */
|
|
81
|
+
type Lenient<Entries> = string extends keyof Entries ? Entries : Partial<Entries>;
|
|
82
|
+
/**
|
|
83
|
+
* The definition record of one kind of id. With generated declarations the
|
|
84
|
+
* record is required (when the manifest declares any id of the kind) and holds
|
|
85
|
+
* exactly the declared ids: a missing and an extra key are compile errors.
|
|
86
|
+
* Without them every key is accepted and the record is optional.
|
|
87
|
+
*/
|
|
88
|
+
type Section<Name extends string, Id extends string, Entries> = [HasGeneratedIds] extends [false] ? { readonly [N in Name]?: Lenient<Entries>; } : [Id] extends [never] ? { readonly [N in Name]?: never; } : { readonly [N in Name]: Entries; };
|
|
89
|
+
type Ids = ResolvedIds;
|
|
90
|
+
/** Learning-event handlers by event name; the events must be declared in `contributes.events`, the `learning.events` permission is needed. */
|
|
91
|
+
type EventHandlers = { readonly [N in Ids['events']]: LearningEventHandler<N> | InActivate; };
|
|
92
|
+
/**
|
|
93
|
+
* What `defineExtension` takes. `exerciseTypes`, `gradePolicies`, `events`,
|
|
94
|
+
* `commands`, `schedules`, `importers` and `exporters` name every id
|
|
95
|
+
* `extension.json` declares for them, exactly: a handler, or `inActivate` for
|
|
96
|
+
* an id that `activate` registers.
|
|
97
|
+
*/
|
|
98
|
+
type ExtensionDefinition = Section<'exerciseTypes', Ids['exerciseTypes'], { readonly [K in Ids['exerciseTypes']]: ExerciseTypeHandler | InActivate; }> & Section<'gradePolicies', Ids['gradePolicies'], { readonly [K in Ids['gradePolicies']]: GradePolicyHandler | InActivate; }> & Section<'events', Ids['events'], EventHandlers> & Section<'commands', Ids['commands'], { readonly [K in Ids['commands']]: CommandHandler | InActivate; }> & Section<'schedules', Ids['schedules'], { readonly [K in Ids['schedules']]: ScheduleHandler | InActivate; }> & Section<'importers', Ids['importers'], { readonly [K in Ids['importers']]: ImporterHandler | InActivate; }> & Section<'exporters', Ids['exporters'], { readonly [K in Ids['exporters']]: ExporterHandler | InActivate; }> & {
|
|
99
|
+
/** Runs after the records are registered. */
|
|
8
100
|
activate?(context: ExtensionContext): void | Promise<void>;
|
|
9
101
|
deactivate?(): void | Promise<void>;
|
|
10
|
-
}
|
|
102
|
+
};
|
|
11
103
|
export declare const defineExerciseType: <Spec, Answer, View>(handler: ExerciseTypeHandler<Spec, Answer, View>) => ExerciseTypeHandler<Spec, Answer, View>;
|
|
12
|
-
export declare const defineExtension: (
|
|
13
|
-
//#endregion
|
|
14
|
-
//#region packages/extension-sdk/src/answer-element.d.ts
|
|
15
|
-
interface AnswerElementApi {
|
|
16
|
-
readonly root: ShadowRoot;
|
|
17
|
-
/** `aria-label` хост-элемента, выставленный приложением; `null`, если нет. */
|
|
18
|
-
readonly label: string | null;
|
|
19
|
-
/** Сообщает приложению текущий ответ: событие `dolphy-answer-change`. */
|
|
20
|
-
setAnswer(value: unknown, complete: boolean): void;
|
|
21
|
-
/** Просит приложение отправить ответ: событие `dolphy-answer-submit`. */
|
|
22
|
-
submit(): void;
|
|
23
|
-
}
|
|
24
|
-
interface AnswerElementInstance {
|
|
25
|
-
/** Вызывается при изменении `view`/`value`/`disabled`/`verdict`. */
|
|
26
|
-
update(props: AnswerElementProps): void;
|
|
27
|
-
destroy?(): void;
|
|
28
|
-
}
|
|
29
|
-
type MountAnswerElement = (api: AnswerElementApi, props: AnswerElementProps) => AnswerElementInstance;
|
|
30
|
-
export declare const defineAnswerElement: (tag: string, mount: MountAnswerElement) => void;
|
|
104
|
+
export declare const defineExtension: (declared: ExtensionDefinition) => ExtensionModule;
|
|
31
105
|
//#endregion
|
|
32
106
|
//#region packages/extension-sdk/src/markdown-renderer.d.ts
|
|
33
|
-
/** `
|
|
107
|
+
/** Entry `markdown[<language>]` in `src/index.ts` (`contributes.markdownRenderers`). */
|
|
34
108
|
export declare const defineMarkdownRenderer: (render: (source: string, container: HTMLElement, context: MarkdownRenderContext) => void | Promise<void>) => MarkdownRendererModule<HTMLElement>;
|
|
35
109
|
//#endregion
|
|
36
|
-
|
|
110
|
+
//#region packages/extension-sdk/src/panel.d.ts
|
|
111
|
+
/** An entry of `panels[<panel id>]` in `src/index.ts` (`contributes.panels`); `ctx.call` accepts the declared command ids. */
|
|
112
|
+
export declare const defineExtensionPanel: (module: PanelModule<HTMLElement, ResolvedIds["commands"]>) => PanelModule<HTMLElement, ResolvedIds["commands"]>;
|
|
113
|
+
/** An entry of `widgets[<widget id>]` in `src/index.ts` (`contributes.widgets`); `ctx.call` accepts the declared command ids. */
|
|
114
|
+
export declare const defineExtensionWidget: (module: WidgetModule<HTMLElement, ResolvedIds["commands"]>) => WidgetModule<HTMLElement, ResolvedIds["commands"]>;
|
|
115
|
+
//#endregion
|
|
116
|
+
export { type AnswerView, type AnswerViewApi, type AnswerViewInstance, type EventHandlers, type ExtensionContext, type ExtensionDefinition, type ExtensionIds, type ExtensionMarkdown, type ExtensionPanels, type ExtensionViews, type ExtensionWidgets, type InActivate, type MountAnswerView, type PanelContext, type WidgetContext, defineAnswerView };
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,27 @@
|
|
|
1
|
-
import { ANSWER_EVENT, ELEMENT_NAME_PATTERN } from "@dolphy-app/extension-api";
|
|
2
|
-
|
|
3
1
|
export * from "@dolphy-app/extension-api"
|
|
4
2
|
|
|
3
|
+
//#region packages/extension-sdk/src/commands.ts
|
|
4
|
+
/** A command result: the app shows a notification (1–500 characters, as is, no markup). */
|
|
5
|
+
const notify = (text) => ({ notify: text });
|
|
6
|
+
/** A command result: the app opens a panel of this extension (a declared panel id); `props` reach the panel as `ctx.props`. */
|
|
7
|
+
const openPanel = (panelId, props) => props === void 0 ? { openPanel: panelId } : {
|
|
8
|
+
openPanel: panelId,
|
|
9
|
+
props
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
//#endregion
|
|
5
13
|
//#region packages/extension-sdk/src/define-extension.ts
|
|
6
|
-
|
|
14
|
+
/**
|
|
15
|
+
* A record value in `defineExtension` that says "this id is registered in
|
|
16
|
+
* `activate`" (`ctx.commands.register`, `ctx.events.on`,
|
|
17
|
+
* `ctx.importers.register`, `ctx.exporters.register`, `ctx.schedule.on`,
|
|
18
|
+
* `ctx.registerExerciseType`, `ctx.registerGradePolicy`) rather than by a
|
|
19
|
+
* handler in the record. Needed because the records must name every declared
|
|
20
|
+
* id: a handler that needs `ctx` is written in `activate`, and its id gets this
|
|
21
|
+
* marker in the record.
|
|
22
|
+
*/
|
|
23
|
+
const inActivate = /*#__PURE__*/ Object.freeze({});
|
|
24
|
+
const defineExerciseType = /* @__NO_SIDE_EFFECTS__ */ (handler) => handler;
|
|
7
25
|
const disposeInReverse = async (registrations) => {
|
|
8
26
|
const errors = [];
|
|
9
27
|
for (const registration of registrations.splice(0).reverse()) try {
|
|
@@ -13,12 +31,19 @@ const disposeInReverse = async (registrations) => {
|
|
|
13
31
|
}
|
|
14
32
|
return errors;
|
|
15
33
|
};
|
|
16
|
-
|
|
34
|
+
/** The entries of a record that carry a handler (not `inActivate`). */
|
|
35
|
+
const handlersOf = (record) => Object.entries(record ?? {}).filter((entry) => entry[1] !== inActivate);
|
|
36
|
+
const defineExtension = /* @__NO_SIDE_EFFECTS__ */ (declared) => {
|
|
37
|
+
const definition = declared;
|
|
17
38
|
const registrations = [];
|
|
18
39
|
const register = (context) => {
|
|
19
|
-
const
|
|
20
|
-
for (const [
|
|
21
|
-
for (const [
|
|
40
|
+
for (const [type, handler] of handlersOf(definition.exerciseTypes)) registrations.push(context.registerExerciseType(type, handler));
|
|
41
|
+
for (const [id, handler] of handlersOf(definition.gradePolicies)) registrations.push(context.registerGradePolicy(id, handler));
|
|
42
|
+
for (const [name, handler] of handlersOf(definition.events)) registrations.push(context.events.on(name, handler));
|
|
43
|
+
for (const [id, handler] of handlersOf(definition.commands)) registrations.push(context.commands.register(id, handler));
|
|
44
|
+
for (const [id, handler] of handlersOf(definition.schedules)) registrations.push(context.schedule.on(id, handler));
|
|
45
|
+
for (const [id, handler] of handlersOf(definition.importers)) registrations.push(context.importers.register(id, handler));
|
|
46
|
+
for (const [id, handler] of handlersOf(definition.exporters)) registrations.push(context.exporters.register(id, handler));
|
|
22
47
|
};
|
|
23
48
|
const activate = async (context) => {
|
|
24
49
|
try {
|
|
@@ -49,118 +74,24 @@ const defineExtension = (definition) => {
|
|
|
49
74
|
};
|
|
50
75
|
|
|
51
76
|
//#endregion
|
|
52
|
-
//#region packages/extension-sdk/src/answer-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
};
|
|
59
|
-
const createAnswerElementClass = (tag, mount) => class AnswerElement extends HTMLElement {
|
|
60
|
-
#root = this.attachShadow({ mode: "open" });
|
|
61
|
-
#props = {
|
|
62
|
-
view: void 0,
|
|
63
|
-
value: void 0,
|
|
64
|
-
disabled: false,
|
|
65
|
-
verdict: null
|
|
66
|
-
};
|
|
67
|
-
#instance = null;
|
|
68
|
-
#isFlushScheduled = false;
|
|
69
|
-
get view() {
|
|
70
|
-
return this.#props.view;
|
|
71
|
-
}
|
|
72
|
-
set view(view) {
|
|
73
|
-
this.#change({ view });
|
|
74
|
-
}
|
|
75
|
-
get value() {
|
|
76
|
-
return this.#props.value;
|
|
77
|
-
}
|
|
78
|
-
set value(value) {
|
|
79
|
-
this.#change({ value });
|
|
80
|
-
}
|
|
81
|
-
get disabled() {
|
|
82
|
-
return this.#props.disabled;
|
|
83
|
-
}
|
|
84
|
-
set disabled(disabled) {
|
|
85
|
-
this.#change({ disabled });
|
|
86
|
-
}
|
|
87
|
-
get verdict() {
|
|
88
|
-
return this.#props.verdict;
|
|
89
|
-
}
|
|
90
|
-
set verdict(verdict) {
|
|
91
|
-
this.#change({ verdict });
|
|
92
|
-
}
|
|
93
|
-
connectedCallback() {
|
|
94
|
-
if (this.#instance !== null) return;
|
|
95
|
-
const readLabel = () => this.getAttribute("aria-label");
|
|
96
|
-
const api = {
|
|
97
|
-
root: this.#root,
|
|
98
|
-
get label() {
|
|
99
|
-
return readLabel();
|
|
100
|
-
},
|
|
101
|
-
setAnswer: (value, complete) => {
|
|
102
|
-
const detail = {
|
|
103
|
-
value,
|
|
104
|
-
complete
|
|
105
|
-
};
|
|
106
|
-
this.#emit(ANSWER_EVENT.change, detail);
|
|
107
|
-
},
|
|
108
|
-
submit: () => this.#emit(ANSWER_EVENT.submit, void 0)
|
|
109
|
-
};
|
|
110
|
-
try {
|
|
111
|
-
this.#instance = mount(api, Object.freeze({ ...this.#props }));
|
|
112
|
-
} catch (error) {
|
|
113
|
-
logFailure(tag, "answer element failed to mount", error);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
disconnectedCallback() {
|
|
117
|
-
const instance = this.#instance;
|
|
118
|
-
this.#instance = null;
|
|
119
|
-
if (instance === null) return;
|
|
120
|
-
try {
|
|
121
|
-
instance.destroy?.();
|
|
122
|
-
} catch (error) {
|
|
123
|
-
logFailure(tag, "answer element failed to destroy", error);
|
|
124
|
-
}
|
|
125
|
-
this.#root.replaceChildren();
|
|
126
|
-
}
|
|
127
|
-
#change(patch) {
|
|
128
|
-
this.#props = {
|
|
129
|
-
...this.#props,
|
|
130
|
-
...patch
|
|
131
|
-
};
|
|
132
|
-
if (this.#instance === null || this.#isFlushScheduled) return;
|
|
133
|
-
this.#isFlushScheduled = true;
|
|
134
|
-
queueMicrotask(() => this.#flush());
|
|
135
|
-
}
|
|
136
|
-
#flush() {
|
|
137
|
-
this.#isFlushScheduled = false;
|
|
138
|
-
const instance = this.#instance;
|
|
139
|
-
if (instance === null) return;
|
|
140
|
-
try {
|
|
141
|
-
instance.update(Object.freeze({ ...this.#props }));
|
|
142
|
-
} catch (error) {
|
|
143
|
-
logFailure(tag, "answer element failed to update", error);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
#emit(name, detail) {
|
|
147
|
-
this.dispatchEvent(new CustomEvent(name, {
|
|
148
|
-
detail,
|
|
149
|
-
bubbles: true,
|
|
150
|
-
composed: true
|
|
151
|
-
}));
|
|
152
|
-
}
|
|
153
|
-
};
|
|
154
|
-
const defineAnswerElement = (tag, mount) => {
|
|
155
|
-
if (!ELEMENT_NAME_PATTERN.test(tag)) throw new TypeError(`invalid custom element name '${tag}'`);
|
|
156
|
-
if (customElements.get(tag) !== void 0) return;
|
|
157
|
-
customElements.define(tag, createAnswerElementClass(tag, mount));
|
|
158
|
-
};
|
|
77
|
+
//#region packages/extension-sdk/src/answer-view.ts
|
|
78
|
+
/**
|
|
79
|
+
* Entry `views[<exercise kind id>]` in `src/index.ts`. Registers nothing:
|
|
80
|
+
* the custom element with the manifest tag is defined by the build's browser file.
|
|
81
|
+
*/
|
|
82
|
+
const defineAnswerView = /* @__NO_SIDE_EFFECTS__ */ (mount) => ({ mount });
|
|
159
83
|
|
|
160
84
|
//#endregion
|
|
161
85
|
//#region packages/extension-sdk/src/markdown-renderer.ts
|
|
162
|
-
/** `
|
|
163
|
-
const defineMarkdownRenderer = (render) => ({ render });
|
|
86
|
+
/** Entry `markdown[<language>]` in `src/index.ts` (`contributes.markdownRenderers`). */
|
|
87
|
+
const defineMarkdownRenderer = /* @__NO_SIDE_EFFECTS__ */ (render) => ({ render });
|
|
88
|
+
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region packages/extension-sdk/src/panel.ts
|
|
91
|
+
/** An entry of `panels[<panel id>]` in `src/index.ts` (`contributes.panels`); `ctx.call` accepts the declared command ids. */
|
|
92
|
+
const defineExtensionPanel = /* @__NO_SIDE_EFFECTS__ */ (module) => module;
|
|
93
|
+
/** An entry of `widgets[<widget id>]` in `src/index.ts` (`contributes.widgets`); `ctx.call` accepts the declared command ids. */
|
|
94
|
+
const defineExtensionWidget = /* @__NO_SIDE_EFFECTS__ */ (module) => module;
|
|
164
95
|
|
|
165
96
|
//#endregion
|
|
166
|
-
export {
|
|
97
|
+
export { defineAnswerView, defineExerciseType, defineExtension, defineExtensionPanel, defineExtensionWidget, defineMarkdownRenderer, inActivate, notify, openPanel };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { t as AnswerView } from "./answer-view-D6wnyThb.js";
|
|
2
|
+
import { MarkdownRendererModule, PanelModule, WidgetModule } from "@dolphy-app/extension-api";
|
|
3
|
+
//#region packages/extension-sdk/src/answer-element.d.ts
|
|
4
|
+
/** Defines the kind's custom element; calling again with the same tag changes nothing. */
|
|
5
|
+
export declare const registerAnswerView: (tag: string, view: AnswerView) => void;
|
|
6
|
+
//#endregion
|
|
7
|
+
//#region packages/extension-sdk/src/runtime.d.ts
|
|
8
|
+
type PanelEntry = PanelModule<HTMLElement>;
|
|
9
|
+
type WidgetEntry = WidgetModule<HTMLElement>;
|
|
10
|
+
type MarkdownEntry = MarkdownRendererModule<HTMLElement>;
|
|
11
|
+
/** Panel module that selects the `panels` entry by `ctx.panelId` (for a file shared by several panels). */
|
|
12
|
+
export declare const dispatchPanels: (panels: Readonly<Record<string, PanelEntry>>) => PanelEntry;
|
|
13
|
+
/** Widget module that selects the `widgets` entry by `ctx.widgetId` (for a file shared by several widgets). */
|
|
14
|
+
export declare const dispatchWidgets: (widgets: Readonly<Record<string, WidgetEntry>>) => WidgetEntry;
|
|
15
|
+
/** Renderer module that selects the `markdown` entry by block language. */
|
|
16
|
+
export declare const dispatchMarkdown: (renderers: Readonly<Record<string, MarkdownEntry>>) => MarkdownEntry;
|
|
17
|
+
//#endregion
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { n as registerAnswerView } from "./answer-element-BOQcuxYh.js";
|
|
2
|
+
|
|
3
|
+
//#region packages/extension-sdk/src/runtime.ts
|
|
4
|
+
/** Panel module that selects the `panels` entry by `ctx.panelId` (for a file shared by several panels). */
|
|
5
|
+
const dispatchPanels = (panels) => ({ mount(container, context) {
|
|
6
|
+
const panel = panels[context.panelId];
|
|
7
|
+
if (panel === void 0) throw new Error(`panel '${context.panelId}' is not exported`);
|
|
8
|
+
return panel.mount(container, context);
|
|
9
|
+
} });
|
|
10
|
+
/** Widget module that selects the `widgets` entry by `ctx.widgetId` (for a file shared by several widgets). */
|
|
11
|
+
const dispatchWidgets = (widgets) => ({ mount(container, context) {
|
|
12
|
+
const widget = widgets[context.widgetId];
|
|
13
|
+
if (widget === void 0) throw new Error(`widget '${context.widgetId}' is not exported`);
|
|
14
|
+
return widget.mount(container, context);
|
|
15
|
+
} });
|
|
16
|
+
/** Renderer module that selects the `markdown` entry by block language. */
|
|
17
|
+
const dispatchMarkdown = (renderers) => ({ render(source, container, context) {
|
|
18
|
+
const renderer = renderers[context.language];
|
|
19
|
+
if (renderer === void 0) throw new Error(`markdown renderer '${context.language}' is not exported`);
|
|
20
|
+
return renderer.render(source, container, context);
|
|
21
|
+
} });
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
export { dispatchMarkdown, dispatchPanels, dispatchWidgets, registerAnswerView };
|