@slidejs/runner-swiper 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/adapter.ts ADDED
@@ -0,0 +1,495 @@
1
+ /**
2
+ * @slidejs/runner-swiper - SwiperAdapter 适配器实现
3
+ *
4
+ * 将 Slide DSL 渲染为 Swiper 幻灯片
5
+ */
6
+
7
+ import { Swiper } from 'swiper';
8
+ import { Navigation, Pagination, Keyboard } from 'swiper/modules';
9
+ import type { SwiperOptions } from 'swiper';
10
+ import type { SlideDefinition } from '@slidejs/core';
11
+ import type { SlideAdapter, AdapterEvent, EventHandler } from '@slidejs/runner';
12
+ import type { SwiperAdapterOptions } from './types';
13
+
14
+ // 注册 Swiper 模块
15
+ Swiper.use([Navigation, Pagination, Keyboard]);
16
+
17
+ /**
18
+ * Swiper 适配器
19
+ *
20
+ * 实现 SlideAdapter 接口,将 SlideDefinition 渲染为 Swiper 幻灯片
21
+ */
22
+ export class SwiperAdapter implements SlideAdapter {
23
+ readonly name = 'swiper';
24
+
25
+ private swiper?: Swiper;
26
+ private swiperContainer?: HTMLElement;
27
+ private swiperWrapper?: HTMLElement;
28
+ private eventHandlers: Map<AdapterEvent, Set<EventHandler>> = new Map();
29
+
30
+ /**
31
+ * 初始化 Swiper 适配器
32
+ *
33
+ * @param container - 容器元素
34
+ * @param options - Swiper 选项
35
+ */
36
+ async initialize(container: HTMLElement, options?: SwiperAdapterOptions): Promise<void> {
37
+ try {
38
+ // 创建 Swiper DOM 结构
39
+ this.createSwiperStructure(container);
40
+
41
+ // 初始化 Swiper
42
+ if (!this.swiperContainer || !this.swiperWrapper) {
43
+ throw new Error('Swiper container not created');
44
+ }
45
+
46
+ const swiperConfig: SwiperOptions = {
47
+ // 默认配置
48
+ direction: 'horizontal',
49
+ loop: false,
50
+ speed: 300,
51
+ spaceBetween: 30,
52
+ slidesPerView: 1,
53
+ // 注册模块
54
+ modules: [Navigation, Pagination, Keyboard],
55
+ // 导航配置
56
+ navigation: {
57
+ nextEl: '.swiper-button-next',
58
+ prevEl: '.swiper-button-prev',
59
+ },
60
+ // 分页配置
61
+ pagination: {
62
+ el: '.swiper-pagination',
63
+ clickable: true,
64
+ },
65
+ // 键盘控制配置
66
+ keyboard: {
67
+ enabled: true,
68
+ onlyInViewport: true,
69
+ },
70
+ ...options?.swiperConfig,
71
+ };
72
+
73
+ this.swiper = new Swiper(this.swiperContainer, swiperConfig);
74
+
75
+ // 等待 Swiper 初始化完成
76
+ // Swiper 初始化是同步的,但我们需要等待 DOM 更新
77
+ await new Promise<void>(resolve => {
78
+ // 使用 requestAnimationFrame 确保 DOM 已更新
79
+ requestAnimationFrame(() => {
80
+ resolve();
81
+ });
82
+ });
83
+
84
+ // 设置事件监听
85
+ this.setupEventListeners();
86
+
87
+ // 触发 ready 事件
88
+ this.emit('ready');
89
+ } catch (error) {
90
+ const errorMessage = error instanceof Error ? error.message : String(error);
91
+ this.emit('error', { message: errorMessage });
92
+ throw new Error(`Failed to initialize SwiperAdapter: ${errorMessage}`);
93
+ }
94
+ }
95
+
96
+ /**
97
+ * 渲染幻灯片
98
+ *
99
+ * @param slides - 幻灯片定义数组
100
+ */
101
+ async render(slides: SlideDefinition[]): Promise<void> {
102
+ if (!this.swiperWrapper || !this.swiper) {
103
+ throw new Error('SwiperAdapter not initialized');
104
+ }
105
+
106
+ try {
107
+ // 清空现有幻灯片
108
+ this.swiperWrapper.innerHTML = '';
109
+
110
+ // 渲染每张幻灯片
111
+ for (const slide of slides) {
112
+ const slideElement = await this.renderSlide(slide);
113
+ this.swiperWrapper.appendChild(slideElement);
114
+ }
115
+
116
+ // 更新 Swiper(重新计算尺寸和更新 slides)
117
+ // 在 Swiper 11 中,update() 会自动重新计算所有内容
118
+ this.swiper.update();
119
+
120
+ // 触发 slideRendered 事件
121
+ this.emit('slideRendered', { totalSlides: slides.length });
122
+ } catch (error) {
123
+ const errorMessage = error instanceof Error ? error.message : String(error);
124
+ this.emit('error', { message: errorMessage });
125
+ throw new Error(`Failed to render slides: ${errorMessage}`);
126
+ }
127
+ }
128
+
129
+ /**
130
+ * 销毁适配器
131
+ */
132
+ async destroy(): Promise<void> {
133
+ if (this.swiper) {
134
+ this.swiper.destroy(true, true);
135
+ this.swiper = undefined;
136
+ }
137
+
138
+ if (this.swiperContainer) {
139
+ this.swiperContainer.innerHTML = '';
140
+ this.swiperContainer = undefined;
141
+ }
142
+
143
+ this.swiperWrapper = undefined;
144
+ this.eventHandlers.clear();
145
+ }
146
+
147
+ /**
148
+ * 导航到指定幻灯片
149
+ *
150
+ * @param index - 幻灯片索引
151
+ */
152
+ navigateTo(index: number): void {
153
+ if (!this.swiper) {
154
+ throw new Error('SwiperAdapter not initialized');
155
+ }
156
+
157
+ this.swiper.slideTo(index);
158
+ }
159
+
160
+ /**
161
+ * 获取当前幻灯片索引
162
+ */
163
+ getCurrentIndex(): number {
164
+ if (!this.swiper) {
165
+ return 0;
166
+ }
167
+
168
+ return this.swiper.activeIndex;
169
+ }
170
+
171
+ /**
172
+ * 获取幻灯片总数
173
+ */
174
+ getTotalSlides(): number {
175
+ if (!this.swiper) {
176
+ return 0;
177
+ }
178
+
179
+ return this.swiper.slides.length;
180
+ }
181
+
182
+ /**
183
+ * 更新指定幻灯片
184
+ *
185
+ * @param index - 幻灯片索引
186
+ * @param slide - 新的幻灯片定义
187
+ */
188
+ async updateSlide(index: number, slide: SlideDefinition): Promise<void> {
189
+ if (!this.swiperWrapper || !this.swiper) {
190
+ throw new Error('SwiperAdapter not initialized');
191
+ }
192
+
193
+ try {
194
+ // 获取指定索引的 slide 元素
195
+ const slides = this.swiperWrapper.querySelectorAll('.swiper-slide');
196
+ const targetSlide = slides[index];
197
+
198
+ if (!targetSlide) {
199
+ throw new Error(`Slide at index ${index} not found`);
200
+ }
201
+
202
+ // 渲染新的幻灯片内容
203
+ const newSlide = await this.renderSlide(slide);
204
+
205
+ // 替换旧的 slide
206
+ targetSlide.replaceWith(newSlide);
207
+
208
+ // 更新 Swiper
209
+ this.swiper.update();
210
+ } catch (error) {
211
+ const errorMessage = error instanceof Error ? error.message : String(error);
212
+ this.emit('error', { message: errorMessage });
213
+ throw new Error(`Failed to update slide: ${errorMessage}`);
214
+ }
215
+ }
216
+
217
+ /**
218
+ * 注册事件监听器
219
+ *
220
+ * @param event - 事件类型
221
+ * @param handler - 事件处理器
222
+ */
223
+ on(event: AdapterEvent, handler: EventHandler): void {
224
+ if (!this.eventHandlers.has(event)) {
225
+ this.eventHandlers.set(event, new Set());
226
+ }
227
+ this.eventHandlers.get(event)!.add(handler);
228
+ }
229
+
230
+ /**
231
+ * 移除事件监听器
232
+ *
233
+ * @param event - 事件类型
234
+ * @param handler - 事件处理器
235
+ */
236
+ off(event: AdapterEvent, handler: EventHandler): void {
237
+ const handlers = this.eventHandlers.get(event);
238
+ if (handlers) {
239
+ handlers.delete(handler);
240
+ }
241
+ }
242
+
243
+ /**
244
+ * 创建 Swiper DOM 结构
245
+ *
246
+ * @param container - 容器元素
247
+ */
248
+ private createSwiperStructure(container: HTMLElement): void {
249
+ // 创建 .swiper 容器
250
+ const swiperDiv = document.createElement('div');
251
+ swiperDiv.className = 'swiper';
252
+
253
+ // 创建 .swiper-wrapper 容器
254
+ const wrapperDiv = document.createElement('div');
255
+ wrapperDiv.className = 'swiper-wrapper';
256
+
257
+ // 创建导航按钮
258
+ const prevButton = document.createElement('div');
259
+ prevButton.className = 'swiper-button-prev';
260
+ const nextButton = document.createElement('div');
261
+ nextButton.className = 'swiper-button-next';
262
+
263
+ // 创建分页器
264
+ const pagination = document.createElement('div');
265
+ pagination.className = 'swiper-pagination';
266
+
267
+ swiperDiv.appendChild(wrapperDiv);
268
+ swiperDiv.appendChild(prevButton);
269
+ swiperDiv.appendChild(nextButton);
270
+ swiperDiv.appendChild(pagination);
271
+ container.appendChild(swiperDiv);
272
+
273
+ this.swiperContainer = swiperDiv;
274
+ this.swiperWrapper = wrapperDiv;
275
+ }
276
+
277
+ /**
278
+ * 设置 Swiper 事件监听
279
+ */
280
+ private setupEventListeners(): void {
281
+ if (!this.swiper) {
282
+ return;
283
+ }
284
+
285
+ // 监听幻灯片切换事件
286
+ this.swiper.on('slideChange', () => {
287
+ const currentIndex = this.swiper!.activeIndex;
288
+ const previousIndex = this.swiper!.previousIndex;
289
+ this.emit('slideChanged', {
290
+ index: currentIndex,
291
+ previousIndex,
292
+ from: previousIndex,
293
+ to: currentIndex,
294
+ });
295
+ });
296
+ }
297
+
298
+ /**
299
+ * 渲染单张幻灯片
300
+ *
301
+ * @param slide - 幻灯片定义
302
+ * @returns slide 元素
303
+ */
304
+ private async renderSlide(slide: SlideDefinition): Promise<HTMLElement> {
305
+ const slideElement = document.createElement('div');
306
+ slideElement.className = 'swiper-slide';
307
+
308
+ // 设置过渡效果(通过 CSS 类)
309
+ if (slide.behavior?.transition) {
310
+ const transitionClass = this.mapTransition(slide.behavior.transition.type);
311
+ if (transitionClass) {
312
+ slideElement.classList.add(`slide-transition-${transitionClass}`);
313
+ }
314
+ }
315
+
316
+ // 渲染内容
317
+ if (slide.content.type === 'dynamic') {
318
+ // 动态内容(Web Components)
319
+ const content = await this.renderDynamicContent(slide.content.component, slide.content.props);
320
+ slideElement.appendChild(content);
321
+ } else {
322
+ // 静态文本内容
323
+ const content = this.renderTextContent(slide.content.lines);
324
+ slideElement.appendChild(content);
325
+ }
326
+
327
+ return slideElement;
328
+ }
329
+
330
+ /**
331
+ * 渲染动态内容(Web Component)
332
+ *
333
+ * 支持所有 Web Components,包括:
334
+ * - 标准 Web Components(原生 Custom Elements)
335
+ * - wsx 组件(编译为标准 Web Components)
336
+ * - 其他框架的 Web Components(Vue、React、Angular 等)
337
+ *
338
+ * @param component - 组件名称
339
+ * @param props - 组件属性
340
+ * @returns 组件元素
341
+ */
342
+ private async renderDynamicContent(
343
+ component: string,
344
+ props: Record<string, unknown>
345
+ ): Promise<HTMLElement> {
346
+ // 创建 Web Component 元素
347
+ const element = document.createElement(component);
348
+
349
+ // 设置属性
350
+ for (const [key, value] of Object.entries(props)) {
351
+ if (typeof value === 'string' || typeof value === 'number') {
352
+ // 字符串和数字 → HTML attributes
353
+ element.setAttribute(key, String(value));
354
+ } else if (typeof value === 'boolean') {
355
+ // 布尔值 → HTML attributes(true 时设置空属性)
356
+ if (value) {
357
+ element.setAttribute(key, '');
358
+ }
359
+ } else {
360
+ // 对象和数组 → JavaScript properties
361
+ (element as Record<string, unknown>)[key] = value;
362
+ }
363
+ }
364
+
365
+ return element;
366
+ }
367
+
368
+ /**
369
+ * 渲染文本内容
370
+ *
371
+ * 支持以下格式:
372
+ * - # 标题 -> h1
373
+ * - ## 标题 -> h2
374
+ * - ### 标题 -> h3
375
+ * - ![alt](url) -> img
376
+ * - - 列表项 -> ul/li
377
+ * - 普通文本 -> p
378
+ *
379
+ * @param lines - 文本行数组
380
+ * @returns 内容容器元素
381
+ */
382
+ private renderTextContent(lines: string[]): HTMLElement {
383
+ const container = document.createElement('div');
384
+ container.className = 'slide-content';
385
+
386
+ let listContainer: HTMLUListElement | null = null;
387
+
388
+ for (const line of lines) {
389
+ const trimmedLine = line.trim();
390
+
391
+ // 空行,结束列表
392
+ if (!trimmedLine) {
393
+ listContainer = null;
394
+ continue;
395
+ }
396
+
397
+ // 标题 - # H1
398
+ if (trimmedLine.startsWith('# ')) {
399
+ listContainer = null;
400
+ const h1 = document.createElement('h1');
401
+ h1.textContent = trimmedLine.substring(2);
402
+ container.appendChild(h1);
403
+ continue;
404
+ }
405
+
406
+ // 标题 - ## H2
407
+ if (trimmedLine.startsWith('## ')) {
408
+ listContainer = null;
409
+ const h2 = document.createElement('h2');
410
+ h2.textContent = trimmedLine.substring(3);
411
+ container.appendChild(h2);
412
+ continue;
413
+ }
414
+
415
+ // 标题 - ### H3
416
+ if (trimmedLine.startsWith('### ')) {
417
+ listContainer = null;
418
+ const h3 = document.createElement('h3');
419
+ h3.textContent = trimmedLine.substring(4);
420
+ container.appendChild(h3);
421
+ continue;
422
+ }
423
+
424
+ // 图片 - ![alt](url)
425
+ const imageMatch = trimmedLine.match(/^!\[(.*?)\]\((.*?)\)$/);
426
+ if (imageMatch) {
427
+ listContainer = null;
428
+ const img = document.createElement('img');
429
+ img.alt = imageMatch[1];
430
+ img.src = imageMatch[2];
431
+ img.style.maxWidth = '80%';
432
+ img.style.maxHeight = '500px';
433
+ container.appendChild(img);
434
+ continue;
435
+ }
436
+
437
+ // 列表项 - - 项目
438
+ if (trimmedLine.startsWith('- ')) {
439
+ if (!listContainer) {
440
+ listContainer = document.createElement('ul');
441
+ container.appendChild(listContainer);
442
+ }
443
+ const li = document.createElement('li');
444
+ li.textContent = trimmedLine.substring(2);
445
+ listContainer.appendChild(li);
446
+ continue;
447
+ }
448
+
449
+ // 普通文本
450
+ listContainer = null;
451
+ const p = document.createElement('p');
452
+ p.textContent = trimmedLine;
453
+ container.appendChild(p);
454
+ }
455
+
456
+ return container;
457
+ }
458
+
459
+ /**
460
+ * 映射 Slide DSL 过渡效果到 Swiper 过渡效果
461
+ *
462
+ * @param transition - Slide DSL 过渡效果
463
+ * @returns Swiper 过渡效果类名(用于 CSS)
464
+ */
465
+ private mapTransition(
466
+ transition?: 'slide' | 'zoom' | 'fade' | 'cube' | 'flip' | 'none'
467
+ ): string | null {
468
+ if (!transition || transition === 'none') {
469
+ return null;
470
+ }
471
+
472
+ // Swiper 默认支持 slide,其他效果可以通过 CSS 实现
473
+ // 返回类名以便通过 CSS 应用自定义过渡效果
474
+ return transition;
475
+ }
476
+
477
+ /**
478
+ * 触发事件
479
+ *
480
+ * @param event - 事件类型
481
+ * @param data - 事件数据
482
+ */
483
+ private emit(event: AdapterEvent, data?: unknown): void {
484
+ const handlers = this.eventHandlers.get(event);
485
+ if (handlers) {
486
+ for (const handler of handlers) {
487
+ try {
488
+ handler(data);
489
+ } catch (error) {
490
+ console.error(`Error in ${event} handler:`, error);
491
+ }
492
+ }
493
+ }
494
+ }
495
+ }
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @slidejs/runner-swiper - Swiper 适配器
3
+ *
4
+ * 将 Slide DSL 渲染为 Swiper 幻灯片
5
+ */
6
+
7
+ export { SwiperAdapter } from './adapter';
8
+ export { createSlideRunner, type SlideRunnerConfig } from './runner';
9
+ export type { SwiperAdapterOptions } from './types';
package/src/runner.ts ADDED
@@ -0,0 +1,84 @@
1
+ /**
2
+ * @slidejs/runner-swiper - SlideRunner 工厂函数
3
+ *
4
+ * 提供创建配置好的 SlideRunner 实例的便捷方法
5
+ */
6
+
7
+ import { parseSlideDSL, compile } from '@slidejs/dsl';
8
+ import { SlideRunner } from '@slidejs/runner';
9
+ import type { SlideContext } from '@slidejs/context';
10
+ import { SwiperAdapter } from './adapter';
11
+ import type { SwiperAdapterOptions } from './types';
12
+
13
+ /**
14
+ * SlideRunner 配置选项
15
+ */
16
+ export interface SlideRunnerConfig {
17
+ /**
18
+ * 容器选择器或 HTMLElement
19
+ */
20
+ container: string | HTMLElement;
21
+
22
+ /**
23
+ * Swiper 配置选项
24
+ */
25
+ swiperOptions?: SwiperAdapterOptions['swiperConfig'];
26
+ }
27
+
28
+ /**
29
+ * 从 DSL 源代码创建并运行 SlideRunner
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * import { createSlideRunner } from '@slidejs/runner-swiper';
34
+ *
35
+ * const dslSource = `
36
+ * present quiz "demo" {
37
+ * rules {
38
+ * rule start "intro" {
39
+ * slide {
40
+ * content text { "Hello World!" }
41
+ * }
42
+ * }
43
+ * }
44
+ * }
45
+ * `;
46
+ *
47
+ * const context = { sourceType: 'quiz', sourceId: 'demo', items: [] };
48
+ * const runner = await createSlideRunner(dslSource, context, {
49
+ * container: '#app',
50
+ * swiperOptions: {
51
+ * navigation: true,
52
+ * pagination: true,
53
+ * },
54
+ * });
55
+ * ```
56
+ */
57
+ export async function createSlideRunner<TContext extends SlideContext = SlideContext>(
58
+ dslSource: string,
59
+ context: TContext,
60
+ config: SlideRunnerConfig
61
+ ): Promise<SlideRunner<TContext>> {
62
+ // 1. 解析 DSL
63
+ const ast = await parseSlideDSL(dslSource);
64
+
65
+ // 2. 编译为 SlideDSL
66
+ const slideDSL = compile<TContext>(ast);
67
+
68
+ // 3. 创建适配器和 Runner
69
+ const adapter = new SwiperAdapter();
70
+ const runner = new SlideRunner<TContext>({
71
+ container: config.container,
72
+ adapter,
73
+ adapterOptions: {
74
+ swiperConfig: config.swiperOptions,
75
+ },
76
+ });
77
+
78
+ // 4. 运行演示(这会初始化适配器并渲染幻灯片)
79
+ await runner.run(slideDSL, context);
80
+
81
+ // 注意:需要手动调用 runner.play() 来启动演示(导航到第一张幻灯片)
82
+ // 返回 runner 以便用户可以控制演示
83
+ return runner;
84
+ }
package/src/types.ts ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @slidejs/runner-swiper - 类型定义
3
+ *
4
+ * 定义 Swiper 适配器的选项和配置
5
+ */
6
+
7
+ import type { SwiperOptions } from 'swiper';
8
+ import type { AdapterOptions } from '@slidejs/runner';
9
+
10
+ /**
11
+ * SwiperAdapter 选项
12
+ *
13
+ * Swiper CSS 需要手动导入:
14
+ * ```typescript
15
+ * import 'swiper/css';
16
+ * import 'swiper/css/navigation';
17
+ * import 'swiper/css/pagination';
18
+ * ```
19
+ *
20
+ * 注意:Keyboard、Navigation 和 Pagination 模块已在适配器中自动注册,
21
+ * 无需在配置中再次指定 modules。
22
+ */
23
+ export interface SwiperAdapterOptions extends AdapterOptions {
24
+ /**
25
+ * Swiper 配置选项
26
+ * @see https://swiperjs.com/swiper-api#parameters
27
+ */
28
+ swiperConfig?: SwiperOptions;
29
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
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": [{ "path": "../core" }, { "path": "../runner" }]
12
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,33 @@
1
+ import { defineConfig } from 'vite';
2
+ import dts from 'vite-plugin-dts';
3
+ import { resolve } from 'path';
4
+
5
+ export default defineConfig({
6
+ build: {
7
+ lib: {
8
+ entry: resolve(__dirname, 'src/index.ts'),
9
+ name: 'SlideJsSwiper',
10
+ formats: ['es', 'cjs'],
11
+ fileName: format => `index.${format === 'es' ? 'js' : 'cjs'}`,
12
+ },
13
+ rollupOptions: {
14
+ external: [
15
+ '@slidejs/core',
16
+ '@slidejs/runner',
17
+ '@slidejs/dsl',
18
+ '@slidejs/context',
19
+ 'swiper',
20
+ // Only externalize JS modules from swiper, NOT CSS
21
+ /^swiper\/.*\.js$/,
22
+ ],
23
+ },
24
+ sourcemap: true,
25
+ },
26
+ plugins: [
27
+ dts({
28
+ include: ['src/**/*'],
29
+ exclude: ['**/*.test.ts'],
30
+ rollupTypes: true,
31
+ }),
32
+ ],
33
+ });