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

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.
@@ -1,4 +1,4 @@
1
- interface SquiffyInitOptions {
1
+ export interface SquiffyInitOptions {
2
2
  element: HTMLElement;
3
3
  story: Story;
4
4
  scroll?: string,
@@ -6,26 +6,45 @@ interface SquiffyInitOptions {
6
6
  onSet?: (attribute: string, value: any) => void,
7
7
  }
8
8
 
9
- interface SquiffySettings {
9
+ export interface SquiffySettings {
10
10
  scroll: string,
11
11
  persist: boolean,
12
12
  onSet: (attribute: string, value: any) => void,
13
13
  }
14
14
 
15
- interface SquiffyApi {
15
+ export interface SquiffyApi {
16
16
  restart: () => void;
17
17
  get: (attribute: string) => any;
18
18
  set: (attribute: string, value: any) => void;
19
+ clickLink: (link: HTMLElement) => boolean;
20
+ update: (story: Story) => void;
19
21
  }
20
22
 
21
- interface Story {
22
- js: (() => void)[];
23
+ // Previous versions of Squiffy had "squiffy", "get" and "set" as globals - we now pass these directly into JS functions.
24
+ // We may tidy up this API at some point, though that would be a breaking change.
25
+ interface SquiffyJsFunctionApi {
26
+ get: (attribute: string) => any;
27
+ set: (attribute: string, value: any) => void;
28
+ ui: {
29
+ transition: (f: any) => void;
30
+ };
31
+ story: {
32
+ go: (section: string) => void;
33
+ };
34
+ }
35
+
36
+ export interface Story {
37
+ js: ((
38
+ squiffy: SquiffyJsFunctionApi,
39
+ get: (attribute: string) => any,
40
+ set: (attribute: string, value: any) => void
41
+ ) => void)[];
23
42
  start: string;
24
43
  id?: string | null;
25
44
  sections: Record<string, Section>;
26
45
  }
27
46
 
28
- interface Section {
47
+ export interface Section {
29
48
  text?: string;
30
49
  clear?: boolean;
31
50
  attributes?: string[],
@@ -34,7 +53,7 @@ interface Section {
34
53
  passageCount?: number;
35
54
  }
36
55
 
37
- interface Passage {
56
+ export interface Passage {
38
57
  text?: string;
39
58
  clear?: boolean;
40
59
  attributes?: string[];
@@ -45,6 +64,7 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
45
64
  let story: Story;
46
65
  let currentSection: Section;
47
66
  let currentSectionElement: HTMLElement;
67
+ let currentBlockOutputElement: HTMLElement;
48
68
  let scrollPosition = 0;
49
69
  let outputElement: HTMLElement;
50
70
  let settings: SquiffySettings;
@@ -73,68 +93,72 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
73
93
  return JSON.parse(result);
74
94
  }
75
95
 
76
- function initLinkHandler() {
77
- function handleLink(link: HTMLElement) {
78
- if (link.classList.contains('disabled')) return;
79
- let passage = link.getAttribute('data-passage');
80
- let section = link.getAttribute('data-section');
81
- const rotateAttr = link.getAttribute('data-rotate');
82
- const sequenceAttr = link.getAttribute('data-sequence');
83
- const rotateOrSequenceAttr = rotateAttr || sequenceAttr;
96
+ function handleLink(link: HTMLElement): boolean {
97
+ const outputSection = link.closest('.squiffy-output-section');
98
+ if (outputSection !== currentSectionElement) return false;
99
+
100
+ if (link.classList.contains('disabled')) return false;
101
+
102
+ let passage = link.getAttribute('data-passage');
103
+ let section = link.getAttribute('data-section');
104
+ const rotateAttr = link.getAttribute('data-rotate');
105
+ const sequenceAttr = link.getAttribute('data-sequence');
106
+ const rotateOrSequenceAttr = rotateAttr || sequenceAttr;
107
+ if (passage) {
108
+ disableLink(link);
109
+ set('_turncount', get('_turncount') + 1);
110
+ passage = processLink(passage);
84
111
  if (passage) {
85
- disableLink(link);
86
- set('_turncount', get('_turncount') + 1);
87
- passage = processLink(passage);
88
- if (passage) {
89
- currentSectionElement?.appendChild(document.createElement('hr'));
90
- showPassage(passage);
91
- }
92
- const turnPassage = '@' + get('_turncount');
93
- if (currentSection.passages) {
94
- if (turnPassage in currentSection.passages) {
95
- showPassage(turnPassage);
96
- }
97
- if ('@last' in currentSection.passages && get('_turncount') >= (currentSection.passageCount || 0)) {
98
- showPassage('@last');
99
- }
100
- }
112
+ newBlockOutputElement();
113
+ showPassage(passage);
101
114
  }
102
- else if (section) {
103
- currentSectionElement?.appendChild(document.createElement('hr'));
104
- disableLink(link);
105
- section = processLink(section);
106
- if (section) {
107
- go(section);
115
+ const turnPassage = '@' + get('_turncount');
116
+ if (currentSection.passages) {
117
+ if (turnPassage in currentSection.passages) {
118
+ showPassage(turnPassage);
108
119
  }
109
- }
110
- else if (rotateOrSequenceAttr) {
111
- const result = rotate(rotateOrSequenceAttr, rotateAttr ? link.innerText : '');
112
- link.innerHTML = result[0]!.replace(/&quot;/g, '"').replace(/&#39;/g, '\'');
113
- const dataAttribute = rotateAttr ? 'data-rotate' : 'data-sequence';
114
- link.setAttribute(dataAttribute, result[1] || '');
115
- if (!result[1]) {
116
- disableLink(link);
117
- }
118
- const attribute = link.getAttribute('data-attribute');
119
- if (attribute) {
120
- set(attribute, result[0]);
120
+ if ('@last' in currentSection.passages && get('_turncount') >= (currentSection.passageCount || 0)) {
121
+ showPassage('@last');
121
122
  }
122
- save();
123
123
  }
124
+
125
+ return true;
124
126
  }
125
-
126
- function handleClick(event: Event) {
127
- const target = event.target as HTMLElement;
128
- if (target.classList.contains('squiffy-link')) {
129
- handleLink(target);
127
+
128
+ if (section) {
129
+ section = processLink(section);
130
+ if (section) {
131
+ go(section);
130
132
  }
133
+
134
+ return true;
135
+ }
136
+
137
+ if (rotateOrSequenceAttr) {
138
+ const result = rotate(rotateOrSequenceAttr, rotateAttr ? link.innerText : '');
139
+ link.innerHTML = result[0]!.replace(/&quot;/g, '"').replace(/&#39;/g, '\'');
140
+ const dataAttribute = rotateAttr ? 'data-rotate' : 'data-sequence';
141
+ link.setAttribute(dataAttribute, result[1] || '');
142
+ if (!result[1]) {
143
+ disableLink(link);
144
+ }
145
+ const attribute = link.getAttribute('data-attribute');
146
+ if (attribute) {
147
+ set(attribute, result[0]);
148
+ }
149
+ save();
150
+
151
+ return true;
152
+ }
153
+
154
+ return false;
155
+ }
156
+
157
+ function handleClick(event: Event) {
158
+ const target = event.target as HTMLElement;
159
+ if (target.classList.contains('squiffy-link')) {
160
+ handleLink(target);
131
161
  }
132
-
133
- document.addEventListener('click', handleClick);
134
- document.addEventListener('keypress', function (event) {
135
- if (event.key !== "Enter") return;
136
- handleClick(event);
137
- });
138
162
  }
139
163
 
140
164
  function disableLink(link: Element) {
@@ -142,10 +166,6 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
142
166
  link.setAttribute('tabindex', '-1');
143
167
  }
144
168
 
145
- function disableLinks(links: NodeListOf<Element>) {
146
- links.forEach(disableLink);
147
- }
148
-
149
169
  function begin() {
150
170
  if (!load()) {
151
171
  go(story.start);
@@ -260,23 +280,23 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
260
280
  labelElement.classList.add('fade-out');
261
281
  }
262
282
 
263
- function go(section: string) {
283
+ function go(sectionName: string) {
264
284
  set('_transition', null);
265
- newSection();
266
- currentSection = story.sections[section];
285
+ newSection(sectionName);
286
+ currentSection = story.sections[sectionName];
267
287
  if (!currentSection) return;
268
- set('_section', section);
269
- setSeen(section);
288
+ set('_section', sectionName);
289
+ setSeen(sectionName);
270
290
  const master = story.sections[''];
271
291
  if (master) {
272
292
  run(master);
273
- ui.write(master.text || '');
293
+ ui.write(master.text || '', "[[]]");
274
294
  }
275
295
  run(currentSection);
276
296
  // The JS might have changed which section we're in
277
- if (get('_section') == section) {
297
+ if (get('_section') == sectionName) {
278
298
  set('_turncount', 0);
279
- ui.write(currentSection.text || '');
299
+ ui.write(currentSection.text || '', `[[${sectionName}]]`);
280
300
  save();
281
301
  }
282
302
  }
@@ -289,7 +309,17 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
289
309
  processAttributes(section.attributes.map(line => line.replace(/^random\s*:\s*(\w+)\s*=\s*(.+)/i, (line, attr, options) => (options = options.split("|")) ? attr + " = " + options[Math.floor(Math.random() * options.length)] : line)));
290
310
  }
291
311
  if (section.jsIndex !== undefined) {
292
- story.js[section.jsIndex]();
312
+ const squiffy = {
313
+ get: get,
314
+ set: set,
315
+ ui: {
316
+ transition: ui.transition,
317
+ },
318
+ story: {
319
+ go: go,
320
+ },
321
+ };
322
+ story.js[section.jsIndex](squiffy, get, set);
293
323
  }
294
324
  }
295
325
 
@@ -297,22 +327,24 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
297
327
  let passage = currentSection.passages && currentSection.passages[passageName];
298
328
  const masterSection = story.sections[''];
299
329
  if (!passage && masterSection && masterSection.passages) passage = masterSection.passages[passageName];
300
- if (!passage) return;
330
+ if (!passage) {
331
+ throw `No passage named ${passageName} in the current section or master section`;
332
+ }
301
333
  setSeen(passageName);
302
334
  if (masterSection && masterSection.passages) {
303
335
  const masterPassage = masterSection.passages[''];
304
336
  if (masterPassage) {
305
337
  run(masterPassage);
306
- ui.write(masterPassage.text || '');
338
+ ui.write(masterPassage.text || '', `[[]][]`);
307
339
  }
308
340
  }
309
341
  const master = currentSection.passages && currentSection.passages[''];
310
342
  if (master) {
311
343
  run(master);
312
- ui.write(master.text || '');
344
+ ui.write(master.text || '', `[[${get("_section")}]][]`);
313
345
  }
314
346
  run(passage);
315
- ui.write(passage.text || '');
347
+ ui.write(passage.text || '', `[[${get("_section")}]][${passageName}]`);
316
348
  save();
317
349
  }
318
350
 
@@ -356,9 +388,10 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
356
388
  const output = get('_output');
357
389
  if (!output) return false;
358
390
  outputElement.innerHTML = output;
359
- const element = document.getElementById(get('_output-section'));
360
- if (!element) return false;
361
- currentSectionElement = element;
391
+
392
+ currentSectionElement = outputElement.querySelector('.squiffy-output-section:last-child');
393
+ currentBlockOutputElement = outputElement.querySelector('.squiffy-output-block:last-child');
394
+
362
395
  currentSection = story.sections[get('_section')];
363
396
  const transition = get('_transition');
364
397
  if (transition) {
@@ -381,10 +414,15 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
381
414
  if (!seenSections) return false;
382
415
  return (seenSections.indexOf(sectionName) > -1);
383
416
  }
417
+
418
+ function newBlockOutputElement() {
419
+ currentBlockOutputElement = document.createElement('div');
420
+ currentBlockOutputElement.classList.add('squiffy-output-block');
421
+ currentSectionElement?.appendChild(currentBlockOutputElement);
422
+ }
384
423
 
385
- function newSection() {
424
+ function newSection(sectionName: string | null) {
386
425
  if (currentSectionElement) {
387
- disableLinks(currentSectionElement.querySelectorAll('.squiffy-link'));
388
426
  currentSectionElement.querySelectorAll('input').forEach(el => {
389
427
  const attribute = el.getAttribute('data-attribute') || el.id;
390
428
  if (attribute) set(attribute, el.value);
@@ -409,26 +447,32 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
409
447
  const id = 'squiffy-section-' + sectionCount;
410
448
 
411
449
  currentSectionElement = document.createElement('div');
450
+ currentSectionElement.classList.add('squiffy-output-section');
412
451
  currentSectionElement.id = id;
452
+ if (sectionName) {
453
+ currentSectionElement.setAttribute('data-section', `${sectionName}`);
454
+ }
413
455
  outputElement.appendChild(currentSectionElement);
414
-
415
- set('_output-section', id);
456
+ newBlockOutputElement();
416
457
  }
417
458
 
418
459
  const ui = {
419
- write: (text: string) => {
420
- if (!currentSectionElement) return;
460
+ write: (text: string, source: string) => {
461
+ if (!currentBlockOutputElement) return;
421
462
  scrollPosition = outputElement.scrollHeight;
422
463
 
423
464
  const div = document.createElement('div');
424
- currentSectionElement.appendChild(div);
465
+ if (source) {
466
+ div.setAttribute('data-source', source);
467
+ }
468
+ currentBlockOutputElement.appendChild(div);
425
469
  div.innerHTML = ui.processText(text);
426
470
 
427
471
  ui.scrollToEnd();
428
472
  },
429
473
  clearScreen: () => {
430
474
  outputElement.innerHTML = '';
431
- newSection();
475
+ newSection(null);
432
476
  },
433
477
  scrollToEnd: () => {
434
478
  if (settings.scroll === 'element') {
@@ -641,6 +685,82 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
641
685
  return [next, remaining];
642
686
  }
643
687
 
688
+ function safeQuerySelector(name: string) {
689
+ return name.replace(/'/g, "\\'");
690
+ }
691
+
692
+ function getSectionContent(section: string) {
693
+ return outputElement.querySelectorAll(`[data-source='[[${safeQuerySelector(section)}]]']`);
694
+ }
695
+
696
+ function getPassageContent(section: string, passage: string) {
697
+ return outputElement.querySelectorAll(`[data-source='[[${safeQuerySelector(section)}]][${safeQuerySelector(passage)}]']`);
698
+ }
699
+
700
+ function updateElementTextPreservingDisabledPassageLinks(element: Element, text: string) {
701
+ // Record which passage links are disabled
702
+ const disabledPassages = Array.from(element
703
+ .querySelectorAll("a.link-passage.disabled"))
704
+ .map((el: HTMLElement) => el.getAttribute("data-passage"));
705
+
706
+ element.innerHTML = text;
707
+
708
+ // Re-disable links that were disabled before the update
709
+ for (const passage of disabledPassages) {
710
+ const link = element.querySelector(`a.link-passage[data-passage="${passage}"]`);
711
+ if (link) disableLink(link);
712
+ }
713
+ }
714
+
715
+ function update(newStory: Story) {
716
+ for (const existingSection of Object.keys(story.sections)) {
717
+ const elements = getSectionContent(existingSection);
718
+ if (elements.length) {
719
+ const newSection = newStory.sections[existingSection];
720
+ if (!newSection) {
721
+ // section has been deleted
722
+ for (const element of elements) {
723
+ const parentOutputSection = element.closest('.squiffy-output-section');
724
+ parentOutputSection.remove();
725
+ }
726
+ }
727
+ else if (newSection.text && newSection.text != story.sections[existingSection].text) {
728
+ // section has been updated
729
+ for (const element of elements) {
730
+ updateElementTextPreservingDisabledPassageLinks(element, newSection.text);
731
+ }
732
+ }
733
+ }
734
+
735
+ if (!story.sections[existingSection].passages) continue;
736
+
737
+ for (const existingPassage of Object.keys(story.sections[existingSection].passages)) {
738
+ const elements = getPassageContent(existingSection, existingPassage);
739
+ if (!elements.length) continue;
740
+
741
+ const newPassage = newStory.sections[existingSection]?.passages && newStory.sections[existingSection]?.passages[existingPassage];
742
+ if (!newPassage) {
743
+ // passage has been deleted
744
+ for (const element of elements) {
745
+ const parentOutputBlock = element.closest('.squiffy-output-block');
746
+ parentOutputBlock.remove();
747
+ }
748
+ }
749
+ else if (newPassage.text && newPassage.text != story.sections[existingSection].passages[existingPassage].text) {
750
+ // passage has been updated
751
+ for (const element of elements) {
752
+ updateElementTextPreservingDisabledPassageLinks(element, newPassage.text);
753
+ }
754
+ }
755
+ }
756
+ }
757
+
758
+ story = newStory;
759
+ currentSectionElement = outputElement.querySelector('.squiffy-output-section:last-child');
760
+ const sectionName = currentSectionElement.getAttribute('data-section');
761
+ currentSection = story.sections[sectionName];
762
+ }
763
+
644
764
  outputElement = options.element;
645
765
  story = options.story;
646
766
 
@@ -659,12 +779,19 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
659
779
  outputElement.style.overflowY = 'auto';
660
780
  }
661
781
 
662
- initLinkHandler();
782
+ outputElement.addEventListener('click', handleClick);
783
+ outputElement.addEventListener('keypress', function (event) {
784
+ if (event.key !== "Enter") return;
785
+ handleClick(event);
786
+ });
787
+
663
788
  begin();
664
789
 
665
790
  return {
666
791
  restart: restart,
667
792
  get: get,
668
793
  set: set,
794
+ clickLink: handleLink,
795
+ update: update,
669
796
  };
670
797
  };
package/tsconfig.json CHANGED
@@ -7,7 +7,11 @@
7
7
  "noFallthroughCasesInSwitch": true,
8
8
  "skipLibCheck": true,
9
9
  "outDir": "dist",
10
+ "declaration": true,
11
+ "module": "NodeNext",
12
+ "moduleResolution": "nodenext",
13
+ "allowSyntheticDefaultImports": true
10
14
  },
11
- "include": ["src/squiffy.runtime.ts"]
15
+ "include": ["src/*.ts"]
12
16
  }
13
17