squiffy-runtime 6.0.0-alpha.10 → 6.0.0-alpha.12

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.
@@ -0,0 +1,15 @@
1
+ type LinkType = 'section' | 'passage' | 'rotate' | 'sequence';
2
+ export type SquiffyEventMap = {
3
+ linkClick: {
4
+ linkType: LinkType;
5
+ };
6
+ };
7
+ export type SquiffyEventHandler<E extends keyof SquiffyEventMap> = (payload: SquiffyEventMap[E]) => void;
8
+ export declare class Emitter<Events extends Record<string, any>> {
9
+ private listeners;
10
+ on<E extends keyof Events>(event: E, handler: (p: Events[E]) => void): () => void;
11
+ off<E extends keyof Events>(event: E, handler: (p: Events[E]) => void): void;
12
+ once<E extends keyof Events>(event: E, handler: (p: Events[E]) => void): () => void;
13
+ emit<E extends keyof Events>(event: E, payload: Events[E]): void;
14
+ }
15
+ export {};
package/dist/events.js ADDED
@@ -0,0 +1,35 @@
1
+ export class Emitter {
2
+ constructor() {
3
+ this.listeners = new Map();
4
+ }
5
+ on(event, handler) {
6
+ if (!this.listeners.has(event))
7
+ this.listeners.set(event, new Set());
8
+ this.listeners.get(event).add(handler);
9
+ return () => this.off(event, handler);
10
+ }
11
+ off(event, handler) {
12
+ this.listeners.get(event)?.delete(handler);
13
+ }
14
+ once(event, handler) {
15
+ const off = this.on(event, (payload) => {
16
+ off();
17
+ handler(payload);
18
+ });
19
+ return off;
20
+ }
21
+ emit(event, payload) {
22
+ // Fire handlers asynchronously so the runtime isn't blocked by user code.
23
+ queueMicrotask(() => {
24
+ this.listeners.get(event)?.forEach(h => {
25
+ try {
26
+ h(payload);
27
+ }
28
+ catch (err) {
29
+ // Swallow so a bad handler doesn't break the game; optionally log.
30
+ console.error(`[Squiffy] handler for "${String(event)}" failed`, err);
31
+ }
32
+ });
33
+ });
34
+ }
35
+ }
@@ -1,51 +1,3 @@
1
- export interface SquiffyInitOptions {
2
- element: HTMLElement;
3
- story: Story;
4
- scroll?: string;
5
- persist?: boolean;
6
- onSet?: (attribute: string, value: any) => void;
7
- }
8
- export interface SquiffySettings {
9
- scroll: string;
10
- persist: boolean;
11
- onSet: (attribute: string, value: any) => void;
12
- }
13
- export interface SquiffyApi {
14
- restart: () => void;
15
- get: (attribute: string) => any;
16
- set: (attribute: string, value: any) => void;
17
- clickLink: (link: HTMLElement) => boolean;
18
- update: (story: Story) => void;
19
- }
20
- interface SquiffyJsFunctionApi {
21
- get: (attribute: string) => any;
22
- set: (attribute: string, value: any) => void;
23
- ui: {
24
- transition: (f: any) => void;
25
- };
26
- story: {
27
- go: (section: string) => void;
28
- };
29
- }
30
- export interface Story {
31
- js: ((squiffy: SquiffyJsFunctionApi, get: (attribute: string) => any, set: (attribute: string, value: any) => void) => void)[];
32
- start: string;
33
- id?: string | null;
34
- sections: Record<string, Section>;
35
- }
36
- export interface Section {
37
- text?: string;
38
- clear?: boolean;
39
- attributes?: string[];
40
- jsIndex?: number;
41
- passages?: Record<string, Passage>;
42
- passageCount?: number;
43
- }
44
- export interface Passage {
45
- text?: string;
46
- clear?: boolean;
47
- attributes?: string[];
48
- jsIndex?: number;
49
- }
1
+ import { SquiffyApi, SquiffyInitOptions } from './types.js';
2
+ export type { SquiffyApi } from "./types.js";
50
3
  export declare const init: (options: SquiffyInitOptions) => SquiffyApi;
