@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 CHANGED
@@ -1,24 +1,41 @@
1
1
  # @dolphy-app/extension-sdk
2
2
 
3
- SDK автора расширений: defineExtension, defineAnswerElement, помощники тестирования
3
+ SDK for extension authors: defineExtension, defineAnswerView, test helpers
4
4
 
5
- Версия пакетов равна версии приложения Dolphy, из релиза которого они опубликованы (0.2.0).
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` — код расширения (`defineExtension`,
8
- `defineExerciseType`), элемент ответа (`defineAnswerElement`) и помощники
9
- тестов (`@dolphy-app/extension-sdk/testing`).
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
- [Расширения Dolphy](https://github.com/dolphy-app/dolphy/blob/main/docs/design/extensions.md).
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 { AnswerElementProps, ExerciseTypeHandler, ExtensionContext, ExtensionModule, GradePolicyHandler, MarkdownRenderContext, MarkdownRendererModule } from "@dolphy-app/extension-api";
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
- interface ExtensionDefinition {
5
- exerciseTypes?: Readonly<Record<string, ExerciseTypeHandler>>;
6
- gradePolicies?: Readonly<Record<string, GradePolicyHandler>>;
7
- /** Вызывается после регистрации `exerciseTypes` и `gradePolicies`. */
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: (definition: ExtensionDefinition) => ExtensionModule;
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
- /** `export default defineMarkdownRenderer(...)` в модуле рендерера содержимого. */
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
- export type { AnswerElementApi, AnswerElementInstance, ExtensionDefinition, MountAnswerElement };
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
- const defineExerciseType = (handler) => handler;
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
- const defineExtension = (definition) => {
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 entries = Object.entries(definition.exerciseTypes ?? {});
20
- for (const [type, handler] of entries) registrations.push(context.registerExerciseType(type, handler));
21
- for (const [id, handler] of Object.entries(definition.gradePolicies ?? {})) registrations.push(context.registerGradePolicy(id, handler));
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-element.ts
53
- const logFailure = (tag, message, error) => {
54
- console.error({
55
- error,
56
- tag
57
- }, message);
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
- /** `export default defineMarkdownRenderer(...)` в модуле рендерера содержимого. */
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 { defineAnswerElement, defineExerciseType, defineExtension, defineMarkdownRenderer };
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
@@ -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 };