@slidejs/runner 0.1.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/src/runner.ts ADDED
@@ -0,0 +1,377 @@
1
+ /**
2
+ * @slidejs/runner - SlideRunner 核心类
3
+ *
4
+ * 提供幻灯片运行时的核心功能,包括适配器管理、插件系统和生命周期控制
5
+ */
6
+
7
+ import type { SlideDSL, SlideDefinition } from '@slidejs/core';
8
+ import { SlideEngine } from '@slidejs/core';
9
+ import type { SlideContext } from '@slidejs/context';
10
+ import type {
11
+ SlideAdapter,
12
+ SlideRunnerConfig,
13
+ SlideRunnerPlugin,
14
+ AdapterEvent,
15
+ EventHandler,
16
+ AdapterOptions,
17
+ } from './types';
18
+ import { SlideRunnerError } from './errors';
19
+
20
+ /**
21
+ * SlideRunner 核心类
22
+ *
23
+ * 负责协调 SlideEngine、SlideAdapter 和插件系统,提供完整的幻灯片运行时环境
24
+ */
25
+ export class SlideRunner<TContext extends SlideContext = SlideContext> {
26
+ private adapter: SlideAdapter;
27
+ private plugins: SlideRunnerPlugin[];
28
+ private container: HTMLElement;
29
+ private adapterOptions?: AdapterOptions;
30
+ private slides: SlideDefinition[] = [];
31
+ private currentIndex = 0;
32
+ private isInitialized = false;
33
+
34
+ /**
35
+ * 创建 SlideRunner 实例
36
+ *
37
+ * @param config - SlideRunner 配置
38
+ * @throws {SlideRunnerError} 如果配置无效
39
+ */
40
+ constructor(config: SlideRunnerConfig) {
41
+ // 验证配置
42
+ if (!config.adapter) {
43
+ throw new SlideRunnerError('Adapter is required', 'MISSING_ADAPTER');
44
+ }
45
+
46
+ this.adapter = config.adapter;
47
+ this.plugins = config.plugins ?? [];
48
+ this.container = this.resolveContainer(config.container);
49
+ this.adapterOptions = config.adapterOptions;
50
+
51
+ // 设置适配器事件监听
52
+ this.setupAdapterEventListeners();
53
+ }
54
+
55
+ /**
56
+ * 运行幻灯片演示
57
+ *
58
+ * 完整流程:
59
+ * 1. 使用 SlideEngine 生成幻灯片
60
+ * 2. 执行 beforeRender 插件钩子
61
+ * 3. 初始化适配器
62
+ * 4. 渲染幻灯片
63
+ * 5. 执行 afterRender 插件钩子
64
+ *
65
+ * @param dsl - Slide DSL 对象
66
+ * @param context - 幻灯片上下文数据
67
+ */
68
+ async run(dsl: SlideDSL<TContext>, context: TContext): Promise<void> {
69
+ try {
70
+ // 1. 使用 SlideEngine 生成幻灯片
71
+ const engine = new SlideEngine(dsl);
72
+ this.slides = engine.generate(context);
73
+
74
+ // 2. 执行 beforeRender 插件钩子
75
+ await this.executePluginHook('beforeRender', this.slides);
76
+
77
+ // 3. 初始化适配器(如果尚未初始化)
78
+ if (!this.isInitialized) {
79
+ await this.adapter.initialize(this.container, this.adapterOptions);
80
+ this.isInitialized = true;
81
+ }
82
+
83
+ // 4. 渲染幻灯片
84
+ await this.adapter.render(this.slides);
85
+
86
+ // 5. 执行 afterRender 插件钩子
87
+ await this.executePluginHook('afterRender', this.slides);
88
+
89
+ // 重置当前索引
90
+ this.currentIndex = 0;
91
+ } catch (error) {
92
+ const errorMessage =
93
+ error instanceof Error ? error.message : String(error);
94
+ throw new SlideRunnerError(
95
+ `Failed to run slides: ${errorMessage}`,
96
+ 'RUN_FAILED'
97
+ );
98
+ }
99
+ }
100
+
101
+ /**
102
+ * 直接渲染幻灯片数组
103
+ *
104
+ * 跳过 SlideEngine 生成步骤,直接渲染提供的幻灯片
105
+ *
106
+ * @param slides - 幻灯片定义数组
107
+ */
108
+ async renderSlides(slides: SlideDefinition[]): Promise<void> {
109
+ try {
110
+ this.slides = slides;
111
+
112
+ // 执行 beforeRender 插件钩子
113
+ await this.executePluginHook('beforeRender', this.slides);
114
+
115
+ // 初始化适配器(如果尚未初始化)
116
+ if (!this.isInitialized) {
117
+ await this.adapter.initialize(this.container, this.adapterOptions);
118
+ this.isInitialized = true;
119
+ }
120
+
121
+ // 渲染幻灯片
122
+ await this.adapter.render(this.slides);
123
+
124
+ // 执行 afterRender 插件钩子
125
+ await this.executePluginHook('afterRender', this.slides);
126
+
127
+ // 重置当前索引
128
+ this.currentIndex = 0;
129
+ } catch (error) {
130
+ const errorMessage =
131
+ error instanceof Error ? error.message : String(error);
132
+ throw new SlideRunnerError(
133
+ `Failed to render slides: ${errorMessage}`,
134
+ 'RENDER_FAILED'
135
+ );
136
+ }
137
+ }
138
+
139
+ /**
140
+ * 导航到指定幻灯片
141
+ *
142
+ * 执行 beforeSlideChange 和 afterSlideChange 插件钩子
143
+ *
144
+ * @param index - 目标幻灯片索引
145
+ * @throws {SlideRunnerError} 如果索引超出范围
146
+ */
147
+ navigateTo(index: number): void {
148
+ if (index < 0 || index >= this.slides.length) {
149
+ throw new SlideRunnerError(
150
+ `Invalid slide index: ${index}. Valid range: 0-${this.slides.length - 1}`,
151
+ 'INVALID_INDEX'
152
+ );
153
+ }
154
+
155
+ const fromIndex = this.currentIndex;
156
+ const toIndex = index;
157
+
158
+ // 执行 beforeSlideChange 插件钩子(同步)
159
+ this.executePluginHook('beforeSlideChange', fromIndex, toIndex).then(
160
+ () => {
161
+ // 调用适配器导航
162
+ this.adapter.navigateTo(index);
163
+ this.currentIndex = index;
164
+
165
+ // 执行 afterSlideChange 插件钩子(异步)
166
+ this.executePluginHook('afterSlideChange', fromIndex, toIndex).catch(
167
+ (error) => {
168
+ console.error('Plugin afterSlideChange hook failed:', error);
169
+ }
170
+ );
171
+ }
172
+ );
173
+ }
174
+
175
+ /**
176
+ * 播放/启动演示文稿
177
+ *
178
+ * 导航到第一张幻灯片(索引 0),开始演示
179
+ */
180
+ play(): void {
181
+ if (this.slides.length === 0) {
182
+ throw new SlideRunnerError(
183
+ 'No slides to play. Call run() or renderSlides() first.',
184
+ 'NO_SLIDES'
185
+ );
186
+ }
187
+
188
+ if (!this.isInitialized) {
189
+ throw new SlideRunnerError(
190
+ 'Adapter not initialized. Call run() or renderSlides() first.',
191
+ 'NOT_INITIALIZED'
192
+ );
193
+ }
194
+
195
+ // 导航到第一张幻灯片
196
+ this.navigateTo(0);
197
+ }
198
+
199
+ /**
200
+ * 获取当前幻灯片索引
201
+ */
202
+ getCurrentIndex(): number {
203
+ return this.adapter.getCurrentIndex();
204
+ }
205
+
206
+ /**
207
+ * 获取幻灯片总数
208
+ */
209
+ getTotalSlides(): number {
210
+ return this.adapter.getTotalSlides();
211
+ }
212
+
213
+ /**
214
+ * 更新指定幻灯片
215
+ *
216
+ * 仅当适配器支持 updateSlide 方法时可用
217
+ *
218
+ * @param index - 幻灯片索引
219
+ * @param slide - 新的幻灯片定义
220
+ * @throws {SlideRunnerError} 如果适配器不支持更新
221
+ */
222
+ async updateSlide(index: number, slide: SlideDefinition): Promise<void> {
223
+ if (!this.adapter.updateSlide) {
224
+ throw new SlideRunnerError(
225
+ `Adapter "${this.adapter.name}" does not support updateSlide`,
226
+ 'UNSUPPORTED_OPERATION'
227
+ );
228
+ }
229
+
230
+ if (index < 0 || index >= this.slides.length) {
231
+ throw new SlideRunnerError(
232
+ `Invalid slide index: ${index}. Valid range: 0-${this.slides.length - 1}`,
233
+ 'INVALID_INDEX'
234
+ );
235
+ }
236
+
237
+ try {
238
+ // 更新内部状态
239
+ this.slides[index] = slide;
240
+
241
+ // 调用适配器更新
242
+ await this.adapter.updateSlide(index, slide);
243
+ } catch (error) {
244
+ const errorMessage =
245
+ error instanceof Error ? error.message : String(error);
246
+ throw new SlideRunnerError(
247
+ `Failed to update slide: ${errorMessage}`,
248
+ 'UPDATE_FAILED'
249
+ );
250
+ }
251
+ }
252
+
253
+ /**
254
+ * 刷新当前幻灯片
255
+ *
256
+ * 重新渲染所有幻灯片
257
+ */
258
+ async refresh(): Promise<void> {
259
+ try {
260
+ await this.adapter.render(this.slides);
261
+ } catch (error) {
262
+ const errorMessage =
263
+ error instanceof Error ? error.message : String(error);
264
+ throw new SlideRunnerError(
265
+ `Failed to refresh slides: ${errorMessage}`,
266
+ 'REFRESH_FAILED'
267
+ );
268
+ }
269
+ }
270
+
271
+ /**
272
+ * 销毁 SlideRunner,清理资源
273
+ */
274
+ async destroy(): Promise<void> {
275
+ try {
276
+ await this.adapter.destroy();
277
+ this.slides = [];
278
+ this.currentIndex = 0;
279
+ this.isInitialized = false;
280
+ } catch (error) {
281
+ const errorMessage =
282
+ error instanceof Error ? error.message : String(error);
283
+ throw new SlideRunnerError(
284
+ `Failed to destroy runner: ${errorMessage}`,
285
+ 'DESTROY_FAILED'
286
+ );
287
+ }
288
+ }
289
+
290
+ /**
291
+ * 注册事件监听器
292
+ *
293
+ * @param event - 事件类型
294
+ * @param handler - 事件处理器
295
+ */
296
+ on(event: AdapterEvent, handler: EventHandler): void {
297
+ this.adapter.on(event, handler);
298
+ }
299
+
300
+ /**
301
+ * 移除事件监听器
302
+ *
303
+ * @param event - 事件类型
304
+ * @param handler - 事件处理器
305
+ */
306
+ off(event: AdapterEvent, handler: EventHandler): void {
307
+ this.adapter.off(event, handler);
308
+ }
309
+
310
+ /**
311
+ * 解析容器元素
312
+ *
313
+ * @param container - 容器元素或选择器
314
+ * @returns 解析后的 HTMLElement
315
+ * @throws {SlideRunnerError} 如果容器无效或找不到
316
+ */
317
+ private resolveContainer(container: HTMLElement | string): HTMLElement {
318
+ if (typeof container === 'string') {
319
+ const element = document.querySelector<HTMLElement>(container);
320
+ if (!element) {
321
+ throw new SlideRunnerError(
322
+ `Container element not found: ${container}`,
323
+ 'CONTAINER_NOT_FOUND'
324
+ );
325
+ }
326
+ return element;
327
+ }
328
+
329
+ if (!(container instanceof HTMLElement)) {
330
+ throw new SlideRunnerError(
331
+ 'Container must be an HTMLElement or a valid selector',
332
+ 'INVALID_CONTAINER'
333
+ );
334
+ }
335
+
336
+ return container;
337
+ }
338
+
339
+ /**
340
+ * 执行插件钩子
341
+ *
342
+ * @param hookName - 钩子名称
343
+ * @param args - 钩子参数
344
+ */
345
+ private async executePluginHook(
346
+ hookName: keyof SlideRunnerPlugin,
347
+ ...args: unknown[]
348
+ ): Promise<void> {
349
+ for (const plugin of this.plugins) {
350
+ const hook = plugin[hookName];
351
+ if (typeof hook === 'function') {
352
+ try {
353
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
354
+ await (hook as any).apply(plugin, args);
355
+ } catch (error) {
356
+ console.error(
357
+ `Plugin "${plugin.name}" ${String(hookName)} hook failed:`,
358
+ error
359
+ );
360
+ // 插件错误不应阻止流程继续
361
+ }
362
+ }
363
+ }
364
+ }
365
+
366
+ /**
367
+ * 设置适配器事件监听
368
+ */
369
+ private setupAdapterEventListeners(): void {
370
+ // 监听 slideChanged 事件,更新内部状态
371
+ this.adapter.on('slideChanged', (data) => {
372
+ if (typeof data === 'object' && data !== null && 'index' in data) {
373
+ this.currentIndex = data.index as number;
374
+ }
375
+ });
376
+ }
377
+ }
package/src/types.ts ADDED
@@ -0,0 +1,164 @@
1
+ /**
2
+ * @slidejs/runner - 类型定义
3
+ *
4
+ * 定义 SlideAdapter 接口、插件系统和配置类型
5
+ */
6
+
7
+ import type { SlideDefinition } from '@slidejs/core';
8
+
9
+ /**
10
+ * 适配器事件类型
11
+ */
12
+ export type AdapterEvent = 'slideChanged' | 'slideRendered' | 'ready' | 'error';
13
+
14
+ /**
15
+ * 事件处理器
16
+ */
17
+ export type EventHandler = (data: unknown) => void;
18
+
19
+ /**
20
+ * 适配器选项基础接口
21
+ */
22
+ export interface AdapterOptions {
23
+ [key: string]: unknown;
24
+ }
25
+
26
+ /**
27
+ * SlideAdapter 接口
28
+ *
29
+ * 所有渲染引擎适配器必须实现此接口
30
+ */
31
+ export interface SlideAdapter {
32
+ /**
33
+ * 适配器名称
34
+ */
35
+ readonly name: string;
36
+
37
+ /**
38
+ * 初始化适配器
39
+ *
40
+ * @param container - 容器元素
41
+ * @param options - 适配器选项
42
+ */
43
+ initialize(container: HTMLElement, options?: AdapterOptions): Promise<void>;
44
+
45
+ /**
46
+ * 渲染幻灯片
47
+ *
48
+ * @param slides - 幻灯片定义数组
49
+ */
50
+ render(slides: SlideDefinition[]): Promise<void>;
51
+
52
+ /**
53
+ * 销毁适配器,清理资源
54
+ */
55
+ destroy(): Promise<void>;
56
+
57
+ /**
58
+ * 导航到指定幻灯片
59
+ *
60
+ * @param index - 幻灯片索引
61
+ */
62
+ navigateTo(index: number): void;
63
+
64
+ /**
65
+ * 获取当前幻灯片索引
66
+ */
67
+ getCurrentIndex(): number;
68
+
69
+ /**
70
+ * 获取幻灯片总数
71
+ */
72
+ getTotalSlides(): number;
73
+
74
+ /**
75
+ * 更新指定幻灯片(可选)
76
+ *
77
+ * @param index - 幻灯片索引
78
+ * @param slide - 新的幻灯片定义
79
+ */
80
+ updateSlide?(index: number, slide: SlideDefinition): Promise<void>;
81
+
82
+ /**
83
+ * 注册事件监听器
84
+ *
85
+ * @param event - 事件类型
86
+ * @param handler - 事件处理器
87
+ */
88
+ on(event: AdapterEvent, handler: EventHandler): void;
89
+
90
+ /**
91
+ * 移除事件监听器
92
+ *
93
+ * @param event - 事件类型
94
+ * @param handler - 事件处理器
95
+ */
96
+ off(event: AdapterEvent, handler: EventHandler): void;
97
+ }
98
+
99
+ /**
100
+ * SlideRunner 插件接口
101
+ *
102
+ * 插件可以在 SlideRunner 的生命周期钩子中执行自定义逻辑
103
+ */
104
+ export interface SlideRunnerPlugin {
105
+ /**
106
+ * 插件名称
107
+ */
108
+ name: string;
109
+
110
+ /**
111
+ * 渲染前钩子
112
+ *
113
+ * @param slides - 即将渲染的幻灯片数组
114
+ */
115
+ beforeRender?(slides: SlideDefinition[]): Promise<void> | void;
116
+
117
+ /**
118
+ * 渲染后钩子
119
+ *
120
+ * @param slides - 已渲染的幻灯片数组
121
+ */
122
+ afterRender?(slides: SlideDefinition[]): Promise<void> | void;
123
+
124
+ /**
125
+ * 幻灯片切换前钩子
126
+ *
127
+ * @param fromIndex - 当前幻灯片索引
128
+ * @param toIndex - 目标幻灯片索引
129
+ */
130
+ beforeSlideChange?(fromIndex: number, toIndex: number): Promise<void> | void;
131
+
132
+ /**
133
+ * 幻灯片切换后钩子
134
+ *
135
+ * @param fromIndex - 上一张幻灯片索引
136
+ * @param toIndex - 当前幻灯片索引
137
+ */
138
+ afterSlideChange?(fromIndex: number, toIndex: number): Promise<void> | void;
139
+ }
140
+
141
+ /**
142
+ * SlideRunner 配置
143
+ */
144
+ export interface SlideRunnerConfig {
145
+ /**
146
+ * 容器元素或选择器
147
+ */
148
+ container: HTMLElement | string;
149
+
150
+ /**
151
+ * 渲染引擎适配器
152
+ */
153
+ adapter: SlideAdapter;
154
+
155
+ /**
156
+ * 适配器选项
157
+ */
158
+ adapterOptions?: AdapterOptions;
159
+
160
+ /**
161
+ * 插件数组
162
+ */
163
+ plugins?: SlideRunnerPlugin[];
164
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "extends": "../../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "composite": true,
7
+ "declarationMap": true
8
+ },
9
+ "include": ["src/**/*"],
10
+ "exclude": ["**/*.test.ts", "dist", "node_modules"],
11
+ "references": [
12
+ { "path": "../core" },
13
+ { "path": "../context" }
14
+ ]
15
+ }