51
- export {};
@@ -1,3 +1,6 @@
1
+ import { startsWith, rotate } from "./utils.js";
2
+ import { TextProcessor } from './textProcessor.js';
3
+ import { Emitter } from './events.js';
1
4
  export const init = (options) => {
2
5
  let story;
3
6
  let currentSection;
@@ -7,6 +10,8 @@ export const init = (options) => {
7
10
  let outputElement;
8
11
  let settings;
9
12
  let storageFallback = {};
13
+ let textProcessor;
14
+ const emitter = new Emitter();
10
15
  function set(attribute, value) {
11
16
  if (typeof value === 'undefined')
12
17
  value = true;
@@ -58,6 +63,7 @@ export const init = (options) => {
58
63
  showPassage('@last');
59
64
  }
60
65
  }
66
+ emitter.emit('linkClick', { linkType: 'passage' });
61
67
  return true;
62
68
  }
63
69
  if (section) {
@@ -65,6 +71,7 @@ export const init = (options) => {
65
71
  if (section) {
66
72
  go(section);
67
73
  }
74
+ emitter.emit('linkClick', { linkType: 'section' });
68
75
  return true;
69
76
  }
70
77
  if (rotateOrSequenceAttr) {
@@ -80,6 +87,7 @@ export const init = (options) => {
80
87
  set(attribute, result[0]);
81
88
  }
82
89
  save();
90
+ emitter.emit('linkClick', { linkType: rotateAttr ? 'rotate' : 'sequence' });
83
91
  return true;
84
92
  }
85
93
  return false;
@@ -408,184 +416,16 @@ export const init = (options) => {
408
416
  }
409
417
  },
410
418
  processText: (text) => {
411
- function process(text, data) {
412
- let containsUnprocessedSection = false;
413
- const open = text.indexOf('{');
414
- let close;
415
- if (open > -1) {
416
- let nestCount = 1;
417
- let searchStart = open + 1;
418
- let finished = false;
419
- while (!finished) {
420
- const nextOpen = text.indexOf('{', searchStart);
421
- const nextClose = text.indexOf('}', searchStart);
422
- if (nextClose > -1) {
423
- if (nextOpen > -1 && nextOpen < nextClose) {
424
- nestCount++;
425
- searchStart = nextOpen + 1;
426
- }
427
- else {
428
- nestCount--;
429
- searchStart = nextClose + 1;
430
- if (nestCount === 0) {
431
- close = nextClose;
432
- containsUnprocessedSection = true;
433
- finished = true;
434
- }
435
- }
436
- }
437
- else {
438
- finished = true;
439
- }
440
- }
441
- }
442
- if (containsUnprocessedSection) {
443
- const section = text.substring(open + 1, close);
444
- const value = processTextCommand(section, data);
445
- text = text.substring(0, open) + value + process(text.substring(close + 1), data);
446
- }
447
- return (text);
448
- }
449
- function processTextCommand(text, data) {
450
- if (startsWith(text, 'if ')) {
451
- return processTextCommand_If(text, data);
452
- }
453
- else if (startsWith(text, 'else:')) {
454
- return processTextCommand_Else(text, data);
455
- }
456
- else if (startsWith(text, 'label:')) {
457
- return processTextCommand_Label(text, data);
458
- }
459
- else if (/^rotate[: ]/.test(text)) {
460
- return processTextCommand_Rotate('rotate', text);
461
- }
462
- else if (/^sequence[: ]/.test(text)) {
463
- return processTextCommand_Rotate('sequence', text);
464
- }
465
- else if (currentSection.passages && text in currentSection.passages) {
466
- return process(currentSection.passages[text].text || '', data);
467
- }
468
- else if (text in story.sections) {
469
- return process(story.sections[text].text || '', data);
470
- }
471
- else if (startsWith(text, '@') && !startsWith(text, '@replace')) {
472
- processAttributes(text.substring(1).split(","));
473
- return "";
474
- }
475
- return get(text);
476
- }
477
- function processTextCommand_If(section, data) {
478
- const command = section.substring(3);
479
- const colon = command.indexOf(':');
480
- if (colon == -1) {
481
- return ('{if ' + command + '}');
482
- }
483
- const text = command.substring(colon + 1);
484
- let condition = command.substring(0, colon);
485
- condition = condition.replace("<", "&lt;");
486
- const operatorRegex = /([\w ]*)(=|&lt;=|&gt;=|&lt;&gt;|&lt;|&gt;)(.*)/;
487
- const match = operatorRegex.exec(condition);
488
- let result = false;
489
- if (match) {
490
- const lhs = get(match[1]);
491
- const op = match[2];
492
- let rhs = match[3];
493
- if (startsWith(rhs, '@'))
494
- rhs = get(rhs.substring(1));
495
- if (op == '=' && lhs == rhs)
496
- result = true;
497
- if (op == '&lt;&gt;' && lhs != rhs)
498
- result = true;
499
- if (op == '&gt;' && lhs > rhs)
500
- result = true;
501
- if (op == '&lt;' && lhs < rhs)
502
- result = true;
503
- if (op == '&gt;=' && lhs >= rhs)
504
- result = true;
505
- if (op == '&lt;=' && lhs <= rhs)
506
- result = true;
507
- }
508
- else {
509
- let checkValue = true;
510
- if (startsWith(condition, 'not ')) {
511
- condition = condition.substring(4);
512
- checkValue = false;
513
- }
514
- if (startsWith(condition, 'seen ')) {
515
- result = (seen(condition.substring(5)) == checkValue);
516
- }
517
- else {
518
- let value = get(condition);
519
- if (value === null)
520
- value = false;
521
- result = (value == checkValue);
522
- }
523
- }
524
- const textResult = result ? process(text, data) : '';
525
- data.lastIf = result;
526
- return textResult;
527
- }
528
- function processTextCommand_Else(section, data) {
529
- if (!('lastIf' in data) || data.lastIf)
530
- return '';
531
- const text = section.substring(5);
532
- return process(text, data);
533
- }
534
- function processTextCommand_Label(section, data) {
535
- const command = section.substring(6);
536
- const eq = command.indexOf('=');
537
- if (eq == -1) {
538
- return ('{label:' + command + '}');
539
- }
540
- const text = command.substring(eq + 1);
541
- const label = command.substring(0, eq);
542
- return '<span class="squiffy-label-' + label + '">' + process(text, data) + '</span>';
543
- }
544
- function processTextCommand_Rotate(type, section) {
545
- let options;
546
- let attribute = '';
547
- if (section.substring(type.length, type.length + 1) == ' ') {
548
- const colon = section.indexOf(':');
549
- if (colon == -1) {
550
- return '{' + section + '}';
551
- }
552
- options = section.substring(colon + 1);
553
- attribute = section.substring(type.length + 1, colon);
554
- }
555
- else {
556
- options = section.substring(type.length + 1);
557
- }
558
- // TODO: Check - previously there was no second parameter here
559
- const rotation = rotate(options.replace(/"/g, '&quot;').replace(/'/g, '&#39;'), null);
560
- if (attribute) {
561
- set(attribute, rotation[0]);
562
- }
563
- return '<a class="squiffy-link" data-' + type + '="' + rotation[1] + '" data-attribute="' + attribute + '" role="link">' + rotation[0] + '</a>';
564
- }
565
419
  const data = {
566
420
  fulltext: text
567
421
  };
568
- return process(text, data);
422
+ return textProcessor.process(text, data);
569
423
  },
570
424
  transition: function (f) {
571
425
  set('_transition', f.toString());
572
426
  f();
573
427
  },
574
428
  };
575
- function startsWith(string, prefix) {
576
- return string.substring(0, prefix.length) === prefix;
577
- }
578
- function rotate(options, current) {
579
- const colon = options.indexOf(':');
580
- if (colon == -1) {
581
- return [options, current];
582
- }
583
- const next = options.substring(0, colon);
584
- let remaining = options.substring(colon + 1);
585
- if (current)
586
- remaining += ':' + current;
587
- return [next, remaining];
588
- }
589
429
  function safeQuerySelector(name) {
590
430
  return name.replace(/'/g, "\\'");
591
431
  }
@@ -674,6 +514,7 @@ export const init = (options) => {
674
514
  return;
675
515
  handleClick(event);
676
516
  });
517
+ textProcessor = new TextProcessor(get, set, story, ui, seen, processAttributes);
677
518
  begin();
678
519
  return {
679
520
  restart: restart,
@@ -681,5 +522,8 @@ export const init = (options) => {
681
522
  set: set,
682
523
  clickLink: handleLink,
683
524
  update: update,
525
+ on: (e, h) => emitter.on(e, h),
526
+ off: (e, h) => emitter.off(e, h),
527
+ once: (e, h) => emitter.once(e, h),
684
528
  };
685
529
  };
@@ -1,4 +1,4 @@
1
- import { expect, test, beforeEach, afterEach } from 'vitest';
1
+ import { expect, test, beforeEach, afterEach, vi } from 'vitest';
2
2
  import fs from 'fs/promises';
3
3
  import globalJsdom from 'global-jsdom';
4
4
  import { init } from './squiffy.runtime.js';
@@ -18,7 +18,6 @@ const html = `
18
18
  `;
19
19
  const compile = async (script) => {
20
20
  const compileResult = await squiffyCompile({
21
- scriptBaseFilename: "filename.squiffy", // TODO: This shouldn't be required
22
21
  script: script,
23
22
  });
24
23
  if (!compileResult.success) {
@@ -34,7 +33,6 @@ const compile = async (script) => {
34
33
  };
35
34
  };
36
35
  const initScript = async (script) => {
37
- globalJsdom(html);
38
36
  const element = document.getElementById('squiffy');
39
37
  if (!element) {
40
38
  throw new Error('Element not found');
@@ -64,7 +62,7 @@ const getTestOutput = () => {
64
62
  };
65
63
  let cleanup;
66
64
  beforeEach(() => {
67
- cleanup = globalJsdom();
65
+ cleanup = globalJsdom(html);
68
66
  });
69
67
  afterEach(() => {
70
68
  cleanup();
@@ -83,11 +81,18 @@ test('Click a section link', async () => {
83
81
  expect(linkToPassage).not.toBeNull();
84
82
  expect(section3Link).not.toBeNull();
85
83
  expect(linkToPassage.classList).not.toContain('disabled');
84
+ const handler = vi.fn();
85
+ const off = squiffyApi.on('linkClick', handler);
86
86
  const handled = squiffyApi.clickLink(section3Link);
87
87
  expect(handled).toBe(true);
88
88
  expect(element.innerHTML).toMatchSnapshot();
89
89
  // passage link is from the previous section, so should be unclickable
90
90
  expect(squiffyApi.clickLink(linkToPassage)).toBe(false);
91
+ // await for the event to be processed
92
+ await Promise.resolve();
93
+ expect(handler).toHaveBeenCalledTimes(1);
94
+ expect(handler).toHaveBeenCalledWith(expect.objectContaining({ linkType: 'section' }));
95
+ off();
91
96
  });
92
97
  test('Click a passage link', async () => {
93
98
  const script = await fs.readFile('../examples/test/example.squiffy', 'utf-8');
@@ -99,12 +104,19 @@ test('Click a passage link', async () => {
99
104
  expect(linkToPassage).not.toBeNull();
100
105
  expect(section3Link).not.toBeNull();
101
106
  expect(linkToPassage.classList).not.toContain('disabled');
107
+ const handler = vi.fn();
108
+ const off = squiffyApi.on('linkClick', handler);
102
109
  const handled = squiffyApi.clickLink(linkToPassage);
103
110
  expect(handled).toBe(true);
104
111
  expect(linkToPassage.classList).toContain('disabled');
105
112
  expect(element.innerHTML).toMatchSnapshot();
106
113
  // shouldn't be able to click it again
107
114
  expect(squiffyApi.clickLink(linkToPassage)).toBe(false);
115
+ // await for the event to be processed
116
+ await Promise.resolve();
117
+ expect(handler).toHaveBeenCalledTimes(1);
118
+ expect(handler).toHaveBeenCalledWith(expect.objectContaining({ linkType: 'passage' }));
119
+ off();
108
120
  });
109
121
  test('Run JavaScript functions', async () => {
110
122
  const script = `
@@ -0,0 +1,15 @@
1
+ export declare class TextProcessor {
2
+ get: (attribute: string) => any;
3
+ set: (attribute: string, value: any) => void;
4
+ story: any;
5
+ currentSection: any;
6
+ seen: (section: string) => boolean;
7
+ processAttributes: (attributes: string[]) => void;
8
+ constructor(get: (attribute: string) => any, set: (attribute: string, value: any) => void, story: any, currentSection: any, seen: (section: string) => boolean, processAttributes: (attributes: string[]) => void);
9
+ process(text: string, data: any): string;
10
+ processTextCommand(text: string, data: any): any;
11
+ processTextCommand_If(section: string, data: any): string;
12
+ processTextCommand_Else(section: string, data: any): string;
13
+ processTextCommand_Label(section: string, data: any): string;
14
+ processTextCommand_Rotate(type: string, section: string): string;
15
+ }
@@ -0,0 +1,165 @@
1
+ import { startsWith, rotate } from "./utils.js";
2
+ export class TextProcessor {
3
+ constructor(get, set, story, currentSection, seen, processAttributes) {
4
+ this.get = get;
5
+ this.set = set;
6
+ this.story = story;
7
+ this.currentSection = currentSection;
8
+ this.seen = seen;
9
+ this.processAttributes = processAttributes;
10
+ }
11
+ process(text, data) {
12
+ let containsUnprocessedSection = false;
13
+ const open = text.indexOf('{');
14
+ let close;
15
+ if (open > -1) {
16
+ let nestCount = 1;
17
+ let searchStart = open + 1;
18
+ let finished = false;
19
+ while (!finished) {
20
+ const nextOpen = text.indexOf('{', searchStart);
21
+ const nextClose = text.indexOf('}', searchStart);
22
+ if (nextClose > -1) {
23
+ if (nextOpen > -1 && nextOpen < nextClose) {
24
+ nestCount++;
25
+ searchStart = nextOpen + 1;
26
+ }
27
+ else {
28
+ nestCount--;
29
+ searchStart = nextClose + 1;
30
+ if (nestCount === 0) {
31
+ close = nextClose;
32
+ containsUnprocessedSection = true;
33
+ finished = true;
34
+ }
35
+ }
36
+ }
37
+ else {
38
+ finished = true;
39
+ }
40
+ }
41
+ }
42
+ if (containsUnprocessedSection) {
43
+ const section = text.substring(open + 1, close);
44
+ const value = this.processTextCommand(section, data);
45
+ text = text.substring(0, open) + value + this.process(text.substring(close + 1), data);
46
+ }
47
+ return (text);
48
+ }
49
+ processTextCommand(text, data) {
50
+ if (startsWith(text, 'if ')) {
51
+ return this.processTextCommand_If(text, data);
52
+ }
53
+ else if (startsWith(text, 'else:')) {
54
+ return this.processTextCommand_Else(text, data);
55
+ }
56
+ else if (startsWith(text, 'label:')) {
57
+ return this.processTextCommand_Label(text, data);
58
+ }
59
+ else if (/^rotate[: ]/.test(text)) {
60
+ return this.processTextCommand_Rotate('rotate', text);
61
+ }
62
+ else if (/^sequence[: ]/.test(text)) {
63
+ return this.processTextCommand_Rotate('sequence', text);
64
+ }
65
+ else if (this.currentSection.passages && text in this.currentSection.passages) {
66
+ return this.process(this.currentSection.passages[text].text || '', data);
67
+ }
68
+ else if (text in this.story.sections) {
69
+ return this.process(this.story.sections[text].text || '', data);
70
+ }
71
+ else if (startsWith(text, '@') && !startsWith(text, '@replace')) {
72
+ this.processAttributes(text.substring(1).split(","));
73
+ return "";
74
+ }
75
+ return this.get(text);
76
+ }
77
+ processTextCommand_If(section, data) {
78
+ const command = section.substring(3);
79
+ const colon = command.indexOf(':');
80
+ if (colon == -1) {
81
+ return ('{if ' + command + '}');
82
+ }
83
+ const text = command.substring(colon + 1);
84
+ let condition = command.substring(0, colon);
85
+ condition = condition.replace("<", "&lt;");
86
+ const operatorRegex = /([\w ]*)(=|&lt;=|&gt;=|&lt;&gt;|&lt;|&gt;)(.*)/;
87
+ const match = operatorRegex.exec(condition);
88
+ let result = false;
89
+ if (match) {
90
+ const lhs = this.get(match[1]);
91
+ const op = match[2];
92
+ let rhs = match[3];
93
+ if (startsWith(rhs, '@'))
94
+ rhs = this.get(rhs.substring(1));
95
+ if (op == '=' && lhs == rhs)
96
+ result = true;
97
+ if (op == '&lt;&gt;' && lhs != rhs)
98
+ result = true;
99
+ if (op == '&gt;' && lhs > rhs)
100
+ result = true;
101
+ if (op == '&lt;' && lhs < rhs)
102
+ result = true;
103
+ if (op == '&gt;=' && lhs >= rhs)
104
+ result = true;
105
+ if (op == '&lt;=' && lhs <= rhs)
106
+ result = true;
107
+ }
108
+ else {
109
+ let checkValue = true;
110
+ if (startsWith(condition, 'not ')) {
111
+ condition = condition.substring(4);
112
+ checkValue = false;
113
+ }
114
+ if (startsWith(condition, 'seen ')) {
115
+ result = (this.seen(condition.substring(5)) == checkValue);
116
+ }
117
+ else {
118
+ let value = this.get(condition);
119
+ if (value === null)
120
+ value = false;
121
+ result = (value == checkValue);
122
+ }
123
+ }
124
+ const textResult = result ? this.process(text, data) : '';
125
+ data.lastIf = result;
126
+ return textResult;
127
+ }
128
+ processTextCommand_Else(section, data) {
129
+ if (!('lastIf' in data) || data.lastIf)
130
+ return '';
131
+ const text = section.substring(5);
132
+ return this.process(text, data);
133
+ }
134
+ processTextCommand_Label(section, data) {
135
+ const command = section.substring(6);
136
+ const eq = command.indexOf('=');
137
+ if (eq == -1) {
138
+ return ('{label:' + command + '}');
139
+ }
140
+ const text = command.substring(eq + 1);
141
+ const label = command.substring(0, eq);
142
+ return '<span class="squiffy-label-' + label + '">' + this.process(text, data) + '</span>';
143
+ }
144
+ processTextCommand_Rotate(type, section) {
145
+ let options;
146
+ let attribute = '';
147
+ if (section.substring(type.length, type.length + 1) == ' ') {
148
+ const colon = section.indexOf(':');
149
+ if (colon == -1) {
150
+ return '{' + section + '}';
151
+ }
152
+ options = section.substring(colon + 1);
153
+ attribute = section.substring(type.length + 1, colon);
154
+ }
155
+ else {
156
+ options = section.substring(type.length + 1);
157
+ }
158
+ // TODO: Check - previously there was no second parameter here
159
+ const rotation = rotate(options.replace(/"/g, '&quot;').replace(/'/g, '&#39;'), null);
160
+ if (attribute) {
161
+ this.set(attribute, rotation[0]);
162
+ }
163
+ return '<a class="squiffy-link" data-' + type + '="' + rotation[1] + '" data-attribute="' + attribute + '" role="link">' + rotation[0] + '</a>';
164
+ }
165
+ }
@@ -0,0 +1,54 @@
1
+ import { SquiffyEventHandler, SquiffyEventMap } from "./events.js";
2
+ export interface SquiffyInitOptions {
3
+ element: HTMLElement;
4
+ story: Story;
5
+ scroll?: string;
6
+ persist?: boolean;
7
+ onSet?: (attribute: string, value: any) => void;
8
+ }
9
+ export interface SquiffySettings {
10
+ scroll: string;
11
+ persist: boolean;
12
+ onSet: (attribute: string, value: any) => void;
13
+ }
14
+ export interface SquiffyApi {
15
+ restart: () => void;
16
+ get: (attribute: string) => any;
17
+ set: (attribute: string, value: any) => void;
18
+ clickLink: (link: HTMLElement) => boolean;
19
+ update: (story: Story) => void;
20
+ on<E extends keyof SquiffyEventMap>(event: E, handler: SquiffyEventHandler<E>): () => void;
21
+ off<E extends keyof SquiffyEventMap>(event: E, handler: SquiffyEventHandler<E>): void;
22
+ once<E extends keyof SquiffyEventMap>(event: E, handler: SquiffyEventHandler<E>): () => void;
23
+ }
24
+ interface SquiffyJsFunctionApi {
25
+ get: (attribute: string) => any;
26
+ set: (attribute: string, value: any) => void;
27
+ ui: {
28
+ transition: (f: any) => void;
29
+ };
30
+ story: {
31
+ go: (section: string) => void;
32
+ };
33
+ }
34
+ export interface Story {
35
+ js: ((squiffy: SquiffyJsFunctionApi, get: (attribute: string) => any, set: (attribute: string, value: any) => void) => void)[];
36
+ start: string;
37
+ id?: string | null;
38
+ sections: Record<string, Section>;
39
+ }
40
+ export interface Section {
41
+ text?: string;
42
+ clear?: boolean;
43
+ attributes?: string[];
44
+ jsIndex?: number;
45
+ passages?: Record<string, Passage>;
46
+ passageCount?: number;
47
+ }
48
+ export interface Passage {
49
+ text?: string;
50
+ clear?: boolean;
51
+ attributes?: string[];
52
+ jsIndex?: number;
53
+ }
54
+ export {};
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export declare function startsWith(string: string, prefix: string): boolean;
2
+ export declare function rotate(options: string, current: string | null): string[];
package/dist/utils.js ADDED
@@ -0,0 +1,14 @@
1
+ export function startsWith(string, prefix) {
2
+ return string.substring(0, prefix.length) === prefix;
3
+ }
4
+ export function rotate(options, current) {
5
+ const colon = options.indexOf(':');
6
+ if (colon == -1) {
7
+ return [options, current];
8
+ }
9
+ const next = options.substring(0, colon);
10
+ let remaining = options.substring(colon + 1);
11
+ if (current)
12
+ remaining += ':' + current;
13
+ return [next, remaining];
14
+ }